@spacefast/common 0.0.11 → 0.0.12

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 (122) 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 +42 -24
  12. package/dist/contracts/access.js +73 -34
  13. package/dist/contracts/activity.d.ts +1 -1
  14. package/dist/contracts/activity.js +5 -5
  15. package/dist/contracts/api-keys.d.ts +53 -45
  16. package/dist/contracts/api-keys.js +1 -0
  17. package/dist/contracts/archives.d.ts +8 -8
  18. package/dist/contracts/builds.d.ts +8 -8
  19. package/dist/contracts/builds.js +28 -48
  20. package/dist/contracts/channels.js +1 -2
  21. package/dist/contracts/comments.d.ts +55 -19
  22. package/dist/contracts/comments.js +56 -17
  23. package/dist/contracts/common.d.ts +4 -3
  24. package/dist/contracts/common.js +2 -0
  25. package/dist/contracts/deployments.d.ts +3 -3
  26. package/dist/contracts/device-auth.d.ts +14 -12
  27. package/dist/contracts/docs.d.ts +5 -5
  28. package/dist/contracts/domains.d.ts +261 -1330
  29. package/dist/contracts/domains.js +39 -301
  30. package/dist/contracts/email-preferences.d.ts +50 -0
  31. package/dist/contracts/email-preferences.js +44 -0
  32. package/dist/contracts/enums.d.ts +1 -1
  33. package/dist/contracts/error-code-meta.d.ts +84 -15
  34. package/dist/contracts/error-code-meta.js +28 -4
  35. package/dist/contracts/error-codes.d.ts +1 -1
  36. package/dist/contracts/error-codes.js +24 -6
  37. package/dist/contracts/events.d.ts +13 -13
  38. package/dist/contracts/feature-lifecycle.d.ts +644 -2
  39. package/dist/contracts/feature-lifecycle.js +685 -2
  40. package/dist/contracts/features.d.ts +236 -24
  41. package/dist/contracts/features.js +3 -272
  42. package/dist/contracts/ids.d.ts +1 -2
  43. package/dist/contracts/ids.js +1 -2
  44. package/dist/contracts/internal.d.ts +3 -3
  45. package/dist/contracts/mcp.d.ts +6 -6
  46. package/dist/contracts/me.js +1 -1
  47. package/dist/contracts/mounts.d.ts +142 -0
  48. package/dist/contracts/mounts.js +56 -0
  49. package/dist/contracts/oauth-resources.d.ts +5 -5
  50. package/dist/contracts/oauth-resources.js +1 -0
  51. package/dist/contracts/operations.d.ts +2 -2
  52. package/dist/contracts/pages.d.ts +2 -2
  53. package/dist/contracts/pages.js +8 -3
  54. package/dist/contracts/plan-policy.d.ts +1 -1
  55. package/dist/contracts/platform.d.ts +3 -3
  56. package/dist/contracts/resources.d.ts +12 -20
  57. package/dist/contracts/resources.js +13 -37
  58. package/dist/contracts/runtime-api.d.ts +330 -16
  59. package/dist/contracts/runtime-api.js +58 -14
  60. package/dist/contracts/sf-config-v1.js +1 -1
  61. package/dist/contracts/sites.d.ts +12 -12
  62. package/dist/contracts/sites.js +18 -11
  63. package/dist/contracts/space-config.d.ts +2 -3
  64. package/dist/contracts/space-config.js +3 -1
  65. package/dist/contracts/spaces.d.ts +143 -102
  66. package/dist/contracts/spaces.js +109 -43
  67. package/dist/contracts/superadmin-emails.d.ts +25 -1
  68. package/dist/contracts/superadmin-emails.js +10 -15
  69. package/dist/contracts/superadmin-runtime.d.ts +2 -2
  70. package/dist/contracts/superadmin-spaces.d.ts +6 -14
  71. package/dist/contracts/tags.d.ts +43 -13
  72. package/dist/contracts/tags.js +195 -11
  73. package/dist/contracts/transfers.d.ts +1 -1
  74. package/dist/contracts/variables.d.ts +4 -4
  75. package/dist/contracts/variables.js +4 -3
  76. package/dist/contracts/webhooks.d.ts +2 -2
  77. package/dist/contracts/zero.d.ts +329 -7
  78. package/dist/contracts/zero.js +121 -2
  79. package/dist/dashboard-paths/index.d.ts +1 -1
  80. package/dist/docs/agent-setup.d.ts +4 -2
  81. package/dist/docs/agent-setup.js +15 -7
  82. package/dist/docs/agent-solutions.js +1 -1
  83. package/dist/docs/catalog.d.ts +8 -10
  84. package/dist/docs/catalog.js +8 -10
  85. package/dist/docs/error-docs.js +86 -18
  86. package/dist/utils/access-grant.d.ts +14 -0
  87. package/dist/utils/access-grant.js +88 -0
  88. package/dist/utils/access-match.d.ts +7 -0
  89. package/dist/utils/access-match.fixtures.json +513 -0
  90. package/dist/utils/access-match.js +32 -16
  91. package/dist/utils/access-verdict.fixtures.json +421 -0
  92. package/dist/utils/credential-policy.d.ts +1 -1
  93. package/dist/utils/dns-instructions.d.ts +29 -0
  94. package/dist/utils/dns-instructions.js +78 -0
  95. package/dist/utils/domain-diagnostics.d.ts +29 -0
  96. package/dist/utils/domain-diagnostics.js +63 -0
  97. package/dist/utils/egress-policy.fixtures.json +51 -0
  98. package/dist/utils/id-hints.d.ts +1 -0
  99. package/dist/utils/id-hints.js +55 -13
  100. package/dist/utils/page-colors.d.ts +50 -0
  101. package/dist/utils/page-colors.js +167 -0
  102. package/dist/utils/page-fonts.d.ts +2 -0
  103. package/dist/utils/page-fonts.js +4 -0
  104. package/dist/utils/pages.d.ts +8 -1
  105. package/dist/utils/pages.js +123 -37
  106. package/dist/utils/production-runtime.d.ts +18 -0
  107. package/dist/utils/production-runtime.js +28 -0
  108. package/dist/utils/query-keys.d.ts +6 -7
  109. package/dist/utils/query-keys.js +11 -12
  110. package/dist/utils/sf-config-v1.fixtures.json +9 -0
  111. package/dist/utils/sf-config-v1.js +28 -19
  112. package/dist/utils/source-archive-policy.d.ts +2 -0
  113. package/dist/utils/source-archive-policy.js +2 -0
  114. package/dist/utils/static-runtime-policy.fixtures.json +57 -0
  115. package/dist/utils/static-runtime-policy.js +1 -1
  116. package/package.json +5 -4
  117. package/dist/contracts/annotations.d.ts +0 -327
  118. package/dist/contracts/annotations.js +0 -235
  119. package/dist/contracts/generated-feature-launch-entries.d.ts +0 -2
  120. package/dist/contracts/generated-feature-launch-entries.js +0 -518
  121. package/dist/contracts/publishes.d.ts +0 -135
  122. package/dist/contracts/publishes.js +0 -162
@@ -1,7 +1,27 @@
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";
2
6
  const encoder = new TextEncoder();
3
7
  const knownElements = new Set(PAGE_ELEMENTS);
4
8
  const customPageIds = new Set(CUSTOM_PAGE_IDS);
9
+ export function isPagePublicPath(path) {
10
+ const lower = path.toLowerCase();
11
+ if (lower === "theme.json" ||
12
+ lower === "_redirects" ||
13
+ lower === "_headers" ||
14
+ lower === "_config.json" ||
15
+ lower === "_routes.json" ||
16
+ SPACE_CONFIG_ACCEPTED_FILES.some((candidate) => candidate === lower) ||
17
+ lower.split("/").includes("_pages") ||
18
+ lower === "_layout.html" ||
19
+ lower.endsWith("/_layout.html"))
20
+ return false;
21
+ return !lower
22
+ .split("/")
23
+ .some((segment, index) => segment.startsWith(".") && !(index === 0 && segment === ".well-known"));
24
+ }
5
25
  export function escapePageHtml(value) {
6
26
  return value
7
27
  .replaceAll("&", "&")
@@ -10,38 +30,92 @@ export function escapePageHtml(value) {
10
30
  .replaceAll('"', """)
11
31
  .replaceAll("'", "'");
12
32
  }
33
+ // Pushed colors are intent; the readability engine (page-colors.ts) derives
34
+ // what the templates actually paint with. A custom background pins a complete
35
+ // scheme for both OS modes — emitting `--sf-bg` alone used to leave dark-mode
36
+ // visitors with the dark foreground on the user's light background.
13
37
  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>` : "";
38
+ const root = [];
39
+ const dark = [];
40
+ if (theme.accent || theme.background) {
41
+ const derived = derivePageColors(theme);
42
+ // `derived.accent` is the parse-validated input (or the default), never
43
+ // an arbitrary string straight from the theme.
44
+ if (theme.accent)
45
+ root.push(`--sf-accent:${derived.accent}`);
46
+ root.push(`--sf-on-accent:${derived.onAccent}`, `--sf-link:${derived.link}`);
47
+ if (derived.pinned) {
48
+ 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}`);
49
+ }
50
+ else if (derived.linkDark) {
51
+ dark.push(`--sf-link:${derived.linkDark}`);
52
+ }
53
+ }
54
+ if (theme.font)
55
+ root.push(`--sf-font:${theme.font}`, `--sf-display-font:${theme.font}`);
56
+ if (root.length === 0)
57
+ return "";
58
+ const darkBlock = dark.length > 0 ? `@media(prefers-color-scheme:dark){:root{${dark.join(";")}}}` : "";
59
+ return `<style>:root{${root.join(";")}}${darkBlock}</style>`;
20
60
  }
61
+ const runtimeSlot = (name, fallback) => `<!--sf-runtime:${name}:start-->${fallback}<!--sf-runtime:${name}:end-->`;
21
62
  function renderFiles(files) {
22
63
  if (files.length === 0)
23
64
  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>`)
65
+ return `<ul class="sf-files" role="list">${files
66
+ .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
67
  .join("")}</ul>`;
27
68
  }
28
69
  function renderFile(file) {
29
70
  if (!file)
30
71
  return '<p class="sf-empty">File unavailable.</p>';
31
- 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>`;
72
+ const rawHref = `${file.href}${file.href.includes("?") ? "&" : "?"}spacefast_raw=1`;
73
+ const href = escapePageHtml(rawHref);
74
+ const name = escapePageHtml(file.name);
75
+ const media = file.mime?.startsWith("image/")
76
+ ? `<img class="sf-preview" src="${href}" alt="">`
77
+ : `<iframe class="sf-preview" src="${href}" title="Preview of ${name}" loading="lazy" sandbox></iframe>`;
78
+ const downloadHref = file.downloadHref
79
+ ? `${file.downloadHref}${file.downloadHref.includes("?") ? "&" : "?"}spacefast_raw=1`
80
+ : rawHref;
81
+ return `<div class="sf-preview-wrap">${media}</div><dl class="sf-file-meta"><div><dt>Name</dt><dd>${name}</dd></div>${file.size ? `<div><dt>Size</dt><dd>${escapePageHtml(file.size)}</dd></div>` : ""}${file.mime ? `<div><dt>Type</dt><dd>${escapePageHtml(file.mime)}</dd></div>` : ""}</dl><div class="sf-actions"><a class="sf-button" href="${escapePageHtml(downloadHref)}" download>Download</a><a class="sf-ghost" href="${href}">View raw</a></div>`;
82
+ }
83
+ function renderSpacefastBranding(hidden) {
84
+ const wordmark = hidden
85
+ ? ""
86
+ : `<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>`;
87
+ 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
88
  }
37
89
  function renderElement(element, context) {
38
90
  switch (element) {
39
91
  case "sf-content":
40
92
  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>`;
93
+ case "sf-password-form": {
94
+ const hasError = Boolean(context.passwordError);
95
+ const error = hasError
96
+ ? `<p class="sf-error" id="sf-password-error" role="alert">${escapePageHtml(context.passwordError ?? "")}</p>`
97
+ : "";
98
+ const form = `<form class="sf-form" method="post" action="${escapePageHtml(context.passwordAction ?? "")}"><label for="sf-password">Password</label><div class="sf-form-row"><input id="sf-password" name="_pw" type="password" required autocomplete="current-password"${hasError ? ' aria-invalid="true" aria-describedby="sf-password-error"' : ""}><button type="submit">Continue</button></div>${error}</form>`;
99
+ return runtimeSlot("challenge", form);
100
+ }
43
101
  case "sf-login":
44
- return `<a class="sf-button" href="${escapePageHtml(context.loginHref ?? "/__spacefast/access/login")}">Log in</a>`;
102
+ return runtimeSlot("challenge", `<div class="sf-actions"><a class="sf-button" href="${escapePageHtml(context.loginHref ?? "/__spacefast/access/login")}">Continue with Spacefast</a></div>`);
103
+ case "sf-denial-context":
104
+ return runtimeSlot("denial", '<p class="sf-copy">Access to this space is restricted.</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,17 @@ export function validatePageSources(input) {
115
190
  });
116
191
  const allowed = element === "sf-logo" ||
117
192
  element === "sf-name" ||
193
+ element === "sf-spacefast-branding" ||
194
+ (id === "404" && element === "sf-page-path") ||
118
195
  (id === "password" && element === "sf-password-form") ||
119
- (id === "index" && element === "sf-files") ||
120
- (id === "preview" && element === "sf-file");
196
+ (id === "denied" && element === "sf-denial-context") ||
197
+ (id === "index" &&
198
+ (element === "sf-files" ||
199
+ element === "sf-page-path" ||
200
+ element === "sf-page-title" ||
201
+ element === "sf-page-summary")) ||
202
+ (id === "preview" &&
203
+ (element === "sf-file" || element === "sf-page-path" || element === "sf-page-title"));
121
204
  if (knownElements.has(element) && !allowed)
122
205
  diagnostics.push({
123
206
  severity: "error",
@@ -169,7 +252,11 @@ export function validatePageSources(input) {
169
252
  message: `Unknown Pages element <${element}>.`,
170
253
  path,
171
254
  });
172
- else if (element !== "sf-content" && element !== "sf-logo" && element !== "sf-name")
255
+ else if (element !== "sf-content" &&
256
+ element !== "sf-logo" &&
257
+ element !== "sf-name" &&
258
+ element !== "sf-page-title" &&
259
+ element !== "sf-spacefast-branding")
173
260
  diagnostics.push({
174
261
  severity: "error",
175
262
  code: "page_element_not_allowed",
@@ -242,26 +329,25 @@ export function defaultPageBody(id) {
242
329
  const source = DEFAULT_PAGE_TEMPLATES[id];
243
330
  return source.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] ?? source;
244
331
  }
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`;
332
+ // The built-in palette runs through the same readability engine as pushed
333
+ // themes, so buttons and links remain readable for every accepted accent.
334
+ const defaultPageColors = derivePageColors({});
335
+ const sharedCss = `/*sf-default-fonts*/:root{color-scheme:light dark;--sf-accent:${PAGE_COLOR_DEFAULTS.accent};--sf-bg:${PAGE_COLOR_DEFAULTS.bg};--sf-fg:${PAGE_COLOR_DEFAULTS.fg};--sf-muted:${PAGE_COLOR_DEFAULTS.muted};--sf-card:${PAGE_COLOR_DEFAULTS.card};--sf-border:${PAGE_COLOR_DEFAULTS.border};--sf-link:${defaultPageColors.link};--sf-on-accent:${defaultPageColors.onAccent};--sf-error:#a82d1a;--sf-font:"Haskoy",-apple-system,BlinkMacSystemFont,ui-sans-serif,"Segoe UI",sans-serif;--sf-display-font:"Merriweather",Georgia,serif;--sf-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace}*{box-sizing:border-box}html{min-width:320px;background:var(--sf-bg);-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh;background:var(--sf-bg);color:var(--sf-fg);font:14px/1.65 var(--sf-font)}a{color:inherit}.sf-page{min-height:100vh;display:flex;flex-direction:column;padding:clamp(22px,4vw,44px)}.sf-header{display:flex;align-items:center;min-height:32px}.sf-page-center .sf-header{justify-content:center}.sf-brand{display:inline-flex;min-width:0;align-items:center;gap:10px;color:var(--sf-fg);text-decoration:none}.sf-brand-mark{width:7px;height:7px;flex:none;background:var(--sf-accent)}.sf-logo{width:28px;height:28px;flex:none;object-fit:contain}.sf-brand strong{min-width:0;overflow-wrap:anywhere;font-size:13px;font-weight:600}.sf-main{flex:1;width:100%;padding:clamp(34px,9vh,84px) 0}.sf-page-plain .sf-main{max-width:680px}.sf-page-center .sf-main{max-width:620px;margin:auto;display:flex;flex-direction:column;align-items:center;text-align:center}.sf-eyebrow{margin:0 0 14px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.12em;text-transform:uppercase;overflow-wrap:anywhere}.sf-page-center .sf-eyebrow{margin-bottom:16px}h1{max-width:20ch;margin:0 0 12px;color:var(--sf-fg);font:430 clamp(1.9rem,5vw,2.8rem)/1.22 var(--sf-display-font);letter-spacing:-.012em;text-wrap:balance}.sf-copy,.sf-sub{max-width:52ch;margin:0;color:var(--sf-muted);text-wrap:pretty}.sf-inline-path{font-family:var(--sf-mono);font-size:.9em;color:var(--sf-fg);overflow-wrap:anywhere}.sf-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:22px}.sf-page-center .sf-actions{justify-content:center}.sf-button,.sf-form button,.sf-login{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,.sf-form button:hover,.sf-login:hover{filter:brightness(1.06)}a:focus-visible,button:focus-visible{outline:2px solid var(--sf-link);outline-offset:2px}.sf-form{width:100%;max-width:360px;margin-top:22px;text-align:left}.sf-form label,.sf-password label{display:block;margin-bottom:8px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.1em;text-transform:uppercase}.sf-password label{flex-basis:100%}.sf-form-row{display:flex;gap:10px}.sf-password form{display:flex;flex-wrap:wrap;gap:10px}.sf-form input,.sf-pw{min-width:0;flex:1;border:1px solid var(--sf-border);background:transparent;color:var(--sf-fg);padding:9px 11px;font:14px/1.5 var(--sf-font)}.sf-form input:focus-visible,.sf-pw:focus-visible{outline:2px solid var(--sf-link);outline-offset:-2px}.sf-form input[aria-invalid=true],.sf-pw[aria-invalid=true]{border-color:var(--sf-error)}.sf-form button,.sf-password button{margin:0}.sf-error{margin:9px 0 0;color:var(--sf-error);font-size:14px}.sf-password .sf-error{flex-basis:100%;margin:0}.sf-block{width:100%}.sf-login-block{display:flex;justify-content:center;margin-top:20px}.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-form-row,.sf-password form{flex-direction:column}.sf-button,.sf-form button,.sf-login{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,.sf-form button,.sf-login{border:1px solid ButtonText}.sf-powered:focus{outline:1px solid CanvasText;outline-offset:3px}}`;
336
+ const pageHeader = '<header class="sf-header"><a class="sf-brand" href="/" aria-label="Homepage"><span class="sf-brand-mark" aria-hidden="true"></span><sf-logo></sf-logo><strong><sf-name></sf-name></strong></a></header>';
337
+ const pageFooter = "<sf-spacefast-branding></sf-spacefast-branding>";
338
+ function document(id, title, body, layout) {
339
+ const noindex = PAGE_CATALOG[id].noindex ? '<meta name="robots" content="noindex">' : "";
340
+ 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
341
  }
249
- const badge = '<p class="sf-badge">Hosted with <a href="https://spacefast.com">Spacefast</a></p>';
250
342
  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),
343
+ "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"),
344
+ password: document("password", "Password required", '<h1>This space is protected</h1><p class="sf-sub">Enter the password to continue.</p><sf-password-form></sf-password-form>', "center"),
345
+ denied: document("denied", "Access denied", "<h1>Access denied</h1><sf-denial-context></sf-denial-context>", "center"),
346
+ login: document("login", "Sign in", '<h1>This space is for members</h1><p class="sf-sub">Sign in to continue — you’ll come right back here.</p><sf-login></sf-login>', "center"),
347
+ index: document("index", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><p class="sf-sub"><sf-page-summary></sf-page-summary></p><sf-files></sf-files>', "plain"),
348
+ 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
349
  };
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`;
350
+ 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
351
  export function defaultPageTemplate(id) {
266
352
  return DEFAULT_PAGE_TEMPLATES[id];
267
353
  }
@@ -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
+ }
@@ -21,7 +21,6 @@ export declare const qk: {
21
21
  teamSpaces: (id: string) => readonly ["teams", string, "spaces"];
22
22
  teamDomains: (id: string) => readonly ["teams", string, "domains"];
23
23
  teamManagedDomains: (id: string) => readonly ["teams", string, "managed-domains"];
24
- domainBindings: () => readonly ["domain-bindings"];
25
24
  slugAvailability: (kind: "team" | "space", slug: string, teamId?: string, currentSlug?: string) => readonly ["slugs", "availability", "space" | "team", string, string, string];
26
25
  teamVariables: (id: string) => readonly ["teams", string, "variables"];
27
26
  teamActivity: (id: string, limit: number) => readonly ["teams", string, "activity", number];
@@ -30,8 +29,8 @@ export declare const qk: {
30
29
  spaceVersions: (id: string) => readonly ["spaces", string, "versions"];
31
30
  spaceVersionsHistory: (id: string) => readonly ["spaces", string, "versions", "history"];
32
31
  spaceCollaboration: (id: string) => readonly ["spaces", string, "collaboration"];
33
- spaceCollaborationEmbedConfig: (id: string) => readonly ["spaces", string, "collaboration", "embed-config"];
34
- spaceAnnotations: (id: string) => readonly ["spaces", string, "annotations"];
32
+ spaceComments: (id: string) => readonly ["spaces", string, "comments"];
33
+ spaceCommentThread: (id: string, threadId: string) => readonly ["spaces", string, "comments", string];
35
34
  spaceActivity: (id: string, limit: number) => readonly ["spaces", string, "activity", number];
36
35
  spaceDeployments: (id: string) => readonly ["spaces", string, "deployments"];
37
36
  spaceTags: (id: string) => readonly ["spaces", string, "tags"];
@@ -39,11 +38,11 @@ export declare const qk: {
39
38
  spaceTagDiff: (id: string, versionId: string) => readonly ["spaces", string, "tags", string, "diff"];
40
39
  spaceTagReleases: (id: string) => readonly ["spaces", string, "tags", "releases"];
41
40
  spaceTagsEffective: (id: string) => readonly ["spaces", string, "tags", "effective"];
42
- build: (id: string) => readonly ["builds", string];
43
- buildLogs: (id: string, limit?: number) => readonly ["builds", string, "logs"] | readonly ["builds", string, "logs", number];
41
+ build: (spaceId: string, id: string) => readonly ["spaces", string, "builds", string];
42
+ buildLogs: (spaceId: string, id: string, limit?: number) => readonly ["spaces", string, "builds", string, "logs"] | readonly ["spaces", string, "builds", string, "logs", number];
44
43
  spaceBuilds: (id: string) => readonly ["spaces", string, "builds"];
45
44
  spaceRepositoryConnection: (id: string, connectionType?: string) => readonly ["spaces", string, "repository-connection", string];
46
- repositoryInstallations: (spaceId: string) => readonly ["repository-connections", "installations", string];
45
+ repositoryInstallations: (spaceId: string) => readonly ["spaces", string, "repository-installations"];
47
46
  repositoryInstallationRepositories: (installationId: string) => readonly ["repository-connections", "installations", string, "repositories"];
48
47
  spaceRepositoryAnalyze: (id: string, connectionType?: string, ref?: string, root?: string) => readonly ["spaces", string, "repository-connection", string, "analyze", string, string];
49
48
  spaceRepositoryWebhookDeliveries: (id: string, connectionType?: string) => readonly ["spaces", string, "repository-webhook-deliveries", string];
@@ -51,7 +50,7 @@ export declare const qk: {
51
50
  webhook: (webhookId: string) => readonly ["webhooks", string];
52
51
  webhookDeliveries: (webhookId: string) => readonly ["webhooks", string, "deliveries"];
53
52
  spaceVersion: (id: string, versionId: string) => readonly ["spaces", string, "versions", string];
54
- spaceVersionAnnotations: (id: string, versionId: string, status?: string) => readonly ["spaces", string, "versions", string, "annotations"] | readonly ["spaces", string, "versions", string, "annotations", string];
53
+ spaceVersionComments: (id: string, versionId: string, status?: string) => readonly ["spaces", string, "versions", string, "comments"] | readonly ["spaces", string, "versions", string, "comments", string];
55
54
  spaceVersionFiles: (id: string, versionId: string) => readonly ["spaces", string, "versions", string, "files"];
56
55
  spaceVersionFile: (id: string, versionId: string, path: string) => readonly ["spaces", string, "versions", string, "files", string];
57
56
  spaceVersionFileDirectUrl: (id: string, versionId: string, path: string) => readonly ["spaces", string, "versions", string, "files", string, "direct-url"];
@@ -26,7 +26,6 @@ export const qk = {
26
26
  teamSpaces: (id) => ["teams", id, "spaces"],
27
27
  teamDomains: (id) => ["teams", id, "domains"],
28
28
  teamManagedDomains: (id) => ["teams", id, "managed-domains"],
29
- domainBindings: () => ["domain-bindings"],
30
29
  slugAvailability: (kind, slug, teamId, currentSlug) => ["slugs", "availability", kind, slug, teamId ?? "", currentSlug ?? ""],
31
30
  teamVariables: (id) => ["teams", id, "variables"],
32
31
  teamActivity: (id, limit) => ["teams", id, "activity", limit],
@@ -38,8 +37,8 @@ export const qk = {
38
37
  // `invalidateQueries({ queryKey: qk.spaceVersions(id) })` still reaches it.
39
38
  spaceVersionsHistory: (id) => [...qk.spaceVersions(id), "history"],
40
39
  spaceCollaboration: (id) => ["spaces", id, "collaboration"],
41
- spaceCollaborationEmbedConfig: (id) => ["spaces", id, "collaboration", "embed-config"],
42
- spaceAnnotations: (id) => ["spaces", id, "annotations"],
40
+ spaceComments: (id) => ["spaces", id, "comments"],
41
+ spaceCommentThread: (id, threadId) => [...qk.spaceComments(id), threadId],
43
42
  spaceActivity: (id, limit) => ["spaces", id, "activity", limit],
44
43
  spaceDeployments: (id) => ["spaces", id, "deployments"],
45
44
  spaceTags: (id) => ["spaces", id, "tags"],
@@ -49,15 +48,15 @@ export const qk = {
49
48
  // The merged (system+team+space) read-only view; never a write base — see
50
49
  // `fetchSpaceReleasedGraph` in apps/my/src/lib/space-queries.ts.
51
50
  spaceTagsEffective: (id) => ["spaces", id, "tags", "effective"],
52
- build: (id) => ["builds", id],
51
+ build: (spaceId, id) => [...qk.spaceBuilds(spaceId), id],
53
52
  // Observers with different limits must not share a cache entry; calling with
54
53
  // no limit yields the bare prefix for invalidating every limit at once.
55
- buildLogs: (id, limit) => limit === undefined
56
- ? ["builds", id, "logs"]
57
- : ["builds", id, "logs", limit],
54
+ buildLogs: (spaceId, id, limit) => limit === undefined
55
+ ? [...qk.build(spaceId, id), "logs"]
56
+ : [...qk.build(spaceId, id), "logs", limit],
58
57
  spaceBuilds: (id) => ["spaces", id, "builds"],
59
58
  spaceRepositoryConnection: (id, connectionType = "connected") => ["spaces", id, "repository-connection", connectionType],
60
- repositoryInstallations: (spaceId) => ["repository-connections", "installations", spaceId],
59
+ repositoryInstallations: (spaceId) => [...qk.space(spaceId), "repository-installations"],
61
60
  repositoryInstallationRepositories: (installationId) => ["repository-connections", "installations", installationId, "repositories"],
62
61
  spaceRepositoryAnalyze: (id, connectionType = "connected", ref = "", root = "") => ["spaces", id, "repository-connection", connectionType, "analyze", ref, root],
63
62
  spaceRepositoryWebhookDeliveries: (id, connectionType = "connected") => ["spaces", id, "repository-webhook-deliveries", connectionType],
@@ -66,10 +65,10 @@ export const qk = {
66
65
  webhookDeliveries: (webhookId) => ["webhooks", webhookId, "deliveries"],
67
66
  spaceVersion: (id, versionId) => ["spaces", id, "versions", versionId],
68
67
  // Calling with no status yields the bare prefix, for invalidating every
69
- // status-scoped annotations list at once.
70
- spaceVersionAnnotations: (id, versionId, status) => status === undefined
71
- ? ["spaces", id, "versions", versionId, "annotations"]
72
- : ["spaces", id, "versions", versionId, "annotations", status],
68
+ // status-scoped version comment list at once.
69
+ spaceVersionComments: (id, versionId, status) => status === undefined
70
+ ? ["spaces", id, "versions", versionId, "comments"]
71
+ : ["spaces", id, "versions", versionId, "comments", status],
73
72
  spaceVersionFiles: (id, versionId) => ["spaces", id, "versions", versionId, "files"],
74
73
  spaceVersionFile: (id, versionId, path) => ["spaces", id, "versions", versionId, "files", path],
75
74
  spaceVersionFileDirectUrl: (id, versionId, path) => ["spaces", id, "versions", versionId, "files", path, "direct-url"],
@@ -0,0 +1,9 @@
1
+ {
2
+ "minimal.jsonc": "{\n // The canonical starter is intentionally tiny.\n \"$schema\": \"https://spacefast.com/schemas/sf.v1.json\",\n \"version\": 1,\n}\n",
3
+ "full.jsonc": "{\n \"version\": 1,\n \"build\": {\n \"installRoot\": \"packages/site\",\n \"installCommand\": \"bun install --frozen-lockfile\",\n \"command\": \"bun run build\",\n \"output\": \"dist\",\n \"timeoutSeconds\": 600\n },\n \"serve\": {\n \"index\": \"index.html\",\n \"spaFallback\": \"index.html\",\n \"cleanUrls\": false,\n \"directoryListing\": false\n },\n \"metadata\": {\n \"title\": \"Docs\",\n \"description\": \"The useful bits.\",\n \"image\": \"/og.png\"\n },\n \"substitute\": { \"files\": [\"assets/runtime.js\"] }\n}\n",
4
+ "invalid-values.jsonc": "{\n \"version\": 1,\n \"build\": {\n \"installRoot\": \"/outside\",\n \"timeoutSeconds\": 30\n },\n \"serve\": { \"cleanUrls\": null },\n \"substitute\": { \"files\": \"index.html\" }\n}\n",
5
+ "removed-identity.jsonc": "{\n \"version\": 1,\n \"space\": \"docs\",\n \"placement\": \"dedicated\",\n \"inject\": { \"head\": \"nope\" }\n}\n",
6
+ "retired-template.jsonc": "{\n \"version\": 1,\n \"substitute\": { \"files\": [\"runtime.js\"] }\n}\n",
7
+ "syntax-error.jsonc": "{\n \"version\": 1,\n \"serve\": { \"index\": \"index.html\" }\n",
8
+ "unknown-nested.jsonc": "{\n \"version\": 1,\n \"serve\": {\n \"directoryListings\": true\n }\n}\n"
9
+ }
@@ -127,7 +127,7 @@ class JsoncReader {
127
127
  }
128
128
  this.offset += 4;
129
129
  }
130
- this.fail("Unterminated string", start);
130
+ return this.fail("Unterminated string", start);
131
131
  }
132
132
  trivia() {
133
133
  while (this.offset < this.source.length) {
@@ -286,16 +286,20 @@ export function compileSfConfigV1(source, options = {}) {
286
286
  config,
287
287
  effective: {
288
288
  version: 1,
289
- ...(config.build ? {
290
- build: {
291
- ...config.build,
292
- installRoot: config.build.installRoot ?? ".",
293
- timeoutSeconds: config.build.timeoutSeconds ?? 900,
294
- },
295
- } : {}),
289
+ ...(config.build
290
+ ? {
291
+ build: {
292
+ ...config.build,
293
+ installRoot: config.build.installRoot ?? ".",
294
+ timeoutSeconds: config.build.timeoutSeconds ?? 900,
295
+ },
296
+ }
297
+ : {}),
296
298
  serve: {
297
299
  index,
298
- ...(config.serve?.spaFallback !== undefined ? { spaFallback: config.serve.spaFallback } : {}),
300
+ ...(config.serve?.spaFallback !== undefined
301
+ ? { spaFallback: config.serve.spaFallback }
302
+ : {}),
299
303
  cleanUrls: config.serve?.cleanUrls ?? true,
300
304
  directoryListing: config.serve?.directoryListing ?? index === false,
301
305
  },
@@ -311,14 +315,16 @@ function invalidResult(message, line, column) {
311
315
  success: false,
312
316
  config: null,
313
317
  effective: null,
314
- issues: [{
318
+ issues: [
319
+ {
315
320
  code: "config_invalid",
316
321
  severity: "error",
317
322
  path: "$",
318
323
  line,
319
324
  column,
320
325
  message,
321
- }],
326
+ },
327
+ ],
322
328
  locations: new Map([["$", { line, column }]]),
323
329
  };
324
330
  }
@@ -374,9 +380,8 @@ function validateShape(value, issue) {
374
380
  }
375
381
  if (isObject(value.build)) {
376
382
  const timeout = value.build.timeoutSeconds;
377
- if (typeof timeout === "number" && (!Number.isInteger(timeout)
378
- || timeout < 60
379
- || timeout > 7200)) {
383
+ if (typeof timeout === "number" &&
384
+ (!Number.isInteger(timeout) || timeout < 60 || timeout > 7200)) {
380
385
  issue("config_invalid", "$.build.timeoutSeconds", "build.timeoutSeconds must be an integer from 60 to 7200.");
381
386
  }
382
387
  const installRoot = value.build.installRoot;
@@ -470,9 +475,9 @@ function editDistance(left, right) {
470
475
  return previous[right.length] ?? right.length;
471
476
  }
472
477
  function isCommittedPath(candidate) {
473
- return candidate.length > 0
474
- && !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(candidate)
475
- && !isAbsoluteOrEscaping(candidate);
478
+ return (candidate.length > 0 &&
479
+ !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(candidate) &&
480
+ !isAbsoluteOrEscaping(candidate));
476
481
  }
477
482
  function isCommittedImagePath(candidate) {
478
483
  return /^\/(?!\/)/.test(candidate) && !candidate.split(/[\\/]/).includes("..");
@@ -505,14 +510,18 @@ function validateArtifactReferences(value, artifactPaths, issue) {
505
510
  if (isObject(value.serve)) {
506
511
  for (const key of ["index", "spaFallback"]) {
507
512
  const candidate = value.serve[key];
508
- if (typeof candidate === "string" && isCommittedPath(candidate) && !artifacts.has(candidate)) {
513
+ if (typeof candidate === "string" &&
514
+ isCommittedPath(candidate) &&
515
+ !artifacts.has(candidate)) {
509
516
  issue("config_invalid", `$.serve.${key}`, `serve.${key} must reference a committed artifact.`);
510
517
  }
511
518
  }
512
519
  }
513
520
  if (isObject(value.substitute) && Array.isArray(value.substitute.files)) {
514
521
  value.substitute.files.forEach((candidate, index) => {
515
- if (typeof candidate === "string" && isCommittedPath(candidate) && !artifacts.has(candidate)) {
522
+ if (typeof candidate === "string" &&
523
+ isCommittedPath(candidate) &&
524
+ !artifacts.has(candidate)) {
516
525
  issue("config_invalid", `$.substitute.files[${index}]`, "substitute.files entries must reference committed artifacts.");
517
526
  }
518
527
  });
@@ -0,0 +1,2 @@
1
+ export declare const SOURCE_ARCHIVE_MAX_EXPANSION_RATIO = 200;
2
+ export declare const SOURCE_ARCHIVE_EXPANSION_RATIO_FLOOR_BYTES: number;
@@ -0,0 +1,2 @@
1
+ export const SOURCE_ARCHIVE_MAX_EXPANSION_RATIO = 200;
2
+ export const SOURCE_ARCHIVE_EXPANSION_RATIO_FLOOR_BYTES = 8 * 1024 * 1024;
@@ -0,0 +1,57 @@
1
+ {
2
+ "phpLike": [
3
+ { "path": "index.php", "expected": true },
4
+ { "path": "nested/app.phtml", "expected": true },
5
+ { "path": "lib.phar", "expected": true },
6
+ { "path": "legacy.php5", "expected": true },
7
+ { "path": "index.html", "expected": false },
8
+ { "path": "assets/main.js", "expected": false },
9
+ { "path": ".env", "expected": false }
10
+ ],
11
+ "immutable": [
12
+ { "path": "_next/static/chunks/main-app-4A_iFExJ.js", "expected": true },
13
+ { "path": "_next/static/chunks/framework-2c1d4f8a.js", "expected": true },
14
+ { "path": "_next/static/chunks/pages/index-a1b2c3d4e5f6.js", "expected": true },
15
+ { "path": "_next/static/chunks/oton0Guf.js", "expected": true },
16
+ { "path": "_next/static/css/app.plainname.css", "expected": true },
17
+ { "path": "/_next/static/chunks/oton0Guf.js", "expected": true },
18
+ { "path": "_app/immutable/entry/start.DYyuCeEr.js", "expected": true },
19
+ { "path": "_nuxt/index.oton0Guf.js", "expected": true },
20
+ { "path": "_astro/about.DYyuCeEr.css", "expected": true },
21
+ { "path": "_next/static/index.html", "expected": false },
22
+ { "path": "_next/static/sitemap.xml", "expected": false },
23
+ { "path": "_app/immutable/manifest.txt", "expected": false },
24
+ { "path": "_nextish/chunks/oton0Guf.js", "expected": false },
25
+ { "path": "nested/_next/static/chunks/oton0Guf.js", "expected": false },
26
+ { "path": "assets/index-BzeDGMM3.js", "expected": false },
27
+ { "path": "assets/auth-client-P1Tx-Cyu.js", "expected": false },
28
+ { "path": "assets/index-DYyuCeEr.js", "expected": false },
29
+ { "path": "assets/index-9f8e7d6c.js", "expected": false },
30
+ { "path": "assets/D3GeoMap.js", "expected": false },
31
+ { "path": "assets/index.js", "expected": false },
32
+ { "path": "assets/client-widget.js", "expected": false },
33
+ { "path": "app.1a2b3c4d.css", "expected": false },
34
+ { "path": "vendor.3f9a2b7e.js", "expected": false },
35
+ { "path": "static/js/main.d41d8cd98f00b204e9800998.js", "expected": false },
36
+ { "path": "index.html", "expected": false },
37
+ { "path": "robots.txt", "expected": false },
38
+ { "path": "config.xml", "expected": false },
39
+ { "path": "page.php", "expected": false },
40
+ { "path": "MyComponent.js", "expected": false },
41
+ { "path": "image01.png", "expected": false }
42
+ ],
43
+ "safeContentType": [
44
+ {
45
+ "path": "index.php",
46
+ "mime": "application/x-httpd-php",
47
+ "expected": "text/plain; charset=utf-8"
48
+ },
49
+ { "path": "lib.phar", "mime": "application/zip", "expected": "application/octet-stream" },
50
+ {
51
+ "path": "assets/main.js",
52
+ "mime": "text/javascript; charset=utf-8",
53
+ "expected": "text/javascript; charset=utf-8"
54
+ },
55
+ { "path": "logo.svg", "mime": "image/svg+xml", "expected": "image/svg+xml" }
56
+ ]
57
+ }
@@ -11,7 +11,7 @@ export const PHP_LIKE_EXTENSIONS = ["php", "php3", "php4", "php5", "php7", "php8
11
11
  // Headers"): user `_headers` rules can never set or remove these. The list includes the
12
12
  // internal-redirect/sendfile family (X-Accel-Redirect, X-Sendfile, X-LIGHTTPD-send-file,
13
13
  // X-Accel-Buffering) because under nginx+php-fpm those headers turn a response into a
14
- // server-side file read — a private-path disclosure primitive. The routing compiler
14
+ // server-side file read — a private-path disclosure primitive. The native Rust compiler
15
15
  // rejects them at publish; the PHP runtime refuses them again at apply time so a compiler
16
16
  // gap can never emit one.
17
17
  export const PLATFORM_MANAGED_RESPONSE_HEADERS = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spacefast/common",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "license": "MIT",
5
5
  "description": "Shared TypeScript contracts and utilities for Spacefast packages.",
6
6
  "repository": {
@@ -91,8 +91,9 @@
91
91
  }
92
92
  },
93
93
  "dependencies": {
94
- "@tanstack/react-query": "^5.101.1",
95
- "smol-toml": "^1.6.1",
96
- "zod": "^4.3.6"
94
+ "@tanstack/react-query": "^5.101.4",
95
+ "culori": "^4.0.2",
96
+ "smol-toml": "^1.7.0",
97
+ "zod": "^4.4.3"
97
98
  }
98
99
  }