@stravica-ai/digital-operator-pa 0.1.32 → 0.1.33

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.
@@ -52,34 +52,34 @@ Two npm packages (declared in `@stravica-ai/digital-operator-pa`):
52
52
  single-file vault. Store the key in the pot the host is already
53
53
  configured to read; no `pa gsc`-specific configuration is needed.
54
54
 
55
- 6. **Store Cloudflare credentials** (also one-time). Two key shapes are
56
- supported; pick whichever fits your setup:
55
+ 6. **Store Cloudflare credentials** (also one-time). Each CF account is
56
+ addressed by a suffixed vault-key pair. `--account <name>` is required
57
+ on every invocation — the unsuffixed bare pair that used to be a
58
+ single-account default was dropped in the 2026-09-01 rename
59
+ (`w-2026-08-31-dave-010`).
57
60
 
58
- **Single-account setup** — one CF account, no `--account` flag needed:
59
- ```bash
60
- pnpm exec secrets set CLOUDFLARE_PAGES_API_KEY <token>
61
- pnpm exec secrets set CLOUDFLARE_PAGES_ACCOUNT_ID <account-id>
62
- ```
61
+ Pick the surface segment that matches how the token is actually
62
+ scoped in Cloudflare:
63
63
 
64
- **Multi-account setup** — multiple CF accounts, each addressed by name:
65
64
  ```bash
66
- # Stravica account → addressed as --account stravica
67
- pnpm exec secrets set CLOUDFLARE_PAGES_API_KEY_STRAVICA <token>
68
- pnpm exec secrets set CLOUDFLARE_PAGES_ACCOUNT_ID_STRAVICA <account-id>
65
+ # Stravica account (Workers Scripts + D1) → --account stravica
66
+ pnpm exec secrets set CLOUDFLARE_WORKERS_API_TOKEN_STRAVICA <token>
67
+ pnpm exec secrets set CLOUDFLARE_WORKERS_ACCOUNT_ID_STRAVICA <account-id>
69
68
 
70
- # Clive account → addressed as --account clive
71
- pnpm exec secrets set CLOUDFLARE_PAGES_API_KEY_CLIVE <token>
69
+ # Clive account (Pages + zone purge) → --account clive
70
+ pnpm exec secrets set CLOUDFLARE_PAGES_API_TOKEN_CLIVE <token>
72
71
  pnpm exec secrets set CLOUDFLARE_PAGES_ACCOUNT_ID_CLIVE <account-id>
73
72
  ```
74
73
 
75
- The CF API token needs `Zone:DNS:Edit` and `Zone:Zone:Read` for any
76
- zones the tool will operate on. Account-level "Write to all resources"
77
- tokens also work.
74
+ The CF API token needs the permissions its surface implies (for DNS
75
+ planting: `Zone:DNS:Edit` and `Zone:Zone:Read` on the target zones;
76
+ account-level "Write to all resources" tokens also work).
78
77
 
79
- For back-compat, the legacy `CLOUDFLARE_API_TOKEN` /
80
- `CLOUDFLARE_ACCOUNT_ID` pair is read as a last-resort fallback when
81
- no `--account` flag is passed and the `CLOUDFLARE_PAGES_*` unsuffixed
82
- pair is absent. Don't rely on it — set the `CLOUDFLARE_PAGES_*` keys.
78
+ For back-compat, the deprecated `CLOUDFLARE_PAGES_API_KEY_<NAME>` /
79
+ `CLOUDFLARE_PAGES_ACCOUNT_ID_<NAME>` pair is still accepted, and a
80
+ hit emits a stderr deprecation warning naming the current pair. Rename
81
+ the vault entries at your convenience — the deprecated pair will be
82
+ removed in a follow-up.
83
83
 
84
84
  Same scoped-vault note as the SA key: on scoped hosts these live in
85
85
  whichever pot `secrets get` reads (see step 5); use `secrets set --scope
@@ -134,26 +134,38 @@ pnpm exec pa gsc list-properties
134
134
 
135
135
  ## Multi-account selection
136
136
 
137
- When `--account <name>` is passed to `verify-via-cf-dns`, the tool resolves
138
- the Cloudflare credentials by upper-casing the name and looking up the
139
- corresponding suffixed vault pair. For example:
137
+ `--account <name>` is required on every invocation. The tool upper-cases
138
+ the name and tries the candidate vault-key pairs below in order — the
139
+ first fully-present pair wins. Storing multiple pairs for the same
140
+ account is a supported migration state; the tool always picks the
141
+ highest-priority pair that resolves.
140
142
 
141
143
  ```
142
- --account stravica -> CLOUDFLARE_PAGES_API_KEY_STRAVICA
143
- CLOUDFLARE_PAGES_ACCOUNT_ID_STRAVICA
144
- --account clive -> CLOUDFLARE_PAGES_API_KEY_CLIVE
145
- CLOUDFLARE_PAGES_ACCOUNT_ID_CLIVE
144
+ 1. CLOUDFLARE_WORKERS_API_TOKEN_<NAME>
145
+ CLOUDFLARE_WORKERS_ACCOUNT_ID_<NAME>
146
+ Workers surface — Workers Scripts, D1, KV. Used by --account stravica.
147
+
148
+ 2. CLOUDFLARE_PAGES_API_TOKEN_<NAME>
149
+ CLOUDFLARE_PAGES_ACCOUNT_ID_<NAME>
150
+ Pages surface — Pages + zone purge. Used by --account clive.
151
+
152
+ 3. CLOUDFLARE_PAGES_API_KEY_<NAME>
153
+ CLOUDFLARE_PAGES_ACCOUNT_ID_<NAME>
154
+ Deprecated pre-2026-09-01 naming. Accepted with a stderr warning;
155
+ will be removed in a follow-up. Rename the vault entries at your
156
+ convenience.
146
157
  ```
147
158
 
148
- If `--account` is passed but the suffixed pair isn't set, the command
149
- fails fast with a message telling you which key is missing. It does
150
- **not** silently fall back to the unsuffixed pair — that would defeat
151
- the safety the flag exists to provide.
159
+ If `--account` is passed but none of the candidate pairs are fully set,
160
+ the command fails fast with a message naming the primary (WORKERS) pair
161
+ you're expected to store. It never silently falls back to any pair not
162
+ addressed by the suffix — that would defeat the safety the flag exists
163
+ to provide.
152
164
 
153
- Without `--account`, resolution order is:
154
- 1. `CLOUDFLARE_PAGES_API_KEY` + `CLOUDFLARE_PAGES_ACCOUNT_ID`
155
- 2. Legacy `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` (back-compat
156
- only; do not rely on this).
165
+ The unsuffixed bare pair (`CLOUDFLARE_PAGES_API_KEY` /
166
+ `CLOUDFLARE_PAGES_ACCOUNT_ID`) and the very-legacy
167
+ `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` pair are no longer
168
+ read — `--account <name>` is mandatory.
157
169
 
158
170
  The account name accepts letters and digits only — anything else is a
159
171
  usage error.
@@ -98,8 +98,11 @@ Edit `src/staging-alias/sites.mjs`. Add one entry:
98
98
  },
99
99
  ```
100
100
 
101
- …and store the vault keys: `CLOUDFLARE_PAGES_API_KEY_NEWSITE` +
102
- `CLOUDFLARE_PAGES_ACCOUNT_ID_NEWSITE`.
101
+ …and store the vault keys under the surface segment that matches how
102
+ the token is scoped: `CLOUDFLARE_WORKERS_API_TOKEN_NEWSITE` +
103
+ `CLOUDFLARE_WORKERS_ACCOUNT_ID_NEWSITE` (Workers surface), or
104
+ `CLOUDFLARE_PAGES_API_TOKEN_NEWSITE` + `CLOUDFLARE_PAGES_ACCOUNT_ID_NEWSITE`
105
+ (Pages surface). See `packages/cli/docs/gsc.md` for the candidate order.
103
106
 
104
107
  ## Exit codes
105
108
 
@@ -53,14 +53,19 @@ Auth (one-time setup; see docs/gsc.md):
53
53
  GSC_SERVICE_ACCOUNT_KEY (entire JSON contents as the value).
54
54
  - SA email added as a user on each GSC property via the GSC dashboard.
55
55
 
56
- Cloudflare DNS planting reads two vault keys. Resolution order:
57
- 1. With --account <name>: CLOUDFLARE_PAGES_API_KEY_<NAME> +
58
- CLOUDFLARE_PAGES_ACCOUNT_ID_<NAME>
59
- (e.g. --account stravica -> *_STRAVICA pair)
60
- 2. Without --account: CLOUDFLARE_PAGES_API_KEY +
61
- CLOUDFLARE_PAGES_ACCOUNT_ID
62
- 3. Legacy fallback: CLOUDFLARE_API_TOKEN +
63
- CLOUDFLARE_ACCOUNT_ID
56
+ Cloudflare DNS planting reads two vault keys. --account <name> is
57
+ required (letters/digits only). The suffix is upper-cased and each
58
+ candidate pair below is tried in order — first fully-present pair wins.
59
+
60
+ 1. CLOUDFLARE_WORKERS_API_TOKEN_<NAME> +
61
+ CLOUDFLARE_WORKERS_ACCOUNT_ID_<NAME>
62
+ (Workers surface — e.g. --account stravica)
63
+ 2. CLOUDFLARE_PAGES_API_TOKEN_<NAME> +
64
+ CLOUDFLARE_PAGES_ACCOUNT_ID_<NAME>
65
+ (Pages surface — e.g. --account clive)
66
+ 3. CLOUDFLARE_PAGES_API_KEY_<NAME> +
67
+ CLOUDFLARE_PAGES_ACCOUNT_ID_<NAME>
68
+ (deprecated pre-2026-09-01 naming; hit emits a stderr warning)
64
69
  `;
65
70
 
66
71
  export async function run(argv, overrides = {}) {
@@ -5,22 +5,29 @@
5
5
  // 2. find an existing TXT record by name (idempotency)
6
6
  // 3. create or update a TXT record
7
7
  //
8
- // Credentials are read from the SOPS vault. The vault holds two key
9
- // shapes for Cloudflare:
8
+ // Credentials are read from the SOPS vault. The vault holds account-suffixed
9
+ // pairs — the caller picks the suffix via the `account` option (lower-cased
10
+ // externally; matched case-insensitively to the upper-case suffix).
10
11
  //
11
- // - Account-suffixed pair (preferred — multi-account safe):
12
- // CLOUDFLARE_PAGES_API_KEY_<SUFFIX>
13
- // CLOUDFLARE_PAGES_ACCOUNT_ID_<SUFFIX>
14
- // The caller picks the suffix via the `account` option (lower-cased
15
- // externally; matched case-insensitively to the upper-case suffix).
12
+ // Vault-key shapes tried in order per account (2026-09-01 rename,
13
+ // w-2026-08-31-dave-010; see vault-key-rename-plan-2026-08-31.md):
16
14
  //
17
- // - Unsuffixed pair (single-account default):
18
- // CLOUDFLARE_PAGES_API_KEY
19
- // CLOUDFLARE_PAGES_ACCOUNT_ID
15
+ // 1. Workers surface (Workers Scripts / D1 / KV) — e.g. Stravica:
16
+ // CLOUDFLARE_WORKERS_API_TOKEN_<SUFFIX>
17
+ // CLOUDFLARE_WORKERS_ACCOUNT_ID_<SUFFIX>
20
18
  //
21
- // - Legacy bare pair (back-compat fallback):
22
- // CLOUDFLARE_API_TOKEN
23
- // CLOUDFLARE_ACCOUNT_ID
19
+ // 2. Pages surface (Pages + zone purge) — e.g. Clive:
20
+ // CLOUDFLARE_PAGES_API_TOKEN_<SUFFIX>
21
+ // CLOUDFLARE_PAGES_ACCOUNT_ID_<SUFFIX>
22
+ //
23
+ // 3. Deprecated (pre-2026-09-01) — accepted with a stderr warning during
24
+ // the transition window; will be removed in a follow-up:
25
+ // CLOUDFLARE_PAGES_API_KEY_<SUFFIX>
26
+ // CLOUDFLARE_PAGES_ACCOUNT_ID_<SUFFIX>
27
+ //
28
+ // The unsuffixed bare pair (CLOUDFLARE_PAGES_API_KEY / CLOUDFLARE_PAGES_
29
+ // ACCOUNT_ID) that used to be accepted as a single-account default was
30
+ // removed in this same rename — `--account <name>` is now required.
24
31
  //
25
32
  // `fetchImpl` and `now` are injectable for tests.
26
33
 
@@ -28,18 +35,23 @@ import { resolveCommandPaths, checkVault, secretsFileExists, sopsDecrypt, VaultE
28
35
 
29
36
  const CF_API = 'https://api.cloudflare.com/client/v4';
30
37
 
31
- // Vault-key shapes. `*_TEMPLATE` uses `<S>` as the suffix placeholder.
32
- const VAULT_KEYS = Object.freeze({
33
- // Account-suffixed (preferred).
34
- tokenTemplate: 'CLOUDFLARE_PAGES_API_KEY_<S>',
35
- accountIdTemplate: 'CLOUDFLARE_PAGES_ACCOUNT_ID_<S>',
36
- // Unsuffixed (single-account default).
37
- tokenUnsuffixed: 'CLOUDFLARE_PAGES_API_KEY',
38
- accountIdUnsuffixed: 'CLOUDFLARE_PAGES_ACCOUNT_ID',
39
- // Legacy bare (back-compat fallback).
40
- tokenLegacy: 'CLOUDFLARE_API_TOKEN',
41
- accountIdLegacy: 'CLOUDFLARE_ACCOUNT_ID',
42
- });
38
+ // Ordered candidate templates. First fully-present pair wins per account.
39
+ // The `deprecated` flag drives the stderr warning + return-value flag.
40
+ const CANDIDATE_TEMPLATES = Object.freeze([
41
+ Object.freeze({
42
+ tokenTemplate: 'CLOUDFLARE_WORKERS_API_TOKEN_<S>',
43
+ accountIdTemplate: 'CLOUDFLARE_WORKERS_ACCOUNT_ID_<S>',
44
+ }),
45
+ Object.freeze({
46
+ tokenTemplate: 'CLOUDFLARE_PAGES_API_TOKEN_<S>',
47
+ accountIdTemplate: 'CLOUDFLARE_PAGES_ACCOUNT_ID_<S>',
48
+ }),
49
+ Object.freeze({
50
+ tokenTemplate: 'CLOUDFLARE_PAGES_API_KEY_<S>',
51
+ accountIdTemplate: 'CLOUDFLARE_PAGES_ACCOUNT_ID_<S>',
52
+ deprecated: true,
53
+ }),
54
+ ]);
43
55
 
44
56
  /**
45
57
  * Normalise an `--account` value to the upper-case suffix used in the
@@ -65,28 +77,32 @@ export function accountSuffix(account) {
65
77
  }
66
78
 
67
79
  /**
68
- * Build the vault key names a given `account` selection resolves to.
69
- * If `account` is omitted, returns the unsuffixed pair (with a legacy
70
- * fallback list for back-compat).
80
+ * Build the ordered list of vault-key candidate pairs a given `account`
81
+ * selection resolves to. Callers try candidates in order — first fully-
82
+ * present pair wins.
71
83
  *
72
- * @param {string} [account]
73
- * @returns {{ tokenKey: string, accountIdKey: string, fallbacks?: Array<{tokenKey: string, accountIdKey: string}> }}
84
+ * @param {string} account Required — the unsuffixed bare pair was dropped
85
+ * in the 2026-09-01 rename (w-2026-08-31-dave-010).
86
+ * @returns {{ candidates: Array<{tokenKey: string, accountIdKey: string, deprecated?: boolean}> }}
74
87
  */
75
88
  export function resolveVaultKeys(account) {
76
- if (account) {
77
- const s = accountSuffix(account);
78
- return {
79
- tokenKey: VAULT_KEYS.tokenTemplate.replace('<S>', s),
80
- accountIdKey: VAULT_KEYS.accountIdTemplate.replace('<S>', s),
81
- };
89
+ if (!account) {
90
+ throw new VaultError(
91
+ '--account <name> is required (e.g. --account stravica). ' +
92
+ 'The unsuffixed vault-key fallback was removed in the 2026-09-01 rename.',
93
+ 1,
94
+ );
82
95
  }
83
- return {
84
- tokenKey: VAULT_KEYS.tokenUnsuffixed,
85
- accountIdKey: VAULT_KEYS.accountIdUnsuffixed,
86
- fallbacks: [
87
- { tokenKey: VAULT_KEYS.tokenLegacy, accountIdKey: VAULT_KEYS.accountIdLegacy },
88
- ],
89
- };
96
+ const s = accountSuffix(account);
97
+ const candidates = CANDIDATE_TEMPLATES.map((tpl) => {
98
+ const entry = {
99
+ tokenKey: tpl.tokenTemplate.replace('<S>', s),
100
+ accountIdKey: tpl.accountIdTemplate.replace('<S>', s),
101
+ };
102
+ if (tpl.deprecated) entry.deprecated = true;
103
+ return entry;
104
+ });
105
+ return { candidates };
90
106
  }
91
107
 
92
108
  /**
@@ -95,19 +111,18 @@ export function resolveVaultKeys(account) {
95
111
  *
96
112
  * Resolution order:
97
113
  * 1. Test overrides (`cloudflareToken` + `cloudflareAccountId`).
98
- * 2. If `account` provided: only the suffixed pair, hard-fail if missing.
99
- * 3. Otherwise: unsuffixed pair, then the legacy bare pair as fallback.
114
+ * 2. `--account` is required. Try each candidate pair in order; first
115
+ * fully-present pair wins. A hit on a deprecated pair emits a
116
+ * stderr warning (unless `warn` is overridden for tests).
100
117
  *
101
118
  * @param {object} overrides
102
119
  * @param {object} [overrides.paths]
103
120
  * @param {string} [overrides.cloudflareToken]
104
121
  * @param {string} [overrides.cloudflareAccountId]
105
- * @param {string} [overrides.account] Account selector (e.g. "stravica").
122
+ * @param {string} [overrides.account] Account selector (e.g. "stravica"). Required unless raw creds supplied.
106
123
  * @param {object} [overrides.vaultData] Pre-decrypted vault map (test seam).
107
- * When provided, the function uses this instead of running the SOPS
108
- * check + decrypt. Lets tests cover the suffix-resolution and
109
- * fallback paths without touching the filesystem.
110
- * @returns {Promise<{token: string, accountId: string, account: string|null, vaultKeys: {tokenKey: string, accountIdKey: string}}>}
124
+ * @param {Function} [overrides.warn] Test seam for the deprecation warning (defaults to writing to stderr).
125
+ * @returns {Promise<{token: string, accountId: string, account: string|null, vaultKeys: {tokenKey: string, accountIdKey: string, deprecated?: boolean}}>}
111
126
  */
112
127
  export async function loadCloudflareCreds(overrides = {}) {
113
128
  if (overrides.cloudflareToken && overrides.cloudflareAccountId) {
@@ -118,6 +133,9 @@ export async function loadCloudflareCreds(overrides = {}) {
118
133
  vaultKeys: null,
119
134
  };
120
135
  }
136
+
137
+ const plan = resolveVaultKeys(overrides.account);
138
+
121
139
  let data;
122
140
  if (overrides.vaultData) {
123
141
  data = overrides.vaultData;
@@ -143,50 +161,33 @@ export async function loadCloudflareCreds(overrides = {}) {
143
161
  data = await sopsDecrypt(paths, paths.secretsFile);
144
162
  }
145
163
 
146
- const plan = resolveVaultKeys(overrides.account);
164
+ const warn = overrides.warn || ((msg) => process.stderr.write(`${msg}\n`));
147
165
 
148
- // Account-suffixed: must hit exactly the suffixed pair.
149
- if (overrides.account) {
150
- const token = data[plan.tokenKey];
151
- const accountId = data[plan.accountIdKey];
152
- if (!token || !accountId) {
153
- const missing = [];
154
- if (!token) missing.push(plan.tokenKey);
155
- if (!accountId) missing.push(plan.accountIdKey);
156
- throw new VaultError(
157
- `--account "${overrides.account}" expects vault keys [${missing.join(', ')}] but they are not set. ` +
158
- `Store them with:\n secrets set ${plan.tokenKey} <token>\n secrets set ${plan.accountIdKey} <id>`,
159
- 1,
160
- );
161
- }
162
- return {
163
- token,
164
- accountId,
165
- account: overrides.account,
166
- vaultKeys: { tokenKey: plan.tokenKey, accountIdKey: plan.accountIdKey },
167
- };
168
- }
169
-
170
- // No --account: try unsuffixed pair first, then legacy bare pair.
171
- const candidates = [
172
- { tokenKey: plan.tokenKey, accountIdKey: plan.accountIdKey },
173
- ...(plan.fallbacks || []),
174
- ];
175
- for (const cand of candidates) {
166
+ for (const cand of plan.candidates) {
176
167
  const token = data[cand.tokenKey];
177
168
  const accountId = data[cand.accountIdKey];
178
169
  if (token && accountId) {
179
- return { token, accountId, account: null, vaultKeys: cand };
170
+ if (cand.deprecated) {
171
+ warn(
172
+ `warning: Cloudflare vault keys [${cand.tokenKey}, ${cand.accountIdKey}] are deprecated ` +
173
+ `(pre-2026-09-01 naming). Rename to the current pair — see ` +
174
+ `packages/cli/docs/gsc.md for the current key shapes.`,
175
+ );
176
+ }
177
+ return {
178
+ token,
179
+ accountId,
180
+ account: overrides.account,
181
+ vaultKeys: { ...cand },
182
+ };
180
183
  }
181
184
  }
182
- // None matched — error message lists the primary pair the operator
183
- // is expected to set; legacy fallback is a back-compat courtesy, not
184
- // a thing we advertise.
185
+
186
+ const primary = plan.candidates[0];
185
187
  throw new VaultError(
186
- `Cloudflare vault keys not found. Pass --account <name> (e.g. --account stravica) ` +
187
- `to use the per-account pair, or store the default pair:\n` +
188
- ` secrets set ${VAULT_KEYS.tokenUnsuffixed} <token>\n` +
189
- ` secrets set ${VAULT_KEYS.accountIdUnsuffixed} <id>`,
188
+ `--account "${overrides.account}" expects vault keys [${primary.tokenKey}, ${primary.accountIdKey}] ` +
189
+ `(or one of the fallback pairs listed in packages/cli/docs/gsc.md) but none are set. ` +
190
+ `Store them with:\n secrets set ${primary.tokenKey} <token>\n secrets set ${primary.accountIdKey} <id>`,
190
191
  1,
191
192
  );
192
193
  }
@@ -16,8 +16,10 @@
16
16
  //
17
17
  // `accountKey` is the vault-key suffix (lowercase form passed to
18
18
  // `loadCloudflareCreds({ account })`). The CF helper upper-cases it
19
- // internally to build `CLOUDFLARE_PAGES_API_KEY_<SUFFIX>` + matching
20
- // account-id key.
19
+ // internally and tries the current candidate templates in order —
20
+ // CLOUDFLARE_WORKERS_API_TOKEN_<SUFFIX>, then CLOUDFLARE_PAGES_API_
21
+ // TOKEN_<SUFFIX>, then the deprecated CLOUDFLARE_PAGES_API_KEY_<SUFFIX>
22
+ // (see cloudflare.mjs for the full list and gsc.md for the rationale).
21
23
 
22
24
  /** @typedef {{
23
25
  * hostname: string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stravica-ai/digital-operator-pa",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
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",