@spacefast/common 0.0.24 → 0.0.26

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 (145) hide show
  1. package/brand-assets/LICENSE-Haskoy.txt +93 -0
  2. package/brand-assets/haskoy-latin-variable.woff2 +0 -0
  3. package/brand-assets/spacefast-favicon.svg +4 -4
  4. package/brand-assets/spacefast-sf-full-bleed.svg +3 -3
  5. package/brand-assets/spacefast-wordmark.svg +2 -4
  6. package/dist/agents/connect-targets.js +1 -1
  7. package/dist/agents/private-key-oauth.d.ts +0 -1
  8. package/dist/agents/private-key-oauth.js +46 -63
  9. package/dist/brand-assets-build.js +4 -6
  10. package/dist/brand-assets.d.ts +5 -15
  11. package/dist/brand-assets.js +15 -14
  12. package/dist/config/domains.d.ts +47 -7
  13. package/dist/config/domains.js +65 -10
  14. package/dist/contracts/access.d.ts +3 -3
  15. package/dist/contracts/activity.d.ts +1 -1
  16. package/dist/contracts/activity.js +5 -3
  17. package/dist/contracts/api-keys.js +2 -2
  18. package/dist/contracts/billing.d.ts +2 -1
  19. package/dist/contracts/billing.js +30 -0
  20. package/dist/contracts/builds.d.ts +34 -22
  21. package/dist/contracts/builds.js +18 -6
  22. package/dist/contracts/channels.js +1 -1
  23. package/dist/contracts/comments.d.ts +1 -1
  24. package/dist/contracts/common.d.ts +6 -6
  25. package/dist/contracts/common.js +5 -5
  26. package/dist/contracts/crons.d.ts +35 -0
  27. package/dist/contracts/crons.js +42 -0
  28. package/dist/contracts/device-auth.js +1 -1
  29. package/dist/contracts/docs.d.ts +5 -5
  30. package/dist/contracts/docs.js +2 -2
  31. package/dist/contracts/domains.d.ts +74 -63
  32. package/dist/contracts/domains.js +29 -2
  33. package/dist/contracts/enums.d.ts +3 -2
  34. package/dist/contracts/enums.js +10 -10
  35. package/dist/contracts/error-code-meta.d.ts +14 -0
  36. package/dist/contracts/error-code-meta.js +17 -0
  37. package/dist/contracts/error-codes.d.ts +1 -1
  38. package/dist/contracts/error-codes.js +4 -1
  39. package/dist/contracts/execution.d.ts +15 -12
  40. package/dist/contracts/execution.js +15 -12
  41. package/dist/contracts/feature-lifecycle.d.ts +4 -14
  42. package/dist/contracts/feature-lifecycle.js +0 -11
  43. package/dist/contracts/features.d.ts +33 -47
  44. package/dist/contracts/features.js +27 -20
  45. package/dist/contracts/functions.d.ts +169 -2
  46. package/dist/contracts/functions.js +160 -4
  47. package/dist/contracts/git.d.ts +27 -33
  48. package/dist/contracts/git.js +10 -11
  49. package/dist/contracts/grants.d.ts +2 -2
  50. package/dist/contracts/ids.js +2 -2
  51. package/dist/contracts/internal.d.ts +5 -16
  52. package/dist/contracts/internal.js +2 -12
  53. package/dist/contracts/mcp.d.ts +11 -0
  54. package/dist/contracts/mcp.js +5 -0
  55. package/dist/contracts/me.js +1 -1
  56. package/dist/contracts/operations.d.ts +4 -4
  57. package/dist/contracts/operations.js +2 -2
  58. package/dist/contracts/pages.d.ts +6 -6
  59. package/dist/contracts/plan-policy.d.ts +1 -1
  60. package/dist/contracts/plan-policy.js +1 -1
  61. package/dist/contracts/platform.d.ts +4 -4
  62. package/dist/contracts/publish-archive.d.ts +170 -160
  63. package/dist/contracts/publish-archive.js +8 -1
  64. package/dist/contracts/push-new.d.ts +4 -4
  65. package/dist/contracts/quotas.js +2 -2
  66. package/dist/contracts/repository-connections.d.ts +43 -30
  67. package/dist/contracts/repository-connections.js +11 -3
  68. package/dist/contracts/resources.d.ts +8 -8
  69. package/dist/contracts/resources.js +6 -6
  70. package/dist/contracts/runtime-api.d.ts +103 -56
  71. package/dist/contracts/runtime-api.js +67 -26
  72. package/dist/contracts/runtime-app.d.ts +2 -2
  73. package/dist/contracts/runtime-db.d.ts +6 -6
  74. package/dist/contracts/runtime-db.js +2 -2
  75. package/dist/contracts/runtime-purge.d.ts +8 -0
  76. package/dist/contracts/runtime-purge.js +7 -0
  77. package/dist/contracts/runtime-storage.d.ts +3 -2
  78. package/dist/contracts/runtime-storage.js +2 -1
  79. package/dist/contracts/sf-config-v1.d.ts +48 -10
  80. package/dist/contracts/space-config.d.ts +60 -3
  81. package/dist/contracts/space-config.js +213 -18
  82. package/dist/contracts/spaces.d.ts +55 -74
  83. package/dist/contracts/spaces.js +11 -14
  84. package/dist/contracts/superadmin-spaces.d.ts +9 -9
  85. package/dist/contracts/superadmin-teams.d.ts +2 -2
  86. package/dist/contracts/superadmin-tenants.d.ts +0 -6
  87. package/dist/contracts/superadmin-tenants.js +1 -7
  88. package/dist/contracts/superadmin.d.ts +135 -22
  89. package/dist/contracts/superadmin.js +43 -12
  90. package/dist/contracts/tags.d.ts +21 -21
  91. package/dist/contracts/tags.js +3 -3
  92. package/dist/contracts/teams.d.ts +14 -17
  93. package/dist/contracts/teams.js +6 -6
  94. package/dist/contracts/transfers.js +2 -2
  95. package/dist/contracts/variables.d.ts +4 -4
  96. package/dist/contracts/variables.js +3 -3
  97. package/dist/contracts/webhooks.d.ts +4 -4
  98. package/dist/contracts/zero.d.ts +15 -17
  99. package/dist/contracts/zero.js +12 -12
  100. package/dist/docs/agent-prose.d.ts +1 -1
  101. package/dist/docs/agent-prose.js +11 -14
  102. package/dist/docs/agent-setup.js +5 -2
  103. package/dist/docs/catalog.d.ts +7 -7
  104. package/dist/docs/catalog.js +7 -7
  105. package/dist/docs/error-docs.js +145 -133
  106. package/dist/docs/skills.d.ts +12 -12
  107. package/dist/docs/skills.js +4 -4
  108. package/dist/test-helpers/fetch-stub.js +1 -0
  109. package/dist/utils/build-settings.d.ts +5 -1
  110. package/dist/utils/build-settings.js +82 -10
  111. package/dist/utils/canonical-json.js +1 -0
  112. package/dist/utils/concurrency.js +1 -0
  113. package/dist/utils/credential-policy.js +4 -3
  114. package/dist/utils/dns-instructions.js +9 -9
  115. package/dist/utils/error-code.js +1 -0
  116. package/dist/utils/grant-decision.fixtures.json +2 -2
  117. package/dist/utils/grants.js +3 -1
  118. package/dist/utils/local-space-state.d.ts +8 -11
  119. package/dist/utils/local-space-state.js +13 -24
  120. package/dist/utils/one-shot-replay.d.ts +4 -1
  121. package/dist/utils/page-colors.d.ts +14 -12
  122. package/dist/utils/page-colors.js +15 -13
  123. package/dist/utils/page-fonts.d.ts +1 -1
  124. package/dist/utils/page-fonts.js +11 -18
  125. package/dist/utils/pages.d.ts +20 -1
  126. package/dist/utils/pages.js +22 -9
  127. package/dist/utils/publish-policy.d.ts +7 -0
  128. package/dist/utils/publish-policy.fixtures.json +11 -5
  129. package/dist/utils/publish-policy.js +9 -1
  130. package/dist/utils/query-keys.d.ts +4 -4
  131. package/dist/utils/query-keys.js +2 -2
  132. package/dist/utils/runtime-upload.d.ts +5 -0
  133. package/dist/utils/runtime-upload.js +7 -0
  134. package/dist/utils/secure-local-file.js +5 -16
  135. package/dist/utils/space-config.d.ts +1 -1
  136. package/dist/utils/space-config.js +3 -0
  137. package/dist/utils/static-runtime-policy.d.ts +9 -3
  138. package/dist/utils/static-runtime-policy.fixtures.json +38 -41
  139. package/dist/utils/static-runtime-policy.js +22 -56
  140. package/dist/utils/upload-session.d.ts +4 -1
  141. package/dist/utils/upload-session.js +33 -5
  142. package/dist/utils/version-path.js +1 -1
  143. package/dist/vocabulary.d.ts +8 -8
  144. package/dist/vocabulary.js +31 -20
  145. package/package.json +2 -17
@@ -1,17 +1,19 @@
1
1
  import type { SpacePageTheme } from "../contracts/space-config.js";
2
- /** Default page palette (mirrored into the shared template CSS). */
2
+ /** Default page palette (mirrored into the shared template CSS) — the
3
+ * printed-matter scheme: warm canvas, ink type, marker accent (always a
4
+ * ground with ink on top; the link derivation handles its text-grade). */
3
5
  export declare const PAGE_COLOR_DEFAULTS: {
4
- readonly accent: "#ff4217";
5
- readonly bg: "#f9f8f4";
6
- readonly fg: "#1a1913";
7
- readonly muted: "#6f6b62";
8
- readonly card: "#f9f8f4";
9
- readonly border: "#ddd9cf";
10
- readonly darkBg: "#141313";
11
- readonly darkFg: "#e9e7e2";
12
- readonly darkMuted: "#8f8b84";
13
- readonly darkCard: "#141313";
14
- readonly darkBorder: "#2b2a27";
6
+ readonly accent: "#d8f24b";
7
+ readonly bg: "#f7f5ef";
8
+ readonly fg: "#0c0c0c";
9
+ readonly muted: "#6e6a5e";
10
+ readonly card: "#f7f5ef";
11
+ readonly border: "#e5e1d4";
12
+ readonly darkBg: "#0c0c0c";
13
+ readonly darkFg: "#f0eee6";
14
+ readonly darkMuted: "#a6a193";
15
+ readonly darkCard: "#0c0c0c";
16
+ readonly darkBorder: "#2b2823";
15
17
  };
16
18
  export type PageSchemeColors = {
17
19
  bg: string;
@@ -18,19 +18,21 @@
18
18
  // Everything is pure and deterministic: publish finalize, dashboard previews,
19
19
  // and tests all call the same functions and get the same pixels.
20
20
  import { formatHex, oklch, parse, wcagContrast } from "culori";
21
- /** Default page palette (mirrored into the shared template CSS). */
21
+ /** Default page palette (mirrored into the shared template CSS) — the
22
+ * printed-matter scheme: warm canvas, ink type, marker accent (always a
23
+ * ground with ink on top; the link derivation handles its text-grade). */
22
24
  export const PAGE_COLOR_DEFAULTS = {
23
- accent: "#ff4217",
24
- bg: "#f9f8f4",
25
- fg: "#1a1913",
26
- muted: "#6f6b62",
27
- card: "#f9f8f4",
28
- border: "#ddd9cf",
29
- darkBg: "#141313",
30
- darkFg: "#e9e7e2",
31
- darkMuted: "#8f8b84",
32
- darkCard: "#141313",
33
- darkBorder: "#2b2a27",
25
+ accent: "#d8f24b",
26
+ bg: "#f7f5ef",
27
+ fg: "#0c0c0c",
28
+ muted: "#6e6a5e",
29
+ card: "#f7f5ef",
30
+ border: "#e5e1d4",
31
+ darkBg: "#0c0c0c",
32
+ darkFg: "#f0eee6",
33
+ darkMuted: "#a6a193",
34
+ darkCard: "#0c0c0c",
35
+ darkBorder: "#2b2823",
34
36
  };
35
37
  /** WCAG AA for body-size text. */
36
38
  const TEXT_CONTRAST = 4.5;
@@ -139,7 +141,7 @@ function schemeFromBackground(background) {
139
141
  /** The one entry point: raw theme in, everything the templates paint with out. */
140
142
  export function derivePageColors(theme) {
141
143
  const accentInput = theme.accent?.trim();
142
- const accent = safeParse(accentInput) ? accentInput : PAGE_COLOR_DEFAULTS.accent;
144
+ const accent = accentInput && safeParse(accentInput) ? accentInput : PAGE_COLOR_DEFAULTS.accent;
143
145
  const onAccent = onAccentColor(accent);
144
146
  const background = theme.background?.trim();
145
147
  if (background && safeParse(background)) {
@@ -1,2 +1,2 @@
1
1
  export declare const PAGE_FONT_FACES: string;
2
- export declare const PAGE_FONT_PRELOAD_HTML: string;
2
+ export declare const PAGE_FONT_PRELOAD_HTML = "<link rel=\"preload\" href=\"https://wordpress.com/i/fonts/recoleta/400.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>";
@@ -1,22 +1,15 @@
1
- // The platform page fonts, loaded from shared origins so every space hostname
2
- // reuses one warm browser-cache entry instead of a cold per-space fetch:
3
- // Recoleta (commercial, never committed to this repo) from wordpress.com's
4
- // font CDN, Haskoy from spacefast.com (apps/www/public/assets/fonts, served
5
- // immutable with CORS see wwwHeaders in apps/www/src/lib/routing.mjs).
1
+ // The platform page fonts. Recoleta is the only downloaded face (DESIGN.md
2
+ // §4) body and mono are system stacks, so a published page pays for exactly
3
+ // one typeface. It is commercial, never committed to this repo, and loads from
4
+ // wordpress.com's font CDN so every space hostname reuses one warm
5
+ // browser-cache entry instead of a cold per-space fetch.
6
6
  // `font-display:optional` renders the fallback and never swaps mid-view when a
7
7
  // font arrives late — cached loads (everything after the first) get the brand
8
- // fonts with no flicker either way.
8
+ // font with no flicker either way.
9
9
  const RECOLETA_BASE = "https://wordpress.com/i/fonts/recoleta/";
10
- const HASKOY_URL = "https://spacefast.com/assets/fonts/haskoy-latin-variable.woff2";
11
10
  const RECOLETA_WEIGHTS = [300, 400, 500, 600, 700];
12
- const faces = [
13
- ...RECOLETA_WEIGHTS.map((weight) => `@font-face{font-family:"Recoleta";src:url("${RECOLETA_BASE}${weight}.woff2") format("woff2");font-style:normal;font-weight:${weight};font-display:optional}`),
14
- `@font-face{font-family:"Haskoy";src:url("${HASKOY_URL}") format("woff2");font-style:normal;font-weight:100 900;font-display:optional}`,
15
- ];
16
- export const PAGE_FONT_FACES = faces.join("");
17
- // Only the faces the default pages actually render: Recoleta 400 for headings,
18
- // Haskoy for everything else. Fonts are a cross-origin fetch; `crossorigin` is
19
- // required — font requests are CORS mode, and a mismatched preload is discarded.
20
- export const PAGE_FONT_PRELOAD_HTML = [`${RECOLETA_BASE}400.woff2`, HASKOY_URL]
21
- .map((url) => `<link rel="preload" href="${url}" as="font" type="font/woff2" crossorigin>`)
22
- .join("");
11
+ export const PAGE_FONT_FACES = RECOLETA_WEIGHTS.map((weight) => `@font-face{font-family:"Recoleta";src:url("${RECOLETA_BASE}${weight}.woff2") format("woff2");font-style:normal;font-weight:${weight};font-display:optional}`).join("");
12
+ // Only the face the default pages actually download: Recoleta 400 for headings.
13
+ // Fonts are a cross-origin fetch; `crossorigin` is required — font requests are
14
+ // CORS mode, and a mismatched preload is discarded.
15
+ export const PAGE_FONT_PRELOAD_HTML = `<link rel="preload" href="${RECOLETA_BASE}400.woff2" as="font" type="font/woff2" crossorigin>`;
@@ -1,4 +1,5 @@
1
1
  import { type CustomPageId, type SitePageId } from "../contracts/pages.js";
2
+ import { type DerivedPageColors } from "./page-colors.js";
2
3
  export { PAGE_PREVIEW_QUERY, previewPageHref } from "./page-preview.js";
3
4
  export type PageTheme = {
4
5
  accent?: string | undefined;
@@ -40,6 +41,18 @@ export type PageDiagnostic = {
40
41
  };
41
42
  export declare function isPagePublicPath(path: string): boolean;
42
43
  export declare function escapePageHtml(value: string): string;
44
+ /**
45
+ * The custom properties a theme paints with. Pushed colors are intent; the
46
+ * readability engine (page-colors.ts) derives what the templates actually use.
47
+ * A custom background pins a complete scheme for both OS modes — emitting
48
+ * `--sf-bg` alone used to leave dark-mode visitors with the dark foreground on
49
+ * the user's light background.
50
+ *
51
+ * Exported because the dashboard's live preview paints the same tokens from an
52
+ * unsaved draft: it passes the colors it has already derived, and gets exactly
53
+ * what the published page would carry.
54
+ */
55
+ export declare function pageThemeCss(theme: PageTheme, derived?: DerivedPageColors): string;
43
56
  export declare function expandPageHtml(source: string, context: PageExpansionContext): string;
44
57
  export declare function validatePageSources(input: {
45
58
  pages: Readonly<Record<string, string>>;
@@ -49,7 +62,13 @@ export declare function isCompleteHtmlDocument(source: string): boolean;
49
62
  export declare function applyPageLayout(layout: string | null, content: string, context: Omit<PageExpansionContext, "page" | "content">): string;
50
63
  export declare function inferPageThemeFromThemeJson(raw: string): Partial<PageTheme>;
51
64
  export declare function defaultPageBody(id: SitePageId): string;
52
- export declare const DEFAULT_PAGE_TEMPLATES: Readonly<Record<SitePageId, string>>;
65
+ export declare const DEFAULT_PAGE_TEMPLATES: {
66
+ "404": string;
67
+ denied: string;
68
+ access: string;
69
+ index: string;
70
+ preview: string;
71
+ };
53
72
  export declare const DEFAULT_LAYOUT_TEMPLATE: string;
54
73
  export declare function defaultPageTemplate(id: SitePageId): string;
55
74
  export declare function pageIdFromFilename(filename: string): CustomPageId | null;
@@ -33,15 +33,21 @@ export function escapePageHtml(value) {
33
33
  .replaceAll('"', "&quot;")
34
34
  .replaceAll("'", "&#039;");
35
35
  }
36
- // Pushed colors are intent; the readability engine (page-colors.ts) derives
37
- // what the templates actually paint with. A custom background pins a complete
38
- // scheme for both OS modes emitting `--sf-bg` alone used to leave dark-mode
39
- // visitors with the dark foreground on the user's light background.
40
- function themeStyle(theme) {
36
+ /**
37
+ * The custom properties a theme paints with. Pushed colors are intent; the
38
+ * readability engine (page-colors.ts) derives what the templates actually use.
39
+ * A custom background pins a complete scheme for both OS modes — emitting
40
+ * `--sf-bg` alone used to leave dark-mode visitors with the dark foreground on
41
+ * the user's light background.
42
+ *
43
+ * Exported because the dashboard's live preview paints the same tokens from an
44
+ * unsaved draft: it passes the colors it has already derived, and gets exactly
45
+ * what the published page would carry.
46
+ */
47
+ export function pageThemeCss(theme, derived = derivePageColors(theme)) {
41
48
  const root = [];
42
49
  const dark = [];
43
50
  if (theme.accent || theme.background) {
44
- const derived = derivePageColors(theme);
45
51
  // `derived.accent` is the parse-validated input (or the default), never
46
52
  // an arbitrary string straight from the theme.
47
53
  if (theme.accent)
@@ -59,7 +65,11 @@ function themeStyle(theme) {
59
65
  if (root.length === 0)
60
66
  return "";
61
67
  const darkBlock = dark.length > 0 ? `@media(prefers-color-scheme:dark){:root{${dark.join(";")}}}` : "";
62
- return `<style>:root{${root.join(";")}}${darkBlock}</style>`;
68
+ return `:root{${root.join(";")}}${darkBlock}`;
69
+ }
70
+ function themeStyle(theme) {
71
+ const css = pageThemeCss(theme);
72
+ return css ? `<style>${css}</style>` : "";
63
73
  }
64
74
  const runtimeSlot = (name, fallback) => `<!--sf-runtime:${name}:start-->${fallback}<!--sf-runtime:${name}:end-->`;
65
75
  function renderFiles(files) {
@@ -84,7 +94,7 @@ function renderFile(file) {
84
94
  function renderSpacefastBranding(hidden) {
85
95
  const wordmark = hidden
86
96
  ? ""
87
- : `<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
+ : `<span class="sf-powered" tabindex="0"><svg class="sf-wordmark" viewBox="0 0 152 28.9062" role="img" aria-label="Spacefast"><path fill="currentColor" 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>`;
88
98
  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>`;
89
99
  }
90
100
  function renderElement(element, context) {
@@ -128,6 +138,7 @@ const ELEMENT_PATTERN = /<\s*(sf-[a-z0-9-]+)\b[^>]*>(?:\s*<\s*\/\s*\1\s*>)?|<\s*
128
138
  export function expandPageHtml(source, context) {
129
139
  let output = source.replace(ELEMENT_PATTERN, (_match, paired, unary) => {
130
140
  const name = (paired ?? unary)?.toLowerCase();
141
+ // SAFETY: The set membership check admits only names from the PageElement literal set.
131
142
  return name && knownElements.has(name) ? renderElement(name, context) : _match;
132
143
  });
133
144
  if (output.includes("/*sf-default-fonts*/")) {
@@ -377,7 +388,7 @@ export function defaultPageBody(id) {
377
388
  // The built-in palette runs through the same readability engine as pushed
378
389
  // themes, so buttons and links remain readable for every accepted accent.
379
390
  const defaultPageColors = derivePageColors({});
380
- 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:"Recoleta",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:400 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}}`;
391
+ 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:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",system-ui,Roboto,"Helvetica Neue",Arial,sans-serif;--sf-display-font:"Recoleta",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:400 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:14px;color:var(--sf-fg)}@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}}`;
381
392
  const accessCss = `.sf-page.access .sf-main{max-width:26rem;padding:clamp(42px,8vh,72px) 0}.sf-page.access h1{max-width:18ch;margin-bottom:14px;font:600 clamp(2rem,7vw,2.65rem)/1.12 var(--sf-font);letter-spacing:-.035em}.sf-page.access .sf-copy{font-size:15px;line-height:1.6}.sf-access-default{display:flex;width:100%;flex-direction:column;align-items:center}.sf-access-status{padding:10px 12px;border:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);border-radius:8px;background:color-mix(in srgb,var(--sf-fg) 4%,transparent);color:var(--sf-fg)}.sf-access-lanes{display:flex;flex-direction:column;gap:16px;width:min(23rem,100%);margin-top:30px;text-align:left}.sf-access-lanes form{display:flex;flex-direction:column;gap:14px;margin:0}.sf-input{box-sizing:border-box;width:100%;min-height:46px;padding:11px 12px;border:1px solid color-mix(in srgb,var(--sf-fg) 18%,transparent);border-radius:8px;outline:2px solid transparent;outline-offset:-1px;background:color-mix(in srgb,var(--sf-bg) 92%,var(--sf-fg));color:var(--sf-fg);font:500 15px/1.45 var(--sf-font);appearance:none}.sf-input::placeholder{color:color-mix(in srgb,var(--sf-muted) 82%,transparent);font:inherit}.sf-input:hover{border-color:color-mix(in srgb,var(--sf-fg) 34%,transparent)}.sf-input:focus-visible{border-color:var(--sf-accent);outline-color:var(--sf-accent)}textarea.sf-input{min-height:92px;resize:vertical}.sf-label{color:var(--sf-fg);font:600 13px/1.4 var(--sf-font)}.sf-access-lanes .sf-button{width:100%;min-height:46px;border-radius:8px}.sf-access-or{display:flex;align-items:center;gap:12px;color:var(--sf-muted);font-size:13px}.sf-access-or:before,.sf-access-or:after{content:"";flex:1;border-top:1px solid color-mix(in srgb,var(--sf-fg) 12%,transparent)}.sf-error{margin:0;color:var(--sf-error);font-size:14px}.sf-access-password-row{display:flex;gap:8px}.sf-access-password-row .sf-input{min-width:0;flex:1}.sf-access-password-row .sf-button{width:auto;flex:none}.sf-access-request{padding-top:2px}.sf-access-request summary{padding:4px 0;cursor:pointer;color:var(--sf-muted);font:550 14px/1.5 var(--sf-font)}.sf-access-request summary:hover{color:var(--sf-fg)}.sf-access-request[open] summary{color:var(--sf-fg)}.sf-access-request form{margin-top:16px}@media(max-width:520px){.sf-page.access .sf-main{padding:38px 0}.sf-page.access h1{font-size:2rem}.sf-page.access .sf-copy{font-size:16px}.sf-input{min-height:48px;font-size:16px}.sf-access-lanes .sf-button{min-height:48px}.sf-access-password-row{flex-direction:column}.sf-access-password-row .sf-button{width:100%}}@media(prefers-color-scheme:dark){.sf-access-status{background:color-mix(in srgb,var(--sf-fg) 5%,transparent)}}`;
382
393
  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>';
383
394
  const pageFooter = "<sf-spacefast-branding></sf-spacefast-branding>";
@@ -400,8 +411,10 @@ export function defaultPageTemplate(id) {
400
411
  }
401
412
  export function pageIdFromFilename(filename) {
402
413
  const id = filename.replace(/\.html$/, "");
414
+ // SAFETY: The set membership check admits only ids from the CustomPageId literal set.
403
415
  return customPageIds.has(id) ? id : null;
404
416
  }
405
417
  export function allDefaultPageTemplates() {
418
+ // SAFETY: SITE_PAGE_IDS maps every SitePageId to its corresponding default template entry.
406
419
  return Object.fromEntries(SITE_PAGE_IDS.map((id) => [id, DEFAULT_PAGE_TEMPLATES[id]]));
407
420
  }
@@ -25,6 +25,13 @@ export declare function normalizePublishPath(relativePath: string): string;
25
25
  * could mint its own tokens.
26
26
  */
27
27
  export declare const PUBLISHABLE_BUILD_ARTIFACT_PREFIX = "__spacefast/functions/bundles/";
28
+ /**
29
+ * The bundle carve-out's sibling for the Next-cache seed document: the same
30
+ * reasoning (travel through the ordinary manifest and upload path, stay
31
+ * unservable by default), the same deliberate narrowness (a prefix of its
32
+ * own, never the whole `functions/` directory — see the bundle comment).
33
+ */
34
+ export declare const PUBLISHABLE_SEED_ARTIFACT_PREFIX = "__spacefast/functions/seeds/";
28
35
  export declare function publishPathIgnored(relativePath: string): boolean;
29
36
  export declare function publishPathIsConvention(relativePath: string): boolean;
30
37
  export declare function providerRuntimeFeatureWarnings(paths: readonly string[]): PublishManifestWarning[];
@@ -146,6 +146,16 @@
146
146
  "path": "__spacefast/functions/bundlesX/evil.json",
147
147
  "expected": "static_runtime_control_path_not_supported"
148
148
  },
149
+ {
150
+ "path": "__spacefast/functions/seeds/abc123/seed.json",
151
+ "expected": null,
152
+ "why": "the Next-cache seed document, the bundle carve-out's sibling"
153
+ },
154
+ {
155
+ "path": "__spacefast/functions/seeds.json",
156
+ "expected": "static_runtime_control_path_not_supported",
157
+ "why": "the carve-out is the seeds/ prefix, not anything starting with seeds"
158
+ },
149
159
  {
150
160
  "path": "__spacefast/zero/deploy.json",
151
161
  "expected": null,
@@ -201,10 +211,6 @@
201
211
  "path": "engine-manifest.json",
202
212
  "expected": "static_runtime_control_path_not_supported"
203
213
  },
204
- {
205
- "path": "entrypoint-shim.php",
206
- "expected": "static_runtime_control_path_not_supported"
207
- },
208
214
  {
209
215
  "path": "nested/custom-redirects.php",
210
216
  "expected": null,
@@ -213,7 +219,7 @@
213
219
  {
214
220
  "path": "init.php",
215
221
  "expected": null,
216
- "why": "engine/init.php installs under .stattic/engine/, not the docroot"
222
+ "why": "engine/init.php installs inside an immutable release, not the docroot"
217
223
  },
218
224
  {
219
225
  "path": ".spacefast/storage/routes/current.php",
@@ -2,7 +2,7 @@ import { BRAND } from "../brand.js";
2
2
  import { WORST_CASE_QUOTAS } from "../contracts/quotas.js";
3
3
  import { SPACE_CONFIG_PRIVATE_FILES } from "../contracts/space-config.js";
4
4
  import { ZERO_RUNTIME_DEPLOY_PATH } from "../contracts/zero.js";
5
- // The ONE static publish policy (internal-docs/platform.md "Upload Contract"):
5
+ // The ONE static publish policy (the platform spec "Upload Contract"):
6
6
  // path denials, manifest scale caps, plan caps, duplicate detection, and the
7
7
  // free-plan content rules live here. Consumed by API manifest validation, CLI
8
8
  // local preflight, inline archive extraction, and the runtime parity fixtures
@@ -112,6 +112,13 @@ export function normalizePublishPath(relativePath) {
112
112
  * could mint its own tokens.
113
113
  */
114
114
  export const PUBLISHABLE_BUILD_ARTIFACT_PREFIX = "__spacefast/functions/bundles/";
115
+ /**
116
+ * The bundle carve-out's sibling for the Next-cache seed document: the same
117
+ * reasoning (travel through the ordinary manifest and upload path, stay
118
+ * unservable by default), the same deliberate narrowness (a prefix of its
119
+ * own, never the whole `functions/` directory — see the bundle comment).
120
+ */
121
+ export const PUBLISHABLE_SEED_ARTIFACT_PREFIX = "__spacefast/functions/seeds/";
115
122
  /**
116
123
  * The one FILE inside the control prefix a publish may write.
117
124
  *
@@ -127,6 +134,7 @@ export const PUBLISHABLE_BUILD_ARTIFACT_PREFIX = "__spacefast/functions/bundles/
127
134
  */
128
135
  function isPublishableBuildArtifactPath(normalizedPath, lowerPath) {
129
136
  return (lowerPath.startsWith(PUBLISHABLE_BUILD_ARTIFACT_PREFIX) ||
137
+ lowerPath.startsWith(PUBLISHABLE_SEED_ARTIFACT_PREFIX) ||
130
138
  normalizedPath === ZERO_RUNTIME_DEPLOY_PATH);
131
139
  }
132
140
  export function publishPathIgnored(relativePath) {
@@ -6,10 +6,10 @@ export declare const qk: {
6
6
  accountInvitations: () => readonly ["account", "invitations"];
7
7
  apiTokens: (teamId?: string) => readonly ["api-tokens"] | readonly ["api-tokens", string];
8
8
  agentAccounts: (teamId: string) => readonly ["agent-accounts", string];
9
- myShelf: () => readonly ["my-shelf"];
10
9
  mcpDashboard: (teamId: string) => readonly ["mcp", "dashboard", string];
11
10
  teams: () => readonly ["teams", "list"];
12
11
  team: (ref: string) => readonly ["teams", "by-ref", string];
12
+ teamDetail: (id: string) => readonly ["teams", string, "detail"];
13
13
  teamMembers: (id: string) => readonly ["teams", string, "members"];
14
14
  teamInvites: (id: string) => readonly ["teams", string, "invites"];
15
15
  teamBilling: (id: string) => readonly ["teams", string, "billing"];
@@ -41,7 +41,7 @@ export declare const qk: {
41
41
  spaceBuilds: (id: string) => readonly ["spaces", string, "builds"];
42
42
  spaceRepositoryConnection: (id: string, connectionType?: string) => readonly ["spaces", string, "repository-connection", string];
43
43
  repositoryInstallations: (spaceId: string) => readonly ["spaces", string, "repository-installations"];
44
- repositoryInstallationRepositories: (installationId: string) => readonly ["repository-connections", "installations", string, "repositories"];
44
+ repositoryInstallationRepositories: (spaceId: string, installationId: string) => readonly ["spaces", string, "repository-installations", string, "repositories"];
45
45
  spaceRepositoryAnalyze: (id: string, connectionType?: string, ref?: string, root?: string) => readonly ["spaces", string, "repository-connection", string, "analyze", string, string];
46
46
  spaceRepositoryWebhookDeliveries: (id: string, connectionType?: string) => readonly ["spaces", string, "repository-webhook-deliveries", string];
47
47
  teamWebhooks: (teamId: string) => readonly ["teams", string, "webhooks"];
@@ -54,7 +54,7 @@ export declare const qk: {
54
54
  spaceVersionFileUrlPreview: (id: string, versionId: string, path: string) => readonly ["spaces", string, "versions", string, "files", string, "url-preview"];
55
55
  spaceVersionFileDiff: (id: string, versionId: string, baseVersionId?: string) => readonly ["spaces", string, "versions", string, "files", "diff", string];
56
56
  spaceVersionConventionArtifacts: (id: string, versionId: string) => readonly ["spaces", string, "versions", string, "convention-artifacts"];
57
- spaceVersionConventionArtifact: (id: string, versionId: string, kind: "redirects" | "headers") => readonly ["spaces", string, "versions", string, "convention-artifacts", "redirects" | "headers"];
57
+ spaceVersionConventionArtifact: (id: string, versionId: string, kind: "redirects" | "headers") => readonly ["spaces", string, "versions", string, "convention-artifacts", "headers" | "redirects"];
58
58
  spaceVersionRoutingSummary: (id: string, versionId: string) => readonly ["spaces", string, "versions", string, "routing-summary"];
59
59
  spaceDiagnostics: (id: string) => readonly ["spaces", string, "diagnostics"];
60
60
  spaceDomains: (id: string) => readonly ["spaces", string, "domains"];
@@ -67,7 +67,7 @@ export declare const qk: {
67
67
  spaceRuntimeRealtimeEvents: (id: string, limit: number) => readonly ["spaces", string, "runtime", "realtime", "events", number];
68
68
  spaceVariables: (id: string) => readonly ["spaces", string, "variables"];
69
69
  spaceCertificates: (id: string) => readonly ["spaces", string, "runtime", "certificates"];
70
- spaceLogs: (id: string, kind: "build" | "access" | "runtime") => readonly ["spaces", string, "logs", "build" | "runtime" | "access"];
70
+ spaceLogs: (id: string, kind: "build" | "access" | "runtime") => readonly ["spaces", string, "logs", "build" | "access" | "runtime"];
71
71
  spaceAnalytics: (id: string, window: string) => readonly ["spaces", string, "analytics", string];
72
72
  teamAgentHandoffs: (teamId: string) => readonly ["teams", string, "agent-handoffs"];
73
73
  teamAccessDefaults: (teamId: string) => readonly ["teams", string, "access", "defaults"];
@@ -8,13 +8,13 @@ export const qk = {
8
8
  // teamId yields the bare prefix (personal scope, and invalidate-all sweeps).
9
9
  apiTokens: (teamId) => teamId === undefined ? ["api-tokens"] : ["api-tokens", teamId],
10
10
  agentAccounts: (teamId) => ["agent-accounts", teamId],
11
- myShelf: () => ["my-shelf"],
12
11
  mcpDashboard: (teamId) => ["mcp", "dashboard", teamId],
13
12
  // The teams LIST gets its own leaf so invalidating it never cascades into
14
13
  // ["teams", id, ...] resource queries (members, spaces, domains, ...).
15
14
  teams: () => ["teams", "list"],
16
15
  // Keyed by slug (the URL ref), so it lives outside the id namespace.
17
16
  team: (ref) => ["teams", "by-ref", ref],
17
+ teamDetail: (id) => ["teams", id, "detail"],
18
18
  teamMembers: (id) => ["teams", id, "members"],
19
19
  teamInvites: (id) => ["teams", id, "invites"],
20
20
  teamBilling: (id) => ["teams", id, "billing"],
@@ -55,7 +55,7 @@ export const qk = {
55
55
  spaceBuilds: (id) => ["spaces", id, "builds"],
56
56
  spaceRepositoryConnection: (id, connectionType = "connected") => ["spaces", id, "repository-connection", connectionType],
57
57
  repositoryInstallations: (spaceId) => [...qk.space(spaceId), "repository-installations"],
58
- repositoryInstallationRepositories: (installationId) => ["repository-connections", "installations", installationId, "repositories"],
58
+ repositoryInstallationRepositories: (spaceId, installationId) => [...qk.space(spaceId), "repository-installations", installationId, "repositories"],
59
59
  spaceRepositoryAnalyze: (id, connectionType = "connected", ref = "", root = "") => ["spaces", id, "repository-connection", connectionType, "analyze", ref, root],
60
60
  spaceRepositoryWebhookDeliveries: (id, connectionType = "connected") => ["spaces", id, "repository-webhook-deliveries", connectionType],
61
61
  teamWebhooks: (teamId) => ["teams", teamId, "webhooks"],
@@ -4,6 +4,11 @@ export declare function runtimeUploadFetchUrl(input: {
4
4
  uploadId: string;
5
5
  path: string;
6
6
  }): string;
7
+ export declare function runtimeUploadFileUrl(input: {
8
+ baseUrl: string;
9
+ uploadId: string;
10
+ path: string;
11
+ }): string;
7
12
  export declare function runtimeUploadBlobUrl(input: {
8
13
  baseUrl: string;
9
14
  spaceId: string;
@@ -15,6 +15,13 @@ export function runtimeUploadFetchUrl(input) {
15
15
  path: runtimeUploadCanonicalPath(input.path),
16
16
  }), input.baseUrl).toString();
17
17
  }
18
+ export function runtimeUploadFileUrl(input) {
19
+ return new URL(runtimeUploadPath({
20
+ op: "file",
21
+ upload_id: input.uploadId,
22
+ path: runtimeUploadCanonicalPath(input.path),
23
+ }), input.baseUrl).toString();
24
+ }
18
25
  export function runtimeUploadBlobUrl(input) {
19
26
  return new URL(runtimeUploadPath({
20
27
  route: `/spaces/${encodeURIComponent(input.spaceId)}/blobs/${encodeURIComponent(input.sha256)}`,
@@ -273,7 +273,6 @@ function lockName(leaf) {
273
273
  const digest = createHash("sha256").update(identity).digest("hex").slice(0, 24);
274
274
  return `.spacefast-${digest}.lock`;
275
275
  }
276
- const LEGACY_LEAF_LOCK_STALE_MS = 2 * 60_000;
277
276
  function serializeLeafLockOwner(owner) {
278
277
  const { phase, ...identity } = owner;
279
278
  return `${phase === "active" ? "A" : "R"}${JSON.stringify(identity)}\n`;
@@ -293,6 +292,7 @@ function parseLeafLockOwner(raw) {
293
292
  (value.temporaryName === undefined ||
294
293
  (typeof value.temporaryName === "string" &&
295
294
  /^\.spacefast-[0-9a-f-]{36}\.tmp$/.test(value.temporaryName)))) {
295
+ // SAFETY: The preceding checks establish every required LeafLockOwner field and the optional temporaryName field.
296
296
  return { ...value, phase };
297
297
  }
298
298
  }
@@ -360,14 +360,8 @@ function processIsAlive(pid) {
360
360
  }
361
361
  }
362
362
  async function leafLockRecordIsStale(record) {
363
- // The original lock format was an empty exclusive file. It carries no owner
364
- // identity, so a short age fence is the only safe compatibility signal: a
365
- // fresh legacy writer stays exclusive, while crash wreckage can recover.
366
- if (record?.kind === "legacy") {
367
- return Date.now() - record.modifiedAtMs > LEGACY_LEAF_LOCK_STALE_MS;
368
- }
369
- // A malformed non-empty record is not proof that no writer owns it. New lock
370
- // records are atomically published, so corruption still needs explicit recovery.
363
+ // A malformed record is not proof that no writer owns it. New lock records are
364
+ // atomically published, so corruption still needs explicit recovery.
371
365
  if (record?.kind !== "owned")
372
366
  return false;
373
367
  const { owner } = record;
@@ -399,13 +393,8 @@ async function readLeafLockRecord(filePath) {
399
393
  visibleMetadata.ino !== metadata.ino) {
400
394
  return { kind: "invalid" };
401
395
  }
402
- const raw = await file.readFile("utf8");
403
- const owner = parseLeafLockOwner(raw);
404
- if (owner)
405
- return { kind: "owned", owner };
406
- return raw.length === 0
407
- ? { kind: "legacy", modifiedAtMs: metadata.mtimeMs }
408
- : { kind: "invalid" };
396
+ const owner = parseLeafLockOwner(await file.readFile("utf8"));
397
+ return owner ? { kind: "owned", owner } : { kind: "invalid" };
409
398
  }
410
399
  finally {
411
400
  await file.close();
@@ -3,7 +3,7 @@ import { type SpaceConfig, type SpaceConfigFile } from "../contracts/space-confi
3
3
  export type SpaceConfigBlockingIssueCode = "config_file_too_large" | "config_templates_over_limit" | "config_meta_too_long" | "config_name_too_long" | "config_functions_key_removed" | "config_invalid" | "config_runtime_invalid_kind" | "config_runtime_entry_missing" | "inject_snippet_too_large" | "inject_invalid";
4
4
  export type SpaceConfigIssue = {
5
5
  severity: "info" | "warning" | "error";
6
- code: SpaceConfigBlockingIssueCode | "single_html_index" | "files_mode_inferred";
6
+ code: SpaceConfigBlockingIssueCode | "single_html_index" | "files_mode_inferred" | "listing_shadows_fallback";
7
7
  message: string;
8
8
  /** JSON-path-lite pointer into the config shape (e.g. `meta.title`). */
9
9
  path?: string;
@@ -57,6 +57,7 @@ function capIssues(candidate) {
57
57
  }
58
58
  else {
59
59
  for (const key of ["head", "bodyStart", "bodyEnd", "noscript"]) {
60
+ // SAFETY: The enclosing branch proves inject is a non-null, non-array object.
60
61
  const snippets = inject[key];
61
62
  if (snippets === undefined) {
62
63
  continue;
@@ -174,6 +175,7 @@ export function stripJsonComments(raw) {
174
175
  let inLineComment = false;
175
176
  let inBlockComment = false;
176
177
  for (let i = 0; i < raw.length; i += 1) {
178
+ // SAFETY: The loop condition proves i addresses a character in raw.
177
179
  const char = raw[i];
178
180
  const next = raw[i + 1];
179
181
  if (inLineComment) {
@@ -220,6 +222,7 @@ export function stripJsonComments(raw) {
220
222
  // Trailing comma: skip when the next non-whitespace/non-comment char closes.
221
223
  let j = i + 1;
222
224
  while (j < raw.length) {
225
+ // SAFETY: The loop condition proves j addresses a character in raw.
223
226
  const ahead = raw[j];
224
227
  if (/\s/.test(ahead)) {
225
228
  j += 1;
@@ -5,14 +5,20 @@ export declare const PLATFORM_MANAGED_RESPONSE_HEADERS: string[];
5
5
  export declare const PLATFORM_MANAGED_RESPONSE_HEADER_PREFIXES: string[];
6
6
  /** True when user rules may neither set nor remove this response header. */
7
7
  export declare function isPlatformManagedResponseHeader(name: string): boolean;
8
- export declare const NON_IMMUTABLE_EXTENSIONS: string[];
9
- export declare const GUARANTEED_IMMUTABLE_PREFIXES: string[];
8
+ /**
9
+ * The token grammar a `_headers` rule accepts for a header NAME. The routing
10
+ * compiler enforces it; anything that GENERATES `_headers` rules (the Next
11
+ * materializer's framework sections) must gate on the same pattern, so a
12
+ * generated rule can never fail the author's publish.
13
+ */
14
+ export declare const HEADER_RULE_NAME_PATTERN: RegExp;
15
+ /** `content-type` → `Content-Type`: the casing compiled rules and generated `_headers` carry. */
16
+ export declare function canonicalHeaderName(input: string): string;
10
17
  /** Lowercased extension without the leading dot, posix semantics. Dotfiles have no extension. */
11
18
  export declare function pathExtension(filePath: string): string;
12
19
  /** Basename without its extension (the "stem"), posix semantics. */
13
20
  export declare function pathStem(filePath: string): string;
14
21
  export declare function isPhpLikeStaticPath(filePath: string): boolean;
15
- export declare function isImmutableAssetPath(filePath: string): boolean;
16
22
  /** Forced inert serving policy for a committed static file. PHP-like files are never executable. */
17
23
  export declare function runtimeFileSafety(filePath: string): {
18
24
  executable: false;