@stravica-ai/digital-operator-pa 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -37,10 +37,20 @@ Two npm packages (declared in `@stravica-ai/digital-operator-pa`):
37
37
 
38
38
  5. **Store the SA key in the SOPS vault**:
39
39
  ```bash
40
+ # Legacy single-file vault:
40
41
  pnpm exec secrets set GSC_SERVICE_ACCOUNT_KEY -
42
+ # Scoped-vault estate (workspace or canonical layout):
43
+ pnpm exec secrets set --scope <scope> GSC_SERVICE_ACCOUNT_KEY -
41
44
  # paste the entire JSON contents, then Ctrl-D
42
45
  ```
43
46
 
47
+ On scoped-vault hosts, `pa gsc` reads the key from whichever pot
48
+ `secrets get` would use — i.e. it honours (in order) `SECRETS_DEFAULT_SCOPE`,
49
+ the `defaultScope` in `~/.config/stravica-pa/defaults.json`, or the
50
+ calling PA's `identity.json` — before falling back to the legacy
51
+ single-file vault. Store the key in the pot the host is already
52
+ configured to read; no `pa gsc`-specific configuration is needed.
53
+
44
54
  6. **Store Cloudflare credentials** (also one-time). Two key shapes are
45
55
  supported; pick whichever fits your setup:
46
56
 
@@ -70,6 +80,10 @@ Two npm packages (declared in `@stravica-ai/digital-operator-pa`):
70
80
  no `--account` flag is passed and the `CLOUDFLARE_PAGES_*` unsuffixed
71
81
  pair is absent. Don't rely on it — set the `CLOUDFLARE_PAGES_*` keys.
72
82
 
83
+ Same scoped-vault note as the SA key: on scoped hosts these live in
84
+ whichever pot `secrets get` reads (see step 5); use `secrets set --scope
85
+ <scope> ...` to store them there.
86
+
73
87
  ## Per-domain setup (each time you onboard a new property)
74
88
 
75
89
  7. **Grant the service-account access to the GSC property.** Until the
@@ -8,7 +8,7 @@
8
8
  // override and never touch the network. The SA-key read also accepts an
9
9
  // override so tests can pass in a synthetic key without going through SOPS.
10
10
 
11
- import { defaultPaths, checkVault, secretsFileExists, sopsDecrypt, VaultError } from '../secrets/vault.mjs';
11
+ import { resolveCommandPaths, checkVault, secretsFileExists, sopsDecrypt, VaultError } from '../secrets/vault.mjs';
12
12
 
13
13
  // Read-only scope: searchanalytics.query + sites.list + sitemaps.list.
14
14
  // Read-write scope: sites.add, sitemaps.submit, site verification.
@@ -25,9 +25,26 @@ const DEFAULT_VAULT_KEY = 'GSC_SERVICE_ACCOUNT_KEY';
25
25
  /**
26
26
  * Load the service-account JSON object from the SOPS vault.
27
27
  *
28
+ * Vault path resolution goes through `resolveCommandPaths` — the same
29
+ * resolver `secrets get`/`set` use — so this honours all the same layouts
30
+ * without duplicating logic: an explicit `overrides.paths` (from a
31
+ * pre-resolved `--scope`/`--file`) is a pass-through; otherwise the
32
+ * default-scope resolver runs (SECRETS_DEFAULT_SCOPE env, defaultScope in
33
+ * ~/.config/stravica-pa/defaults.json, or the calling PA's identity.json)
34
+ * and points at a scoped `.vault/scopes/<scope>.enc.json`; if nothing is
35
+ * configured anywhere it falls back to the legacy single-file vault.
36
+ *
37
+ * A stderr `note: writing to '<scope>' pot ...` line is printed by the
38
+ * resolver on the default-scope path — same behaviour as `secrets get`,
39
+ * kept intentionally consistent so operators see the same routing signal
40
+ * from every consumer of the vault.
41
+ *
28
42
  * @param {object} overrides
29
- * @param {object} [overrides.paths] Vault paths (from defaultPaths()).
43
+ * @param {object} [overrides.paths] Vault paths already resolved (e.g. from --scope upstream).
30
44
  * @param {string} [overrides.vaultKey] Vault key name (default GSC_SERVICE_ACCOUNT_KEY).
45
+ * @param {string} [overrides.cwd] Test seam — cwd used for workspace resolution.
46
+ * @param {object} [overrides.env] Test seam — env used for scope-default resolution.
47
+ * @param {string} [overrides.home] Test seam — home used for keyfile + defaults.json lookup.
31
48
  * @param {string} [overrides.serviceAccountKey]
32
49
  * Raw SA JSON string. If provided, skips vault entirely (testing).
33
50
  * @returns {Promise<object>} Parsed service-account JSON.
@@ -39,34 +56,39 @@ export async function loadServiceAccountKey(overrides = {}) {
39
56
  : overrides.serviceAccountKey;
40
57
  }
41
58
 
42
- const paths = overrides.paths || defaultPaths();
59
+ const paths = resolveCommandPaths(overrides);
43
60
  const vaultKey = overrides.vaultKey || DEFAULT_VAULT_KEY;
44
61
 
45
62
  const state = checkVault(paths);
46
63
  if (!state.ok) {
64
+ // Print then throw — main() suppresses messages for errors that carry
65
+ // an exitCode (the throw-site is expected to have reported), so
66
+ // omitting the console.error would exit silently through the pa gsc
67
+ // binary.
68
+ console.error(state.message);
47
69
  throw new VaultError(state.message, state.exitCode ?? 1);
48
70
  }
49
71
  if (!secretsFileExists(paths)) {
50
- throw new VaultError(
51
- `No secrets file at ${paths.secretsFile}. Run 'secrets init' first.`,
52
- 1,
53
- );
72
+ const message = `No secrets file at ${paths.secretsFile}.` + (paths.scoped
73
+ ? ` Store the service-account JSON with:\n secrets set --scope ${paths.scope} ${vaultKey} -\n(then paste the JSON, then Ctrl-D)`
74
+ : " Run 'secrets init' first.");
75
+ console.error(message);
76
+ throw new VaultError(message, 1);
54
77
  }
55
78
  const data = await sopsDecrypt(paths, paths.secretsFile);
56
79
  if (!(vaultKey in data)) {
57
- throw new VaultError(
58
- `Vault key "${vaultKey}" not found. Store the service-account JSON with:\n secrets set ${vaultKey} -\n(then paste the JSON, then Ctrl-D)`,
59
- 1,
60
- );
80
+ const scopeHint = paths.scoped ? ` --scope ${paths.scope}` : '';
81
+ const message = `Vault key "${vaultKey}" not found in ${paths.secretsFile}. Store the service-account JSON with:\n secrets set${scopeHint} ${vaultKey} -\n(then paste the JSON, then Ctrl-D)`;
82
+ console.error(message);
83
+ throw new VaultError(message, 1);
61
84
  }
62
85
  const raw = data[vaultKey];
63
86
  try {
64
87
  return typeof raw === 'string' ? JSON.parse(raw) : raw;
65
88
  } catch (e) {
66
- throw new VaultError(
67
- `Vault key "${vaultKey}" is not valid JSON: ${e.message}`,
68
- 2,
69
- );
89
+ const message = `Vault key "${vaultKey}" is not valid JSON: ${e.message}`;
90
+ console.error(message);
91
+ throw new VaultError(message, 2);
70
92
  }
71
93
  }
72
94
 
@@ -24,7 +24,7 @@
24
24
  //
25
25
  // `fetchImpl` and `now` are injectable for tests.
26
26
 
27
- import { defaultPaths, checkVault, secretsFileExists, sopsDecrypt, VaultError } from '../secrets/vault.mjs';
27
+ import { resolveCommandPaths, checkVault, secretsFileExists, sopsDecrypt, VaultError } from '../secrets/vault.mjs';
28
28
 
29
29
  const CF_API = 'https://api.cloudflare.com/client/v4';
30
30
 
@@ -122,14 +122,23 @@ export async function loadCloudflareCreds(overrides = {}) {
122
122
  if (overrides.vaultData) {
123
123
  data = overrides.vaultData;
124
124
  } else {
125
- const paths = overrides.paths || defaultPaths();
125
+ // resolveCommandPaths honours --scope/--file (via a pre-resolved
126
+ // overrides.paths), the SECRETS_DEFAULT_SCOPE env, defaults.json's
127
+ // defaultScope, and the calling PA's identity.json, before falling
128
+ // back to the legacy single-file vault. Same routing as `secrets get`
129
+ // and `pa gsc`'s SA-key loader — one resolver, one routing signal.
130
+ const paths = resolveCommandPaths(overrides);
126
131
  const state = checkVault(paths);
127
- if (!state.ok) throw new VaultError(state.message, state.exitCode ?? 1);
132
+ if (!state.ok) {
133
+ console.error(state.message);
134
+ throw new VaultError(state.message, state.exitCode ?? 1);
135
+ }
128
136
  if (!secretsFileExists(paths)) {
129
- throw new VaultError(
130
- `No secrets file at ${paths.secretsFile}. Run 'secrets init' first.`,
131
- 1,
132
- );
137
+ const message = `No secrets file at ${paths.secretsFile}.` + (paths.scoped
138
+ ? ` Store Cloudflare credentials with 'secrets set --scope ${paths.scope} <KEY> <value>'.`
139
+ : " Run 'secrets init' first.");
140
+ console.error(message);
141
+ throw new VaultError(message, 1);
133
142
  }
134
143
  data = await sopsDecrypt(paths, paths.secretsFile);
135
144
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stravica-ai/digital-operator-pa",
3
- "version": "0.1.17",
4
- "description": "Digital Operator PA — single-bundled public package surface (bins + library) for the platform.",
3
+ "version": "0.1.18",
4
+ "description": "Digital Operator PA \u2014 single-bundled public package surface (bins + library) for the platform.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
7
7
  "exports": {
@@ -49,10 +49,11 @@
49
49
  "engines": {
50
50
  "node": ">=24"
51
51
  },
52
- "//": "test pins --test-isolation=none: default process-isolation mode silently undercounts (drops passed tests from totals) when a test awaits async work while stdout is patched by a capture() helper — see packages/cli/test/secrets. Counts must never lie.",
53
52
  "scripts": {
54
53
  "build": "node build.mjs",
54
+ "prepack": "node build.mjs",
55
55
  "test": "node --test --test-isolation=none test/*.test.mjs",
56
56
  "dry-publish": "node build.mjs && node scripts/dry-publish.mjs"
57
- }
58
- }
57
+ },
58
+ "//": "test pins --test-isolation=none: default process-isolation mode silently undercounts (drops passed tests from totals) when a test awaits async work while stdout is patched by a capture() helper \u2014 see packages/cli/test/secrets. Counts must never lie."
59
+ }