@spacefast/common 0.0.12 → 0.0.13

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 (113) hide show
  1. package/dist/contracts/access.d.ts +386 -1201
  2. package/dist/contracts/access.js +240 -1057
  3. package/dist/contracts/activity.d.ts +1 -1
  4. package/dist/contracts/activity.js +6 -14
  5. package/dist/contracts/api-keys.d.ts +16 -0
  6. package/dist/contracts/api-keys.js +2 -0
  7. package/dist/contracts/archives.d.ts +1 -2
  8. package/dist/contracts/archives.js +4 -6
  9. package/dist/contracts/comments.d.ts +58 -85
  10. package/dist/contracts/comments.js +86 -67
  11. package/dist/contracts/countries.js +2 -7
  12. package/dist/contracts/data-source-routes.d.ts +20 -0
  13. package/dist/contracts/data-source-routes.js +15 -0
  14. package/dist/contracts/device-auth.d.ts +7 -3
  15. package/dist/contracts/enums.d.ts +2 -4
  16. package/dist/contracts/enums.js +3 -8
  17. package/dist/contracts/error-code-meta.d.ts +17 -20
  18. package/dist/contracts/error-code-meta.js +8 -5
  19. package/dist/contracts/error-codes.d.ts +1 -1
  20. package/dist/contracts/error-codes.js +4 -5
  21. package/dist/contracts/events.d.ts +13 -13
  22. package/dist/contracts/execution.d.ts +212 -0
  23. package/dist/contracts/execution.js +288 -0
  24. package/dist/contracts/feature-lifecycle.d.ts +21 -167
  25. package/dist/contracts/feature-lifecycle.js +27 -173
  26. package/dist/contracts/features.d.ts +13 -115
  27. package/dist/contracts/features.js +1 -31
  28. package/dist/contracts/functions.d.ts +729 -0
  29. package/dist/contracts/functions.js +485 -0
  30. package/dist/contracts/git.d.ts +1 -1
  31. package/dist/contracts/grants.d.ts +1158 -0
  32. package/dist/contracts/grants.js +696 -0
  33. package/dist/contracts/ids.d.ts +5 -3
  34. package/dist/contracts/ids.js +5 -3
  35. package/dist/contracts/mcp.d.ts +6 -6
  36. package/dist/contracts/oauth-resources.d.ts +5 -5
  37. package/dist/contracts/oauth-resources.js +1 -0
  38. package/dist/contracts/operations.d.ts +2 -2
  39. package/dist/contracts/pages.d.ts +6 -14
  40. package/dist/contracts/pages.js +6 -23
  41. package/dist/contracts/plan-policy.d.ts +1 -8
  42. package/dist/contracts/plan-policy.js +1 -4
  43. package/dist/contracts/platform.d.ts +78 -30
  44. package/dist/contracts/platform.js +59 -12
  45. package/dist/contracts/push-new.d.ts +26 -2
  46. package/dist/contracts/push-new.js +8 -0
  47. package/dist/contracts/quotas.d.ts +0 -1
  48. package/dist/contracts/quotas.js +0 -5
  49. package/dist/contracts/repository-connections.d.ts +1 -1
  50. package/dist/contracts/resources.d.ts +10 -66
  51. package/dist/contracts/resources.js +4 -10
  52. package/dist/contracts/runtime-api.d.ts +558 -265
  53. package/dist/contracts/runtime-api.js +178 -91
  54. package/dist/contracts/runtime-app.d.ts +138 -0
  55. package/dist/contracts/runtime-app.js +21 -0
  56. package/dist/contracts/sf-config-v1.d.ts +5 -0
  57. package/dist/contracts/sites.d.ts +5 -0
  58. package/dist/contracts/sites.js +12 -1
  59. package/dist/contracts/space-config.d.ts +46 -116
  60. package/dist/contracts/space-config.js +320 -24
  61. package/dist/contracts/spaces.d.ts +253 -158
  62. package/dist/contracts/spaces.js +68 -54
  63. package/dist/contracts/superadmin-emails.d.ts +2 -2
  64. package/dist/contracts/superadmin-spaces.d.ts +15 -130
  65. package/dist/contracts/superadmin-spaces.js +7 -0
  66. package/dist/contracts/tags.d.ts +23 -23
  67. package/dist/contracts/teams.d.ts +9 -9
  68. package/dist/contracts/transfers.d.ts +0 -1
  69. package/dist/contracts/zero.d.ts +195 -273
  70. package/dist/contracts/zero.js +78 -206
  71. package/dist/dashboard-paths/index.d.ts +2 -2
  72. package/dist/dashboard-paths/index.js +1 -0
  73. package/dist/docs/agent-prose.js +10 -8
  74. package/dist/docs/agent-setup.d.ts +2 -2
  75. package/dist/docs/agent-setup.js +11 -9
  76. package/dist/docs/agent-solutions.js +1 -1
  77. package/dist/docs/catalog.d.ts +28 -28
  78. package/dist/docs/catalog.js +29 -29
  79. package/dist/docs/error-docs.js +23 -27
  80. package/dist/utils/asset-fingerprint.d.ts +11 -0
  81. package/dist/utils/asset-fingerprint.js +46 -0
  82. package/dist/utils/auth-redirect.d.ts +1 -3
  83. package/dist/utils/auth-redirect.js +1 -21
  84. package/dist/utils/build-settings.d.ts +2 -0
  85. package/dist/utils/build-settings.js +61 -9
  86. package/dist/utils/credential-policy.d.ts +1 -1
  87. package/dist/utils/credential-policy.js +4 -0
  88. package/dist/utils/egress-policy.fixtures.json +1 -1
  89. package/dist/utils/grants.d.ts +53 -0
  90. package/dist/utils/grants.js +160 -0
  91. package/dist/utils/local-space-state.js +1 -0
  92. package/dist/utils/pages.d.ts +11 -5
  93. package/dist/utils/pages.js +19 -32
  94. package/dist/utils/publish-policy.d.ts +34 -4
  95. package/dist/utils/publish-policy.js +132 -61
  96. package/dist/utils/query-keys.d.ts +1 -6
  97. package/dist/utils/query-keys.js +1 -6
  98. package/dist/utils/space-config.d.ts +5 -1
  99. package/dist/utils/space-config.js +35 -0
  100. package/dist/utils/static-runtime-policy.d.ts +5 -0
  101. package/dist/utils/static-runtime-policy.js +54 -10
  102. package/dist/utils/upload-session.d.ts +3 -0
  103. package/dist/utils/upload-session.js +31 -0
  104. package/package.json +2 -1
  105. package/dist/utils/access-grant.d.ts +0 -14
  106. package/dist/utils/access-grant.js +0 -88
  107. package/dist/utils/access-match.d.ts +0 -46
  108. package/dist/utils/access-match.fixtures.json +0 -513
  109. package/dist/utils/access-match.js +0 -312
  110. package/dist/utils/access-verdict.fixtures.json +0 -421
  111. package/dist/utils/sf-config-v1.d.ts +0 -2
  112. package/dist/utils/sf-config-v1.fixtures.json +0 -9
  113. package/dist/utils/sf-config-v1.js +0 -551
@@ -17,8 +17,8 @@ export declare const SPACE_CONFIG_CAPS: {
17
17
  readonly injectSnippetBytes: number;
18
18
  /** Max snippets per `inject` array. */
19
19
  readonly injectSnippets: 16;
20
- /** Max `access.rules` entries in the file lane. */
21
- readonly accessRules: 100;
20
+ /** Max public WordPress sources exposed to repository builds. */
21
+ readonly dataSources: 32;
22
22
  /** Max `theme` color token length. */
23
23
  readonly themeColorChars: 64;
24
24
  /** Max `theme.logo` URL/path length. */
@@ -63,6 +63,17 @@ export declare const spacePlacementConfigSchema: z.ZodObject<{
63
63
  burstable: z.ZodOptional<z.ZodBoolean>;
64
64
  }, z.core.$strict>;
65
65
  export type SpacePlacementConfig = z.infer<typeof spacePlacementConfigSchema>;
66
+ export declare const dataSourceNameSchema: z.ZodString;
67
+ export declare const wordpressDataSourceUrlSchema: z.ZodString;
68
+ export declare const wordpressDataSourceSchema: z.ZodObject<{
69
+ kind: z.ZodLiteral<"wordpress">;
70
+ url: z.ZodString;
71
+ }, z.core.$strict>;
72
+ export type WordPressDataSource = z.infer<typeof wordpressDataSourceSchema>;
73
+ export declare const spaceDataSourcesSchema: z.ZodPreprocess<z.ZodRecord<z.ZodString, z.ZodObject<{
74
+ kind: z.ZodLiteral<"wordpress">;
75
+ url: z.ZodString;
76
+ }, z.core.$strict>>>;
66
77
  export declare const spaceConfigSchema: z.ZodObject<{
67
78
  index: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
68
79
  fallback: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
@@ -141,70 +152,38 @@ export declare const spaceConfigSchema: z.ZodObject<{
141
152
  bodyEnd: z.ZodOptional<z.ZodArray<z.ZodString>>;
142
153
  noscript: z.ZodOptional<z.ZodArray<z.ZodString>>;
143
154
  }, z.core.$strip>>;
155
+ dataSources: z.ZodOptional<z.ZodPreprocess<z.ZodRecord<z.ZodString, z.ZodObject<{
156
+ kind: z.ZodLiteral<"wordpress">;
157
+ url: z.ZodString;
158
+ }, z.core.$strict>>>>;
159
+ defaultDataSource: z.ZodOptional<z.ZodString>;
144
160
  }, z.core.$strip>;
145
161
  export type SpaceConfig = z.infer<typeof spaceConfigSchema>;
162
+ export declare function compareDataSourceNames(left: string, right: string): number;
163
+ export declare function resolveDefaultDataSourceName(config: Pick<SpaceConfig, "dataSources" | "defaultDataSource"> | null | undefined): string | null;
146
164
  export declare function spaceSuperpowersEnabled(config: SpaceConfig | null | undefined): boolean;
147
- export declare const spaceAccessConfigSchema: z.ZodObject<{
148
- rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
149
- id: z.ZodOptional<z.ZodString>;
150
- match: z.ZodDefault<z.ZodObject<{
151
- host: z.ZodOptional<z.ZodString>;
152
- hostPattern: z.ZodOptional<z.ZodString>;
153
- hostTemplate: z.ZodOptional<z.ZodString>;
154
- pathPattern: z.ZodOptional<z.ZodString>;
155
- channel: z.ZodOptional<z.ZodString>;
156
- ipCidrs: z.ZodOptional<z.ZodArray<z.ZodString>>;
157
- agent: z.ZodOptional<z.ZodString>;
158
- country: z.ZodOptional<z.ZodString>;
159
- countryNotIn: z.ZodOptional<z.ZodArray<z.ZodString>>;
160
- header: z.ZodOptional<z.ZodObject<{
161
- name: z.ZodString;
162
- value: z.ZodString;
163
- }, z.core.$strip>>;
164
- }, z.core.$strip>>;
165
- effect: z.ZodEnum<{
166
- allow: "allow";
167
- deny: "deny";
168
- challenge: "challenge";
169
- }>;
170
- auth: z.ZodOptional<z.ZodObject<{
171
- requiredGrants: z.ZodArray<z.ZodString>;
172
- issuers: z.ZodOptional<z.ZodArray<z.ZodObject<{
173
- kid: z.ZodString;
174
- alg: z.ZodLiteral<"EdDSA">;
175
- publicKey: z.ZodString;
176
- grantNamespaces: z.ZodArray<z.ZodString>;
177
- }, z.core.$strip>>>;
178
- acquire: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
179
- type: z.ZodLiteral<"password">;
180
- ref: z.ZodString;
181
- transport: z.ZodEnum<{
182
- basic: "basic";
183
- form: "form";
184
- }>;
185
- username: z.ZodOptional<z.ZodString>;
186
- }, z.core.$strip>, z.ZodObject<{
187
- type: z.ZodLiteral<"login">;
188
- url: z.ZodString;
189
- label: z.ZodOptional<z.ZodString>;
190
- }, z.core.$strip>], "type">>>;
191
- }, z.core.$strip>>;
192
- managedBy: z.ZodOptional<z.ZodEnum<{
193
- sharing: "sharing";
194
- firewall: "firewall";
195
- file_share: "file_share";
196
- cast_reviewer: "cast_reviewer";
197
- team_default: "team_default";
198
- }>>;
199
- expiresAt: z.ZodOptional<z.ZodNumber>;
200
- reasonCode: z.ZodOptional<z.ZodString>;
201
- message: z.ZodOptional<z.ZodString>;
202
- }, z.core.$strip>>>;
203
- }, z.core.$strip>;
204
165
  export declare const spacePublishOptionsSchema: z.ZodObject<{
205
166
  fingerprint: z.ZodOptional<z.ZodBoolean>;
206
167
  }, z.core.$strip>;
207
168
  export type SpacePublishOptions = z.infer<typeof spacePublishOptionsSchema>;
169
+ export declare const spaceConfigFileAccessSchema: z.ZodObject<{
170
+ public: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
171
+ }, z.core.$strict>;
172
+ export type SpaceConfigFileAccess = z.infer<typeof spaceConfigFileAccessSchema>;
173
+ /**
174
+ * Overrides for the worker `sf publish` compiles.
175
+ *
176
+ * Every field is optional because the normal case is detection: a project with
177
+ * `handler.ts` or an OpenNext build needs no configuration at all. These exist
178
+ * for the layouts detection cannot guess — a worker somewhere unusual, or a
179
+ * compatibility date the author wants pinned rather than defaulted.
180
+ */
181
+ export declare const spaceFunctionsOptionsSchema: z.ZodObject<{
182
+ entry: z.ZodOptional<z.ZodString>;
183
+ database: z.ZodDefault<z.ZodBoolean>;
184
+ compatibilityDate: z.ZodOptional<z.ZodString>;
185
+ }, z.core.$strip>;
186
+ export type SpaceFunctionsOptions = z.infer<typeof spaceFunctionsOptionsSchema>;
208
187
  export declare const spaceConfigFileSchema: z.ZodObject<{
209
188
  index: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
210
189
  fallback: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
@@ -285,66 +264,17 @@ export declare const spaceConfigFileSchema: z.ZodObject<{
285
264
  }, z.core.$strip>>;
286
265
  $schema: z.ZodOptional<z.ZodString>;
287
266
  space: z.ZodOptional<z.ZodString>;
288
- access: z.ZodOptional<z.ZodObject<{
289
- rules: z.ZodDefault<z.ZodArray<z.ZodObject<{
290
- id: z.ZodOptional<z.ZodString>;
291
- match: z.ZodDefault<z.ZodObject<{
292
- host: z.ZodOptional<z.ZodString>;
293
- hostPattern: z.ZodOptional<z.ZodString>;
294
- hostTemplate: z.ZodOptional<z.ZodString>;
295
- pathPattern: z.ZodOptional<z.ZodString>;
296
- channel: z.ZodOptional<z.ZodString>;
297
- ipCidrs: z.ZodOptional<z.ZodArray<z.ZodString>>;
298
- agent: z.ZodOptional<z.ZodString>;
299
- country: z.ZodOptional<z.ZodString>;
300
- countryNotIn: z.ZodOptional<z.ZodArray<z.ZodString>>;
301
- header: z.ZodOptional<z.ZodObject<{
302
- name: z.ZodString;
303
- value: z.ZodString;
304
- }, z.core.$strip>>;
305
- }, z.core.$strip>>;
306
- effect: z.ZodEnum<{
307
- allow: "allow";
308
- deny: "deny";
309
- challenge: "challenge";
310
- }>;
311
- auth: z.ZodOptional<z.ZodObject<{
312
- requiredGrants: z.ZodArray<z.ZodString>;
313
- issuers: z.ZodOptional<z.ZodArray<z.ZodObject<{
314
- kid: z.ZodString;
315
- alg: z.ZodLiteral<"EdDSA">;
316
- publicKey: z.ZodString;
317
- grantNamespaces: z.ZodArray<z.ZodString>;
318
- }, z.core.$strip>>>;
319
- acquire: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
320
- type: z.ZodLiteral<"password">;
321
- ref: z.ZodString;
322
- transport: z.ZodEnum<{
323
- basic: "basic";
324
- form: "form";
325
- }>;
326
- username: z.ZodOptional<z.ZodString>;
327
- }, z.core.$strip>, z.ZodObject<{
328
- type: z.ZodLiteral<"login">;
329
- url: z.ZodString;
330
- label: z.ZodOptional<z.ZodString>;
331
- }, z.core.$strip>], "type">>>;
332
- }, z.core.$strip>>;
333
- managedBy: z.ZodOptional<z.ZodEnum<{
334
- sharing: "sharing";
335
- firewall: "firewall";
336
- file_share: "file_share";
337
- cast_reviewer: "cast_reviewer";
338
- team_default: "team_default";
339
- }>>;
340
- expiresAt: z.ZodOptional<z.ZodNumber>;
341
- reasonCode: z.ZodOptional<z.ZodString>;
342
- message: z.ZodOptional<z.ZodString>;
343
- }, z.core.$strip>>>;
344
- }, z.core.$strip>>;
345
267
  publish: z.ZodOptional<z.ZodObject<{
346
268
  fingerprint: z.ZodOptional<z.ZodBoolean>;
347
269
  }, z.core.$strip>>;
270
+ access: z.ZodOptional<z.ZodObject<{
271
+ public: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
272
+ }, z.core.$strict>>;
273
+ functions: z.ZodOptional<z.ZodObject<{
274
+ entry: z.ZodOptional<z.ZodString>;
275
+ database: z.ZodDefault<z.ZodBoolean>;
276
+ compatibilityDate: z.ZodOptional<z.ZodString>;
277
+ }, z.core.$strip>>;
348
278
  }, z.core.$strip>;
349
279
  export type SpaceConfigFile = z.infer<typeof spaceConfigFileSchema>;
350
280
  /**
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { BRAND } from "../brand.js";
3
- import { runtimeRuleSchema } from "./access.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,8 +35,8 @@ 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. */
@@ -138,11 +138,220 @@ export const spacePlacementConfigSchema = z
138
138
  })
139
139
  .strict()
140
140
  .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`.
141
+ const RESERVED_DATA_SOURCE_NAMES = new Set(["__proto__", "constructor", "prototype"]);
142
+ export const dataSourceNameSchema = z
143
+ .string()
144
+ .min(1)
145
+ .max(128)
146
+ .regex(/^[A-Za-z_][A-Za-z0-9_]*$/, {
147
+ message: "Use a non-reserved name that starts with a letter or underscore and contains only letters, numbers, and underscores.",
148
+ })
149
+ .refine((name) => !RESERVED_DATA_SOURCE_NAMES.has(name), {
150
+ message: "Use a non-reserved name that starts with a letter or underscore and contains only letters, numbers, and underscores.",
151
+ })
152
+ .describe("Stable source identifier: 1-128 letters, numbers, or underscores; starts with a letter or underscore; excludes __proto__, constructor, and prototype.")
153
+ .meta({ not: { enum: [...RESERVED_DATA_SOURCE_NAMES] } });
154
+ 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.";
155
+ const WORDPRESS_DATA_SOURCE_URL_PATTERN = /^https:\/\/[^/?#@%\\\s]+(?:\/[^?#\\\s]*)?$/u;
156
+ const WORDPRESS_DATA_SOURCE_WHITESPACE_PATTERN = /\p{White_Space}/u;
157
+ const DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
158
+ const IPV6_LITERAL_PATTERN = /^[0-9a-f:]+$/i;
159
+ const DECIMAL_PORT_PATTERN = /^\d{1,5}$/;
160
+ const IPV4_OCTET_PATTERN = /^(?:0|[1-9]\d{0,2})$/;
161
+ function hasAsciiControlCharacter(value) {
162
+ for (let index = 0; index < value.length; index += 1) {
163
+ const code = value.charCodeAt(index);
164
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f))
165
+ return true;
166
+ }
167
+ return false;
168
+ }
169
+ function isCanonicalIpv4Address(value) {
170
+ const octets = value.split(".");
171
+ return (octets.length === 4 &&
172
+ octets.every((octet) => IPV4_OCTET_PATTERN.test(octet) && Number.parseInt(octet, 10) <= 255));
173
+ }
174
+ function isValidDnsHostname(value) {
175
+ if (value.length === 0 || value.length > 253 || value.endsWith("."))
176
+ return false;
177
+ const labels = value.split(".");
178
+ return labels.every((label) => label.length > 0 && label.length <= 63 && DNS_LABEL_PATTERN.test(label));
179
+ }
180
+ function parseWordPressAuthority(authority) {
181
+ if (!authority || authority.includes("@") || authority.includes("%"))
182
+ return null;
183
+ let rawHostname;
184
+ let rawPort = null;
185
+ let ipv6 = false;
186
+ if (authority.startsWith("[")) {
187
+ const closingBracket = authority.indexOf("]");
188
+ if (closingBracket <= 1 || authority.indexOf("]", closingBracket + 1) !== -1)
189
+ return null;
190
+ rawHostname = authority.slice(1, closingBracket);
191
+ const suffix = authority.slice(closingBracket + 1);
192
+ if (suffix) {
193
+ if (!suffix.startsWith(":"))
194
+ return null;
195
+ rawPort = suffix.slice(1);
196
+ }
197
+ ipv6 = true;
198
+ }
199
+ else {
200
+ if (authority.includes("[") || authority.includes("]"))
201
+ return null;
202
+ const firstColon = authority.indexOf(":");
203
+ if (firstColon !== authority.lastIndexOf(":"))
204
+ return null;
205
+ if (firstColon === -1) {
206
+ rawHostname = authority;
207
+ }
208
+ else {
209
+ rawHostname = authority.slice(0, firstColon);
210
+ rawPort = authority.slice(firstColon + 1);
211
+ }
212
+ }
213
+ if (!rawHostname || rawHostname.endsWith("."))
214
+ return null;
215
+ if (rawPort !== null) {
216
+ if (!DECIMAL_PORT_PATTERN.test(rawPort))
217
+ return null;
218
+ const port = Number.parseInt(rawPort, 10);
219
+ if (port === 0 || port > 65_535)
220
+ return null;
221
+ }
222
+ try {
223
+ const url = new URL(`https://${authority}/`);
224
+ if (ipv6) {
225
+ if (!IPV6_LITERAL_PATTERN.test(rawHostname))
226
+ return null;
227
+ if (!url.hostname.startsWith("[") || !url.hostname.endsWith("]"))
228
+ return null;
229
+ }
230
+ else {
231
+ const normalizedHostname = url.hostname;
232
+ if (normalizedHostname.startsWith("[") || normalizedHostname.endsWith("]"))
233
+ return null;
234
+ if (isCanonicalIpv4Address(normalizedHostname)) {
235
+ if (rawHostname !== normalizedHostname || !isCanonicalIpv4Address(rawHostname))
236
+ return null;
237
+ }
238
+ else if (!isValidDnsHostname(normalizedHostname)) {
239
+ return null;
240
+ }
241
+ }
242
+ return url.origin;
243
+ }
244
+ catch {
245
+ return null;
246
+ }
247
+ }
248
+ function canonicalizeWordPressPath(rawPath) {
249
+ if (!rawPath)
250
+ return "";
251
+ // Reject encoded percents so validation has one authoritative decode layer.
252
+ // Encoded path separators are never valid in a WordPress source base path.
253
+ if (/%(?:25|2f|5c)/i.test(rawPath))
254
+ return null;
255
+ let decodedPath = rawPath;
256
+ if (rawPath.includes("%")) {
257
+ const encodedSegments = rawPath.split("/");
258
+ let nextPath;
259
+ try {
260
+ nextPath = decodeURIComponent(rawPath);
261
+ void encodeURIComponent(nextPath);
262
+ }
263
+ catch {
264
+ return null;
265
+ }
266
+ const decodedSegments = nextPath.split("/");
267
+ if (decodedSegments.some((segment, index) => (segment === "." || segment === "..") && encodedSegments[index] !== segment)) {
268
+ return null;
269
+ }
270
+ if (hasAsciiControlCharacter(nextPath) ||
271
+ nextPath.includes("\\") ||
272
+ WORDPRESS_DATA_SOURCE_WHITESPACE_PATTERN.test(nextPath.replaceAll(" ", ""))) {
273
+ return null;
274
+ }
275
+ decodedPath = nextPath;
276
+ }
277
+ const url = new URL("https://path.invalid/");
278
+ url.pathname = decodedPath;
279
+ return url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, "");
280
+ }
281
+ function canonicalWordPressDataSourceUrl(value) {
282
+ if (!value.startsWith("https://") ||
283
+ hasAsciiControlCharacter(value) ||
284
+ WORDPRESS_DATA_SOURCE_WHITESPACE_PATTERN.test(value) ||
285
+ value.includes("\\") ||
286
+ value.includes("?") ||
287
+ value.includes("#")) {
288
+ return null;
289
+ }
290
+ const remainder = value.slice("https://".length);
291
+ const pathIndex = remainder.indexOf("/");
292
+ const authority = pathIndex === -1 ? remainder : remainder.slice(0, pathIndex);
293
+ const rawPath = pathIndex === -1 ? "" : remainder.slice(pathIndex);
294
+ const origin = parseWordPressAuthority(authority);
295
+ const pathname = canonicalizeWordPressPath(rawPath);
296
+ return origin && pathname !== null ? `${origin}${pathname}` : null;
297
+ }
298
+ export const wordpressDataSourceUrlSchema = z
299
+ .string()
300
+ .min(1)
301
+ .max(2048)
302
+ .refine((value) => !hasAsciiControlCharacter(value), {
303
+ message: WORDPRESS_DATA_SOURCE_URL_MESSAGE,
304
+ })
305
+ .regex(WORDPRESS_DATA_SOURCE_URL_PATTERN, { message: WORDPRESS_DATA_SOURCE_URL_MESSAGE })
306
+ .url({ message: WORDPRESS_DATA_SOURCE_URL_MESSAGE })
307
+ .refine((value) => canonicalWordPressDataSourceUrl(value) !== null, {
308
+ message: WORDPRESS_DATA_SOURCE_URL_MESSAGE,
309
+ })
310
+ .overwrite((value) => canonicalWordPressDataSourceUrl(value) ?? value)
311
+ .max(2048)
312
+ .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.");
313
+ export const wordpressDataSourceSchema = z
314
+ .object({
315
+ kind: z.literal("wordpress"),
316
+ url: wordpressDataSourceUrlSchema,
317
+ })
318
+ .strict()
319
+ .describe("A public WordPress site exposed to repository builds as a content source.");
320
+ function rejectReservedDataSourceRecordKeys(value) {
321
+ if (typeof value === "object" &&
322
+ value !== null &&
323
+ !Array.isArray(value) &&
324
+ Object.keys(value).some((name) => RESERVED_DATA_SOURCE_NAMES.has(name))) {
325
+ return null;
326
+ }
327
+ return value;
328
+ }
329
+ export const spaceDataSourcesSchema = z
330
+ .preprocess(rejectReservedDataSourceRecordKeys, z
331
+ .record(dataSourceNameSchema, wordpressDataSourceSchema)
332
+ .refine((sources) => Object.keys(sources).length <= SPACE_CONFIG_CAPS.dataSources, {
333
+ message: `dataSources supports up to ${SPACE_CONFIG_CAPS.dataSources} entries.`,
334
+ }))
335
+ .meta({
336
+ maxProperties: SPACE_CONFIG_CAPS.dataSources,
337
+ description: `Named public WordPress sources. Supports at most ${SPACE_CONFIG_CAPS.dataSources} entries.`,
338
+ });
339
+ function refineDefaultDataSource(config, ctx) {
340
+ if (config.defaultDataSource !== undefined &&
341
+ !Object.prototype.hasOwnProperty.call(config.dataSources ?? {}, config.defaultDataSource)) {
342
+ ctx.addIssue({
343
+ code: "custom",
344
+ message: "defaultDataSource must name a configured dataSources entry.",
345
+ path: ["defaultDataSource"],
346
+ });
347
+ }
348
+ }
349
+ // Shared SpaceConfig fields (internal-docs/platform.md "SpaceConfig"): the
350
+ // declarative serving config also accepted by `sf.jsonc`. Cloud-only overlay
351
+ // fields such as pre-build data sources are added to `spaceConfigSchema` below.
143
352
  // Meta caps are the spec parser-safety numbers (title 300, description 1000,
144
353
  // image 2000); plan policy may lower them but never raise them.
145
- export const spaceConfigSchema = z.object({
354
+ const spaceConfigBaseSchema = z.object({
146
355
  index: z
147
356
  .union([z.string().min(1), z.literal(false)])
148
357
  .optional()
@@ -254,26 +463,31 @@ export const spaceConfigSchema = z.object({
254
463
  })
255
464
  .optional(),
256
465
  });
466
+ export const spaceConfigSchema = spaceConfigBaseSchema
467
+ .extend({
468
+ dataSources: spaceDataSourcesSchema
469
+ .optional()
470
+ .describe(`Named public WordPress sources exposed to repository builds; at most ${SPACE_CONFIG_CAPS.dataSources} entries.`),
471
+ defaultDataSource: dataSourceNameSchema
472
+ .optional()
473
+ .describe("Name of the configured source exposed through WORDPRESS_URL. Must match a dataSources entry; when omitted, the first source in bytewise name order is used."),
474
+ })
475
+ .superRefine(refineDefaultDataSource);
476
+ export function compareDataSourceNames(left, right) {
477
+ return left < right ? -1 : left > right ? 1 : 0;
478
+ }
479
+ export function resolveDefaultDataSourceName(config) {
480
+ const names = Object.keys(config?.dataSources ?? {}).toSorted(compareDataSourceNames);
481
+ const configuredDefault = config?.defaultDataSource;
482
+ return configuredDefault &&
483
+ Object.prototype.hasOwnProperty.call(config?.dataSources ?? {}, configuredDefault)
484
+ ? configuredDefault
485
+ : (names[0] ?? null);
486
+ }
257
487
  export function spaceSuperpowersEnabled(config) {
258
488
  const superpowers = config?.superpowers;
259
489
  return superpowers?.enabled !== false && superpowers?.disable_all_injection !== true;
260
490
  }
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
491
  // The `sf.jsonc` FILE shape (internal-docs/platform.md "Space
278
492
  // Configuration Files"): SpaceConfig plus the editor `$schema` pointer and the
279
493
  // optional `space` link (slug or `spc_` id) resolved by CLI/publish flows.
@@ -293,7 +507,88 @@ export const spacePublishOptionsSchema = z
293
507
  "Originals keep serving unchanged. Default off."),
294
508
  })
295
509
  .describe("Client publish-time options consumed by the Spacefast CLI, not the runtime.");
296
- export const spaceConfigFileSchema = spaceConfigSchema.extend({
510
+ const spaceConfigPublicPatternSchema = z
511
+ .string()
512
+ .min(1)
513
+ .max(1025)
514
+ .transform((candidate, context) => {
515
+ const excluded = candidate.startsWith("!");
516
+ try {
517
+ const normalized = normalizeGrantResourcePattern(excluded ? candidate.slice(1) : candidate);
518
+ return excluded ? `!${normalized}` : normalized;
519
+ }
520
+ catch (error) {
521
+ context.addIssue({
522
+ code: "custom",
523
+ message: error instanceof Error ? error.message : "Invalid access.public resource pattern.",
524
+ });
525
+ return z.NEVER;
526
+ }
527
+ });
528
+ export const spaceConfigFileAccessSchema = z
529
+ .object({
530
+ public: z
531
+ .array(spaceConfigPublicPatternSchema)
532
+ .max(GRANT_RESOURCE_PATTERN_LIMIT)
533
+ .describe("Always-public live paths. Prefix exclusions with !; * and ** are whole-segment wildcards."),
534
+ })
535
+ .strict()
536
+ .superRefine((value, context) => {
537
+ const seen = new Set();
538
+ let inclusionCount = 0;
539
+ value.public.forEach((pattern, index) => {
540
+ if (seen.has(pattern)) {
541
+ context.addIssue({
542
+ code: "custom",
543
+ path: ["public", index],
544
+ message: "access.public patterns must be unique.",
545
+ });
546
+ }
547
+ seen.add(pattern);
548
+ if (!pattern.startsWith("!"))
549
+ inclusionCount += 1;
550
+ });
551
+ if (value.public.length > 0 && inclusionCount === 0) {
552
+ context.addIssue({
553
+ code: "custom",
554
+ path: ["public"],
555
+ message: "access.public requires at least one inclusion pattern.",
556
+ });
557
+ }
558
+ })
559
+ .describe("File-only public access declaration. It is inactive until the Space is claimed.");
560
+ /**
561
+ * Overrides for the worker `sf publish` compiles.
562
+ *
563
+ * Every field is optional because the normal case is detection: a project with
564
+ * `handler.ts` or an OpenNext build needs no configuration at all. These exist
565
+ * for the layouts detection cannot guess — a worker somewhere unusual, or a
566
+ * compatibility date the author wants pinned rather than defaulted.
567
+ */
568
+ export const spaceFunctionsOptionsSchema = z
569
+ .object({
570
+ entry: z
571
+ .string()
572
+ .optional()
573
+ .describe("Path to the worker entry module, relative to the published directory. " +
574
+ "Detected automatically when absent; naming one that does not exist is an error " +
575
+ "rather than a silent static publish."),
576
+ database: z
577
+ .boolean()
578
+ .default(false)
579
+ .describe("Give the worker `env.DB`, a D1-shaped binding to this space's database. " +
580
+ "Declared rather than detected: a capability an author did not ask for is " +
581
+ "authority they cannot see, and reading it out of minified build output " +
582
+ "would be a guess. Off by default, so a worker reaches nothing until it says so."),
583
+ compatibilityDate: z
584
+ .string()
585
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
586
+ .optional()
587
+ .describe("Runtime compatibility date this worker was written against. Pinning it makes " +
588
+ "rollback honest: promoting an old version restores the semantics it was built with."),
589
+ })
590
+ .describe("Overrides for the worker compiled at publish time.");
591
+ export const spaceConfigFileSchema = spaceConfigBaseSchema.extend({
297
592
  $schema: z
298
593
  .string()
299
594
  .optional()
@@ -303,8 +598,9 @@ export const spaceConfigFileSchema = spaceConfigSchema.extend({
303
598
  .min(1)
304
599
  .optional()
305
600
  .describe("Optional space link: a space slug or spc_ id this project publishes to."),
306
- access: spaceAccessConfigSchema.optional(),
307
601
  publish: spacePublishOptionsSchema.optional(),
602
+ access: spaceConfigFileAccessSchema.optional(),
603
+ functions: spaceFunctionsOptionsSchema.optional(),
308
604
  });
309
605
  /**
310
606
  * The published JSON Schema artifact for `sf.jsonc`, generated from the