@spacefast/common 0.0.12 → 0.0.17

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 (189) hide show
  1. package/dist/agents/client-registry.d.ts +22 -0
  2. package/dist/agents/client-registry.js +41 -0
  3. package/dist/agents/connect-targets.d.ts +51 -6
  4. package/dist/agents/connect-targets.js +93 -28
  5. package/dist/brand.d.ts +0 -11
  6. package/dist/brand.js +0 -20
  7. package/dist/config/domains.d.ts +15 -6
  8. package/dist/config/domains.js +24 -10
  9. package/dist/config/postgres-budget.d.ts +3 -0
  10. package/dist/config/postgres-budget.js +6 -1
  11. package/dist/config/postgres-budget.json +2 -1
  12. package/dist/contracts/abuse.js +1 -1
  13. package/dist/contracts/access.d.ts +486 -1196
  14. package/dist/contracts/access.js +323 -1054
  15. package/dist/contracts/activity.d.ts +1 -1
  16. package/dist/contracts/activity.js +11 -15
  17. package/dist/contracts/api-keys.d.ts +16 -0
  18. package/dist/contracts/api-keys.js +11 -9
  19. package/dist/contracts/archives.d.ts +1 -2
  20. package/dist/contracts/archives.js +15 -17
  21. package/dist/contracts/beta.js +1 -1
  22. package/dist/contracts/builds.js +14 -14
  23. package/dist/contracts/channels.js +2 -2
  24. package/dist/contracts/comments.d.ts +143 -99
  25. package/dist/contracts/comments.js +194 -79
  26. package/dist/contracts/common.d.ts +12 -12
  27. package/dist/contracts/common.js +33 -30
  28. package/dist/contracts/continuation.js +5 -5
  29. package/dist/contracts/countries.js +2 -7
  30. package/dist/contracts/data-source-routes.d.ts +20 -0
  31. package/dist/contracts/data-source-routes.js +15 -0
  32. package/dist/contracts/device-auth.d.ts +13 -9
  33. package/dist/contracts/device-auth.js +12 -12
  34. package/dist/contracts/docs.d.ts +10 -10
  35. package/dist/contracts/domains.js +39 -33
  36. package/dist/contracts/enums.d.ts +4 -14
  37. package/dist/contracts/enums.js +3 -10
  38. package/dist/contracts/error-code-meta.d.ts +122 -56
  39. package/dist/contracts/error-code-meta.js +43 -19
  40. package/dist/contracts/error-codes.d.ts +7 -4
  41. package/dist/contracts/error-codes.js +48 -24
  42. package/dist/contracts/events.d.ts +22 -15
  43. package/dist/contracts/events.js +33 -5
  44. package/dist/contracts/execution.d.ts +238 -0
  45. package/dist/contracts/execution.js +309 -0
  46. package/dist/contracts/feature-lifecycle.d.ts +39 -188
  47. package/dist/contracts/feature-lifecycle.js +52 -196
  48. package/dist/contracts/features.d.ts +13 -115
  49. package/dist/contracts/features.js +5 -35
  50. package/dist/contracts/functions.d.ts +597 -0
  51. package/dist/contracts/functions.js +475 -0
  52. package/dist/contracts/grants.d.ts +1175 -0
  53. package/dist/contracts/grants.js +783 -0
  54. package/dist/contracts/ids.d.ts +7 -5
  55. package/dist/contracts/ids.js +7 -7
  56. package/dist/contracts/internal.js +2 -2
  57. package/dist/contracts/mcp.d.ts +17 -556
  58. package/dist/contracts/mcp.js +9 -103
  59. package/dist/contracts/me.js +3 -4
  60. package/dist/contracts/notifications.d.ts +108 -0
  61. package/dist/contracts/notifications.js +54 -0
  62. package/dist/contracts/oauth-resources.d.ts +22 -5
  63. package/dist/contracts/oauth-resources.js +23 -2
  64. package/dist/contracts/operations.d.ts +0 -48
  65. package/dist/contracts/operations.js +0 -19
  66. package/dist/contracts/pages.d.ts +10 -14
  67. package/dist/contracts/pages.js +17 -21
  68. package/dist/contracts/plan-policy.d.ts +1 -8
  69. package/dist/contracts/plan-policy.js +1 -4
  70. package/dist/contracts/platform.d.ts +78 -30
  71. package/dist/contracts/platform.js +69 -20
  72. package/dist/contracts/problem-document.d.ts +14 -0
  73. package/dist/contracts/problem-document.js +35 -0
  74. package/dist/contracts/push-new.d.ts +41 -2
  75. package/dist/contracts/push-new.js +4 -0
  76. package/dist/contracts/quotas.d.ts +0 -1
  77. package/dist/contracts/quotas.js +0 -5
  78. package/dist/contracts/repository-connections.d.ts +1 -1
  79. package/dist/contracts/resources.d.ts +16 -71
  80. package/dist/contracts/resources.js +13 -16
  81. package/dist/contracts/runtime-api.d.ts +666 -303
  82. package/dist/contracts/runtime-api.js +272 -113
  83. package/dist/contracts/runtime-app.d.ts +168 -0
  84. package/dist/contracts/runtime-app.js +42 -0
  85. package/dist/contracts/runtime-db.d.ts +424 -0
  86. package/dist/contracts/runtime-db.js +108 -0
  87. package/dist/contracts/runtime-storage.d.ts +120 -0
  88. package/dist/contracts/runtime-storage.js +40 -0
  89. package/dist/contracts/sf-config-v1.d.ts +54 -1
  90. package/dist/contracts/sf-config-v1.js +2 -2
  91. package/dist/contracts/sites.d.ts +6 -0
  92. package/dist/contracts/sites.js +30 -11
  93. package/dist/contracts/space-config.d.ts +91 -121
  94. package/dist/contracts/space-config.js +464 -42
  95. package/dist/contracts/spaces.d.ts +300 -171
  96. package/dist/contracts/spaces.js +128 -98
  97. package/dist/contracts/superadmin-emails.d.ts +2 -2
  98. package/dist/contracts/superadmin-spaces.d.ts +26 -130
  99. package/dist/contracts/superadmin-spaces.js +22 -0
  100. package/dist/contracts/superadmin-tenants.js +4 -4
  101. package/dist/contracts/superadmin.d.ts +4 -0
  102. package/dist/contracts/superadmin.js +14 -1
  103. package/dist/contracts/tags.d.ts +6 -6
  104. package/dist/contracts/tags.js +1 -1
  105. package/dist/contracts/teams.d.ts +9 -9
  106. package/dist/contracts/teams.js +25 -16
  107. package/dist/contracts/theme-json.js +1 -1
  108. package/dist/contracts/transfers.d.ts +0 -1
  109. package/dist/contracts/transfers.js +4 -4
  110. package/dist/contracts/variables.js +3 -3
  111. package/dist/contracts/zero.d.ts +384 -596
  112. package/dist/contracts/zero.js +167 -299
  113. package/dist/dashboard-paths/index.d.ts +18 -3
  114. package/dist/dashboard-paths/index.js +19 -0
  115. package/dist/docs/agent-handoff-document.d.ts +2 -2
  116. package/dist/docs/agent-handoff-document.js +8 -7
  117. package/dist/docs/agent-prose.d.ts +22 -3
  118. package/dist/docs/agent-prose.js +227 -55
  119. package/dist/docs/agent-setup.d.ts +14 -11
  120. package/dist/docs/agent-setup.js +289 -61
  121. package/dist/docs/agent-solutions.d.ts +6 -6
  122. package/dist/docs/agent-solutions.js +24 -22
  123. package/dist/docs/catalog.d.ts +53 -98
  124. package/dist/docs/catalog.js +90 -117
  125. package/dist/docs/error-docs.js +304 -236
  126. package/dist/docs/skill-distribution.d.ts +2 -0
  127. package/dist/docs/skill-distribution.js +7 -4
  128. package/dist/docs/skills.d.ts +546 -0
  129. package/dist/docs/skills.js +340 -0
  130. package/dist/utils/auth-redirect.d.ts +1 -3
  131. package/dist/utils/auth-redirect.js +1 -21
  132. package/dist/utils/browser-credential.d.ts +19 -0
  133. package/dist/utils/browser-credential.js +62 -0
  134. package/dist/utils/build-settings.d.ts +2 -0
  135. package/dist/utils/build-settings.js +61 -9
  136. package/dist/utils/comment-avatars.d.ts +7 -0
  137. package/dist/utils/comment-avatars.js +42 -0
  138. package/dist/utils/content-type.d.ts +3 -3
  139. package/dist/utils/content-type.js +38 -61
  140. package/dist/utils/credential-policy.d.ts +1 -1
  141. package/dist/utils/credential-policy.js +4 -0
  142. package/dist/utils/domain-diagnostics.d.ts +5 -5
  143. package/dist/utils/domain-diagnostics.js +21 -10
  144. package/dist/utils/egress-policy.fixtures.json +1 -1
  145. package/dist/utils/grants.d.ts +53 -0
  146. package/dist/utils/grants.js +160 -0
  147. package/dist/utils/local-space-state.d.ts +78 -4
  148. package/dist/utils/local-space-state.js +384 -82
  149. package/dist/utils/one-shot-replay.d.ts +14 -0
  150. package/dist/utils/one-shot-replay.js +21 -0
  151. package/dist/utils/page-preview.d.ts +7 -0
  152. package/dist/utils/page-preview.js +9 -0
  153. package/dist/utils/pages.d.ts +2 -5
  154. package/dist/utils/pages.js +64 -33
  155. package/dist/utils/publish-form-data.d.ts +22 -0
  156. package/dist/utils/publish-form-data.js +26 -0
  157. package/dist/utils/publish-policy.d.ts +34 -4
  158. package/dist/utils/publish-policy.js +149 -62
  159. package/dist/utils/query-keys.d.ts +8 -10
  160. package/dist/utils/query-keys.js +14 -10
  161. package/dist/utils/runtime-paths.d.ts +3 -0
  162. package/dist/utils/runtime-paths.js +7 -0
  163. package/dist/utils/secure-local-file.d.ts +73 -0
  164. package/dist/utils/secure-local-file.js +1373 -0
  165. package/dist/utils/space-config.d.ts +17 -4
  166. package/dist/utils/space-config.js +151 -8
  167. package/dist/utils/static-runtime-policy.d.ts +5 -0
  168. package/dist/utils/static-runtime-policy.js +54 -10
  169. package/dist/utils/storage-policy.d.ts +20 -0
  170. package/dist/utils/storage-policy.fixtures.json +174 -0
  171. package/dist/utils/storage-policy.js +193 -0
  172. package/dist/utils/upload-session.d.ts +15 -1
  173. package/dist/utils/upload-session.js +68 -2
  174. package/dist/utils/version-file-browser.d.ts +13 -0
  175. package/dist/utils/version-file-browser.js +59 -0
  176. package/package.json +20 -1
  177. package/dist/contracts/mounts.d.ts +0 -142
  178. package/dist/contracts/mounts.js +0 -56
  179. package/dist/utils/access-grant.d.ts +0 -14
  180. package/dist/utils/access-grant.js +0 -88
  181. package/dist/utils/access-match.d.ts +0 -46
  182. package/dist/utils/access-match.fixtures.json +0 -513
  183. package/dist/utils/access-match.js +0 -312
  184. package/dist/utils/access-verdict.fixtures.json +0 -421
  185. package/dist/utils/asset-fingerprint.d.ts +0 -53
  186. package/dist/utils/asset-fingerprint.js +0 -574
  187. package/dist/utils/sf-config-v1.d.ts +0 -2
  188. package/dist/utils/sf-config-v1.fixtures.json +0 -9
  189. package/dist/utils/sf-config-v1.js +0 -551
@@ -0,0 +1,340 @@
1
+ import { DOCS_BASE_URL } from "../config/domains.js";
2
+ const BOTH_RUNTIMES = ["local", "hosted"];
3
+ /** Canonical agent workflows shared by MCP tools, prompts, resources, and recipe docs. */
4
+ export const SPACEFAST_SKILLS = [
5
+ {
6
+ name: "create-and-publish-site",
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.",
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
+ keywords: ["create", "website", "design", "build", "responsive", "launch", "publish"],
11
+ runtimes: BOTH_RUNTIMES,
12
+ tools: ["publish", "execute"],
13
+ docs: ["quickstart", "publishing"],
14
+ instructions: [
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
+ "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.",
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
+ "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
+ "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.",
21
+ ],
22
+ verification: [
23
+ "Call `status` after publishing and confirm the intended version is live; use `diagnose` if serving is stalled or failed.",
24
+ "Open the live URL and verify the primary page, navigation, assets, responsive layout, and main call to action before claiming success.",
25
+ ],
26
+ runtimeNotes: {
27
+ local: "On-Device can work directly inside the configured local workspace root and publish a local build path.",
28
+ hosted: "Hosted has no filesystem. Generate small files in the client and publish them inline, or use catalog manifest and import operations for larger artifacts.",
29
+ },
30
+ prompt: true,
31
+ recipe: {
32
+ slug: "recipes/create-and-publish-site",
33
+ tier: "full",
34
+ url: `${DOCS_BASE_URL}/getting-started/agents`,
35
+ },
36
+ },
37
+ {
38
+ name: "publish-and-verify",
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.",
41
+ whenToUse: "Use when the artifact already exists and the user wants it published or updated.",
42
+ keywords: ["publish", "deploy", "live", "verify", "version", "claim"],
43
+ runtimes: BOTH_RUNTIMES,
44
+ tools: ["publish", "status", "diagnose"],
45
+ docs: ["publishing", "workflow/publish-receipt", "workflow/secret-handling"],
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.",
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
+ "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
+ ],
51
+ verification: [
52
+ "Call `status` and confirm the intended version is live. Use `diagnose` for a failed or stalled publish.",
53
+ "Open the live URL and verify representative content and assets rather than treating a successful upload or health check as serving proof.",
54
+ ],
55
+ runtimeNotes: {
56
+ local: "Publish a bounded local path; locally custodied claim credentials are reused automatically.",
57
+ hosted: "Publish inline files, or use catalog manifest and import operations for larger artifacts. Hosted has no filesystem.",
58
+ },
59
+ prompt: true,
60
+ recipe: {
61
+ slug: "recipes/agent-publish",
62
+ tier: "full",
63
+ url: `${DOCS_BASE_URL}/publishing/anonymous`,
64
+ },
65
+ },
66
+ {
67
+ name: "ci-github-actions",
68
+ title: "Publish from GitHub Actions",
69
+ description: "Create a repository-aware GitHub Actions workflow that builds and publishes with a masked Spacefast token.",
70
+ whenToUse: "Use when pushes or manual workflow runs should publish automatically from GitHub.",
71
+ keywords: ["github", "actions", "ci", "push", "automatic", "workflow"],
72
+ runtimes: BOTH_RUNTIMES,
73
+ tools: ["execute"],
74
+ docs: ["builds", "publishing", "workflow/secret-handling"],
75
+ instructions: [
76
+ "Inspect the repository's package manager, lockfile, build command, runtime version, output directory, and existing workflows before writing YAML.",
77
+ "Create a least-privilege workflow triggered by the intended branch and `workflow_dispatch`: check out the repository, install with the locked dependency command, build, and run `npx -y spacefast@<pinned-version> publish <output> --json`.",
78
+ "Store the credential as a masked `SPACEFAST_TOKEN` repository or environment secret. Never echo it or upload generated auth files, claim credentials, or upload credentials as artifacts.",
79
+ "Use the repository's real package manager and output directory; do not copy a generic npm/dist example over a Bun, pnpm, yarn, or custom build.",
80
+ ],
81
+ verification: [
82
+ "Validate the workflow syntax and run it on an explicit test branch or manual dispatch.",
83
+ "Inspect the publish receipt and live URL; a green build step alone is not serving proof.",
84
+ ],
85
+ prompt: true,
86
+ recipe: {
87
+ slug: "recipes/github-deploys",
88
+ tier: "full",
89
+ url: `${DOCS_BASE_URL}/publishing/git`,
90
+ },
91
+ },
92
+ {
93
+ name: "preview-channels",
94
+ title: "Preview changes safely",
95
+ description: "Publish branch or pull-request work to an isolated private preview and share it explicitly without touching production.",
96
+ whenToUse: "Use when reviewers need a realistic preview before a change goes live.",
97
+ keywords: ["preview", "channel", "branch", "pull request", "review", "private"],
98
+ runtimes: BOTH_RUNTIMES,
99
+ tools: ["publish", "status", "execute"],
100
+ docs: ["builds", "access-control"],
101
+ instructions: [
102
+ "Publish the branch through a connected repository or to an explicit non-production channel so the live production channel is unchanged.",
103
+ "Keep the preview private by default. Use an Open URL for the owner's browser or create the narrowest scoped Link needed for reviewers.",
104
+ "Label the preview with the branch, change, or pull request it represents and report what is intentionally out of scope.",
105
+ ],
106
+ verification: [
107
+ "Confirm the preview serves the expected version and production still points at its previous version.",
108
+ "Test the reviewer link in a signed-out context and revoke it when review is complete.",
109
+ ],
110
+ recipe: {
111
+ slug: "recipes/preview-channels",
112
+ tier: "full",
113
+ url: `${DOCS_BASE_URL}/publishing/git`,
114
+ },
115
+ },
116
+ {
117
+ name: "claim-flow",
118
+ title: "Claim an anonymous space",
119
+ description: "Attach an anonymous space to an account or team before expiry while keeping every claim credential in server or local custody.",
120
+ whenToUse: "Use after an anonymous publish returns a claim URL and expiry.",
121
+ keywords: ["claim", "anonymous", "ownership", "team", "continuation", "credential"],
122
+ runtimes: BOTH_RUNTIMES,
123
+ tools: ["execute", "publish"],
124
+ docs: ["anonymous-publish", "workflow/continue-after-claim", "workflow/secret-handling"],
125
+ instructions: [
126
+ "Return the claim URL and expiry to the user, never the raw claim credential. On-Device stores that credential in `.spacefast/state.json` and reuses it without returning it to the model.",
127
+ "When authenticated On-Device credentials are available, use the catalog `claim` operation with only `teamId` and the expected `spaceId`; otherwise let the user open the claim URL in their browser.",
128
+ "Publish again from On-Device after the browser claim. `publish` automatically exchanges the locally held claim credential once, persists the durable space-scoped credential, and retries without exposing either secret.",
129
+ ],
130
+ verification: [
131
+ "Confirm the space no longer has a claim expiry and appears under the intended account or team.",
132
+ "Publish again from the same local root or inspect the space through `execute` to verify durable access works without asking for a raw token.",
133
+ ],
134
+ prompt: true,
135
+ },
136
+ {
137
+ name: "rollback-safely",
138
+ title: "Roll back safely",
139
+ description: "Promote a known-good immutable version without rebuilding, then prove the live URL serves it.",
140
+ whenToUse: "Use when the current live version is broken and a prior healthy version is known or discoverable.",
141
+ keywords: ["rollback", "revert", "version", "incident", "restore"],
142
+ runtimes: BOTH_RUNTIMES,
143
+ tools: ["status", "rollback", "execute", "diagnose"],
144
+ docs: ["versions-and-rollback"],
145
+ instructions: [
146
+ "Inspect recent immutable versions and current status. Compare the current live version with the candidate using the catalog `versions` and `diff` operations.",
147
+ "Call `rollback` with the selected `versionId`. Rollback is additive and keeps later immutable versions available.",
148
+ "If promotion returns an operation that is still settling, poll the catalog `operation_status` operation until it is done instead of assuming success.",
149
+ ],
150
+ verification: [
151
+ "Call `status` and confirm the live pointer references the selected version.",
152
+ "Open the live URL and verify the failing path or behavior is restored; use `diagnose` if it is not.",
153
+ ],
154
+ prompt: true,
155
+ },
156
+ {
157
+ name: "set-up-custom-domain",
158
+ title: "Set up a custom domain",
159
+ description: "Attach a domain, provide the exact DNS records, and wait for HTTPS serving proof before reporting it live.",
160
+ whenToUse: "Use when the user owns a domain they want a Spacefast space to serve.",
161
+ keywords: ["domain", "dns", "tls", "https", "cname", "custom"],
162
+ runtimes: BOTH_RUNTIMES,
163
+ tools: ["execute"],
164
+ docs: ["custom-domains", "recipes/migrate-dns"],
165
+ instructions: [
166
+ "Add the domain with `sf domains add <domain>` or catalog operation `domains_add`; preserve the exact record values Spacefast returns.",
167
+ "Give the user those DNS records to create at their provider. Do not claim this MCP changed third-party DNS unless it actually has an authorized provider integration.",
168
+ "Keep the existing production hostname available during the cutover and avoid deleting old records until the new path is proven.",
169
+ ],
170
+ verification: [
171
+ "Run `sf domains check` or catalog operation `domains_check` until DNS and certificate state are ready.",
172
+ "Open the final custom-domain HTTPS URL and verify representative content before reporting it live.",
173
+ ],
174
+ prompt: true,
175
+ recipe: {
176
+ slug: "recipes/custom-domain",
177
+ tier: "full",
178
+ url: "https://spacefast.com/help/domains",
179
+ },
180
+ },
181
+ {
182
+ name: "redirects",
183
+ title: "Ship redirects with a version",
184
+ description: "Add redirect rules to the publish artifact so routing changes are versioned and roll back with the content.",
185
+ whenToUse: "Use for moved pages, canonical host paths, and intentional permanent or temporary redirects.",
186
+ keywords: ["redirect", "rewrite", "routing", "301", "302", "path"],
187
+ runtimes: BOTH_RUNTIMES,
188
+ tools: ["publish", "execute"],
189
+ docs: ["redirects"],
190
+ instructions: [
191
+ "Inspect existing routing rules, then add the smallest non-conflicting rules to `_redirects` at the publish root.",
192
+ "Choose temporary or permanent status deliberately, preserve path/query semantics where required, and avoid redirect loops or rules that shadow real assets.",
193
+ "Publish the redirect file as part of the same immutable version as the related content change.",
194
+ ],
195
+ verification: [
196
+ "Request representative old and new paths and confirm the exact status and Location header.",
197
+ "Verify the final target serves successfully and unrelated assets are not redirected.",
198
+ ],
199
+ recipe: {
200
+ slug: "recipes/redirects",
201
+ tier: "reference",
202
+ url: "https://spacefast.com/help/redirects",
203
+ },
204
+ },
205
+ {
206
+ name: "migrate-dns",
207
+ title: "Migrate DNS without avoidable downtime",
208
+ description: "Pre-stage a custom domain, prove certificate readiness, and cut traffic over with a reversible DNS change.",
209
+ whenToUse: "Use when an existing production domain must move to Spacefast.",
210
+ keywords: ["dns", "migrate", "cutover", "domain", "downtime", "certificate"],
211
+ runtimes: BOTH_RUNTIMES,
212
+ tools: ["execute"],
213
+ docs: ["custom-domains"],
214
+ instructions: [
215
+ "Inventory the current authoritative DNS records and lower TTL ahead of the cutover when the provider and timing allow it.",
216
+ "Add the domain to Spacefast and pre-stage every record it returns while preserving a working rollback path.",
217
+ "Cut over only after the Spacefast endpoint, expected content, and TLS readiness can be validated. Keep the previous origin available until propagation is complete.",
218
+ ],
219
+ verification: [
220
+ "Check resolution from more than one resolver and confirm the certificate matches the final hostname.",
221
+ "Open the public HTTPS URL, verify representative content, and monitor for old-origin traffic before removing rollback records.",
222
+ ],
223
+ recipe: {
224
+ slug: "recipes/migrate-dns",
225
+ tier: "reference",
226
+ url: "https://spacefast.com/help/domains",
227
+ },
228
+ },
229
+ {
230
+ name: "debug-a-failed-build",
231
+ title: "Debug a failed build",
232
+ description: "Use ranked diagnostics and real logs to find why a publish failed or stalled, fix it, and re-verify.",
233
+ whenToUse: "Use when a build, finalize, activation, or live serving step fails or stalls.",
234
+ keywords: ["debug", "failed", "build", "logs", "diagnose", "stalled"],
235
+ runtimes: BOTH_RUNTIMES,
236
+ tools: ["execute"],
237
+ docs: ["troubleshooting"],
238
+ instructions: [
239
+ "Use `execute` to find and call the catalog diagnostics operation for the space and optional version. Read its ranked findings, locations, error codes, fix hints, and docs links instead of guessing from a generic status.",
240
+ "Use the catalog `build_logs` operation for publish/finalize detail, runtime-log operations for serving errors, and version operations to compare with the last ready version.",
241
+ "Apply the smallest fix tied to the evidence, rebuild, and publish again. Follow the problem document's `type` URL for any referenced error code.",
242
+ ],
243
+ verification: [
244
+ "Re-run the relevant diagnostics or status operation through `execute` and confirm the new version reaches the intended live state.",
245
+ "Open the affected live path and verify the concrete failure no longer reproduces.",
246
+ ],
247
+ prompt: true,
248
+ },
249
+ {
250
+ name: "manage-access",
251
+ title: "Manage access",
252
+ description: "Create narrowly scoped Grants, People, and Links, then verify a concrete request without leaking credentials.",
253
+ whenToUse: "Use when a space or path must be made public, team-only, or shared with specific people or machines.",
254
+ keywords: ["access", "private", "public", "people", "links", "grant", "share"],
255
+ runtimes: BOTH_RUNTIMES,
256
+ tools: ["execute", "whoami"],
257
+ docs: ["access-control"],
258
+ instructions: [
259
+ "Inspect current Grants with `sf share` or the catalog `grants_list` operation and identify one explicit access intent. Anonymous spaces start Private.",
260
+ "Apply the narrowest suitable Public or Team Grant, People invitation, scoped Link/password, or machine/external-identity Grant. Keep resource scopes explicit.",
261
+ "Show newly created Link or machine secrets only to the requesting user. Never put credentials, claim credentials, or API keys in logs or committed files.",
262
+ ],
263
+ verification: [
264
+ "Use `sf share check --as public --path /docs` or the catalog `grants_check` operation for a representative request.",
265
+ "Test a new Link in the intended signed-out or recipient context and confirm unrelated resources remain inaccessible.",
266
+ ],
267
+ prompt: true,
268
+ },
269
+ ];
270
+ export const SPACEFAST_PROMPT_SKILLS = SPACEFAST_SKILLS.filter((skill) => "prompt" in skill && skill.prompt);
271
+ export const SPACEFAST_RECIPE_SKILLS = SPACEFAST_SKILLS.filter((skill) => "recipe" in skill);
272
+ export function findSpacefastSkill(name) {
273
+ const normalized = name.trim().toLowerCase();
274
+ return SPACEFAST_SKILLS.find((skill) => skill.name === normalized);
275
+ }
276
+ export function searchSpacefastSkills(query = "", limit = 5) {
277
+ const normalized = query.trim().toLowerCase();
278
+ const boundedLimit = Number.isFinite(limit) ? Math.max(1, Math.min(10, Math.trunc(limit))) : 5;
279
+ if (!normalized)
280
+ return SPACEFAST_SKILLS.slice(0, boundedLimit);
281
+ const tokens = normalized.split(/\s+/).filter(Boolean);
282
+ return SPACEFAST_SKILLS.map((skill) => {
283
+ const name = skill.name.toLowerCase();
284
+ const title = skill.title.toLowerCase();
285
+ const keywords = skill.keywords.map((candidate) => candidate.toLowerCase());
286
+ const body = [skill.description, skill.whenToUse, ...skill.docs].join(" ").toLowerCase();
287
+ let score = skill.name === normalized ? 100 : 0;
288
+ for (const token of tokens) {
289
+ let tokenScore = 0;
290
+ if (name === token)
291
+ tokenScore = 30;
292
+ else if (name.includes(token))
293
+ tokenScore = 20;
294
+ if (title.includes(token))
295
+ tokenScore = Math.max(tokenScore, 15);
296
+ if (keywords.some((candidate) => candidate.includes(token))) {
297
+ tokenScore = Math.max(tokenScore, 10);
298
+ }
299
+ if (body.includes(token))
300
+ tokenScore = Math.max(tokenScore, 5);
301
+ if (tokenScore === 0)
302
+ return { skill, score: 0 };
303
+ score += tokenScore;
304
+ }
305
+ return { skill, score };
306
+ })
307
+ .filter((match) => match.score > 0)
308
+ .toSorted((left, right) => right.score - left.score || left.skill.name.localeCompare(right.skill.name))
309
+ .slice(0, boundedLimit)
310
+ .map((match) => match.skill);
311
+ }
312
+ export function renderSpacefastSkill(skillOrName, runtime) {
313
+ const skill = typeof skillOrName === "string" ? findSpacefastSkill(skillOrName) : skillOrName;
314
+ if (!skill) {
315
+ const requestedName = typeof skillOrName === "string" ? skillOrName : skillOrName.name;
316
+ throw new Error(`Unknown Spacefast skill: ${requestedName}`);
317
+ }
318
+ const runtimeNote = skill.runtimeNotes?.[runtime];
319
+ return [
320
+ `# ${skill.title}`,
321
+ "",
322
+ skill.description,
323
+ "",
324
+ `**Use when:** ${skill.whenToUse}`,
325
+ ...(runtimeNote
326
+ ? ["", `**${runtime === "local" ? "On-Device" : "Hosted"}:** ${runtimeNote}`]
327
+ : []),
328
+ "",
329
+ "## Workflow",
330
+ "",
331
+ ...skill.instructions.map((step, index) => `${index + 1}. ${step}`),
332
+ "",
333
+ "## Verify",
334
+ "",
335
+ ...skill.verification.map((step) => `- ${step}`),
336
+ "",
337
+ `**Tools:** ${skill.tools.map((tool) => `\`${tool}\``).join(", ") || "none"}`,
338
+ `**Docs:** ${skill.docs.map((doc) => `\`${doc}\``).join(", ") || "none"}`,
339
+ ].join("\n");
340
+ }
@@ -1,3 +1 @@
1
- export declare function sanitizeDashboardReturnTo(returnTo: string | null | undefined, options?: {
2
- apiOrigin?: string | null;
3
- }): string | undefined;
1
+ export declare function sanitizeDashboardReturnTo(returnTo: string | null | undefined): string | undefined;
@@ -17,29 +17,9 @@ function hasUnsafeReturnToChars(value) {
17
17
  }
18
18
  return false;
19
19
  }
20
- // The ONE absolute-URL exception to the same-origin returnTo policy
21
- // (access-plan X-41): the api-origin GET /v1/access/authorize URL exactly —
22
- // the sign-in leg of the visitor redirect chain. Everything else absolute is
23
- // an open-redirect surface and stays rejected.
24
- const API_AUTHORIZE_PATH = "/v1/access/authorize";
25
- export function sanitizeDashboardReturnTo(returnTo, options) {
20
+ export function sanitizeDashboardReturnTo(returnTo) {
26
21
  const normalized = returnTo?.trim();
27
22
  const decoded = normalized ? decodedReturnTo(normalized) : null;
28
- if (normalized && decoded && options?.apiOrigin) {
29
- try {
30
- const candidate = new URL(normalized);
31
- const apiOrigin = new URL(options.apiOrigin).origin;
32
- if (candidate.origin === apiOrigin &&
33
- candidate.pathname === API_AUTHORIZE_PATH &&
34
- !hasUnsafeReturnToChars(normalized) &&
35
- !hasUnsafeReturnToChars(decoded)) {
36
- return candidate.toString();
37
- }
38
- }
39
- catch {
40
- // Not an absolute URL — fall through to the same-origin path rules.
41
- }
42
- }
43
23
  if (!normalized ||
44
24
  !decoded ||
45
25
  !normalized.startsWith("/") ||
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Builds a browser handoff URL around a short-lived, one-use exchange token.
3
+ *
4
+ * The durable credential never appears in this URL. The access origin consumes
5
+ * the final path segment once and immediately redirects browsers to a clean
6
+ * path-scoped session. `publicId` is used only by agent handoffs, whose clean
7
+ * static document remains `/agent/<id>`.
8
+ */
9
+ export declare function createBrowserCredentialUrl(input: {
10
+ origin: string;
11
+ path: string;
12
+ publicId?: string;
13
+ credential: string;
14
+ }): string;
15
+ /** Parses a one-use browser handoff URL and reconstructs its clean public path. */
16
+ export declare function parseBrowserCredentialUrl(value: string): {
17
+ credential: string;
18
+ requestUrl: string;
19
+ };
@@ -0,0 +1,62 @@
1
+ const browserHandoffSegmentPattern = /^[A-Za-z0-9_-]{16,512}$/;
2
+ const browserHandoffPublicIdPattern = /^[A-Za-z0-9_-]{1,512}$/;
3
+ function requireBrowserHandoffSegment(value) {
4
+ if (!browserHandoffSegmentPattern.test(value)) {
5
+ throw new TypeError("Browser handoff token must be an opaque URL-safe value.");
6
+ }
7
+ return value;
8
+ }
9
+ /**
10
+ * Builds a browser handoff URL around a short-lived, one-use exchange token.
11
+ *
12
+ * The durable credential never appears in this URL. The access origin consumes
13
+ * the final path segment once and immediately redirects browsers to a clean
14
+ * path-scoped session. `publicId` is used only by agent handoffs, whose clean
15
+ * static document remains `/agent/<id>`.
16
+ */
17
+ export function createBrowserCredentialUrl(input) {
18
+ const origin = new URL(input.origin);
19
+ if ((origin.protocol !== "https:" && origin.protocol !== "http:") ||
20
+ origin.username ||
21
+ origin.password ||
22
+ origin.pathname !== "/" ||
23
+ origin.search ||
24
+ origin.hash) {
25
+ throw new TypeError("Browser handoff URLs require a clean HTTP(S) origin.");
26
+ }
27
+ const basePath = `/${input.path.split("/").filter(Boolean).join("/")}`;
28
+ if (basePath === "/" || input.path.includes("?") || input.path.includes("#")) {
29
+ throw new TypeError("Browser handoff URLs require a clean absolute path.");
30
+ }
31
+ const segments = [
32
+ ...(input.publicId === undefined
33
+ ? []
34
+ : [
35
+ browserHandoffPublicIdPattern.test(input.publicId)
36
+ ? input.publicId
37
+ : (() => {
38
+ throw new TypeError("Browser handoff public id must be URL-safe.");
39
+ })(),
40
+ ]),
41
+ requireBrowserHandoffSegment(input.credential),
42
+ ];
43
+ return new URL(`${basePath}/${segments.map(encodeURIComponent).join("/")}`, origin).toString();
44
+ }
45
+ /** Parses a one-use browser handoff URL and reconstructs its clean public path. */
46
+ export function parseBrowserCredentialUrl(value) {
47
+ const url = new URL(value);
48
+ if ((url.protocol !== "https:" && url.protocol !== "http:") ||
49
+ url.username ||
50
+ url.password ||
51
+ url.search ||
52
+ url.hash) {
53
+ throw new TypeError("Browser handoff URL is not a clean HTTP(S) URL.");
54
+ }
55
+ const segments = url.pathname.split("/").filter(Boolean);
56
+ const credential = segments.pop();
57
+ if (!credential || !browserHandoffSegmentPattern.test(credential) || segments.length === 0) {
58
+ throw new TypeError("Browser handoff URL does not carry a valid one-use token.");
59
+ }
60
+ url.pathname = `/${segments.map(encodeURIComponent).join("/")}`;
61
+ return { credential, requestUrl: url.toString() };
62
+ }
@@ -1,6 +1,8 @@
1
1
  export type BuildSettingsInput = {
2
2
  fileNames: Iterable<string>;
3
3
  packageJson?: string | null | undefined;
4
+ jekyllYaml?: string | null | undefined;
5
+ jekyllToml?: string | null | undefined;
4
6
  vercelJson?: string | null | undefined;
5
7
  netlifyToml?: string | null | undefined;
6
8
  wranglerToml?: string | null | undefined;
@@ -1,3 +1,4 @@
1
+ import { load as parseYaml } from "js-yaml";
1
2
  import { parse as parseToml } from "smol-toml";
2
3
  export const BUILD_ANALYZER_CANDIDATE_ROOTS = ["apps", "packages", "sites", "docs"];
3
4
  const PACKAGE_MANAGER_ONLY_FILES = new Set([
@@ -7,6 +8,8 @@ const PACKAGE_MANAGER_ONLY_FILES = new Set([
7
8
  "pnpm-lock.yaml",
8
9
  "yarn.lock",
9
10
  "package-lock.json",
11
+ "Gemfile",
12
+ "Gemfile.lock",
10
13
  ]);
11
14
  export function hasWorkspaceSignal(fileNames, packageJsonText) {
12
15
  const names = new Set(fileNames);
@@ -102,6 +105,20 @@ function parseTomlObject(value) {
102
105
  return null;
103
106
  }
104
107
  }
108
+ function parseYamlObject(value) {
109
+ if (!value) {
110
+ return null;
111
+ }
112
+ try {
113
+ const parsed = parseYaml(value);
114
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
115
+ ? parsed
116
+ : null;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
105
122
  export function stringField(value, key) {
106
123
  const field = value?.[key];
107
124
  return typeof field === "string" && field.trim() ? field.trim() : null;
@@ -628,9 +645,8 @@ const FRAMEWORKS = [
628
645
  },
629
646
  {
630
647
  preset: "jekyll",
631
- configFiles: ["_config.yml", "_config.yaml"],
648
+ configFiles: ["_config.yml", "_config.yaml", "_config.toml"],
632
649
  outputDirectory: "_site",
633
- defaultBuildCommand: "bundle exec jekyll build",
634
650
  },
635
651
  {
636
652
  preset: "mkdocs",
@@ -675,6 +691,26 @@ function outputDirectoryForFramework(frameworkPreset, files) {
675
691
  return framework.outputDirectory;
676
692
  return files.has("dist") ? "dist" : null;
677
693
  }
694
+ const JEKYLL_BUNDLE_PATH = ".spacefast/cache/jekyll-bundle";
695
+ const JEKYLL_GEM_HOME = ".spacefast/cache/jekyll-gems";
696
+ function jekyllInstallCommand(files) {
697
+ return files.has("Gemfile")
698
+ ? `BUNDLE_PATH=${JEKYLL_BUNDLE_PATH} bundle install`
699
+ : `GEM_HOME=${JEKYLL_GEM_HOME} gem install jekyll --no-document`;
700
+ }
701
+ function jekyllConfigFile(files) {
702
+ return ["_config.yml", "_config.yaml", "_config.toml"].find((file) => files.has(file));
703
+ }
704
+ function jekyllBuildCommand(files) {
705
+ const command = files.has("Gemfile")
706
+ ? `JEKYLL_ENV=production BUNDLE_PATH=${JEKYLL_BUNDLE_PATH} bundle exec jekyll build`
707
+ : `JEKYLL_ENV=production GEM_HOME=${JEKYLL_GEM_HOME} ${JEKYLL_GEM_HOME}/bin/jekyll build`;
708
+ const configFile = jekyllConfigFile(files);
709
+ return configFile ? `${command} --config ${configFile}` : command;
710
+ }
711
+ function frameworkInstallCommand(frameworkPreset, files) {
712
+ return frameworkPreset === "jekyll" ? jekyllInstallCommand(files) : null;
713
+ }
678
714
  function installCommandForManager(packageManager, packageJson) {
679
715
  switch (packageManager) {
680
716
  case "bun":
@@ -687,13 +723,15 @@ function installCommandForManager(packageManager, packageJson) {
687
723
  return packageJson ? "npm ci" : null;
688
724
  }
689
725
  }
690
- function buildCommandForDefaults(packageJson, run, frameworkPreset) {
726
+ function buildCommandForDefaults(packageJson, run, frameworkPreset, files) {
691
727
  if (packageJson?.scripts?.build)
692
728
  return `${run} build`;
729
+ if (frameworkPreset === "jekyll")
730
+ return jekyllBuildCommand(files);
693
731
  return frameworkForPreset(frameworkPreset)?.defaultBuildCommand ?? null;
694
732
  }
695
- function packageDefaults(files, packageJsonText) {
696
- const packageJson = parsePackageJson(packageJsonText);
733
+ function packageDefaults(input, files) {
734
+ const packageJson = parsePackageJson(input.packageJson);
697
735
  const dependencies = {
698
736
  ...packageJson?.dependencies,
699
737
  ...packageJson?.devDependencies,
@@ -701,17 +739,26 @@ function packageDefaults(files, packageJsonText) {
701
739
  const packageManager = packageManagerForFiles(files);
702
740
  const run = packageManager === "npm" ? "npm run" : packageManager === "bun" ? "bun run" : packageManager;
703
741
  const frameworkPreset = detectFrameworkPreset(files, dependencies);
742
+ const installCommands = [
743
+ installCommandForManager(packageManager, packageJson),
744
+ frameworkInstallCommand(frameworkPreset, files),
745
+ ].filter((command) => Boolean(command));
746
+ const jekyllConfig = input.jekyllYaml
747
+ ? parseYamlObject(input.jekyllYaml)
748
+ : parseTomlObject(input.jekyllToml);
704
749
  return {
705
750
  frameworkPreset,
706
- installCommand: installCommandForManager(packageManager, packageJson),
707
- buildCommand: buildCommandForDefaults(packageJson, run, frameworkPreset),
708
- outputDirectory: outputDirectoryForFramework(frameworkPreset, files),
751
+ installCommand: installCommands.length > 0 ? installCommands.join(" && ") : null,
752
+ buildCommand: buildCommandForDefaults(packageJson, run, frameworkPreset, files),
753
+ outputDirectory: frameworkPreset === "jekyll"
754
+ ? (stringField(jekyllConfig, "destination") ?? "_site")
755
+ : outputDirectoryForFramework(frameworkPreset, files),
709
756
  };
710
757
  }
711
758
  export function detectBuildSettings(input) {
712
759
  const files = new Set(input.fileNames);
713
760
  const defaults = {
714
- ...packageDefaults(files, input.packageJson),
761
+ ...packageDefaults(input, files),
715
762
  rootDirectory: input.rootDirectory ?? null,
716
763
  platformPreset: null,
717
764
  detectedFiles: [
@@ -721,6 +768,11 @@ export function detectBuildSettings(input) {
721
768
  files.has("pnpm-lock.yaml") ? "pnpm-lock.yaml" : null,
722
769
  files.has("yarn.lock") ? "yarn.lock" : null,
723
770
  files.has("package-lock.json") ? "package-lock.json" : null,
771
+ files.has("Gemfile") ? "Gemfile" : null,
772
+ files.has("Gemfile.lock") ? "Gemfile.lock" : null,
773
+ files.has("_config.yml") ? "_config.yml" : null,
774
+ files.has("_config.yaml") ? "_config.yaml" : null,
775
+ files.has("_config.toml") ? "_config.toml" : null,
724
776
  ].filter((file) => Boolean(file)),
725
777
  };
726
778
  const vercelFramework = input.vercelJson
@@ -0,0 +1,7 @@
1
+ export declare const COMMENT_AVATAR_IMAGE_ORIGINS: readonly ["https://gravatar.com", "https://i0.wp.com"];
2
+ /**
3
+ * Keep Comments avatar requests on origins that can be named explicitly in
4
+ * the host page's CSP. Gravatar is already canonical; every other HTTP(S)
5
+ * profile image goes through the HTTPS WordPress.com image CDN.
6
+ */
7
+ export declare function normalizeCommentAvatarUrl(value: string): string | null;
@@ -0,0 +1,42 @@
1
+ export const COMMENT_AVATAR_IMAGE_ORIGINS = ["https://gravatar.com", "https://i0.wp.com"];
2
+ const CANONICAL_GRAVATAR_ORIGIN = COMMENT_AVATAR_IMAGE_ORIGINS[0];
3
+ const AVATAR_PROXY_ORIGIN = COMMENT_AVATAR_IMAGE_ORIGINS[1];
4
+ const MAX_AVATAR_URL_LENGTH = 2_048;
5
+ /**
6
+ * Keep Comments avatar requests on origins that can be named explicitly in
7
+ * the host page's CSP. Gravatar is already canonical; every other HTTP(S)
8
+ * profile image goes through the HTTPS WordPress.com image CDN.
9
+ */
10
+ export function normalizeCommentAvatarUrl(value) {
11
+ const trimmed = value.trim();
12
+ if (!trimmed || trimmed.length > MAX_AVATAR_URL_LENGTH)
13
+ return null;
14
+ try {
15
+ const source = new URL(trimmed);
16
+ if ((source.protocol !== "https:" && source.protocol !== "http:") ||
17
+ source.username !== "" ||
18
+ source.password !== "") {
19
+ return null;
20
+ }
21
+ if (source.origin === CANONICAL_GRAVATAR_ORIGIN || source.origin === AVATAR_PROXY_ORIGIN) {
22
+ return source.href.length <= MAX_AVATAR_URL_LENGTH ? source.href : null;
23
+ }
24
+ // Assign the proxy path instead of parsing `<host><path>` as a URL. A host
25
+ // with a non-default port contains a colon and would otherwise become a
26
+ // made-up scheme (`javascript:8443/...`, for example). Photon addresses an
27
+ // HTTPS source with `ssl=1`; without it, an HTTPS and HTTP source collapse
28
+ // to the same proxy URL. `source.host` deliberately retains an explicit
29
+ // non-default port.
30
+ const proxied = new URL(AVATAR_PROXY_ORIGIN);
31
+ proxied.pathname = `/${source.host}${source.pathname}`;
32
+ proxied.search = source.search;
33
+ if (source.protocol === "https:")
34
+ proxied.searchParams.set("ssl", "1");
35
+ if (proxied.protocol !== "https:" || proxied.origin !== AVATAR_PROXY_ORIGIN)
36
+ return null;
37
+ return proxied.href.length <= MAX_AVATAR_URL_LENGTH ? proxied.href : null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }