@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
package/dist/utils/pages.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { defaultTreeAdapter, parse } from "parse5";
|
|
1
2
|
import { SPACEFAST_WORDMARK_PATH } from "../brand-assets.js";
|
|
2
3
|
import { CUSTOM_PAGE_IDS, PAGE_CATALOG, PAGE_ELEMENTS, PAGE_LAYOUT_FILE, PAGE_SOURCE_MAX_BYTES, SITE_PAGE_IDS, } from "../contracts/pages.js";
|
|
3
4
|
import { SPACE_CONFIG_ACCEPTED_FILES } from "../contracts/space-config.js";
|
|
4
5
|
import { derivePageColors, PAGE_COLOR_DEFAULTS } from "./page-colors.js";
|
|
5
6
|
import { PAGE_EMBEDDED_FONT_FACES, PAGE_FONT_FACES } from "./page-fonts.js";
|
|
7
|
+
export { PAGE_PREVIEW_QUERY, previewPageHref } from "./page-preview.js";
|
|
6
8
|
const encoder = new TextEncoder();
|
|
7
9
|
const knownElements = new Set(PAGE_ELEMENTS);
|
|
8
10
|
const customPageIds = new Set(CUSTOM_PAGE_IDS);
|
|
@@ -69,16 +71,14 @@ function renderFiles(files) {
|
|
|
69
71
|
function renderFile(file) {
|
|
70
72
|
if (!file)
|
|
71
73
|
return '<p class="sf-empty">File unavailable.</p>';
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
// This page is itself the `?spacefast_view=1` representation, so the embed,
|
|
75
|
+
// the download and the raw link all point straight at the canonical path.
|
|
76
|
+
const href = escapePageHtml(file.href);
|
|
74
77
|
const name = escapePageHtml(file.name);
|
|
75
78
|
const media = file.mime?.startsWith("image/")
|
|
76
79
|
? `<img class="sf-preview" src="${href}" alt="">`
|
|
77
80
|
: `<iframe class="sf-preview" src="${href}" title="Preview of ${name}" loading="lazy" sandbox></iframe>`;
|
|
78
|
-
|
|
79
|
-
? `${file.downloadHref}${file.downloadHref.includes("?") ? "&" : "?"}spacefast_raw=1`
|
|
80
|
-
: rawHref;
|
|
81
|
-
return `<div class="sf-preview-wrap">${media}</div><dl class="sf-file-meta"><div><dt>Name</dt><dd>${name}</dd></div>${file.size ? `<div><dt>Size</dt><dd>${escapePageHtml(file.size)}</dd></div>` : ""}${file.mime ? `<div><dt>Type</dt><dd>${escapePageHtml(file.mime)}</dd></div>` : ""}</dl><div class="sf-actions"><a class="sf-button" href="${escapePageHtml(downloadHref)}" download>Download</a><a class="sf-ghost" href="${href}">View raw</a></div>`;
|
|
81
|
+
return `<div class="sf-preview-wrap">${media}</div><dl class="sf-file-meta"><div><dt>Name</dt><dd>${name}</dd></div>${file.size ? `<div><dt>Size</dt><dd>${escapePageHtml(file.size)}</dd></div>` : ""}${file.mime ? `<div><dt>Type</dt><dd>${escapePageHtml(file.mime)}</dd></div>` : ""}</dl><div class="sf-actions"><a class="sf-button" href="${href}" download>Download</a><a class="sf-ghost" href="${href}">View raw</a></div>`;
|
|
82
82
|
}
|
|
83
83
|
function renderSpacefastBranding(hidden) {
|
|
84
84
|
const wordmark = hidden
|
|
@@ -90,18 +90,12 @@ function renderElement(element, context) {
|
|
|
90
90
|
switch (element) {
|
|
91
91
|
case "sf-content":
|
|
92
92
|
return context.content ?? "";
|
|
93
|
-
case "sf-password-form": {
|
|
94
|
-
const hasError = Boolean(context.passwordError);
|
|
95
|
-
const error = hasError
|
|
96
|
-
? `<p class="sf-error" id="sf-password-error" role="alert">${escapePageHtml(context.passwordError ?? "")}</p>`
|
|
97
|
-
: "";
|
|
98
|
-
const form = `<form class="sf-form" method="post" action="${escapePageHtml(context.passwordAction ?? "")}"><label for="sf-password">Password</label><div class="sf-form-row"><input id="sf-password" name="_pw" type="password" required autocomplete="current-password"${hasError ? ' aria-invalid="true" aria-describedby="sf-password-error"' : ""}><button type="submit">Continue</button></div>${error}</form>`;
|
|
99
|
-
return runtimeSlot("challenge", form);
|
|
100
|
-
}
|
|
101
|
-
case "sf-login":
|
|
102
|
-
return runtimeSlot("challenge", `<div class="sf-actions"><a class="sf-button" href="${escapePageHtml(context.loginHref ?? "/__spacefast/access/login")}">Continue with Spacefast</a></div>`);
|
|
103
93
|
case "sf-denial-context":
|
|
104
|
-
return runtimeSlot("denial", '<p class="sf-copy">
|
|
94
|
+
return runtimeSlot("denial", '<p class="sf-copy">This page is private.</p>');
|
|
95
|
+
case "sf-access-status":
|
|
96
|
+
return runtimeSlot("access-status", "");
|
|
97
|
+
case "sf-access-lanes":
|
|
98
|
+
return runtimeSlot("access-lanes", '<p class="sf-copy">This page is private.</p>');
|
|
105
99
|
case "sf-page-path":
|
|
106
100
|
return context.displayPath
|
|
107
101
|
? escapePageHtml(context.displayPath)
|
|
@@ -150,6 +144,35 @@ function foundElements(source) {
|
|
|
150
144
|
result.add((match[1] ?? "").toLowerCase());
|
|
151
145
|
return [...result];
|
|
152
146
|
}
|
|
147
|
+
function accessCspBlocksForms(source) {
|
|
148
|
+
const documentNodes = [...parse(source).childNodes];
|
|
149
|
+
const head = documentNodes
|
|
150
|
+
.flatMap((node) => defaultTreeAdapter.isElementNode(node) && node.tagName === "html" ? node.childNodes : [])
|
|
151
|
+
.find((node) => defaultTreeAdapter.isElementNode(node) && node.tagName === "head");
|
|
152
|
+
if (!head || !defaultTreeAdapter.isElementNode(head))
|
|
153
|
+
return false;
|
|
154
|
+
const pending = [...head.childNodes];
|
|
155
|
+
while (pending.length > 0) {
|
|
156
|
+
const node = pending.pop();
|
|
157
|
+
if (!node || !defaultTreeAdapter.isElementNode(node))
|
|
158
|
+
continue;
|
|
159
|
+
if (node.tagName === "meta") {
|
|
160
|
+
const attributes = new Map(node.attrs.map(({ name, value }) => [name, value]));
|
|
161
|
+
if (attributes.get("http-equiv")?.trim().toLowerCase() === "content-security-policy") {
|
|
162
|
+
const policy = (attributes.get("content") ?? "")
|
|
163
|
+
.split(";")
|
|
164
|
+
.map((directive) => directive.trim().split(/\s+/))
|
|
165
|
+
.filter((directive) => directive[0]);
|
|
166
|
+
const formAction = policy.find((directive) => directive[0]?.toLowerCase() === "form-action");
|
|
167
|
+
if (formAction &&
|
|
168
|
+
!formAction.slice(1).some((expression) => expression.toLowerCase() === "'self'"))
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
pending.push(...node.childNodes);
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
153
176
|
export function validatePageSources(input) {
|
|
154
177
|
const diagnostics = [];
|
|
155
178
|
for (const [sourcePath, source] of Object.entries(input.pages)) {
|
|
@@ -192,8 +215,8 @@ export function validatePageSources(input) {
|
|
|
192
215
|
element === "sf-name" ||
|
|
193
216
|
element === "sf-spacefast-branding" ||
|
|
194
217
|
(id === "404" && element === "sf-page-path") ||
|
|
195
|
-
(id === "password" && element === "sf-password-form") ||
|
|
196
218
|
(id === "denied" && element === "sf-denial-context") ||
|
|
219
|
+
(id === "access" && (element === "sf-access-status" || element === "sf-access-lanes")) ||
|
|
197
220
|
(id === "index" &&
|
|
198
221
|
(element === "sf-files" ||
|
|
199
222
|
element === "sf-page-path" ||
|
|
@@ -209,16 +232,6 @@ export function validatePageSources(input) {
|
|
|
209
232
|
path,
|
|
210
233
|
});
|
|
211
234
|
}
|
|
212
|
-
if (id === "password" &&
|
|
213
|
-
!/<\s*sf-password-form\b/i.test(source) &&
|
|
214
|
-
!/<\s*input\b[^>]*\bname\s*=\s*["']_pw["']/i.test(source)) {
|
|
215
|
-
diagnostics.push({
|
|
216
|
-
severity: "error",
|
|
217
|
-
code: "password_form_missing",
|
|
218
|
-
message: "The password page needs <sf-password-form> or a form input named _pw.",
|
|
219
|
-
path,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
235
|
if (id === "index" && !/<\s*sf-files\b/i.test(source))
|
|
223
236
|
diagnostics.push({
|
|
224
237
|
severity: "warning",
|
|
@@ -226,7 +239,23 @@ export function validatePageSources(input) {
|
|
|
226
239
|
message: "The index page does not render <sf-files>.",
|
|
227
240
|
path,
|
|
228
241
|
});
|
|
229
|
-
|
|
242
|
+
// Without the lane slot the runtime has nowhere to inject the working
|
|
243
|
+
// sign-in/password/request blocks and the template ships a dead gate.
|
|
244
|
+
if (id === "access" && !/<\s*sf-access-lanes\b/i.test(source))
|
|
245
|
+
diagnostics.push({
|
|
246
|
+
severity: "error",
|
|
247
|
+
code: "access_lanes_missing",
|
|
248
|
+
message: "The access page must render <sf-access-lanes> so visitors keep a way in.",
|
|
249
|
+
path,
|
|
250
|
+
});
|
|
251
|
+
if (id === "access" && accessCspBlocksForms(source))
|
|
252
|
+
diagnostics.push({
|
|
253
|
+
severity: "error",
|
|
254
|
+
code: "access_csp_form_action_missing_self",
|
|
255
|
+
message: "The access page CSP must allow its runtime forms: use form-action 'self' when declaring form-action.",
|
|
256
|
+
path,
|
|
257
|
+
});
|
|
258
|
+
if ((id === "denied" || id === "access") &&
|
|
230
259
|
/<\s*meta\b[^>]*\bname\s*=\s*["']robots["'][^>]*\bcontent\s*=\s*["'][^"']*\bindex\b/i.test(source))
|
|
231
260
|
diagnostics.push({
|
|
232
261
|
severity: "warning",
|
|
@@ -332,18 +361,20 @@ export function defaultPageBody(id) {
|
|
|
332
361
|
// The built-in palette runs through the same readability engine as pushed
|
|
333
362
|
// themes, so buttons and links remain readable for every accepted accent.
|
|
334
363
|
const defaultPageColors = derivePageColors({});
|
|
335
|
-
const sharedCss = `/*sf-default-fonts*/:root{color-scheme:light dark;--sf-accent:${PAGE_COLOR_DEFAULTS.accent};--sf-bg:${PAGE_COLOR_DEFAULTS.bg};--sf-fg:${PAGE_COLOR_DEFAULTS.fg};--sf-muted:${PAGE_COLOR_DEFAULTS.muted};--sf-card:${PAGE_COLOR_DEFAULTS.card};--sf-border:${PAGE_COLOR_DEFAULTS.border};--sf-link:${defaultPageColors.link};--sf-on-accent:${defaultPageColors.onAccent};--sf-error:#a82d1a;--sf-font:"Haskoy",-apple-system,BlinkMacSystemFont,ui-sans-serif,"Segoe UI",sans-serif;--sf-display-font:"Merriweather",Georgia,serif;--sf-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace}*{box-sizing:border-box}html{min-width:320px;background:var(--sf-bg);-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh;background:var(--sf-bg);color:var(--sf-fg);font:14px/1.65 var(--sf-font)}a{color:inherit}.sf-page{min-height:100vh;display:flex;flex-direction:column;padding:clamp(22px,4vw,44px)}.sf-header{display:flex;align-items:center;min-height:32px}.sf-page-center .sf-header{justify-content:center}.sf-brand{display:inline-flex;min-width:0;align-items:center;gap:10px;color:var(--sf-fg);text-decoration:none}.sf-brand-mark{width:7px;height:7px;flex:none;background:var(--sf-accent)}.sf-logo{width:28px;height:28px;flex:none;object-fit:contain}.sf-brand strong{min-width:0;overflow-wrap:anywhere;font-size:13px;font-weight:600}.sf-main{flex:1;width:100%;padding:clamp(34px,9vh,84px) 0}.sf-page-plain .sf-main{max-width:680px}.sf-page-center .sf-main{max-width:620px;margin:auto;display:flex;flex-direction:column;align-items:center;text-align:center}.sf-eyebrow{margin:0 0 14px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.12em;text-transform:uppercase;overflow-wrap:anywhere}.sf-page-center .sf-eyebrow{margin-bottom:16px}h1{max-width:20ch;margin:0 0 12px;color:var(--sf-fg);font:430 clamp(1.9rem,5vw,2.8rem)/1.22 var(--sf-display-font);letter-spacing:-.012em;text-wrap:balance}.sf-copy,.sf-sub{max-width:52ch;margin:0;color:var(--sf-muted);text-wrap:pretty}.sf-inline-path{font-family:var(--sf-mono);font-size:.9em;color:var(--sf-fg);overflow-wrap:anywhere}.sf-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:22px}.sf-page-center .sf-actions{justify-content:center}.sf-button
|
|
364
|
+
const sharedCss = `/*sf-default-fonts*/:root{color-scheme:light dark;--sf-accent:${PAGE_COLOR_DEFAULTS.accent};--sf-bg:${PAGE_COLOR_DEFAULTS.bg};--sf-fg:${PAGE_COLOR_DEFAULTS.fg};--sf-muted:${PAGE_COLOR_DEFAULTS.muted};--sf-card:${PAGE_COLOR_DEFAULTS.card};--sf-border:${PAGE_COLOR_DEFAULTS.border};--sf-link:${defaultPageColors.link};--sf-on-accent:${defaultPageColors.onAccent};--sf-error:#a82d1a;--sf-font:"Haskoy",-apple-system,BlinkMacSystemFont,ui-sans-serif,"Segoe UI",sans-serif;--sf-display-font:"Merriweather",Georgia,serif;--sf-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace}*{box-sizing:border-box}html{min-width:320px;background:var(--sf-bg);-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh;background:var(--sf-bg);color:var(--sf-fg);font:14px/1.65 var(--sf-font)}a{color:inherit}.sf-page{min-height:100vh;display:flex;flex-direction:column;padding:clamp(22px,4vw,44px)}.sf-header{display:flex;align-items:center;min-height:32px}.sf-page-center .sf-header{justify-content:center}.sf-brand{display:inline-flex;min-width:0;align-items:center;gap:10px;color:var(--sf-fg);text-decoration:none}.sf-brand-mark{width:7px;height:7px;flex:none;background:var(--sf-accent)}.sf-logo{width:28px;height:28px;flex:none;object-fit:contain}.sf-brand strong{min-width:0;overflow-wrap:anywhere;font-size:13px;font-weight:600}.sf-main{flex:1;width:100%;padding:clamp(34px,9vh,84px) 0}.sf-page-plain .sf-main{max-width:680px}.sf-page-center .sf-main{max-width:620px;margin:auto;display:flex;flex-direction:column;align-items:center;text-align:center}.sf-eyebrow{margin:0 0 14px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.12em;text-transform:uppercase;overflow-wrap:anywhere}.sf-page-center .sf-eyebrow{margin-bottom:16px}h1{max-width:20ch;margin:0 0 12px;color:var(--sf-fg);font:430 clamp(1.9rem,5vw,2.8rem)/1.22 var(--sf-display-font);letter-spacing:-.012em;text-wrap:balance}.sf-copy,.sf-sub{max-width:52ch;margin:0;color:var(--sf-muted);text-wrap:pretty}.sf-inline-path{font-family:var(--sf-mono);font-size:.9em;color:var(--sf-fg);overflow-wrap:anywhere}.sf-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:22px}.sf-page-center .sf-actions{justify-content:center}.sf-button{display:inline-flex;min-height:44px;align-items:center;justify-content:center;border:0;background:var(--sf-accent);color:var(--sf-on-accent);padding:10px 16px;font:600 13px/1.4 var(--sf-font);text-decoration:none;cursor:pointer}.sf-ghost{color:var(--sf-muted);text-underline-offset:3px}.sf-button:hover{filter:brightness(1.06)}a:focus-visible,button:focus-visible{outline:2px solid var(--sf-link);outline-offset:2px}.sf-copy+.sf-copy{margin-top:12px}.sf-files{width:100%;margin:24px 0 0;padding:0;list-style:none}.sf-files li{display:flex;min-width:0;align-items:baseline;gap:18px;border-bottom:1px solid var(--sf-border)}.sf-files a{min-width:0;flex:1;padding:10px 2px;color:var(--sf-fg);font-weight:500;text-decoration:none}.sf-files a:hover{color:var(--sf-link)}.sf-file-name{overflow-wrap:anywhere}.sf-file-size{flex:none;padding:10px 2px;color:var(--sf-muted);font:12px/1.5 var(--sf-mono);white-space:nowrap}.sf-empty{max-width:420px;margin:24px 0 0;border:1px dashed var(--sf-border);padding:18px 20px;color:var(--sf-muted)}.sf-preview-wrap{width:min(100%,560px);aspect-ratio:16/9;margin:10px 0 18px;overflow:hidden;background:#fff;outline:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);outline-offset:-1px}.sf-preview{display:block;width:100%;height:100%;border:0;object-fit:contain}.sf-file-meta{width:min(100%,560px);margin:0}.sf-file-meta div{display:flex;min-width:0;justify-content:space-between;gap:20px;border-bottom:1px solid var(--sf-border);padding:9px 2px}.sf-file-meta dt{color:var(--sf-fg);font-weight:600}.sf-file-meta dd{min-width:0;margin:0;color:var(--sf-muted);font-family:var(--sf-mono);overflow-wrap:anywhere;text-align:right}.sf-colophon{display:flex;flex-direction:column;align-items:center;gap:10px;padding:44px 0 26px;margin-top:auto;color:var(--sf-muted)}.sf-page-plain .sf-colophon{align-items:flex-start}.sf-colophon-row{display:flex;align-items:center;gap:20px;flex-wrap:wrap}.sf-powered{display:inline-flex;align-items:center;gap:10px;cursor:pointer;outline:none}.sf-powered-line{display:none;font-size:12px;color:var(--sf-muted)}.sf-powered:hover .sf-powered-line,.sf-powered:focus .sf-powered-line,.sf-powered:focus-within .sf-powered-line{display:inline}.sf-powered-line a{color:var(--sf-link);text-decoration:underline;text-underline-offset:3px}.sf-help{font-size:12px;color:var(--sf-muted);text-decoration:underline;text-underline-offset:3px}.sf-help:hover{color:var(--sf-fg)}.sf-wordmark{display:block;flex:none;width:auto;height:11px}.sf-wordmark-shadow{fill:var(--sf-accent)}.sf-wordmark-plate{fill:#141419}.sf-wordmark-paper{fill:#fbf8f1}@media(max-width:520px){.sf-page{padding:20px}.sf-button{min-height:48px}.sf-file-meta div{flex-direction:column;gap:2px}.sf-file-meta dd{text-align:left}.sf-colophon{padding-top:32px}}@media(prefers-color-scheme:dark){:root{--sf-bg:${PAGE_COLOR_DEFAULTS.darkBg};--sf-fg:${PAGE_COLOR_DEFAULTS.darkFg};--sf-muted:${PAGE_COLOR_DEFAULTS.darkMuted};--sf-card:${PAGE_COLOR_DEFAULTS.darkCard};--sf-border:${PAGE_COLOR_DEFAULTS.darkBorder};--sf-link:${defaultPageColors.linkDark};--sf-error:#ff8a7a}.sf-preview-wrap{outline-color:#ffffff1a}}@media(forced-colors:active){.sf-brand-mark{background:CanvasText}.sf-button{border:1px solid ButtonText}.sf-powered:focus{outline:1px solid CanvasText;outline-offset:3px}}`;
|
|
365
|
+
const accessCss = `.sf-page.access .sf-main{max-width:26rem;padding:clamp(42px,8vh,72px) 0}.sf-page.access h1{max-width:18ch;margin-bottom:14px;font:600 clamp(2rem,7vw,2.65rem)/1.12 var(--sf-font);letter-spacing:-.035em}.sf-page.access .sf-copy{font-size:15px;line-height:1.6}.sf-access-default{display:flex;width:100%;flex-direction:column;align-items:center}.sf-access-status{padding:10px 12px;border:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);border-radius:8px;background:color-mix(in srgb,var(--sf-fg) 4%,transparent);color:var(--sf-fg)}.sf-access-lanes{display:flex;flex-direction:column;gap:16px;width:min(23rem,100%);margin-top:30px;text-align:left}.sf-access-lanes form{display:flex;flex-direction:column;gap:14px;margin:0}.sf-input{box-sizing:border-box;width:100%;min-height:46px;padding:11px 12px;border:1px solid color-mix(in srgb,var(--sf-fg) 18%,transparent);border-radius:8px;outline:2px solid transparent;outline-offset:-1px;background:color-mix(in srgb,var(--sf-bg) 92%,var(--sf-fg));color:var(--sf-fg);font:500 15px/1.45 var(--sf-font);appearance:none}.sf-input::placeholder{color:color-mix(in srgb,var(--sf-muted) 82%,transparent);font:inherit}.sf-input:hover{border-color:color-mix(in srgb,var(--sf-fg) 34%,transparent)}.sf-input:focus-visible{border-color:var(--sf-accent);outline-color:var(--sf-accent)}textarea.sf-input{min-height:92px;resize:vertical}.sf-label{color:var(--sf-fg);font:600 13px/1.4 var(--sf-font)}.sf-access-lanes .sf-button{width:100%;min-height:46px;border-radius:8px}.sf-access-or{display:flex;align-items:center;gap:12px;color:var(--sf-muted);font-size:13px}.sf-access-or:before,.sf-access-or:after{content:"";flex:1;border-top:1px solid color-mix(in srgb,var(--sf-fg) 12%,transparent)}.sf-error{margin:0;color:var(--sf-error);font-size:14px}.sf-access-password-row{display:flex;gap:8px}.sf-access-password-row .sf-input{min-width:0;flex:1}.sf-access-password-row .sf-button{width:auto;flex:none}.sf-access-request{padding-top:2px}.sf-access-request summary{padding:4px 0;cursor:pointer;color:var(--sf-muted);font:550 14px/1.5 var(--sf-font)}.sf-access-request summary:hover{color:var(--sf-fg)}.sf-access-request[open] summary{color:var(--sf-fg)}.sf-access-request form{margin-top:16px}@media(max-width:520px){.sf-page.access .sf-main{padding:38px 0}.sf-page.access h1{font-size:2rem}.sf-page.access .sf-copy{font-size:16px}.sf-input{min-height:48px;font-size:16px}.sf-access-lanes .sf-button{min-height:48px}.sf-access-password-row{flex-direction:column}.sf-access-password-row .sf-button{width:100%}}@media(prefers-color-scheme:dark){.sf-access-status{background:color-mix(in srgb,var(--sf-fg) 5%,transparent)}}`;
|
|
336
366
|
const pageHeader = '<header class="sf-header"><a class="sf-brand" href="/" aria-label="Homepage"><span class="sf-brand-mark" aria-hidden="true"></span><sf-logo></sf-logo><strong><sf-name></sf-name></strong></a></header>';
|
|
337
367
|
const pageFooter = "<sf-spacefast-branding></sf-spacefast-branding>";
|
|
338
368
|
function document(id, title, body, layout) {
|
|
339
369
|
const noindex = PAGE_CATALOG[id].noindex ? '<meta name="robots" content="noindex">' : "";
|
|
340
|
-
|
|
370
|
+
const accessClass = id === "access" ? " access" : "";
|
|
371
|
+
const pageCss = id === "access" ? sharedCss + accessCss : sharedCss;
|
|
372
|
+
return `<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${noindex}<title>${title}</title><style>${pageCss}</style></head>\n<body><div class="sf-page${accessClass} sf-page-${layout}">${pageHeader}<main class="sf-main">${body}</main>${pageFooter}</div></body>\n</html>\n`;
|
|
341
373
|
}
|
|
342
374
|
export const DEFAULT_PAGE_TEMPLATES = {
|
|
343
375
|
"404": document("404", "Page not found", '<p class="sf-eyebrow">404 · Not Found</p><h1>Page not found</h1><p class="sf-copy">There’s nothing at <span class="sf-inline-path"><sf-page-path></sf-page-path></span>.</p><div class="sf-actions"><a class="sf-button" href="/">Go to the homepage</a></div>', "center"),
|
|
344
|
-
password: document("password", "Password required", '<h1>This space is protected</h1><p class="sf-sub">Enter the password to continue.</p><sf-password-form></sf-password-form>', "center"),
|
|
345
376
|
denied: document("denied", "Access denied", "<h1>Access denied</h1><sf-denial-context></sf-denial-context>", "center"),
|
|
346
|
-
|
|
377
|
+
access: document("access", "This space is private", '<h1>This space is private</h1><div class="sf-access-default"><sf-access-status></sf-access-status><sf-access-lanes></sf-access-lanes></div>', "center"),
|
|
347
378
|
index: document("index", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><p class="sf-sub"><sf-page-summary></sf-page-summary></p><sf-files></sf-files>', "plain"),
|
|
348
379
|
preview: document("preview", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><sf-file></sf-file>', "plain"),
|
|
349
380
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type DirectPublishUpload = {
|
|
2
|
+
kind: "archive";
|
|
3
|
+
name: string;
|
|
4
|
+
bytes: Uint8Array;
|
|
5
|
+
contentType: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "files";
|
|
8
|
+
files: Array<{
|
|
9
|
+
path: string;
|
|
10
|
+
bytes: Uint8Array;
|
|
11
|
+
contentType?: string | undefined;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
/** Build the canonical multipart body used by direct one-shot publish clients. */
|
|
15
|
+
export declare function createPublishFormData(input: {
|
|
16
|
+
upload: DirectPublishUpload;
|
|
17
|
+
clientAttribution: string;
|
|
18
|
+
spaceId?: string | undefined;
|
|
19
|
+
slug?: string | undefined;
|
|
20
|
+
title?: string | undefined;
|
|
21
|
+
description?: string | undefined;
|
|
22
|
+
}): FormData;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Build the canonical multipart body used by direct one-shot publish clients. */
|
|
2
|
+
export function createPublishFormData(input) {
|
|
3
|
+
const form = new FormData();
|
|
4
|
+
if (input.spaceId)
|
|
5
|
+
form.append("spaceId", input.spaceId);
|
|
6
|
+
if (input.description)
|
|
7
|
+
form.append("description", input.description);
|
|
8
|
+
if (input.slug || input.title) {
|
|
9
|
+
form.append("space", JSON.stringify({
|
|
10
|
+
...(input.slug ? { slug: input.slug } : {}),
|
|
11
|
+
...(input.title ? { title: input.title } : {}),
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
form.append("clientAttribution", input.clientAttribution);
|
|
15
|
+
form.append("source", JSON.stringify({ kind: "agent", client: input.clientAttribution }));
|
|
16
|
+
if (input.upload.kind === "archive") {
|
|
17
|
+
form.append("archive", new File([Uint8Array.from(input.upload.bytes)], input.upload.name, {
|
|
18
|
+
type: input.upload.contentType,
|
|
19
|
+
}));
|
|
20
|
+
return form;
|
|
21
|
+
}
|
|
22
|
+
for (const file of input.upload.files.toSorted((left, right) => left.path.localeCompare(right.path))) {
|
|
23
|
+
form.append("files", new File([Uint8Array.from(file.bytes)], file.path, file.contentType ? { type: file.contentType } : undefined));
|
|
24
|
+
}
|
|
25
|
+
return form;
|
|
26
|
+
}
|
|
@@ -46,6 +46,40 @@ export type PublishManifestResult<T extends {
|
|
|
46
46
|
files: T[];
|
|
47
47
|
warnings: PublishManifestWarning[];
|
|
48
48
|
};
|
|
49
|
+
export type PublishRouteAnalysis = {
|
|
50
|
+
collisions: Array<{
|
|
51
|
+
route: string;
|
|
52
|
+
paths: string[];
|
|
53
|
+
}>;
|
|
54
|
+
caseCollisions: Array<{
|
|
55
|
+
route: string;
|
|
56
|
+
siblingRoute: string;
|
|
57
|
+
paths: string[];
|
|
58
|
+
}>;
|
|
59
|
+
routeOwnership: PublishRouteOwnership[];
|
|
60
|
+
};
|
|
61
|
+
export type PublishRouteOwnership = {
|
|
62
|
+
requestedRoute: string;
|
|
63
|
+
artifactPath: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Canonicalizes a route expanded from an already-canonical manifest path.
|
|
67
|
+
* Manifest intake has decoded percent escapes exactly once, so this must not
|
|
68
|
+
* decode again. It applies only the route-identity aliases shared with access.
|
|
69
|
+
*/
|
|
70
|
+
export declare function canonicalizePublishRequestedRoute(route: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Expands artifact paths into the request routes they can serve. The runtime
|
|
73
|
+
* resolves literal files, directory indexes, and clean `.html` aliases; two
|
|
74
|
+
* different artifacts owning one request route is rejected at publish/finalize
|
|
75
|
+
* instead of depending on lookup order. `routeOwnership` exposes the same
|
|
76
|
+
* expansion in canonical access-route form so authorization and diagnostics
|
|
77
|
+
* never have to infer a route from an artifact path.
|
|
78
|
+
*/
|
|
79
|
+
export declare function analyzePublishRoutes(paths: readonly string[], options?: {
|
|
80
|
+
index?: string | false;
|
|
81
|
+
cleanUrls?: boolean;
|
|
82
|
+
}): PublishRouteAnalysis;
|
|
49
83
|
export declare function resolveManifestFileCap(maxFiles?: number | null): number;
|
|
50
84
|
export declare function validatePublishManifest<T extends {
|
|
51
85
|
path: string;
|
|
@@ -64,7 +98,3 @@ export declare function filterFreePlanManifest<T extends {
|
|
|
64
98
|
allowed: T[];
|
|
65
99
|
ignored: string[];
|
|
66
100
|
};
|
|
67
|
-
export declare function filterFreePlanHeaders(content: string): {
|
|
68
|
-
headers: string | undefined;
|
|
69
|
-
droppedBasicAuthCount: number;
|
|
70
|
-
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BRAND } from "../brand.js";
|
|
2
2
|
import { WORST_CASE_QUOTAS } from "../contracts/quotas.js";
|
|
3
3
|
import { SPACE_CONFIG_PRIVATE_FILES } from "../contracts/space-config.js";
|
|
4
|
+
import { ZERO_RUNTIME_DEPLOY_PATH } from "../contracts/zero.js";
|
|
4
5
|
// The ONE static publish policy (internal-docs/platform.md "Upload Contract"):
|
|
5
6
|
// path denials, manifest scale caps, plan caps, duplicate detection, and the
|
|
6
7
|
// free-plan content rules live here. Consumed by API manifest validation, CLI
|
|
@@ -60,6 +61,10 @@ const STATIC_EXECUTION_CONTROL_FILES = new Set([".htaccess", ".user.ini"]);
|
|
|
60
61
|
// (`/__stattic_probe` today, more `/__stattic/*` routes planned) stays
|
|
61
62
|
// reserved the same way. Mid-segment occurrences are ordinary content.
|
|
62
63
|
const STATIC_RUNTIME_CONTROL_SEGMENT_PREFIXES = ["__spacefast", "__stattic"];
|
|
64
|
+
// Compact platform endpoints live below `/__sf`. Reserve the complete root
|
|
65
|
+
// segment prefix before the first endpoint ships so uploaded content can never
|
|
66
|
+
// shadow a later platform route. Nested `__sf*` directories remain content.
|
|
67
|
+
const STATIC_RUNTIME_ROOT_SEGMENT_PREFIXES = ["__sf"];
|
|
63
68
|
// Both the current and the legacy control-path spellings stay denied: spaces
|
|
64
69
|
// published before the `.stattic` → `.spacefast` rename still serve from the
|
|
65
70
|
// legacy on-disk layout, and user archives may carry either name.
|
|
@@ -76,13 +81,46 @@ const STATIC_RUNTIME_CONTROL_PREFIXES = [
|
|
|
76
81
|
export function normalizePublishPath(relativePath) {
|
|
77
82
|
return relativePath.replace(/\\/g, "/").replace(/^\/+/g, "");
|
|
78
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The one subtree inside the runtime control prefix that a publish may write.
|
|
86
|
+
*
|
|
87
|
+
* `sf publish` compiles a version's worker into `__spacefast/functions/bundles/`
|
|
88
|
+
* so the bundle travels through the ordinary manifest and upload path instead of
|
|
89
|
+
* needing a parallel one. Keeping it under the control prefix is what makes it
|
|
90
|
+
* unservable as static content by default — the runtime answers it only behind a
|
|
91
|
+
* signature.
|
|
92
|
+
*
|
|
93
|
+
* Scoped to `bundles/` rather than the whole `__spacefast/functions/` directory
|
|
94
|
+
* on purpose: the sibling `config.json` holds the key those signatures are
|
|
95
|
+
* minted with, and a publish that could overwrite it would be a publish that
|
|
96
|
+
* could mint its own tokens.
|
|
97
|
+
*/
|
|
98
|
+
const PUBLISHABLE_BUILD_ARTIFACT_PREFIX = "__spacefast/functions/bundles/";
|
|
99
|
+
/**
|
|
100
|
+
* The one FILE inside the control prefix a publish may write.
|
|
101
|
+
*
|
|
102
|
+
* A Zero capsule's compiled server travels as version metadata, not content, so
|
|
103
|
+
* a server-only change ships a byte-identical app shell and the version intent's
|
|
104
|
+
* manifest comparison reads it as "no changes". `sf publish` writes the deploy's
|
|
105
|
+
* digest here so the manifest moves whenever the deploy does — same reasoning as
|
|
106
|
+
* the Functions bundle above, same channel.
|
|
107
|
+
*
|
|
108
|
+
* One exact path, not a prefix: its siblings under `__spacefast/zero/` are the
|
|
109
|
+
* runtime's own artifact, config and binary, and a publish that could overwrite
|
|
110
|
+
* those would be a publish that could replace the runtime.
|
|
111
|
+
*/
|
|
112
|
+
function isPublishableBuildArtifactPath(normalizedPath, lowerPath) {
|
|
113
|
+
return (lowerPath.startsWith(PUBLISHABLE_BUILD_ARTIFACT_PREFIX) ||
|
|
114
|
+
normalizedPath === ZERO_RUNTIME_DEPLOY_PATH);
|
|
115
|
+
}
|
|
79
116
|
export function publishPathIgnored(relativePath) {
|
|
80
117
|
const path = normalizePublishPath(relativePath);
|
|
81
118
|
// The agent state dirs hold one secret (state.json) plus committable tooling
|
|
82
119
|
// (space.json). Drop only those files at any depth so a checkout archived one
|
|
83
120
|
// level too high never ships a claim token — but keep serving legitimate
|
|
84
121
|
// content in the same dirs (e.g. the legacy .stattic/routes.json convention).
|
|
85
|
-
if (/(?:^|\/)\.(?:spacefast|stattic)\/(?:state|space)\.json$/.test(path)
|
|
122
|
+
if (/(?:^|\/)\.(?:spacefast|stattic)\/(?:state|space)\.json$/.test(path) ||
|
|
123
|
+
/(?:^|\/)\.spacefast-(?:[0-9a-f-]{36}\.(?:tmp|rollback|retired)|[0-9a-f]{24}\.lock)$/.test(path)) {
|
|
86
124
|
return true;
|
|
87
125
|
}
|
|
88
126
|
if (IGNORED_EXACT_PATHS.has(path) || /(?:^|\/)\.next\/cache(?:\/|$)/.test(path)) {
|
|
@@ -101,27 +139,27 @@ export function publishPathIsConvention(relativePath) {
|
|
|
101
139
|
// Headers", migration diagnostics): Cloudflare Pages `_worker.js`/`_routes.json`
|
|
102
140
|
// and Pages/Netlify function directories publish as inert content with a
|
|
103
141
|
// warning so migrations are never silent.
|
|
142
|
+
function providerRuntimeFeatureForPath(path) {
|
|
143
|
+
if (path === "_worker.js") {
|
|
144
|
+
return "Cloudflare Pages _worker.js";
|
|
145
|
+
}
|
|
146
|
+
if (path === "_routes.json") {
|
|
147
|
+
return "Cloudflare Pages _routes.json";
|
|
148
|
+
}
|
|
149
|
+
if (path.startsWith("functions/")) {
|
|
150
|
+
return "Pages Functions (functions/)";
|
|
151
|
+
}
|
|
152
|
+
if (path.startsWith("netlify/functions/")) {
|
|
153
|
+
return "Netlify Functions (netlify/functions/)";
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
104
157
|
export function providerRuntimeFeatureWarnings(paths) {
|
|
105
158
|
const warnings = [];
|
|
106
159
|
const seen = new Set();
|
|
107
|
-
const featureForPath = (path) => {
|
|
108
|
-
if (path === "_worker.js") {
|
|
109
|
-
return "Cloudflare Pages _worker.js";
|
|
110
|
-
}
|
|
111
|
-
if (path === "_routes.json") {
|
|
112
|
-
return "Cloudflare Pages _routes.json";
|
|
113
|
-
}
|
|
114
|
-
if (path.startsWith("functions/")) {
|
|
115
|
-
return "Pages Functions (functions/)";
|
|
116
|
-
}
|
|
117
|
-
if (path.startsWith("netlify/functions/")) {
|
|
118
|
-
return "Netlify Functions (netlify/functions/)";
|
|
119
|
-
}
|
|
120
|
-
return null;
|
|
121
|
-
};
|
|
122
160
|
for (const rawPath of paths) {
|
|
123
161
|
const path = normalizePublishPath(rawPath);
|
|
124
|
-
const feature =
|
|
162
|
+
const feature = providerRuntimeFeatureForPath(path);
|
|
125
163
|
if (!feature || seen.has(feature)) {
|
|
126
164
|
continue;
|
|
127
165
|
}
|
|
@@ -179,11 +217,15 @@ export function staticUploadPathViolation(path, options = {}) {
|
|
|
179
217
|
if (reservedRuntimeControlFiles?.has(lowerName) && lowerPath === lowerName) {
|
|
180
218
|
return pathViolation("static_runtime_control_path_not_supported", "Static deploys cannot upload runtime control paths.", normalizedPath);
|
|
181
219
|
}
|
|
220
|
+
const lowerRootSegment = segments[0]?.toLowerCase() ?? "";
|
|
221
|
+
if (STATIC_RUNTIME_ROOT_SEGMENT_PREFIXES.some((prefix) => lowerRootSegment.startsWith(prefix))) {
|
|
222
|
+
return pathViolation("static_runtime_control_path_not_supported", "Static deploys cannot upload runtime control paths.", normalizedPath);
|
|
223
|
+
}
|
|
182
224
|
const controlSegment = segments.find((segment) => {
|
|
183
225
|
const lowerSegment = segment.toLowerCase();
|
|
184
226
|
return STATIC_RUNTIME_CONTROL_SEGMENT_PREFIXES.some((prefix) => lowerSegment.startsWith(prefix));
|
|
185
227
|
});
|
|
186
|
-
if (controlSegment) {
|
|
228
|
+
if (controlSegment && !isPublishableBuildArtifactPath(normalizedPath, lowerPath)) {
|
|
187
229
|
return pathViolation("static_runtime_control_path_not_supported", "Static deploys cannot upload runtime control paths.", normalizedPath);
|
|
188
230
|
}
|
|
189
231
|
if (STATIC_RUNTIME_CONTROL_PREFIXES.some((prefix) => lowerPath === prefix || lowerPath.startsWith(`${prefix}/`))) {
|
|
@@ -203,6 +245,95 @@ export function firstStaticUploadPathViolation(paths, options = {}) {
|
|
|
203
245
|
}
|
|
204
246
|
return null;
|
|
205
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Canonicalizes a route expanded from an already-canonical manifest path.
|
|
250
|
+
* Manifest intake has decoded percent escapes exactly once, so this must not
|
|
251
|
+
* decode again. It applies only the route-identity aliases shared with access.
|
|
252
|
+
*/
|
|
253
|
+
export function canonicalizePublishRequestedRoute(route) {
|
|
254
|
+
if (!route.startsWith("/")) {
|
|
255
|
+
throw new TypeError("publish requested routes must be absolute");
|
|
256
|
+
}
|
|
257
|
+
const segments = route
|
|
258
|
+
.split("/")
|
|
259
|
+
.filter((segment) => segment !== "" && segment !== ".")
|
|
260
|
+
.map((segment) => segment.normalize("NFC"));
|
|
261
|
+
if (segments.at(-1) === "index.html")
|
|
262
|
+
segments.pop();
|
|
263
|
+
return segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Expands artifact paths into the request routes they can serve. The runtime
|
|
267
|
+
* resolves literal files, directory indexes, and clean `.html` aliases; two
|
|
268
|
+
* different artifacts owning one request route is rejected at publish/finalize
|
|
269
|
+
* instead of depending on lookup order. `routeOwnership` exposes the same
|
|
270
|
+
* expansion in canonical access-route form so authorization and diagnostics
|
|
271
|
+
* never have to infer a route from an artifact path.
|
|
272
|
+
*/
|
|
273
|
+
export function analyzePublishRoutes(paths, options = {}) {
|
|
274
|
+
const index = options.index === undefined ? "index.html" : options.index;
|
|
275
|
+
const cleanUrls = options.cleanUrls ?? true;
|
|
276
|
+
const routes = new Map();
|
|
277
|
+
const add = (route, path) => {
|
|
278
|
+
const owners = routes.get(route) ?? new Set();
|
|
279
|
+
owners.add(path);
|
|
280
|
+
routes.set(route, owners);
|
|
281
|
+
};
|
|
282
|
+
for (const path of paths) {
|
|
283
|
+
add(`/${path}`, path);
|
|
284
|
+
if (cleanUrls && path.endsWith(".html")) {
|
|
285
|
+
add(`/${path.slice(0, -".html".length)}`, path);
|
|
286
|
+
}
|
|
287
|
+
if (index !== false) {
|
|
288
|
+
if (path === index) {
|
|
289
|
+
add("/", path);
|
|
290
|
+
}
|
|
291
|
+
else if (path.endsWith(`/${index}`)) {
|
|
292
|
+
const directory = path.slice(0, -(index.length + 1));
|
|
293
|
+
add(`/${directory}`, path);
|
|
294
|
+
add(`/${directory}/`, path);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const collisions = [...routes.entries()]
|
|
299
|
+
.filter(([, owners]) => owners.size > 1)
|
|
300
|
+
.map(([route, owners]) => ({ route, paths: [...owners].toSorted() }))
|
|
301
|
+
.toSorted((left, right) => left.route.localeCompare(right.route));
|
|
302
|
+
const firstByFoldedRoute = new Map();
|
|
303
|
+
const caseCollisions = [];
|
|
304
|
+
for (const [route, owners] of routes) {
|
|
305
|
+
const folded = route.toLowerCase();
|
|
306
|
+
const pathsForRoute = [...owners].toSorted();
|
|
307
|
+
const sibling = firstByFoldedRoute.get(folded);
|
|
308
|
+
if (sibling && sibling.route !== route) {
|
|
309
|
+
caseCollisions.push({
|
|
310
|
+
route,
|
|
311
|
+
siblingRoute: sibling.route,
|
|
312
|
+
paths: [...new Set([...sibling.paths, ...pathsForRoute])].toSorted(),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
else if (!sibling) {
|
|
316
|
+
firstByFoldedRoute.set(folded, { route, paths: pathsForRoute });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const canonicalOwnership = new Map();
|
|
320
|
+
for (const [route, owners] of routes) {
|
|
321
|
+
const canonicalRoute = canonicalizePublishRequestedRoute(route);
|
|
322
|
+
const canonicalOwners = canonicalOwnership.get(canonicalRoute) ?? new Set();
|
|
323
|
+
for (const owner of owners)
|
|
324
|
+
canonicalOwners.add(owner);
|
|
325
|
+
canonicalOwnership.set(canonicalRoute, canonicalOwners);
|
|
326
|
+
}
|
|
327
|
+
const routeOwnership = [...canonicalOwnership.entries()]
|
|
328
|
+
.flatMap(([requestedRoute, owners]) => [...owners].map((artifactPath) => ({ requestedRoute, artifactPath })))
|
|
329
|
+
.toSorted((left, right) => left.requestedRoute.localeCompare(right.requestedRoute) ||
|
|
330
|
+
left.artifactPath.localeCompare(right.artifactPath));
|
|
331
|
+
return {
|
|
332
|
+
collisions,
|
|
333
|
+
caseCollisions: caseCollisions.toSorted((left, right) => left.route.localeCompare(right.route)),
|
|
334
|
+
routeOwnership,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
206
337
|
export function resolveManifestFileCap(maxFiles) {
|
|
207
338
|
return Math.min(MANIFEST_MAX_FILES_CEILING, maxFiles && maxFiles > 0 ? maxFiles : MANIFEST_MAX_FILES_CEILING);
|
|
208
339
|
}
|
|
@@ -374,8 +505,6 @@ export const ANONYMOUS_SERVE_CONTENT_TYPES = [
|
|
|
374
505
|
"application/rss+xml",
|
|
375
506
|
"application/atom+xml",
|
|
376
507
|
];
|
|
377
|
-
const LINE_SPLIT_PATTERN = /\r?\n/;
|
|
378
|
-
const BASIC_AUTH_HEADER_PATTERN = /^basic-auth\s*:/i;
|
|
379
508
|
function extensionForPath(filePath) {
|
|
380
509
|
const normalized = normalizePublishPath(filePath).toLowerCase();
|
|
381
510
|
const fileName = normalized.split("/").at(-1) ?? normalized;
|
|
@@ -449,45 +578,3 @@ export function filterFreePlanManifest(files, options) {
|
|
|
449
578
|
}
|
|
450
579
|
return { allowed, ignored };
|
|
451
580
|
}
|
|
452
|
-
// Free-plan `_headers` compile policy (spec "Plans And Limits", amended
|
|
453
|
-
// 2026-06-10): custom response headers stay FREE; the Basic-Auth header
|
|
454
|
-
// MECHANISM is the paid feature. Basic-Auth operations are dropped from free
|
|
455
|
-
// compiles with a `free_headers_basic_auth_disabled` warning; every other
|
|
456
|
-
// header operation passes through untouched.
|
|
457
|
-
export function filterFreePlanHeaders(content) {
|
|
458
|
-
const output = [];
|
|
459
|
-
let droppedBasicAuthCount = 0;
|
|
460
|
-
let pendingMatcher = null;
|
|
461
|
-
let matcherEmitted = false;
|
|
462
|
-
const emitOperation = (rawLine) => {
|
|
463
|
-
if (pendingMatcher !== null && !matcherEmitted) {
|
|
464
|
-
output.push(pendingMatcher);
|
|
465
|
-
matcherEmitted = true;
|
|
466
|
-
}
|
|
467
|
-
output.push(rawLine);
|
|
468
|
-
};
|
|
469
|
-
for (const rawLine of content.split(LINE_SPLIT_PATTERN)) {
|
|
470
|
-
const trimmed = rawLine.trim();
|
|
471
|
-
if (!trimmed || trimmed.startsWith("#")) {
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
if (!rawLine.startsWith(" ") && !rawLine.startsWith("\t")) {
|
|
475
|
-
pendingMatcher = trimmed;
|
|
476
|
-
matcherEmitted = false;
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
if (pendingMatcher === null) {
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
if (BASIC_AUTH_HEADER_PATTERN.test(trimmed)) {
|
|
483
|
-
droppedBasicAuthCount += 1;
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
emitOperation(rawLine);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
return {
|
|
490
|
-
headers: output.length > 0 ? `${output.join("\n")}\n` : undefined,
|
|
491
|
-
droppedBasicAuthCount,
|
|
492
|
-
};
|
|
493
|
-
}
|
|
@@ -8,7 +8,6 @@ export declare const qk: {
|
|
|
8
8
|
agentAccounts: (teamId: string) => readonly ["agent-accounts", string];
|
|
9
9
|
myShelf: () => readonly ["my-shelf"];
|
|
10
10
|
mcpDashboard: (teamId: string) => readonly ["mcp", "dashboard", string];
|
|
11
|
-
mcpApproval: (teamId: string, id: string) => readonly ["mcp", "approval", string, string];
|
|
12
11
|
teams: () => readonly ["teams", "list"];
|
|
13
12
|
team: (ref: string) => readonly ["teams", "by-ref", string];
|
|
14
13
|
teamMembers: (id: string) => readonly ["teams", string, "members"];
|
|
@@ -53,7 +52,6 @@ export declare const qk: {
|
|
|
53
52
|
spaceVersionComments: (id: string, versionId: string, status?: string) => readonly ["spaces", string, "versions", string, "comments"] | readonly ["spaces", string, "versions", string, "comments", string];
|
|
54
53
|
spaceVersionFiles: (id: string, versionId: string) => readonly ["spaces", string, "versions", string, "files"];
|
|
55
54
|
spaceVersionFile: (id: string, versionId: string, path: string) => readonly ["spaces", string, "versions", string, "files", string];
|
|
56
|
-
spaceVersionFileDirectUrl: (id: string, versionId: string, path: string) => readonly ["spaces", string, "versions", string, "files", string, "direct-url"];
|
|
57
55
|
spaceVersionFileUrlPreview: (id: string, versionId: string, path: string) => readonly ["spaces", string, "versions", string, "files", string, "url-preview"];
|
|
58
56
|
spaceVersionFileDiff: (id: string, versionId: string, baseVersionId?: string) => readonly ["spaces", string, "versions", string, "files", "diff", string];
|
|
59
57
|
spaceVersionConventionArtifacts: (id: string, versionId: string) => readonly ["spaces", string, "versions", string, "convention-artifacts"];
|
|
@@ -63,20 +61,17 @@ export declare const qk: {
|
|
|
63
61
|
spaceDomains: (id: string) => readonly ["spaces", string, "domains"];
|
|
64
62
|
spaceRuntimeStatus: (id: string) => readonly ["spaces", string, "runtime"];
|
|
65
63
|
spaceApplyState: (id: string) => readonly ["spaces", string, "apply-state"];
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
spaceDatabase: (id: string) => readonly ["spaces", string, "db"];
|
|
65
|
+
spaceDatabaseDump: (id: string, table: string | undefined, limit: number) => readonly ["spaces", string, "db", "dump", string, number];
|
|
66
|
+
spaceStorage: (id: string) => readonly ["spaces", string, "storage"];
|
|
67
|
+
spaceRuntimeLogs: (id: string, limit: number) => readonly ["spaces", string, "logs", "runtime", number];
|
|
68
|
+
spaceRuntimeRealtimeEvents: (id: string, limit: number) => readonly ["spaces", string, "runtime", "realtime", "events", number];
|
|
70
69
|
spaceVariables: (id: string) => readonly ["spaces", string, "variables"];
|
|
71
70
|
spaceCertificates: (id: string) => readonly ["spaces", string, "runtime", "certificates"];
|
|
72
71
|
spaceLogs: (id: string, kind: "build" | "access" | "runtime") => readonly ["spaces", string, "logs", "build" | "runtime" | "access"];
|
|
73
72
|
spaceAnalytics: (id: string, window: string) => readonly ["spaces", string, "analytics", string];
|
|
74
|
-
spaceFirewall: (id: string) => readonly ["spaces", string, "access", "firewall"];
|
|
75
|
-
spaceAccessViews: (id: string, since?: string) => readonly ["spaces", string, "access", "views", string];
|
|
76
|
-
teamServiceTokens: (teamId: string) => readonly ["teams", string, "access", "service-tokens"];
|
|
77
73
|
teamAgentHandoffs: (teamId: string) => readonly ["teams", string, "agent-handoffs"];
|
|
78
74
|
teamAccessDefaults: (teamId: string) => readonly ["teams", string, "access", "defaults"];
|
|
79
|
-
teamAccessConnections: (teamId: string) => readonly ["teams", string, "access", "connections"];
|
|
80
75
|
domain: (teamId: string, ref: string) => readonly ["teams", string, "domains", string];
|
|
81
76
|
domainDiagnostics: (teamId: string, domainId: string) => readonly ["teams", string, "domains", string, "diagnostics"];
|
|
82
77
|
domainDnsCapabilities: (domainId: string) => readonly ["domains", string, "dns", "capabilities"];
|
|
@@ -84,4 +79,7 @@ export declare const qk: {
|
|
|
84
79
|
domainDnsOperations: (domainId: string) => readonly ["domains", string, "dns", "operations"];
|
|
85
80
|
domainActions: (domainId: string) => readonly ["domains", string, "actions"];
|
|
86
81
|
domainProviderConnections: (teamId: string) => readonly ["domains", "provider-connections", string];
|
|
82
|
+
notifications: () => readonly ["notifications"];
|
|
83
|
+
notificationsList: (unreadOnly: boolean) => readonly ["notifications", "list", boolean];
|
|
84
|
+
notificationsUnreadCount: () => readonly ["notifications", "unread-count"];
|
|
87
85
|
};
|