@spacefast/common 0.0.13 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/agents/client-registry.d.ts +22 -0
  2. package/dist/agents/client-registry.js +41 -0
  3. package/dist/agents/connect-targets.d.ts +51 -6
  4. package/dist/agents/connect-targets.js +93 -28
  5. package/dist/brand.d.ts +0 -11
  6. package/dist/brand.js +0 -20
  7. package/dist/config/domains.d.ts +15 -6
  8. package/dist/config/domains.js +24 -10
  9. package/dist/config/postgres-budget.d.ts +3 -0
  10. package/dist/config/postgres-budget.js +6 -1
  11. package/dist/config/postgres-budget.json +2 -1
  12. package/dist/contracts/abuse.js +1 -1
  13. package/dist/contracts/access.d.ts +149 -44
  14. package/dist/contracts/access.js +94 -8
  15. package/dist/contracts/activity.js +5 -1
  16. package/dist/contracts/api-keys.js +9 -9
  17. package/dist/contracts/archives.js +11 -11
  18. package/dist/contracts/beta.js +1 -1
  19. package/dist/contracts/builds.js +14 -14
  20. package/dist/contracts/channels.js +2 -2
  21. package/dist/contracts/comments.d.ts +86 -15
  22. package/dist/contracts/comments.js +116 -20
  23. package/dist/contracts/common.d.ts +12 -12
  24. package/dist/contracts/common.js +33 -30
  25. package/dist/contracts/continuation.js +5 -5
  26. package/dist/contracts/data-source-routes.js +1 -1
  27. package/dist/contracts/device-auth.d.ts +6 -6
  28. package/dist/contracts/device-auth.js +12 -12
  29. package/dist/contracts/docs.d.ts +10 -10
  30. package/dist/contracts/domains.js +39 -33
  31. package/dist/contracts/enums.d.ts +2 -10
  32. package/dist/contracts/enums.js +0 -2
  33. package/dist/contracts/error-code-meta.d.ts +116 -47
  34. package/dist/contracts/error-code-meta.js +37 -16
  35. package/dist/contracts/error-codes.d.ts +7 -4
  36. package/dist/contracts/error-codes.js +46 -21
  37. package/dist/contracts/events.d.ts +10 -3
  38. package/dist/contracts/events.js +33 -5
  39. package/dist/contracts/execution.d.ts +26 -0
  40. package/dist/contracts/execution.js +21 -0
  41. package/dist/contracts/feature-lifecycle.d.ts +21 -24
  42. package/dist/contracts/feature-lifecycle.js +28 -26
  43. package/dist/contracts/features.js +4 -4
  44. package/dist/contracts/functions.d.ts +8 -140
  45. package/dist/contracts/functions.js +9 -19
  46. package/dist/contracts/git.d.ts +1 -1
  47. package/dist/contracts/grants.d.ts +96 -79
  48. package/dist/contracts/grants.js +130 -43
  49. package/dist/contracts/ids.d.ts +2 -2
  50. package/dist/contracts/ids.js +2 -4
  51. package/dist/contracts/internal.js +2 -2
  52. package/dist/contracts/mcp.d.ts +17 -556
  53. package/dist/contracts/mcp.js +9 -103
  54. package/dist/contracts/me.js +3 -4
  55. package/dist/contracts/notifications.d.ts +108 -0
  56. package/dist/contracts/notifications.js +54 -0
  57. package/dist/contracts/oauth-resources.d.ts +18 -1
  58. package/dist/contracts/oauth-resources.js +22 -2
  59. package/dist/contracts/operations.d.ts +2 -50
  60. package/dist/contracts/operations.js +0 -19
  61. package/dist/contracts/pages.d.ts +8 -4
  62. package/dist/contracts/pages.js +16 -3
  63. package/dist/contracts/platform.js +10 -8
  64. package/dist/contracts/problem-document.d.ts +14 -0
  65. package/dist/contracts/problem-document.js +35 -0
  66. package/dist/contracts/push-new.d.ts +21 -6
  67. package/dist/contracts/push-new.js +3 -7
  68. package/dist/contracts/resources.d.ts +8 -7
  69. package/dist/contracts/resources.js +9 -6
  70. package/dist/contracts/runtime-api.d.ts +162 -92
  71. package/dist/contracts/runtime-api.js +99 -27
  72. package/dist/contracts/runtime-app.d.ts +41 -11
  73. package/dist/contracts/runtime-app.js +22 -1
  74. package/dist/contracts/runtime-db.d.ts +424 -0
  75. package/dist/contracts/runtime-db.js +108 -0
  76. package/dist/contracts/runtime-storage.d.ts +120 -0
  77. package/dist/contracts/runtime-storage.js +40 -0
  78. package/dist/contracts/sf-config-v1.d.ts +49 -1
  79. package/dist/contracts/sf-config-v1.js +2 -2
  80. package/dist/contracts/sites.d.ts +1 -0
  81. package/dist/contracts/sites.js +20 -12
  82. package/dist/contracts/space-config.d.ts +62 -22
  83. package/dist/contracts/space-config.js +173 -47
  84. package/dist/contracts/spaces.d.ts +79 -45
  85. package/dist/contracts/spaces.js +66 -50
  86. package/dist/contracts/superadmin-spaces.d.ts +13 -2
  87. package/dist/contracts/superadmin-spaces.js +15 -0
  88. package/dist/contracts/superadmin-tenants.js +4 -4
  89. package/dist/contracts/superadmin.d.ts +4 -0
  90. package/dist/contracts/superadmin.js +14 -1
  91. package/dist/contracts/tags.d.ts +17 -17
  92. package/dist/contracts/tags.js +1 -1
  93. package/dist/contracts/teams.js +25 -16
  94. package/dist/contracts/theme-json.js +1 -1
  95. package/dist/contracts/transfers.js +4 -4
  96. package/dist/contracts/variables.js +3 -3
  97. package/dist/contracts/zero.d.ts +204 -338
  98. package/dist/contracts/zero.js +94 -98
  99. package/dist/dashboard-paths/index.d.ts +16 -1
  100. package/dist/dashboard-paths/index.js +18 -0
  101. package/dist/docs/agent-handoff-document.d.ts +2 -2
  102. package/dist/docs/agent-handoff-document.js +8 -7
  103. package/dist/docs/agent-prose.d.ts +22 -3
  104. package/dist/docs/agent-prose.js +221 -51
  105. package/dist/docs/agent-setup.d.ts +13 -10
  106. package/dist/docs/agent-setup.js +279 -53
  107. package/dist/docs/agent-solutions.d.ts +6 -6
  108. package/dist/docs/agent-solutions.js +23 -21
  109. package/dist/docs/catalog.d.ts +29 -74
  110. package/dist/docs/catalog.js +65 -92
  111. package/dist/docs/error-docs.js +293 -221
  112. package/dist/docs/skill-distribution.d.ts +2 -0
  113. package/dist/docs/skill-distribution.js +7 -4
  114. package/dist/docs/skills.d.ts +546 -0
  115. package/dist/docs/skills.js +340 -0
  116. package/dist/utils/browser-credential.d.ts +19 -0
  117. package/dist/utils/browser-credential.js +62 -0
  118. package/dist/utils/comment-avatars.d.ts +7 -0
  119. package/dist/utils/comment-avatars.js +42 -0
  120. package/dist/utils/content-type.d.ts +3 -3
  121. package/dist/utils/content-type.js +38 -61
  122. package/dist/utils/domain-diagnostics.d.ts +5 -5
  123. package/dist/utils/domain-diagnostics.js +21 -10
  124. package/dist/utils/local-space-state.d.ts +78 -4
  125. package/dist/utils/local-space-state.js +383 -82
  126. package/dist/utils/one-shot-replay.d.ts +14 -0
  127. package/dist/utils/one-shot-replay.js +21 -0
  128. package/dist/utils/page-preview.d.ts +7 -0
  129. package/dist/utils/page-preview.js +9 -0
  130. package/dist/utils/pages.d.ts +2 -11
  131. package/dist/utils/pages.js +58 -14
  132. package/dist/utils/publish-form-data.d.ts +22 -0
  133. package/dist/utils/publish-form-data.js +26 -0
  134. package/dist/utils/publish-policy.js +20 -4
  135. package/dist/utils/query-keys.d.ts +7 -4
  136. package/dist/utils/query-keys.js +13 -4
  137. package/dist/utils/runtime-paths.d.ts +3 -0
  138. package/dist/utils/runtime-paths.js +7 -0
  139. package/dist/utils/secure-local-file.d.ts +73 -0
  140. package/dist/utils/secure-local-file.js +1373 -0
  141. package/dist/utils/space-config.d.ts +13 -4
  142. package/dist/utils/space-config.js +123 -15
  143. package/dist/utils/storage-policy.d.ts +20 -0
  144. package/dist/utils/storage-policy.fixtures.json +174 -0
  145. package/dist/utils/storage-policy.js +193 -0
  146. package/dist/utils/upload-session.d.ts +12 -1
  147. package/dist/utils/upload-session.js +37 -2
  148. package/dist/utils/version-file-browser.d.ts +13 -0
  149. package/dist/utils/version-file-browser.js +59 -0
  150. package/package.json +19 -1
  151. package/dist/contracts/mounts.d.ts +0 -142
  152. package/dist/contracts/mounts.js +0 -56
  153. package/dist/utils/asset-fingerprint.d.ts +0 -64
  154. package/dist/utils/asset-fingerprint.js +0 -620
@@ -1,20 +1,10 @@
1
+ import { defaultTreeAdapter, parse } from "parse5";
1
2
  import { SPACEFAST_WORDMARK_PATH } from "../brand-assets.js";
2
3
  import { CUSTOM_PAGE_IDS, PAGE_CATALOG, PAGE_ELEMENTS, PAGE_LAYOUT_FILE, PAGE_SOURCE_MAX_BYTES, SITE_PAGE_IDS, } from "../contracts/pages.js";
3
4
  import { SPACE_CONFIG_ACCEPTED_FILES } from "../contracts/space-config.js";
4
5
  import { derivePageColors, PAGE_COLOR_DEFAULTS } from "./page-colors.js";
5
6
  import { PAGE_EMBEDDED_FONT_FACES, PAGE_FONT_FACES } from "./page-fonts.js";
6
- /**
7
- * One URL, one representation. A file's bare path always serves its bytes;
8
- * the preview page is reachable only under this query flag. The alternative —
9
- * negotiating on `Accept` at the bare path — depends on `Vary`, which the edge
10
- * ignores, so whichever client arrived first pinned that representation for
11
- * everyone (an `<img>` could poison the human's link for a year).
12
- */
13
- export const PAGE_PREVIEW_QUERY = "spacefast_view";
14
- /** The preview page for a file whose bytes live at `fileHref`. */
15
- export function previewPageHref(fileHref) {
16
- return `${fileHref}${fileHref.includes("?") ? "&" : "?"}${PAGE_PREVIEW_QUERY}=1`;
17
- }
7
+ export { PAGE_PREVIEW_QUERY, previewPageHref } from "./page-preview.js";
18
8
  const encoder = new TextEncoder();
19
9
  const knownElements = new Set(PAGE_ELEMENTS);
20
10
  const customPageIds = new Set(CUSTOM_PAGE_IDS);
@@ -102,6 +92,10 @@ function renderElement(element, context) {
102
92
  return context.content ?? "";
103
93
  case "sf-denial-context":
104
94
  return runtimeSlot("denial", '<p class="sf-copy">This page is private.</p>');
95
+ case "sf-access-status":
96
+ return runtimeSlot("access-status", "");
97
+ case "sf-access-lanes":
98
+ return runtimeSlot("access-lanes", '<p class="sf-copy">This page is private.</p>');
105
99
  case "sf-page-path":
106
100
  return context.displayPath
107
101
  ? escapePageHtml(context.displayPath)
@@ -150,6 +144,35 @@ function foundElements(source) {
150
144
  result.add((match[1] ?? "").toLowerCase());
151
145
  return [...result];
152
146
  }
147
+ function accessCspBlocksForms(source) {
148
+ const documentNodes = [...parse(source).childNodes];
149
+ const head = documentNodes
150
+ .flatMap((node) => defaultTreeAdapter.isElementNode(node) && node.tagName === "html" ? node.childNodes : [])
151
+ .find((node) => defaultTreeAdapter.isElementNode(node) && node.tagName === "head");
152
+ if (!head || !defaultTreeAdapter.isElementNode(head))
153
+ return false;
154
+ const pending = [...head.childNodes];
155
+ while (pending.length > 0) {
156
+ const node = pending.pop();
157
+ if (!node || !defaultTreeAdapter.isElementNode(node))
158
+ continue;
159
+ if (node.tagName === "meta") {
160
+ const attributes = new Map(node.attrs.map(({ name, value }) => [name, value]));
161
+ if (attributes.get("http-equiv")?.trim().toLowerCase() === "content-security-policy") {
162
+ const policy = (attributes.get("content") ?? "")
163
+ .split(";")
164
+ .map((directive) => directive.trim().split(/\s+/))
165
+ .filter((directive) => directive[0]);
166
+ const formAction = policy.find((directive) => directive[0]?.toLowerCase() === "form-action");
167
+ if (formAction &&
168
+ !formAction.slice(1).some((expression) => expression.toLowerCase() === "'self'"))
169
+ return true;
170
+ }
171
+ }
172
+ pending.push(...node.childNodes);
173
+ }
174
+ return false;
175
+ }
153
176
  export function validatePageSources(input) {
154
177
  const diagnostics = [];
155
178
  for (const [sourcePath, source] of Object.entries(input.pages)) {
@@ -193,6 +216,7 @@ export function validatePageSources(input) {
193
216
  element === "sf-spacefast-branding" ||
194
217
  (id === "404" && element === "sf-page-path") ||
195
218
  (id === "denied" && element === "sf-denial-context") ||
219
+ (id === "access" && (element === "sf-access-status" || element === "sf-access-lanes")) ||
196
220
  (id === "index" &&
197
221
  (element === "sf-files" ||
198
222
  element === "sf-page-path" ||
@@ -215,7 +239,23 @@ export function validatePageSources(input) {
215
239
  message: "The index page does not render <sf-files>.",
216
240
  path,
217
241
  });
218
- if (id === "denied" &&
242
+ // Without the lane slot the runtime has nowhere to inject the working
243
+ // sign-in/password/request blocks and the template ships a dead gate.
244
+ if (id === "access" && !/<\s*sf-access-lanes\b/i.test(source))
245
+ diagnostics.push({
246
+ severity: "error",
247
+ code: "access_lanes_missing",
248
+ message: "The access page must render <sf-access-lanes> so visitors keep a way in.",
249
+ path,
250
+ });
251
+ if (id === "access" && accessCspBlocksForms(source))
252
+ diagnostics.push({
253
+ severity: "error",
254
+ code: "access_csp_form_action_missing_self",
255
+ message: "The access page CSP must allow its runtime forms: use form-action 'self' when declaring form-action.",
256
+ path,
257
+ });
258
+ if ((id === "denied" || id === "access") &&
219
259
  /<\s*meta\b[^>]*\bname\s*=\s*["']robots["'][^>]*\bcontent\s*=\s*["'][^"']*\bindex\b/i.test(source))
220
260
  diagnostics.push({
221
261
  severity: "warning",
@@ -322,15 +362,19 @@ export function defaultPageBody(id) {
322
362
  // themes, so buttons and links remain readable for every accepted accent.
323
363
  const defaultPageColors = derivePageColors({});
324
364
  const sharedCss = `/*sf-default-fonts*/:root{color-scheme:light dark;--sf-accent:${PAGE_COLOR_DEFAULTS.accent};--sf-bg:${PAGE_COLOR_DEFAULTS.bg};--sf-fg:${PAGE_COLOR_DEFAULTS.fg};--sf-muted:${PAGE_COLOR_DEFAULTS.muted};--sf-card:${PAGE_COLOR_DEFAULTS.card};--sf-border:${PAGE_COLOR_DEFAULTS.border};--sf-link:${defaultPageColors.link};--sf-on-accent:${defaultPageColors.onAccent};--sf-error:#a82d1a;--sf-font:"Haskoy",-apple-system,BlinkMacSystemFont,ui-sans-serif,"Segoe UI",sans-serif;--sf-display-font:"Merriweather",Georgia,serif;--sf-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace}*{box-sizing:border-box}html{min-width:320px;background:var(--sf-bg);-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh;background:var(--sf-bg);color:var(--sf-fg);font:14px/1.65 var(--sf-font)}a{color:inherit}.sf-page{min-height:100vh;display:flex;flex-direction:column;padding:clamp(22px,4vw,44px)}.sf-header{display:flex;align-items:center;min-height:32px}.sf-page-center .sf-header{justify-content:center}.sf-brand{display:inline-flex;min-width:0;align-items:center;gap:10px;color:var(--sf-fg);text-decoration:none}.sf-brand-mark{width:7px;height:7px;flex:none;background:var(--sf-accent)}.sf-logo{width:28px;height:28px;flex:none;object-fit:contain}.sf-brand strong{min-width:0;overflow-wrap:anywhere;font-size:13px;font-weight:600}.sf-main{flex:1;width:100%;padding:clamp(34px,9vh,84px) 0}.sf-page-plain .sf-main{max-width:680px}.sf-page-center .sf-main{max-width:620px;margin:auto;display:flex;flex-direction:column;align-items:center;text-align:center}.sf-eyebrow{margin:0 0 14px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.12em;text-transform:uppercase;overflow-wrap:anywhere}.sf-page-center .sf-eyebrow{margin-bottom:16px}h1{max-width:20ch;margin:0 0 12px;color:var(--sf-fg);font:430 clamp(1.9rem,5vw,2.8rem)/1.22 var(--sf-display-font);letter-spacing:-.012em;text-wrap:balance}.sf-copy,.sf-sub{max-width:52ch;margin:0;color:var(--sf-muted);text-wrap:pretty}.sf-inline-path{font-family:var(--sf-mono);font-size:.9em;color:var(--sf-fg);overflow-wrap:anywhere}.sf-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:22px}.sf-page-center .sf-actions{justify-content:center}.sf-button{display:inline-flex;min-height:44px;align-items:center;justify-content:center;border:0;background:var(--sf-accent);color:var(--sf-on-accent);padding:10px 16px;font:600 13px/1.4 var(--sf-font);text-decoration:none;cursor:pointer}.sf-ghost{color:var(--sf-muted);text-underline-offset:3px}.sf-button:hover{filter:brightness(1.06)}a:focus-visible,button:focus-visible{outline:2px solid var(--sf-link);outline-offset:2px}.sf-copy+.sf-copy{margin-top:12px}.sf-files{width:100%;margin:24px 0 0;padding:0;list-style:none}.sf-files li{display:flex;min-width:0;align-items:baseline;gap:18px;border-bottom:1px solid var(--sf-border)}.sf-files a{min-width:0;flex:1;padding:10px 2px;color:var(--sf-fg);font-weight:500;text-decoration:none}.sf-files a:hover{color:var(--sf-link)}.sf-file-name{overflow-wrap:anywhere}.sf-file-size{flex:none;padding:10px 2px;color:var(--sf-muted);font:12px/1.5 var(--sf-mono);white-space:nowrap}.sf-empty{max-width:420px;margin:24px 0 0;border:1px dashed var(--sf-border);padding:18px 20px;color:var(--sf-muted)}.sf-preview-wrap{width:min(100%,560px);aspect-ratio:16/9;margin:10px 0 18px;overflow:hidden;background:#fff;outline:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);outline-offset:-1px}.sf-preview{display:block;width:100%;height:100%;border:0;object-fit:contain}.sf-file-meta{width:min(100%,560px);margin:0}.sf-file-meta div{display:flex;min-width:0;justify-content:space-between;gap:20px;border-bottom:1px solid var(--sf-border);padding:9px 2px}.sf-file-meta dt{color:var(--sf-fg);font-weight:600}.sf-file-meta dd{min-width:0;margin:0;color:var(--sf-muted);font-family:var(--sf-mono);overflow-wrap:anywhere;text-align:right}.sf-colophon{display:flex;flex-direction:column;align-items:center;gap:10px;padding:44px 0 26px;margin-top:auto;color:var(--sf-muted)}.sf-page-plain .sf-colophon{align-items:flex-start}.sf-colophon-row{display:flex;align-items:center;gap:20px;flex-wrap:wrap}.sf-powered{display:inline-flex;align-items:center;gap:10px;cursor:pointer;outline:none}.sf-powered-line{display:none;font-size:12px;color:var(--sf-muted)}.sf-powered:hover .sf-powered-line,.sf-powered:focus .sf-powered-line,.sf-powered:focus-within .sf-powered-line{display:inline}.sf-powered-line a{color:var(--sf-link);text-decoration:underline;text-underline-offset:3px}.sf-help{font-size:12px;color:var(--sf-muted);text-decoration:underline;text-underline-offset:3px}.sf-help:hover{color:var(--sf-fg)}.sf-wordmark{display:block;flex:none;width:auto;height:11px}.sf-wordmark-shadow{fill:var(--sf-accent)}.sf-wordmark-plate{fill:#141419}.sf-wordmark-paper{fill:#fbf8f1}@media(max-width:520px){.sf-page{padding:20px}.sf-button{min-height:48px}.sf-file-meta div{flex-direction:column;gap:2px}.sf-file-meta dd{text-align:left}.sf-colophon{padding-top:32px}}@media(prefers-color-scheme:dark){:root{--sf-bg:${PAGE_COLOR_DEFAULTS.darkBg};--sf-fg:${PAGE_COLOR_DEFAULTS.darkFg};--sf-muted:${PAGE_COLOR_DEFAULTS.darkMuted};--sf-card:${PAGE_COLOR_DEFAULTS.darkCard};--sf-border:${PAGE_COLOR_DEFAULTS.darkBorder};--sf-link:${defaultPageColors.linkDark};--sf-error:#ff8a7a}.sf-preview-wrap{outline-color:#ffffff1a}}@media(forced-colors:active){.sf-brand-mark{background:CanvasText}.sf-button{border:1px solid ButtonText}.sf-powered:focus{outline:1px solid CanvasText;outline-offset:3px}}`;
365
+ const accessCss = `.sf-page.access .sf-main{max-width:26rem;padding:clamp(42px,8vh,72px) 0}.sf-page.access h1{max-width:18ch;margin-bottom:14px;font:600 clamp(2rem,7vw,2.65rem)/1.12 var(--sf-font);letter-spacing:-.035em}.sf-page.access .sf-copy{font-size:15px;line-height:1.6}.sf-access-default{display:flex;width:100%;flex-direction:column;align-items:center}.sf-access-status{padding:10px 12px;border:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);border-radius:8px;background:color-mix(in srgb,var(--sf-fg) 4%,transparent);color:var(--sf-fg)}.sf-access-lanes{display:flex;flex-direction:column;gap:16px;width:min(23rem,100%);margin-top:30px;text-align:left}.sf-access-lanes form{display:flex;flex-direction:column;gap:14px;margin:0}.sf-input{box-sizing:border-box;width:100%;min-height:46px;padding:11px 12px;border:1px solid color-mix(in srgb,var(--sf-fg) 18%,transparent);border-radius:8px;outline:2px solid transparent;outline-offset:-1px;background:color-mix(in srgb,var(--sf-bg) 92%,var(--sf-fg));color:var(--sf-fg);font:500 15px/1.45 var(--sf-font);appearance:none}.sf-input::placeholder{color:color-mix(in srgb,var(--sf-muted) 82%,transparent);font:inherit}.sf-input:hover{border-color:color-mix(in srgb,var(--sf-fg) 34%,transparent)}.sf-input:focus-visible{border-color:var(--sf-accent);outline-color:var(--sf-accent)}textarea.sf-input{min-height:92px;resize:vertical}.sf-label{color:var(--sf-fg);font:600 13px/1.4 var(--sf-font)}.sf-access-lanes .sf-button{width:100%;min-height:46px;border-radius:8px}.sf-access-or{display:flex;align-items:center;gap:12px;color:var(--sf-muted);font-size:13px}.sf-access-or:before,.sf-access-or:after{content:"";flex:1;border-top:1px solid color-mix(in srgb,var(--sf-fg) 12%,transparent)}.sf-error{margin:0;color:var(--sf-error);font-size:14px}.sf-access-password-row{display:flex;gap:8px}.sf-access-password-row .sf-input{min-width:0;flex:1}.sf-access-password-row .sf-button{width:auto;flex:none}.sf-access-request{padding-top:2px}.sf-access-request summary{padding:4px 0;cursor:pointer;color:var(--sf-muted);font:550 14px/1.5 var(--sf-font)}.sf-access-request summary:hover{color:var(--sf-fg)}.sf-access-request[open] summary{color:var(--sf-fg)}.sf-access-request form{margin-top:16px}@media(max-width:520px){.sf-page.access .sf-main{padding:38px 0}.sf-page.access h1{font-size:2rem}.sf-page.access .sf-copy{font-size:16px}.sf-input{min-height:48px;font-size:16px}.sf-access-lanes .sf-button{min-height:48px}.sf-access-password-row{flex-direction:column}.sf-access-password-row .sf-button{width:100%}}@media(prefers-color-scheme:dark){.sf-access-status{background:color-mix(in srgb,var(--sf-fg) 5%,transparent)}}`;
325
366
  const pageHeader = '<header class="sf-header"><a class="sf-brand" href="/" aria-label="Homepage"><span class="sf-brand-mark" aria-hidden="true"></span><sf-logo></sf-logo><strong><sf-name></sf-name></strong></a></header>';
326
367
  const pageFooter = "<sf-spacefast-branding></sf-spacefast-branding>";
327
368
  function document(id, title, body, layout) {
328
369
  const noindex = PAGE_CATALOG[id].noindex ? '<meta name="robots" content="noindex">' : "";
329
- return `<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${noindex}<title>${title}</title><style>${sharedCss}</style></head>\n<body><div class="sf-page sf-page-${layout}">${pageHeader}<main class="sf-main">${body}</main>${pageFooter}</div></body>\n</html>\n`;
370
+ const accessClass = id === "access" ? " access" : "";
371
+ const pageCss = id === "access" ? sharedCss + accessCss : sharedCss;
372
+ return `<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${noindex}<title>${title}</title><style>${pageCss}</style></head>\n<body><div class="sf-page${accessClass} sf-page-${layout}">${pageHeader}<main class="sf-main">${body}</main>${pageFooter}</div></body>\n</html>\n`;
330
373
  }
331
374
  export const DEFAULT_PAGE_TEMPLATES = {
332
375
  "404": document("404", "Page not found", '<p class="sf-eyebrow">404 · Not Found</p><h1>Page not found</h1><p class="sf-copy">There’s nothing at <span class="sf-inline-path"><sf-page-path></sf-page-path></span>.</p><div class="sf-actions"><a class="sf-button" href="/">Go to the homepage</a></div>', "center"),
333
376
  denied: document("denied", "Access denied", "<h1>Access denied</h1><sf-denial-context></sf-denial-context>", "center"),
377
+ access: document("access", "This space is private", '<h1>This space is private</h1><div class="sf-access-default"><sf-access-status></sf-access-status><sf-access-lanes></sf-access-lanes></div>', "center"),
334
378
  index: document("index", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><p class="sf-sub"><sf-page-summary></sf-page-summary></p><sf-files></sf-files>', "plain"),
335
379
  preview: document("preview", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><sf-file></sf-file>', "plain"),
336
380
  };
@@ -0,0 +1,22 @@
1
+ export type DirectPublishUpload = {
2
+ kind: "archive";
3
+ name: string;
4
+ bytes: Uint8Array;
5
+ contentType: string;
6
+ } | {
7
+ kind: "files";
8
+ files: Array<{
9
+ path: string;
10
+ bytes: Uint8Array;
11
+ contentType?: string | undefined;
12
+ }>;
13
+ };
14
+ /** Build the canonical multipart body used by direct one-shot publish clients. */
15
+ export declare function createPublishFormData(input: {
16
+ upload: DirectPublishUpload;
17
+ clientAttribution: string;
18
+ spaceId?: string | undefined;
19
+ slug?: string | undefined;
20
+ title?: string | undefined;
21
+ description?: string | undefined;
22
+ }): FormData;
@@ -0,0 +1,26 @@
1
+ /** Build the canonical multipart body used by direct one-shot publish clients. */
2
+ export function createPublishFormData(input) {
3
+ const form = new FormData();
4
+ if (input.spaceId)
5
+ form.append("spaceId", input.spaceId);
6
+ if (input.description)
7
+ form.append("description", input.description);
8
+ if (input.slug || input.title) {
9
+ form.append("space", JSON.stringify({
10
+ ...(input.slug ? { slug: input.slug } : {}),
11
+ ...(input.title ? { title: input.title } : {}),
12
+ }));
13
+ }
14
+ form.append("clientAttribution", input.clientAttribution);
15
+ form.append("source", JSON.stringify({ kind: "agent", client: input.clientAttribution }));
16
+ if (input.upload.kind === "archive") {
17
+ form.append("archive", new File([Uint8Array.from(input.upload.bytes)], input.upload.name, {
18
+ type: input.upload.contentType,
19
+ }));
20
+ return form;
21
+ }
22
+ for (const file of input.upload.files.toSorted((left, right) => left.path.localeCompare(right.path))) {
23
+ form.append("files", new File([Uint8Array.from(file.bytes)], file.path, file.contentType ? { type: file.contentType } : undefined));
24
+ }
25
+ return form;
26
+ }
@@ -1,6 +1,7 @@
1
1
  import { BRAND } from "../brand.js";
2
2
  import { WORST_CASE_QUOTAS } from "../contracts/quotas.js";
3
3
  import { SPACE_CONFIG_PRIVATE_FILES } from "../contracts/space-config.js";
4
+ import { ZERO_RUNTIME_DEPLOY_PATH } from "../contracts/zero.js";
4
5
  // The ONE static publish policy (internal-docs/platform.md "Upload Contract"):
5
6
  // path denials, manifest scale caps, plan caps, duplicate detection, and the
6
7
  // free-plan content rules live here. Consumed by API manifest validation, CLI
@@ -95,8 +96,22 @@ export function normalizePublishPath(relativePath) {
95
96
  * could mint its own tokens.
96
97
  */
97
98
  const PUBLISHABLE_BUILD_ARTIFACT_PREFIX = "__spacefast/functions/bundles/";
98
- function isPublishableBuildArtifactPath(lowerPath) {
99
- return lowerPath.startsWith(PUBLISHABLE_BUILD_ARTIFACT_PREFIX);
99
+ /**
100
+ * The one FILE inside the control prefix a publish may write.
101
+ *
102
+ * A Zero capsule's compiled server travels as version metadata, not content, so
103
+ * a server-only change ships a byte-identical app shell and the version intent's
104
+ * manifest comparison reads it as "no changes". `sf publish` writes the deploy's
105
+ * digest here so the manifest moves whenever the deploy does — same reasoning as
106
+ * the Functions bundle above, same channel.
107
+ *
108
+ * One exact path, not a prefix: its siblings under `__spacefast/zero/` are the
109
+ * runtime's own artifact, config and binary, and a publish that could overwrite
110
+ * those would be a publish that could replace the runtime.
111
+ */
112
+ function isPublishableBuildArtifactPath(normalizedPath, lowerPath) {
113
+ return (lowerPath.startsWith(PUBLISHABLE_BUILD_ARTIFACT_PREFIX) ||
114
+ normalizedPath === ZERO_RUNTIME_DEPLOY_PATH);
100
115
  }
101
116
  export function publishPathIgnored(relativePath) {
102
117
  const path = normalizePublishPath(relativePath);
@@ -104,7 +119,8 @@ export function publishPathIgnored(relativePath) {
104
119
  // (space.json). Drop only those files at any depth so a checkout archived one
105
120
  // level too high never ships a claim token — but keep serving legitimate
106
121
  // content in the same dirs (e.g. the legacy .stattic/routes.json convention).
107
- if (/(?:^|\/)\.(?:spacefast|stattic)\/(?:state|space)\.json$/.test(path)) {
122
+ if (/(?:^|\/)\.(?:spacefast|stattic)\/(?:state|space)\.json$/.test(path) ||
123
+ /(?:^|\/)\.spacefast-(?:[0-9a-f-]{36}\.(?:tmp|rollback|retired)|[0-9a-f]{24}\.lock)$/.test(path)) {
108
124
  return true;
109
125
  }
110
126
  if (IGNORED_EXACT_PATHS.has(path) || /(?:^|\/)\.next\/cache(?:\/|$)/.test(path)) {
@@ -209,7 +225,7 @@ export function staticUploadPathViolation(path, options = {}) {
209
225
  const lowerSegment = segment.toLowerCase();
210
226
  return STATIC_RUNTIME_CONTROL_SEGMENT_PREFIXES.some((prefix) => lowerSegment.startsWith(prefix));
211
227
  });
212
- if (controlSegment && !isPublishableBuildArtifactPath(lowerPath)) {
228
+ if (controlSegment && !isPublishableBuildArtifactPath(normalizedPath, lowerPath)) {
213
229
  return pathViolation("static_runtime_control_path_not_supported", "Static deploys cannot upload runtime control paths.", normalizedPath);
214
230
  }
215
231
  if (STATIC_RUNTIME_CONTROL_PREFIXES.some((prefix) => lowerPath === prefix || lowerPath.startsWith(`${prefix}/`))) {
@@ -8,7 +8,6 @@ export declare const qk: {
8
8
  agentAccounts: (teamId: string) => readonly ["agent-accounts", string];
9
9
  myShelf: () => readonly ["my-shelf"];
10
10
  mcpDashboard: (teamId: string) => readonly ["mcp", "dashboard", string];
11
- mcpApproval: (teamId: string, id: string) => readonly ["mcp", "approval", string, string];
12
11
  teams: () => readonly ["teams", "list"];
13
12
  team: (ref: string) => readonly ["teams", "by-ref", string];
14
13
  teamMembers: (id: string) => readonly ["teams", string, "members"];
@@ -53,7 +52,6 @@ export declare const qk: {
53
52
  spaceVersionComments: (id: string, versionId: string, status?: string) => readonly ["spaces", string, "versions", string, "comments"] | readonly ["spaces", string, "versions", string, "comments", string];
54
53
  spaceVersionFiles: (id: string, versionId: string) => readonly ["spaces", string, "versions", string, "files"];
55
54
  spaceVersionFile: (id: string, versionId: string, path: string) => readonly ["spaces", string, "versions", string, "files", string];
56
- spaceVersionFileDirectUrl: (id: string, versionId: string, path: string) => readonly ["spaces", string, "versions", string, "files", string, "direct-url"];
57
55
  spaceVersionFileUrlPreview: (id: string, versionId: string, path: string) => readonly ["spaces", string, "versions", string, "files", string, "url-preview"];
58
56
  spaceVersionFileDiff: (id: string, versionId: string, baseVersionId?: string) => readonly ["spaces", string, "versions", string, "files", "diff", string];
59
57
  spaceVersionConventionArtifacts: (id: string, versionId: string) => readonly ["spaces", string, "versions", string, "convention-artifacts"];
@@ -63,9 +61,11 @@ export declare const qk: {
63
61
  spaceDomains: (id: string) => readonly ["spaces", string, "domains"];
64
62
  spaceRuntimeStatus: (id: string) => readonly ["spaces", string, "runtime"];
65
63
  spaceApplyState: (id: string) => readonly ["spaces", string, "apply-state"];
66
- spaceZeroDb: (id: string) => readonly ["spaces", string, "zero", "db"];
64
+ spaceDatabase: (id: string) => readonly ["spaces", string, "db"];
65
+ spaceDatabaseDump: (id: string, table: string | undefined, limit: number) => readonly ["spaces", string, "db", "dump", string, number];
66
+ spaceStorage: (id: string) => readonly ["spaces", string, "storage"];
67
67
  spaceRuntimeLogs: (id: string, limit: number) => readonly ["spaces", string, "logs", "runtime", number];
68
- spaceZeroRealtimeEvents: (id: string, limit: number) => readonly ["spaces", string, "zero", "realtime", "events", number];
68
+ spaceRuntimeRealtimeEvents: (id: string, limit: number) => readonly ["spaces", string, "runtime", "realtime", "events", number];
69
69
  spaceVariables: (id: string) => readonly ["spaces", string, "variables"];
70
70
  spaceCertificates: (id: string) => readonly ["spaces", string, "runtime", "certificates"];
71
71
  spaceLogs: (id: string, kind: "build" | "access" | "runtime") => readonly ["spaces", string, "logs", "build" | "runtime" | "access"];
@@ -79,4 +79,7 @@ export declare const qk: {
79
79
  domainDnsOperations: (domainId: string) => readonly ["domains", string, "dns", "operations"];
80
80
  domainActions: (domainId: string) => readonly ["domains", string, "actions"];
81
81
  domainProviderConnections: (teamId: string) => readonly ["domains", "provider-connections", string];
82
+ notifications: () => readonly ["notifications"];
83
+ notificationsList: (unreadOnly: boolean) => readonly ["notifications", "list", boolean];
84
+ notificationsUnreadCount: () => readonly ["notifications", "unread-count"];
82
85
  };
@@ -10,7 +10,6 @@ export const qk = {
10
10
  agentAccounts: (teamId) => ["agent-accounts", teamId],
11
11
  myShelf: () => ["my-shelf"],
12
12
  mcpDashboard: (teamId) => ["mcp", "dashboard", teamId],
13
- mcpApproval: (teamId, id) => ["mcp", "approval", teamId, id],
14
13
  // The teams LIST gets its own leaf so invalidating it never cascades into
15
14
  // ["teams", id, ...] resource queries (members, spaces, domains, ...).
16
15
  teams: () => ["teams", "list"],
@@ -71,7 +70,6 @@ export const qk = {
71
70
  : ["spaces", id, "versions", versionId, "comments", status],
72
71
  spaceVersionFiles: (id, versionId) => ["spaces", id, "versions", versionId, "files"],
73
72
  spaceVersionFile: (id, versionId, path) => ["spaces", id, "versions", versionId, "files", path],
74
- spaceVersionFileDirectUrl: (id, versionId, path) => ["spaces", id, "versions", versionId, "files", path, "direct-url"],
75
73
  spaceVersionFileUrlPreview: (id, versionId, path) => ["spaces", id, "versions", versionId, "files", path, "url-preview"],
76
74
  spaceVersionFileDiff: (id, versionId, baseVersionId) => ["spaces", id, "versions", versionId, "files", "diff", baseVersionId ?? ""],
77
75
  spaceVersionConventionArtifacts: (id, versionId) => ["spaces", id, "versions", versionId, "convention-artifacts"],
@@ -81,9 +79,15 @@ export const qk = {
81
79
  spaceDomains: (id) => ["spaces", id, "domains"],
82
80
  spaceRuntimeStatus: (id) => ["spaces", id, "runtime"],
83
81
  spaceApplyState: (id) => ["spaces", id, "apply-state"],
84
- spaceZeroDb: (id) => ["spaces", id, "zero", "db"],
82
+ // A space has one database, whichever runtime claims it — the key says `db`,
83
+ // not `zero`, for the same reason the route does.
84
+ spaceDatabase: (id) => ["spaces", id, "db"],
85
+ // Row reads are per table and per page size; the bare prefix above sweeps
86
+ // every dump alongside the schema read.
87
+ spaceDatabaseDump: (id, table, limit) => [...qk.spaceDatabase(id), "dump", table ?? "", limit],
88
+ spaceStorage: (id) => ["spaces", id, "storage"],
85
89
  spaceRuntimeLogs: (id, limit) => ["spaces", id, "logs", "runtime", limit],
86
- spaceZeroRealtimeEvents: (id, limit) => ["spaces", id, "zero", "realtime", "events", limit],
90
+ spaceRuntimeRealtimeEvents: (id, limit) => [...qk.spaceRuntimeStatus(id), "realtime", "events", limit],
87
91
  spaceVariables: (id) => ["spaces", id, "variables"],
88
92
  spaceCertificates: (id) => ["spaces", id, "runtime", "certificates"],
89
93
  spaceLogs: (id, kind) => ["spaces", id, "logs", kind],
@@ -97,4 +101,9 @@ export const qk = {
97
101
  domainDnsOperations: (domainId) => ["domains", domainId, "dns", "operations"],
98
102
  domainActions: (domainId) => ["domains", domainId, "actions"],
99
103
  domainProviderConnections: (teamId) => ["domains", "provider-connections", teamId],
104
+ // The shell's notification bell. The bare prefix is the invalidate-all sweep
105
+ // that a "mark read" performs across both the count and every list view.
106
+ notifications: () => ["notifications"],
107
+ notificationsList: (unreadOnly) => ["notifications", "list", unreadOnly],
108
+ notificationsUnreadCount: () => ["notifications", "unread-count"],
100
109
  };
@@ -2,5 +2,8 @@ export declare const SPACEFAST_RUNTIME_NAMESPACE: "/__spacefast";
2
2
  export declare const RUNTIME_MANAGEMENT_API_PATH: "/__spacefast/api.php";
3
3
  export declare const RUNTIME_UPLOAD_API_PATH: "/__spacefast/upload.php";
4
4
  export declare const RUNTIME_HEALTH_PATH: "/__spacefast/health.php";
5
+ export declare const RUNTIME_COMMENTS_CONFIG_PATH: "/__spacefast/comments/config";
6
+ export declare const RUNTIME_COMMENTS_TICKET_PATH: "/__spacefast/comments/ticket";
7
+ export declare const RUNTIME_COMMENTS_VERSION_URLS_PATH: "/__spacefast/comments/version-urls";
5
8
  export declare function runtimeManagementPath(suffix: string): string;
6
9
  export declare function runtimeUploadPath(params: URLSearchParams | Record<string, string>): string;
@@ -2,6 +2,13 @@ export const SPACEFAST_RUNTIME_NAMESPACE = "/__spacefast";
2
2
  export const RUNTIME_MANAGEMENT_API_PATH = `${SPACEFAST_RUNTIME_NAMESPACE}/api.php`;
3
3
  export const RUNTIME_UPLOAD_API_PATH = `${SPACEFAST_RUNTIME_NAMESPACE}/upload.php`;
4
4
  export const RUNTIME_HEALTH_PATH = `${SPACEFAST_RUNTIME_NAMESPACE}/health.php`;
5
+ // Same-origin Comments exchange paths served on every published host. The PHP
6
+ // mirror is SPACEFAST_COMMENTS_*_PATH in runtime/engine/shared/context.php;
7
+ // runtime/tests/spacefast-sdk.test.ts pins the PHP side and
8
+ // packages/collab-sdk/test/entry.test.ts pins the SDK side to these literals.
9
+ export const RUNTIME_COMMENTS_CONFIG_PATH = `${SPACEFAST_RUNTIME_NAMESPACE}/comments/config`;
10
+ export const RUNTIME_COMMENTS_TICKET_PATH = `${SPACEFAST_RUNTIME_NAMESPACE}/comments/ticket`;
11
+ export const RUNTIME_COMMENTS_VERSION_URLS_PATH = `${SPACEFAST_RUNTIME_NAMESPACE}/comments/version-urls`;
5
12
  export function runtimeManagementPath(suffix) {
6
13
  const route = normalizeRuntimeRoute(suffix);
7
14
  if (route === "/") {
@@ -0,0 +1,73 @@
1
+ import { type FileHandle } from "node:fs/promises";
2
+ export declare class SecureLocalFileError extends Error {
3
+ name: string;
4
+ }
5
+ export declare class SecureLocalFileConflictError extends SecureLocalFileError {
6
+ name: string;
7
+ }
8
+ export type SecureWriteOptions = {
9
+ root: string;
10
+ relativePath: string;
11
+ data?: Uint8Array | string;
12
+ write?: (file: FileHandle) => Promise<void>;
13
+ overwrite?: boolean;
14
+ mode?: number;
15
+ rootMode?: number;
16
+ expectedContent?: Uint8Array | string | null;
17
+ beforeCommit?: () => void | Promise<void>;
18
+ /** Fault-injection hook after rollback custody exists but before the atomic rename. */
19
+ afterBackup?: () => void | Promise<void>;
20
+ };
21
+ export type SecureReadOptions = {
22
+ root: string;
23
+ relativePath: string;
24
+ encoding?: BufferEncoding;
25
+ };
26
+ export type SecureDirectoryOptions = {
27
+ root: string;
28
+ relativePath: string;
29
+ mode?: number;
30
+ };
31
+ export type SecureLocalEntryIdentity = {
32
+ ctimeNs: bigint;
33
+ dev: bigint;
34
+ ino: bigint;
35
+ mode: bigint;
36
+ };
37
+ export type SecureRemoveOptions = {
38
+ root: string;
39
+ relativePath: string;
40
+ expectedIdentity: SecureLocalEntryIdentity;
41
+ };
42
+ export type SecureCreatedDirectory = {
43
+ path: string;
44
+ validate: () => Promise<void>;
45
+ commit: () => Promise<void>;
46
+ rollback: () => Promise<void>;
47
+ };
48
+ export declare function secureOutputLocation(trustedPath: string, outputPath: string): {
49
+ root: string;
50
+ relativePath: string;
51
+ };
52
+ /**
53
+ * Writes beneath an explicitly trusted root without following output-tree
54
+ * symlinks. Linux uses descriptor-relative operations; other supported
55
+ * platforms use exclusive staging plus before-and-after ancestor identity
56
+ * validation and fail closed if any binding changes.
57
+ */
58
+ export declare function secureWriteFile(options: SecureWriteOptions): Promise<void>;
59
+ export declare function secureReadFile(options: SecureReadOptions & {
60
+ encoding: BufferEncoding;
61
+ }): Promise<string>;
62
+ export declare function secureReadFile(options: SecureReadOptions): Promise<Buffer>;
63
+ /**
64
+ * Removes only the exact non-directory leaf identity previously inspected.
65
+ * The same leaf lock used by secureWriteFile covers the identity check and
66
+ * rename, so a cooperating writer cannot replace the leaf between them.
67
+ */
68
+ export declare function secureRemoveLocalEntry(options: SecureRemoveOptions): Promise<boolean>;
69
+ /**
70
+ * Creates one new directory leaf beneath a trusted root. The returned rollback
71
+ * removes only the exact directory inode created by this call.
72
+ */
73
+ export declare function secureCreateDirectory(options: SecureDirectoryOptions): Promise<SecureCreatedDirectory>;