@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.
Files changed (160) hide show
  1. package/dist/agents/connect-targets.d.ts +4 -4
  2. package/dist/agents/connect-targets.js +3 -3
  3. package/dist/agents/private-key-oauth.d.ts +37 -0
  4. package/dist/agents/private-key-oauth.js +158 -0
  5. package/dist/config/domains.js +1 -1
  6. package/dist/config/index.d.ts +1 -0
  7. package/dist/config/index.js +1 -0
  8. package/dist/config/postgres-budget.d.ts +126 -0
  9. package/dist/config/postgres-budget.js +135 -0
  10. package/dist/config/postgres-budget.json +50 -0
  11. package/dist/contracts/access.d.ts +386 -1183
  12. package/dist/contracts/access.js +240 -1018
  13. package/dist/contracts/activity.d.ts +1 -1
  14. package/dist/contracts/activity.js +11 -19
  15. package/dist/contracts/api-keys.d.ts +69 -45
  16. package/dist/contracts/api-keys.js +3 -0
  17. package/dist/contracts/archives.d.ts +9 -10
  18. package/dist/contracts/archives.js +4 -6
  19. package/dist/contracts/builds.d.ts +8 -8
  20. package/dist/contracts/builds.js +28 -48
  21. package/dist/contracts/channels.js +1 -2
  22. package/dist/contracts/comments.d.ts +102 -93
  23. package/dist/contracts/comments.js +121 -63
  24. package/dist/contracts/common.d.ts +4 -3
  25. package/dist/contracts/common.js +2 -0
  26. package/dist/contracts/countries.js +2 -7
  27. package/dist/contracts/data-source-routes.d.ts +20 -0
  28. package/dist/contracts/data-source-routes.js +15 -0
  29. package/dist/contracts/deployments.d.ts +3 -3
  30. package/dist/contracts/device-auth.d.ts +21 -15
  31. package/dist/contracts/docs.d.ts +5 -5
  32. package/dist/contracts/domains.d.ts +261 -1330
  33. package/dist/contracts/domains.js +39 -301
  34. package/dist/contracts/email-preferences.d.ts +50 -0
  35. package/dist/contracts/email-preferences.js +44 -0
  36. package/dist/contracts/enums.d.ts +3 -5
  37. package/dist/contracts/enums.js +3 -8
  38. package/dist/contracts/error-code-meta.d.ts +99 -33
  39. package/dist/contracts/error-code-meta.js +36 -9
  40. package/dist/contracts/error-codes.d.ts +1 -1
  41. package/dist/contracts/error-codes.js +28 -11
  42. package/dist/contracts/execution.d.ts +212 -0
  43. package/dist/contracts/execution.js +288 -0
  44. package/dist/contracts/feature-lifecycle.d.ts +498 -2
  45. package/dist/contracts/feature-lifecycle.js +539 -2
  46. package/dist/contracts/features.d.ts +184 -74
  47. package/dist/contracts/features.js +4 -303
  48. package/dist/contracts/functions.d.ts +729 -0
  49. package/dist/contracts/functions.js +485 -0
  50. package/dist/contracts/git.d.ts +1 -1
  51. package/dist/contracts/grants.d.ts +1158 -0
  52. package/dist/contracts/grants.js +696 -0
  53. package/dist/contracts/ids.d.ts +6 -5
  54. package/dist/contracts/ids.js +6 -5
  55. package/dist/contracts/internal.d.ts +3 -3
  56. package/dist/contracts/mcp.d.ts +12 -12
  57. package/dist/contracts/me.js +1 -1
  58. package/dist/contracts/mounts.d.ts +142 -0
  59. package/dist/contracts/mounts.js +56 -0
  60. package/dist/contracts/oauth-resources.d.ts +5 -5
  61. package/dist/contracts/oauth-resources.js +2 -0
  62. package/dist/contracts/operations.d.ts +4 -4
  63. package/dist/contracts/pages.d.ts +6 -14
  64. package/dist/contracts/pages.js +11 -23
  65. package/dist/contracts/plan-policy.d.ts +2 -9
  66. package/dist/contracts/plan-policy.js +1 -4
  67. package/dist/contracts/platform.d.ts +80 -32
  68. package/dist/contracts/platform.js +59 -12
  69. package/dist/contracts/push-new.d.ts +26 -2
  70. package/dist/contracts/push-new.js +8 -0
  71. package/dist/contracts/quotas.d.ts +0 -1
  72. package/dist/contracts/quotas.js +0 -5
  73. package/dist/contracts/repository-connections.d.ts +1 -1
  74. package/dist/contracts/resources.d.ts +21 -85
  75. package/dist/contracts/resources.js +17 -47
  76. package/dist/contracts/runtime-api.d.ts +799 -192
  77. package/dist/contracts/runtime-api.js +231 -100
  78. package/dist/contracts/runtime-app.d.ts +138 -0
  79. package/dist/contracts/runtime-app.js +21 -0
  80. package/dist/contracts/sf-config-v1.d.ts +5 -0
  81. package/dist/contracts/sf-config-v1.js +1 -1
  82. package/dist/contracts/sites.d.ts +17 -12
  83. package/dist/contracts/sites.js +30 -12
  84. package/dist/contracts/space-config.d.ts +46 -117
  85. package/dist/contracts/space-config.js +323 -25
  86. package/dist/contracts/spaces.d.ts +392 -256
  87. package/dist/contracts/spaces.js +163 -83
  88. package/dist/contracts/superadmin-emails.d.ts +27 -3
  89. package/dist/contracts/superadmin-emails.js +10 -15
  90. package/dist/contracts/superadmin-runtime.d.ts +2 -2
  91. package/dist/contracts/superadmin-spaces.d.ts +19 -142
  92. package/dist/contracts/superadmin-spaces.js +7 -0
  93. package/dist/contracts/tags.d.ts +66 -36
  94. package/dist/contracts/tags.js +195 -11
  95. package/dist/contracts/teams.d.ts +9 -9
  96. package/dist/contracts/transfers.d.ts +1 -2
  97. package/dist/contracts/variables.d.ts +4 -4
  98. package/dist/contracts/variables.js +4 -3
  99. package/dist/contracts/webhooks.d.ts +2 -2
  100. package/dist/contracts/zero.d.ts +437 -193
  101. package/dist/contracts/zero.js +149 -158
  102. package/dist/dashboard-paths/index.d.ts +3 -3
  103. package/dist/dashboard-paths/index.js +1 -0
  104. package/dist/docs/agent-prose.js +10 -8
  105. package/dist/docs/agent-setup.d.ts +6 -4
  106. package/dist/docs/agent-setup.js +26 -16
  107. package/dist/docs/agent-solutions.js +1 -1
  108. package/dist/docs/catalog.d.ts +35 -37
  109. package/dist/docs/catalog.js +36 -38
  110. package/dist/docs/error-docs.js +108 -44
  111. package/dist/utils/asset-fingerprint.d.ts +11 -0
  112. package/dist/utils/asset-fingerprint.js +46 -0
  113. package/dist/utils/auth-redirect.d.ts +1 -3
  114. package/dist/utils/auth-redirect.js +1 -21
  115. package/dist/utils/build-settings.d.ts +2 -0
  116. package/dist/utils/build-settings.js +61 -9
  117. package/dist/utils/credential-policy.d.ts +1 -1
  118. package/dist/utils/credential-policy.js +4 -0
  119. package/dist/utils/dns-instructions.d.ts +29 -0
  120. package/dist/utils/dns-instructions.js +78 -0
  121. package/dist/utils/domain-diagnostics.d.ts +29 -0
  122. package/dist/utils/domain-diagnostics.js +63 -0
  123. package/dist/utils/egress-policy.fixtures.json +51 -0
  124. package/dist/utils/grants.d.ts +53 -0
  125. package/dist/utils/grants.js +160 -0
  126. package/dist/utils/id-hints.d.ts +1 -0
  127. package/dist/utils/id-hints.js +55 -13
  128. package/dist/utils/local-space-state.js +1 -0
  129. package/dist/utils/page-colors.d.ts +50 -0
  130. package/dist/utils/page-colors.js +167 -0
  131. package/dist/utils/page-fonts.d.ts +2 -0
  132. package/dist/utils/page-fonts.js +4 -0
  133. package/dist/utils/pages.d.ts +19 -6
  134. package/dist/utils/pages.js +122 -49
  135. package/dist/utils/production-runtime.d.ts +18 -0
  136. package/dist/utils/production-runtime.js +28 -0
  137. package/dist/utils/publish-policy.d.ts +34 -4
  138. package/dist/utils/publish-policy.js +132 -61
  139. package/dist/utils/query-keys.d.ts +7 -13
  140. package/dist/utils/query-keys.js +12 -18
  141. package/dist/utils/source-archive-policy.d.ts +2 -0
  142. package/dist/utils/source-archive-policy.js +2 -0
  143. package/dist/utils/space-config.d.ts +5 -1
  144. package/dist/utils/space-config.js +35 -0
  145. package/dist/utils/static-runtime-policy.d.ts +5 -0
  146. package/dist/utils/static-runtime-policy.fixtures.json +57 -0
  147. package/dist/utils/static-runtime-policy.js +54 -10
  148. package/dist/utils/upload-session.d.ts +3 -0
  149. package/dist/utils/upload-session.js +31 -0
  150. package/package.json +6 -4
  151. package/dist/contracts/annotations.d.ts +0 -327
  152. package/dist/contracts/annotations.js +0 -235
  153. package/dist/contracts/generated-feature-launch-entries.d.ts +0 -2
  154. package/dist/contracts/generated-feature-launch-entries.js +0 -518
  155. package/dist/contracts/publishes.d.ts +0 -135
  156. package/dist/contracts/publishes.js +0 -162
  157. package/dist/utils/access-match.d.ts +0 -39
  158. package/dist/utils/access-match.js +0 -296
  159. package/dist/utils/sf-config-v1.d.ts +0 -2
  160. package/dist/utils/sf-config-v1.js +0 -542
@@ -1,7 +1,39 @@
1
- import { CUSTOM_PAGE_IDS, PAGE_ELEMENTS, PAGE_LAYOUT_FILE, PAGE_SOURCE_MAX_BYTES, SITE_PAGE_IDS, } from "../contracts/pages.js";
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("&", "&amp;")
@@ -10,38 +42,80 @@ export function escapePageHtml(value) {
10
42
  .replaceAll('"', "&quot;")
11
43
  .replaceAll("'", "&#039;");
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 declarations = [
15
- theme.accent ? `--sf-accent:${theme.accent}` : "",
16
- theme.background ? `--sf-bg:${theme.background}` : "",
17
- theme.font ? `--sf-font:${theme.font}` : "",
18
- ].filter(Boolean);
19
- return declarations.length > 0 ? `<style>:root{${declarations.join(";")}}</style>` : "";
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 ? "↳ " : ""}${escapePageHtml(file.name)}</a>${file.size ? `<span>${escapePageHtml(file.size)}</span>` : ""}</li>`)
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 image = file.mime?.startsWith("image/")
33
- ? `<img class="sf-preview" src="${href}" alt="${escapePageHtml(file.name)}">`
34
- : `<iframe class="sf-preview" src="${href}" title="${escapePageHtml(file.name)}"></iframe>`;
35
- return `${image}<dl class="sf-file-meta"><div><dt>Name</dt><dd>${escapePageHtml(file.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><a class="sf-button" href="${escapePageHtml(file.downloadHref ?? file.href)}" download>Download</a>`;
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-password-form":
42
- return `${context.passwordError ? `<p class="sf-error" role="alert">${escapePageHtml(context.passwordError)}</p>` : ""}<form class="sf-form" method="post" action="${escapePageHtml(context.passwordAction ?? "")}"><label>Password<input name="_pw" type="password" required autocomplete="current-password"></label><button type="submit">Continue</button></form>`;
43
- case "sf-login":
44
- return `<a class="sf-button" href="${escapePageHtml(context.loginHref ?? "/__spacefast/access/login")}">Log in</a>`;
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
- (id === "password" && element === "sf-password-form") ||
119
- (id === "index" && element === "sf-files") ||
120
- (id === "preview" && element === "sf-file");
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 ((id === "password" || id === "denied") &&
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" && element !== "sf-logo" && element !== "sf-name")
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
- const sharedCss = `:root{color-scheme:light dark;--sf-accent:#ff603d;--sf-bg:#faf9f7;--sf-fg:#1c1b1a;--sf-muted:#6b6862;--sf-card:#fff;--sf-border:#e5e2dc;--sf-font:Inter,ui-sans-serif,system-ui,sans-serif}*{box-sizing:border-box}body{margin:0;background:var(--sf-bg);color:var(--sf-fg);font:16px/1.55 var(--sf-font);min-height:100vh;display:grid;place-items:center;padding:24px}.sf-shell{width:min(100%,640px)}.sf-card{background:var(--sf-card);border:1px solid var(--sf-border);border-radius:18px;padding:clamp(24px,5vw,48px);box-shadow:0 18px 55px #1c1b1a12}h1{font-size:clamp(2rem,7vw,3.5rem);line-height:1;letter-spacing:-.05em;margin:16px 0}p{color:var(--sf-muted)}a{color:var(--sf-accent)}.sf-brand{display:flex;align-items:center;gap:10px;font-weight:650}.sf-logo{width:28px;height:28px;object-fit:contain}.sf-badge{margin-top:28px;font-size:12px;color:var(--sf-muted)}.sf-badge a{text-decoration:none}.sf-form label{display:grid;gap:8px;font-weight:600}.sf-form input{width:100%;margin-top:6px;padding:12px 14px;border:1px solid var(--sf-border);border-radius:10px;background:transparent;color:inherit;font:inherit}.sf-form button,.sf-button{display:inline-block;border:0;border-radius:10px;background:var(--sf-accent);color:#fff;padding:12px 16px;font:600 14px/1 var(--sf-font);text-decoration:none;margin-top:14px;cursor:pointer}.sf-error{color:#b42318}.sf-files{list-style:none;padding:0;margin:24px 0}.sf-files li{display:flex;border-top:1px solid var(--sf-border)}.sf-files a{padding:12px 0;flex:1;text-decoration:none}.sf-files span{padding:12px 0;color:var(--sf-muted);font-size:13px}.sf-preview{width:100%;min-height:320px;border:1px solid var(--sf-border);border-radius:10px;object-fit:contain;background:#fff}.sf-file-meta div{display:flex;justify-content:space-between;border-bottom:1px solid var(--sf-border);padding:8px 0}.sf-file-meta dt{color:var(--sf-muted)}.sf-file-meta dd{margin:0}@media(prefers-color-scheme:dark){:root{--sf-bg:#16151a;--sf-fg:#e8e6e3;--sf-muted:#a8a49c;--sf-card:#1e1d23;--sf-border:#343239}.sf-card{box-shadow:0 18px 55px #0005}}`;
246
- function document(title, body, noindex = false) {
247
- return `<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${noindex ? '<meta name="robots" content="noindex">' : ""}<title>${title}</title><style>${sharedCss}</style></head>\n<body><main class="sf-shell"><section class="sf-card">${body}</section></main></body>\n</html>\n`;
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", '<div class="sf-brand"><sf-logo></sf-logo><sf-name></sf-name></div><h1>Lost in space.</h1><p>This page doesnt exist. Check the address or head back home.</p><a class="sf-button" href="/">Go home</a>' +
252
- badge),
253
- password: document("Password required", '<div class="sf-brand"><sf-logo></sf-logo><sf-name></sf-name></div><h1>This one’s private.</h1><p>Enter the password to keep going.</p><sf-password-form></sf-password-form>' +
254
- badge, true),
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">Theres 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-name></sf-name></title></head>\n<body>\n <header><a href="/"><sf-logo></sf-logo><sf-name></sf-name></a></header>\n <main><sf-content></sf-content></main>\n</body>\n</html>\n`;
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 = featureForPath(path);
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
- }