@spacefast/common 0.0.11 → 0.0.13
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/connect-targets.d.ts +4 -4
- package/dist/agents/connect-targets.js +3 -3
- package/dist/agents/private-key-oauth.d.ts +37 -0
- package/dist/agents/private-key-oauth.js +158 -0
- package/dist/config/domains.js +1 -1
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +1 -0
- package/dist/config/postgres-budget.d.ts +126 -0
- package/dist/config/postgres-budget.js +135 -0
- package/dist/config/postgres-budget.json +50 -0
- package/dist/contracts/access.d.ts +386 -1183
- package/dist/contracts/access.js +240 -1018
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +11 -19
- package/dist/contracts/api-keys.d.ts +69 -45
- package/dist/contracts/api-keys.js +3 -0
- package/dist/contracts/archives.d.ts +9 -10
- package/dist/contracts/archives.js +4 -6
- package/dist/contracts/builds.d.ts +8 -8
- package/dist/contracts/builds.js +28 -48
- package/dist/contracts/channels.js +1 -2
- package/dist/contracts/comments.d.ts +102 -93
- package/dist/contracts/comments.js +121 -63
- package/dist/contracts/common.d.ts +4 -3
- package/dist/contracts/common.js +2 -0
- 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/deployments.d.ts +3 -3
- package/dist/contracts/device-auth.d.ts +21 -15
- package/dist/contracts/docs.d.ts +5 -5
- package/dist/contracts/domains.d.ts +261 -1330
- package/dist/contracts/domains.js +39 -301
- package/dist/contracts/email-preferences.d.ts +50 -0
- package/dist/contracts/email-preferences.js +44 -0
- package/dist/contracts/enums.d.ts +3 -5
- package/dist/contracts/enums.js +3 -8
- package/dist/contracts/error-code-meta.d.ts +99 -33
- package/dist/contracts/error-code-meta.js +36 -9
- package/dist/contracts/error-codes.d.ts +1 -1
- package/dist/contracts/error-codes.js +28 -11
- package/dist/contracts/execution.d.ts +212 -0
- package/dist/contracts/execution.js +288 -0
- package/dist/contracts/feature-lifecycle.d.ts +498 -2
- package/dist/contracts/feature-lifecycle.js +539 -2
- package/dist/contracts/features.d.ts +184 -74
- package/dist/contracts/features.js +4 -303
- package/dist/contracts/functions.d.ts +729 -0
- package/dist/contracts/functions.js +485 -0
- package/dist/contracts/git.d.ts +1 -1
- package/dist/contracts/grants.d.ts +1158 -0
- package/dist/contracts/grants.js +696 -0
- package/dist/contracts/ids.d.ts +6 -5
- package/dist/contracts/ids.js +6 -5
- package/dist/contracts/internal.d.ts +3 -3
- package/dist/contracts/mcp.d.ts +12 -12
- package/dist/contracts/me.js +1 -1
- package/dist/contracts/mounts.d.ts +142 -0
- package/dist/contracts/mounts.js +56 -0
- package/dist/contracts/oauth-resources.d.ts +5 -5
- package/dist/contracts/oauth-resources.js +2 -0
- package/dist/contracts/operations.d.ts +4 -4
- package/dist/contracts/pages.d.ts +6 -14
- package/dist/contracts/pages.js +11 -23
- package/dist/contracts/plan-policy.d.ts +2 -9
- package/dist/contracts/plan-policy.js +1 -4
- package/dist/contracts/platform.d.ts +80 -32
- package/dist/contracts/platform.js +59 -12
- package/dist/contracts/push-new.d.ts +26 -2
- package/dist/contracts/push-new.js +8 -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 +21 -85
- package/dist/contracts/resources.js +17 -47
- package/dist/contracts/runtime-api.d.ts +799 -192
- package/dist/contracts/runtime-api.js +231 -100
- package/dist/contracts/runtime-app.d.ts +138 -0
- package/dist/contracts/runtime-app.js +21 -0
- package/dist/contracts/sf-config-v1.d.ts +5 -0
- package/dist/contracts/sf-config-v1.js +1 -1
- package/dist/contracts/sites.d.ts +17 -12
- package/dist/contracts/sites.js +30 -12
- package/dist/contracts/space-config.d.ts +46 -117
- package/dist/contracts/space-config.js +323 -25
- package/dist/contracts/spaces.d.ts +392 -256
- package/dist/contracts/spaces.js +163 -83
- package/dist/contracts/superadmin-emails.d.ts +27 -3
- package/dist/contracts/superadmin-emails.js +10 -15
- package/dist/contracts/superadmin-runtime.d.ts +2 -2
- package/dist/contracts/superadmin-spaces.d.ts +19 -142
- package/dist/contracts/superadmin-spaces.js +7 -0
- package/dist/contracts/tags.d.ts +66 -36
- package/dist/contracts/tags.js +195 -11
- package/dist/contracts/teams.d.ts +9 -9
- package/dist/contracts/transfers.d.ts +1 -2
- package/dist/contracts/variables.d.ts +4 -4
- package/dist/contracts/variables.js +4 -3
- package/dist/contracts/webhooks.d.ts +2 -2
- package/dist/contracts/zero.d.ts +437 -193
- package/dist/contracts/zero.js +149 -158
- package/dist/dashboard-paths/index.d.ts +3 -3
- package/dist/dashboard-paths/index.js +1 -0
- package/dist/docs/agent-prose.js +10 -8
- package/dist/docs/agent-setup.d.ts +6 -4
- package/dist/docs/agent-setup.js +26 -16
- package/dist/docs/agent-solutions.js +1 -1
- package/dist/docs/catalog.d.ts +35 -37
- package/dist/docs/catalog.js +36 -38
- package/dist/docs/error-docs.js +108 -44
- package/dist/utils/asset-fingerprint.d.ts +11 -0
- package/dist/utils/asset-fingerprint.js +46 -0
- package/dist/utils/auth-redirect.d.ts +1 -3
- package/dist/utils/auth-redirect.js +1 -21
- package/dist/utils/build-settings.d.ts +2 -0
- package/dist/utils/build-settings.js +61 -9
- package/dist/utils/credential-policy.d.ts +1 -1
- package/dist/utils/credential-policy.js +4 -0
- package/dist/utils/dns-instructions.d.ts +29 -0
- package/dist/utils/dns-instructions.js +78 -0
- package/dist/utils/domain-diagnostics.d.ts +29 -0
- package/dist/utils/domain-diagnostics.js +63 -0
- package/dist/utils/egress-policy.fixtures.json +51 -0
- package/dist/utils/grants.d.ts +53 -0
- package/dist/utils/grants.js +160 -0
- package/dist/utils/id-hints.d.ts +1 -0
- package/dist/utils/id-hints.js +55 -13
- package/dist/utils/local-space-state.js +1 -0
- package/dist/utils/page-colors.d.ts +50 -0
- package/dist/utils/page-colors.js +167 -0
- package/dist/utils/page-fonts.d.ts +2 -0
- package/dist/utils/page-fonts.js +4 -0
- package/dist/utils/pages.d.ts +19 -6
- package/dist/utils/pages.js +122 -49
- package/dist/utils/production-runtime.d.ts +18 -0
- package/dist/utils/production-runtime.js +28 -0
- package/dist/utils/publish-policy.d.ts +34 -4
- package/dist/utils/publish-policy.js +132 -61
- package/dist/utils/query-keys.d.ts +7 -13
- package/dist/utils/query-keys.js +12 -18
- package/dist/utils/source-archive-policy.d.ts +2 -0
- package/dist/utils/source-archive-policy.js +2 -0
- package/dist/utils/space-config.d.ts +5 -1
- package/dist/utils/space-config.js +35 -0
- package/dist/utils/static-runtime-policy.d.ts +5 -0
- package/dist/utils/static-runtime-policy.fixtures.json +57 -0
- package/dist/utils/static-runtime-policy.js +54 -10
- package/dist/utils/upload-session.d.ts +3 -0
- package/dist/utils/upload-session.js +31 -0
- package/package.json +6 -4
- package/dist/contracts/annotations.d.ts +0 -327
- package/dist/contracts/annotations.js +0 -235
- package/dist/contracts/generated-feature-launch-entries.d.ts +0 -2
- package/dist/contracts/generated-feature-launch-entries.js +0 -518
- package/dist/contracts/publishes.d.ts +0 -135
- package/dist/contracts/publishes.js +0 -162
- package/dist/utils/access-match.d.ts +0 -39
- package/dist/utils/access-match.js +0 -296
- package/dist/utils/sf-config-v1.d.ts +0 -2
- package/dist/utils/sf-config-v1.js +0 -542
package/dist/utils/pages.js
CHANGED
|
@@ -1,7 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SPACEFAST_WORDMARK_PATH } from "../brand-assets.js";
|
|
2
|
+
import { CUSTOM_PAGE_IDS, PAGE_CATALOG, PAGE_ELEMENTS, PAGE_LAYOUT_FILE, PAGE_SOURCE_MAX_BYTES, SITE_PAGE_IDS, } from "../contracts/pages.js";
|
|
3
|
+
import { SPACE_CONFIG_ACCEPTED_FILES } from "../contracts/space-config.js";
|
|
4
|
+
import { derivePageColors, PAGE_COLOR_DEFAULTS } from "./page-colors.js";
|
|
5
|
+
import { PAGE_EMBEDDED_FONT_FACES, PAGE_FONT_FACES } from "./page-fonts.js";
|
|
6
|
+
/**
|
|
7
|
+
* One URL, one representation. A file's bare path always serves its bytes;
|
|
8
|
+
* the preview page is reachable only under this query flag. The alternative —
|
|
9
|
+
* negotiating on `Accept` at the bare path — depends on `Vary`, which the edge
|
|
10
|
+
* ignores, so whichever client arrived first pinned that representation for
|
|
11
|
+
* everyone (an `<img>` could poison the human's link for a year).
|
|
12
|
+
*/
|
|
13
|
+
export const PAGE_PREVIEW_QUERY = "spacefast_view";
|
|
14
|
+
/** The preview page for a file whose bytes live at `fileHref`. */
|
|
15
|
+
export function previewPageHref(fileHref) {
|
|
16
|
+
return `${fileHref}${fileHref.includes("?") ? "&" : "?"}${PAGE_PREVIEW_QUERY}=1`;
|
|
17
|
+
}
|
|
2
18
|
const encoder = new TextEncoder();
|
|
3
19
|
const knownElements = new Set(PAGE_ELEMENTS);
|
|
4
20
|
const customPageIds = new Set(CUSTOM_PAGE_IDS);
|
|
21
|
+
export function isPagePublicPath(path) {
|
|
22
|
+
const lower = path.toLowerCase();
|
|
23
|
+
if (lower === "theme.json" ||
|
|
24
|
+
lower === "_redirects" ||
|
|
25
|
+
lower === "_headers" ||
|
|
26
|
+
lower === "_config.json" ||
|
|
27
|
+
lower === "_routes.json" ||
|
|
28
|
+
SPACE_CONFIG_ACCEPTED_FILES.some((candidate) => candidate === lower) ||
|
|
29
|
+
lower.split("/").includes("_pages") ||
|
|
30
|
+
lower === "_layout.html" ||
|
|
31
|
+
lower.endsWith("/_layout.html"))
|
|
32
|
+
return false;
|
|
33
|
+
return !lower
|
|
34
|
+
.split("/")
|
|
35
|
+
.some((segment, index) => segment.startsWith(".") && !(index === 0 && segment === ".well-known"));
|
|
36
|
+
}
|
|
5
37
|
export function escapePageHtml(value) {
|
|
6
38
|
return value
|
|
7
39
|
.replaceAll("&", "&")
|
|
@@ -10,38 +42,80 @@ export function escapePageHtml(value) {
|
|
|
10
42
|
.replaceAll('"', """)
|
|
11
43
|
.replaceAll("'", "'");
|
|
12
44
|
}
|
|
45
|
+
// Pushed colors are intent; the readability engine (page-colors.ts) derives
|
|
46
|
+
// what the templates actually paint with. A custom background pins a complete
|
|
47
|
+
// scheme for both OS modes — emitting `--sf-bg` alone used to leave dark-mode
|
|
48
|
+
// visitors with the dark foreground on the user's light background.
|
|
13
49
|
function themeStyle(theme) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
50
|
+
const root = [];
|
|
51
|
+
const dark = [];
|
|
52
|
+
if (theme.accent || theme.background) {
|
|
53
|
+
const derived = derivePageColors(theme);
|
|
54
|
+
// `derived.accent` is the parse-validated input (or the default), never
|
|
55
|
+
// an arbitrary string straight from the theme.
|
|
56
|
+
if (theme.accent)
|
|
57
|
+
root.push(`--sf-accent:${derived.accent}`);
|
|
58
|
+
root.push(`--sf-on-accent:${derived.onAccent}`, `--sf-link:${derived.link}`);
|
|
59
|
+
if (derived.pinned) {
|
|
60
|
+
root.push(`--sf-bg:${derived.pinned.bg}`, `--sf-fg:${derived.pinned.fg}`, `--sf-muted:${derived.pinned.muted}`, `--sf-card:${derived.pinned.card}`, `--sf-border:${derived.pinned.border}`, `--sf-error:${derived.pinned.error}`, `color-scheme:${derived.scheme}`);
|
|
61
|
+
}
|
|
62
|
+
else if (derived.linkDark) {
|
|
63
|
+
dark.push(`--sf-link:${derived.linkDark}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (theme.font)
|
|
67
|
+
root.push(`--sf-font:${theme.font}`, `--sf-display-font:${theme.font}`);
|
|
68
|
+
if (root.length === 0)
|
|
69
|
+
return "";
|
|
70
|
+
const darkBlock = dark.length > 0 ? `@media(prefers-color-scheme:dark){:root{${dark.join(";")}}}` : "";
|
|
71
|
+
return `<style>:root{${root.join(";")}}${darkBlock}</style>`;
|
|
20
72
|
}
|
|
73
|
+
const runtimeSlot = (name, fallback) => `<!--sf-runtime:${name}:start-->${fallback}<!--sf-runtime:${name}:end-->`;
|
|
21
74
|
function renderFiles(files) {
|
|
22
75
|
if (files.length === 0)
|
|
23
76
|
return '<p class="sf-empty">Nothing here yet.</p>';
|
|
24
|
-
return `<ul class="sf-files">${files
|
|
25
|
-
.map((file) => `<li><a href="${escapePageHtml(file.href)}">${file.directory ?
|
|
77
|
+
return `<ul class="sf-files" role="list">${files
|
|
78
|
+
.map((file) => `<li><a href="${escapePageHtml(file.href)}"><span class="sf-file-name">${escapePageHtml(file.name)}${file.directory ? '<span aria-hidden="true">/</span>' : ""}</span></a>${file.size ? `<span class="sf-file-size">${escapePageHtml(file.size)}</span>` : ""}</li>`)
|
|
26
79
|
.join("")}</ul>`;
|
|
27
80
|
}
|
|
28
81
|
function renderFile(file) {
|
|
29
82
|
if (!file)
|
|
30
83
|
return '<p class="sf-empty">File unavailable.</p>';
|
|
84
|
+
// This page is itself the `?spacefast_view=1` representation, so the embed,
|
|
85
|
+
// the download and the raw link all point straight at the canonical path.
|
|
31
86
|
const href = escapePageHtml(file.href);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
87
|
+
const name = escapePageHtml(file.name);
|
|
88
|
+
const media = file.mime?.startsWith("image/")
|
|
89
|
+
? `<img class="sf-preview" src="${href}" alt="">`
|
|
90
|
+
: `<iframe class="sf-preview" src="${href}" title="Preview of ${name}" loading="lazy" sandbox></iframe>`;
|
|
91
|
+
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>`;
|
|
92
|
+
}
|
|
93
|
+
function renderSpacefastBranding(hidden) {
|
|
94
|
+
const wordmark = hidden
|
|
95
|
+
? ""
|
|
96
|
+
: `<span class="sf-powered" tabindex="0"><svg class="sf-wordmark" viewBox="0 0 326 62" role="img" aria-label="Spacefast"><rect class="sf-wordmark-shadow" x="14" y="12" width="306" height="48"></rect><rect class="sf-wordmark-plate" x="10" y="8" width="306" height="48"></rect><path class="sf-wordmark-paper" d="${SPACEFAST_WORDMARK_PATH}"></path></svg><span class="sf-powered-line"><a href="https://spacefast.com">Best way to share what your agent made</a></span></span>`;
|
|
97
|
+
return `<footer class="sf-colophon"><div class="sf-colophon-row">${wordmark}<a class="sf-help" href="https://spacefast.com/help">Need help?</a></div></footer>`;
|
|
36
98
|
}
|
|
37
99
|
function renderElement(element, context) {
|
|
38
100
|
switch (element) {
|
|
39
101
|
case "sf-content":
|
|
40
102
|
return context.content ?? "";
|
|
41
|
-
case "sf-
|
|
42
|
-
return
|
|
43
|
-
case "sf-
|
|
44
|
-
return
|
|
103
|
+
case "sf-denial-context":
|
|
104
|
+
return runtimeSlot("denial", '<p class="sf-copy">This page is private.</p>');
|
|
105
|
+
case "sf-page-path":
|
|
106
|
+
return context.displayPath
|
|
107
|
+
? escapePageHtml(context.displayPath)
|
|
108
|
+
: runtimeSlot("request-path", "this address");
|
|
109
|
+
case "sf-page-title":
|
|
110
|
+
return escapePageHtml(context.displayTitle ?? context.theme?.name ?? "This space");
|
|
111
|
+
case "sf-page-summary": {
|
|
112
|
+
const count = context.itemCount ?? context.files?.length ?? 0;
|
|
113
|
+
const visible = context.visibleItemCount ?? context.files?.length ?? count;
|
|
114
|
+
const label = `${count} ${count === 1 ? "item" : "items"}`;
|
|
115
|
+
return escapePageHtml(visible < count ? `${label} · showing first ${visible}` : label);
|
|
116
|
+
}
|
|
117
|
+
case "sf-spacefast-branding":
|
|
118
|
+
return renderSpacefastBranding(context.hideSpacefastBranding);
|
|
45
119
|
case "sf-files":
|
|
46
120
|
return renderFiles(context.files ?? []);
|
|
47
121
|
case "sf-file":
|
|
@@ -61,6 +135,7 @@ export function expandPageHtml(source, context) {
|
|
|
61
135
|
const name = (paired ?? unary)?.toLowerCase();
|
|
62
136
|
return name && knownElements.has(name) ? renderElement(name, context) : _match;
|
|
63
137
|
});
|
|
138
|
+
output = output.replaceAll("/*sf-default-fonts*/", context.embedPageFonts ? PAGE_EMBEDDED_FONT_FACES : PAGE_FONT_FACES);
|
|
64
139
|
const tokens = themeStyle(context.theme ?? {});
|
|
65
140
|
if (tokens) {
|
|
66
141
|
output = /<\s*\/\s*head\s*>/i.test(output)
|
|
@@ -115,9 +190,16 @@ export function validatePageSources(input) {
|
|
|
115
190
|
});
|
|
116
191
|
const allowed = element === "sf-logo" ||
|
|
117
192
|
element === "sf-name" ||
|
|
118
|
-
|
|
119
|
-
(id === "
|
|
120
|
-
(id === "
|
|
193
|
+
element === "sf-spacefast-branding" ||
|
|
194
|
+
(id === "404" && element === "sf-page-path") ||
|
|
195
|
+
(id === "denied" && element === "sf-denial-context") ||
|
|
196
|
+
(id === "index" &&
|
|
197
|
+
(element === "sf-files" ||
|
|
198
|
+
element === "sf-page-path" ||
|
|
199
|
+
element === "sf-page-title" ||
|
|
200
|
+
element === "sf-page-summary")) ||
|
|
201
|
+
(id === "preview" &&
|
|
202
|
+
(element === "sf-file" || element === "sf-page-path" || element === "sf-page-title"));
|
|
121
203
|
if (knownElements.has(element) && !allowed)
|
|
122
204
|
diagnostics.push({
|
|
123
205
|
severity: "error",
|
|
@@ -126,16 +208,6 @@ export function validatePageSources(input) {
|
|
|
126
208
|
path,
|
|
127
209
|
});
|
|
128
210
|
}
|
|
129
|
-
if (id === "password" &&
|
|
130
|
-
!/<\s*sf-password-form\b/i.test(source) &&
|
|
131
|
-
!/<\s*input\b[^>]*\bname\s*=\s*["']_pw["']/i.test(source)) {
|
|
132
|
-
diagnostics.push({
|
|
133
|
-
severity: "error",
|
|
134
|
-
code: "password_form_missing",
|
|
135
|
-
message: "The password page needs <sf-password-form> or a form input named _pw.",
|
|
136
|
-
path,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
211
|
if (id === "index" && !/<\s*sf-files\b/i.test(source))
|
|
140
212
|
diagnostics.push({
|
|
141
213
|
severity: "warning",
|
|
@@ -143,7 +215,7 @@ export function validatePageSources(input) {
|
|
|
143
215
|
message: "The index page does not render <sf-files>.",
|
|
144
216
|
path,
|
|
145
217
|
});
|
|
146
|
-
if (
|
|
218
|
+
if (id === "denied" &&
|
|
147
219
|
/<\s*meta\b[^>]*\bname\s*=\s*["']robots["'][^>]*\bcontent\s*=\s*["'][^"']*\bindex\b/i.test(source))
|
|
148
220
|
diagnostics.push({
|
|
149
221
|
severity: "warning",
|
|
@@ -169,7 +241,11 @@ export function validatePageSources(input) {
|
|
|
169
241
|
message: `Unknown Pages element <${element}>.`,
|
|
170
242
|
path,
|
|
171
243
|
});
|
|
172
|
-
else if (element !== "sf-content" &&
|
|
244
|
+
else if (element !== "sf-content" &&
|
|
245
|
+
element !== "sf-logo" &&
|
|
246
|
+
element !== "sf-name" &&
|
|
247
|
+
element !== "sf-page-title" &&
|
|
248
|
+
element !== "sf-spacefast-branding")
|
|
173
249
|
diagnostics.push({
|
|
174
250
|
severity: "error",
|
|
175
251
|
code: "page_element_not_allowed",
|
|
@@ -242,26 +318,23 @@ export function defaultPageBody(id) {
|
|
|
242
318
|
const source = DEFAULT_PAGE_TEMPLATES[id];
|
|
243
319
|
return source.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] ?? source;
|
|
244
320
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
321
|
+
// The built-in palette runs through the same readability engine as pushed
|
|
322
|
+
// themes, so buttons and links remain readable for every accepted accent.
|
|
323
|
+
const defaultPageColors = derivePageColors({});
|
|
324
|
+
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}}`;
|
|
325
|
+
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>';
|
|
326
|
+
const pageFooter = "<sf-spacefast-branding></sf-spacefast-branding>";
|
|
327
|
+
function document(id, title, body, layout) {
|
|
328
|
+
const noindex = PAGE_CATALOG[id].noindex ? '<meta name="robots" content="noindex">' : "";
|
|
329
|
+
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>${sharedCss}</style></head>\n<body><div class="sf-page sf-page-${layout}">${pageHeader}<main class="sf-main">${body}</main>${pageFooter}</div></body>\n</html>\n`;
|
|
248
330
|
}
|
|
249
|
-
const badge = '<p class="sf-badge">Hosted with <a href="https://spacefast.com">Spacefast</a></p>';
|
|
250
331
|
export const DEFAULT_PAGE_TEMPLATES = {
|
|
251
|
-
"404": document("Page not found", '<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
denied: document("Access denied", '<div class="sf-brand"><sf-logo></sf-logo><sf-name></sf-name></div><h1>Not for this account.</h1><p>You don’t have access to this page.</p>' +
|
|
256
|
-
badge, true),
|
|
257
|
-
login: document("Log in", '<div class="sf-brand"><sf-logo></sf-logo><sf-name></sf-name></div><h1>Log in to continue.</h1><p>This space knows its people.</p><sf-login></sf-login>' +
|
|
258
|
-
badge, true),
|
|
259
|
-
index: document("Files", '<div class="sf-brand"><sf-logo></sf-logo><sf-name></sf-name></div><h1>Files</h1><sf-files></sf-files>' +
|
|
260
|
-
badge),
|
|
261
|
-
preview: document("File preview", '<div class="sf-brand"><sf-logo></sf-logo><sf-name></sf-name></div><h1>Preview</h1><sf-file></sf-file>' +
|
|
262
|
-
badge),
|
|
332
|
+
"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"),
|
|
333
|
+
denied: document("denied", "Access denied", "<h1>Access denied</h1><sf-denial-context></sf-denial-context>", "center"),
|
|
334
|
+
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"),
|
|
335
|
+
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"),
|
|
263
336
|
};
|
|
264
|
-
export const DEFAULT_LAYOUT_TEMPLATE = `<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title><sf-
|
|
337
|
+
export const DEFAULT_LAYOUT_TEMPLATE = `<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title><sf-page-title></sf-page-title></title><style>${sharedCss}</style></head>\n<body><sf-content></sf-content></body>\n</html>\n`;
|
|
265
338
|
export function defaultPageTemplate(id) {
|
|
266
339
|
return DEFAULT_PAGE_TEMPLATES[id];
|
|
267
340
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type RuntimeEnvironment = Record<string, string | undefined>;
|
|
2
|
+
type RailwayRuntimeEvidence = {
|
|
3
|
+
kind: "railway";
|
|
4
|
+
runtimeEnvironment: string | undefined;
|
|
5
|
+
env: RuntimeEnvironment;
|
|
6
|
+
};
|
|
7
|
+
type BrowserRuntimeEvidence = {
|
|
8
|
+
kind: "browser";
|
|
9
|
+
currentUrl: string;
|
|
10
|
+
productionOrigin: string;
|
|
11
|
+
};
|
|
12
|
+
export type ProductionRuntimeEvidence = RailwayRuntimeEvidence | BrowserRuntimeEvidence;
|
|
13
|
+
/**
|
|
14
|
+
* Proves that telemetry is executing in a deployed production runtime.
|
|
15
|
+
* Environment labels alone are configuration, not runtime evidence.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isProductionRuntime(evidence: ProductionRuntimeEvidence): boolean;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const RAILWAY_RUNTIME_IDENTITY_KEYS = [
|
|
2
|
+
"RAILWAY_PROJECT_ID",
|
|
3
|
+
"RAILWAY_ENVIRONMENT_ID",
|
|
4
|
+
"RAILWAY_SERVICE_ID",
|
|
5
|
+
"RAILWAY_DEPLOYMENT_ID",
|
|
6
|
+
];
|
|
7
|
+
function present(value) {
|
|
8
|
+
return Boolean(value?.trim());
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Proves that telemetry is executing in a deployed production runtime.
|
|
12
|
+
* Environment labels alone are configuration, not runtime evidence.
|
|
13
|
+
*/
|
|
14
|
+
export function isProductionRuntime(evidence) {
|
|
15
|
+
if (evidence.kind === "railway") {
|
|
16
|
+
return (evidence.runtimeEnvironment?.trim().toLowerCase() === "production" &&
|
|
17
|
+
evidence.env.RAILWAY_ENVIRONMENT_NAME?.trim().toLowerCase() === "production" &&
|
|
18
|
+
RAILWAY_RUNTIME_IDENTITY_KEYS.every((key) => present(evidence.env[key])));
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const current = new URL(evidence.currentUrl);
|
|
22
|
+
const production = new URL(evidence.productionOrigin);
|
|
23
|
+
return production.protocol === "https:" && current.origin === production.origin;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -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
|
-
};
|
|
@@ -60,6 +60,10 @@ const STATIC_EXECUTION_CONTROL_FILES = new Set([".htaccess", ".user.ini"]);
|
|
|
60
60
|
// (`/__stattic_probe` today, more `/__stattic/*` routes planned) stays
|
|
61
61
|
// reserved the same way. Mid-segment occurrences are ordinary content.
|
|
62
62
|
const STATIC_RUNTIME_CONTROL_SEGMENT_PREFIXES = ["__spacefast", "__stattic"];
|
|
63
|
+
// Compact platform endpoints live below `/__sf`. Reserve the complete root
|
|
64
|
+
// segment prefix before the first endpoint ships so uploaded content can never
|
|
65
|
+
// shadow a later platform route. Nested `__sf*` directories remain content.
|
|
66
|
+
const STATIC_RUNTIME_ROOT_SEGMENT_PREFIXES = ["__sf"];
|
|
63
67
|
// Both the current and the legacy control-path spellings stay denied: spaces
|
|
64
68
|
// published before the `.stattic` → `.spacefast` rename still serve from the
|
|
65
69
|
// legacy on-disk layout, and user archives may carry either name.
|
|
@@ -76,6 +80,24 @@ const STATIC_RUNTIME_CONTROL_PREFIXES = [
|
|
|
76
80
|
export function normalizePublishPath(relativePath) {
|
|
77
81
|
return relativePath.replace(/\\/g, "/").replace(/^\/+/g, "");
|
|
78
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The one subtree inside the runtime control prefix that a publish may write.
|
|
85
|
+
*
|
|
86
|
+
* `sf publish` compiles a version's worker into `__spacefast/functions/bundles/`
|
|
87
|
+
* so the bundle travels through the ordinary manifest and upload path instead of
|
|
88
|
+
* needing a parallel one. Keeping it under the control prefix is what makes it
|
|
89
|
+
* unservable as static content by default — the runtime answers it only behind a
|
|
90
|
+
* signature.
|
|
91
|
+
*
|
|
92
|
+
* Scoped to `bundles/` rather than the whole `__spacefast/functions/` directory
|
|
93
|
+
* on purpose: the sibling `config.json` holds the key those signatures are
|
|
94
|
+
* minted with, and a publish that could overwrite it would be a publish that
|
|
95
|
+
* could mint its own tokens.
|
|
96
|
+
*/
|
|
97
|
+
const PUBLISHABLE_BUILD_ARTIFACT_PREFIX = "__spacefast/functions/bundles/";
|
|
98
|
+
function isPublishableBuildArtifactPath(lowerPath) {
|
|
99
|
+
return lowerPath.startsWith(PUBLISHABLE_BUILD_ARTIFACT_PREFIX);
|
|
100
|
+
}
|
|
79
101
|
export function publishPathIgnored(relativePath) {
|
|
80
102
|
const path = normalizePublishPath(relativePath);
|
|
81
103
|
// The agent state dirs hold one secret (state.json) plus committable tooling
|
|
@@ -101,27 +123,27 @@ export function publishPathIsConvention(relativePath) {
|
|
|
101
123
|
// Headers", migration diagnostics): Cloudflare Pages `_worker.js`/`_routes.json`
|
|
102
124
|
// and Pages/Netlify function directories publish as inert content with a
|
|
103
125
|
// warning so migrations are never silent.
|
|
126
|
+
function providerRuntimeFeatureForPath(path) {
|
|
127
|
+
if (path === "_worker.js") {
|
|
128
|
+
return "Cloudflare Pages _worker.js";
|
|
129
|
+
}
|
|
130
|
+
if (path === "_routes.json") {
|
|
131
|
+
return "Cloudflare Pages _routes.json";
|
|
132
|
+
}
|
|
133
|
+
if (path.startsWith("functions/")) {
|
|
134
|
+
return "Pages Functions (functions/)";
|
|
135
|
+
}
|
|
136
|
+
if (path.startsWith("netlify/functions/")) {
|
|
137
|
+
return "Netlify Functions (netlify/functions/)";
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
104
141
|
export function providerRuntimeFeatureWarnings(paths) {
|
|
105
142
|
const warnings = [];
|
|
106
143
|
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
144
|
for (const rawPath of paths) {
|
|
123
145
|
const path = normalizePublishPath(rawPath);
|
|
124
|
-
const feature =
|
|
146
|
+
const feature = providerRuntimeFeatureForPath(path);
|
|
125
147
|
if (!feature || seen.has(feature)) {
|
|
126
148
|
continue;
|
|
127
149
|
}
|
|
@@ -179,11 +201,15 @@ export function staticUploadPathViolation(path, options = {}) {
|
|
|
179
201
|
if (reservedRuntimeControlFiles?.has(lowerName) && lowerPath === lowerName) {
|
|
180
202
|
return pathViolation("static_runtime_control_path_not_supported", "Static deploys cannot upload runtime control paths.", normalizedPath);
|
|
181
203
|
}
|
|
204
|
+
const lowerRootSegment = segments[0]?.toLowerCase() ?? "";
|
|
205
|
+
if (STATIC_RUNTIME_ROOT_SEGMENT_PREFIXES.some((prefix) => lowerRootSegment.startsWith(prefix))) {
|
|
206
|
+
return pathViolation("static_runtime_control_path_not_supported", "Static deploys cannot upload runtime control paths.", normalizedPath);
|
|
207
|
+
}
|
|
182
208
|
const controlSegment = segments.find((segment) => {
|
|
183
209
|
const lowerSegment = segment.toLowerCase();
|
|
184
210
|
return STATIC_RUNTIME_CONTROL_SEGMENT_PREFIXES.some((prefix) => lowerSegment.startsWith(prefix));
|
|
185
211
|
});
|
|
186
|
-
if (controlSegment) {
|
|
212
|
+
if (controlSegment && !isPublishableBuildArtifactPath(lowerPath)) {
|
|
187
213
|
return pathViolation("static_runtime_control_path_not_supported", "Static deploys cannot upload runtime control paths.", normalizedPath);
|
|
188
214
|
}
|
|
189
215
|
if (STATIC_RUNTIME_CONTROL_PREFIXES.some((prefix) => lowerPath === prefix || lowerPath.startsWith(`${prefix}/`))) {
|
|
@@ -203,6 +229,95 @@ export function firstStaticUploadPathViolation(paths, options = {}) {
|
|
|
203
229
|
}
|
|
204
230
|
return null;
|
|
205
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Canonicalizes a route expanded from an already-canonical manifest path.
|
|
234
|
+
* Manifest intake has decoded percent escapes exactly once, so this must not
|
|
235
|
+
* decode again. It applies only the route-identity aliases shared with access.
|
|
236
|
+
*/
|
|
237
|
+
export function canonicalizePublishRequestedRoute(route) {
|
|
238
|
+
if (!route.startsWith("/")) {
|
|
239
|
+
throw new TypeError("publish requested routes must be absolute");
|
|
240
|
+
}
|
|
241
|
+
const segments = route
|
|
242
|
+
.split("/")
|
|
243
|
+
.filter((segment) => segment !== "" && segment !== ".")
|
|
244
|
+
.map((segment) => segment.normalize("NFC"));
|
|
245
|
+
if (segments.at(-1) === "index.html")
|
|
246
|
+
segments.pop();
|
|
247
|
+
return segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Expands artifact paths into the request routes they can serve. The runtime
|
|
251
|
+
* resolves literal files, directory indexes, and clean `.html` aliases; two
|
|
252
|
+
* different artifacts owning one request route is rejected at publish/finalize
|
|
253
|
+
* instead of depending on lookup order. `routeOwnership` exposes the same
|
|
254
|
+
* expansion in canonical access-route form so authorization and diagnostics
|
|
255
|
+
* never have to infer a route from an artifact path.
|
|
256
|
+
*/
|
|
257
|
+
export function analyzePublishRoutes(paths, options = {}) {
|
|
258
|
+
const index = options.index === undefined ? "index.html" : options.index;
|
|
259
|
+
const cleanUrls = options.cleanUrls ?? true;
|
|
260
|
+
const routes = new Map();
|
|
261
|
+
const add = (route, path) => {
|
|
262
|
+
const owners = routes.get(route) ?? new Set();
|
|
263
|
+
owners.add(path);
|
|
264
|
+
routes.set(route, owners);
|
|
265
|
+
};
|
|
266
|
+
for (const path of paths) {
|
|
267
|
+
add(`/${path}`, path);
|
|
268
|
+
if (cleanUrls && path.endsWith(".html")) {
|
|
269
|
+
add(`/${path.slice(0, -".html".length)}`, path);
|
|
270
|
+
}
|
|
271
|
+
if (index !== false) {
|
|
272
|
+
if (path === index) {
|
|
273
|
+
add("/", path);
|
|
274
|
+
}
|
|
275
|
+
else if (path.endsWith(`/${index}`)) {
|
|
276
|
+
const directory = path.slice(0, -(index.length + 1));
|
|
277
|
+
add(`/${directory}`, path);
|
|
278
|
+
add(`/${directory}/`, path);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const collisions = [...routes.entries()]
|
|
283
|
+
.filter(([, owners]) => owners.size > 1)
|
|
284
|
+
.map(([route, owners]) => ({ route, paths: [...owners].toSorted() }))
|
|
285
|
+
.toSorted((left, right) => left.route.localeCompare(right.route));
|
|
286
|
+
const firstByFoldedRoute = new Map();
|
|
287
|
+
const caseCollisions = [];
|
|
288
|
+
for (const [route, owners] of routes) {
|
|
289
|
+
const folded = route.toLowerCase();
|
|
290
|
+
const pathsForRoute = [...owners].toSorted();
|
|
291
|
+
const sibling = firstByFoldedRoute.get(folded);
|
|
292
|
+
if (sibling && sibling.route !== route) {
|
|
293
|
+
caseCollisions.push({
|
|
294
|
+
route,
|
|
295
|
+
siblingRoute: sibling.route,
|
|
296
|
+
paths: [...new Set([...sibling.paths, ...pathsForRoute])].toSorted(),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
else if (!sibling) {
|
|
300
|
+
firstByFoldedRoute.set(folded, { route, paths: pathsForRoute });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const canonicalOwnership = new Map();
|
|
304
|
+
for (const [route, owners] of routes) {
|
|
305
|
+
const canonicalRoute = canonicalizePublishRequestedRoute(route);
|
|
306
|
+
const canonicalOwners = canonicalOwnership.get(canonicalRoute) ?? new Set();
|
|
307
|
+
for (const owner of owners)
|
|
308
|
+
canonicalOwners.add(owner);
|
|
309
|
+
canonicalOwnership.set(canonicalRoute, canonicalOwners);
|
|
310
|
+
}
|
|
311
|
+
const routeOwnership = [...canonicalOwnership.entries()]
|
|
312
|
+
.flatMap(([requestedRoute, owners]) => [...owners].map((artifactPath) => ({ requestedRoute, artifactPath })))
|
|
313
|
+
.toSorted((left, right) => left.requestedRoute.localeCompare(right.requestedRoute) ||
|
|
314
|
+
left.artifactPath.localeCompare(right.artifactPath));
|
|
315
|
+
return {
|
|
316
|
+
collisions,
|
|
317
|
+
caseCollisions: caseCollisions.toSorted((left, right) => left.route.localeCompare(right.route)),
|
|
318
|
+
routeOwnership,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
206
321
|
export function resolveManifestFileCap(maxFiles) {
|
|
207
322
|
return Math.min(MANIFEST_MAX_FILES_CEILING, maxFiles && maxFiles > 0 ? maxFiles : MANIFEST_MAX_FILES_CEILING);
|
|
208
323
|
}
|
|
@@ -374,8 +489,6 @@ export const ANONYMOUS_SERVE_CONTENT_TYPES = [
|
|
|
374
489
|
"application/rss+xml",
|
|
375
490
|
"application/atom+xml",
|
|
376
491
|
];
|
|
377
|
-
const LINE_SPLIT_PATTERN = /\r?\n/;
|
|
378
|
-
const BASIC_AUTH_HEADER_PATTERN = /^basic-auth\s*:/i;
|
|
379
492
|
function extensionForPath(filePath) {
|
|
380
493
|
const normalized = normalizePublishPath(filePath).toLowerCase();
|
|
381
494
|
const fileName = normalized.split("/").at(-1) ?? normalized;
|
|
@@ -449,45 +562,3 @@ export function filterFreePlanManifest(files, options) {
|
|
|
449
562
|
}
|
|
450
563
|
return { allowed, ignored };
|
|
451
564
|
}
|
|
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
|
-
}
|