@spacefast/common 0.0.23 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/brand-assets/LICENSE-Haskoy.txt +93 -0
  2. package/brand-assets/haskoy-latin-variable.woff2 +0 -0
  3. package/brand-assets/spacefast-favicon.svg +4 -4
  4. package/brand-assets/spacefast-sf-full-bleed.svg +3 -3
  5. package/brand-assets/spacefast-wordmark.svg +2 -4
  6. package/dist/agents/connect-targets.js +1 -1
  7. package/dist/agents/private-key-oauth.d.ts +0 -1
  8. package/dist/agents/private-key-oauth.js +46 -63
  9. package/dist/brand-assets-build.js +4 -6
  10. package/dist/brand-assets.d.ts +5 -15
  11. package/dist/brand-assets.js +15 -14
  12. package/dist/config/domains.d.ts +47 -7
  13. package/dist/config/domains.js +66 -10
  14. package/dist/config/index.d.ts +0 -1
  15. package/dist/config/index.js +0 -1
  16. package/dist/contracts/abuse.d.ts +2 -0
  17. package/dist/contracts/abuse.js +12 -0
  18. package/dist/contracts/access.d.ts +124 -12
  19. package/dist/contracts/access.js +73 -9
  20. package/dist/contracts/activity.d.ts +1 -1
  21. package/dist/contracts/activity.js +5 -12
  22. package/dist/contracts/api-keys.d.ts +21 -21
  23. package/dist/contracts/api-keys.js +2 -2
  24. package/dist/contracts/beta.js +1 -1
  25. package/dist/contracts/billing.d.ts +3 -35
  26. package/dist/contracts/billing.js +33 -52
  27. package/dist/contracts/builds.d.ts +30 -18
  28. package/dist/contracts/builds.js +28 -9
  29. package/dist/contracts/channels.js +1 -1
  30. package/dist/contracts/cli.d.ts +10 -0
  31. package/dist/contracts/cli.js +10 -0
  32. package/dist/contracts/collab-public.d.ts +63 -0
  33. package/dist/contracts/collab-public.js +46 -0
  34. package/dist/contracts/comments.d.ts +282 -53
  35. package/dist/contracts/comments.js +173 -52
  36. package/dist/contracts/common.d.ts +3 -3
  37. package/dist/contracts/common.js +5 -5
  38. package/dist/contracts/crons.d.ts +35 -0
  39. package/dist/contracts/crons.js +42 -0
  40. package/dist/contracts/device-auth.d.ts +6 -6
  41. package/dist/contracts/device-auth.js +2 -2
  42. package/dist/contracts/docs.js +2 -2
  43. package/dist/contracts/domains.d.ts +38 -27
  44. package/dist/contracts/domains.js +29 -2
  45. package/dist/contracts/enums.d.ts +34 -30
  46. package/dist/contracts/enums.js +62 -48
  47. package/dist/contracts/error-code-meta.d.ts +50 -125
  48. package/dist/contracts/error-code-meta.js +29 -41
  49. package/dist/contracts/error-codes.d.ts +1 -1
  50. package/dist/contracts/error-codes.js +14 -33
  51. package/dist/contracts/execution.d.ts +47 -66
  52. package/dist/contracts/execution.js +53 -49
  53. package/dist/contracts/feature-lifecycle-core.d.ts +0 -6
  54. package/dist/contracts/feature-lifecycle-core.js +2 -31
  55. package/dist/contracts/feature-lifecycle.d.ts +12 -52
  56. package/dist/contracts/feature-lifecycle.js +16 -59
  57. package/dist/contracts/features.d.ts +48 -60
  58. package/dist/contracts/features.js +59 -51
  59. package/dist/contracts/functions.d.ts +266 -72
  60. package/dist/contracts/functions.js +260 -37
  61. package/dist/contracts/git.d.ts +28 -34
  62. package/dist/contracts/git.js +10 -11
  63. package/dist/contracts/grants.d.ts +9 -5
  64. package/dist/contracts/grants.js +18 -9
  65. package/dist/contracts/ids.js +2 -2
  66. package/dist/contracts/internal.d.ts +8 -6
  67. package/dist/contracts/internal.js +12 -12
  68. package/dist/contracts/mcp.d.ts +11 -0
  69. package/dist/contracts/mcp.js +5 -0
  70. package/dist/contracts/me.js +1 -1
  71. package/dist/contracts/oauth-resources.d.ts +4 -3
  72. package/dist/contracts/oauth-resources.js +12 -3
  73. package/dist/contracts/operations.d.ts +90 -5
  74. package/dist/contracts/operations.js +77 -3
  75. package/dist/contracts/pages.d.ts +12 -7
  76. package/dist/contracts/pages.js +24 -1
  77. package/dist/contracts/plan-policy.d.ts +0 -2
  78. package/dist/contracts/plan-policy.js +2 -3
  79. package/dist/contracts/platform.d.ts +61 -148
  80. package/dist/contracts/platform.js +70 -126
  81. package/dist/contracts/publish-archive.d.ts +508 -0
  82. package/dist/contracts/publish-archive.js +32 -0
  83. package/dist/contracts/push-new.d.ts +4 -4
  84. package/dist/contracts/quotas.js +2 -2
  85. package/dist/contracts/repository-connections.d.ts +43 -30
  86. package/dist/contracts/repository-connections.js +11 -3
  87. package/dist/contracts/resources.d.ts +12 -24
  88. package/dist/contracts/resources.js +36 -16
  89. package/dist/contracts/runtime-api.d.ts +433 -192
  90. package/dist/contracts/runtime-api.js +421 -356
  91. package/dist/contracts/runtime-app.d.ts +127 -120
  92. package/dist/contracts/runtime-app.js +19 -10
  93. package/dist/contracts/runtime-db.d.ts +5 -5
  94. package/dist/contracts/runtime-db.js +2 -2
  95. package/dist/contracts/runtime-purge.d.ts +8 -0
  96. package/dist/contracts/runtime-purge.js +7 -0
  97. package/dist/contracts/runtime-services.d.ts +219 -0
  98. package/dist/contracts/runtime-services.js +437 -0
  99. package/dist/contracts/runtime-storage.d.ts +46 -32
  100. package/dist/contracts/runtime-storage.js +31 -8
  101. package/dist/contracts/sf-config-v1.d.ts +48 -10
  102. package/dist/contracts/space-config.d.ts +69 -18
  103. package/dist/contracts/space-config.js +226 -27
  104. package/dist/contracts/spaces.d.ts +236 -450
  105. package/dist/contracts/spaces.js +131 -151
  106. package/dist/contracts/superadmin-activity.d.ts +57 -4
  107. package/dist/contracts/superadmin-activity.js +48 -2
  108. package/dist/contracts/superadmin-search.d.ts +126 -0
  109. package/dist/contracts/superadmin-search.js +61 -0
  110. package/dist/contracts/superadmin-spaces.d.ts +361 -92
  111. package/dist/contracts/superadmin-spaces.js +97 -24
  112. package/dist/contracts/superadmin-teams.d.ts +28 -10
  113. package/dist/contracts/superadmin-teams.js +5 -8
  114. package/dist/contracts/superadmin-tenants.d.ts +0 -6
  115. package/dist/contracts/superadmin-tenants.js +1 -7
  116. package/dist/contracts/superadmin.d.ts +135 -54
  117. package/dist/contracts/superadmin.js +46 -23
  118. package/dist/contracts/tags.d.ts +12 -12
  119. package/dist/contracts/tags.js +3 -3
  120. package/dist/contracts/teams.d.ts +49 -14
  121. package/dist/contracts/teams.js +40 -22
  122. package/dist/contracts/transfers.d.ts +0 -49
  123. package/dist/contracts/transfers.js +4 -8
  124. package/dist/contracts/usage.d.ts +61 -0
  125. package/dist/contracts/usage.js +64 -0
  126. package/dist/contracts/variables.d.ts +2 -7
  127. package/dist/contracts/variables.js +9 -8
  128. package/dist/contracts/webhooks.d.ts +2 -2
  129. package/dist/contracts/zero.d.ts +102 -73
  130. package/dist/contracts/zero.js +94 -32
  131. package/dist/dashboard-paths/index.d.ts +1 -1
  132. package/dist/dashboard-paths/index.js +1 -0
  133. package/dist/docs/agent-prose.d.ts +2 -2
  134. package/dist/docs/agent-prose.js +63 -70
  135. package/dist/docs/agent-setup.js +45 -15
  136. package/dist/docs/catalog.d.ts +24 -24
  137. package/dist/docs/catalog.js +35 -24
  138. package/dist/docs/error-docs.js +194 -270
  139. package/dist/docs/skills.d.ts +12 -12
  140. package/dist/docs/skills.js +4 -4
  141. package/dist/test-helpers/fetch-stub.d.ts +16 -0
  142. package/dist/test-helpers/fetch-stub.js +25 -0
  143. package/dist/utils/build-settings.d.ts +5 -1
  144. package/dist/utils/build-settings.js +91 -13
  145. package/dist/utils/canonical-json.d.ts +10 -0
  146. package/dist/utils/canonical-json.js +22 -0
  147. package/dist/utils/cast-transport.d.ts +28 -0
  148. package/dist/utils/cast-transport.js +52 -0
  149. package/dist/utils/claim-token.d.ts +0 -8
  150. package/dist/utils/claim-token.js +3 -15
  151. package/dist/utils/concurrency.js +1 -0
  152. package/dist/utils/credential-policy.d.ts +1 -1
  153. package/dist/utils/credential-policy.js +4 -3
  154. package/dist/utils/dns-instructions.js +9 -9
  155. package/dist/utils/env-file.d.ts +17 -0
  156. package/dist/utils/env-file.js +96 -0
  157. package/dist/utils/error-code.d.ts +2 -0
  158. package/dist/utils/error-code.js +8 -0
  159. package/dist/utils/generate-space-name.d.ts +8 -10
  160. package/dist/utils/generate-space-name.js +206 -21
  161. package/dist/utils/grant-decision.fixtures.json +2938 -0
  162. package/dist/utils/grants.d.ts +2 -0
  163. package/dist/utils/grants.js +26 -1
  164. package/dist/utils/gravatar.d.ts +9 -0
  165. package/dist/utils/gravatar.js +31 -0
  166. package/dist/utils/idempotency.d.ts +6 -0
  167. package/dist/utils/idempotency.js +28 -0
  168. package/dist/utils/local-space-state.d.ts +9 -16
  169. package/dist/utils/local-space-state.js +20 -50
  170. package/dist/utils/one-shot-replay.d.ts +4 -1
  171. package/dist/utils/page-colors.d.ts +14 -12
  172. package/dist/utils/page-colors.js +15 -13
  173. package/dist/utils/page-fonts.d.ts +2 -2
  174. package/dist/utils/page-fonts.js +15 -4
  175. package/dist/utils/pages.d.ts +21 -3
  176. package/dist/utils/pages.js +42 -13
  177. package/dist/utils/publish-form-data.d.ts +12 -0
  178. package/dist/utils/publish-form-data.js +10 -0
  179. package/dist/utils/publish-policy.d.ts +27 -0
  180. package/dist/utils/publish-policy.fixtures.json +403 -0
  181. package/dist/utils/publish-policy.js +45 -20
  182. package/dist/utils/push-new.d.ts +7 -0
  183. package/dist/utils/push-new.js +7 -0
  184. package/dist/utils/query-keys.d.ts +4 -5
  185. package/dist/utils/query-keys.js +2 -3
  186. package/dist/utils/runtime-paths.d.ts +5 -0
  187. package/dist/utils/runtime-paths.js +5 -0
  188. package/dist/utils/runtime-upload-batch.d.ts +0 -5
  189. package/dist/utils/runtime-upload-batch.js +1 -27
  190. package/dist/utils/runtime-upload.d.ts +6 -11
  191. package/dist/utils/runtime-upload.js +10 -13
  192. package/dist/utils/secure-local-file.js +5 -16
  193. package/dist/utils/space-config.d.ts +1 -34
  194. package/dist/utils/space-config.js +8 -326
  195. package/dist/utils/static-runtime-policy.d.ts +9 -3
  196. package/dist/utils/static-runtime-policy.fixtures.json +38 -41
  197. package/dist/utils/static-runtime-policy.js +22 -56
  198. package/dist/utils/storage-policy.d.ts +1 -1
  199. package/dist/utils/storage-policy.js +3 -1
  200. package/dist/utils/structured-error.d.ts +14 -0
  201. package/dist/utils/structured-error.js +22 -0
  202. package/dist/utils/upload-session.d.ts +4 -1
  203. package/dist/utils/upload-session.js +33 -5
  204. package/dist/utils/version-path.js +13 -4
  205. package/dist/vocabulary.d.ts +13 -11
  206. package/dist/vocabulary.js +42 -28
  207. package/package.json +7 -17
  208. package/dist/config/postgres-budget.d.ts +0 -129
  209. package/dist/config/postgres-budget.js +0 -140
  210. package/dist/config/postgres-budget.json +0 -51
  211. package/dist/contracts/archives.d.ts +0 -331
  212. package/dist/contracts/archives.js +0 -120
  213. package/dist/contracts/deployments.d.ts +0 -574
  214. package/dist/contracts/deployments.js +0 -97
  215. package/dist/contracts/sites.d.ts +0 -416
  216. package/dist/contracts/sites.js +0 -168
  217. package/dist/contracts/superadmin-queues.d.ts +0 -414
  218. package/dist/contracts/superadmin-queues.js +0 -202
  219. package/dist/contracts/superadmin-runtime.d.ts +0 -27
  220. package/dist/contracts/superadmin-runtime.js +0 -38
  221. package/dist/index.d.ts +0 -1
  222. package/dist/index.js +0 -1
  223. package/dist/utils/api.d.ts +0 -4
  224. package/dist/utils/api.js +0 -13
@@ -1,5 +1,5 @@
1
1
  import { CONFIG_PUBLIC_GRANT_SOURCE_REFERENCE, grantSchema, } from "../contracts/grants.js";
2
- import { SPACE_CONFIG_CAPS, SPACE_CONFIG_CANONICAL_FILE, SPACE_CONFIG_PRIVATE_FILES, spaceConfigFileSchema, spaceConfigSchema, } from "../contracts/space-config.js";
2
+ import { SPACE_CONFIG_CAPS, SPACE_CONFIG_CANONICAL_FILE, } from "../contracts/space-config.js";
3
3
  const TEXT_BYTES = new TextEncoder();
4
4
  export const CONFIG_PUBLIC_GRANT_COMPILED_ID = "config:access.public";
5
5
  function byteLength(value) {
@@ -57,6 +57,7 @@ function capIssues(candidate) {
57
57
  }
58
58
  else {
59
59
  for (const key of ["head", "bodyStart", "bodyEnd", "noscript"]) {
60
+ // SAFETY: The enclosing branch proves inject is a non-null, non-array object.
60
61
  const snippets = inject[key];
61
62
  if (snippets === undefined) {
62
63
  continue;
@@ -107,6 +108,7 @@ function capIssues(candidate) {
107
108
  ["title", SPACE_CONFIG_CAPS.metaTitleChars],
108
109
  ["description", SPACE_CONFIG_CAPS.metaDescriptionChars],
109
110
  ["image", SPACE_CONFIG_CAPS.metaImageChars],
111
+ ["favicon", SPACE_CONFIG_CAPS.metaImageChars],
110
112
  ];
111
113
  for (const [key, limit] of caps) {
112
114
  const value = meta[key];
@@ -123,75 +125,6 @@ function capIssues(candidate) {
123
125
  }
124
126
  return issues;
125
127
  }
126
- // File-only keys (`name`, `runtime`) never reach the API overlay, so their
127
- // checks live beside the shared caps rather than inside them. Both run BEFORE
128
- // zod so a bad declaration reports its stable code instead of a shape error.
129
- const RUNTIME_KINDS = new Set(["zero", "functions"]);
130
- function fileOnlyIssues(candidate) {
131
- const issues = [];
132
- const name = candidate.name;
133
- if (typeof name === "string" && name.trim().length > SPACE_CONFIG_CAPS.nameChars) {
134
- issues.push({
135
- severity: "error",
136
- code: "config_name_too_long",
137
- message: `name supports up to ${SPACE_CONFIG_CAPS.nameChars} characters.`,
138
- path: "name",
139
- details: { length: name.trim().length, limit: SPACE_CONFIG_CAPS.nameChars },
140
- });
141
- }
142
- const runtime = candidate.runtime;
143
- if (runtime === undefined) {
144
- return issues;
145
- }
146
- if (typeof runtime !== "object" || runtime === null || Array.isArray(runtime)) {
147
- issues.push({
148
- severity: "error",
149
- code: "config_runtime_invalid_kind",
150
- message: 'runtime must be an object declaring a kind of "zero" or "functions".',
151
- path: "runtime",
152
- });
153
- return issues;
154
- }
155
- const declaration = runtime;
156
- const kind = declaration.kind;
157
- if (typeof kind !== "string" || !RUNTIME_KINDS.has(kind)) {
158
- issues.push({
159
- severity: "error",
160
- code: "config_runtime_invalid_kind",
161
- message: 'runtime.kind must be "zero" or "functions".',
162
- path: "runtime.kind",
163
- ...(typeof kind === "string" ? { details: { kind } } : {}),
164
- });
165
- return issues;
166
- }
167
- if (kind === "zero") {
168
- // Zero is never inferred, so a declaration that names neither entry has
169
- // nothing to compile — failing here beats publishing static files under a
170
- // config that promised a database app.
171
- for (const key of ["server", "client"]) {
172
- const value = declaration[key];
173
- if (typeof value !== "string" || value.trim().length === 0) {
174
- issues.push({
175
- severity: "error",
176
- code: "config_runtime_entry_missing",
177
- message: `runtime.${key} is required and must name a module when runtime.kind is "zero".`,
178
- path: `runtime.${key}`,
179
- });
180
- }
181
- }
182
- return issues;
183
- }
184
- const entry = declaration.entry;
185
- if (entry !== undefined && (typeof entry !== "string" || entry.trim().length === 0)) {
186
- issues.push({
187
- severity: "error",
188
- code: "config_runtime_entry_missing",
189
- message: "runtime.entry must name a module when present; omit it to detect the worker instead.",
190
- path: "runtime.entry",
191
- });
192
- }
193
- return issues;
194
- }
195
128
  /**
196
129
  * Keys a committed `sf.jsonc` owns outright: they are facts about the content
197
130
  * of a version (who may read it, what it is called, what runs it), so they roll
@@ -233,26 +166,16 @@ export function spaceConfigOverlayIssue(config) {
233
166
  }
234
167
  return capIssues(config)[0] ?? null;
235
168
  }
236
- // Other platforms' routing vocabulary. We have one matcher grammar; naming it
237
- // four more ways would be four more things to keep in sync.
238
- const ROUTING_KEYS = new Set(["proxies", "proxy", "routes"]);
239
- // OUR routing vocabulary. These keys are real sf.jsonc keys, owned end to end
240
- // by the routing compiler (crates/stattic-runtime-core): the strict v1 compiler
241
- // validates them and the merge folds them in behind `_redirects`/`_headers`.
242
- // This lane resolves the serving config, so it neither validates nor carries
243
- // them — it just leaves them alone.
244
- const ROUTING_RULE_KEYS = new Set(["redirects", "rewrites", "headers"]);
245
- // Replaced by the `runtime` declaration; never silently ignored like other
246
- // unknown keys, because it is the one key whose loss changes what gets served.
247
- const REMOVED_FUNCTIONS_KEY = "functions";
248
- const KNOWN_FILE_KEYS = new Set(Object.keys(spaceConfigFileSchema.shape));
249
- // Strips JSONC comments and trailing commas (string-aware single pass).
169
+ // Strips JSONC comments and trailing commas (string-aware single pass). The
170
+ // config file itself is parsed by the crate; this stays for the OTHER JSONC
171
+ // files the CLI reads (agent configs, exported platform env).
250
172
  export function stripJsonComments(raw) {
251
173
  let output = "";
252
174
  let inString = false;
253
175
  let inLineComment = false;
254
176
  let inBlockComment = false;
255
177
  for (let i = 0; i < raw.length; i += 1) {
178
+ // SAFETY: The loop condition proves i addresses a character in raw.
256
179
  const char = raw[i];
257
180
  const next = raw[i + 1];
258
181
  if (inLineComment) {
@@ -299,6 +222,7 @@ export function stripJsonComments(raw) {
299
222
  // Trailing comma: skip when the next non-whitespace/non-comment char closes.
300
223
  let j = i + 1;
301
224
  while (j < raw.length) {
225
+ // SAFETY: The loop condition proves j addresses a character in raw.
302
226
  const ahead = raw[j];
303
227
  if (/\s/.test(ahead)) {
304
228
  j += 1;
@@ -326,245 +250,3 @@ export function stripJsonComments(raw) {
326
250
  }
327
251
  return output;
328
252
  }
329
- /**
330
- * Parses a raw `sf.jsonc` at finalize: 256 KB cap, JSONC tolerance,
331
- * routing-key rejection, unknown-key diagnostics, shared shape caps. Errors
332
- * fail the version; warnings ride version diagnostics.
333
- */
334
- export function parseSpaceConfigFile(raw, options = {}) {
335
- const configPath = options.path ?? SPACE_CONFIG_CANONICAL_FILE;
336
- if (byteLength(raw) > SPACE_CONFIG_CAPS.fileBytes) {
337
- return {
338
- config: null,
339
- issues: [
340
- {
341
- severity: "error",
342
- code: "config_file_too_large",
343
- message: `${configPath} supports up to ${SPACE_CONFIG_CAPS.fileBytes} bytes.`,
344
- path: configPath,
345
- details: { size: byteLength(raw), limit: SPACE_CONFIG_CAPS.fileBytes },
346
- },
347
- ],
348
- };
349
- }
350
- let candidate;
351
- try {
352
- candidate = JSON.parse(stripJsonComments(raw));
353
- }
354
- catch (error) {
355
- return {
356
- config: null,
357
- issues: [
358
- {
359
- severity: "error",
360
- code: "config_invalid",
361
- message: `${configPath} is not valid JSONC: ${error instanceof Error ? error.message : "parse error"}.`,
362
- path: configPath,
363
- },
364
- ],
365
- };
366
- }
367
- if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
368
- return {
369
- config: null,
370
- issues: [
371
- {
372
- severity: "error",
373
- code: "config_invalid",
374
- message: `${configPath} must contain a JSON object.`,
375
- path: configPath,
376
- },
377
- ],
378
- };
379
- }
380
- const record = candidate;
381
- const issues = [];
382
- for (const key of Object.keys(record)) {
383
- if (KNOWN_FILE_KEYS.has(key)) {
384
- continue;
385
- }
386
- if (ROUTING_RULE_KEYS.has(key)) {
387
- delete record[key];
388
- continue;
389
- }
390
- if (key === REMOVED_FUNCTIONS_KEY) {
391
- // The runtime declaration replaced this key outright (spec "The old
392
- // top-level functions.* key is removed in the same cutover"): warning-and-
393
- // strip would publish static files under a config that asked for a
394
- // worker, so it blocks with its own code instead.
395
- issues.push({
396
- severity: "error",
397
- code: "config_functions_key_removed",
398
- message: `The "${REMOVED_FUNCTIONS_KEY}" key was removed from ${configPath}; declare runtime.kind "functions" instead.`,
399
- path: key,
400
- });
401
- }
402
- else if (ROUTING_KEYS.has(key)) {
403
- issues.push({
404
- severity: "error",
405
- code: "config_invalid",
406
- message: `Routing never lives in ${configPath}; move "${key}" into _redirects/_headers.`,
407
- path: key,
408
- });
409
- }
410
- else {
411
- issues.push({
412
- severity: "warning",
413
- code: "config_invalid",
414
- message: `Unknown ${configPath} key "${key}" was ignored.`,
415
- path: key,
416
- });
417
- }
418
- delete record[key];
419
- }
420
- issues.push(...capIssues(record), ...fileOnlyIssues(record));
421
- if (issues.some((issue) => issue.severity === "error")) {
422
- return { config: null, issues };
423
- }
424
- const parsed = spaceConfigFileSchema.safeParse(record);
425
- if (!parsed.success) {
426
- for (const issue of parsed.error.issues) {
427
- issues.push({
428
- severity: "error",
429
- code: "config_invalid",
430
- message: issue.message,
431
- path: issue.path.map(String).join(".") || configPath,
432
- });
433
- }
434
- return { config: null, issues };
435
- }
436
- return { config: parsed.data, issues };
437
- }
438
- // Mirrors the runtime's private-file rules (_stattic_runtime_file_private):
439
- // convention/config files and dot-segment paths never serve, so they don't
440
- // count toward root HTML inference below.
441
- const PRIVATE_SERVING_FILES = new Set([
442
- "_redirects",
443
- "_headers",
444
- "_config.json",
445
- "_routes.json",
446
- ...SPACE_CONFIG_PRIVATE_FILES,
447
- ]);
448
- function privateServingPath(path) {
449
- if (PRIVATE_SERVING_FILES.has(path) ||
450
- path === ".well-known/spacefast-runtime" ||
451
- path === ".well-known/stattic-runtime") {
452
- return true;
453
- }
454
- const segments = path.toLowerCase().split("/");
455
- return segments.some((segment, index) => !(index === 0 && segment === ".well-known") && segment.startsWith("."));
456
- }
457
- function rootHtmlPaths(manifestPaths) {
458
- const htmlPaths = [];
459
- for (const path of manifestPaths) {
460
- if (path === "" || privateServingPath(path)) {
461
- continue;
462
- }
463
- if (!path.includes("/") && /\.html?$/i.test(path)) {
464
- htmlPaths.push(path);
465
- }
466
- }
467
- return htmlPaths.toSorted((left, right) => left.localeCompare(right));
468
- }
469
- function normalizeFallback(fallback) {
470
- if (fallback === undefined) {
471
- return null;
472
- }
473
- if (typeof fallback === "string") {
474
- return { path: fallback.replace(/^\/+/, ""), status: 200 };
475
- }
476
- const status = fallback.status === 404 ? 404 : 200;
477
- return { path: fallback.path.replace(/^\/+/, ""), status };
478
- }
479
- /**
480
- * Resolves the effective SpaceConfig for a version at finalize (or
481
- * config-only regeneration). `manifestPaths` is the canonical committed file
482
- * set; `fileConfig` the parsed sf.jsonc (null when absent); `overlay`
483
- * the stored space config overlay (null when nothing overridden).
484
- */
485
- export function resolveEffectiveSpaceConfig(input) {
486
- const rootHtml = rootHtmlPaths(input.manifestPaths);
487
- const hasRootIndex = rootHtml.includes("index.html");
488
- const inferredRootHtmlIndex = hasRootIndex ? "index.html" : (rootHtml[0] ?? null);
489
- const explicitServingShape = input.fileConfig?.index !== undefined ||
490
- input.fileConfig?.listing !== undefined ||
491
- input.overlay?.index !== undefined ||
492
- input.overlay?.listing !== undefined;
493
- // Content-inferred defaults (spec: the old website/files modes are exactly
494
- // these two default bundles; never API fields). A Functions version is an
495
- // application even when it has no root HTML: unresolved requests, including
496
- // `/`, belong to its worker. Static versions retain the content inference —
497
- // a root index.html means website mode, any other root HTML becomes the
498
- // homepage rewrite, and no root HTML means files mode.
499
- const functionsApplication = input.runtimeKind === "functions";
500
- const defaults = inferredRootHtmlIndex
501
- ? { index: inferredRootHtmlIndex, listing: false }
502
- : { index: false, listing: !functionsApplication };
503
- const merged = { ...defaults };
504
- for (const layer of [input.fileConfig, input.overlay]) {
505
- if (!layer) {
506
- continue;
507
- }
508
- for (const key of Object.keys(spaceConfigSchema.shape)) {
509
- const value = layer[key];
510
- if (value !== undefined) {
511
- merged[key] = value;
512
- }
513
- }
514
- }
515
- const issues = [];
516
- const fallback = normalizeFallback(merged.fallback);
517
- // Invariant (spec "Serving resolution"): a fallback must point at a
518
- // committed file — no implicit extension guessing, fail at finalize.
519
- if (fallback && !input.manifestPaths.has(fallback.path)) {
520
- issues.push({
521
- severity: "error",
522
- code: "config_invalid",
523
- message: `fallback path "/${fallback.path}" is not a committed file in this version.`,
524
- path: "fallback.path",
525
- details: { path: fallback.path },
526
- });
527
- }
528
- const listing = merged.listing ?? (!functionsApplication && !inferredRootHtmlIndex);
529
- const serving = {
530
- index: merged.index ?? inferredRootHtmlIndex ?? false,
531
- fallback,
532
- // Clean URLs (runtime W7.1): an explicit config value wins; the default is
533
- // ON for plain static sites and OFF when a 200-status SPA fallback owns
534
- // extensionless routes (the fallback answers them instead). The runtime
535
- // resolver (management.php) applies the same default for artifacts
536
- // finalized without the key.
537
- cleanUrls: merged.cleanUrls ?? !(fallback && fallback.status === 200),
538
- listing,
539
- // The single-file viewer rides the listing bundle (artifact-style spaces).
540
- viewer: listing,
541
- meta: merged.meta ?? null,
542
- pageTheme: merged.theme && Object.keys(merged.theme).length > 0 ? merged.theme : null,
543
- experimentalGutenberg: merged.experimental_gutenberg === true,
544
- inject: merged.inject ?? null,
545
- };
546
- if (inferredRootHtmlIndex !== null && !hasRootIndex && serving.index === inferredRootHtmlIndex) {
547
- // Surfaced on the publish receipt and version diagnostics so the CLI and
548
- // agents learn the rewrite without probing the live URL.
549
- issues.push({
550
- severity: "info",
551
- code: "single_html_index",
552
- message: `${inferredRootHtmlIndex} is the root HTML file selected for this version, so it serves as the homepage: / rewrites to /${inferredRootHtmlIndex}.`,
553
- path: "index",
554
- details: { path: inferredRootHtmlIndex, rootHtmlCount: rootHtml.length },
555
- });
556
- }
557
- if (inferredRootHtmlIndex === null &&
558
- serving.index === false &&
559
- serving.listing &&
560
- !explicitServingShape) {
561
- issues.push({
562
- severity: "info",
563
- code: "files_mode_inferred",
564
- message: "No HTML files were found in the root of this version, so it serves in files mode with directory listing enabled.",
565
- path: "listing",
566
- details: { rootHtmlCount: 0 },
567
- });
568
- }
569
- return { config: merged, serving, issues };
570
- }
@@ -5,14 +5,20 @@ export declare const PLATFORM_MANAGED_RESPONSE_HEADERS: string[];
5
5
  export declare const PLATFORM_MANAGED_RESPONSE_HEADER_PREFIXES: string[];
6
6
  /** True when user rules may neither set nor remove this response header. */
7
7
  export declare function isPlatformManagedResponseHeader(name: string): boolean;
8
- export declare const NON_IMMUTABLE_EXTENSIONS: string[];
9
- export declare const GUARANTEED_IMMUTABLE_PREFIXES: string[];
8
+ /**
9
+ * The token grammar a `_headers` rule accepts for a header NAME. The routing
10
+ * compiler enforces it; anything that GENERATES `_headers` rules (the Next
11
+ * materializer's framework sections) must gate on the same pattern, so a
12
+ * generated rule can never fail the author's publish.
13
+ */
14
+ export declare const HEADER_RULE_NAME_PATTERN: RegExp;
15
+ /** `content-type` → `Content-Type`: the casing compiled rules and generated `_headers` carry. */
16
+ export declare function canonicalHeaderName(input: string): string;
10
17
  /** Lowercased extension without the leading dot, posix semantics. Dotfiles have no extension. */
11
18
  export declare function pathExtension(filePath: string): string;
12
19
  /** Basename without its extension (the "stem"), posix semantics. */
13
20
  export declare function pathStem(filePath: string): string;
14
21
  export declare function isPhpLikeStaticPath(filePath: string): boolean;
15
- export declare function isImmutableAssetPath(filePath: string): boolean;
16
22
  /** Forced inert serving policy for a committed static file. PHP-like files are never executable. */
17
23
  export declare function runtimeFileSafety(filePath: string): {
18
24
  executable: false;
@@ -1,44 +1,33 @@
1
1
  {
2
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 }
3
+ {
4
+ "path": "index.php",
5
+ "expected": true
6
+ },
7
+ {
8
+ "path": "nested/app.phtml",
9
+ "expected": true
10
+ },
11
+ {
12
+ "path": "lib.phar",
13
+ "expected": true
14
+ },
15
+ {
16
+ "path": "legacy.php5",
17
+ "expected": true
18
+ },
19
+ {
20
+ "path": "index.html",
21
+ "expected": false
22
+ },
23
+ {
24
+ "path": "assets/main.js",
25
+ "expected": false
26
+ },
27
+ {
28
+ "path": ".env",
29
+ "expected": false
30
+ }
42
31
  ],
43
32
  "safeContentType": [
44
33
  {
@@ -46,12 +35,20 @@
46
35
  "mime": "application/x-httpd-php",
47
36
  "expected": "text/plain; charset=utf-8"
48
37
  },
49
- { "path": "lib.phar", "mime": "application/zip", "expected": "application/octet-stream" },
38
+ {
39
+ "path": "lib.phar",
40
+ "mime": "application/zip",
41
+ "expected": "application/octet-stream"
42
+ },
50
43
  {
51
44
  "path": "assets/main.js",
52
45
  "mime": "text/javascript; charset=utf-8",
53
46
  "expected": "text/javascript; charset=utf-8"
54
47
  },
55
- { "path": "logo.svg", "mime": "image/svg+xml", "expected": "image/svg+xml" }
48
+ {
49
+ "path": "logo.svg",
50
+ "mime": "image/svg+xml",
51
+ "expected": "image/svg+xml"
52
+ }
56
53
  ]
57
54
  }
@@ -1,5 +1,5 @@
1
1
  // Single source of truth for static-runtime content policy: PHP-like inert
2
- // classification, immutable-asset detection, and safe content-type forcing.
2
+ // classification and safe content-type forcing.
3
3
  //
4
4
  // This module is intentionally pure (no node:path, no I/O) so the exact same
5
5
  // rules can be mechanically code-generated into the PHP runtime (runtime
@@ -30,7 +30,7 @@ export const INTERNAL_REDIRECT_RESPONSE_HEADERS = [
30
30
  "x-reproxy-url",
31
31
  "x-sendfile",
32
32
  ];
33
- // Rejected/platform-managed response headers (internal-docs/platform.md "Routing And
33
+ // Rejected/platform-managed response headers (the platform spec "Routing And
34
34
  // Headers"): user `_headers` rules can never set or remove these. The
35
35
  // internal-redirect/sendfile family above is spread in, so it stays a strict subset —
36
36
  // anything the proxy lane refuses to relay is also refused from `_headers`. The rest is
@@ -87,52 +87,27 @@ export function isPlatformManagedResponseHeader(name) {
87
87
  return (PLATFORM_MANAGED_RESPONSE_HEADER_SET.has(lower) ||
88
88
  PLATFORM_MANAGED_RESPONSE_HEADER_PREFIXES.some((prefix) => lower.startsWith(prefix)));
89
89
  }
90
- export const NON_IMMUTABLE_EXTENSIONS = ["html", "htm", "php", "txt", "xml"];
91
- const PHP_LIKE_EXTENSION_SET = new Set(PHP_LIKE_EXTENSIONS);
92
- const NON_IMMUTABLE_EXTENSION_SET = new Set(NON_IMMUTABLE_EXTENSIONS);
93
- // Immutable caching is decided by DETERMINISTIC signals only reserved framework
94
- // build directories below, plus user-declared `_headers` Cache-Control rules that
95
- // override the baked default at serve time. There is deliberately NO filename-entropy
96
- // heuristic: an 8-char base64url bundler hash is character-for-character
97
- // indistinguishable from a short human name (D3GeoMap, McKayCV2), so any guess either
98
- // pins a user's mutable file for a year (stale site nobody can fix — browsers have no
99
- // bust channel) or silently downgrades real hashes to revalidate-every-load, which
100
- // under the wp.cloud edge per-IP rate limiter turned dashboard page loads into 429
101
- // storms (2026-07-06). Unrecognized files default to the safe direction:
102
- // `max-age=0, must-revalidate` for browsers, long s-maxage + purge-on-publish at the
103
- // edge.
104
- //
105
- // Each prefix is HIGH-CONFIDENCE guaranteed-hashed:
106
- // - `_next/static/` Next.js immutable build assets (chunks/css/media), all
107
- // [contenthash]; Next itself serves this dir with `immutable`.
108
- // - `_app/immutable/` SvelteKit (Vite) — the directory is literally named `immutable`
109
- // and holds only fingerprinted chunks/assets.
110
- // - `_nuxt/` Nuxt build dir — Vite-hashed client bundle, served from a
111
- // reserved app-owned path.
112
- // - `_astro/` Astro bundled assets — all content-hashed.
113
- //
114
- // DELIBERATELY EXCLUDED: generic user-writable dirs (Vite's `assets/`, CRA/Gatsby's
115
- // `static/`). Users drop NON-hashed files there, so widening to them would create
116
- // year-long false positives. Deployments of bundler output declare those dirs immutable
117
- // themselves via a `_headers` rule (e.g. `/assets/*` → `Cache-Control: public,
118
- // max-age=31536000, immutable`), which the publisher — a build we ran, an agent, a CI
119
- // deploy — writes because it KNOWS the output is content-hashed.
120
- export const GUARANTEED_IMMUTABLE_PREFIXES = [
121
- "_next/static/",
122
- "_app/immutable/",
123
- "_nuxt/",
124
- "_astro/",
125
- ];
126
- /** True when the path lives under a framework dir where every emitted asset is content-hashed. */
127
- function isUnderGuaranteedImmutablePrefix(filePath) {
128
- const normalized = filePath.startsWith("/") ? filePath.slice(1) : filePath;
129
- for (const prefix of GUARANTEED_IMMUTABLE_PREFIXES) {
130
- if (normalized.startsWith(prefix)) {
131
- return true;
132
- }
133
- }
134
- return false;
90
+ /**
91
+ * The token grammar a `_headers` rule accepts for a header NAME. The routing
92
+ * compiler enforces it; anything that GENERATES `_headers` rules (the Next
93
+ * materializer's framework sections) must gate on the same pattern, so a
94
+ * generated rule can never fail the author's publish.
95
+ */
96
+ export const HEADER_RULE_NAME_PATTERN = /^[A-Za-z0-9-]+$/;
97
+ /** `content-type` `Content-Type`: the casing compiled rules and generated `_headers` carry. */
98
+ export function canonicalHeaderName(input) {
99
+ return input
100
+ .trim()
101
+ .toLowerCase()
102
+ .split("-")
103
+ .map((part) => (part ? `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}` : part))
104
+ .join("-");
135
105
  }
106
+ const PHP_LIKE_EXTENSION_SET = new Set(PHP_LIKE_EXTENSIONS);
107
+ // Immutable-asset detection is NOT decided here: `compiled_cache_class` in
108
+ // crates/stattic-runtime-core (responses.rs) is the one implementation, at
109
+ // finalize. A TS re-implementation lived here once, drifted from the
110
+ // enforcer, and was deleted rather than re-synced.
136
111
  function lastSegment(filePath) {
137
112
  const slash = filePath.lastIndexOf("/");
138
113
  return slash >= 0 ? filePath.slice(slash + 1) : filePath;
@@ -152,15 +127,6 @@ export function pathStem(filePath) {
152
127
  export function isPhpLikeStaticPath(filePath) {
153
128
  return PHP_LIKE_EXTENSION_SET.has(pathExtension(filePath));
154
129
  }
155
- export function isImmutableAssetPath(filePath) {
156
- if (NON_IMMUTABLE_EXTENSION_SET.has(pathExtension(filePath))) {
157
- return false;
158
- }
159
- // Path-convention layer: anything under a guaranteed-content-hashed framework build
160
- // dir is immutable regardless of filename. The extension gate above still applies,
161
- // so an `index.html` accidentally under `_next/static/` is never pinned.
162
- return isUnderGuaranteedImmutablePrefix(filePath);
163
- }
164
130
  /** Forced inert serving policy for a committed static file. PHP-like files are never executable. */
165
131
  export function runtimeFileSafety(filePath) {
166
132
  return {
@@ -1,6 +1,6 @@
1
1
  export declare const STORAGE_OBJECT_MAX_FILE_BYTES: number;
2
- export declare const STORAGE_OBJECT_MAX_OWNER_BYTES: number;
3
2
  export declare const STORAGE_CONTENT_SNIFF_BYTES = 512;
3
+ export declare const STORAGE_ANON_DAILY_BYTES: number;
4
4
  export declare const STORAGE_BLOCKED_CONTENT_TYPES: string[];
5
5
  export declare const STORAGE_BLOCKED_CONTENT_TYPE_FRAGMENTS: string[];
6
6
  export declare const STORAGE_BLOCKED_MAGIC_HEX: string[];
@@ -5,8 +5,10 @@
5
5
  // `bun --filter @spacefast/control-plane runtime:codegen-policy`. Both execute
6
6
  // the same storage-policy.fixtures.json corpus.
7
7
  export const STORAGE_OBJECT_MAX_FILE_BYTES = 5 * 1024 * 1024;
8
- export const STORAGE_OBJECT_MAX_OWNER_BYTES = 100 * 1024 * 1024;
9
8
  export const STORAGE_CONTENT_SNIFF_BYTES = 512;
9
+ // Anonymous commenters share a per-space rolling daily upload budget; identified
10
+ // principals and Functions relays are accountable and carry no budget.
11
+ export const STORAGE_ANON_DAILY_BYTES = 200 * 1024 * 1024;
10
12
  export const STORAGE_BLOCKED_CONTENT_TYPES = [
11
13
  "application/java-archive",
12
14
  "application/java-vm",
@@ -0,0 +1,14 @@
1
+ export declare class StructuredError extends Error {
2
+ readonly code: string;
3
+ readonly details: Record<string, unknown> | undefined;
4
+ readonly exitCode: number;
5
+ readonly type: string | undefined;
6
+ constructor(input: {
7
+ code: string;
8
+ message: string;
9
+ details?: Record<string, unknown> | undefined;
10
+ exitCode?: number | undefined;
11
+ type?: string | undefined;
12
+ });
13
+ }
14
+ export declare function structuredValidationError(message: string, details?: Record<string, unknown>): StructuredError;