@spacefast/common 0.0.26 → 0.2.1

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.
Files changed (302) hide show
  1. package/dist/agents/connect-targets.d.ts +6 -146
  2. package/dist/agents/connect-targets.js +63 -295
  3. package/dist/agents/private-key-oauth.d.ts +31 -1
  4. package/dist/agents/private-key-oauth.js +124 -33
  5. package/dist/agents/registry/core.d.ts +292 -0
  6. package/dist/agents/registry/core.js +1388 -0
  7. package/dist/agents/registry/deeplink-probes.d.ts +23 -0
  8. package/dist/agents/registry/deeplink-probes.js +18 -0
  9. package/dist/agents/registry/node.d.ts +56 -0
  10. package/dist/agents/registry/node.js +339 -0
  11. package/dist/brand-assets-build.js +1 -2
  12. package/dist/brand-assets.js +4 -7
  13. package/dist/brand.d.ts +17 -0
  14. package/dist/brand.js +24 -4
  15. package/dist/config/domains.d.ts +55 -28
  16. package/dist/config/domains.js +85 -41
  17. package/dist/config/index.js +1 -2
  18. package/dist/contracts/abuse.js +9 -24
  19. package/dist/contracts/access-profiles.d.ts +86 -0
  20. package/dist/contracts/access-profiles.js +89 -0
  21. package/dist/contracts/access.d.ts +21 -27
  22. package/dist/contracts/access.js +33 -64
  23. package/dist/contracts/activity.d.ts +18 -1
  24. package/dist/contracts/activity.js +70 -26
  25. package/dist/contracts/analytics.js +8 -10
  26. package/dist/contracts/api-keys.d.ts +77 -52
  27. package/dist/contracts/api-keys.js +44 -24
  28. package/dist/contracts/application-journal.d.ts +216 -0
  29. package/dist/contracts/application-journal.js +167 -0
  30. package/dist/contracts/auth.d.ts +90 -17
  31. package/dist/contracts/auth.js +86 -17
  32. package/dist/contracts/beta.d.ts +9 -4
  33. package/dist/contracts/beta.js +20 -16
  34. package/dist/contracts/billing.js +7 -11
  35. package/dist/contracts/bootstrap.d.ts +791 -0
  36. package/dist/contracts/bootstrap.js +51 -0
  37. package/dist/contracts/builds.d.ts +186 -7
  38. package/dist/contracts/builds.js +75 -21
  39. package/dist/contracts/channels.d.ts +43 -0
  40. package/dist/contracts/channels.js +25 -9
  41. package/dist/contracts/cli.d.ts +7 -0
  42. package/dist/contracts/cli.js +6 -0
  43. package/dist/contracts/collab-public.d.ts +1 -1
  44. package/dist/contracts/collab-public.js +5 -8
  45. package/dist/contracts/comments.d.ts +45 -35
  46. package/dist/contracts/comments.js +107 -77
  47. package/dist/contracts/commerce.d.ts +165 -0
  48. package/dist/contracts/commerce.js +146 -0
  49. package/dist/contracts/common.d.ts +3 -3
  50. package/dist/contracts/common.js +18 -29
  51. package/dist/contracts/content-contract-verification.d.ts +521 -0
  52. package/dist/contracts/content-contract-verification.js +320 -0
  53. package/dist/contracts/content-platform-fixture.d.ts +1278 -0
  54. package/dist/contracts/content-platform-fixture.js +44 -0
  55. package/dist/contracts/content-program.d.ts +994 -0
  56. package/dist/contracts/content-program.js +658 -0
  57. package/dist/contracts/content-sync.d.ts +290 -0
  58. package/dist/contracts/content-sync.js +167 -0
  59. package/dist/contracts/content.d.ts +432 -0
  60. package/dist/contracts/content.js +303 -0
  61. package/dist/contracts/continuation.d.ts +17 -1
  62. package/dist/contracts/continuation.js +23 -23
  63. package/dist/contracts/countries.js +3 -4
  64. package/dist/contracts/crons.d.ts +17 -0
  65. package/dist/contracts/crons.js +33 -15
  66. package/dist/contracts/dashboard-prefs.d.ts +56 -0
  67. package/dist/contracts/dashboard-prefs.js +51 -0
  68. package/dist/contracts/device-auth.d.ts +234 -37
  69. package/dist/contracts/device-auth.js +267 -53
  70. package/dist/contracts/docs.d.ts +6 -10
  71. package/dist/contracts/docs.js +5 -13
  72. package/dist/contracts/domains.d.ts +499 -102
  73. package/dist/contracts/domains.js +169 -58
  74. package/dist/contracts/email-preferences.d.ts +3 -3
  75. package/dist/contracts/email-preferences.js +6 -7
  76. package/dist/contracts/enums.d.ts +26 -26
  77. package/dist/contracts/enums.js +61 -54
  78. package/dist/contracts/error-code-meta.d.ts +269 -7
  79. package/dist/contracts/error-code-meta.js +80 -5
  80. package/dist/contracts/error-codes.d.ts +5 -5
  81. package/dist/contracts/error-codes.js +78 -16
  82. package/dist/contracts/events.d.ts +100 -20
  83. package/dist/contracts/events.js +107 -20
  84. package/dist/contracts/execution.d.ts +47 -66
  85. package/dist/contracts/execution.js +51 -64
  86. package/dist/contracts/feature-lifecycle.d.ts +70 -14
  87. package/dist/contracts/feature-lifecycle.js +86 -15
  88. package/dist/contracts/features.d.ts +100 -37
  89. package/dist/contracts/features.js +21 -27
  90. package/dist/contracts/fixtures/content-platform-v1.json +1108 -0
  91. package/dist/contracts/frame-session.d.ts +192 -0
  92. package/dist/contracts/frame-session.js +164 -0
  93. package/dist/contracts/functions.d.ts +218 -211
  94. package/dist/contracts/functions.js +273 -260
  95. package/dist/contracts/grant-copy.d.ts +43 -0
  96. package/dist/contracts/grant-copy.js +44 -0
  97. package/dist/contracts/grants.d.ts +64 -56
  98. package/dist/contracts/grants.js +124 -23
  99. package/dist/contracts/ids.d.ts +4 -1
  100. package/dist/contracts/ids.js +4 -1
  101. package/dist/contracts/internal.d.ts +5 -4
  102. package/dist/contracts/internal.js +12 -3
  103. package/dist/contracts/mcp.d.ts +17 -0
  104. package/dist/contracts/mcp.js +23 -0
  105. package/dist/contracts/me.d.ts +6 -1
  106. package/dist/contracts/me.js +12 -6
  107. package/dist/contracts/notifications.d.ts +27 -29
  108. package/dist/contracts/notifications.js +9 -4
  109. package/dist/contracts/oauth-resources.d.ts +112 -10
  110. package/dist/contracts/oauth-resources.js +116 -11
  111. package/dist/contracts/oauth-scope-actions.d.ts +90 -0
  112. package/dist/contracts/oauth-scope-actions.js +126 -0
  113. package/dist/contracts/operations.d.ts +27 -4
  114. package/dist/contracts/operations.js +11 -5
  115. package/dist/contracts/pages.d.ts +29 -5
  116. package/dist/contracts/pages.js +22 -5
  117. package/dist/contracts/partner-notifications.d.ts +3 -0
  118. package/dist/contracts/partner-notifications.js +65 -0
  119. package/dist/contracts/{platform.d.ts → partner.d.ts} +29 -42
  120. package/dist/contracts/{platform.js → partner.js} +27 -40
  121. package/dist/contracts/plan-policy.d.ts +1 -1
  122. package/dist/contracts/principal-assertion.d.ts +43 -0
  123. package/dist/contracts/principal-assertion.js +67 -0
  124. package/dist/contracts/principals.d.ts +73 -0
  125. package/dist/contracts/principals.js +59 -0
  126. package/dist/contracts/privacy.d.ts +11 -0
  127. package/dist/contracts/privacy.js +17 -0
  128. package/dist/contracts/problem-document.js +3 -4
  129. package/dist/contracts/publish-archive.d.ts +116 -18
  130. package/dist/contracts/publish-archive.js +6 -3
  131. package/dist/contracts/push-new.d.ts +4 -4
  132. package/dist/contracts/quotas.js +3 -3
  133. package/dist/contracts/realtime.d.ts +52 -0
  134. package/dist/contracts/realtime.js +54 -0
  135. package/dist/contracts/repository-connections.d.ts +1 -1
  136. package/dist/contracts/repository-connections.js +6 -2
  137. package/dist/contracts/resources.d.ts +105 -14
  138. package/dist/contracts/resources.js +145 -21
  139. package/dist/contracts/route-inventory.d.ts +358 -0
  140. package/dist/contracts/route-inventory.js +141 -0
  141. package/dist/contracts/runtime-api.d.ts +112 -35
  142. package/dist/contracts/runtime-api.js +86 -66
  143. package/dist/contracts/runtime-app.d.ts +11 -21
  144. package/dist/contracts/runtime-app.js +39 -24
  145. package/dist/contracts/runtime-components.d.ts +143 -0
  146. package/dist/contracts/runtime-components.js +199 -0
  147. package/dist/contracts/runtime-db.d.ts +45 -19
  148. package/dist/contracts/runtime-db.js +85 -20
  149. package/dist/contracts/runtime-purge.js +3 -5
  150. package/dist/contracts/runtime-services.d.ts +43 -75
  151. package/dist/contracts/runtime-services.js +29 -49
  152. package/dist/contracts/runtime-storage.d.ts +14 -14
  153. package/dist/contracts/runtime-storage.js +28 -13
  154. package/dist/contracts/sf-config-v1.d.ts +14 -18
  155. package/dist/contracts/slugs.js +13 -4
  156. package/dist/contracts/space-config.d.ts +235 -28
  157. package/dist/contracts/space-config.js +246 -91
  158. package/dist/contracts/spaces.d.ts +360 -255
  159. package/dist/contracts/spaces.js +190 -122
  160. package/dist/contracts/superadmin-activity.d.ts +3 -43
  161. package/dist/contracts/superadmin-activity.js +6 -30
  162. package/dist/contracts/superadmin-emails.d.ts +1 -0
  163. package/dist/contracts/superadmin-emails.js +17 -1
  164. package/dist/contracts/superadmin-runtime.d.ts +199 -0
  165. package/dist/contracts/superadmin-runtime.js +141 -0
  166. package/dist/contracts/superadmin-search.d.ts +22 -2
  167. package/dist/contracts/superadmin-search.js +14 -6
  168. package/dist/contracts/superadmin-spaces.d.ts +435 -186
  169. package/dist/contracts/superadmin-spaces.js +131 -25
  170. package/dist/contracts/superadmin-teams.d.ts +3 -0
  171. package/dist/contracts/superadmin-teams.js +1 -2
  172. package/dist/contracts/superadmin-tenants.d.ts +627 -6
  173. package/dist/contracts/superadmin-tenants.js +32 -7
  174. package/dist/contracts/superadmin.d.ts +98 -16
  175. package/dist/contracts/superadmin.js +63 -10
  176. package/dist/contracts/tags.d.ts +85 -58
  177. package/dist/contracts/tags.js +44 -34
  178. package/dist/contracts/teams.d.ts +72 -9
  179. package/dist/contracts/teams.js +84 -25
  180. package/dist/contracts/tenants.d.ts +135 -0
  181. package/dist/contracts/tenants.js +120 -0
  182. package/dist/contracts/test-triggers.d.ts +70 -0
  183. package/dist/contracts/test-triggers.js +116 -0
  184. package/dist/contracts/theme-json.js +3 -8
  185. package/dist/contracts/transfers.d.ts +1 -0
  186. package/dist/contracts/transfers.js +1 -0
  187. package/dist/contracts/usage.d.ts +209 -0
  188. package/dist/contracts/usage.js +124 -12
  189. package/dist/contracts/variables.d.ts +2 -2
  190. package/dist/contracts/variables.js +9 -13
  191. package/dist/contracts/webhooks.d.ts +13 -5
  192. package/dist/contracts/webhooks.js +3 -1
  193. package/dist/contracts/zero.d.ts +57 -64
  194. package/dist/contracts/zero.js +68 -66
  195. package/dist/dashboard-paths/index.d.ts +24 -21
  196. package/dist/dashboard-paths/index.js +35 -47
  197. package/dist/dashboard-paths/route-reservations.generated.d.ts +2 -0
  198. package/dist/dashboard-paths/route-reservations.generated.js +36 -0
  199. package/dist/docs/agent-arrival.d.ts +92 -0
  200. package/dist/docs/agent-arrival.js +227 -0
  201. package/dist/docs/agent-output-policy.d.ts +6 -0
  202. package/dist/docs/agent-output-policy.js +22 -0
  203. package/dist/docs/agent-prose.d.ts +11 -17
  204. package/dist/docs/agent-prose.js +62 -37
  205. package/dist/docs/agent-setup.d.ts +72 -41
  206. package/dist/docs/agent-setup.js +336 -436
  207. package/dist/docs/catalog.d.ts +147 -89
  208. package/dist/docs/catalog.js +157 -42
  209. package/dist/docs/error-docs.d.ts +2098 -1
  210. package/dist/docs/error-docs.js +281 -16
  211. package/dist/docs/index-build.d.ts +0 -1
  212. package/dist/docs/index-build.js +2 -3
  213. package/dist/docs/page-context-prompt.d.ts +77 -0
  214. package/dist/docs/page-context-prompt.js +131 -0
  215. package/dist/docs/reference-urls.d.ts +46 -0
  216. package/dist/docs/reference-urls.js +79 -0
  217. package/dist/docs/search.d.ts +6 -8
  218. package/dist/docs/search.js +10 -13
  219. package/dist/docs/skill-distribution.d.ts +4 -1
  220. package/dist/docs/skill-distribution.js +10 -5
  221. package/dist/docs/skills.d.ts +117 -117
  222. package/dist/docs/skills.js +120 -72
  223. package/dist/docs/start-prompts.d.ts +45 -0
  224. package/dist/docs/start-prompts.js +62 -0
  225. package/dist/domain-exploration.js +9 -13
  226. package/dist/og-template.d.ts +56 -0
  227. package/dist/og-template.js +56 -0
  228. package/dist/slug-policy/index.js +5 -11
  229. package/dist/utils/browser-credential.d.ts +13 -6
  230. package/dist/utils/browser-credential.js +14 -7
  231. package/dist/utils/build-settings.d.ts +16 -0
  232. package/dist/utils/build-settings.js +63 -17
  233. package/dist/utils/canonical-json.d.ts +3 -5
  234. package/dist/utils/canonical-json.js +3 -5
  235. package/dist/utils/cast-transport.d.ts +5 -17
  236. package/dist/utils/cast-transport.js +6 -19
  237. package/dist/utils/claim-token.js +1 -2
  238. package/dist/utils/comment-avatars.js +4 -6
  239. package/dist/utils/concurrency.js +2 -3
  240. package/dist/utils/content-type.d.ts +7 -6
  241. package/dist/utils/content-type.js +73 -51
  242. package/dist/utils/credential-policy.d.ts +50 -6
  243. package/dist/utils/credential-policy.js +123 -57
  244. package/dist/utils/egress-policy.fixtures.json +1 -1
  245. package/dist/utils/generate-space-name.d.ts +3 -8
  246. package/dist/utils/generate-space-name.js +3 -8
  247. package/dist/utils/grants.d.ts +0 -1
  248. package/dist/utils/grants.js +4 -7
  249. package/dist/utils/gravatar.d.ts +2 -4
  250. package/dist/utils/gravatar.js +6 -12
  251. package/dist/utils/id-hints.js +14 -23
  252. package/dist/utils/idempotency.d.ts +3 -3
  253. package/dist/utils/idempotency.js +6 -7
  254. package/dist/utils/local-space-state.d.ts +15 -15
  255. package/dist/utils/local-space-state.js +34 -23
  256. package/dist/utils/oauth-signed-query.d.ts +3 -5
  257. package/dist/utils/oauth-signed-query.js +6 -9
  258. package/dist/utils/one-shot-replay.d.ts +3 -5
  259. package/dist/utils/one-shot-replay.js +3 -5
  260. package/dist/utils/page-colors.d.ts +4 -10
  261. package/dist/utils/page-colors.js +11 -35
  262. package/dist/utils/page-fonts.js +6 -11
  263. package/dist/utils/page-preview.d.ts +0 -1
  264. package/dist/utils/page-preview.js +0 -1
  265. package/dist/utils/pages.d.ts +22 -11
  266. package/dist/utils/pages.js +70 -30
  267. package/dist/utils/privacy-regions.d.ts +19 -0
  268. package/dist/utils/privacy-regions.js +91 -0
  269. package/dist/utils/publish-form-data.d.ts +4 -5
  270. package/dist/utils/publish-form-data.js +2 -3
  271. package/dist/utils/publish-policy.d.ts +10 -23
  272. package/dist/utils/publish-policy.js +63 -116
  273. package/dist/utils/query-client.d.ts +2 -0
  274. package/dist/utils/query-client.js +10 -1
  275. package/dist/utils/runtime-paths.d.ts +4 -4
  276. package/dist/utils/runtime-paths.js +6 -8
  277. package/dist/utils/runtime-upload.d.ts +4 -0
  278. package/dist/utils/runtime-upload.js +5 -0
  279. package/dist/utils/secure-local-file.d.ts +3 -3
  280. package/dist/utils/secure-local-file.js +18 -24
  281. package/dist/utils/space-config.d.ts +11 -14
  282. package/dist/utils/space-config.js +10 -16
  283. package/dist/utils/static-runtime-policy.d.ts +5 -8
  284. package/dist/utils/static-runtime-policy.generated.d.ts +16 -0
  285. package/dist/utils/static-runtime-policy.generated.js +70 -0
  286. package/dist/utils/static-runtime-policy.js +17 -84
  287. package/dist/utils/storage-policy.js +1 -2
  288. package/dist/utils/upload-session.js +8 -14
  289. package/dist/utils/version-path.js +11 -19
  290. package/dist/utils/wpcom-auth-redirects.d.ts +5 -14
  291. package/dist/utils/wpcom-auth-redirects.js +1 -4
  292. package/dist/vocabulary.d.ts +7 -7
  293. package/dist/vocabulary.js +30 -16
  294. package/package.json +14 -2
  295. package/dist/agents/client-registry.d.ts +0 -22
  296. package/dist/agents/client-registry.js +0 -41
  297. package/dist/docs/agent-handoff-document.d.ts +0 -15
  298. package/dist/docs/agent-handoff-document.js +0 -180
  299. package/dist/docs/agent-solutions.d.ts +0 -110
  300. package/dist/docs/agent-solutions.js +0 -183
  301. package/dist/utils/query-keys.d.ts +0 -84
  302. package/dist/utils/query-keys.js +0 -108
@@ -1,3 +1,4 @@
1
+ import { type ApprovableScope } from "../contracts/access-profiles.js";
1
2
  import { type ApiKeyPreset, type ApiCredentialPolicy, type StatticAction } from "../contracts/api-keys.js";
2
3
  type RequestCondition = NonNullable<ApiCredentialPolicy["statements"][number]["conditions"]>;
3
4
  export type PolicyCompileInput = {
@@ -8,21 +9,64 @@ export type PolicyAuthorizeInput = {
8
9
  policy: ApiCredentialPolicy;
9
10
  action: StatticAction;
10
11
  /**
11
- * Candidate resource URIs for one request target, evaluated together: spaces
12
- * are addressed by their canonical site-rooted URI AND a principal-scoped
13
- * candidate so principal wildcards cover them. A deny matching ANY candidate
14
- * wins; otherwise an allow matching any candidate permits.
12
+ * Candidate URIs for one request target: a space is addressed both by its
13
+ * site-rooted URI and by a principal-scoped one. A deny on any candidate
14
+ * wins; an allow on any candidate permits.
15
15
  */
16
16
  resource: string | readonly string[];
17
17
  conditions?: RequestCondition;
18
18
  };
19
- export declare const publishActions: ("spaces:read" | "spaces:write" | "spaces:publish" | "builds:trigger" | "spaces:create" | "versions:read" | "versions:promote" | "versions:download" | "builds:read" | "builds:create")[];
19
+ /**
20
+ * What a publishing credential may do — the same set the `publish:write` OAuth
21
+ * scope grants, derived from the one scope⇄action table so an API-key preset
22
+ * and an OAuth client of the same tier can never drift apart.
23
+ */
24
+ export declare const publishActions: ("teams:read" | "teams:create" | "spaces:read" | "spaces:write" | "spaces:rename" | "domains:read" | "spaces:publish" | "builds:trigger" | "*" | "spaces:create" | "spaces:delete" | "spaces:transfer" | "spaces:export" | "spaces:import" | "spaces:policy.write" | "versions:read" | "versions:promote" | "versions:delete" | "versions:download" | "domains:create" | "domains:verify" | "domains:assign" | "domains:bind" | "domains:delete" | "domains:dns.write" | "domains:dns.credentials.write" | "domains:purchase" | "webhooks:read" | "webhooks:write" | "builds:read" | "builds:create" | "variables:read" | "variables:write" | "tenants:read" | "tenants:write" | "principals:read" | "principals:write" | "infrastructure:read" | "teams:write" | "teams:members.write" | "billing:read" | "billing:write" | "tokens:read" | "tokens:create" | "tokens:revoke" | "runtime:manage")[];
25
+ /** The canonical policy resource for a team principal's subtree. */
26
+ export declare function teamResource(input: PolicyCompileInput): string;
20
27
  export declare function compileApiKeyPreset(preset: ApiKeyPreset, input: PolicyCompileInput): ApiCredentialPolicy;
21
- /** Expand a named API-key preset into the finite permissions sent on the public wire contract. */
28
+ /**
29
+ * Expand a named API-key preset into the finite permissions sent on the public
30
+ * wire contract. Deduplicated, because presets compose overlapping action
31
+ * groups and `allowPolicy` dedupes what it compiles — the wire list and the
32
+ * compiled statement must name the same actions.
33
+ */
22
34
  export declare function apiKeyPresetPermissions(preset: ApiKeyPreset): StatticAction[];
35
+ export declare function compileTenantRootedPolicy(input: {
36
+ tenantId: string;
37
+ preset: ApiKeyPreset;
38
+ }): ApiCredentialPolicy;
39
+ export declare function compilePartnerPrincipalPolicy(input: {
40
+ tenantId: string;
41
+ normalizedPrincipalId: string;
42
+ }): ApiCredentialPolicy;
23
43
  export declare function compileTeamRolePolicy(input: PolicyCompileInput & {
24
44
  role: string;
25
45
  }): ApiCredentialPolicy;
46
+ /**
47
+ * The authority a membership-following key resolves to for one team: an action
48
+ * set bounded by the member's live role.
49
+ *
50
+ * Takes the action set rather than a preset name because a key's authority is
51
+ * not always its preset's CURRENT meaning. A key whose stored preset was
52
+ * redefined under it carries an explicit pin of the actions it was granted
53
+ * (`preset_actions_pin`), and that pin is what it keeps authorizing by.
54
+ */
55
+ export declare function compileBoundedActionsPolicy(actions: readonly StatticAction[], input: PolicyCompileInput & {
56
+ role: string;
57
+ }): ApiCredentialPolicy;
58
+ /**
59
+ * Whether a person holding `role` in a team can currently exercise `scope`
60
+ * there. Account-plane scopes are admission the person always holds for
61
+ * themselves — no team role could bound them. Team-plane scopes need every
62
+ * action they grant inside the role's compiled policy.
63
+ *
64
+ * Informational, not a gate: approval no longer fails on role (authority is
65
+ * bounded live, per request, by `compileBoundedActionsPolicy`); the approval
66
+ * page uses this to tell the approver what a grant will actually do under
67
+ * their current role.
68
+ */
69
+ export declare function scopeGrantableByRole(scope: ApprovableScope, role: string): boolean;
26
70
  export declare function compileBoundedPresetPolicy(preset: ApiKeyPreset, input: PolicyCompileInput & {
27
71
  role: string;
28
72
  }): ApiCredentialPolicy;
@@ -1,16 +1,12 @@
1
+ import { SCOPE_PLANES } from "../contracts/access-profiles.js";
1
2
  import { SPACEFAST_ACCESS_POLICY_VERSION, parseStatticResource, } from "../contracts/api-keys.js";
2
- export const publishActions = [
3
- "spaces:read",
4
- "spaces:create",
5
- "spaces:write",
6
- "spaces:publish",
7
- "versions:read",
8
- "versions:promote",
9
- "versions:download",
10
- "builds:read",
11
- "builds:trigger",
12
- "builds:create",
13
- ];
3
+ import { oauthScopeActions, oauthScopeActionsFor } from "../contracts/oauth-scope-actions.js";
4
+ /**
5
+ * What a publishing credential may do — the same set the `publish:write` OAuth
6
+ * scope grants, derived from the one scope⇄action table so an API-key preset
7
+ * and an OAuth client of the same tier can never drift apart.
8
+ */
9
+ export const publishActions = oauthScopeActionsFor("publish:write");
14
10
  const spaceAdminActions = [
15
11
  ...publishActions,
16
12
  "spaces:rename",
@@ -19,22 +15,13 @@ const spaceAdminActions = [
19
15
  "spaces:export",
20
16
  "spaces:import",
21
17
  "versions:delete",
22
- // Resolving the owning team (name/slug) is part of administering its spaces:
23
- // the CLI resolves teams before every publish, so admin-tier presets carry
24
- // read access to the team resource itself.
18
+ // The CLI resolves the owning team before every publish, so admin-tier
19
+ // presets need read access to the team resource itself.
25
20
  "teams:read",
26
21
  ];
27
- const domainActions = [
28
- "domains:read",
29
- "domains:create",
30
- "domains:verify",
31
- "domains:assign",
32
- "domains:bind",
33
- "domains:delete",
34
- "domains:dns.write",
35
- "domains:purchase",
36
- ];
37
- function teamResource(input) {
22
+ const domainActions = oauthScopeActionsFor("domains:write");
23
+ /** The canonical policy resource for a team principal's subtree. */
24
+ export function teamResource(input) {
38
25
  return `stt://tenant/${input.tenantId}/principal/team/${input.teamId}/*`;
39
26
  }
40
27
  function spaceResource(input) {
@@ -43,21 +30,29 @@ function spaceResource(input) {
43
30
  export function compileApiKeyPreset(preset, input) {
44
31
  return allowPolicy(apiKeyPresetPermissions(preset), [teamResource(input)]);
45
32
  }
46
- /** Expand a named API-key preset into the finite permissions sent on the public wire contract. */
33
+ /**
34
+ * Expand a named API-key preset into the finite permissions sent on the public
35
+ * wire contract. Deduplicated, because presets compose overlapping action
36
+ * groups and `allowPolicy` dedupes what it compiles — the wire list and the
37
+ * compiled statement must name the same actions.
38
+ */
47
39
  export function apiKeyPresetPermissions(preset) {
40
+ return [...new Set(presetActions(preset))];
41
+ }
42
+ function presetActions(preset) {
48
43
  switch (preset) {
49
44
  case "ci_deploy":
50
45
  case "space_publisher":
51
- return [...publishActions];
46
+ // `sf publish` resolves the owning team before it creates a Space, so a
47
+ // publishing key that cannot read its own team cannot publish a new one
48
+ // at all - the CLI's team pre-flight sees an empty list and stops before
49
+ // the API, which would have resolved the team from the key itself.
50
+ // `publishActions` stays the pure set of publish actions: claim
51
+ // continuation checks a team role against it.
52
+ return [...publishActions, "teams:read"];
52
53
  case "space_admin":
53
- return [...spaceAdminActions];
54
- case "site_admin":
55
54
  return [
56
- "sites:read",
57
- "sites:create",
58
- "sites:write",
59
- "sites:delete",
60
- "sites:policy.write",
55
+ "spaces:policy.write",
61
56
  ...spaceAdminActions,
62
57
  ...domainActions,
63
58
  "webhooks:read",
@@ -69,9 +64,54 @@ export function apiKeyPresetPermissions(preset) {
69
64
  return ["teams:read", "billing:read"];
70
65
  case "team_admin":
71
66
  return ["*"];
67
+ // The partner tier: everything a partner needs to run its own customers'
68
+ // spaces on our infrastructure, and nothing that mints credentials, edits
69
+ // team membership, or moves money.
70
+ case "partner_admin":
71
+ return [
72
+ "tenants:read",
73
+ "tenants:write",
74
+ "principals:read",
75
+ "principals:write",
76
+ "infrastructure:read",
77
+ "spaces:policy.write",
78
+ ...spaceAdminActions,
79
+ ...domainActions,
80
+ "webhooks:read",
81
+ "webhooks:write",
82
+ "teams:read",
83
+ "billing:read",
84
+ ];
72
85
  }
73
86
  throw new Error("Unsupported API key preset.");
74
87
  }
88
+ // A partner credential is rooted at the tenant, not at a team principal: it
89
+ // reaches every principal the tenant owns — teams and external customers alike —
90
+ // and stops at the tenant boundary. `compileApiKeyPreset` stays the team-rooted
91
+ // compiler; it needs a teamId this shape has no meaning for.
92
+ export function compileTenantRootedPolicy(input) {
93
+ return allowPolicy(apiKeyPresetPermissions(input.preset), [`stt://tenant/${input.tenantId}`]);
94
+ }
95
+ export function compilePartnerPrincipalPolicy(input) {
96
+ if (input.normalizedPrincipalId.includes("*") || input.normalizedPrincipalId.includes("/")) {
97
+ throw new Error("Partner principal id contains a reserved policy character.");
98
+ }
99
+ return allowPolicy([
100
+ "spaces:read",
101
+ "spaces:create",
102
+ "spaces:write",
103
+ "spaces:rename",
104
+ "spaces:publish",
105
+ "versions:read",
106
+ "versions:promote",
107
+ "versions:download",
108
+ "builds:read",
109
+ "builds:trigger",
110
+ "builds:create",
111
+ "variables:read",
112
+ "variables:write",
113
+ ], [`stt://tenant/${input.tenantId}/principal/external/${input.normalizedPrincipalId}/*`]);
114
+ }
75
115
  export function compileTeamRolePolicy(input) {
76
116
  switch (input.role) {
77
117
  case "owner":
@@ -79,7 +119,6 @@ export function compileTeamRolePolicy(input) {
79
119
  return compileApiKeyPreset("team_admin", input);
80
120
  case "member":
81
121
  return allowPolicy([
82
- "sites:read",
83
122
  "spaces:read",
84
123
  "spaces:create",
85
124
  "spaces:write",
@@ -99,24 +138,56 @@ export function compileTeamRolePolicy(input) {
99
138
  return denyAllPolicy();
100
139
  }
101
140
  }
102
- export function compileBoundedPresetPolicy(preset, input) {
103
- const presetPolicy = compileApiKeyPreset(preset, input);
141
+ /**
142
+ * The authority a membership-following key resolves to for one team: an action
143
+ * set bounded by the member's live role.
144
+ *
145
+ * Takes the action set rather than a preset name because a key's authority is
146
+ * not always its preset's CURRENT meaning. A key whose stored preset was
147
+ * redefined under it carries an explicit pin of the actions it was granted
148
+ * (`preset_actions_pin`), and that pin is what it keeps authorizing by.
149
+ */
150
+ export function compileBoundedActionsPolicy(actions, input) {
104
151
  const rolePolicy = compileTeamRolePolicy(input);
105
- const presetActions = new Set(presetPolicy.statements
106
- .filter((statement) => statement.effect === "allow")
107
- .flatMap((statement) => statement.actions));
152
+ const presetActions = new Set(actions);
108
153
  const roleActions = new Set(rolePolicy.statements
109
154
  .filter((statement) => statement.effect === "allow")
110
155
  .flatMap((statement) => statement.actions));
111
- const actions = presetActions.has("*")
156
+ const bounded = presetActions.has("*")
112
157
  ? [...roleActions]
113
158
  : roleActions.has("*")
114
159
  ? [...presetActions]
115
160
  : [...presetActions].filter((action) => roleActions.has(action));
116
- if (actions.length === 0) {
161
+ if (bounded.length === 0) {
117
162
  return { version: SPACEFAST_ACCESS_POLICY_VERSION, statements: [] };
118
163
  }
119
- return allowPolicy(actions, [teamResource(input)]);
164
+ return allowPolicy(bounded, [teamResource(input)]);
165
+ }
166
+ /**
167
+ * Whether a person holding `role` in a team can currently exercise `scope`
168
+ * there. Account-plane scopes are admission the person always holds for
169
+ * themselves — no team role could bound them. Team-plane scopes need every
170
+ * action they grant inside the role's compiled policy.
171
+ *
172
+ * Informational, not a gate: approval no longer fails on role (authority is
173
+ * bounded live, per request, by `compileBoundedActionsPolicy`); the approval
174
+ * page uses this to tell the approver what a grant will actually do under
175
+ * their current role.
176
+ */
177
+ export function scopeGrantableByRole(scope, role) {
178
+ if (SCOPE_PLANES[scope] === "account") {
179
+ return true;
180
+ }
181
+ const roleActions = new Set(compileTeamRolePolicy({ tenantId: "-", teamId: "-", role })
182
+ .statements.filter((statement) => statement.effect === "allow")
183
+ .flatMap((statement) => statement.actions));
184
+ if (roleActions.has("*")) {
185
+ return true;
186
+ }
187
+ return oauthScopeActions([scope]).every((action) => roleActions.has(action));
188
+ }
189
+ export function compileBoundedPresetPolicy(preset, input) {
190
+ return compileBoundedActionsPolicy(apiKeyPresetPermissions(preset), input);
120
191
  }
121
192
  export function compileSpaceContinuationPolicy(input) {
122
193
  return allowPolicy(publishActions.filter((action) => action !== "spaces:create"), [spaceResource(input)]);
@@ -189,9 +260,8 @@ export function grantedPrincipalsFromPolicy(policy, input) {
189
260
  if (!parsed || (parsed.tenantId !== input.tenantId && parsed.tenantId !== "*")) {
190
261
  continue;
191
262
  }
192
- // Tenant-rooted grants (both `stt://tenant/*` and a concrete
193
- // `stt://tenant/{t}` already filtered to this tenant) are scope-wide:
194
- // they reach every principal in the tenant.
263
+ // A tenant-rooted grant is scope-wide: `stt://tenant/*` and a concrete
264
+ // `stt://tenant/{t}` both reach every principal in the tenant.
195
265
  if (parsed.kind === "tenant") {
196
266
  all = true;
197
267
  }
@@ -226,9 +296,8 @@ export function grantedPrincipalsFromPolicy(policy, input) {
226
296
  ],
227
297
  };
228
298
  }
229
- // Whether the policy's allow statements reach a resource subtree at all
230
- // either a statement resource covers the base, or it lives inside the base.
231
- // This is a visibility/scoping check (404 vs 403), never an action grant.
299
+ // Whether the policy's allow statements reach a resource subtree at all. This
300
+ // is a visibility/scoping check (404 vs 403), never an action grant.
232
301
  export function policyReaches(policy, resourceBase) {
233
302
  return policy.statements.some((statement) => statement.effect === "allow" &&
234
303
  statement.resources.some((resource) => resourceMatches(resource, resourceBase) ||
@@ -259,10 +328,8 @@ function resourceMatches(pattern, resource) {
259
328
  if (pattern.endsWith("/*")) {
260
329
  return resource === pattern.slice(0, -2) || resource.startsWith(pattern.slice(0, -1));
261
330
  }
262
- // A tenant-rooted resource is scope-wide (the platform spec:
263
- // `stt://tenant/{t}` is a grantable StatticResource and resource grants cover
264
- // their subtree): it covers every resource inside that tenant. This is the
265
- // tenant-scoped (partner/agency) credential shape.
331
+ // A tenant-rooted resource covers every resource inside that tenant. This is
332
+ // the tenant-scoped (partner/agency) credential shape.
266
333
  if (parseStatticResource(pattern)?.kind === "tenant") {
267
334
  return resource.startsWith(`${pattern}/`);
268
335
  }
@@ -276,9 +343,8 @@ function conditionsMatch(statementConditions, contextConditions, effect, mode) {
276
343
  // SAFETY: Object.entries obtains key from the named RequestCondition object immediately above.
277
344
  const resolved = contextConditions?.[key];
278
345
  if (resolved === undefined && mode === "request") {
279
- // Fail closed on unresolvable condition attributes: the condition never
280
- // matches for allow statements and always matches for deny statements, so
281
- // callers cannot dodge a deny by stripping request context.
346
+ // Fail closed: a caller cannot dodge a deny by stripping the request
347
+ // context its condition reads.
282
348
  return effect === "deny";
283
349
  }
284
350
  return resolved === value;
@@ -45,7 +45,7 @@
45
45
  { "host": "sub.localhost", "allowed": false },
46
46
  { "host": "169.254.169.254", "allowed": false },
47
47
  { "host": "view.fast", "allowed": false },
48
- { "host": "wpc-manage-site-123.view.fast", "allowed": false },
48
+ { "host": "site-123.view.fast", "allowed": false },
49
49
  { "host": "example.com", "allowed": true }
50
50
  ]
51
51
  }
@@ -1,12 +1,7 @@
1
1
  /**
2
- * Generates a short, readable space title.
3
- *
4
- * The pool mixes a little WordPress and web flavor with broad nature, color,
5
- * motion, and craft vocabulary. The result should suit any site instead of
6
- * reading like a CMS inside joke, while staying compact enough for a hostname.
7
- *
8
- * The generator is intentionally dependency-free so it can be shared by the
9
- * control-plane, dashboard, CLI, and future publishing surfaces.
2
+ * Short, readable space titles. Keep the pools site-neutral and short enough
3
+ * for a hostname. Dependency-free so the control plane, dashboard and CLI can
4
+ * all share it.
10
5
  */
11
6
  export declare const SPACE_TITLE_COMBINATION_COUNT: number;
12
7
  export declare function generateSpaceTitle(random?: () => number): string;
@@ -1,12 +1,7 @@
1
1
  /**
2
- * Generates a short, readable space title.
3
- *
4
- * The pool mixes a little WordPress and web flavor with broad nature, color,
5
- * motion, and craft vocabulary. The result should suit any site instead of
6
- * reading like a CMS inside joke, while staying compact enough for a hostname.
7
- *
8
- * The generator is intentionally dependency-free so it can be shared by the
9
- * control-plane, dashboard, CLI, and future publishing surfaces.
2
+ * Short, readable space titles. Keep the pools site-neutral and short enough
3
+ * for a hostname. Dependency-free so the control plane, dashboard and CLI can
4
+ * all share it.
10
5
  */
11
6
  const ADJECTIVES = [
12
7
  "Atomic",
@@ -44,7 +44,6 @@ export type GrantEvaluation = {
44
44
  matches: GrantMatch[];
45
45
  };
46
46
  export declare function grantResourcePatternMatches(pattern: string, resource: string, resourceKind?: "route" | "artifact"): boolean;
47
- /** Does a Grant's resource set reach this path? Include, minus exclude. */
48
47
  export declare function grantResourceSetMatches(resources: Grant["resources"], resource: string, resourceKind?: "route" | "artifact"): boolean;
49
48
  export type GrantEvaluationExplanation = {
50
49
  grantId: string;
@@ -73,12 +73,10 @@ export function grantResourcePatternMatches(pattern, resource, resourceKind = "r
73
73
  return matches(0, 0);
74
74
  }
75
75
  /**
76
- * Exclude patterns read wider than include patterns, and that asymmetry is the
77
- * rule, not an accident: the route lane collapses a terminal `index.html`
78
- * (normalizeAccessScopePath) while the artifact lane keeps it, so an exclude
79
- * naming an index artifact must also close the route that artifact serves.
80
- * Include never gets the same courtesy — granting the stripped route would hand
81
- * out access nobody wrote down. The serving engine compiles the identical pair
76
+ * Exclude reads wider than include, deliberately: the route lane collapses a
77
+ * terminal `index.html`, so an exclude naming an index artifact must also close
78
+ * the route it serves. Include must not, or it would grant a route nobody wrote
79
+ * down. The serving engine compiles the same pair
82
80
  * (access-rules.php _stattic_grant_exclude_pattern_variants).
83
81
  */
84
82
  function grantExcludePatternMatches(pattern, resource, resourceKind) {
@@ -90,7 +88,6 @@ function grantExcludePatternMatches(pattern, resource, resourceKind) {
90
88
  const stripped = canonical.slice(0, -"/index.html".length);
91
89
  return grantResourcePatternMatches(stripped === "" ? "/" : stripped, resource, resourceKind);
92
90
  }
93
- /** Does a Grant's resource set reach this path? Include, minus exclude. */
94
91
  export function grantResourceSetMatches(resources, resource, resourceKind = "route") {
95
92
  return (resources.include.some((pattern) => grantResourcePatternMatches(pattern, resource, resourceKind)) &&
96
93
  !resources.exclude.some((pattern) => grantExcludePatternMatches(pattern, resource, resourceKind)));
@@ -1,9 +1,7 @@
1
- /** Gravatar's canonical address form: trimmed and lowercased. */
2
1
  export declare function normalizeGravatarEmail(email: string | null | undefined): string | null;
3
- /** The canonical avatar URL for an already-computed SHA-256 hash. */
4
2
  export declare function gravatarUrlFromHash(hashHex: string): string;
5
3
  /**
6
- * SHA-256 hex of a normalized email — Gravatar's identifier. Uses WebCrypto so
7
- * a browser can derive it locally and never send the raw address anywhere.
4
+ * Gravatar's identifier, the SHA-256 hex of a normalized email. WebCrypto lets
5
+ * a browser derive it locally and never send the raw address anywhere.
8
6
  */
9
7
  export declare function gravatarEmailHash(email: string | null | undefined): Promise<string | null>;
@@ -1,26 +1,20 @@
1
1
  /**
2
- * The one Gravatar URL shape Spacefast emits. Every surface control-plane
3
- * profiles, the Comments SDK, Slack notifications derives the same address
4
- * for the same person, so an avatar is recognizable wherever it appears and
5
- * Cast can validate a guest avatar against a single pattern.
6
- *
7
- * `d=404` is deliberate: a person without a Gravatar gets no image at all, and
8
- * the consumer renders its own initials/palette fallback instead of a stock
9
- * silhouette. `s=160` covers the largest avatar we render, retina included.
2
+ * The one Gravatar URL shape Spacefast emits, so Cast can validate a guest
3
+ * avatar against a single pattern. `d=404` returns no image for a person
4
+ * without a Gravatar, leaving the consumer's own fallback to render; `s=160`
5
+ * covers the largest avatar we render, retina included.
10
6
  */
11
7
  const GRAVATAR_AVATAR_QUERY = "d=404&r=g&s=160";
12
- /** Gravatar's canonical address form: trimmed and lowercased. */
13
8
  export function normalizeGravatarEmail(email) {
14
9
  const normalized = email?.trim().toLowerCase();
15
10
  return normalized ? normalized : null;
16
11
  }
17
- /** The canonical avatar URL for an already-computed SHA-256 hash. */
18
12
  export function gravatarUrlFromHash(hashHex) {
19
13
  return `https://gravatar.com/avatar/${hashHex}?${GRAVATAR_AVATAR_QUERY}`;
20
14
  }
21
15
  /**
22
- * SHA-256 hex of a normalized email — Gravatar's identifier. Uses WebCrypto so
23
- * a browser can derive it locally and never send the raw address anywhere.
16
+ * Gravatar's identifier, the SHA-256 hex of a normalized email. WebCrypto lets
17
+ * a browser derive it locally and never send the raw address anywhere.
24
18
  */
25
19
  export async function gravatarEmailHash(email) {
26
20
  const normalized = normalizeGravatarEmail(email);
@@ -1,17 +1,12 @@
1
- // Shared not-found self-correction (W1). One source for the closest-candidate
2
- // ranking used by every resource's 404 hint builder (spaces, versions, teams,
3
- // domains). Resources differ only in which caller-scoped rows they feed in and
4
- // which keys they expose for matching; keeping the levenshtein/closeness/ranking
5
- // here prevents per-resource drift. Callers are responsible for scoping the rows
6
- // to the caller principal — this module never touches the DB or auth.
7
- // How many caller-scoped rows a hint builder will scan to rank candidates. Above
8
- // this the collection is treated as too large to rank for free, so the builder
9
- // returns only the recovery hint (no candidate ids).
1
+ // One closest-candidate ranking for every resource's 404 hint builder (spaces,
2
+ // versions, teams, domains). Callers scope the rows to the caller principal;
3
+ // this module never touches the DB or auth.
4
+ // Above this many rows the collection is too large to rank for free, and the
5
+ // builder returns the recovery hint without candidate ids.
10
6
  export const HINT_SCAN_CAP = 200;
11
7
  export const HINT_MAX_CANDIDATES = 5;
12
- // All supported hint keys (ids, slugs, hostnames, and numeric version refs) fit
13
- // within this ceiling. Rejecting larger values keeps best-effort 404 recovery
14
- // from turning attacker-controlled refs or stored labels into quadratic work.
8
+ // Every hint key (ids, slugs, hostnames, version refs) fits here. Rejecting
9
+ // larger values keeps attacker-controlled refs out of quadratic work.
15
10
  export const HINT_MAX_KEY_LENGTH = 256;
16
11
  function boundedLevenshtein(a, b, maxDistance) {
17
12
  if (Math.abs(a.length - b.length) > maxDistance)
@@ -48,9 +43,8 @@ export function levenshtein(a, b) {
48
43
  }
49
44
  return boundedLevenshtein(a, b, Math.max(a.length, b.length));
50
45
  }
51
- // Returns a small finite score the closer `candidate` is to `needle`, or
52
- // +Infinity when they are not genuinely close. Exact match = 0; substring
53
- // containment = 1; otherwise a typo within a length-bounded edit distance.
46
+ // Lower is closer, +Infinity when not close at all: exact = 0, substring
47
+ // containment = 1, otherwise 1 + a length-bounded edit distance.
54
48
  export function refCloseness(needle, candidate) {
55
49
  if (!candidate)
56
50
  return Number.POSITIVE_INFINITY;
@@ -61,22 +55,19 @@ export function refCloseness(needle, candidate) {
61
55
  return 0;
62
56
  if (candidate.includes(needle) || needle.includes(candidate))
63
57
  return 1;
64
- // Only surface genuinely close typos: bound the allowed distance by the shorter
65
- // string so short refs don't match everything.
58
+ // Bound the distance by the shorter string so short refs don't match everything.
66
59
  const tolerance = Math.max(2, Math.floor(Math.min(needle.length, candidate.length) / 3));
67
60
  const distance = boundedLevenshtein(needle, candidate, tolerance);
68
61
  return distance <= tolerance ? 1 + distance : Number.POSITIVE_INFINITY;
69
62
  }
70
- // Rank `rows` by how close any of their keys are to `ref`, returning at most
71
- // `max` closest rows. A row whose every key is far (or absent) is dropped, so
72
- // only genuine near-matches surface. Ties break by the optional comparator
73
- // (e.g. recency); keys are matched case-insensitively.
63
+ // Rank `rows` by their closest key to `ref`, at most `max` rows. Rows with no
64
+ // close key are dropped. Ties break by the optional comparator; keys match
65
+ // case-insensitively.
74
66
  export function rankCandidates(ref, rows, keysOf, options) {
75
67
  if (rows.length > HINT_SCAN_CAP || ref.length > HINT_MAX_KEY_LENGTH)
76
68
  return [];
77
69
  const needle = ref.toLowerCase();
78
- // Unicode case conversion can expand a string (for example, U+0130). Keep
79
- // that normalized representation under the same work ceiling too.
70
+ // Lowercasing can expand a string (U+0130), so re-check the ceiling.
80
71
  if (needle.length > HINT_MAX_KEY_LENGTH)
81
72
  return [];
82
73
  const max = options?.max ?? HINT_MAX_CANDIDATES;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Stable, secret-safe key for one exact logical mutation attempt. Shared by
3
- * every client surface: the same publish issued through the CLI and through
4
- * MCP must land on one key or the API stops deduplicating the retry.
2
+ * Stable, secret-safe key for one logical mutation attempt. The same publish
3
+ * from the CLI and from MCP must land on one key, or the API stops
4
+ * deduplicating the retry.
5
5
  */
6
6
  export declare function stableIdempotencyKey(prefix: string, parts: unknown): string;
@@ -1,9 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  /**
3
- * Key-sorted JSON that treats an absent key and an `undefined` value as the
4
- * same thing. Deliberately *not* `canonicalJson`: a key the CLI omits and one
5
- * the MCP server sets to `undefined` describe the same mutation, so they have
6
- * to hash alike. Merging the two spellings would change every minted key.
3
+ * Key-sorted JSON that treats an absent key and an `undefined` value alike: a
4
+ * key the CLI omits and one MCP sets to `undefined` are the same mutation.
5
+ * Deliberately not `canonicalJson`; merging the two would change every key.
7
6
  */
8
7
  function keyJson(value) {
9
8
  if (Array.isArray(value)) {
@@ -19,9 +18,9 @@ function keyJson(value) {
19
18
  return JSON.stringify(value ?? null);
20
19
  }
21
20
  /**
22
- * Stable, secret-safe key for one exact logical mutation attempt. Shared by
23
- * every client surface: the same publish issued through the CLI and through
24
- * MCP must land on one key or the API stops deduplicating the retry.
21
+ * Stable, secret-safe key for one logical mutation attempt. The same publish
22
+ * from the CLI and from MCP must land on one key, or the API stops
23
+ * deduplicating the retry.
25
24
  */
26
25
  export function stableIdempotencyKey(prefix, parts) {
27
26
  return `${prefix}-${createHash("sha256").update(keyJson(parts)).digest("hex")}`;