@rubytech/create-sitedesk-code 0.1.401 → 0.1.402

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-sitedesk-code",
3
- "version": "0.1.401",
3
+ "version": "0.1.402",
4
4
  "description": "Install SiteDesk — automated back office for independent building contractors",
5
5
  "bin": {
6
6
  "create-sitedesk-code": "./dist/index.js"
@@ -48,7 +48,7 @@ echo "\$@" >> "\$SB/curl.log"
48
48
  url=""; for a in "\$@"; do case "\$a" in https://*) url="\$a";; esac; done
49
49
  case "\$url" in
50
50
  *"/tokens/permission_groups"*)
51
- echo '{"success":true,"result":[{"id":"pg_pages","name":"Cloudflare Pages Write"},{"id":"pg_d1","name":"D1 Write"},{"id":"pg_dns","name":"DNS Write"},{"id":"pg_acc1","name":"Access: Apps and Policies Write"},{"id":"pg_acc2","name":"Access: Apps and Policies Write"}]}'
51
+ echo '{"success":true,"result":[{"id":"pg_pages","name":"Pages Write"},{"id":"pg_d1","name":"D1 Write"},{"id":"pg_dns","name":"DNS Write"},{"id":"pg_acc1","name":"Access: Apps and Policies Write"},{"id":"pg_acc2","name":"Access: Apps and Policies Write"}]}'
52
52
  ;;
53
53
  *"/tokens/verify"*) echo '{"success":true,"result":{"status":"active"}}';;
54
54
  *"/tokens"*) echo '{"success":true,"result":{"id":"tok1","value":"'"$SECRET_VALUE"'"}}';;
@@ -191,5 +191,100 @@ echo "$ERR" | grep -q "code=1001" && ok "failure code surfaced" || bad "code not
191
191
  no_leak "real failure"
192
192
  teardown
193
193
 
194
+ # 9. pages exact-name: three custom-pages decoys ordered BEFORE the real group.
195
+ # The old fuzzy /pages.*(write|edit)/ + head -1 picks "Access: Custom Pages Write";
196
+ # exact-name must mint the real Pages Write + D1 Write ids and none of the decoys.
197
+ setup "cfat_master_abc"
198
+ cat > "$BIN/curl" <<'CURL'
199
+ #!/usr/bin/env bash
200
+ SB="$(cd "$(dirname "$0")/.." && pwd)"
201
+ echo "$@" >> "$SB/curl.log"
202
+ url=""; for a in "$@"; do case "$a" in https://*) url="$a";; esac; done
203
+ case "$url" in
204
+ *"/permission_groups"*) echo '{"success":true,"result":[{"id":"pg_x1","name":"Access: Custom Pages Write"},{"id":"pg_x2","name":"Account Custom Pages Write"},{"id":"pg_x3","name":"Custom Pages Write"},{"id":"pg_pages_real","name":"Pages Write"},{"id":"pg_d1_real","name":"D1 Write"}]}';;
205
+ *"/tokens/verify"*) echo '{"success":true,"result":{"status":"active"}}';;
206
+ *"/tokens"*) echo '{"success":true,"result":{"value":"cfat_minted_SECRET_do_not_leak_0123456789"}}';;
207
+ *) echo '{"success":false,"errors":[{"code":9999}]}';;
208
+ esac
209
+ CURL
210
+ chmod +x "$BIN/curl"
211
+ run "pages exact-name skips custom-pages decoys" pages 0
212
+ POSTLINE=$(grep -- "-X POST" "$SANDBOX/curl.log")
213
+ echo "$POSTLINE" | grep -q "pg_pages_real" && ok "minted real Pages Write id" || bad "did not mint real pages id: $POSTLINE"
214
+ echo "$POSTLINE" | grep -q "pg_d1_real" && ok "minted real D1 Write id" || bad "did not mint real d1 id"
215
+ echo "$POSTLINE" | grep -qE "pg_x1|pg_x2|pg_x3" && bad "minted a custom-pages decoy id: $POSTLINE" || ok "no custom-pages decoy id minted"
216
+ no_leak "pages exact-name"
217
+ teardown
218
+
219
+ # 10. dns exact-name on cfat_: skip the "Account DNS Settings Write" / "DNS Firewall
220
+ # Write" decoys, mint the real "DNS Write" id, AND nest the zone resource under
221
+ # the account resource (account-owned masters reject the flat zone.* with 1001).
222
+ setup "cfat_master_abc"
223
+ cat > "$BIN/curl" <<'CURL'
224
+ #!/usr/bin/env bash
225
+ SB="$(cd "$(dirname "$0")/.." && pwd)"
226
+ echo "$@" >> "$SB/curl.log"
227
+ url=""; for a in "$@"; do case "$a" in https://*) url="$a";; esac; done
228
+ case "$url" in
229
+ *"/permission_groups"*) echo '{"success":true,"result":[{"id":"pg_dnsx1","name":"Account DNS Settings Write"},{"id":"pg_dnsx2","name":"DNS Firewall Write"},{"id":"pg_dns_real","name":"DNS Write"}]}';;
230
+ *"/tokens/verify"*) echo '{"success":true,"result":{"status":"active"}}';;
231
+ *"/tokens"*) echo '{"success":true,"result":{"value":"cfat_minted_SECRET_do_not_leak_0123456789"}}';;
232
+ *) echo '{"success":false,"errors":[{"code":9999}]}';;
233
+ esac
234
+ CURL
235
+ chmod +x "$BIN/curl"
236
+ run "dns exact-name cfat nests zone under account" dns 0
237
+ POSTLINE=$(grep -- "-X POST" "$SANDBOX/curl.log")
238
+ echo "$POSTLINE" | grep -q "pg_dns_real" && ok "minted real DNS Write id" || bad "did not mint real dns id: $POSTLINE"
239
+ echo "$POSTLINE" | grep -qE "pg_dnsx1|pg_dnsx2" && bad "minted a dns decoy id: $POSTLINE" || ok "no dns decoy id minted"
240
+ echo "$POSTLINE" | grep -qF '"resources":{"com.cloudflare.api.account.acc123":{"com.cloudflare.api.account.zone.*":"*"}}' && ok "cfat nests zone under account" || bad "zone resource not nested for cfat: $POSTLINE"
241
+ no_leak "dns cfat nested"
242
+ teardown
243
+
244
+ # 11. dns exact-name on cfut_: the flat top-level zone resource is preserved, with
245
+ # no account nesting (user-owned masters accept the flat zone.* shape).
246
+ setup "cfut_master_xyz"
247
+ cat > "$BIN/curl" <<'CURL'
248
+ #!/usr/bin/env bash
249
+ SB="$(cd "$(dirname "$0")/.." && pwd)"
250
+ echo "$@" >> "$SB/curl.log"
251
+ url=""; for a in "$@"; do case "$a" in https://*) url="$a";; esac; done
252
+ case "$url" in
253
+ *"/permission_groups"*) echo '{"success":true,"result":[{"id":"pg_dns_real","name":"DNS Write"}]}';;
254
+ *"/tokens/verify"*) echo '{"success":true,"result":{"status":"active"}}';;
255
+ *"/tokens"*) echo '{"success":true,"result":{"value":"cfut_minted_SECRET_do_not_leak_0123456789"}}';;
256
+ *) echo '{"success":false,"errors":[{"code":9999}]}';;
257
+ esac
258
+ CURL
259
+ chmod +x "$BIN/curl"
260
+ run "dns cfut keeps flat zone resource" dns 0
261
+ POSTLINE=$(grep -- "-X POST" "$SANDBOX/curl.log")
262
+ echo "$POSTLINE" | grep -qF '"resources":{"com.cloudflare.api.account.zone.*":"*"}' && ok "cfut flat zone resource" || bad "cfut resource not flat: $POSTLINE"
263
+ echo "$POSTLINE" | grep -qF 'account.acc123":{' && bad "cfut wrongly nested under account: $POSTLINE" || ok "cfut not nested under account"
264
+ no_leak "dns cfut flat"
265
+ teardown
266
+
267
+ # 12. a Cloudflare group rename surfaces as a NAMED miss, not a silent wrong-group
268
+ # mint: with only decoys present and no exact "DNS Write", the helper dies 1 and
269
+ # names the exact group it looked for.
270
+ setup "cfat_master_abc"
271
+ cat > "$BIN/curl" <<'CURL'
272
+ #!/usr/bin/env bash
273
+ SB="$(cd "$(dirname "$0")/.." && pwd)"
274
+ echo "$@" >> "$SB/curl.log"
275
+ url=""; for a in "$@"; do case "$a" in https://*) url="$a";; esac; done
276
+ case "$url" in
277
+ *"/permission_groups"*) echo '{"success":true,"result":[{"id":"pg_dnsx1","name":"Account DNS Settings Write"},{"id":"pg_dnsx2","name":"DNS Firewall Write"}]}';;
278
+ *"/tokens/verify"*) echo '{"success":true,"result":{"status":"active"}}';;
279
+ *"/tokens"*) echo '{"success":true,"result":{"value":"cfat_minted_SECRET_do_not_leak_0123456789"}}';;
280
+ *) echo '{"success":false,"errors":[{"code":9999}]}';;
281
+ esac
282
+ CURL
283
+ chmod +x "$BIN/curl"
284
+ run "missing exact group fails closed" dns 1
285
+ grep -q -- "-X POST" "$SANDBOX/curl.log" && bad "minted despite no exact group match" || ok "no mint on missing exact group"
286
+ echo "$ERR" | grep -qF "no permission group named 'DNS Write'" && ok "die names the missing exact group" || bad "die message not named: $ERR"
287
+ teardown
288
+
194
289
  echo "----"; echo "PASS=$PASS FAIL=$FAIL"
195
290
  [ "$FAIL" -eq 0 ]
@@ -41,13 +41,17 @@ fi
41
41
  [ -n "$brand" ] || die "cannot derive brand from path; set CF_TOKEN_BRAND"
42
42
 
43
43
  # Scope table - resolved by case, never inferred.
44
- legacy=""
44
+ # pages/d1/dns select their permission group by EXACT name (a broad substring once
45
+ # picked "Access: Custom Pages Write" over "Pages Write"); access keeps a regex
46
+ # because it legitimately enumerates duplicate-named candidates and functional-probes.
47
+ legacy=""; names=""; regexes=""; zone_scoped=""
45
48
  case "$scope" in
46
49
  pages|d1) suffix="pages-d1"; key="CF_PAGES_D1_TOKEN"; legacy="CF_PAGES_TOKEN"
47
- # per-group regexes separated by ';' (each may use '|' internally)
48
- regexes="pages.*(write|edit);d1.*(write|edit)"; res='{"com.cloudflare.api.account.'"$ACC"'":"*"}';;
50
+ # exact permission-group names separated by ';'
51
+ names="Pages Write;D1 Write"; res='{"com.cloudflare.api.account.'"$ACC"'":"*"}';;
49
52
  dns) suffix="dns"; key="CF_DNS_TOKEN"
50
- regexes="dns.*(write|edit)"; res='{"com.cloudflare.api.account.zone.*":"*"}';;
53
+ # zone-scoped: res is set after the master-prefix routing (nesting differs)
54
+ names="DNS Write"; zone_scoped=1;;
51
55
  access) suffix="access"; key="CF_ACCESS_TOKEN"
52
56
  regexes="access.*apps.*polic.*(write|edit)"; res='{"com.cloudflare.api.account.'"$ACC"'":"*"}';;
53
57
  esac
@@ -74,9 +78,20 @@ case "$CLOUDFLARE_API_TOKEN" in
74
78
  die "unrecognised master token prefix (not cfat_/cfut_); refusing to guess an endpoint";;
75
79
  esac
76
80
 
81
+ # Zone-scoped resource shape branches by master type. An account-owned (cfat_) master
82
+ # rejects a flat zone.* resource with code 1001 "Must specify a zone for account owned
83
+ # tokens, or nest zone under specific account resource"; nest it under the account.
84
+ # A user-owned (cfut_) master accepts the flat zone.* shape.
85
+ if [ -n "$zone_scoped" ]; then
86
+ case "$mtag" in
87
+ cfat) res='{"com.cloudflare.api.account.'"$ACC"'":{"com.cloudflare.api.account.zone.*":"*"}}';;
88
+ cfut) res='{"com.cloudflare.api.account.zone.*":"*"}';;
89
+ esac
90
+ fi
91
+
77
92
  api_get() { curl -sS -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" "$1"; }
78
93
 
79
- # Resolve permission-group id(s) by name regex (case-insensitive).
94
+ # Resolve permission-group id(s): access matches by regex, pages/d1/dns by exact name.
80
95
  pg_json="$(api_get "${base}/tokens/permission_groups")"
81
96
  echo "$pg_json" | jq -e '.success==true' >/dev/null 2>&1 || {
82
97
  code=$(echo "$pg_json" | jq -r '.errors[0].code // "?"')
@@ -85,6 +100,7 @@ echo "$pg_json" | jq -e '.success==true' >/dev/null 2>&1 || {
85
100
  }
86
101
 
87
102
  collect_ids() { echo "$pg_json" | jq -r --arg re "$1" '.result[] | select(.name|test($re;"i")) | .id'; }
103
+ collect_exact() { echo "$pg_json" | jq -r --arg n "$1" '.result[] | select(.name==$n) | .id'; }
88
104
 
89
105
  mint_with() { # $1 = json permission_groups array -> echoes minted token value or empty
90
106
  local body
@@ -136,12 +152,12 @@ if [ "$scope" = "access" ]; then
136
152
  else
137
153
  pg_array='[]'
138
154
  OLDIFS=$IFS; IFS=';'
139
- for rx in $regexes; do
140
- id=$(collect_ids "$rx" | head -1)
155
+ for nm in $names; do
156
+ id=$(collect_exact "$nm" | head -1)
141
157
  [ -n "$id" ] || {
142
158
  IFS=$OLDIFS
143
159
  echo "[cf-token] op=resolve scope=${scope} master=${mtag} action=mint result=error code=0" >&2
144
- die "no permission group matched /${rx}/"; }
160
+ die "no permission group named '${nm}'"; }
145
161
  pg_array=$(echo "$pg_array" | jq -c --arg id "$id" '. + [{id:$id}]')
146
162
  done
147
163
  IFS=$OLDIFS
@@ -112,6 +112,8 @@ The master an operator provisions is usually **account-scoped** (`cfat_…`, cre
112
112
 
113
113
  Cross-type calls fail with a stable, recognizable signal: a `cfat_…` token at `/user/tokens/verify` returns `Invalid API Token`; a `cfut_…` token at any `/accounts/{id}/…` endpoint returns `9109`. The diagnostic is the verify call's JSON `errors[].code`.
114
114
 
115
+ The prefix also governs the **resource shape** of a zone-scoped mint body, not just the endpoint. A `cfut_…` master accepts a flat zone resource — `"resources": { "com.cloudflare.api.account.zone.*": "*" }`. A `cfat_…` (account-owned) master **rejects** that flat shape with code `1001` ("Must specify a zone for account owned tokens, or nest zone under specific account resource"); it requires the zone resource nested under the account resource — `"resources": { "com.cloudflare.api.account.${ACC}": { "com.cloudflare.api.account.zone.*": "*" } }`. Non-zone scopes (Pages/D1/Access) already scope to `com.cloudflare.api.account.${ACC}` and need no branch.
116
+
115
117
  **Creating** a token stays an account-level, operator-owned concern — each account documents its own master token and its type (e.g. a per-account `secrets/cloudflare.README.md`). Where an account *can* mint an account-scoped token that is the cleaner default; but a skill that merely *consumes* a supplied token must detect the prefix and adapt, never assume it can mint at `/accounts/…`.
116
118
 
117
119
  ---
@@ -154,7 +156,7 @@ CLOUDFLARE_API_TOKEN="${PAGES_D1}" wrangler pages deploy ./dist --project-name <
154
156
 
155
157
  No `expires_on`: the per-scope token is permanent and reused, so decay is the job of the **reconcile pass** (§ Reconcile), not a per-token TTL — the old short-TTL guidance was never honored in practice and produced only never-expiring strays. Look up `<pages-edit-permission-group-id>` (and the ids for DNS Edit, D1 Edit, etc.) once via `GET /accounts/{account_id}/tokens/permission_groups`; they are stable per account. Before reusing or before reconciling, `GET …/tokens` (§ Curated endpoint map) shows what already exists, so a scope is never minted twice under different names.
156
158
 
157
- **Per-scope token resources.** Each reused per-scope token carries the resource that matches its operation class. The Pages-and-D1 token (`<brand>-pages-d1`) and the Access token (`<brand>-access`) are account-scoped: `"resources": { "com.cloudflare.api.account.${CLOUDFLARE_ACCOUNT_ID}": "*" }`. The DNS token (`<brand>-dns`) is zone-scoped, but since it is a single stable token reused across every zone the install routes, its resource covers the account's zones: `"resources": { "com.cloudflare.api.account.zone.*": "*" }`. A per-zone DNS token would defeat the one-stable-token-per-scope model (one token, reused, not one per zone); the minted token is still strictly narrower than the master, which already carries Zone DNS Edit over the zones in scope.
159
+ **Per-scope token resources.** Each reused per-scope token carries the resource that matches its operation class. The Pages-and-D1 token (`<brand>-pages-d1`) and the Access token (`<brand>-access`) are account-scoped: `"resources": { "com.cloudflare.api.account.${CLOUDFLARE_ACCOUNT_ID}": "*" }`. The DNS token (`<brand>-dns`) is zone-scoped, and since it is a single stable token reused across every zone the install routes, its resource covers the account's zones — but the exact shape depends on the master's prefix (see § Token type — route endpoints by prefix): a `cfut_…` master takes the flat `"resources": { "com.cloudflare.api.account.zone.*": "*" }`, while a `cfat_…` master requires the zone nested under the account, `"resources": { "com.cloudflare.api.account.${CLOUDFLARE_ACCOUNT_ID}": { "com.cloudflare.api.account.zone.*": "*" } }` (the flat shape returns `1001` on an account-owned master). A per-zone DNS token would defeat the one-stable-token-per-scope model (one token, reused, not one per zone); the minted token is still strictly narrower than the master, which already carries Zone DNS Edit over the zones in scope.
158
160
 
159
161
  **Redaction is binding.** Every example above pipes the token into a variable or an `Authorization` header and never to stdout. The agent surfaces the operation as verb + target — "minting a Pages-deploy token", "creating CNAME `chat.example.com`" — never the secret. A failure surfaces the API error body with any `Authorization` value masked.
160
162