@spacefast/common 0.0.12 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/dist/agents/client-registry.d.ts +22 -0
  2. package/dist/agents/client-registry.js +41 -0
  3. package/dist/agents/connect-targets.d.ts +51 -6
  4. package/dist/agents/connect-targets.js +93 -28
  5. package/dist/brand.d.ts +0 -11
  6. package/dist/brand.js +0 -20
  7. package/dist/config/domains.d.ts +15 -6
  8. package/dist/config/domains.js +24 -10
  9. package/dist/config/postgres-budget.d.ts +3 -0
  10. package/dist/config/postgres-budget.js +6 -1
  11. package/dist/config/postgres-budget.json +2 -1
  12. package/dist/contracts/abuse.js +1 -1
  13. package/dist/contracts/access.d.ts +486 -1196
  14. package/dist/contracts/access.js +323 -1054
  15. package/dist/contracts/activity.d.ts +1 -1
  16. package/dist/contracts/activity.js +11 -15
  17. package/dist/contracts/api-keys.d.ts +16 -0
  18. package/dist/contracts/api-keys.js +11 -9
  19. package/dist/contracts/archives.d.ts +1 -2
  20. package/dist/contracts/archives.js +15 -17
  21. package/dist/contracts/beta.js +1 -1
  22. package/dist/contracts/builds.js +14 -14
  23. package/dist/contracts/channels.js +2 -2
  24. package/dist/contracts/comments.d.ts +143 -99
  25. package/dist/contracts/comments.js +194 -79
  26. package/dist/contracts/common.d.ts +12 -12
  27. package/dist/contracts/common.js +33 -30
  28. package/dist/contracts/continuation.js +5 -5
  29. package/dist/contracts/countries.js +2 -7
  30. package/dist/contracts/data-source-routes.d.ts +20 -0
  31. package/dist/contracts/data-source-routes.js +15 -0
  32. package/dist/contracts/device-auth.d.ts +13 -9
  33. package/dist/contracts/device-auth.js +12 -12
  34. package/dist/contracts/docs.d.ts +10 -10
  35. package/dist/contracts/domains.js +39 -33
  36. package/dist/contracts/enums.d.ts +4 -14
  37. package/dist/contracts/enums.js +3 -10
  38. package/dist/contracts/error-code-meta.d.ts +122 -56
  39. package/dist/contracts/error-code-meta.js +43 -19
  40. package/dist/contracts/error-codes.d.ts +7 -4
  41. package/dist/contracts/error-codes.js +48 -24
  42. package/dist/contracts/events.d.ts +22 -15
  43. package/dist/contracts/events.js +33 -5
  44. package/dist/contracts/execution.d.ts +238 -0
  45. package/dist/contracts/execution.js +309 -0
  46. package/dist/contracts/feature-lifecycle.d.ts +39 -188
  47. package/dist/contracts/feature-lifecycle.js +52 -196
  48. package/dist/contracts/features.d.ts +13 -115
  49. package/dist/contracts/features.js +5 -35
  50. package/dist/contracts/functions.d.ts +597 -0
  51. package/dist/contracts/functions.js +475 -0
  52. package/dist/contracts/grants.d.ts +1175 -0
  53. package/dist/contracts/grants.js +783 -0
  54. package/dist/contracts/ids.d.ts +7 -5
  55. package/dist/contracts/ids.js +7 -7
  56. package/dist/contracts/internal.js +2 -2
  57. package/dist/contracts/mcp.d.ts +17 -556
  58. package/dist/contracts/mcp.js +9 -103
  59. package/dist/contracts/me.js +3 -4
  60. package/dist/contracts/notifications.d.ts +108 -0
  61. package/dist/contracts/notifications.js +54 -0
  62. package/dist/contracts/oauth-resources.d.ts +22 -5
  63. package/dist/contracts/oauth-resources.js +23 -2
  64. package/dist/contracts/operations.d.ts +0 -48
  65. package/dist/contracts/operations.js +0 -19
  66. package/dist/contracts/pages.d.ts +10 -14
  67. package/dist/contracts/pages.js +17 -21
  68. package/dist/contracts/plan-policy.d.ts +1 -8
  69. package/dist/contracts/plan-policy.js +1 -4
  70. package/dist/contracts/platform.d.ts +78 -30
  71. package/dist/contracts/platform.js +69 -20
  72. package/dist/contracts/problem-document.d.ts +14 -0
  73. package/dist/contracts/problem-document.js +35 -0
  74. package/dist/contracts/push-new.d.ts +41 -2
  75. package/dist/contracts/push-new.js +4 -0
  76. package/dist/contracts/quotas.d.ts +0 -1
  77. package/dist/contracts/quotas.js +0 -5
  78. package/dist/contracts/repository-connections.d.ts +1 -1
  79. package/dist/contracts/resources.d.ts +16 -71
  80. package/dist/contracts/resources.js +13 -16
  81. package/dist/contracts/runtime-api.d.ts +666 -303
  82. package/dist/contracts/runtime-api.js +272 -113
  83. package/dist/contracts/runtime-app.d.ts +168 -0
  84. package/dist/contracts/runtime-app.js +42 -0
  85. package/dist/contracts/runtime-db.d.ts +424 -0
  86. package/dist/contracts/runtime-db.js +108 -0
  87. package/dist/contracts/runtime-storage.d.ts +120 -0
  88. package/dist/contracts/runtime-storage.js +40 -0
  89. package/dist/contracts/sf-config-v1.d.ts +54 -1
  90. package/dist/contracts/sf-config-v1.js +2 -2
  91. package/dist/contracts/sites.d.ts +6 -0
  92. package/dist/contracts/sites.js +30 -11
  93. package/dist/contracts/space-config.d.ts +91 -121
  94. package/dist/contracts/space-config.js +464 -42
  95. package/dist/contracts/spaces.d.ts +300 -171
  96. package/dist/contracts/spaces.js +128 -98
  97. package/dist/contracts/superadmin-emails.d.ts +2 -2
  98. package/dist/contracts/superadmin-spaces.d.ts +26 -130
  99. package/dist/contracts/superadmin-spaces.js +22 -0
  100. package/dist/contracts/superadmin-tenants.js +4 -4
  101. package/dist/contracts/superadmin.d.ts +4 -0
  102. package/dist/contracts/superadmin.js +14 -1
  103. package/dist/contracts/tags.d.ts +6 -6
  104. package/dist/contracts/tags.js +1 -1
  105. package/dist/contracts/teams.d.ts +9 -9
  106. package/dist/contracts/teams.js +25 -16
  107. package/dist/contracts/theme-json.js +1 -1
  108. package/dist/contracts/transfers.d.ts +0 -1
  109. package/dist/contracts/transfers.js +4 -4
  110. package/dist/contracts/variables.js +3 -3
  111. package/dist/contracts/zero.d.ts +384 -596
  112. package/dist/contracts/zero.js +167 -299
  113. package/dist/dashboard-paths/index.d.ts +18 -3
  114. package/dist/dashboard-paths/index.js +19 -0
  115. package/dist/docs/agent-handoff-document.d.ts +2 -2
  116. package/dist/docs/agent-handoff-document.js +8 -7
  117. package/dist/docs/agent-prose.d.ts +22 -3
  118. package/dist/docs/agent-prose.js +227 -55
  119. package/dist/docs/agent-setup.d.ts +14 -11
  120. package/dist/docs/agent-setup.js +289 -61
  121. package/dist/docs/agent-solutions.d.ts +6 -6
  122. package/dist/docs/agent-solutions.js +24 -22
  123. package/dist/docs/catalog.d.ts +53 -98
  124. package/dist/docs/catalog.js +90 -117
  125. package/dist/docs/error-docs.js +304 -236
  126. package/dist/docs/skill-distribution.d.ts +2 -0
  127. package/dist/docs/skill-distribution.js +7 -4
  128. package/dist/docs/skills.d.ts +546 -0
  129. package/dist/docs/skills.js +340 -0
  130. package/dist/utils/auth-redirect.d.ts +1 -3
  131. package/dist/utils/auth-redirect.js +1 -21
  132. package/dist/utils/browser-credential.d.ts +19 -0
  133. package/dist/utils/browser-credential.js +62 -0
  134. package/dist/utils/build-settings.d.ts +2 -0
  135. package/dist/utils/build-settings.js +61 -9
  136. package/dist/utils/comment-avatars.d.ts +7 -0
  137. package/dist/utils/comment-avatars.js +42 -0
  138. package/dist/utils/content-type.d.ts +3 -3
  139. package/dist/utils/content-type.js +38 -61
  140. package/dist/utils/credential-policy.d.ts +1 -1
  141. package/dist/utils/credential-policy.js +4 -0
  142. package/dist/utils/domain-diagnostics.d.ts +5 -5
  143. package/dist/utils/domain-diagnostics.js +21 -10
  144. package/dist/utils/egress-policy.fixtures.json +1 -1
  145. package/dist/utils/grants.d.ts +53 -0
  146. package/dist/utils/grants.js +160 -0
  147. package/dist/utils/local-space-state.d.ts +78 -4
  148. package/dist/utils/local-space-state.js +384 -82
  149. package/dist/utils/one-shot-replay.d.ts +14 -0
  150. package/dist/utils/one-shot-replay.js +21 -0
  151. package/dist/utils/page-preview.d.ts +7 -0
  152. package/dist/utils/page-preview.js +9 -0
  153. package/dist/utils/pages.d.ts +2 -5
  154. package/dist/utils/pages.js +64 -33
  155. package/dist/utils/publish-form-data.d.ts +22 -0
  156. package/dist/utils/publish-form-data.js +26 -0
  157. package/dist/utils/publish-policy.d.ts +34 -4
  158. package/dist/utils/publish-policy.js +149 -62
  159. package/dist/utils/query-keys.d.ts +8 -10
  160. package/dist/utils/query-keys.js +14 -10
  161. package/dist/utils/runtime-paths.d.ts +3 -0
  162. package/dist/utils/runtime-paths.js +7 -0
  163. package/dist/utils/secure-local-file.d.ts +73 -0
  164. package/dist/utils/secure-local-file.js +1373 -0
  165. package/dist/utils/space-config.d.ts +17 -4
  166. package/dist/utils/space-config.js +151 -8
  167. package/dist/utils/static-runtime-policy.d.ts +5 -0
  168. package/dist/utils/static-runtime-policy.js +54 -10
  169. package/dist/utils/storage-policy.d.ts +20 -0
  170. package/dist/utils/storage-policy.fixtures.json +174 -0
  171. package/dist/utils/storage-policy.js +193 -0
  172. package/dist/utils/upload-session.d.ts +15 -1
  173. package/dist/utils/upload-session.js +68 -2
  174. package/dist/utils/version-file-browser.d.ts +13 -0
  175. package/dist/utils/version-file-browser.js +59 -0
  176. package/package.json +20 -1
  177. package/dist/contracts/mounts.d.ts +0 -142
  178. package/dist/contracts/mounts.js +0 -56
  179. package/dist/utils/access-grant.d.ts +0 -14
  180. package/dist/utils/access-grant.js +0 -88
  181. package/dist/utils/access-match.d.ts +0 -46
  182. package/dist/utils/access-match.fixtures.json +0 -513
  183. package/dist/utils/access-match.js +0 -312
  184. package/dist/utils/access-verdict.fixtures.json +0 -421
  185. package/dist/utils/asset-fingerprint.d.ts +0 -53
  186. package/dist/utils/asset-fingerprint.js +0 -574
  187. package/dist/utils/sf-config-v1.d.ts +0 -2
  188. package/dist/utils/sf-config-v1.fixtures.json +0 -9
  189. package/dist/utils/sf-config-v1.js +0 -551
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
- import { BRAND } from "../brand.js";
3
- import { runtimeRuleSchema } from "./access.js";
2
+ import { serviceOrigin } from "../config/domains.js";
4
3
  import { normalizedSiteClass } from "./enums.js";
4
+ import { GRANT_RESOURCE_PATTERN_LIMIT, normalizeGrantResourcePattern } from "./grants.js";
5
5
  export const SPACE_CONFIG_CANONICAL_FILE = "sf.jsonc";
6
6
  export const SPACE_CONFIG_ALIAS_FILES = [
7
7
  "spacefast.jsonc",
@@ -35,17 +35,19 @@ export const SPACE_CONFIG_CAPS = {
35
35
  injectSnippetBytes: 8 * 1024,
36
36
  /** Max snippets per `inject` array. */
37
37
  injectSnippets: 16,
38
- /** Max `access.rules` entries in the file lane. */
39
- accessRules: 100,
38
+ /** Max public WordPress sources exposed to repository builds. */
39
+ dataSources: 32,
40
40
  /** Max `theme` color token length. */
41
41
  themeColorChars: 64,
42
42
  /** Max `theme.logo` URL/path length. */
43
43
  themeLogoChars: 2000,
44
44
  /** Max `theme.name` display-name length. */
45
45
  themeNameChars: 120,
46
+ /** Max top-level `name` (space display name) length. Matches the stored column. */
47
+ nameChars: 255,
46
48
  };
47
49
  /** Published JSON Schema URL for `sf.jsonc` ($schema autocomplete). */
48
- export const SPACE_CONFIG_SCHEMA_URL = `${BRAND.websiteOrigin}/schemas/sf.json`;
50
+ export const SPACE_CONFIG_SCHEMA_URL = `${serviceOrigin("www", "prod")}/schemas/sf.json`;
49
51
  // Pages theme tokens: visual values consumed by publish-rendered site pages.
50
52
  // Presentation only — never layout, behavior, or protocol.
51
53
  // Color grammar accepts hex/rgb(a)/hsl(a)/
@@ -138,11 +140,219 @@ export const spacePlacementConfigSchema = z
138
140
  })
139
141
  .strict()
140
142
  .describe("Placement controls for this space.");
141
- // SpaceConfig (internal-docs/platform.md "SpaceConfig"): the declarative
142
- // serving config also the shape of `sf.jsonc` minus `$schema`/`space`.
143
+ const RESERVED_DATA_SOURCE_NAMES = new Set(["__proto__", "constructor", "prototype"]);
144
+ export const dataSourceNameSchema = z
145
+ .string()
146
+ .min(1)
147
+ .max(128)
148
+ .regex(/^[A-Za-z_][A-Za-z0-9_]*$/, {
149
+ message: "Use a non-reserved name that starts with a letter or underscore and contains only letters, numbers, and underscores.",
150
+ })
151
+ .refine((name) => !RESERVED_DATA_SOURCE_NAMES.has(name), {
152
+ message: "Use a non-reserved name that starts with a letter or underscore and contains only letters, numbers, and underscores.",
153
+ })
154
+ .describe("Stable source identifier: 1-128 letters, numbers, or underscores; starts with a letter or underscore; excludes __proto__, constructor, and prototype.")
155
+ .meta({ not: { enum: [...RESERVED_DATA_SOURCE_NAMES] } });
156
+ const WORDPRESS_DATA_SOURCE_URL_MESSAGE = "WordPress data source URLs must use lowercase https:// with a valid DNS name, canonical dotted-decimal IPv4 address, or bracketed IPv6 address. Whitespace, controls, malformed percent encoding, encoded percent signs, slashes or backslashes, credentials, queries, fragments, trailing-dot hosts, and port 0 are not allowed.";
157
+ const WORDPRESS_DATA_SOURCE_URL_PATTERN = /^https:\/\/[^/?#@%\\\s]+(?:\/[^?#\\\s]*)?$/u;
158
+ const WORDPRESS_DATA_SOURCE_WHITESPACE_PATTERN = /\p{White_Space}/u;
159
+ const DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
160
+ const IPV6_LITERAL_PATTERN = /^[0-9a-f:]+$/i;
161
+ const DECIMAL_PORT_PATTERN = /^\d{1,5}$/;
162
+ const IPV4_OCTET_PATTERN = /^(?:0|[1-9]\d{0,2})$/;
163
+ function hasAsciiControlCharacter(value) {
164
+ for (let index = 0; index < value.length; index += 1) {
165
+ const code = value.charCodeAt(index);
166
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f))
167
+ return true;
168
+ }
169
+ return false;
170
+ }
171
+ function isCanonicalIpv4Address(value) {
172
+ const octets = value.split(".");
173
+ return (octets.length === 4 &&
174
+ octets.every((octet) => IPV4_OCTET_PATTERN.test(octet) && Number.parseInt(octet, 10) <= 255));
175
+ }
176
+ function isValidDnsHostname(value) {
177
+ if (value.length === 0 || value.length > 253 || value.endsWith("."))
178
+ return false;
179
+ const labels = value.split(".");
180
+ return labels.every((label) => label.length > 0 && label.length <= 63 && DNS_LABEL_PATTERN.test(label));
181
+ }
182
+ function parseWordPressAuthority(authority) {
183
+ if (!authority || authority.includes("@") || authority.includes("%"))
184
+ return null;
185
+ let rawHostname;
186
+ let rawPort = null;
187
+ let ipv6 = false;
188
+ if (authority.startsWith("[")) {
189
+ const closingBracket = authority.indexOf("]");
190
+ if (closingBracket <= 1 || authority.indexOf("]", closingBracket + 1) !== -1)
191
+ return null;
192
+ rawHostname = authority.slice(1, closingBracket);
193
+ const suffix = authority.slice(closingBracket + 1);
194
+ if (suffix) {
195
+ if (!suffix.startsWith(":"))
196
+ return null;
197
+ rawPort = suffix.slice(1);
198
+ }
199
+ ipv6 = true;
200
+ }
201
+ else {
202
+ if (authority.includes("[") || authority.includes("]"))
203
+ return null;
204
+ const firstColon = authority.indexOf(":");
205
+ if (firstColon !== authority.lastIndexOf(":"))
206
+ return null;
207
+ if (firstColon === -1) {
208
+ rawHostname = authority;
209
+ }
210
+ else {
211
+ rawHostname = authority.slice(0, firstColon);
212
+ rawPort = authority.slice(firstColon + 1);
213
+ }
214
+ }
215
+ if (!rawHostname || rawHostname.endsWith("."))
216
+ return null;
217
+ if (rawPort !== null) {
218
+ if (!DECIMAL_PORT_PATTERN.test(rawPort))
219
+ return null;
220
+ const port = Number.parseInt(rawPort, 10);
221
+ if (port === 0 || port > 65_535)
222
+ return null;
223
+ }
224
+ try {
225
+ const url = new URL(`https://${authority}/`);
226
+ if (ipv6) {
227
+ if (!IPV6_LITERAL_PATTERN.test(rawHostname))
228
+ return null;
229
+ if (!url.hostname.startsWith("[") || !url.hostname.endsWith("]"))
230
+ return null;
231
+ }
232
+ else {
233
+ const normalizedHostname = url.hostname;
234
+ if (normalizedHostname.startsWith("[") || normalizedHostname.endsWith("]"))
235
+ return null;
236
+ if (isCanonicalIpv4Address(normalizedHostname)) {
237
+ if (rawHostname !== normalizedHostname || !isCanonicalIpv4Address(rawHostname))
238
+ return null;
239
+ }
240
+ else if (!isValidDnsHostname(normalizedHostname)) {
241
+ return null;
242
+ }
243
+ }
244
+ return url.origin;
245
+ }
246
+ catch {
247
+ return null;
248
+ }
249
+ }
250
+ function canonicalizeWordPressPath(rawPath) {
251
+ if (!rawPath)
252
+ return "";
253
+ // Reject encoded percents so validation has one authoritative decode layer.
254
+ // Encoded path separators are never valid in a WordPress source base path.
255
+ if (/%(?:25|2f|5c)/i.test(rawPath))
256
+ return null;
257
+ let decodedPath = rawPath;
258
+ if (rawPath.includes("%")) {
259
+ const encodedSegments = rawPath.split("/");
260
+ let nextPath;
261
+ try {
262
+ nextPath = decodeURIComponent(rawPath);
263
+ void encodeURIComponent(nextPath);
264
+ }
265
+ catch {
266
+ return null;
267
+ }
268
+ const decodedSegments = nextPath.split("/");
269
+ if (decodedSegments.some((segment, index) => (segment === "." || segment === "..") && encodedSegments[index] !== segment)) {
270
+ return null;
271
+ }
272
+ if (hasAsciiControlCharacter(nextPath) ||
273
+ nextPath.includes("\\") ||
274
+ WORDPRESS_DATA_SOURCE_WHITESPACE_PATTERN.test(nextPath.replaceAll(" ", ""))) {
275
+ return null;
276
+ }
277
+ decodedPath = nextPath;
278
+ }
279
+ const url = new URL("https://path.invalid/");
280
+ url.pathname = decodedPath;
281
+ return url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, "");
282
+ }
283
+ function canonicalWordPressDataSourceUrl(value) {
284
+ if (!value.startsWith("https://") ||
285
+ hasAsciiControlCharacter(value) ||
286
+ WORDPRESS_DATA_SOURCE_WHITESPACE_PATTERN.test(value) ||
287
+ value.includes("\\") ||
288
+ value.includes("?") ||
289
+ value.includes("#")) {
290
+ return null;
291
+ }
292
+ const remainder = value.slice("https://".length);
293
+ const pathIndex = remainder.indexOf("/");
294
+ const authority = pathIndex === -1 ? remainder : remainder.slice(0, pathIndex);
295
+ const rawPath = pathIndex === -1 ? "" : remainder.slice(pathIndex);
296
+ const origin = parseWordPressAuthority(authority);
297
+ const pathname = canonicalizeWordPressPath(rawPath);
298
+ return origin && pathname !== null ? `${origin}${pathname}` : null;
299
+ }
300
+ export const wordpressDataSourceUrlSchema = z
301
+ .string()
302
+ .min(1)
303
+ .max(2048)
304
+ .refine((value) => !hasAsciiControlCharacter(value), {
305
+ message: WORDPRESS_DATA_SOURCE_URL_MESSAGE,
306
+ })
307
+ .regex(WORDPRESS_DATA_SOURCE_URL_PATTERN, { message: WORDPRESS_DATA_SOURCE_URL_MESSAGE })
308
+ .url({ message: WORDPRESS_DATA_SOURCE_URL_MESSAGE })
309
+ .refine((value) => canonicalWordPressDataSourceUrl(value) !== null, {
310
+ message: WORDPRESS_DATA_SOURCE_URL_MESSAGE,
311
+ })
312
+ .overwrite((value) => canonicalWordPressDataSourceUrl(value) ?? value)
313
+ .max(2048)
314
+ .describe("Canonical public WordPress URL: lowercase https:// scheme; IDNA-normalized DNS labels (1-63 characters each, 253 total, alphanumeric edges, alphanumeric or hyphen interior), canonical dotted-decimal IPv4, or bracketed hexadecimal IPv6; optional nonzero port and UTF-8 path. Percent escapes are validated and canonicalized; encoded percent signs, slashes, backslashes, controls, invalid UTF-8, whitespace other than encoded ASCII path spaces, credentials, queries, fragments, and trailing-dot hosts are rejected. Default port, host case, IDNs, dot segments, path encoding, and trailing path slashes are normalized before storage.");
315
+ export const wordpressDataSourceSchema = z
316
+ .object({
317
+ kind: z.literal("wordpress"),
318
+ url: wordpressDataSourceUrlSchema,
319
+ })
320
+ .strict()
321
+ .describe("A public WordPress site exposed to repository builds as a content source.");
322
+ function hasNoReservedDataSourceRecordKeys(value) {
323
+ return !(typeof value === "object" &&
324
+ value !== null &&
325
+ !Array.isArray(value) &&
326
+ Object.keys(value).some((name) => RESERVED_DATA_SOURCE_NAMES.has(name)));
327
+ }
328
+ export const spaceDataSourcesSchema = z
329
+ .unknown()
330
+ .refine(hasNoReservedDataSourceRecordKeys, { message: "dataSources contains a reserved name." })
331
+ .pipe(z
332
+ .record(dataSourceNameSchema, wordpressDataSourceSchema)
333
+ .refine((sources) => Object.keys(sources).length <= SPACE_CONFIG_CAPS.dataSources, {
334
+ message: `dataSources supports up to ${SPACE_CONFIG_CAPS.dataSources} entries.`,
335
+ }))
336
+ .meta({
337
+ maxProperties: SPACE_CONFIG_CAPS.dataSources,
338
+ description: `Named public WordPress sources. Supports at most ${SPACE_CONFIG_CAPS.dataSources} entries.`,
339
+ });
340
+ function refineDefaultDataSource(config, ctx) {
341
+ if (config.defaultDataSource !== undefined &&
342
+ !Object.prototype.hasOwnProperty.call(config.dataSources ?? {}, config.defaultDataSource)) {
343
+ ctx.addIssue({
344
+ code: "custom",
345
+ message: "defaultDataSource must name a configured dataSources entry.",
346
+ path: ["defaultDataSource"],
347
+ });
348
+ }
349
+ }
350
+ // Shared SpaceConfig fields (internal-docs/platform.md "SpaceConfig"): the
351
+ // declarative serving config also accepted by `sf.jsonc`. Cloud-only overlay
352
+ // fields such as pre-build data sources are added to `spaceConfigSchema` below.
143
353
  // Meta caps are the spec parser-safety numbers (title 300, description 1000,
144
354
  // image 2000); plan policy may lower them but never raise them.
145
- export const spaceConfigSchema = z.object({
355
+ const spaceConfigBaseSchema = z.object({
146
356
  index: z
147
357
  .union([z.string().min(1), z.literal(false)])
148
358
  .optional()
@@ -156,11 +366,11 @@ export const spaceConfigSchema = z.object({
156
366
  }),
157
367
  ])
158
368
  .optional()
159
- .describe('Fallback document; "/index.html" means SPA. Default: nearest 404.html.'),
369
+ .describe('Fallback document. "/index.html" means SPA. Defaults to the nearest 404.html.'),
160
370
  cleanUrls: z
161
371
  .boolean()
162
372
  .optional()
163
- .describe("Serve flat `<name>.html` files at their extensionless URL (/about serves about.html). Default: on, unless a 200-status SPA fallback is configured."),
373
+ .describe("Serve flat `<name>.html` files at their extensionless URL (`/about` serves `about.html`). Defaults to on, unless a 200-status SPA fallback is configured."),
164
374
  listing: z.boolean().optional().describe("Enable automatic directory listings."),
165
375
  meta: z
166
376
  .object({
@@ -254,46 +464,159 @@ export const spaceConfigSchema = z.object({
254
464
  })
255
465
  .optional(),
256
466
  });
467
+ export const spaceConfigSchema = spaceConfigBaseSchema
468
+ .extend({
469
+ dataSources: spaceDataSourcesSchema
470
+ .optional()
471
+ .describe(`Named public WordPress sources exposed to repository builds; at most ${SPACE_CONFIG_CAPS.dataSources} entries.`),
472
+ defaultDataSource: dataSourceNameSchema
473
+ .optional()
474
+ .describe("Name of the configured source exposed through `WORDPRESS_URL`. It must match a `dataSources` entry. When omitted, the first source in bytewise name order is used."),
475
+ })
476
+ .superRefine(refineDefaultDataSource);
477
+ export function compareDataSourceNames(left, right) {
478
+ return left < right ? -1 : left > right ? 1 : 0;
479
+ }
480
+ export function resolveDefaultDataSourceName(config) {
481
+ const names = Object.keys(config?.dataSources ?? {}).toSorted(compareDataSourceNames);
482
+ const configuredDefault = config?.defaultDataSource;
483
+ return configuredDefault &&
484
+ Object.prototype.hasOwnProperty.call(config?.dataSources ?? {}, configuredDefault)
485
+ ? configuredDefault
486
+ : (names[0] ?? null);
487
+ }
257
488
  export function spaceSuperpowersEnabled(config) {
258
489
  const superpowers = config?.superpowers;
259
490
  return superpowers?.enabled !== false && superpowers?.disable_all_injection !== true;
260
491
  }
261
- // The declarative file-lane access/firewall block (internal-docs/
262
- // internal-docs/access-plan.html, "file" provenance): the SAME unified Rule shape
263
- // the policy document and the cloud lane use — no parallel contract. Rules
264
- // committed here compile at publish finalize and version + roll back WITH the
265
- // content version; live edits without a republish belong to the cloud lane
266
- // (`sf access`). The file lane lives ONLY in the config FILE — the API `config`
267
- // overlay never carries it (single-home, no drift).
268
- export const spaceAccessConfigSchema = z
269
- .object({
270
- rules: z
271
- .array(runtimeRuleSchema)
272
- .max(SPACE_CONFIG_CAPS.accessRules)
273
- .default([])
274
- .describe("Ordered unified access/firewall rules; first match wins."),
275
- })
276
- .describe("Declarative access lane: rules compiled at publish that version and roll back with the site.");
277
492
  // The `sf.jsonc` FILE shape (internal-docs/platform.md "Space
278
493
  // Configuration Files"): SpaceConfig plus the editor `$schema` pointer and the
279
494
  // optional `space` link (slug or `spc_` id) resolved by CLI/publish flows.
280
- // Routing never lives here — redirects/rewrites/proxies/headers stay
281
- // exclusively in `_redirects`/`_headers` (one way per concern).
282
- // Client-side publish options (caching plan §6): consumed by the CLI/agent
283
- // publish tooling before upload, never by the runtime — like `$schema` and
284
- // `space`, this key lives only in the FILE shape and never reaches the space
285
- // `config` overlay.
286
- export const spacePublishOptionsSchema = z
495
+ // Routing rules (`redirects`/`rewrites`/`headers`) are sf.jsonc keys too, but they belong
496
+ // to the routing compiler, not to this shape: it resolves serving, they compile
497
+ // into the canonical rules and merge behind `_redirects`/`_headers`.
498
+ const spaceConfigPublicPatternSchema = z
499
+ .string()
500
+ .min(1)
501
+ .max(1025)
502
+ .transform((candidate, context) => {
503
+ const excluded = candidate.startsWith("!");
504
+ try {
505
+ const normalized = normalizeGrantResourcePattern(excluded ? candidate.slice(1) : candidate);
506
+ return excluded ? `!${normalized}` : normalized;
507
+ }
508
+ catch (error) {
509
+ context.addIssue({
510
+ code: "custom",
511
+ message: error instanceof Error ? error.message : "Invalid access.public resource pattern.",
512
+ });
513
+ return z.NEVER;
514
+ }
515
+ });
516
+ const spaceConfigFileAccessObjectSchema = z
517
+ .object({
518
+ public: z
519
+ .array(spaceConfigPublicPatternSchema)
520
+ .max(GRANT_RESOURCE_PATTERN_LIMIT)
521
+ .describe("Always-public live paths. Prefix exclusions with !; * and ** are whole-segment wildcards."),
522
+ })
523
+ .strict()
524
+ .superRefine((value, context) => {
525
+ const seen = new Set();
526
+ let inclusionCount = 0;
527
+ value.public.forEach((pattern, index) => {
528
+ if (seen.has(pattern)) {
529
+ context.addIssue({
530
+ code: "custom",
531
+ path: ["public", index],
532
+ message: "access.public patterns must be unique.",
533
+ });
534
+ }
535
+ seen.add(pattern);
536
+ if (!pattern.startsWith("!"))
537
+ inclusionCount += 1;
538
+ });
539
+ if (value.public.length > 0 && inclusionCount === 0) {
540
+ context.addIssue({
541
+ code: "custom",
542
+ path: ["public"],
543
+ message: "access.public requires at least one inclusion pattern.",
544
+ });
545
+ }
546
+ });
547
+ export const spaceConfigFileAccessSchema = z
548
+ .union([
549
+ z
550
+ .literal("public")
551
+ .describe('Shorthand for { "public": ["/**"] }: the whole live space is public.')
552
+ .transform(() => ({ public: ["/**"] })),
553
+ spaceConfigFileAccessObjectSchema,
554
+ ])
555
+ .describe("File-only public access declaration. It is inactive until the space is claimed.");
556
+ /**
557
+ * Zero: a transactional app with its own database.
558
+ *
559
+ * Both entries are required because Zero is never inferred. A runtime that owns
560
+ * a database must not spring into existence from a directory shape, so the only
561
+ * way to get one is to say so — and once said, a server or client that cannot
562
+ * be resolved fails the publish rather than degrading to static files.
563
+ */
564
+ export const spaceRuntimeZeroSchema = z
287
565
  .object({
288
- fingerprint: z
566
+ kind: z.literal("zero"),
567
+ server: z
568
+ .string()
569
+ .trim()
570
+ .min(1)
571
+ .describe("Path to the server entry module, relative to the published directory."),
572
+ client: z
573
+ .string()
574
+ .trim()
575
+ .min(1)
576
+ .describe("Path to the client entry module, relative to the published directory."),
577
+ })
578
+ .describe("Zero: a transactional app with its own database, compiled at publish time.");
579
+ /**
580
+ * Functions: the worker `sf publish` compiles.
581
+ *
582
+ * Every field beyond `kind` is optional because the normal case is detection: a
583
+ * project with `handler.ts` or an OpenNext build needs no configuration at all.
584
+ * These exist for the layouts detection cannot guess — a worker somewhere
585
+ * unusual, or a compatibility date the author wants pinned rather than
586
+ * defaulted.
587
+ */
588
+ export const spaceRuntimeFunctionsSchema = z
589
+ .object({
590
+ kind: z.literal("functions"),
591
+ entry: z
592
+ .string()
593
+ .optional()
594
+ .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."),
596
+ database: z
289
597
  .boolean()
598
+ .default(false)
599
+ .describe("Give the worker `env.DB`, a D1-shaped binding to this space's database. " +
600
+ "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."),
601
+ compatibilityDate: z
602
+ .string()
603
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
290
604
  .optional()
291
- .describe("Publish-time asset fingerprinting: adds content-hashed immutable aliases " +
292
- "(name.<sha256-8>.ext) for stable assets and rewrites exact HTML/CSS references. " +
293
- "Originals keep serving unchanged. Default off."),
605
+ .describe("Runtime compatibility date this worker was written against. Pinning it makes rollback honest. Promoting an old version restores the semantics it was built with."),
294
606
  })
295
- .describe("Client publish-time options consumed by the Spacefast CLI, not the runtime.");
296
- export const spaceConfigFileSchema = spaceConfigSchema.extend({
607
+ .describe("Functions: a worker compiled at publish time.");
608
+ /**
609
+ * The one runtime declaration, discriminated on `kind`.
610
+ *
611
+ * One block, one kind — a version cannot express two runtimes at once, so the
612
+ * ambiguity never has to be resolved downstream. File-only on purpose: a
613
+ * runtime is a fact about the content of a version, so it rolls forward and
614
+ * back with the version instead of being set through the API config overlay.
615
+ */
616
+ export const spaceRuntimeSchema = z
617
+ .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.");
619
+ export const spaceConfigFileSchema = spaceConfigBaseSchema.extend({
297
620
  $schema: z
298
621
  .string()
299
622
  .optional()
@@ -303,8 +626,17 @@ export const spaceConfigFileSchema = spaceConfigSchema.extend({
303
626
  .min(1)
304
627
  .optional()
305
628
  .describe("Optional space link: a space slug or spc_ id this project publishes to."),
306
- access: spaceAccessConfigSchema.optional(),
307
- publish: spacePublishOptionsSchema.optional(),
629
+ // File-owned naming: the live version's config decides the display name, so
630
+ // it rolls forward and back with the version like every other content fact.
631
+ name: z
632
+ .string()
633
+ .trim()
634
+ .min(1)
635
+ .max(SPACE_CONFIG_CAPS.nameChars)
636
+ .optional()
637
+ .describe("Display name for this space. While set, the API refuses to rename the space."),
638
+ access: spaceConfigFileAccessSchema.optional(),
639
+ runtime: spaceRuntimeSchema.optional(),
308
640
  });
309
641
  /**
310
642
  * The published JSON Schema artifact for `sf.jsonc`, generated from the
@@ -312,12 +644,102 @@ export const spaceConfigFileSchema = spaceConfigSchema.extend({
312
644
  * autocomplete and validation.
313
645
  */
314
646
  export function spaceConfigJsonSchema() {
315
- return stripHiddenSpaceConfigKeys({
647
+ const schema = stripHiddenSpaceConfigKeys({
316
648
  ...z.toJSONSchema(spaceConfigFileSchema, { target: "draft-7", io: "input" }),
317
649
  $id: SPACE_CONFIG_SCHEMA_URL,
318
650
  title: "Spacefast space configuration (sf.jsonc)",
319
651
  });
652
+ // `redirects`, `rewrites` and `headers` are real sf.jsonc keys, owned by the routing
653
+ // compiler rather than by the serving shape above — which is why they are
654
+ // described here instead of in the zod schema that resolves serving. An editor
655
+ // reading this file still has to know they exist, or the documented routing
656
+ // examples read as unknown keys.
657
+ return {
658
+ ...schema,
659
+ properties: {
660
+ ...schema.properties,
661
+ ...SPACE_CONFIG_ROUTING_SCHEMA_PROPERTIES,
662
+ },
663
+ };
320
664
  }
665
+ const SPACE_CONFIG_ROUTING_SCHEMA_PROPERTIES = {
666
+ redirects: {
667
+ type: "array",
668
+ description: "Browser redirect rules. Same matcher grammar as _redirects, and _redirects rules match first.",
669
+ items: {
670
+ type: "object",
671
+ properties: {
672
+ source: {
673
+ type: "string",
674
+ minLength: 1,
675
+ description: "Request path to match. * is a splat, :name captures a segment.",
676
+ },
677
+ destination: {
678
+ type: "string",
679
+ minLength: 1,
680
+ description: "Path or absolute URL. :splat and :name from the source are substituted.",
681
+ },
682
+ status: {
683
+ enum: [301, 302, 303, 307, 308],
684
+ default: 302,
685
+ description: "Redirect status. To serve other content under this URL, use rewrites.",
686
+ },
687
+ },
688
+ required: ["source", "destination"],
689
+ additionalProperties: false,
690
+ },
691
+ },
692
+ rewrites: {
693
+ type: "array",
694
+ description: "Rewrite and proxy rules: the URL stays, the content comes from the destination. _redirects rules match first, then redirects.",
695
+ items: {
696
+ type: "object",
697
+ properties: {
698
+ source: {
699
+ type: "string",
700
+ minLength: 1,
701
+ description: "Request path to match. * is a splat, :name captures a segment.",
702
+ },
703
+ destination: {
704
+ type: "string",
705
+ minLength: 1,
706
+ description: "Path to serve under this URL, or an absolute URL to proxy. :splat and :name from the source are substituted.",
707
+ },
708
+ cache: {
709
+ const: "shared",
710
+ description: "Let shared caches store the proxy response. Absolute-URL destinations only.",
711
+ },
712
+ },
713
+ required: ["source", "destination"],
714
+ additionalProperties: false,
715
+ },
716
+ },
717
+ headers: {
718
+ type: "array",
719
+ description: "Response headers to set on matching requests. A _headers rule wins on the same header name.",
720
+ items: {
721
+ type: "object",
722
+ properties: {
723
+ source: { type: "string", minLength: 1, description: "Request path to match." },
724
+ headers: {
725
+ type: "array",
726
+ minItems: 1,
727
+ items: {
728
+ type: "object",
729
+ properties: {
730
+ key: { type: "string", minLength: 1 },
731
+ value: { type: "string" },
732
+ },
733
+ required: ["key", "value"],
734
+ additionalProperties: false,
735
+ },
736
+ },
737
+ },
738
+ required: ["source", "headers"],
739
+ additionalProperties: false,
740
+ },
741
+ },
742
+ };
321
743
  /*
322
744
  * Hidden-feature discipline (internal-docs/build-pipeline-plan.md): the
323
745
  * `experimental_gutenberg` flag and its `inject` companion are ACCEPTED by the