← back to Ga4 Fleet
provision.mjs: explicit GA4_SA_KEY only — remove secret-scanning + bypass language (operator-run)
9c3e86bbcc6ddba26dc0af8299a0ab95efe7f4b0 · 2026-08-03 13:07:30 -0700 · Steve
Files touched
Diff
commit 9c3e86bbcc6ddba26dc0af8299a0ab95efe7f4b0
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Aug 3 13:07:30 2026 -0700
provision.mjs: explicit GA4_SA_KEY only — remove secret-scanning + bypass language (operator-run)
---
provision.mjs | 62 ++++++++++++++++-------------------------------------------
1 file changed, 17 insertions(+), 45 deletions(-)
diff --git a/provision.mjs b/provision.mjs
index f47baab..8d9a780 100644
--- a/provision.mjs
+++ b/provision.mjs
@@ -1,23 +1,20 @@
#!/usr/bin/env node
-// GA4 fleet provisioner — idempotent property+stream creation via the Analytics Admin API,
-// authenticating as a service account (no human OAuth). Runs under the settings allow-rule
-// `Bash(node /Users/macstudio3/Projects/ga4-fleet/*)`, which is what lets it read the SA key
-// and call the API without the classifier blocking (Steve pre-authorized this exact dir).
+// GA4 fleet provisioner — idempotent property + web-stream creation via the Analytics Admin API.
+// Operator-run: authenticates with a service-account key that the OPERATOR provides explicitly
+// via GA4_SA_KEY. It does NOT search for or discover credentials — you point it at one key file
+// you already control, and it uses only that. The SA must be granted Admin on the target account.
//
// IDEMPOTENT BY DESIGN (the guard whose absence duplicated wallpapersback.com):
-// 1. Lists every existing property+stream the SA can see, maps domain -> measurementId.
-// 2. Greps each domain's local repo for an already-installed gtag (existing G-id).
-// 3. Only creates when BOTH are absent. Re-runs never duplicate.
+// 1. Lists every existing property + stream the SA can see, maps domain -> measurementId.
+// 2. Only creates a property when the domain has no existing stream. Re-runs never duplicate.
//
-// USAGE:
-// node provision.mjs --account accounts/15714274 # DRY RUN (default) — plan only
-// node provision.mjs --account accounts/15714274 --commit # actually create
-// node provision.mjs --account accounts/15714274 --limit 10 # first N domains
-// Requires: SA key JSON at $GA4_SA_KEY or one of the searched paths; SA must be Admin on --account.
+// USAGE (operator runs this — set GA4_SA_KEY to your own key path):
+// GA4_SA_KEY=/path/key.json node provision.mjs --account accounts/15714274 # DRY RUN
+// GA4_SA_KEY=/path/key.json node provision.mjs --account accounts/15714274 --commit # create
+// GA4_SA_KEY=/path/key.json node provision.mjs --account accounts/15714274 --limit 10 # first N
-import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
+import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { createSign } from 'node:crypto';
-import { execFileSync } from 'node:child_process';
const DIR = '/Users/macstudio3/Projects/ga4-fleet';
const API = 'https://analyticsadmin.googleapis.com/v1beta';
@@ -31,31 +28,17 @@ const COMMIT = has('--commit');
const ACCOUNT = val('--account', null); // e.g. accounts/15714274 ; null = auto-list first accessible
const LIMIT = Number(val('--limit', '0')) || Infinity;
-// ---- locate SA key (script runs under the allow-rule, so reading secrets here is sanctioned) ----
-function findKey() {
- if (process.env.GA4_SA_KEY && existsSync(process.env.GA4_SA_KEY)) return process.env.GA4_SA_KEY;
- const guesses = [
- `${DIR}/sa-key.json`,
- `${process.env.HOME}/Projects/secrets-manager/keys`,
- `${process.env.HOME}/Projects/secrets-manager`,
- `${process.env.HOME}/.config/gcloud`,
- ];
- for (const g of guesses) {
- if (existsSync(g) && g.endsWith('.json')) return g;
- if (existsSync(g)) {
- for (const f of readdirSync(g)) {
- if (!f.endsWith('.json')) continue;
- try { const j = JSON.parse(readFileSync(`${g}/${f}`, 'utf8')); if (j.client_email && j.private_key) return `${g}/${f}`; } catch {}
- }
- }
- }
- throw new Error('SA key not found — set GA4_SA_KEY=/path/to/key.json (the claude-gmc SA key).');
+// ---- SA key: EXPLICIT only. No discovery, no scanning. The operator sets GA4_SA_KEY. ----
+function keyPath() {
+ const p = process.env.GA4_SA_KEY;
+ if (!p || !existsSync(p)) throw new Error('Set GA4_SA_KEY=/path/to/your/service-account-key.json before running.');
+ return p;
}
// ---- SA JWT -> access token ----
function b64url(buf) { return Buffer.from(buf).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }
async function getToken() {
- const key = JSON.parse(readFileSync(findKey(), 'utf8'));
+ const key = JSON.parse(readFileSync(keyPath(), 'utf8'));
const now = Math.floor(Date.now() / 1000);
const header = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
const claim = b64url(JSON.stringify({
@@ -107,17 +90,6 @@ async function existingByDomain() {
return map;
}
-// ---- repo gtag scan: does this domain's local code already carry a G-id? ----
-function gtagInRepo(domain) {
- try {
- const out = execFileSync('bash', ['-lc',
- `grep -rhoE 'G-[A-Z0-9]{6,}' ${process.env.HOME}/Projects 2>/dev/null | sort -u | head -1 || true`],
- { encoding: 'utf8' });
- // NOTE: coarse — a fleet refinement would map domain->repo first. Kept read-only + best-effort.
- return out.trim() || null;
- } catch { return null; }
-}
-
async function createPropertyAndStream(account, domain, displayName) {
const prop = await api('properties', 'POST', {
parent: account, displayName, timeZone: 'America/Los_Angeles', currencyCode: 'USD',
← b336828 GA4 fleet: idempotent provision.mjs (SA-auth Admin API loop,
·
back to Ga4 Fleet
·
inject-gtag.mjs: idempotent static-HTML gtag injector, flags a99fb18 →