@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,6 +1,5 @@
1
1
  import { z } from "zod";
2
2
  import { serviceOrigin } from "../config/domains.js";
3
- import { normalizedSiteClass } from "./enums.js";
4
3
  import { GRANT_RESOURCE_PATTERN_LIMIT, normalizeGrantResourcePattern } from "./grants.js";
5
4
  export const SPACE_CONFIG_CANONICAL_FILE = "sf.jsonc";
6
5
  export const SPACE_CONFIG_ALIAS_FILES = [
@@ -15,8 +14,15 @@ export const SPACE_CONFIG_ACCEPTED_FILES = [
15
14
  SPACE_CONFIG_CANONICAL_FILE,
16
15
  ...SPACE_CONFIG_ALIAS_FILES,
17
16
  ];
17
+ /**
18
+ * Accepted config filenames in read precedence order, highest first. Kept in
19
+ * step with the finalizer's `CONFIG_ACCEPTED_FILES` (crates/stattic-runtime-core
20
+ * protocol.rs) — the runtime compile is the selection authority, and a local
21
+ * read choosing a different file than the publish serves is a split brain.
22
+ */
23
+ export const SPACE_CONFIG_PRECEDENCE = SPACE_CONFIG_ACCEPTED_FILES;
18
24
  export const SPACE_CONFIG_PRIVATE_FILES = [...SPACE_CONFIG_ACCEPTED_FILES];
19
- // Config compiler parser-safety caps (internal-docs/platform.md "Config
25
+ // Config compiler parser-safety caps (the platform spec "Config
20
26
  // compiler scale targets"): hard from launch, enforced by ONE shared validator
21
27
  // at every intake seam — finalize for the `sf.jsonc` file, request
22
28
  // validation for the API `config` overlay. Plan policy may LOWER these caps,
@@ -30,6 +36,7 @@ export const SPACE_CONFIG_CAPS = {
30
36
  templates: 100,
31
37
  metaTitleChars: 300,
32
38
  metaDescriptionChars: 1000,
39
+ /** Max OG image or favicon path/URL length; enough for signed URLs while staying request-safe. */
33
40
  metaImageChars: 2000,
34
41
  /** Max bytes per `inject` snippet. */
35
42
  injectSnippetBytes: 8 * 1024,
@@ -45,6 +52,10 @@ export const SPACE_CONFIG_CAPS = {
45
52
  themeNameChars: 120,
46
53
  /** Max top-level `name` (space display name) length. Matches the stored column. */
47
54
  nameChars: 255,
55
+ /** Max `crons` entries. One provider crontab slot each, so the cap is small and hard. */
56
+ crons: 8,
57
+ /** Max `crons[].path` length. */
58
+ cronPathChars: 512,
48
59
  };
49
60
  /** Published JSON Schema URL for `sf.jsonc` ($schema autocomplete). */
50
61
  export const SPACE_CONFIG_SCHEMA_URL = `${serviceOrigin("www", "prod")}/schemas/sf.json`;
@@ -126,16 +137,12 @@ export const spaceBuildSettingsSchema = z
126
137
  })
127
138
  .strict()
128
139
  .describe("Remote build settings used by repository builds.");
129
- // Reuses the same shared/dedicated vocabulary as `normalizedSiteClass`
130
- // (enums.ts) rather than declaring a second "shared"/"dedicated" enum
131
- // space placement mode and normalized site class are the same axis.
132
- export const spacePlacementModeSchema = normalizedSiteClass;
133
- // No region knob: a space binds to the site it was placed on at creation and
134
- // the only placement change that relocates it is shared -> dedicated. Data
135
- // location is chosen once, at creation, and never changes.
140
+ // Every space gets its own site, so there is nothing to place between: no mode,
141
+ // no region knob. `burstable` is the one real provider knob left whether the
142
+ // space's site may burst provider PHP workers. Data location is chosen once, at
143
+ // creation, and never changes.
136
144
  export const spacePlacementConfigSchema = z
137
145
  .object({
138
- mode: spacePlacementModeSchema.optional(),
139
146
  burstable: z.boolean().optional(),
140
147
  })
141
148
  .strict()
@@ -347,7 +354,7 @@ function refineDefaultDataSource(config, ctx) {
347
354
  });
348
355
  }
349
356
  }
350
- // Shared SpaceConfig fields (internal-docs/platform.md "SpaceConfig"): the
357
+ // Shared SpaceConfig fields (the platform spec "SpaceConfig"): the
351
358
  // declarative serving config also accepted by `sf.jsonc`. Cloud-only overlay
352
359
  // fields such as pre-build data sources are added to `spaceConfigSchema` below.
353
360
  // Meta caps are the spec parser-safety numbers (title 300, description 1000,
@@ -377,9 +384,10 @@ const spaceConfigBaseSchema = z.object({
377
384
  title: z.string().min(1).max(SPACE_CONFIG_CAPS.metaTitleChars).optional(),
378
385
  description: z.string().max(SPACE_CONFIG_CAPS.metaDescriptionChars).optional(),
379
386
  image: z.string().max(SPACE_CONFIG_CAPS.metaImageChars).optional(),
387
+ favicon: z.string().max(SPACE_CONFIG_CAPS.metaImageChars).optional(),
380
388
  })
381
389
  .optional()
382
- .describe("Listing/viewer/OG metadata."),
390
+ .describe("Listing, viewer, sharing, and browser icon metadata."),
383
391
  superpowers: z
384
392
  .object({
385
393
  enabled: z.boolean().optional().describe("Master switch for platform-managed injections."),
@@ -449,7 +457,7 @@ const spaceConfigBaseSchema = z.object({
449
457
  .optional()
450
458
  .describe("Accepted serving flag; runtime enforcement ships separately."),
451
459
  // Intentionally undescribed (hidden, experimental): enables the finalize-time
452
- // build pipeline (internal-docs/build-pipeline-plan.md). Accepted without
460
+ // build pipeline (the build-pipeline plan). Accepted without
453
461
  // diagnostics; absent/false keeps today's behavior byte-for-byte.
454
462
  experimental_gutenberg: z.boolean().optional(),
455
463
  // Intentionally minimally described (hidden, experimental): static HTML
@@ -489,7 +497,7 @@ export function spaceSuperpowersEnabled(config) {
489
497
  const superpowers = config?.superpowers;
490
498
  return superpowers?.enabled !== false && superpowers?.disable_all_injection !== true;
491
499
  }
492
- // The `sf.jsonc` FILE shape (internal-docs/platform.md "Space
500
+ // The `sf.jsonc` FILE shape (the platform spec "Space
493
501
  // Configuration Files"): SpaceConfig plus the editor `$schema` pointer and the
494
502
  // optional `space` link (slug or `spc_` id) resolved by CLI/publish flows.
495
503
  // Routing rules (`redirects`/`rewrites`/`headers`) are sf.jsonc keys too, but they belong
@@ -592,12 +600,17 @@ export const spaceRuntimeFunctionsSchema = z
592
600
  .string()
593
601
  .optional()
594
602
  .describe("Path to the worker entry module, relative to the published directory. " +
595
- "When absent, the server detects it automatically. Naming an entry that does not exist is an error, not a silent static publish."),
603
+ "When absent, the server detects it automatically. Naming an entry that does not exist is an error, not a silent file-only publish."),
596
604
  database: z
597
605
  .boolean()
598
606
  .default(false)
599
607
  .describe("Give the worker `env.DB`, a D1-shaped binding to this space's database. " +
600
608
  "This capability is declared, not detected. A capability an author did not request is authority they cannot see. Reading it from minified build output is a guess. Defaults to off, so a worker reaches nothing until it says so."),
609
+ fetch: z
610
+ .boolean()
611
+ .optional()
612
+ .describe("Give the worker `ctx.fetch`, brokered outbound HTTP through the relay. " +
613
+ "Declared, not detected, for the same reason as `database`. No schema default, because the right one depends on what was published: a Next build gets egress unless this says false — SSR fetches while rendering, so denying it breaks the build rather than narrowing it — while a hand-written worker reaches nothing until it says so."),
601
614
  compatibilityDate: z
602
615
  .string()
603
616
  .regex(/^\d{4}-\d{2}-\d{2}$/)
@@ -615,7 +628,154 @@ export const spaceRuntimeFunctionsSchema = z
615
628
  */
616
629
  export const spaceRuntimeSchema = z
617
630
  .discriminatedUnion("kind", [spaceRuntimeZeroSchema, spaceRuntimeFunctionsSchema])
618
- .describe("Runtime this version's code runs on. Omit it for a static space. Functions is also detected from the published directory when it is not stated.");
631
+ .describe("Runtime this version's code runs on. Omit it when the space does not run code. Functions is also detected from the published directory when it is not stated.");
632
+ /**
633
+ * Scheduled requests: the provider crontab asks this space for these paths on
634
+ * these schedules.
635
+ *
636
+ * File-only for the same reason `runtime` is: what runs on a schedule is a fact
637
+ * about the content of a version, so it rolls forward and back with the version
638
+ * instead of being set through the API config overlay.
639
+ *
640
+ * The Rust config compiler is the selection authority for both fields; this
641
+ * schema exists so an editor and an agent get the same answer before publish.
642
+ */
643
+ export const SPACE_CRON_NAMED_SCHEDULES = ["hourly", "daily", "twicedaily", "weekly"];
644
+ /** Field bounds, in crontab order: minute, hour, day of month, month, weekday. */
645
+ const CRON_FIELD_BOUNDS = [
646
+ [0, 59],
647
+ [0, 23],
648
+ [1, 31],
649
+ [1, 12],
650
+ [0, 7],
651
+ ];
652
+ /** How many times per hour, day or week the `Nh`/`Nd`/`Nw` shorthand may ask. */
653
+ const CRON_SHORTHAND_LIMITS = new Map([
654
+ ["h", 12],
655
+ ["d", 23],
656
+ ["w", 6],
657
+ ]);
658
+ const CRON_STEP_MAX = 0xffff_ffff;
659
+ function cronNumberInBounds(value, min, max) {
660
+ if (!/^\d+$/.test(value))
661
+ return false;
662
+ const parsed = Number(value);
663
+ return parsed >= min && parsed <= max;
664
+ }
665
+ function cronFieldIsValid(field, [min, max]) {
666
+ if (field.length === 0)
667
+ return false;
668
+ return field.split(",").every((item) => {
669
+ const slash = item.indexOf("/");
670
+ const range = slash === -1 ? item : item.slice(0, slash);
671
+ const step = slash === -1 ? undefined : item.slice(slash + 1);
672
+ // A step only makes sense over a span, which is what `*` and `a-b` are;
673
+ // `5/2` names one value and then asks to skip within it.
674
+ let span;
675
+ if (range === "*") {
676
+ span = true;
677
+ }
678
+ else if (range.includes("-")) {
679
+ const [start, end, ...rest] = range.split("-");
680
+ if (rest.length > 0 ||
681
+ start === undefined ||
682
+ end === undefined ||
683
+ !cronNumberInBounds(start, min, max) ||
684
+ !cronNumberInBounds(end, min, max) ||
685
+ Number(start) > Number(end)) {
686
+ return false;
687
+ }
688
+ span = true;
689
+ }
690
+ else {
691
+ if (!cronNumberInBounds(range, min, max))
692
+ return false;
693
+ span = false;
694
+ }
695
+ if (step === undefined)
696
+ return true;
697
+ const parsedStep = Number(step);
698
+ return (span &&
699
+ /^\d+$/.test(step) &&
700
+ Number.isInteger(parsedStep) &&
701
+ parsedStep >= 1 &&
702
+ parsedStep <= CRON_STEP_MAX);
703
+ });
704
+ }
705
+ /**
706
+ * The wp.cloud schedule grammar: a named interval, an `Nh`/`Nd`/`Nw` count, or
707
+ * a five-field crontab expression. No `@daily` macros and no month/day names —
708
+ * the provider does not accept them either.
709
+ */
710
+ export function isSpaceCronSchedule(schedule) {
711
+ if (SPACE_CRON_NAMED_SCHEDULES.some((named) => named === schedule))
712
+ return true;
713
+ const shorthand = /^(\d+)([hdw])$/.exec(schedule);
714
+ const limit = CRON_SHORTHAND_LIMITS.get(shorthand?.[2] ?? "");
715
+ if (shorthand?.[1] !== undefined && limit !== undefined) {
716
+ const count = Number(shorthand[1]);
717
+ return count >= 1 && count <= limit;
718
+ }
719
+ const fields = schedule.split(" ");
720
+ return (fields.length === CRON_FIELD_BOUNDS.length &&
721
+ fields.every((field, index) => {
722
+ const bounds = CRON_FIELD_BOUNDS[index];
723
+ return bounds !== undefined && cronFieldIsValid(field, bounds);
724
+ }));
725
+ }
726
+ /**
727
+ * The artifact key for a cron path: lowercase, leading `/` stripped, every run
728
+ * of non-alphanumerics collapsed to one `-`, trimmed. Two paths that produce
729
+ * one key are rejected, so a key names exactly one schedule.
730
+ */
731
+ export function spaceCronKey(path) {
732
+ return path
733
+ .replace(/^\/+/, "")
734
+ .toLowerCase()
735
+ .replace(/[^a-z0-9]+/g, "-")
736
+ .replace(/^-+|-+$/g, "");
737
+ }
738
+ /** Generated runtime file the serving engine reads, written only when a version declares a cron. */
739
+ export const SPACE_CRONS_ARTIFACT_PATH = "__spacefast/crons.json";
740
+ export const SPACE_CRONS_ARTIFACT_VERSION = 1;
741
+ export const spaceCronSchema = z.object({
742
+ path: z
743
+ .string()
744
+ .max(SPACE_CONFIG_CAPS.cronPathChars)
745
+ .regex(/^\/[^*:?#\s\\]*$/, "must be a literal path starting with / (no * splats, :placeholders, query or fragment)")
746
+ .refine((path) => spaceCronKey(path).length > 0, "must contain at least one letter or digit")
747
+ .describe("Path this space is asked for on the schedule."),
748
+ schedule: z
749
+ .string()
750
+ .refine(isSpaceCronSchedule, "must be hourly, daily, twicedaily, weekly, an Nh/Nd/Nw interval, or a five-field crontab expression")
751
+ .describe("hourly, daily, twicedaily, weekly, an Nh/Nd/Nw interval, or a five-field crontab expression."),
752
+ });
753
+ export const spaceCronsSchema = z
754
+ .array(spaceCronSchema)
755
+ .max(SPACE_CONFIG_CAPS.crons)
756
+ .superRefine((entries, context) => {
757
+ // Identical paths always slug to one key, so keying on the key alone
758
+ // catches both defects: a path written twice, and two paths that slug to
759
+ // one key — which would give two schedules one identity downstream.
760
+ const seenByKey = new Map();
761
+ entries.forEach((entry, index) => {
762
+ const key = spaceCronKey(entry.path);
763
+ const at = seenByKey.get(key);
764
+ if (at !== undefined) {
765
+ // Name the exact duplicate the author wrote: the path when it repeats
766
+ // verbatim, the shared key when two distinct paths collide on it.
767
+ const collided = entries[at];
768
+ context.addIssue({
769
+ code: "custom",
770
+ path: [index, "path"],
771
+ message: `crons entry ${at} already declares ${collided?.path === entry.path ? entry.path : key}.`,
772
+ });
773
+ return;
774
+ }
775
+ seenByKey.set(key, index);
776
+ });
777
+ })
778
+ .describe(`Scheduled requests Spacefast issues against this space's own paths; at most ${SPACE_CONFIG_CAPS.crons} entries.`);
619
779
  export const spaceConfigFileSchema = spaceConfigBaseSchema.extend({
620
780
  $schema: z
621
781
  .string()
@@ -637,6 +797,7 @@ export const spaceConfigFileSchema = spaceConfigBaseSchema.extend({
637
797
  .describe("Display name for this space. While set, the API refuses to rename the space."),
638
798
  access: spaceConfigFileAccessSchema.optional(),
639
799
  runtime: spaceRuntimeSchema.optional(),
800
+ crons: spaceCronsSchema.optional(),
640
801
  });
641
802
  /**
642
803
  * The published JSON Schema artifact for `sf.jsonc`, generated from the
@@ -662,6 +823,19 @@ export function spaceConfigJsonSchema() {
662
823
  },
663
824
  };
664
825
  }
826
+ // The `!` and `name=:capture` of a `_redirects` line, shared by both rule
827
+ // sections — the same shapes `config::strict::public_json_schema` publishes.
828
+ const SPACE_CONFIG_ROUTING_FORCE_SCHEMA = {
829
+ type: "boolean",
830
+ default: false,
831
+ description: "Answer even when the request path resolves to a committed file — the ! of a _redirects rule.",
832
+ };
833
+ const SPACE_CONFIG_ROUTING_QUERY_SCHEMA = {
834
+ type: "object",
835
+ description: 'Query parameters the request must carry, as parameter name to capture name: { "id": "id" } is the name=:capture token id=:id.',
836
+ propertyNames: { pattern: "^[A-Za-z][A-Za-z0-9_-]*$" },
837
+ additionalProperties: { type: "string", pattern: "^[A-Za-z][A-Za-z0-9_]*$" },
838
+ };
665
839
  const SPACE_CONFIG_ROUTING_SCHEMA_PROPERTIES = {
666
840
  redirects: {
667
841
  type: "array",
@@ -684,6 +858,8 @@ const SPACE_CONFIG_ROUTING_SCHEMA_PROPERTIES = {
684
858
  default: 302,
685
859
  description: "Redirect status. To serve other content under this URL, use rewrites.",
686
860
  },
861
+ force: SPACE_CONFIG_ROUTING_FORCE_SCHEMA,
862
+ query: SPACE_CONFIG_ROUTING_QUERY_SCHEMA,
687
863
  },
688
864
  required: ["source", "destination"],
689
865
  additionalProperties: false,
@@ -709,6 +885,13 @@ const SPACE_CONFIG_ROUTING_SCHEMA_PROPERTIES = {
709
885
  const: "shared",
710
886
  description: "Let shared caches store the proxy response. Absolute-URL destinations only.",
711
887
  },
888
+ force: SPACE_CONFIG_ROUTING_FORCE_SCHEMA,
889
+ query: SPACE_CONFIG_ROUTING_QUERY_SCHEMA,
890
+ notFound: {
891
+ type: "boolean",
892
+ default: false,
893
+ description: "Serve the destination with status 404 — the 404 of a _redirects rule. The destination must be a path.",
894
+ },
712
895
  },
713
896
  required: ["source", "destination"],
714
897
  additionalProperties: false,
@@ -716,7 +899,7 @@ const SPACE_CONFIG_ROUTING_SCHEMA_PROPERTIES = {
716
899
  },
717
900
  headers: {
718
901
  type: "array",
719
- description: "Response headers to set on matching requests. A _headers rule wins on the same header name.",
902
+ description: "Response headers to set or remove on matching requests. A _headers rule wins on the same header name.",
720
903
  items: {
721
904
  type: "object",
722
905
  properties: {
@@ -725,13 +908,29 @@ const SPACE_CONFIG_ROUTING_SCHEMA_PROPERTIES = {
725
908
  type: "array",
726
909
  minItems: 1,
727
910
  items: {
728
- type: "object",
729
- properties: {
730
- key: { type: "string", minLength: 1 },
731
- value: { type: "string" },
732
- },
733
- required: ["key", "value"],
734
- additionalProperties: false,
911
+ oneOf: [
912
+ {
913
+ type: "object",
914
+ properties: {
915
+ key: { type: "string", minLength: 1 },
916
+ value: { type: "string" },
917
+ },
918
+ required: ["key", "value"],
919
+ additionalProperties: false,
920
+ },
921
+ {
922
+ type: "object",
923
+ properties: {
924
+ key: { type: "string", minLength: 1 },
925
+ remove: {
926
+ const: true,
927
+ description: "Remove this header instead of setting it — the !Name line of a _headers block.",
928
+ },
929
+ },
930
+ required: ["key", "remove"],
931
+ additionalProperties: false,
932
+ },
933
+ ],
735
934
  },
736
935
  },
737
936
  },
@@ -741,7 +940,7 @@ const SPACE_CONFIG_ROUTING_SCHEMA_PROPERTIES = {
741
940
  },
742
941
  };
743
942
  /*
744
- * Hidden-feature discipline (internal-docs/build-pipeline-plan.md): the
943
+ * Hidden-feature discipline (the build-pipeline plan): the
745
944
  * `experimental_gutenberg` flag and its `inject` companion are ACCEPTED by the
746
945
  * parser but never published — the bare key names alone would document the
747
946
  * feature to every editor/agent reading a schema artifact. Every generator
@@ -765,14 +964,14 @@ export function stripHiddenSpaceConfigKeys(schema) {
765
964
  if (name === "experimental_gutenberg") {
766
965
  return false;
767
966
  }
768
- return !(name === "inject" && isHiddenInjectShape(value));
967
+ return !(name === "inject" && isHiddenInjectSchema(value));
769
968
  }));
770
969
  return [key, stripHiddenSpaceConfigKeys(kept)];
771
970
  }
772
971
  return [key, stripHiddenSpaceConfigKeys(entry)];
773
972
  }));
774
973
  }
775
- function isHiddenInjectShape(value) {
974
+ function isHiddenInjectSchema(value) {
776
975
  if (!value || typeof value !== "object" || Array.isArray(value)) {
777
976
  return false;
778
977
  }