@spacefast/common 0.0.21 → 0.0.24

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 (182) hide show
  1. package/dist/agents/connect-targets.d.ts +9 -6
  2. package/dist/agents/connect-targets.js +9 -6
  3. package/dist/config/domains.d.ts +1 -1
  4. package/dist/config/domains.js +1 -0
  5. package/dist/config/index.d.ts +0 -1
  6. package/dist/config/index.js +0 -1
  7. package/dist/contracts/abuse.d.ts +2 -0
  8. package/dist/contracts/abuse.js +12 -0
  9. package/dist/contracts/access.d.ts +121 -9
  10. package/dist/contracts/access.js +73 -9
  11. package/dist/contracts/activity.d.ts +1 -1
  12. package/dist/contracts/activity.js +0 -9
  13. package/dist/contracts/api-keys.d.ts +21 -21
  14. package/dist/contracts/beta.js +1 -1
  15. package/dist/contracts/billing.d.ts +1 -34
  16. package/dist/contracts/billing.js +4 -53
  17. package/dist/contracts/builds.d.ts +8 -8
  18. package/dist/contracts/builds.js +10 -3
  19. package/dist/contracts/cli.d.ts +10 -0
  20. package/dist/contracts/cli.js +10 -0
  21. package/dist/contracts/collab-public.d.ts +63 -0
  22. package/dist/contracts/collab-public.js +46 -0
  23. package/dist/contracts/comments.d.ts +283 -54
  24. package/dist/contracts/comments.js +173 -52
  25. package/dist/contracts/common.d.ts +3 -3
  26. package/dist/contracts/device-auth.d.ts +6 -6
  27. package/dist/contracts/device-auth.js +1 -1
  28. package/dist/contracts/docs.d.ts +5 -5
  29. package/dist/contracts/domains.d.ts +36 -36
  30. package/dist/contracts/enums.d.ts +33 -30
  31. package/dist/contracts/enums.js +52 -38
  32. package/dist/contracts/error-code-meta.d.ts +37 -126
  33. package/dist/contracts/error-code-meta.js +12 -41
  34. package/dist/contracts/error-codes.d.ts +1 -1
  35. package/dist/contracts/error-codes.js +10 -32
  36. package/dist/contracts/execution.d.ts +35 -57
  37. package/dist/contracts/execution.js +41 -40
  38. package/dist/contracts/feature-lifecycle-core.d.ts +0 -6
  39. package/dist/contracts/feature-lifecycle-core.js +2 -31
  40. package/dist/contracts/feature-lifecycle.d.ts +8 -38
  41. package/dist/contracts/feature-lifecycle.js +16 -48
  42. package/dist/contracts/features.d.ts +15 -13
  43. package/dist/contracts/features.js +39 -38
  44. package/dist/contracts/functions.d.ts +97 -70
  45. package/dist/contracts/functions.js +101 -34
  46. package/dist/contracts/git.d.ts +1 -1
  47. package/dist/contracts/grants.d.ts +7 -3
  48. package/dist/contracts/grants.js +18 -9
  49. package/dist/contracts/internal.d.ts +14 -1
  50. package/dist/contracts/internal.js +17 -7
  51. package/dist/contracts/oauth-resources.d.ts +4 -3
  52. package/dist/contracts/oauth-resources.js +12 -3
  53. package/dist/contracts/operations.d.ts +90 -5
  54. package/dist/contracts/operations.js +75 -1
  55. package/dist/contracts/pages.d.ts +6 -1
  56. package/dist/contracts/pages.js +24 -1
  57. package/dist/contracts/plan-policy.d.ts +1 -3
  58. package/dist/contracts/plan-policy.js +1 -2
  59. package/dist/contracts/platform.d.ts +61 -148
  60. package/dist/contracts/platform.js +70 -126
  61. package/dist/contracts/publish-archive.d.ts +498 -0
  62. package/dist/contracts/publish-archive.js +25 -0
  63. package/dist/contracts/resources.d.ts +12 -24
  64. package/dist/contracts/resources.js +32 -12
  65. package/dist/contracts/runtime-api.d.ts +369 -175
  66. package/dist/contracts/runtime-api.js +372 -348
  67. package/dist/contracts/runtime-app.d.ts +127 -120
  68. package/dist/contracts/runtime-app.js +19 -10
  69. package/dist/contracts/runtime-db.d.ts +1 -1
  70. package/dist/contracts/runtime-services.d.ts +219 -0
  71. package/dist/contracts/runtime-services.js +437 -0
  72. package/dist/contracts/runtime-storage.d.ts +45 -32
  73. package/dist/contracts/runtime-storage.js +30 -8
  74. package/dist/contracts/space-config.d.ts +10 -16
  75. package/dist/contracts/space-config.js +14 -10
  76. package/dist/contracts/spaces.d.ts +241 -436
  77. package/dist/contracts/spaces.js +128 -145
  78. package/dist/contracts/superadmin-activity.d.ts +57 -4
  79. package/dist/contracts/superadmin-activity.js +48 -2
  80. package/dist/contracts/superadmin-search.d.ts +126 -0
  81. package/dist/contracts/superadmin-search.js +61 -0
  82. package/dist/contracts/superadmin-spaces.d.ts +358 -89
  83. package/dist/contracts/superadmin-spaces.js +97 -24
  84. package/dist/contracts/superadmin-teams.d.ts +28 -10
  85. package/dist/contracts/superadmin-teams.js +5 -8
  86. package/dist/contracts/superadmin.d.ts +0 -32
  87. package/dist/contracts/superadmin.js +3 -11
  88. package/dist/contracts/tags.d.ts +9 -9
  89. package/dist/contracts/teams.d.ts +44 -2
  90. package/dist/contracts/teams.js +43 -23
  91. package/dist/contracts/transfers.d.ts +0 -49
  92. package/dist/contracts/transfers.js +2 -6
  93. package/dist/contracts/usage.d.ts +61 -0
  94. package/dist/contracts/usage.js +64 -0
  95. package/dist/contracts/variables.d.ts +2 -7
  96. package/dist/contracts/variables.js +6 -5
  97. package/dist/contracts/webhooks.d.ts +2 -2
  98. package/dist/contracts/zero.d.ts +87 -56
  99. package/dist/contracts/zero.js +82 -20
  100. package/dist/dashboard-paths/index.d.ts +1 -1
  101. package/dist/dashboard-paths/index.js +1 -0
  102. package/dist/docs/agent-prose.d.ts +1 -1
  103. package/dist/docs/agent-prose.js +62 -66
  104. package/dist/docs/agent-setup.js +49 -23
  105. package/dist/docs/agent-solutions.d.ts +4 -4
  106. package/dist/docs/agent-solutions.js +8 -8
  107. package/dist/docs/catalog.d.ts +19 -19
  108. package/dist/docs/catalog.js +30 -19
  109. package/dist/docs/error-docs.js +63 -151
  110. package/dist/docs/skills.d.ts +3 -3
  111. package/dist/docs/skills.js +1 -1
  112. package/dist/test-helpers/fetch-stub.d.ts +16 -0
  113. package/dist/test-helpers/fetch-stub.js +24 -0
  114. package/dist/utils/build-settings.js +9 -3
  115. package/dist/utils/canonical-json.d.ts +10 -0
  116. package/dist/utils/canonical-json.js +21 -0
  117. package/dist/utils/cast-transport.d.ts +28 -0
  118. package/dist/utils/cast-transport.js +52 -0
  119. package/dist/utils/claim-token.d.ts +0 -8
  120. package/dist/utils/claim-token.js +3 -15
  121. package/dist/utils/credential-policy.d.ts +1 -1
  122. package/dist/utils/env-file.d.ts +17 -0
  123. package/dist/utils/env-file.js +96 -0
  124. package/dist/utils/error-code.d.ts +2 -0
  125. package/dist/utils/error-code.js +7 -0
  126. package/dist/utils/generate-space-name.d.ts +8 -10
  127. package/dist/utils/generate-space-name.js +206 -21
  128. package/dist/utils/grant-decision.fixtures.json +2938 -0
  129. package/dist/utils/grants.d.ts +2 -0
  130. package/dist/utils/grants.js +25 -2
  131. package/dist/utils/gravatar.d.ts +9 -0
  132. package/dist/utils/gravatar.js +31 -0
  133. package/dist/utils/idempotency.d.ts +6 -0
  134. package/dist/utils/idempotency.js +28 -0
  135. package/dist/utils/local-space-state.d.ts +1 -5
  136. package/dist/utils/local-space-state.js +7 -26
  137. package/dist/utils/page-fonts.d.ts +2 -2
  138. package/dist/utils/page-fonts.js +22 -4
  139. package/dist/utils/pages.d.ts +1 -2
  140. package/dist/utils/pages.js +21 -5
  141. package/dist/utils/publish-form-data.d.ts +12 -0
  142. package/dist/utils/publish-form-data.js +10 -0
  143. package/dist/utils/publish-policy.d.ts +20 -0
  144. package/dist/utils/publish-policy.fixtures.json +397 -0
  145. package/dist/utils/publish-policy.js +36 -19
  146. package/dist/utils/push-new.d.ts +7 -0
  147. package/dist/utils/push-new.js +7 -0
  148. package/dist/utils/query-keys.d.ts +0 -1
  149. package/dist/utils/query-keys.js +0 -1
  150. package/dist/utils/runtime-paths.d.ts +5 -0
  151. package/dist/utils/runtime-paths.js +5 -0
  152. package/dist/utils/runtime-upload-batch.d.ts +0 -5
  153. package/dist/utils/runtime-upload-batch.js +1 -27
  154. package/dist/utils/runtime-upload.d.ts +3 -13
  155. package/dist/utils/runtime-upload.js +4 -14
  156. package/dist/utils/space-config.d.ts +0 -33
  157. package/dist/utils/space-config.js +5 -326
  158. package/dist/utils/storage-policy.d.ts +1 -1
  159. package/dist/utils/storage-policy.js +3 -1
  160. package/dist/utils/structured-error.d.ts +14 -0
  161. package/dist/utils/structured-error.js +22 -0
  162. package/dist/utils/version-path.js +12 -3
  163. package/dist/vocabulary.d.ts +5 -3
  164. package/dist/vocabulary.js +13 -10
  165. package/package.json +10 -5
  166. package/dist/config/postgres-budget.d.ts +0 -129
  167. package/dist/config/postgres-budget.js +0 -140
  168. package/dist/config/postgres-budget.json +0 -51
  169. package/dist/contracts/archives.d.ts +0 -331
  170. package/dist/contracts/archives.js +0 -120
  171. package/dist/contracts/deployments.d.ts +0 -574
  172. package/dist/contracts/deployments.js +0 -97
  173. package/dist/contracts/sites.d.ts +0 -416
  174. package/dist/contracts/sites.js +0 -168
  175. package/dist/contracts/superadmin-queues.d.ts +0 -414
  176. package/dist/contracts/superadmin-queues.js +0 -202
  177. package/dist/contracts/superadmin-runtime.d.ts +0 -27
  178. package/dist/contracts/superadmin-runtime.js +0 -38
  179. package/dist/index.d.ts +0 -1
  180. package/dist/index.js +0 -1
  181. package/dist/utils/api.d.ts +0 -4
  182. package/dist/utils/api.js +0 -13
@@ -2,38 +2,27 @@ import { z } from "zod";
2
2
  import { accessFenceKindSchema } from "./access.js";
3
3
  import { functionsRuntimeConfigSchema } from "./functions.js";
4
4
  import { grantSchema } from "./grants.js";
5
- import { conventionFilesSchema, manifestFileSchema } from "./internal.js";
5
+ import { manifestFileSchema } from "./internal.js";
6
6
  import { runtimePagesSchema } from "./pages.js";
7
+ import { SPACE_CONFIG_CAPS } from "./space-config.js";
7
8
  import { zeroEndpointSchema, zeroRuntimeConfigSchema } from "./zero.js";
8
- const runtimeConventionFilesSchema = conventionFilesSchema.omit({ routes: true }).strict();
9
9
  const hostCanonicalRedirectSchema = z.object({
10
10
  from: z.string().min(1),
11
11
  to: z.string().url(),
12
12
  status: z.union([z.literal(301), z.literal(302), z.literal(307), z.literal(308)]).default(308),
13
13
  });
14
- // Immutable version hosts carry their own target version: the intent is the
15
- // COMPLETE declarative hostname map for the space (every retained ready
16
- // version), so a route update can never orphan an older version URL.
14
+ // The intent is the COMPLETE declarative hostname map for the space (every
15
+ // retained ready version), so a route update can never orphan a version URL.
17
16
  const versionHostnameEntrySchema = z.object({
18
17
  hostname: z.string().min(1),
19
18
  version_id: z.string().min(1),
20
19
  });
21
- // Future expansion: this intent could support explicit path-to-artifact composition
22
- // after independently deployable components have a real product model.
23
20
  const runtimeRouteIntentSchema = z.object({
24
21
  production_hostnames: z.array(z.string().min(1)).default([]),
25
22
  noindex_production_hostnames: z.array(z.string().min(1)).default([]),
26
23
  version_hostnames: z.array(versionHostnameEntrySchema).default([]),
27
24
  host_canonical_redirects: z.array(hostCanonicalRedirectSchema).default([]),
28
25
  });
29
- // Changed-path purge contract (internal-docs/platform.md "Cache Management"):
30
- // finalize computes the changed-path set against the previous pointer target
31
- // — expanded to canonical request paths (leading slash, directory-index
32
- // variants included) — and carries it through the runtime callback journal;
33
- // the callback receiver translates journal events into edge purges scoped to
34
- // changed paths, never a blanket purge when a diff exists. Past this cap (or
35
- // when no previous pointer target exists) the control plane omits the set and
36
- // purge handling falls back to one host-wide purge.
37
26
  export const RUNTIME_CHANGED_PATHS_MAX = 900;
38
27
  const changedPathsSchema = z
39
28
  .array(z
@@ -43,23 +32,58 @@ const changedPathsSchema = z
43
32
  .describe("Canonical request path (decoded, leading slash) whose served bytes changed."))
44
33
  .max(RUNTIME_CHANGED_PATHS_MAX)
45
34
  .optional()
46
- .describe("Request paths changed from the previous pointer target. When absent, no diff is available and purge handling falls back to a host-wide purge. Echoed verbatim into the `route_updated` journal event.");
35
+ .describe("Request paths changed from the previous pointer target. When absent, the runtime purges the whole domain.");
47
36
  const previousVersionIdSchema = z
48
37
  .string()
49
38
  .min(1)
50
39
  .optional()
51
40
  .describe("The pointer target this activation replaces (diff baseline).");
41
+ export const runtimePurgeReceiptSchema = z.object({
42
+ status: z.enum(["ok", "failed", "pending"]),
43
+ mode: z.enum(["urls", "domain"]),
44
+ urls: z.number().int().nonnegative().optional(),
45
+ });
46
+ const runtimeMutationPurgeShape = {
47
+ purge: runtimePurgeReceiptSchema.optional(),
48
+ };
49
+ export const runtimeSpacePurgeRequestSchema = z.object({
50
+ urls: z.array(z.string().startsWith("/")).optional(),
51
+ });
52
+ export const runtimeSpacePurgeResponseSchema = z.object({
53
+ space_id: z.string().min(1),
54
+ purge: runtimePurgeReceiptSchema,
55
+ });
56
+ // One bounded page of a version's file catalog — the runtime's own answer to
57
+ // "which files does this version have". `view=source` is the uploaded,
58
+ // pre-substitution object; `view=served` is the immutable byte a visitor
59
+ // receives, and `variant_route` marks a channel's substituted bytes within it.
60
+ export const runtimeVersionFilesResponseSchema = z.object({
61
+ space_id: z.string().min(1),
62
+ version_id: z.string().min(1),
63
+ view: z.enum(["source", "served"]),
64
+ files: z.array(z.object({
65
+ path: z.string().min(1),
66
+ size: z.number().int().nonnegative(),
67
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
68
+ content_type: z.string().min(1).optional(),
69
+ public: z.boolean(),
70
+ variant_route: z.string().min(1).optional(),
71
+ })),
72
+ next_cursor: z.string().min(1).nullable().optional(),
73
+ });
52
74
  export const runtimeVersionCreateRequestSchema = z.object({
53
75
  version_id: z.string().min(1).optional(),
54
76
  files: z.array(manifestFileSchema),
55
- retained_files: z.array(manifestFileSchema).default([]),
77
+ // Retention is expressed as "everything from `reusable_version_id`": naming
78
+ // the base and NO list is the retain-all signal, which the runtime
79
+ // materializes from that version's own catalog at finalize (under the space
80
+ // lock). An explicit list is the narrower form — it retains exactly these
81
+ // paths — and is what a publish that prunes the base uses.
82
+ retained_files: z.array(manifestFileSchema).optional(),
56
83
  reusable_version_id: z.string().min(1).optional(),
57
- // Session state stores the upload truth (spec "Upload Contract"): the version's
58
- // manifest hash and the draft expiry so resume answers come from runtime state.
59
84
  manifest_hash: z.string().optional(),
60
85
  expires_at: z.string().optional(),
61
86
  metadata: z.record(z.string(), z.unknown()).default({}),
62
- convention_files: runtimeConventionFilesSchema.default({}),
63
87
  });
64
88
  export const runtimeVersionCreateResponseSchema = z.object({
65
89
  space_id: z.string(),
@@ -75,26 +99,19 @@ export const runtimeUploadSessionDescriptorSchema = z.object({
75
99
  manifest_hash: z.string().optional(),
76
100
  files: z.array(manifestFileSchema),
77
101
  // Retained files only travel in the authenticated finalize request: they are
78
- // unbounded and unused while accepting a PUT.
102
+ // unbounded and unused while accepting a PUT. Omitting them beside a
103
+ // `reusable_version_id` is the retain-all signal (see the create request),
104
+ // which is also what keeps a lazy session's descriptor inside its JWT budget.
79
105
  retained_files: z.array(manifestFileSchema).optional(),
80
106
  reusable_version_id: z.string().min(1).optional(),
81
107
  metadata: z.record(z.string(), z.unknown()).default({}),
82
- convention_files: runtimeConventionFilesSchema.default({}),
83
108
  });
84
- // Candidate budget for the half that rides in the upload JWT (everything
85
- // except the retained manifest). This bounds signing work, but is not itself a
86
- // header-size proof: callback capabilities also ride in auto-finalize tokens.
87
109
  export const RUNTIME_UPLOAD_SESSION_DESCRIPTOR_MAX_BYTES = 4096;
88
110
  // Hard wire budget for the complete `Authorization: Bearer <jwt>\r\n` line sent
89
111
  // on every PUT. Lazy-session selection measures the signed line and falls back
90
112
  // to eager runtime creation rather than returning an oversized credential.
91
113
  export const RUNTIME_UPLOAD_AUTHORIZATION_HEADER_MAX_BYTES = 8 * 1024;
92
- // Budget for the whole descriptor, which is persisted on the version row and
93
- // replayed inside the durable finalize receipt.
94
114
  export const RUNTIME_UPLOAD_SESSION_STORED_MAX_BYTES = 256 * 1024;
95
- // Runtime-truth upload session state (spec "Upload Contract"): the resume flow asks
96
- // the runtime which declared files/chunks are actually present instead of guessing
97
- // from control-plane pending-upload state.
98
115
  export const runtimeUploadSessionResponseSchema = z.object({
99
116
  space_id: z.string(),
100
117
  version_id: z.string(),
@@ -110,12 +127,7 @@ export const runtimeUploadSessionResponseSchema = z.object({
110
127
  })),
111
128
  // Declared paths the runtime has not committed yet.
112
129
  pending_paths: z.array(z.string()),
113
- // Staged chunked-upload parts by canonical path (contiguity is checked at complete).
114
- chunks: z.record(z.string(), z.array(z.number().int().positive())),
115
130
  });
116
- // Route-pointer config is pure serving policy plus the anonymous claim
117
- // countdown. Per-version serving behavior (index/fallback/listing) remains a
118
- // version artifact concern.
119
131
  export const visitorJwkSchema = z.object({
120
132
  kty: z.literal("OKP"),
121
133
  crv: z.literal("Ed25519"),
@@ -136,20 +148,13 @@ export const runtimePublicExposureDescriptorSchema = z.object({
136
148
  unmodeled: z.string(),
137
149
  });
138
150
  // Everything the runtime needs to render the visitor access page itself.
139
- // Presentation-only: which lanes exist is derived runtime-side from the
140
- // projected grants; this descriptor carries what the grants cannot — the
141
- // space's display identity, the central endpoints each lane talks to, and the
142
- // deterministic credential that authenticates the runtime's own
143
- // server-to-server exchange calls (password, verified email, request-invite)
144
- // so the central authority may trust the visitor context the runtime forwards.
145
151
  export const runtimeAccessPageDescriptorSchema = z
146
152
  .object({
147
153
  displayName: z.string().min(1).max(200).nullable(),
148
154
  // API-origin account lane (`/v1/access/acquire/<target>`), null until the
149
155
  // space is claimed. Doubles as the silent-SSO probe target.
150
156
  accountUrl: z.string().url().nullable(),
151
- // Identity connections rendered as SSO lanes. The runtime appends
152
- // `host`/`return` to `startUrl` at render time.
157
+ // The runtime appends `host`/`return` to `startUrl` at render time.
153
158
  connections: z.array(z.object({
154
159
  id: z.string().min(1).max(64),
155
160
  label: z.string().min(1).max(200),
@@ -158,15 +163,11 @@ export const runtimeAccessPageDescriptorSchema = z
158
163
  exchange: z
159
164
  .object({
160
165
  passwordUrl: z.string().url(),
161
- // Browser entry links and integration credentials are exchanged over
162
- // separate runtime-authenticated lanes. Neither credential reaches
163
- // customer code.
164
166
  linkUrl: z.string().url(),
165
167
  tokenUrl: z.string().url(),
166
168
  emailUrl: z.string().url(),
167
169
  requestUrl: z.string().url(),
168
170
  logoutUrl: z.string().url(),
169
- commentsConfigUrl: z.string().url().optional(),
170
171
  commentsTicketUrl: z.string().url().optional(),
171
172
  commentsVersionUrlsUrl: z.string().url().optional(),
172
173
  zeroRealtimeTicketUrl: z.string().url().optional(),
@@ -175,17 +176,11 @@ export const runtimeAccessPageDescriptorSchema = z
175
176
  .nullable(),
176
177
  })
177
178
  .strict();
178
- // Runtime route artifacts are compiled into an authority-keyed index. Keep a
179
- // hard ceiling as a last-resort safety boundary for route-apply CPU, artifact
180
- // size, and a compromised producer; indexing, not this limit, owns hot-path
181
- // performance.
182
179
  export const RUNTIME_AUTHORIZATION_GRANT_LIMIT = 1_024;
183
180
  export const runtimeAuthorizationProjectionSchema = z
184
181
  .object({
185
182
  generation: z.number().int().nonnegative(),
186
- // Browser sessions follow their still-live authority references across
187
- // ordinary policy/ownership projections. Only an explicit logout-all
188
- // rotation invalidates every otherwise-valid session.
183
+ // Only an explicit logout-all rotation invalidates otherwise-valid sessions.
189
184
  sessionVersion: z.number().int().nonnegative(),
190
185
  fence: accessFenceKindSchema,
191
186
  acquireUrl: z.string().url(),
@@ -193,48 +188,78 @@ export const runtimeAuthorizationProjectionSchema = z
193
188
  spaceClaimed: z.boolean(),
194
189
  grants: z
195
190
  .array(grantSchema.extend({
196
- // The durable browser session records this generation for the exact
197
- // authority it acquired. Rotating or editing one credential Grant
198
- // therefore removes only that authority from the session.
191
+ // The browser session records this per acquired authority, so rotating
192
+ // one Grant removes only that authority from the session.
199
193
  generation: z.number().int().positive(),
200
194
  }))
201
195
  .max(RUNTIME_AUTHORIZATION_GRANT_LIMIT),
202
196
  })
203
197
  .strict();
198
+ // The space-level Comments projection the runtime serves as the whole answer
199
+ // for `/__spacefast/comments/config`; the runtime adds only per-request fields.
200
+ const runtimeSdkCommentsSchema = z.object({
201
+ // Comments boot on the live/published host.
202
+ live: z.boolean(),
203
+ // ...and on an immutable version (preview) host.
204
+ preview: z.boolean(),
205
+ // Null before the Space has ever published.
206
+ live_url: z.string().url().max(2000).nullable(),
207
+ theme: z.object({
208
+ accent: z
209
+ .string()
210
+ .regex(/^#[0-9a-fA-F]{6}$/)
211
+ .nullable(),
212
+ hide_branding: z.boolean(),
213
+ }),
214
+ features: z.object({
215
+ picker: z.boolean(),
216
+ drawing: z.boolean(),
217
+ capture: z.boolean(),
218
+ attachments: z.boolean(),
219
+ notices: z.boolean(),
220
+ }),
221
+ });
222
+ const runtimeSdkProjectionSchema = z.object({
223
+ // Content-derived identity, used to version the injected
224
+ // `/__spacefast/sdk.js?v=` URL.
225
+ revision: z.string().min(1).max(256),
226
+ config: z.object({
227
+ cast_api_base: z.string().url().max(2000).nullable().optional(),
228
+ cast_ws_url: z.string().url().max(2000).nullable().optional(),
229
+ cast_resource_key: z.string().min(1).max(256).nullable().optional(),
230
+ comments: runtimeSdkCommentsSchema.nullable().optional(),
231
+ }),
232
+ // Production tag JavaScript, served inside the same loader response so
233
+ // private Spaces never fetch an anonymously readable per-Space artifact.
234
+ body: z
235
+ .string()
236
+ .max(5 * 1024 * 1024)
237
+ .nullable()
238
+ .optional(),
239
+ });
204
240
  const runtimeRouteConfigSchema = z.object({
205
- // Canonical scoped admission projection. When present it is the sole serving
206
- // authority; the legacy rule document below is ignored.
241
+ // The sole serving admission authority when present.
207
242
  authorization: runtimeAuthorizationProjectionSchema.nullable().optional(),
208
- // Digest of every input that can change anonymous/public exposure (canonical
209
- // authorization boundaries, content types, proxy entitlement, and
210
- // unknown serving keys). Authority-only rotation deliberately leaves it
211
- // stable so Link/Person changes do not block on an unnecessary edge purge.
243
+ // Digest of every input that can change anonymous/public exposure.
244
+ // Authority-only rotation deliberately leaves it stable, so Link/Person
245
+ // changes do not block on an unnecessary edge purge.
212
246
  public_exposure_digest: z
213
247
  .string()
214
248
  .regex(/^[a-f0-9]{64}$/)
215
249
  .optional(),
216
- // The digest detects equality; the descriptor preserves enough structure for
217
- // the callback consumer to distinguish widening from narrowing without
218
- // reconstructing a previous config from mutable control-plane state.
250
+ // The digest detects equality; the descriptor lets the consumer distinguish
251
+ // widening from narrowing.
219
252
  public_exposure: runtimePublicExposureDescriptorSchema.optional(),
220
- // Monotonic authorization projection generation. Short-lived handoffs are
221
- // bound to this exact generation, Space, and host. Browser sessions use the
222
- // projection's independent sessionVersion revocation floor.
253
+ // Short-lived handoffs are bound to this exact generation, Space, and host.
223
254
  projection_generation: z.number().int().nonnegative().nullable().optional(),
224
- // First-party visitor-token verification is explicit.
225
255
  visitor_issuer: z.string().min(1).optional(),
226
256
  visitor_jwks: visitorJwksSchema.optional(),
227
- // Anonymous claim-window countdown (ISO timestamp), compiled into serving
228
- // state so the runtime can render the expiry-rescue banner for visitors
229
- // holding the claim-link viewer session. Null/absent once claimed.
257
+ // Anonymous claim-window countdown (ISO timestamp). Null/absent once claimed.
230
258
  anonymous_expires_at: z.string().nullable().optional(),
231
- // Serve-time content-type allowlist. Generic serving policy: the engine has
232
- // no notion of WHY a file type is blocked — it refuses (403, blocked_message
259
+ // Serve-time content-type allowlist: the engine refuses (403, blocked_message
233
260
  // body) any file whose stored Content-Type matches no pattern. Patterns are
234
- // exact types or `prefix/*` wildcards ("text/*"). The control plane pushes
235
- // it for unclaimed anonymous spaces (no opaque binaries pre-claim); explicit
236
- // null clears the stored policy — route config is rebuilt wholesale, so a
237
- // claim clears it structurally on the next sync.
261
+ // exact types or `prefix/*` wildcards ("text/*"). Explicit null clears the
262
+ // stored policy.
238
263
  content_types: z
239
264
  .object({
240
265
  allowed: z.array(z.string().min(1)),
@@ -242,28 +267,18 @@ const runtimeRouteConfigSchema = z.object({
242
267
  })
243
268
  .nullable()
244
269
  .optional(),
245
- // Per-space admission-control override (plan §5/§25; contract A1): the
246
- // in-flight concurrency limit the engine enforces on uncacheable requests
247
- // for this space, superseding SPACEFAST_UNCACHEABLE_CONC_PER_SPACE. Ops-only
248
- // in V1 (set via the superadmin admission endpoint); absent means the
249
- // env-knob default applies.
270
+ // In-flight concurrency limit the engine enforces on uncacheable requests for
271
+ // this space, superseding SPACEFAST_UNCACHEABLE_CONC_PER_SPACE. Absent means
272
+ // the env-knob default applies.
250
273
  admission: z.object({ concurrency: z.number().int().positive() }).optional(),
251
- // Same-host production tag JavaScript. It rides the mutable route projection
252
- // so private Spaces never fetch an anonymously readable per-Space artifact.
253
- tag_sdk: z
254
- .object({
255
- revision: z.string().min(1),
256
- body: z.string().max(5 * 1024 * 1024),
257
- })
258
- .nullable()
259
- .optional(),
260
- // Serve-time plan entitlements (proxy-routes.md: "the rule stays in your
261
- // config and activates the moment you upgrade — no redeploy needed"). Stored
262
- // per space beside `policy`; a `planGated` compiled proxy rule
263
- // (packages/routing plan.ts) is checked against this doc at REQUEST time by
264
- // runtime/redirects.php, never baked into the compiled artifact. Explicit
265
- // null clears the stored doc (falls back to fail-closed); absence leaves it
266
- // untouched.
274
+ // Projection for the same-host loader at `/__spacefast/sdk.js`, and the
275
+ // source of the space overlay the runtime answers
276
+ // `/__spacefast/comments/config` from.
277
+ sdk: runtimeSdkProjectionSchema.nullable().optional(),
278
+ // Serve-time plan entitlements. A `planGated` compiled proxy rule is checked
279
+ // against this doc at REQUEST time, never baked into the compiled artifact.
280
+ // Explicit null clears the stored doc (falls back to fail-closed); absence
281
+ // leaves it untouched.
267
282
  entitlements: z
268
283
  .object({
269
284
  externalProxy: z.boolean(),
@@ -271,11 +286,9 @@ const runtimeRouteConfigSchema = z.object({
271
286
  .nullable()
272
287
  .optional(),
273
288
  });
274
- // Compiled SpaceConfig serving primitives (spec "Serving resolution"): the finalize
275
- // compiler resolves content defaults -> sf.jsonc -> space overlay into this shape;
276
- // the runtime stores it on the version's serving artifact and never re-infers. When
277
- // absent, the runtime infers content defaults (root index.html => website-style,
278
- // otherwise artifact-style listing + viewer).
289
+ // Compiled SpaceConfig serving primitives. When absent, the runtime infers
290
+ // content defaults (root index.html => website-style, otherwise artifact-style
291
+ // listing + viewer).
279
292
  const runtimeServingConfigSchema = z.object({
280
293
  index: z
281
294
  .union([z.string().min(1), z.literal(false)])
@@ -304,36 +317,34 @@ const runtimeServingConfigSchema = z.object({
304
317
  .describe("Generate the single-file viewer page when a version has exactly one public file."),
305
318
  meta: z
306
319
  .object({
307
- title: z.string().max(300).optional().describe("Listing, viewer, and OG title."),
308
- description: z.string().max(1000).optional().describe("Listing, viewer, and OG description."),
309
- image: z.string().max(2000).optional().describe("OG image path."),
320
+ title: z
321
+ .string()
322
+ .max(SPACE_CONFIG_CAPS.metaTitleChars)
323
+ .optional()
324
+ .describe("Listing, viewer, and OG title."),
325
+ description: z
326
+ .string()
327
+ .max(SPACE_CONFIG_CAPS.metaDescriptionChars)
328
+ .optional()
329
+ .describe("Listing, viewer, and OG description."),
330
+ image: z.string().max(SPACE_CONFIG_CAPS.metaImageChars).optional().describe("OG image path."),
331
+ favicon: z
332
+ .string()
333
+ .max(SPACE_CONFIG_CAPS.metaImageChars)
334
+ .optional()
335
+ .describe("Favicon path."),
310
336
  })
311
337
  .nullable()
312
338
  .optional()
313
- .describe("Finalize-resolved listing, viewer, and OG metadata (effective SpaceConfig metadata). This value wins over session metadata."),
339
+ .describe("Finalize-resolved listing, viewer, sharing, and browser icon metadata (effective SpaceConfig metadata). This value wins over session metadata."),
314
340
  pages: runtimePagesSchema.optional(),
315
- // Hidden experimental build-pipeline flag: absent on every version finalized
316
- // without it (byte-identical payload to today). Only ever sent as true.
341
+ // Hidden experimental build-pipeline flag. Only ever sent as true.
317
342
  experimental_gutenberg: z.boolean().optional(),
318
- // Platform-owned head metadata injection for normal HTML finalization. This is
319
- // distinct from the hidden user build-pipeline flag: it only enables missing
320
- // title/description/OG/Twitter tags derived from finalized SpaceConfig meta.
343
+ // Platform-owned head metadata injection: enables missing title,
344
+ // description, favicon, OG, and Twitter tags from finalized SpaceConfig meta.
321
345
  platform_meta: z.boolean().optional(),
322
- // Platform-owned same-host SDK bootstrap metadata. The public Cast base is
323
- // finalized with the version so locally-created remote runtimes do not guess
324
- // between local/prod; resource identity is also per-version because one
325
- // runtime/Atomic can serve many spaces.
326
- spacefast_sdk: z
327
- .object({
328
- cast_api_base: z.string().url().max(2000).nullable().optional(),
329
- cast_ws_url: z.string().url().max(2000).nullable().optional(),
330
- cast_resource_key: z.string().min(1).max(256).nullable().optional(),
331
- published_comments: z.boolean().optional(),
332
- })
333
- .optional(),
334
- // CP-validated inject snippets (<= 8 KB each, <= 16 per array); sent only
335
- // when experimental_gutenberg is true. Char caps here are a sanity bound —
336
- // the byte caps are enforced by the shared SpaceConfig validator.
346
+ // Sent only when experimental_gutenberg is true. The char caps here are a
347
+ // sanity bound; the byte caps live in the shared SpaceConfig validator.
337
348
  inject: z
338
349
  .object({
339
350
  head: z.array(z.string().max(8192)).max(16).optional(),
@@ -375,61 +386,49 @@ const runtimeZeroRunSchema = z.object({
375
386
  capabilities: runtimeZeroEndpointCapabilitiesSchema.optional(),
376
387
  db: z.record(z.string(), z.unknown()).optional(),
377
388
  });
378
- // Gated-activation mode (plan §9, Placement V2): whether a version/import
379
- // installs as immediately live ("active") or staged pending an explicit
380
- // activation call ("activating", the engine's own fail-safe default when
381
- // absent). Shared by finalize and import — do not re-inline this enum.
382
- export const runtimeZeroModeSchema = z.enum(["active", "activating"]);
389
+ // One variable scope as finalize resolved it. Secrets are NEVER resolved: a
390
+ // secret match at any scope fails the publish in the control plane, so the
391
+ // runtime receives the flag and no value.
392
+ const runtimeVariableScopeSchema = z.object({
393
+ kind: z.enum(["space", "principal"]),
394
+ values: z.record(z.string().min(1), z.object({
395
+ value: z.string().optional(),
396
+ secret: z.boolean(),
397
+ channelValues: z.record(z.string().min(1), z.string()).optional(),
398
+ })),
399
+ });
400
+ const runtimeSubstitutionChannelSchema = z.object({
401
+ name: z.string().min(1).max(128),
402
+ route_name: z
403
+ .string()
404
+ .min(1)
405
+ .max(128)
406
+ .regex(/^[A-Za-z0-9._-]+$/)
407
+ .describe("Runtime route name this channel's variant files serve under."),
408
+ });
383
409
  export const runtimeVersionFinalizeRequestSchema = z.object({
384
410
  upload_id: z.string().min(1),
385
411
  session: runtimeUploadSessionDescriptorSchema.optional(),
386
- zero_mode: runtimeZeroModeSchema.optional(),
387
412
  zero: zeroRuntimeConfigSchema.optional(),
388
413
  // Dispatch configuration for a Functions version. Absent for static versions,
389
- // and absent when a worker was published while no execution host is
390
- // configured — the version then serves as pure static content, because a
391
- // config is the only thing that makes the origin dispatch at all.
414
+ // and absent when no execution host is configured the version then serves
415
+ // as pure static content, because this config is what makes the origin
416
+ // dispatch at all.
392
417
  functions: functionsRuntimeConfigSchema.optional(),
393
- convention_files: runtimeConventionFilesSchema.optional(),
394
- // Finalize-time `{{ vars.NAME }}` substitution output (spec "Variable
395
- // rules"): substituted bytes replace the served files; the engine keeps the
396
- // pre-substitution bytes in `files-original/` so declared-sha dedup still
397
- // verifies upload identity. Keyed by canonical version-relative path.
398
- template_files: z
399
- .record(z.string().min(1).describe("Canonical committed path declared in `templates`."), z
400
- .string()
401
- .max(2 * 1024 * 1024)
402
- .describe("Substituted file contents (secret values never reach this payload)."))
403
- .refine((files) => Object.keys(files).length <= 100, {
404
- message: "template_files supports up to 100 entries.",
405
- })
406
- .optional()
407
- .describe("Substituted template contents applied over the committed tree at finalize."),
408
- // Per-channel template variants (spec "Per-channel values"): channelValues
409
- // overrides compile channel-specific contents of declared template files as
410
- // channel-scoped serving artifacts. Keyed by RUNTIME ROUTE NAME (route names
411
- // are runtime-only vocabulary; the live channel serves via `production`).
412
- // Variants serve only on the route's hosts — permanent Version URLs always
413
- // serve the version's own finalize-resolved values.
414
- template_variants: z
415
- .record(z
416
- .string()
417
- .min(1)
418
- .max(128)
419
- .regex(/^[A-Za-z0-9._-]+$/)
420
- .describe("Runtime route name this variant set serves."), z
421
- .record(z.string().min(1).describe("Canonical committed path declared in `templates`."), z
422
- .string()
423
- .max(2 * 1024 * 1024)
424
- .describe("Variant file contents (secret values never reach this payload)."))
425
- .refine((files) => Object.keys(files).length <= 100, {
426
- message: "template_variants supports up to 100 entries per route.",
427
- }))
428
- .refine((routes) => Object.keys(routes).length <= 8, {
429
- message: "template_variants supports up to 8 routes.",
430
- })
431
- .optional()
432
- .describe("Channel-variant template contents, keyed by runtime route name."),
418
+ // Variable resolution is the control plane's half of substitution and stays
419
+ // there: scopes come from its database, secret values are never decrypted for
420
+ // it, and `SPACEFAST_*` names are platform-provided. The finalizer owns the
421
+ // substitution itself — it has the staged bytes and the config that declares
422
+ // which paths are templates, so nothing pre-substituted crosses this wire.
423
+ variable_scopes: z.array(runtimeVariableScopeSchema).optional(),
424
+ system_variables: z.record(z.string().min(1), z.string()).optional(),
425
+ // The space's channels, so the finalizer can compile each one's variant files
426
+ // under the route name that serves them.
427
+ channels: z.array(runtimeSubstitutionChannelSchema).max(8).optional(),
428
+ // Hostnames this space answers on, which convention compilation needs to tell
429
+ // an internal redirect from an external proxy. Only the control plane knows
430
+ // the assignment.
431
+ routing_assigned_hostnames: z.array(z.string().min(1)).optional(),
433
432
  // Finalize-rendered Pages documents stored outside the public file tree.
434
433
  page_artifacts: z
435
434
  .record(z.string().min(1).max(240), z.string().max(2 * 1024 * 1024))
@@ -437,18 +436,24 @@ export const runtimeVersionFinalizeRequestSchema = z.object({
437
436
  message: "page_artifacts supports up to 100,000 rendered documents.",
438
437
  })
439
438
  .optional(),
439
+ // Serving inputs only the control plane can produce. The compiled routing
440
+ // rules are NOT here: the finalizer compiles `_redirects`, `_headers` and the
441
+ // config's routing sections from the staged files itself.
440
442
  serving: z
441
443
  .object({
442
- headers_exact: z.record(z.string(), z.array(z.record(z.string(), z.unknown()))).default({}),
443
- headers_pattern: z.array(z.record(z.string(), z.unknown())).default([]),
444
- redirects_exact: z.record(z.string(), z.array(z.record(z.string(), z.unknown()))).default({}),
445
- redirects_pattern: z.array(z.record(z.string(), z.unknown())).default([]),
444
+ noindexHost: z.boolean().optional(),
446
445
  config: runtimeServingConfigSchema.optional(),
447
- // Trusted Zero endpoint source compiled by the runtime into private
448
- // endpoint-specific JS, QuickJS bytecode, and PHP manifest invoke actions.
446
+ // Origins the platform's own browser code loads from, keyed by the CSP
447
+ // directive each belongs in. Which origins those are is a control-plane
448
+ // decision — they come from its environment and are validated against the
449
+ // host this version will serve from — while the merge itself belongs to
450
+ // the finalizer, which is the only side that sees every CSP the version
451
+ // sets, in `_headers` and in config rules alike.
452
+ platform_csp_sources: z
453
+ .record(z.string().min(1).max(64), z.array(z.string().min(1).max(512)).max(16))
454
+ .optional(),
449
455
  zero_endpoints: z.array(runtimeZeroEndpointSchema).max(128).optional(),
450
- // Trusted Zero run-handler source compiled into private artifacts for
451
- // non-HTTP Zero execution surfaces. These are not PHP route actions.
456
+ // Non-HTTP Zero execution surfaces; not PHP route actions.
452
457
  zero_runs: z.array(runtimeZeroRunSchema).max(128).optional(),
453
458
  })
454
459
  .optional(),
@@ -460,10 +465,8 @@ export const runtimeVersionFinalizeRequestSchema = z.object({
460
465
  .default("production")
461
466
  .describe("Runtime route-pointer to activate; route names are runtime-only vocabulary."),
462
467
  config: runtimeRouteConfigSchema.optional(),
463
- // Same contract as the route PUT's config_digest: stored with the
464
- // pointer so later conditional route PUTs can no-op against it.
468
+ // Same contract as the route PUT's config_digest.
465
469
  config_digest: z.string().min(1).optional(),
466
- changed_paths: changedPathsSchema,
467
470
  previous_version_id: previousVersionIdSchema,
468
471
  production_hostnames: runtimeRouteIntentSchema.shape.production_hostnames,
469
472
  noindex_production_hostnames: runtimeRouteIntentSchema.shape.noindex_production_hostnames,
@@ -486,23 +489,79 @@ export const runtimeReadinessTargetSchema = z.object({
486
489
  path: z.string().startsWith("/"),
487
490
  expected_statuses: z.array(z.union([runtimePublicReadinessStatusSchema, z.literal(404)])).min(1),
488
491
  });
492
+ // The scalars the control plane stores instead of a file list. Both digests are
493
+ // over the version's own catalog: `source` folds path → uploaded sha (two
494
+ // publishes of identical bytes agree even when serving config differs),
495
+ // `served` folds what the version actually answers with, per channel included.
496
+ export const runtimeCatalogDigestsSchema = z.object({
497
+ source: z.string().regex(/^sha256:[a-f0-9]{64}$/),
498
+ served: z.string().regex(/^sha256:[a-f0-9]{64}$/),
499
+ });
500
+ // What this publish changed against the version it supersedes. The runtime
501
+ // diffs two catalogs it already holds; the control plane persists the three
502
+ // counts for the changelog and forwards nothing.
503
+ export const runtimeCatalogDeltaSchema = z.object({
504
+ added: z.number().int().nonnegative(),
505
+ changed: z.number().int().nonnegative(),
506
+ removed: z.number().int().nonnegative(),
507
+ });
508
+ // One compiled diagnostic from the finalizer. `path` is the committed file it
509
+ // points at, carrying `file:line` when the compiler knew a line; `details` is
510
+ // what it points at INSIDE that file — the variable, the channel, the colliding
511
+ // routes — which is the difference between a publish failure you can act on and
512
+ // one you can only retry.
513
+ export const runtimeCompileDiagnosticSchema = z.object({
514
+ severity: z.enum(["info", "warning", "error"]),
515
+ code: z.string().min(1),
516
+ message: z.string(),
517
+ path: z.string().optional(),
518
+ details: z.record(z.string(), z.unknown()).optional(),
519
+ });
520
+ // What the version's conventions and config compiled TO. The counts are the
521
+ // control plane's version-row projection; `proxy_rules` is what it overlays the
522
+ // publishing team's plan onto, because the compiled artifact never bakes a plan
523
+ // verdict.
524
+ export const runtimeCompiledRoutingSchema = z.object({
525
+ redirect_rule_count: z.number().int().nonnegative(),
526
+ header_rule_count: z.number().int().nonnegative(),
527
+ proxy_rule_count: z.number().int().nonnegative(),
528
+ proxy_rules: z
529
+ .array(z.object({ source: z.string(), destination: z.string() }))
530
+ .max(1000)
531
+ .default([]),
532
+ });
489
533
  export const runtimeVersionFinalizeResponseSchema = z.object({
490
534
  space_id: z.string(),
491
535
  version_id: z.string(),
492
536
  status: z.literal("ready"),
493
- // Runtime-compiled truth from zero/endpoints-index.json. Optional so older
494
- // runtimes remain parseable during rolling deploys.
495
537
  zero_endpoint_count: z.number().int().nonnegative().optional(),
496
- // Canonical runtime-committed manifest (path, size, streaming sha256).
497
- // Returned by both first and idempotent finalize responses so recovery
498
- // never needs an already-consumed upload session.
538
+ // The finalizer is the one conventions/config compiler, so what it compiled is
539
+ // reported back rather than recomputed: the control plane runs no second
540
+ // compile pass to learn its own version row.
541
+ diagnostics: z.array(runtimeCompileDiagnosticSchema).default([]),
542
+ routing: runtimeCompiledRoutingSchema.optional(),
543
+ // Provenance for the substitution the finalizer performed: dependency name
544
+ // (`NAME` or `NAME@channel`) -> SHA-256 of the value it resolved to. This is
545
+ // what answers "does changing this variable require a republish?".
546
+ variable_digests: z.record(z.string().min(1), z.string()).default({}),
547
+ system_variable_dependencies: z.array(z.string().min(1)).default([]),
548
+ // Canonical runtime-committed manifest, returned by both first and idempotent
549
+ // finalize responses so recovery never needs a consumed upload session.
499
550
  manifest: z.array(manifestFileSchema).optional(),
551
+ catalog_digests: runtimeCatalogDigestsSchema.optional(),
552
+ // Absent when this version supersedes nothing, or when the previous version
553
+ // predates the catalog.
554
+ delta: runtimeCatalogDeltaSchema.optional(),
555
+ // The served public image the space card shows for this version, chosen from
556
+ // the catalog at finalize. Null when the version serves no image.
557
+ preview_image_path: z.string().min(1).nullable().optional(),
500
558
  readiness_target: runtimeReadinessTargetSchema,
501
559
  activation_event_id: z
502
560
  .string()
503
561
  .min(1)
504
562
  .optional()
505
563
  .describe("Stable route_updated event id when finalize also activated a route."),
564
+ ...runtimeMutationPurgeShape,
506
565
  });
507
566
  export const runtimeZeroMigrationsApplyResponseSchema = z.object({
508
567
  space_id: z.string(),
@@ -512,12 +571,12 @@ export const runtimeZeroMigrationsApplyResponseSchema = z.object({
512
571
  export const runtimeRouteUpdateRequestSchema = z.object({
513
572
  version_id: z.string().min(1),
514
573
  config: runtimeRouteConfigSchema.optional(),
515
- // Canonical digest of `config` (control-plane routeConfigDigest). The engine
516
- // stores it with the route pointer and no-ops the whole PUT no policy
517
- // store, no journal event, no edge purge — when version, digest, and
574
+ // The engine stores this with the route pointer and no-ops the whole PUT — no
575
+ // policy store, no journal event, no edge purge when version, digest, and
518
576
  // hostname intent all match the stored route. Absent = always write.
519
577
  config_digest: z.string().min(1).optional(),
520
- changed_paths: changedPathsSchema,
578
+ // No `changed_paths`: the runtime holds both catalogs and scopes the purge
579
+ // from `previous_version_id` itself.
521
580
  previous_version_id: previousVersionIdSchema,
522
581
  production_hostnames: runtimeRouteIntentSchema.shape.production_hostnames,
523
582
  noindex_production_hostnames: runtimeRouteIntentSchema.shape.noindex_production_hostnames,
@@ -535,36 +594,36 @@ export const runtimeRouteUpdateResponseSchema = z.object({
535
594
  route_name: z.string(),
536
595
  version_id: z.string(),
537
596
  // True when the engine skipped the write because version_id, config_digest,
538
- // and hostname intent all matched the stored route: nothing was journaled,
539
- // so callers skip the event drain too.
597
+ // and hostname intent all matched: nothing was journaled, so callers skip the
598
+ // event drain too.
540
599
  unchanged: z.boolean().optional(),
541
600
  activation_event_id: z
542
601
  .string()
543
602
  .min(1)
544
603
  .optional()
545
604
  .describe("Stable route_updated event id for this exact route command, including retries."),
605
+ ...runtimeMutationPurgeShape,
546
606
  });
547
607
  export const runtimeHostnameIntentUpdateResponseSchema = z.object({
548
608
  space_id: z.string(),
549
609
  route_count: z.number().int().nonnegative(),
610
+ ...runtimeMutationPurgeShape,
550
611
  });
551
612
  export const runtimeTombstonesUpdateRequestSchema = z.object({
552
613
  hostnames: runtimeRouteIntentSchema.shape.production_hostnames,
553
614
  mode: z.enum(["replace", "add", "remove"]).default("replace"),
554
- // Optional reason/category drive a differentiated tombstone variant on the
555
- // runtime (e.g. csam -> bare 404, copyright -> 451, suspension -> 402). The
556
- // runtime falls back to a generic 404 when both are absent, so these are
557
- // additive and back-compatible.
615
+ // Select a differentiated tombstone variant (csam -> bare 404, copyright ->
616
+ // 451, suspension -> 402); absent means a generic 404.
558
617
  reason: z.string().optional(),
559
618
  category: z.string().optional(),
560
619
  });
561
620
  export const runtimeTombstonesUpdateResponseSchema = z.object({
562
621
  space_id: z.string(),
563
622
  tombstone_count: z.number().int().nonnegative(),
623
+ ...runtimeMutationPurgeShape,
564
624
  });
565
- // Retention/pruning policy artifact (Placement V2 tiering, plan §8/§26): the
566
- // control plane computes prunable version ids; the engine stores the list and
567
- // its housekeeping tick prunes trees after re-checking live route pointers.
625
+ // The control plane computes prunable version ids; the engine stores the list
626
+ // and its housekeeping tick prunes trees after re-checking live route pointers.
568
627
  // An empty list clears the policy.
569
628
  export const runtimeRetentionPolicyUpdateRequestSchema = z.object({
570
629
  prunable_version_ids: z.array(z.string()),
@@ -572,127 +631,93 @@ export const runtimeRetentionPolicyUpdateRequestSchema = z.object({
572
631
  export const runtimeRetentionPolicyUpdateResponseSchema = z.object({
573
632
  space_id: z.string(),
574
633
  prunable_count: z.number().int().nonnegative(),
634
+ ...runtimeMutationPurgeShape,
575
635
  });
576
636
  export const runtimeSpaceDeleteResponseSchema = z.object({
577
637
  space_id: z.string(),
578
638
  status: z.literal("deleted"),
639
+ ...runtimeMutationPurgeShape,
579
640
  });
580
641
  export const runtimeVersionDeleteResponseSchema = z.object({
581
642
  space_id: z.string(),
582
643
  version_id: z.string(),
583
644
  status: z.literal("deleted"),
645
+ ...runtimeMutationPurgeShape,
584
646
  });
585
647
  export const runtimeSpaceRepairResponseSchema = z.object({
586
648
  space_id: z.string(),
587
649
  status: z.literal("repaired"),
588
- });
589
- const runtimeExportImportStatusSchema = z.enum([
590
- "waiting_for_archive",
591
- "pending",
592
- "running",
593
- "complete",
650
+ ...runtimeMutationPurgeShape,
651
+ });
652
+ // A pin freezes a set of blob SHAs against the collector for as long as it
653
+ // lives. `snapshot` pins everything a set of versions plus space storage
654
+ // currently reference; `shas` pins an explicit list. `expires_at` is the
655
+ // engine's hard backstop, so a mover that dies mid-flight never wedges the
656
+ // collector.
657
+ const runtimeSha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
658
+ export const runtimePinCreateRequestSchema = z.union([
659
+ z.object({
660
+ mode: z.literal("snapshot"),
661
+ version_ids: z.array(z.string().min(1)),
662
+ include_uploads: z.boolean().default(true),
663
+ ttl_seconds: z
664
+ .number()
665
+ .int()
666
+ .positive()
667
+ .max(24 * 60 * 60),
668
+ }),
669
+ z.object({
670
+ mode: z.literal("shas"),
671
+ shas: z.array(runtimeSha256Schema).max(100_000),
672
+ ttl_seconds: z
673
+ .number()
674
+ .int()
675
+ .positive()
676
+ .max(24 * 60 * 60),
677
+ }),
594
678
  ]);
595
- export const runtimeSpaceExportStartRequestSchema = z
596
- .object({
597
- version_ids: z.array(z.string().min(1)).min(1).optional(),
598
- // include_files=false produces a metadata-only archive that cannot be
599
- // imported as a serving Space. Recorded in stattic.json `includes`.
600
- include_files: z.boolean().optional(),
601
- })
602
- .strict();
603
- export const runtimeSpaceExportResponseSchema = z.object({
604
- type: z.literal("space_export"),
605
- export_id: z.string(),
606
- space_id: z.string(),
607
- status: runtimeExportImportStatusSchema,
608
- cursor: z.number().int().nonnegative(),
609
- processed_files: z.number().int().nonnegative(),
610
- total_files: z.number().int().nonnegative(),
611
- version_ids: z.array(z.string()),
612
- archive_size: z.number().int().nonnegative().optional(),
613
- // SHA-256 of the finished archive (64 lowercase hex), computed once at
614
- // completion so the control plane can expose SpaceExport.archive.sha256.
615
- archive_sha256: z.string().optional(),
616
- created_at: z.string(),
617
- updated_at: z.string(),
618
- completed_at: z.string().optional(),
619
- });
620
- export const runtimeSpaceImportStartRequestSchema = z
621
- .object({
622
- version_id_map: z.record(z.string(), z.string()).optional(),
623
- // Plan §9 gated activation, extended to import (I-4): shares
624
- // runtimeZeroModeSchema with runtimeVersionFinalizeRequestSchema.
625
- // Absent/anything other than 'active' is the engine's own fail-safe
626
- // 'activating'.
627
- zero_mode: runtimeZeroModeSchema.optional(),
628
- })
629
- .strict();
630
- // Deferred version-id mapping: control-plane-driven uploads land on the
631
- // runtime before the control plane has read the archive, so the freshly
632
- // minted target ids (and, for upload-sourced imports, zero_mode) are
633
- // supplied here before the first step.
634
- export const runtimeSpaceImportMapRequestSchema = z
635
- .object({
636
- version_id_map: z.record(z.string(), z.string()),
637
- zero_mode: runtimeZeroModeSchema.optional(),
638
- })
639
- .strict();
640
- export const runtimeSpaceImportResponseSchema = z.object({
641
- type: z.literal("space_import"),
642
- import_id: z.string(),
643
- space_id: z.string(),
644
- status: runtimeExportImportStatusSchema,
645
- cursor: z.number().int().nonnegative(),
646
- processed_files: z.number().int().nonnegative(),
647
- total_files: z.number().int().nonnegative(),
648
- version_ids: z.array(z.string()),
649
- created_at: z.string(),
650
- updated_at: z.string(),
651
- completed_at: z.string().optional(),
652
- });
653
- const runtimeTransferBundleFileSchema = z.object({
654
- path: z.string().min(1),
655
- content_b64: z.string(),
656
- });
657
- export const runtimeTransferBundleSourceRequestSchema = z.object({
679
+ export const runtimePinCreateResponseSchema = z.object({
658
680
  space_id: z.string().min(1),
659
- version_ids: z.array(z.string().min(1)),
660
- cursor: z.string().nullable(),
661
- include_serving_state: z.boolean().optional(),
681
+ pin_id: z.string().min(1),
682
+ expires_at: z.string().min(1),
683
+ sha_count: z.number().int().nonnegative(),
662
684
  });
663
- export const runtimeTransferBundleTargetRequestSchema = z.object({
685
+ export const runtimePinReleaseResponseSchema = z.object({
664
686
  space_id: z.string().min(1),
665
- store: z.literal(true),
666
- files: z.array(runtimeTransferBundleFileSchema),
687
+ pin_id: z.string().min(1),
688
+ released: z.boolean(),
689
+ });
690
+ // Path-blind on purpose: the response names only the SHAs the space does not
691
+ // already hold.
692
+ export const runtimeBlobsHaveRequestSchema = z.object({
693
+ shas: z.array(runtimeSha256Schema),
694
+ });
695
+ export const runtimeBlobsHaveResponseSchema = z.object({
696
+ missing: z.array(runtimeSha256Schema),
697
+ });
698
+ export const runtimeBlobPutResponseSchema = z.object({
699
+ sha256: runtimeSha256Schema,
700
+ size: z.number().int().nonnegative(),
701
+ stored: z.boolean(),
702
+ });
703
+ // The mirror of the paged export lane: pages replay in the same table/ordering
704
+ // against a schema the target already migrated. Import is idempotent on primary
705
+ // key, so a retried page is not a duplicate row.
706
+ export const runtimeZeroDbImportRequestSchema = z.object({
707
+ schema_hash: z.string().min(1),
708
+ table: z.string().min(1),
709
+ rows: z.array(z.record(z.string(), z.unknown())).max(1_000),
667
710
  done: z.boolean(),
668
711
  });
669
- export const runtimeTransferBundleResponseSchema = z.object({
670
- chunk: z.object({ files: z.array(runtimeTransferBundleFileSchema) }),
671
- next_cursor: z.string().nullable(),
672
- });
673
- export const runtimeTransferBundleStoredResponseSchema = z.object({
674
- stored: z.literal(true),
675
- });
676
- export const runtimeTransferCommitRequestSchema = z.object({
677
- space_id: z.string().min(1),
678
- version_ids: z.array(z.string().min(1)),
679
- live_version_id: z.string().nullable(),
680
- install_serving_state: z.boolean().optional(),
681
- });
682
- export const runtimeTransferCommitResponseSchema = z.object({
683
- installed: z.literal(true),
684
- });
685
- export const runtimeTransferAbortRequestSchema = z.object({
712
+ export const runtimeZeroDbImportResponseSchema = z.object({
686
713
  space_id: z.string().min(1),
687
- });
688
- export const runtimeTransferAbortResponseSchema = z.object({
689
- aborted: z.boolean().optional(),
714
+ version_id: z.string().min(1),
715
+ table: z.string().min(1),
716
+ imported: z.number().int().nonnegative(),
690
717
  });
691
718
  // Per-space generation state reported by the authed state endpoint. The
692
- // reconciliation sweep compares this against control-plane expectations
693
- // (space liveVersionId vs the runtime's route pointers) and repairs targeted
694
- // mismatches — never a blanket re-sync (internal-docs/platform.md "Cache
695
- // Management": missed callbacks degrade to bounded staleness).
719
+ // reconciliation sweep compares it against control-plane expectations and
720
+ // repairs targeted mismatches never a blanket re-sync.
696
721
  export const runtimeSpaceStateSchema = z.object({
697
722
  space_id: z.string().min(1).describe("Space whose runtime route pointers follow."),
698
723
  routes: z
@@ -703,12 +728,6 @@ export const runtimeSpaceStateSchema = z.object({
703
728
  .int()
704
729
  .nonnegative()
705
730
  .describe("Hostnames the runtime serves a tombstone page for on this space."),
706
- // Optional for the same reason the whole block is: the control plane deploys
707
- // in one step while engines pull their bundle on their own schedule, so every
708
- // field added here is absent on the fleet for hours. Absence means "this
709
- // engine cannot report hostname scope", never "the scope is empty" — callers
710
- // must degrade rather than sign a narrower callback claim than the engine
711
- // will report.
712
731
  intent_hostnames: z
713
732
  .array(z.string().min(1))
714
733
  .optional()
@@ -739,25 +758,19 @@ export const runtimeStateResponseSchema = z.object({
739
758
  .min(1)
740
759
  .optional()
741
760
  .describe("Runtime engine bundle revision. Missing means the engine predates revision state."),
742
- // Optional generation-state fields (absent on engines predating the reconcile
743
- // handshake; the sweep treats absence as "cannot compare").
744
761
  routes_generation: z
745
762
  .string()
746
763
  .nullable()
747
764
  .optional()
748
- .describe("Active route-index generation id (routes/current.php pointer)."),
765
+ .describe("Active route-index generation id (routes/current.json pointer)."),
749
766
  spaces: z
750
767
  .array(runtimeSpaceStateSchema)
751
768
  .optional()
752
769
  .describe("Per-space route-pointer state for reconcile comparison."),
753
770
  });
754
- // Runtime callback journal event (internal-docs/platform.md "Cache
755
- // Management"): the engine journals every management mutation and delivers it
756
- // at-least-once to the control-plane callback receiver, which translates
757
- // serving-affecting events into provider edge purges. `event_id` is the stable
758
- // journal id — purge enqueues are idempotent on (event_id, hostname) so
759
- // replays are no-ops. The object is intentionally loose: engines may journal
760
- // extra diagnostic fields without breaking older control planes.
771
+ // Runtime callback journal event. Purging is complete (or durably queued for a
772
+ // maintenance retry) before a mutation returns, so this lane carries only
773
+ // observability and background events.
761
774
  export const runtimeCallbackEventSchema = z.looseObject({
762
775
  event: z
763
776
  .string()
@@ -767,7 +780,7 @@ export const runtimeCallbackEventSchema = z.looseObject({
767
780
  .string()
768
781
  .min(1)
769
782
  .optional()
770
- .describe("Stable journal/callback id; purge handling replays as no-ops on this id."),
783
+ .describe("Stable journal/callback id for idempotent delivery."),
771
784
  operation_id: z
772
785
  .string()
773
786
  .optional()
@@ -815,6 +828,13 @@ const runtimeEventDeliveryIdentitySchema = z.object({
815
828
  delivery_id: z.string().min(1),
816
829
  });
817
830
  const runtimeEventDrainSessionIdSchema = z.string().min(1).max(128);
831
+ export const runtimeJournalCursorSchema = z
832
+ .object({
833
+ file: z.string(),
834
+ offset: z.number().int().nonnegative(),
835
+ inode: z.number().int().nonnegative().optional(),
836
+ })
837
+ .strict();
818
838
  export const runtimeEventsDrainRequestSchema = z
819
839
  .object({
820
840
  session_id: runtimeEventDrainSessionIdSchema,
@@ -841,49 +861,55 @@ export const runtimeEventsDrainRequestSchema = z
841
861
  context.addIssue({ code: "custom", message: "A drain page requires page_id." });
842
862
  });
843
863
  export const runtimeEventsDrainResponseSchema = z.object({
864
+ // Cursor runtimes keep these at zero. Older engines report their legacy
865
+ // delivery counters here while a rolling bundle update is still converging.
844
866
  delivered_count: z.number().int().nonnegative(),
845
867
  failed_count: z.number().int().nonnegative(),
846
- expired_count: z
847
- .number()
848
- .int()
849
- .nonnegative()
850
- .optional()
851
- .describe("Undeliverable callbacks dropped after exceeding the attempt cap."),
868
+ expired_count: z.number().int().nonnegative().optional(),
852
869
  returned_count: z.number().int().nonnegative().optional(),
853
870
  pending_count: z
854
871
  .number()
855
872
  .int()
856
873
  .nonnegative()
857
874
  .optional()
858
- .describe("Journal entries not returned because of the response cap, another active lease, or this session's failed-delivery deferral. A non-zero value means a later page or repair session still has work."),
859
- // The drain is pull-first: the caller IS the control plane and is blocked on
860
- // this response, so the engine hands its journal back here instead of posting
861
- // each event over a separate HTTP round trip. Processing replays are no-ops
862
- // on (event_id, hostname).
875
+ .describe("Whether the journal has records beyond this page. A non-zero value means a later drain still has work."),
863
876
  events: z
864
877
  .array(z.object({
865
878
  delivery_id: z.string().min(1),
866
879
  operation_id: z.string().optional(),
867
- callback_token: z.string().min(1),
868
880
  event: runtimeCallbackEventSchema,
869
881
  }))
870
882
  .optional(),
883
+ cursor: runtimeJournalCursorSchema
884
+ .optional()
885
+ .describe("The journal commit coordinate. Absent on pre-cursor runtime bundles."),
871
886
  });
872
- export const runtimeEventsAckRequestSchema = z.object({
873
- session_id: runtimeEventDrainSessionIdSchema,
874
- deliveries: z.array(runtimeEventDeliveryIdentitySchema).max(100),
887
+ export const runtimeEventsAckRequestSchema = z
888
+ .object({
889
+ // The cursor is the authoritative commit coordinate.
890
+ cursor: runtimeJournalCursorSchema.optional(),
891
+ session_id: runtimeEventDrainSessionIdSchema.optional(),
892
+ deliveries: z.array(runtimeEventDeliveryIdentitySchema).max(100).default([]),
893
+ })
894
+ .superRefine((value, context) => {
895
+ if (!value.cursor && (!value.session_id || value.deliveries.length === 0)) {
896
+ context.addIssue({
897
+ code: "custom",
898
+ message: "An event acknowledgement requires a cursor or legacy deliveries.",
899
+ });
900
+ }
875
901
  });
876
902
  export const runtimeEventsAckResponseSchema = z.object({
877
903
  acknowledged_count: z.number().int().nonnegative(),
878
904
  idempotent_count: z.number().int().nonnegative(),
879
905
  stale_count: z.number().int().nonnegative(),
906
+ cursor: runtimeJournalCursorSchema
907
+ .optional()
908
+ .describe("The committed journal coordinate. Absent after a legacy delivery acknowledgement."),
880
909
  });
881
- // Engine job runner (plan §22, Placement V2): the box-local job runner behind
882
- // the two-lane tick. The control plane creates a job, drives it with repeated
883
- // ticks (kick-on-enqueue; the site-cron watchdog ticks the same route), and
884
- // reads status from either the tick response or a direct GET. Field names
885
- // mirror the engine's job record (§22 "Storage layout & job record").
886
- export const runtimeEngineJobLaneSchema = z.enum(["interactive", "bulk"]);
910
+ // One lane; the enum keeps its single value because the engine's job record and
911
+ // tick response still carry the field.
912
+ export const runtimeEngineJobLaneSchema = z.literal("bulk");
887
913
  export const runtimeEngineJobStatusSchema = z.enum(["pending", "running", "complete", "failed"]);
888
914
  export const runtimeEngineJobErrorSchema = z.object({
889
915
  code: z.string().min(1),
@@ -912,15 +938,13 @@ export const runtimeEngineJobRecordSchema = z.object({
912
938
  export const runtimeEngineJobCreateRequestSchema = z.object({
913
939
  type: z.string().min(1),
914
940
  payload: z.record(z.string(), z.unknown()).default({}),
915
- // Unique per (type, space, operation) — create is upsert-by-key (§22).
941
+ // Unique per (type, space, operation) — create is upsert-by-key.
916
942
  idempotency_key: z.string().min(1),
917
943
  });
918
944
  export const runtimeEngineJobResponseSchema = z.object({
919
945
  job: runtimeEngineJobRecordSchema,
920
946
  });
921
- // The tick response carries the ticked job's fresh state when one was
922
- // claimed/advanced this call, so a driver loop can usually avoid a follow-up
923
- // GET; `job` is absent/null when the lane had nothing eligible to run.
947
+ // `job` is absent/null when the lane had nothing eligible to run.
924
948
  export const runtimeEngineJobTickResponseSchema = z.object({
925
949
  lane: runtimeEngineJobLaneSchema,
926
950
  job: runtimeEngineJobRecordSchema.nullable().optional(),