@spacefast/common 0.0.23 → 0.0.26

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 (224) hide show
  1. package/brand-assets/LICENSE-Haskoy.txt +93 -0
  2. package/brand-assets/haskoy-latin-variable.woff2 +0 -0
  3. package/brand-assets/spacefast-favicon.svg +4 -4
  4. package/brand-assets/spacefast-sf-full-bleed.svg +3 -3
  5. package/brand-assets/spacefast-wordmark.svg +2 -4
  6. package/dist/agents/connect-targets.js +1 -1
  7. package/dist/agents/private-key-oauth.d.ts +0 -1
  8. package/dist/agents/private-key-oauth.js +46 -63
  9. package/dist/brand-assets-build.js +4 -6
  10. package/dist/brand-assets.d.ts +5 -15
  11. package/dist/brand-assets.js +15 -14
  12. package/dist/config/domains.d.ts +47 -7
  13. package/dist/config/domains.js +66 -10
  14. package/dist/config/index.d.ts +0 -1
  15. package/dist/config/index.js +0 -1
  16. package/dist/contracts/abuse.d.ts +2 -0
  17. package/dist/contracts/abuse.js +12 -0
  18. package/dist/contracts/access.d.ts +124 -12
  19. package/dist/contracts/access.js +73 -9
  20. package/dist/contracts/activity.d.ts +1 -1
  21. package/dist/contracts/activity.js +5 -12
  22. package/dist/contracts/api-keys.d.ts +21 -21
  23. package/dist/contracts/api-keys.js +2 -2
  24. package/dist/contracts/beta.js +1 -1
  25. package/dist/contracts/billing.d.ts +3 -35
  26. package/dist/contracts/billing.js +33 -52
  27. package/dist/contracts/builds.d.ts +30 -18
  28. package/dist/contracts/builds.js +28 -9
  29. package/dist/contracts/channels.js +1 -1
  30. package/dist/contracts/cli.d.ts +10 -0
  31. package/dist/contracts/cli.js +10 -0
  32. package/dist/contracts/collab-public.d.ts +63 -0
  33. package/dist/contracts/collab-public.js +46 -0
  34. package/dist/contracts/comments.d.ts +282 -53
  35. package/dist/contracts/comments.js +173 -52
  36. package/dist/contracts/common.d.ts +3 -3
  37. package/dist/contracts/common.js +5 -5
  38. package/dist/contracts/crons.d.ts +35 -0
  39. package/dist/contracts/crons.js +42 -0
  40. package/dist/contracts/device-auth.d.ts +6 -6
  41. package/dist/contracts/device-auth.js +2 -2
  42. package/dist/contracts/docs.js +2 -2
  43. package/dist/contracts/domains.d.ts +38 -27
  44. package/dist/contracts/domains.js +29 -2
  45. package/dist/contracts/enums.d.ts +34 -30
  46. package/dist/contracts/enums.js +62 -48
  47. package/dist/contracts/error-code-meta.d.ts +50 -125
  48. package/dist/contracts/error-code-meta.js +29 -41
  49. package/dist/contracts/error-codes.d.ts +1 -1
  50. package/dist/contracts/error-codes.js +14 -33
  51. package/dist/contracts/execution.d.ts +47 -66
  52. package/dist/contracts/execution.js +53 -49
  53. package/dist/contracts/feature-lifecycle-core.d.ts +0 -6
  54. package/dist/contracts/feature-lifecycle-core.js +2 -31
  55. package/dist/contracts/feature-lifecycle.d.ts +12 -52
  56. package/dist/contracts/feature-lifecycle.js +16 -59
  57. package/dist/contracts/features.d.ts +48 -60
  58. package/dist/contracts/features.js +59 -51
  59. package/dist/contracts/functions.d.ts +266 -72
  60. package/dist/contracts/functions.js +260 -37
  61. package/dist/contracts/git.d.ts +28 -34
  62. package/dist/contracts/git.js +10 -11
  63. package/dist/contracts/grants.d.ts +9 -5
  64. package/dist/contracts/grants.js +18 -9
  65. package/dist/contracts/ids.js +2 -2
  66. package/dist/contracts/internal.d.ts +8 -6
  67. package/dist/contracts/internal.js +12 -12
  68. package/dist/contracts/mcp.d.ts +11 -0
  69. package/dist/contracts/mcp.js +5 -0
  70. package/dist/contracts/me.js +1 -1
  71. package/dist/contracts/oauth-resources.d.ts +4 -3
  72. package/dist/contracts/oauth-resources.js +12 -3
  73. package/dist/contracts/operations.d.ts +90 -5
  74. package/dist/contracts/operations.js +77 -3
  75. package/dist/contracts/pages.d.ts +12 -7
  76. package/dist/contracts/pages.js +24 -1
  77. package/dist/contracts/plan-policy.d.ts +0 -2
  78. package/dist/contracts/plan-policy.js +2 -3
  79. package/dist/contracts/platform.d.ts +61 -148
  80. package/dist/contracts/platform.js +70 -126
  81. package/dist/contracts/publish-archive.d.ts +508 -0
  82. package/dist/contracts/publish-archive.js +32 -0
  83. package/dist/contracts/push-new.d.ts +4 -4
  84. package/dist/contracts/quotas.js +2 -2
  85. package/dist/contracts/repository-connections.d.ts +43 -30
  86. package/dist/contracts/repository-connections.js +11 -3
  87. package/dist/contracts/resources.d.ts +12 -24
  88. package/dist/contracts/resources.js +36 -16
  89. package/dist/contracts/runtime-api.d.ts +433 -192
  90. package/dist/contracts/runtime-api.js +421 -356
  91. package/dist/contracts/runtime-app.d.ts +127 -120
  92. package/dist/contracts/runtime-app.js +19 -10
  93. package/dist/contracts/runtime-db.d.ts +5 -5
  94. package/dist/contracts/runtime-db.js +2 -2
  95. package/dist/contracts/runtime-purge.d.ts +8 -0
  96. package/dist/contracts/runtime-purge.js +7 -0
  97. package/dist/contracts/runtime-services.d.ts +219 -0
  98. package/dist/contracts/runtime-services.js +437 -0
  99. package/dist/contracts/runtime-storage.d.ts +46 -32
  100. package/dist/contracts/runtime-storage.js +31 -8
  101. package/dist/contracts/sf-config-v1.d.ts +48 -10
  102. package/dist/contracts/space-config.d.ts +69 -18
  103. package/dist/contracts/space-config.js +226 -27
  104. package/dist/contracts/spaces.d.ts +236 -450
  105. package/dist/contracts/spaces.js +131 -151
  106. package/dist/contracts/superadmin-activity.d.ts +57 -4
  107. package/dist/contracts/superadmin-activity.js +48 -2
  108. package/dist/contracts/superadmin-search.d.ts +126 -0
  109. package/dist/contracts/superadmin-search.js +61 -0
  110. package/dist/contracts/superadmin-spaces.d.ts +361 -92
  111. package/dist/contracts/superadmin-spaces.js +97 -24
  112. package/dist/contracts/superadmin-teams.d.ts +28 -10
  113. package/dist/contracts/superadmin-teams.js +5 -8
  114. package/dist/contracts/superadmin-tenants.d.ts +0 -6
  115. package/dist/contracts/superadmin-tenants.js +1 -7
  116. package/dist/contracts/superadmin.d.ts +135 -54
  117. package/dist/contracts/superadmin.js +46 -23
  118. package/dist/contracts/tags.d.ts +12 -12
  119. package/dist/contracts/tags.js +3 -3
  120. package/dist/contracts/teams.d.ts +49 -14
  121. package/dist/contracts/teams.js +40 -22
  122. package/dist/contracts/transfers.d.ts +0 -49
  123. package/dist/contracts/transfers.js +4 -8
  124. package/dist/contracts/usage.d.ts +61 -0
  125. package/dist/contracts/usage.js +64 -0
  126. package/dist/contracts/variables.d.ts +2 -7
  127. package/dist/contracts/variables.js +9 -8
  128. package/dist/contracts/webhooks.d.ts +2 -2
  129. package/dist/contracts/zero.d.ts +102 -73
  130. package/dist/contracts/zero.js +94 -32
  131. package/dist/dashboard-paths/index.d.ts +1 -1
  132. package/dist/dashboard-paths/index.js +1 -0
  133. package/dist/docs/agent-prose.d.ts +2 -2
  134. package/dist/docs/agent-prose.js +63 -70
  135. package/dist/docs/agent-setup.js +45 -15
  136. package/dist/docs/catalog.d.ts +24 -24
  137. package/dist/docs/catalog.js +35 -24
  138. package/dist/docs/error-docs.js +194 -270
  139. package/dist/docs/skills.d.ts +12 -12
  140. package/dist/docs/skills.js +4 -4
  141. package/dist/test-helpers/fetch-stub.d.ts +16 -0
  142. package/dist/test-helpers/fetch-stub.js +25 -0
  143. package/dist/utils/build-settings.d.ts +5 -1
  144. package/dist/utils/build-settings.js +91 -13
  145. package/dist/utils/canonical-json.d.ts +10 -0
  146. package/dist/utils/canonical-json.js +22 -0
  147. package/dist/utils/cast-transport.d.ts +28 -0
  148. package/dist/utils/cast-transport.js +52 -0
  149. package/dist/utils/claim-token.d.ts +0 -8
  150. package/dist/utils/claim-token.js +3 -15
  151. package/dist/utils/concurrency.js +1 -0
  152. package/dist/utils/credential-policy.d.ts +1 -1
  153. package/dist/utils/credential-policy.js +4 -3
  154. package/dist/utils/dns-instructions.js +9 -9
  155. package/dist/utils/env-file.d.ts +17 -0
  156. package/dist/utils/env-file.js +96 -0
  157. package/dist/utils/error-code.d.ts +2 -0
  158. package/dist/utils/error-code.js +8 -0
  159. package/dist/utils/generate-space-name.d.ts +8 -10
  160. package/dist/utils/generate-space-name.js +206 -21
  161. package/dist/utils/grant-decision.fixtures.json +2938 -0
  162. package/dist/utils/grants.d.ts +2 -0
  163. package/dist/utils/grants.js +26 -1
  164. package/dist/utils/gravatar.d.ts +9 -0
  165. package/dist/utils/gravatar.js +31 -0
  166. package/dist/utils/idempotency.d.ts +6 -0
  167. package/dist/utils/idempotency.js +28 -0
  168. package/dist/utils/local-space-state.d.ts +9 -16
  169. package/dist/utils/local-space-state.js +20 -50
  170. package/dist/utils/one-shot-replay.d.ts +4 -1
  171. package/dist/utils/page-colors.d.ts +14 -12
  172. package/dist/utils/page-colors.js +15 -13
  173. package/dist/utils/page-fonts.d.ts +2 -2
  174. package/dist/utils/page-fonts.js +15 -4
  175. package/dist/utils/pages.d.ts +21 -3
  176. package/dist/utils/pages.js +42 -13
  177. package/dist/utils/publish-form-data.d.ts +12 -0
  178. package/dist/utils/publish-form-data.js +10 -0
  179. package/dist/utils/publish-policy.d.ts +27 -0
  180. package/dist/utils/publish-policy.fixtures.json +403 -0
  181. package/dist/utils/publish-policy.js +45 -20
  182. package/dist/utils/push-new.d.ts +7 -0
  183. package/dist/utils/push-new.js +7 -0
  184. package/dist/utils/query-keys.d.ts +4 -5
  185. package/dist/utils/query-keys.js +2 -3
  186. package/dist/utils/runtime-paths.d.ts +5 -0
  187. package/dist/utils/runtime-paths.js +5 -0
  188. package/dist/utils/runtime-upload-batch.d.ts +0 -5
  189. package/dist/utils/runtime-upload-batch.js +1 -27
  190. package/dist/utils/runtime-upload.d.ts +6 -11
  191. package/dist/utils/runtime-upload.js +10 -13
  192. package/dist/utils/secure-local-file.js +5 -16
  193. package/dist/utils/space-config.d.ts +1 -34
  194. package/dist/utils/space-config.js +8 -326
  195. package/dist/utils/static-runtime-policy.d.ts +9 -3
  196. package/dist/utils/static-runtime-policy.fixtures.json +38 -41
  197. package/dist/utils/static-runtime-policy.js +22 -56
  198. package/dist/utils/storage-policy.d.ts +1 -1
  199. package/dist/utils/storage-policy.js +3 -1
  200. package/dist/utils/structured-error.d.ts +14 -0
  201. package/dist/utils/structured-error.js +22 -0
  202. package/dist/utils/upload-session.d.ts +4 -1
  203. package/dist/utils/upload-session.js +33 -5
  204. package/dist/utils/version-path.js +13 -4
  205. package/dist/vocabulary.d.ts +13 -11
  206. package/dist/vocabulary.js +42 -28
  207. package/package.json +7 -17
  208. package/dist/config/postgres-budget.d.ts +0 -129
  209. package/dist/config/postgres-budget.js +0 -140
  210. package/dist/config/postgres-budget.json +0 -51
  211. package/dist/contracts/archives.d.ts +0 -331
  212. package/dist/contracts/archives.js +0 -120
  213. package/dist/contracts/deployments.d.ts +0 -574
  214. package/dist/contracts/deployments.js +0 -97
  215. package/dist/contracts/sites.d.ts +0 -416
  216. package/dist/contracts/sites.js +0 -168
  217. package/dist/contracts/superadmin-queues.d.ts +0 -414
  218. package/dist/contracts/superadmin-queues.js +0 -202
  219. package/dist/contracts/superadmin-runtime.d.ts +0 -27
  220. package/dist/contracts/superadmin-runtime.js +0 -38
  221. package/dist/index.d.ts +0 -1
  222. package/dist/index.js +0 -1
  223. package/dist/utils/api.d.ts +0 -4
  224. package/dist/utils/api.js +0 -13
@@ -23,13 +23,13 @@ export interface SpacefastSkillDefinition {
23
23
  export declare const SPACEFAST_SKILLS: readonly [{
24
24
  readonly name: "create-and-publish-site";
25
25
  readonly title: "Create and publish a polished website";
26
- readonly description: "Turn a brief into a responsive static website, review the result, publish it to Spacefast, and verify the live version.";
26
+ readonly description: "Turn a brief into a responsive website, review the result, publish it to Spacefast, and verify the live version.";
27
27
  readonly whenToUse: "Use when the user wants an agent to design, build, improve, or launch a new website rather than only upload an existing build.";
28
28
  readonly keywords: readonly ["create", "website", "design", "build", "responsive", "launch", "publish"];
29
29
  readonly runtimes: readonly ["local", "hosted"];
30
30
  readonly tools: readonly ["publish", "execute"];
31
31
  readonly docs: readonly ["quickstart", "publishing"];
32
- readonly instructions: readonly ["Clarify the audience, goal, required content, brand constraints, and desired call to action. If the visual direction is open, propose two or three distinct directions and choose one with the user.", "Inspect the existing workspace before editing. Reuse its framework, package manager, components, assets, and build conventions when they are sound.", "Build a focused, accessible static experience with a clear hierarchy, real content, responsive layouts, useful interaction states, and no placeholder copy. Keep the main path simple and put secondary detail behind progressive disclosure.", "Run the real build and inspect the result at mobile and desktop widths. Fix overflow, illegible contrast, broken links, missing assets, keyboard traps, and avoidable layout shift.", "Review the workspace diff and summarize material design or content judgments before publishing. Never include API keys, claim credentials, upload credentials, or local auth state in the artifact.", "Publish the built output with `publish`. Present Open as the reusable access link and Claim as the one-time ownership link. Treat live and immutable-version URLs as receipt metadata, not extra actions."];
32
+ readonly instructions: readonly ["Clarify the audience, goal, required content, brand constraints, and desired call to action. If the visual direction is open, propose two or three distinct directions and choose one with the user.", "Inspect the existing workspace before editing. Reuse its framework, package manager, components, assets, and build conventions when they are sound.", "Build a focused, accessible experience with a clear hierarchy, real content, responsive layouts, useful interaction states, and no placeholder copy. Keep the main path simple and put secondary detail behind progressive disclosure.", "Run the real build and inspect the result at mobile and desktop widths. Fix overflow, illegible contrast, broken links, missing assets, keyboard traps, and avoidable layout shift.", "Review the workspace diff and summarize material design or content judgments before publishing. Never include API keys, claim credentials, upload credentials, or local auth state in the artifact.", "Publish the built output with `publish`. Present Open as the reusable access link and Claim as the one-time ownership link. Treat live and immutable-version URLs as receipt metadata, not extra actions."];
33
33
  readonly verification: readonly ["Call `status` after publishing and confirm the intended version is live; use `diagnose` if serving is stalled or failed.", "Open the live URL and verify the primary page, navigation, assets, responsive layout, and main call to action before claiming success."];
34
34
  readonly runtimeNotes: {
35
35
  readonly local: "On-Device can work directly inside the configured local workspace root and publish a local build path.";
@@ -44,13 +44,13 @@ export declare const SPACEFAST_SKILLS: readonly [{
44
44
  }, {
45
45
  readonly name: "publish-and-verify";
46
46
  readonly title: "Publish and verify";
47
- readonly description: "Publish an existing static build, confirm the new version is actually live, and return the useful URLs without leaking credentials.";
47
+ readonly description: "Publish an existing build, confirm the new version is actually live, and return the useful URLs without leaking credentials.";
48
48
  readonly whenToUse: "Use when the artifact already exists and the user wants it published or updated.";
49
49
  readonly keywords: readonly ["publish", "deploy", "live", "verify", "version", "claim"];
50
50
  readonly runtimes: readonly ["local", "hosted"];
51
51
  readonly tools: readonly ["publish", "status", "diagnose"];
52
52
  readonly docs: readonly ["publishing", "workflow/publish-receipt", "workflow/secret-handling"];
53
- readonly instructions: readonly ["Identify the actual publish root or build output. Run the real build first when the source tree is not itself the static artifact.", "Publish with `publish`. For an anonymous On-Device publish, keep the claim credential in local custody and return only the claim URL and expiry; never expose or ask the user to paste the raw token.", "Report the bare Live URL, private Access URL when present, immutable Version URL when present, anonymous Claim URL and expiry when present, plus actionable diagnostics."];
53
+ readonly instructions: readonly ["Identify the actual publish root or build output. Run the real build first when the source tree is not itself the artifact to publish.", "Publish with `publish`. For an anonymous On-Device publish, keep the claim credential in local custody and return only the claim URL and expiry; never expose or ask the user to paste the raw token.", "Report the bare Live URL, private Access URL when present, immutable Version URL when present, anonymous Claim URL and expiry when present, plus actionable diagnostics."];
54
54
  readonly verification: readonly ["Call `status` and confirm the intended version is live. Use `diagnose` for a failed or stalled publish.", "Open the live URL and verify representative content and assets rather than treating a successful upload or health check as serving proof."];
55
55
  readonly runtimeNotes: {
56
56
  readonly local: "Publish a bounded local path; locally custodied claim credentials are reused automatically.";
@@ -196,13 +196,13 @@ export declare const SPACEFAST_SKILLS: readonly [{
196
196
  export declare const SPACEFAST_PROMPT_SKILLS: ({
197
197
  readonly name: "create-and-publish-site";
198
198
  readonly title: "Create and publish a polished website";
199
- readonly description: "Turn a brief into a responsive static website, review the result, publish it to Spacefast, and verify the live version.";
199
+ readonly description: "Turn a brief into a responsive website, review the result, publish it to Spacefast, and verify the live version.";
200
200
  readonly whenToUse: "Use when the user wants an agent to design, build, improve, or launch a new website rather than only upload an existing build.";
201
201
  readonly keywords: readonly ["create", "website", "design", "build", "responsive", "launch", "publish"];
202
202
  readonly runtimes: readonly ["local", "hosted"];
203
203
  readonly tools: readonly ["publish", "execute"];
204
204
  readonly docs: readonly ["quickstart", "publishing"];
205
- readonly instructions: readonly ["Clarify the audience, goal, required content, brand constraints, and desired call to action. If the visual direction is open, propose two or three distinct directions and choose one with the user.", "Inspect the existing workspace before editing. Reuse its framework, package manager, components, assets, and build conventions when they are sound.", "Build a focused, accessible static experience with a clear hierarchy, real content, responsive layouts, useful interaction states, and no placeholder copy. Keep the main path simple and put secondary detail behind progressive disclosure.", "Run the real build and inspect the result at mobile and desktop widths. Fix overflow, illegible contrast, broken links, missing assets, keyboard traps, and avoidable layout shift.", "Review the workspace diff and summarize material design or content judgments before publishing. Never include API keys, claim credentials, upload credentials, or local auth state in the artifact.", "Publish the built output with `publish`. Present Open as the reusable access link and Claim as the one-time ownership link. Treat live and immutable-version URLs as receipt metadata, not extra actions."];
205
+ readonly instructions: readonly ["Clarify the audience, goal, required content, brand constraints, and desired call to action. If the visual direction is open, propose two or three distinct directions and choose one with the user.", "Inspect the existing workspace before editing. Reuse its framework, package manager, components, assets, and build conventions when they are sound.", "Build a focused, accessible experience with a clear hierarchy, real content, responsive layouts, useful interaction states, and no placeholder copy. Keep the main path simple and put secondary detail behind progressive disclosure.", "Run the real build and inspect the result at mobile and desktop widths. Fix overflow, illegible contrast, broken links, missing assets, keyboard traps, and avoidable layout shift.", "Review the workspace diff and summarize material design or content judgments before publishing. Never include API keys, claim credentials, upload credentials, or local auth state in the artifact.", "Publish the built output with `publish`. Present Open as the reusable access link and Claim as the one-time ownership link. Treat live and immutable-version URLs as receipt metadata, not extra actions."];
206
206
  readonly verification: readonly ["Call `status` after publishing and confirm the intended version is live; use `diagnose` if serving is stalled or failed.", "Open the live URL and verify the primary page, navigation, assets, responsive layout, and main call to action before claiming success."];
207
207
  readonly runtimeNotes: {
208
208
  readonly local: "On-Device can work directly inside the configured local workspace root and publish a local build path.";
@@ -217,13 +217,13 @@ export declare const SPACEFAST_PROMPT_SKILLS: ({
217
217
  } | {
218
218
  readonly name: "publish-and-verify";
219
219
  readonly title: "Publish and verify";
220
- readonly description: "Publish an existing static build, confirm the new version is actually live, and return the useful URLs without leaking credentials.";
220
+ readonly description: "Publish an existing build, confirm the new version is actually live, and return the useful URLs without leaking credentials.";
221
221
  readonly whenToUse: "Use when the artifact already exists and the user wants it published or updated.";
222
222
  readonly keywords: readonly ["publish", "deploy", "live", "verify", "version", "claim"];
223
223
  readonly runtimes: readonly ["local", "hosted"];
224
224
  readonly tools: readonly ["publish", "status", "diagnose"];
225
225
  readonly docs: readonly ["publishing", "workflow/publish-receipt", "workflow/secret-handling"];
226
- readonly instructions: readonly ["Identify the actual publish root or build output. Run the real build first when the source tree is not itself the static artifact.", "Publish with `publish`. For an anonymous On-Device publish, keep the claim credential in local custody and return only the claim URL and expiry; never expose or ask the user to paste the raw token.", "Report the bare Live URL, private Access URL when present, immutable Version URL when present, anonymous Claim URL and expiry when present, plus actionable diagnostics."];
226
+ readonly instructions: readonly ["Identify the actual publish root or build output. Run the real build first when the source tree is not itself the artifact to publish.", "Publish with `publish`. For an anonymous On-Device publish, keep the claim credential in local custody and return only the claim URL and expiry; never expose or ask the user to paste the raw token.", "Report the bare Live URL, private Access URL when present, immutable Version URL when present, anonymous Claim URL and expiry when present, plus actionable diagnostics."];
227
227
  readonly verification: readonly ["Call `status` and confirm the intended version is live. Use `diagnose` for a failed or stalled publish.", "Open the live URL and verify representative content and assets rather than treating a successful upload or health check as serving proof."];
228
228
  readonly runtimeNotes: {
229
229
  readonly local: "Publish a bounded local path; locally custodied claim credentials are reused automatically.";
@@ -369,13 +369,13 @@ export declare const SPACEFAST_PROMPT_SKILLS: ({
369
369
  export declare const SPACEFAST_RECIPE_SKILLS: (({
370
370
  readonly name: "create-and-publish-site";
371
371
  readonly title: "Create and publish a polished website";
372
- readonly description: "Turn a brief into a responsive static website, review the result, publish it to Spacefast, and verify the live version.";
372
+ readonly description: "Turn a brief into a responsive website, review the result, publish it to Spacefast, and verify the live version.";
373
373
  readonly whenToUse: "Use when the user wants an agent to design, build, improve, or launch a new website rather than only upload an existing build.";
374
374
  readonly keywords: readonly ["create", "website", "design", "build", "responsive", "launch", "publish"];
375
375
  readonly runtimes: readonly ["local", "hosted"];
376
376
  readonly tools: readonly ["publish", "execute"];
377
377
  readonly docs: readonly ["quickstart", "publishing"];
378
- readonly instructions: readonly ["Clarify the audience, goal, required content, brand constraints, and desired call to action. If the visual direction is open, propose two or three distinct directions and choose one with the user.", "Inspect the existing workspace before editing. Reuse its framework, package manager, components, assets, and build conventions when they are sound.", "Build a focused, accessible static experience with a clear hierarchy, real content, responsive layouts, useful interaction states, and no placeholder copy. Keep the main path simple and put secondary detail behind progressive disclosure.", "Run the real build and inspect the result at mobile and desktop widths. Fix overflow, illegible contrast, broken links, missing assets, keyboard traps, and avoidable layout shift.", "Review the workspace diff and summarize material design or content judgments before publishing. Never include API keys, claim credentials, upload credentials, or local auth state in the artifact.", "Publish the built output with `publish`. Present Open as the reusable access link and Claim as the one-time ownership link. Treat live and immutable-version URLs as receipt metadata, not extra actions."];
378
+ readonly instructions: readonly ["Clarify the audience, goal, required content, brand constraints, and desired call to action. If the visual direction is open, propose two or three distinct directions and choose one with the user.", "Inspect the existing workspace before editing. Reuse its framework, package manager, components, assets, and build conventions when they are sound.", "Build a focused, accessible experience with a clear hierarchy, real content, responsive layouts, useful interaction states, and no placeholder copy. Keep the main path simple and put secondary detail behind progressive disclosure.", "Run the real build and inspect the result at mobile and desktop widths. Fix overflow, illegible contrast, broken links, missing assets, keyboard traps, and avoidable layout shift.", "Review the workspace diff and summarize material design or content judgments before publishing. Never include API keys, claim credentials, upload credentials, or local auth state in the artifact.", "Publish the built output with `publish`. Present Open as the reusable access link and Claim as the one-time ownership link. Treat live and immutable-version URLs as receipt metadata, not extra actions."];
379
379
  readonly verification: readonly ["Call `status` after publishing and confirm the intended version is live; use `diagnose` if serving is stalled or failed.", "Open the live URL and verify the primary page, navigation, assets, responsive layout, and main call to action before claiming success."];
380
380
  readonly runtimeNotes: {
381
381
  readonly local: "On-Device can work directly inside the configured local workspace root and publish a local build path.";
@@ -390,13 +390,13 @@ export declare const SPACEFAST_RECIPE_SKILLS: (({
390
390
  } | {
391
391
  readonly name: "publish-and-verify";
392
392
  readonly title: "Publish and verify";
393
- readonly description: "Publish an existing static build, confirm the new version is actually live, and return the useful URLs without leaking credentials.";
393
+ readonly description: "Publish an existing build, confirm the new version is actually live, and return the useful URLs without leaking credentials.";
394
394
  readonly whenToUse: "Use when the artifact already exists and the user wants it published or updated.";
395
395
  readonly keywords: readonly ["publish", "deploy", "live", "verify", "version", "claim"];
396
396
  readonly runtimes: readonly ["local", "hosted"];
397
397
  readonly tools: readonly ["publish", "status", "diagnose"];
398
398
  readonly docs: readonly ["publishing", "workflow/publish-receipt", "workflow/secret-handling"];
399
- readonly instructions: readonly ["Identify the actual publish root or build output. Run the real build first when the source tree is not itself the static artifact.", "Publish with `publish`. For an anonymous On-Device publish, keep the claim credential in local custody and return only the claim URL and expiry; never expose or ask the user to paste the raw token.", "Report the bare Live URL, private Access URL when present, immutable Version URL when present, anonymous Claim URL and expiry when present, plus actionable diagnostics."];
399
+ readonly instructions: readonly ["Identify the actual publish root or build output. Run the real build first when the source tree is not itself the artifact to publish.", "Publish with `publish`. For an anonymous On-Device publish, keep the claim credential in local custody and return only the claim URL and expiry; never expose or ask the user to paste the raw token.", "Report the bare Live URL, private Access URL when present, immutable Version URL when present, anonymous Claim URL and expiry when present, plus actionable diagnostics."];
400
400
  readonly verification: readonly ["Call `status` and confirm the intended version is live. Use `diagnose` for a failed or stalled publish.", "Open the live URL and verify representative content and assets rather than treating a successful upload or health check as serving proof."];
401
401
  readonly runtimeNotes: {
402
402
  readonly local: "Publish a bounded local path; locally custodied claim credentials are reused automatically.";
@@ -5,7 +5,7 @@ export const SPACEFAST_SKILLS = [
5
5
  {
6
6
  name: "create-and-publish-site",
7
7
  title: "Create and publish a polished website",
8
- description: "Turn a brief into a responsive static website, review the result, publish it to Spacefast, and verify the live version.",
8
+ description: "Turn a brief into a responsive website, review the result, publish it to Spacefast, and verify the live version.",
9
9
  whenToUse: "Use when the user wants an agent to design, build, improve, or launch a new website rather than only upload an existing build.",
10
10
  keywords: ["create", "website", "design", "build", "responsive", "launch", "publish"],
11
11
  runtimes: BOTH_RUNTIMES,
@@ -14,7 +14,7 @@ export const SPACEFAST_SKILLS = [
14
14
  instructions: [
15
15
  "Clarify the audience, goal, required content, brand constraints, and desired call to action. If the visual direction is open, propose two or three distinct directions and choose one with the user.",
16
16
  "Inspect the existing workspace before editing. Reuse its framework, package manager, components, assets, and build conventions when they are sound.",
17
- "Build a focused, accessible static experience with a clear hierarchy, real content, responsive layouts, useful interaction states, and no placeholder copy. Keep the main path simple and put secondary detail behind progressive disclosure.",
17
+ "Build a focused, accessible experience with a clear hierarchy, real content, responsive layouts, useful interaction states, and no placeholder copy. Keep the main path simple and put secondary detail behind progressive disclosure.",
18
18
  "Run the real build and inspect the result at mobile and desktop widths. Fix overflow, illegible contrast, broken links, missing assets, keyboard traps, and avoidable layout shift.",
19
19
  "Review the workspace diff and summarize material design or content judgments before publishing. Never include API keys, claim credentials, upload credentials, or local auth state in the artifact.",
20
20
  "Publish the built output with `publish`. Present Open as the reusable access link and Claim as the one-time ownership link. Treat live and immutable-version URLs as receipt metadata, not extra actions.",
@@ -37,14 +37,14 @@ export const SPACEFAST_SKILLS = [
37
37
  {
38
38
  name: "publish-and-verify",
39
39
  title: "Publish and verify",
40
- description: "Publish an existing static build, confirm the new version is actually live, and return the useful URLs without leaking credentials.",
40
+ description: "Publish an existing build, confirm the new version is actually live, and return the useful URLs without leaking credentials.",
41
41
  whenToUse: "Use when the artifact already exists and the user wants it published or updated.",
42
42
  keywords: ["publish", "deploy", "live", "verify", "version", "claim"],
43
43
  runtimes: BOTH_RUNTIMES,
44
44
  tools: ["publish", "status", "diagnose"],
45
45
  docs: ["publishing", "workflow/publish-receipt", "workflow/secret-handling"],
46
46
  instructions: [
47
- "Identify the actual publish root or build output. Run the real build first when the source tree is not itself the static artifact.",
47
+ "Identify the actual publish root or build output. Run the real build first when the source tree is not itself the artifact to publish.",
48
48
  "Publish with `publish`. For an anonymous On-Device publish, keep the claim credential in local custody and return only the claim URL and expiry; never expose or ask the user to paste the raw token.",
49
49
  "Report the bare Live URL, private Access URL when present, immutable Version URL when present, anonymous Claim URL and expiry when present, plus actionable diagnostics.",
50
50
  ],
@@ -0,0 +1,16 @@
1
+ /** The original fetch implementation captured before a test installs a stub. */
2
+ export declare const realFetch: typeof fetch;
3
+ export type FetchStubHandler = (request: Request) => Response | Promise<Response>;
4
+ export declare function stubFetch(handler: FetchStubHandler): void;
5
+ export declare function restoreFetch(): void;
6
+ /** Build the RFC 9457 problem response served by Spacefast APIs. */
7
+ export declare function problemResponse(input: {
8
+ code: string;
9
+ status: number;
10
+ detail: string;
11
+ pointer?: string;
12
+ details?: unknown;
13
+ suggestions?: readonly string[];
14
+ next?: readonly string[];
15
+ requestId?: string;
16
+ }): Response;
@@ -0,0 +1,25 @@
1
+ import { errorDocsUrl, errorTitle, PROBLEM_CONTENT_TYPE } from "../contracts/error-codes.js";
2
+ /** The original fetch implementation captured before a test installs a stub. */
3
+ export const realFetch = globalThis.fetch;
4
+ export function stubFetch(handler) {
5
+ // SAFETY: The wrapper accepts fetch's parameter types and always resolves to the handler's Response.
6
+ globalThis.fetch = (async (input, init) => handler(input instanceof Request && init === undefined ? input : new Request(input, init)));
7
+ }
8
+ export function restoreFetch() {
9
+ globalThis.fetch = realFetch;
10
+ }
11
+ /** Build the RFC 9457 problem response served by Spacefast APIs. */
12
+ export function problemResponse(input) {
13
+ return new Response(JSON.stringify({
14
+ type: errorDocsUrl(input.code),
15
+ title: errorTitle(input.code),
16
+ status: input.status,
17
+ detail: input.detail,
18
+ code: input.code,
19
+ ...(input.pointer === undefined ? {} : { pointer: input.pointer }),
20
+ ...(input.details === undefined ? {} : { details: input.details }),
21
+ ...(input.suggestions === undefined ? {} : { suggestions: input.suggestions }),
22
+ ...(input.next === undefined ? {} : { next: input.next }),
23
+ requestId: input.requestId ?? "req_test",
24
+ }), { status: input.status, headers: { "content-type": PROBLEM_CONTENT_TYPE } });
25
+ }
@@ -1,3 +1,4 @@
1
+ import { type SpaceCronConfig } from "../contracts/space-config.js";
1
2
  export type BuildSettingsInput = {
2
3
  fileNames: Iterable<string>;
3
4
  packageJson?: string | null | undefined;
@@ -24,7 +25,7 @@ export type BuildAnalyzerCandidateRankInput = {
24
25
  rootDirectory: string | null;
25
26
  fileNames: Iterable<string>;
26
27
  };
27
- export declare function hasWorkspaceSignal(fileNames: Iterable<string>, packageJsonText?: string | null): unknown;
28
+ export declare function hasWorkspaceSignal(fileNames: Iterable<string>, packageJsonText?: string | null): boolean;
28
29
  export declare function rankBuildAnalyzerCandidate(input: BuildAnalyzerCandidateRankInput): {
29
30
  confidence: number;
30
31
  reasons: string[];
@@ -43,6 +44,7 @@ export type PlatformConfig = {
43
44
  redirects: Array<Record<string, unknown>>;
44
45
  rewrites: Array<Record<string, unknown>>;
45
46
  headers: Array<Record<string, unknown>>;
47
+ crons: Array<Record<string, unknown>>;
46
48
  unsupportedFeatures: string[];
47
49
  };
48
50
  export declare function stringField(value: Record<string, unknown> | null, key: string): string | null;
@@ -51,7 +53,9 @@ export declare function detectPlatformConfigs(input: Omit<BuildSettingsInput, "f
51
53
  export type PlatformRoutingConversion = {
52
54
  redirectLines: string[];
53
55
  headerBlocks: string[];
56
+ crons: SpaceCronConfig[];
54
57
  notes: string[];
55
58
  };
56
59
  export declare function convertPlatformRouting(configs: PlatformConfig[]): PlatformRoutingConversion;
60
+ export declare const BUILD_MARKER_FILE_NAMES: Set<string>;
57
61
  export declare function detectBuildSettings(input: BuildSettingsInput): DetectedBuildSettings;
@@ -1,5 +1,6 @@
1
1
  import { load as parseYaml } from "js-yaml";
2
2
  import { parse as parseToml } from "smol-toml";
3
+ import { SPACE_CONFIG_CAPS, spaceCronKey, spaceCronSchema, } from "../contracts/space-config.js";
3
4
  export const BUILD_ANALYZER_CANDIDATE_ROOTS = ["apps", "packages", "sites", "docs"];
4
5
  const PACKAGE_MANAGER_ONLY_FILES = new Set([
5
6
  "package.json",
@@ -8,15 +9,19 @@ const PACKAGE_MANAGER_ONLY_FILES = new Set([
8
9
  "pnpm-lock.yaml",
9
10
  "yarn.lock",
10
11
  "package-lock.json",
12
+ "npm-shrinkwrap.json",
11
13
  "Gemfile",
12
14
  "Gemfile.lock",
13
15
  ]);
16
+ const WORKSPACE_ROOT_FILES = new Set([
17
+ "pnpm-workspace.yaml",
18
+ "turbo.json",
19
+ "nx.json",
20
+ "lerna.json",
21
+ ]);
14
22
  export function hasWorkspaceSignal(fileNames, packageJsonText) {
15
23
  const names = new Set(fileNames);
16
- if (names.has("pnpm-workspace.yaml") ||
17
- names.has("turbo.json") ||
18
- names.has("nx.json") ||
19
- names.has("lerna.json")) {
24
+ if ([...WORKSPACE_ROOT_FILES].some((file) => names.has(file))) {
20
25
  return true;
21
26
  }
22
27
  if (!packageJsonText) {
@@ -24,10 +29,14 @@ export function hasWorkspaceSignal(fileNames, packageJsonText) {
24
29
  }
25
30
  try {
26
31
  const parsed = JSON.parse(packageJsonText);
27
- return (Array.isArray(parsed.workspaces) ||
28
- (parsed.workspaces &&
29
- typeof parsed.workspaces === "object" &&
30
- Array.isArray(parsed.workspaces.packages)));
32
+ if (Array.isArray(parsed.workspaces)) {
33
+ return true;
34
+ }
35
+ if (!parsed.workspaces || typeof parsed.workspaces !== "object") {
36
+ return false;
37
+ }
38
+ // SAFETY: The preceding non-null object check proves workspaces can carry an optional packages property.
39
+ return Array.isArray(parsed.workspaces.packages);
31
40
  }
32
41
  catch {
33
42
  return false;
@@ -77,6 +86,7 @@ function parseJsonObject(value) {
77
86
  }
78
87
  try {
79
88
  const parsed = JSON.parse(value);
89
+ // SAFETY: The conditional establishes parsed is a non-null, non-array object before returning it as a dictionary.
80
90
  return parsed && typeof parsed === "object" && !Array.isArray(parsed)
81
91
  ? parsed
82
92
  : null;
@@ -97,6 +107,7 @@ function parseTomlObject(value) {
97
107
  }
98
108
  try {
99
109
  const parsed = parseToml(value);
110
+ // SAFETY: The conditional establishes parsed is a non-null, non-array object before returning it as a dictionary.
100
111
  return parsed && typeof parsed === "object" && !Array.isArray(parsed)
101
112
  ? parsed
102
113
  : null;
@@ -111,6 +122,7 @@ function parseYamlObject(value) {
111
122
  }
112
123
  try {
113
124
  const parsed = parseYaml(value);
125
+ // SAFETY: The conditional establishes parsed is a non-null, non-array object before returning it as a dictionary.
114
126
  return parsed && typeof parsed === "object" && !Array.isArray(parsed)
115
127
  ? parsed
116
128
  : null;
@@ -129,6 +141,7 @@ export function numberField(value, key) {
129
141
  }
130
142
  function objectField(value, key) {
131
143
  const field = value?.[key];
144
+ // SAFETY: The conditional establishes field is a non-null, non-array object before returning it as a dictionary.
132
145
  return field && typeof field === "object" && !Array.isArray(field)
133
146
  ? field
134
147
  : null;
@@ -154,11 +167,11 @@ function vercelConfig(value) {
154
167
  redirects: arrayField(config, "redirects"),
155
168
  rewrites: arrayField(config, "rewrites"),
156
169
  headers: arrayField(config, "headers"),
170
+ crons: arrayField(config, "crons"),
157
171
  unsupportedFeatures: [
158
172
  ...("functions" in config ? ["Vercel functions are not converted."] : []),
159
173
  ...("middleware" in config ? ["Vercel middleware is not converted."] : []),
160
174
  ...("images" in config ? ["Vercel image optimization config is not converted."] : []),
161
- ...("crons" in config ? ["Vercel cron jobs are not converted."] : []),
162
175
  ],
163
176
  };
164
177
  }
@@ -178,6 +191,9 @@ function netlifyConfig(value) {
178
191
  redirects: arrayField(config, "redirects"),
179
192
  rewrites: [],
180
193
  headers: arrayField(config, "headers"),
194
+ // Netlify has no scheduled-request config: its scheduled functions are
195
+ // declared in function code, which this importer does not read.
196
+ crons: [],
181
197
  unsupportedFeatures: [
182
198
  ...("functions" in config ? ["Netlify functions are not converted."] : []),
183
199
  ...("edge_functions" in config ? ["Netlify edge functions are not converted."] : []),
@@ -223,6 +239,7 @@ function cloudflarePagesConfig(input) {
223
239
  redirects: [],
224
240
  rewrites: [],
225
241
  headers: [],
242
+ crons: [],
226
243
  unsupportedFeatures,
227
244
  };
228
245
  }
@@ -386,6 +403,7 @@ function convertVercelHeaderRule(rule, out) {
386
403
  const entries = rule.headers;
387
404
  const headers = (Array.isArray(entries) ? entries : [])
388
405
  .map((entry) => {
406
+ // SAFETY: The conditional establishes entry is a non-null object before reading dictionary properties.
389
407
  const header = entry && typeof entry === "object" ? entry : null;
390
408
  const key = stringField(header, "key");
391
409
  const value = header && typeof header.value === "string" ? header.value : null;
@@ -424,8 +442,47 @@ function convertNetlifyHeaderRule(rule, out) {
424
442
  }
425
443
  out.headerBlocks.push(headerBlock(matcher, headers));
426
444
  }
445
+ /**
446
+ * One `vercel.json` cron entry into an `sf.jsonc` `crons` entry.
447
+ *
448
+ * Both sides are `{ path, schedule }` over a literal path and a five-field
449
+ * crontab expression, so a valid Vercel cron converts verbatim — there is no
450
+ * translation to get wrong. What this guards is the tail: Vercel accepts paths
451
+ * and schedules our grammar does not, and it has no per-project cap the way we
452
+ * do. Every rejection names the entry, because a scheduled job that silently
453
+ * stops existing is the one migration failure nobody notices until the work
454
+ * has not run for a week.
455
+ */
456
+ function convertVercelCron(rule, out) {
457
+ const path = stringField(rule, "path");
458
+ const schedule = stringField(rule, "schedule");
459
+ const label = `vercel.json cron ${path ?? "(missing path)"}`;
460
+ if (!path || !schedule) {
461
+ out.notes.push(`${label}: skipped — missing path or schedule.`);
462
+ return;
463
+ }
464
+ const parsed = spaceCronSchema.safeParse({ path, schedule });
465
+ if (!parsed.success) {
466
+ out.notes.push(`${label}: skipped — ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
467
+ return;
468
+ }
469
+ if (out.crons.some((entry) => spaceCronKey(entry.path) === spaceCronKey(path))) {
470
+ out.notes.push(`${label}: skipped — another cron already schedules this path.`);
471
+ return;
472
+ }
473
+ if (out.crons.length >= SPACE_CONFIG_CAPS.crons) {
474
+ out.notes.push(`${label}: skipped — at most ${SPACE_CONFIG_CAPS.crons} crons are supported.`);
475
+ return;
476
+ }
477
+ out.crons.push(parsed.data);
478
+ }
427
479
  export function convertPlatformRouting(configs) {
428
- const out = { redirectLines: [], headerBlocks: [], notes: [] };
480
+ const out = {
481
+ redirectLines: [],
482
+ headerBlocks: [],
483
+ crons: [],
484
+ notes: [],
485
+ };
429
486
  for (const config of configs) {
430
487
  if (config.platform === "vercel") {
431
488
  for (const rule of config.redirects) {
@@ -437,6 +494,9 @@ export function convertPlatformRouting(configs) {
437
494
  for (const rule of config.headers) {
438
495
  convertVercelHeaderRule(rule, out);
439
496
  }
497
+ for (const rule of config.crons) {
498
+ convertVercelCron(rule, out);
499
+ }
440
500
  }
441
501
  if (config.platform === "netlify") {
442
502
  // Netlify expresses rewrites as 200-status redirects; both arrive in
@@ -670,6 +730,19 @@ const FRAMEWORKS = [
670
730
  defaultBuildCommand: "bundle exec middleman build",
671
731
  },
672
732
  ];
733
+ // Every file name the build analyzer keys on: package manifests, lockfiles,
734
+ // workspace roots, and the platform and framework configs it detects a build
735
+ // from. An archive carrying any of them is a project to build, not a website.
736
+ export const BUILD_MARKER_FILE_NAMES = new Set([
737
+ ...PACKAGE_MANAGER_ONLY_FILES,
738
+ ...WORKSPACE_ROOT_FILES,
739
+ "vercel.json",
740
+ "netlify.toml",
741
+ "wrangler.toml",
742
+ "wrangler.json",
743
+ "wrangler.jsonc",
744
+ ...FRAMEWORKS.flatMap((framework) => framework.configFiles ?? []),
745
+ ]);
673
746
  function frameworkForPreset(frameworkPreset) {
674
747
  return FRAMEWORKS.find((framework) => framework.preset === frameworkPreset) ?? null;
675
748
  }
@@ -711,7 +784,7 @@ function jekyllBuildCommand(files) {
711
784
  function frameworkInstallCommand(frameworkPreset, files) {
712
785
  return frameworkPreset === "jekyll" ? jekyllInstallCommand(files) : null;
713
786
  }
714
- function installCommandForManager(packageManager, packageJson) {
787
+ function installCommandForManager(packageManager, packageJson, files) {
715
788
  switch (packageManager) {
716
789
  case "bun":
717
790
  return "bun install --frozen-lockfile";
@@ -720,7 +793,11 @@ function installCommandForManager(packageManager, packageJson) {
720
793
  case "yarn":
721
794
  return "corepack enable && (yarn install --immutable || yarn install --frozen-lockfile)";
722
795
  default:
723
- return packageJson ? "npm ci" : null;
796
+ if (!packageJson)
797
+ return null;
798
+ return files.has("package-lock.json") || files.has("npm-shrinkwrap.json")
799
+ ? "npm ci"
800
+ : "npm install";
724
801
  }
725
802
  }
726
803
  function buildCommandForDefaults(packageJson, run, frameworkPreset, files) {
@@ -740,7 +817,7 @@ function packageDefaults(input, files) {
740
817
  const run = packageManager === "npm" ? "npm run" : packageManager === "bun" ? "bun run" : packageManager;
741
818
  const frameworkPreset = detectFrameworkPreset(files, dependencies);
742
819
  const installCommands = [
743
- installCommandForManager(packageManager, packageJson),
820
+ installCommandForManager(packageManager, packageJson, files),
744
821
  frameworkInstallCommand(frameworkPreset, files),
745
822
  ].filter((command) => Boolean(command));
746
823
  const jekyllConfig = input.jekyllYaml
@@ -768,6 +845,7 @@ export function detectBuildSettings(input) {
768
845
  files.has("pnpm-lock.yaml") ? "pnpm-lock.yaml" : null,
769
846
  files.has("yarn.lock") ? "yarn.lock" : null,
770
847
  files.has("package-lock.json") ? "package-lock.json" : null,
848
+ files.has("npm-shrinkwrap.json") ? "npm-shrinkwrap.json" : null,
771
849
  files.has("Gemfile") ? "Gemfile" : null,
772
850
  files.has("Gemfile.lock") ? "Gemfile.lock" : null,
773
851
  files.has("_config.yml") ? "_config.yml" : null,
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Recursively key-sorted JSON, so a value round-tripped through Postgres jsonb
3
+ * (which canonicalizes key order) digests identically to one freshly built in
4
+ * JS. Arrays keep their original order since order is meaningful there.
5
+ *
6
+ * Every digest that has to agree across the control plane, the CLI and the
7
+ * capsule compiler is built on this exact spelling — change it and stored
8
+ * hashes stop matching freshly computed ones.
9
+ */
10
+ export declare function canonicalJson(value: unknown): string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Recursively key-sorted JSON, so a value round-tripped through Postgres jsonb
3
+ * (which canonicalizes key order) digests identically to one freshly built in
4
+ * JS. Arrays keep their original order since order is meaningful there.
5
+ *
6
+ * Every digest that has to agree across the control plane, the CLI and the
7
+ * capsule compiler is built on this exact spelling — change it and stored
8
+ * hashes stop matching freshly computed ones.
9
+ */
10
+ export function canonicalJson(value) {
11
+ if (Array.isArray(value)) {
12
+ return `[${value.map(canonicalJson).join(",")}]`;
13
+ }
14
+ if (value && typeof value === "object") {
15
+ // SAFETY: This branch establishes value is a non-null object before enumerating its entries.
16
+ return `{${Object.entries(value)
17
+ .toSorted(([left], [right]) => left.localeCompare(right))
18
+ .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
19
+ .join(",")}}`;
20
+ }
21
+ return JSON.stringify(value);
22
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * How a client addresses Cast over Phoenix. Both Spacefast clients — the
3
+ * dashboard's notice subscription and the injected Collab SDK — build their
4
+ * socket and their topics here, and nowhere else.
5
+ */
6
+ /**
7
+ * The Phoenix topic a Cast room's messages travel on.
8
+ *
9
+ * Phoenix routes a broadcast by the *literal* topic string. Cast broadcasts
10
+ * anything computed outside a channel process — Space notices, REST-published
11
+ * room events — to the topic `Cast.RoomTopic.for/2` builds, so a client that
12
+ * encodes the room key any other way joins a real and permanently silent room.
13
+ * Both sides therefore encode exactly once, here and in that module: the `~n`
14
+ * table first (it survives round-tripping a key that itself contains `~` or
15
+ * `%`), then `encodeURIComponent`.
16
+ *
17
+ * Proven by delivery, not by inspection: `./cast-transport.test.ts` pins the
18
+ * strings, and on the other side `room_channel_notice_test.exs` and
19
+ * `spacefast_inbox_fanout_test.exs` join those literal topics and assert a real
20
+ * Cast broadcast arrives on them.
21
+ */
22
+ export declare function castRoomTopic(resourceKey: string, roomKey: string): string;
23
+ /**
24
+ * Phoenix's own `Socket` appends `/websocket` to whatever endpoint it is given,
25
+ * so a configured URL that already carries the suffix would otherwise become
26
+ * `/websocket/websocket`.
27
+ */
28
+ export declare function phoenixEndpoint(url: string): string;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * How a client addresses Cast over Phoenix. Both Spacefast clients — the
3
+ * dashboard's notice subscription and the injected Collab SDK — build their
4
+ * socket and their topics here, and nowhere else.
5
+ */
6
+ /**
7
+ * `~n` escapes, the inverse of `CastWeb.ApiContext.decode_path_segment/1`.
8
+ * `~` goes first in intent: the map is applied in one pass, so an already
9
+ * escaped marker can never be escaped twice.
10
+ */
11
+ const ESCAPED = new Map([
12
+ ["~", "~0"],
13
+ ["/", "~1"],
14
+ [":", "~2"],
15
+ ["\\", "~3"],
16
+ ["%", "~4"],
17
+ ["\0", "~5"],
18
+ ]);
19
+ /**
20
+ * The Phoenix topic a Cast room's messages travel on.
21
+ *
22
+ * Phoenix routes a broadcast by the *literal* topic string. Cast broadcasts
23
+ * anything computed outside a channel process — Space notices, REST-published
24
+ * room events — to the topic `Cast.RoomTopic.for/2` builds, so a client that
25
+ * encodes the room key any other way joins a real and permanently silent room.
26
+ * Both sides therefore encode exactly once, here and in that module: the `~n`
27
+ * table first (it survives round-tripping a key that itself contains `~` or
28
+ * `%`), then `encodeURIComponent`.
29
+ *
30
+ * Proven by delivery, not by inspection: `./cast-transport.test.ts` pins the
31
+ * strings, and on the other side `room_channel_notice_test.exs` and
32
+ * `spacefast_inbox_fanout_test.exs` join those literal topics and assert a real
33
+ * Cast broadcast arrives on them.
34
+ */
35
+ export function castRoomTopic(resourceKey, roomKey) {
36
+ let escaped = "";
37
+ for (const character of roomKey)
38
+ escaped += ESCAPED.get(character) ?? character;
39
+ return `room:${resourceKey}:${encodeURIComponent(escaped)}`;
40
+ }
41
+ /**
42
+ * Phoenix's own `Socket` appends `/websocket` to whatever endpoint it is given,
43
+ * so a configured URL that already carries the suffix would otherwise become
44
+ * `/websocket/websocket`.
45
+ */
46
+ export function phoenixEndpoint(url) {
47
+ const parsed = new URL(url);
48
+ if (parsed.pathname.endsWith("/websocket")) {
49
+ parsed.pathname = parsed.pathname.slice(0, -"/websocket".length);
50
+ }
51
+ return parsed.href.replace(/\/$/, "");
52
+ }
@@ -1,9 +1 @@
1
- export type ClaimCredential = {
2
- kind: "token";
3
- claimToken: string;
4
- } | {
5
- kind: "nonce";
6
- nonce: string;
7
- };
8
- export declare function classifyClaimCredential(raw: string | null | undefined): ClaimCredential | undefined;
9
1
  export declare function normalizeClaimToken(token: string | null | undefined): string | undefined;
@@ -1,18 +1,6 @@
1
- const LEGACY_WORD_TOKEN_SHAPE = /^[a-z]+(?:-[a-z]+){2,}$/;
2
- export function classifyClaimCredential(raw) {
3
- const trimmed = raw?.trim();
4
- if (!trimmed) {
5
- return undefined;
6
- }
7
- if (trimmed.startsWith("sfc_")) {
8
- return { kind: "token", claimToken: trimmed };
9
- }
10
- const normalized = normalizeClaimToken(trimmed);
11
- if (normalized && LEGACY_WORD_TOKEN_SHAPE.test(normalized)) {
12
- return { kind: "token", claimToken: normalized };
13
- }
14
- return { kind: "nonce", nonce: trimmed };
15
- }
1
+ // The space key (`sfc_…`, or the legacy five-word form) is the one claim
2
+ // credential; it authenticates as a `Bearer` header and rides claim-link
3
+ // fragments verbatim.
16
4
  export function normalizeClaimToken(token) {
17
5
  const trimmed = token?.trim();
18
6
  if (!trimmed) {
@@ -10,6 +10,7 @@ export async function mapConcurrent(items, limit, mapper) {
10
10
  if (index >= items.length) {
11
11
  return;
12
12
  }
13
+ // SAFETY: the bounds check above proves this index addresses an item in the input array.
13
14
  results[index] = await mapper(items[index], index);
14
15
  await worker();
15
16
  }
@@ -16,7 +16,7 @@ export type PolicyAuthorizeInput = {
16
16
  resource: string | readonly string[];
17
17
  conditions?: RequestCondition;
18
18
  };
19
- export declare const publishActions: ("spaces:read" | "spaces:write" | "spaces:create" | "spaces:publish" | "versions:read" | "versions:promote" | "versions:download" | "builds:read" | "builds:trigger" | "builds:create")[];
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")[];
20
20
  export declare function compileApiKeyPreset(preset: ApiKeyPreset, input: PolicyCompileInput): ApiCredentialPolicy;
21
21
  /** Expand a named API-key preset into the finite permissions sent on the public wire contract. */
22
22
  export declare function apiKeyPresetPermissions(preset: ApiKeyPreset): StatticAction[];