[object Object]

← 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 →