@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.
- package/dist/agents/client-registry.d.ts +22 -0
- package/dist/agents/client-registry.js +41 -0
- package/dist/agents/connect-targets.d.ts +51 -6
- package/dist/agents/connect-targets.js +93 -28
- package/dist/brand.d.ts +0 -11
- package/dist/brand.js +0 -20
- package/dist/config/domains.d.ts +15 -6
- package/dist/config/domains.js +24 -10
- package/dist/config/postgres-budget.d.ts +3 -0
- package/dist/config/postgres-budget.js +6 -1
- package/dist/config/postgres-budget.json +2 -1
- package/dist/contracts/abuse.js +1 -1
- package/dist/contracts/access.d.ts +486 -1196
- package/dist/contracts/access.js +323 -1054
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +11 -15
- package/dist/contracts/api-keys.d.ts +16 -0
- package/dist/contracts/api-keys.js +11 -9
- package/dist/contracts/archives.d.ts +1 -2
- package/dist/contracts/archives.js +15 -17
- package/dist/contracts/beta.js +1 -1
- package/dist/contracts/builds.js +14 -14
- package/dist/contracts/channels.js +2 -2
- package/dist/contracts/comments.d.ts +143 -99
- package/dist/contracts/comments.js +194 -79
- package/dist/contracts/common.d.ts +12 -12
- package/dist/contracts/common.js +33 -30
- package/dist/contracts/continuation.js +5 -5
- package/dist/contracts/countries.js +2 -7
- package/dist/contracts/data-source-routes.d.ts +20 -0
- package/dist/contracts/data-source-routes.js +15 -0
- package/dist/contracts/device-auth.d.ts +13 -9
- package/dist/contracts/device-auth.js +12 -12
- package/dist/contracts/docs.d.ts +10 -10
- package/dist/contracts/domains.js +39 -33
- package/dist/contracts/enums.d.ts +4 -14
- package/dist/contracts/enums.js +3 -10
- package/dist/contracts/error-code-meta.d.ts +122 -56
- package/dist/contracts/error-code-meta.js +43 -19
- package/dist/contracts/error-codes.d.ts +7 -4
- package/dist/contracts/error-codes.js +48 -24
- package/dist/contracts/events.d.ts +22 -15
- package/dist/contracts/events.js +33 -5
- package/dist/contracts/execution.d.ts +238 -0
- package/dist/contracts/execution.js +309 -0
- package/dist/contracts/feature-lifecycle.d.ts +39 -188
- package/dist/contracts/feature-lifecycle.js +52 -196
- package/dist/contracts/features.d.ts +13 -115
- package/dist/contracts/features.js +5 -35
- package/dist/contracts/functions.d.ts +597 -0
- package/dist/contracts/functions.js +475 -0
- package/dist/contracts/grants.d.ts +1175 -0
- package/dist/contracts/grants.js +783 -0
- package/dist/contracts/ids.d.ts +7 -5
- package/dist/contracts/ids.js +7 -7
- package/dist/contracts/internal.js +2 -2
- package/dist/contracts/mcp.d.ts +17 -556
- package/dist/contracts/mcp.js +9 -103
- package/dist/contracts/me.js +3 -4
- package/dist/contracts/notifications.d.ts +108 -0
- package/dist/contracts/notifications.js +54 -0
- package/dist/contracts/oauth-resources.d.ts +22 -5
- package/dist/contracts/oauth-resources.js +23 -2
- package/dist/contracts/operations.d.ts +0 -48
- package/dist/contracts/operations.js +0 -19
- package/dist/contracts/pages.d.ts +10 -14
- package/dist/contracts/pages.js +17 -21
- package/dist/contracts/plan-policy.d.ts +1 -8
- package/dist/contracts/plan-policy.js +1 -4
- package/dist/contracts/platform.d.ts +78 -30
- package/dist/contracts/platform.js +69 -20
- package/dist/contracts/problem-document.d.ts +14 -0
- package/dist/contracts/problem-document.js +35 -0
- package/dist/contracts/push-new.d.ts +41 -2
- package/dist/contracts/push-new.js +4 -0
- package/dist/contracts/quotas.d.ts +0 -1
- package/dist/contracts/quotas.js +0 -5
- package/dist/contracts/repository-connections.d.ts +1 -1
- package/dist/contracts/resources.d.ts +16 -71
- package/dist/contracts/resources.js +13 -16
- package/dist/contracts/runtime-api.d.ts +666 -303
- package/dist/contracts/runtime-api.js +272 -113
- package/dist/contracts/runtime-app.d.ts +168 -0
- package/dist/contracts/runtime-app.js +42 -0
- package/dist/contracts/runtime-db.d.ts +424 -0
- package/dist/contracts/runtime-db.js +108 -0
- package/dist/contracts/runtime-storage.d.ts +120 -0
- package/dist/contracts/runtime-storage.js +40 -0
- package/dist/contracts/sf-config-v1.d.ts +54 -1
- package/dist/contracts/sf-config-v1.js +2 -2
- package/dist/contracts/sites.d.ts +6 -0
- package/dist/contracts/sites.js +30 -11
- package/dist/contracts/space-config.d.ts +91 -121
- package/dist/contracts/space-config.js +464 -42
- package/dist/contracts/spaces.d.ts +300 -171
- package/dist/contracts/spaces.js +128 -98
- package/dist/contracts/superadmin-emails.d.ts +2 -2
- package/dist/contracts/superadmin-spaces.d.ts +26 -130
- package/dist/contracts/superadmin-spaces.js +22 -0
- package/dist/contracts/superadmin-tenants.js +4 -4
- package/dist/contracts/superadmin.d.ts +4 -0
- package/dist/contracts/superadmin.js +14 -1
- package/dist/contracts/tags.d.ts +6 -6
- package/dist/contracts/tags.js +1 -1
- package/dist/contracts/teams.d.ts +9 -9
- package/dist/contracts/teams.js +25 -16
- package/dist/contracts/theme-json.js +1 -1
- package/dist/contracts/transfers.d.ts +0 -1
- package/dist/contracts/transfers.js +4 -4
- package/dist/contracts/variables.js +3 -3
- package/dist/contracts/zero.d.ts +384 -596
- package/dist/contracts/zero.js +167 -299
- package/dist/dashboard-paths/index.d.ts +18 -3
- package/dist/dashboard-paths/index.js +19 -0
- package/dist/docs/agent-handoff-document.d.ts +2 -2
- package/dist/docs/agent-handoff-document.js +8 -7
- package/dist/docs/agent-prose.d.ts +22 -3
- package/dist/docs/agent-prose.js +227 -55
- package/dist/docs/agent-setup.d.ts +14 -11
- package/dist/docs/agent-setup.js +289 -61
- package/dist/docs/agent-solutions.d.ts +6 -6
- package/dist/docs/agent-solutions.js +24 -22
- package/dist/docs/catalog.d.ts +53 -98
- package/dist/docs/catalog.js +90 -117
- package/dist/docs/error-docs.js +304 -236
- package/dist/docs/skill-distribution.d.ts +2 -0
- package/dist/docs/skill-distribution.js +7 -4
- package/dist/docs/skills.d.ts +546 -0
- package/dist/docs/skills.js +340 -0
- package/dist/utils/auth-redirect.d.ts +1 -3
- package/dist/utils/auth-redirect.js +1 -21
- package/dist/utils/browser-credential.d.ts +19 -0
- package/dist/utils/browser-credential.js +62 -0
- package/dist/utils/build-settings.d.ts +2 -0
- package/dist/utils/build-settings.js +61 -9
- package/dist/utils/comment-avatars.d.ts +7 -0
- package/dist/utils/comment-avatars.js +42 -0
- package/dist/utils/content-type.d.ts +3 -3
- package/dist/utils/content-type.js +38 -61
- package/dist/utils/credential-policy.d.ts +1 -1
- package/dist/utils/credential-policy.js +4 -0
- package/dist/utils/domain-diagnostics.d.ts +5 -5
- package/dist/utils/domain-diagnostics.js +21 -10
- package/dist/utils/egress-policy.fixtures.json +1 -1
- package/dist/utils/grants.d.ts +53 -0
- package/dist/utils/grants.js +160 -0
- package/dist/utils/local-space-state.d.ts +78 -4
- package/dist/utils/local-space-state.js +384 -82
- package/dist/utils/one-shot-replay.d.ts +14 -0
- package/dist/utils/one-shot-replay.js +21 -0
- package/dist/utils/page-preview.d.ts +7 -0
- package/dist/utils/page-preview.js +9 -0
- package/dist/utils/pages.d.ts +2 -5
- package/dist/utils/pages.js +64 -33
- package/dist/utils/publish-form-data.d.ts +22 -0
- package/dist/utils/publish-form-data.js +26 -0
- package/dist/utils/publish-policy.d.ts +34 -4
- package/dist/utils/publish-policy.js +149 -62
- package/dist/utils/query-keys.d.ts +8 -10
- package/dist/utils/query-keys.js +14 -10
- package/dist/utils/runtime-paths.d.ts +3 -0
- package/dist/utils/runtime-paths.js +7 -0
- package/dist/utils/secure-local-file.d.ts +73 -0
- package/dist/utils/secure-local-file.js +1373 -0
- package/dist/utils/space-config.d.ts +17 -4
- package/dist/utils/space-config.js +151 -8
- package/dist/utils/static-runtime-policy.d.ts +5 -0
- package/dist/utils/static-runtime-policy.js +54 -10
- package/dist/utils/storage-policy.d.ts +20 -0
- package/dist/utils/storage-policy.fixtures.json +174 -0
- package/dist/utils/storage-policy.js +193 -0
- package/dist/utils/upload-session.d.ts +15 -1
- package/dist/utils/upload-session.js +68 -2
- package/dist/utils/version-file-browser.d.ts +13 -0
- package/dist/utils/version-file-browser.js +59 -0
- package/package.json +20 -1
- package/dist/contracts/mounts.d.ts +0 -142
- package/dist/contracts/mounts.js +0 -56
- package/dist/utils/access-grant.d.ts +0 -14
- package/dist/utils/access-grant.js +0 -88
- package/dist/utils/access-match.d.ts +0 -46
- package/dist/utils/access-match.fixtures.json +0 -513
- package/dist/utils/access-match.js +0 -312
- package/dist/utils/access-verdict.fixtures.json +0 -421
- package/dist/utils/asset-fingerprint.d.ts +0 -53
- package/dist/utils/asset-fingerprint.js +0 -574
- package/dist/utils/sf-config-v1.d.ts +0 -2
- package/dist/utils/sf-config-v1.fixtures.json +0 -9
- 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
|
+
}
|
|
@@ -17,29 +17,9 @@ function hasUnsafeReturnToChars(value) {
|
|
|
17
17
|
}
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
20
|
-
|
|
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(
|
|
696
|
-
const packageJson = parsePackageJson(
|
|
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:
|
|
707
|
-
buildCommand: buildCommandForDefaults(packageJson, run, frameworkPreset),
|
|
708
|
-
outputDirectory:
|
|
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(
|
|
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
|
+
}
|