@spacefast/common 0.0.11 → 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.
- package/dist/agents/connect-targets.d.ts +4 -4
- package/dist/agents/connect-targets.js +3 -3
- package/dist/agents/private-key-oauth.d.ts +37 -0
- package/dist/agents/private-key-oauth.js +158 -0
- package/dist/config/domains.js +1 -1
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +1 -0
- package/dist/config/postgres-budget.d.ts +126 -0
- package/dist/config/postgres-budget.js +135 -0
- package/dist/config/postgres-budget.json +50 -0
- package/dist/contracts/access.d.ts +386 -1183
- package/dist/contracts/access.js +240 -1018
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +11 -19
- package/dist/contracts/api-keys.d.ts +69 -45
- package/dist/contracts/api-keys.js +3 -0
- package/dist/contracts/archives.d.ts +9 -10
- package/dist/contracts/archives.js +4 -6
- package/dist/contracts/builds.d.ts +8 -8
- package/dist/contracts/builds.js +28 -48
- package/dist/contracts/channels.js +1 -2
- package/dist/contracts/comments.d.ts +102 -93
- package/dist/contracts/comments.js +121 -63
- package/dist/contracts/common.d.ts +4 -3
- package/dist/contracts/common.js +2 -0
- package/dist/contracts/countries.js +2 -7
- package/dist/contracts/data-source-routes.d.ts +20 -0
- package/dist/contracts/data-source-routes.js +15 -0
- package/dist/contracts/deployments.d.ts +3 -3
- package/dist/contracts/device-auth.d.ts +21 -15
- package/dist/contracts/docs.d.ts +5 -5
- package/dist/contracts/domains.d.ts +261 -1330
- package/dist/contracts/domains.js +39 -301
- package/dist/contracts/email-preferences.d.ts +50 -0
- package/dist/contracts/email-preferences.js +44 -0
- package/dist/contracts/enums.d.ts +3 -5
- package/dist/contracts/enums.js +3 -8
- package/dist/contracts/error-code-meta.d.ts +99 -33
- package/dist/contracts/error-code-meta.js +36 -9
- package/dist/contracts/error-codes.d.ts +1 -1
- package/dist/contracts/error-codes.js +28 -11
- package/dist/contracts/execution.d.ts +212 -0
- package/dist/contracts/execution.js +288 -0
- package/dist/contracts/feature-lifecycle.d.ts +498 -2
- package/dist/contracts/feature-lifecycle.js +539 -2
- package/dist/contracts/features.d.ts +184 -74
- package/dist/contracts/features.js +4 -303
- package/dist/contracts/functions.d.ts +729 -0
- package/dist/contracts/functions.js +485 -0
- package/dist/contracts/git.d.ts +1 -1
- package/dist/contracts/grants.d.ts +1158 -0
- package/dist/contracts/grants.js +696 -0
- package/dist/contracts/ids.d.ts +6 -5
- package/dist/contracts/ids.js +6 -5
- package/dist/contracts/internal.d.ts +3 -3
- package/dist/contracts/mcp.d.ts +12 -12
- package/dist/contracts/me.js +1 -1
- package/dist/contracts/mounts.d.ts +142 -0
- package/dist/contracts/mounts.js +56 -0
- package/dist/contracts/oauth-resources.d.ts +5 -5
- package/dist/contracts/oauth-resources.js +2 -0
- package/dist/contracts/operations.d.ts +4 -4
- package/dist/contracts/pages.d.ts +6 -14
- package/dist/contracts/pages.js +11 -23
- package/dist/contracts/plan-policy.d.ts +2 -9
- package/dist/contracts/plan-policy.js +1 -4
- package/dist/contracts/platform.d.ts +80 -32
- package/dist/contracts/platform.js +59 -12
- package/dist/contracts/push-new.d.ts +26 -2
- package/dist/contracts/push-new.js +8 -0
- package/dist/contracts/quotas.d.ts +0 -1
- package/dist/contracts/quotas.js +0 -5
- package/dist/contracts/repository-connections.d.ts +1 -1
- package/dist/contracts/resources.d.ts +21 -85
- package/dist/contracts/resources.js +17 -47
- package/dist/contracts/runtime-api.d.ts +799 -192
- package/dist/contracts/runtime-api.js +231 -100
- package/dist/contracts/runtime-app.d.ts +138 -0
- package/dist/contracts/runtime-app.js +21 -0
- package/dist/contracts/sf-config-v1.d.ts +5 -0
- package/dist/contracts/sf-config-v1.js +1 -1
- package/dist/contracts/sites.d.ts +17 -12
- package/dist/contracts/sites.js +30 -12
- package/dist/contracts/space-config.d.ts +46 -117
- package/dist/contracts/space-config.js +323 -25
- package/dist/contracts/spaces.d.ts +392 -256
- package/dist/contracts/spaces.js +163 -83
- package/dist/contracts/superadmin-emails.d.ts +27 -3
- package/dist/contracts/superadmin-emails.js +10 -15
- package/dist/contracts/superadmin-runtime.d.ts +2 -2
- package/dist/contracts/superadmin-spaces.d.ts +19 -142
- package/dist/contracts/superadmin-spaces.js +7 -0
- package/dist/contracts/tags.d.ts +66 -36
- package/dist/contracts/tags.js +195 -11
- package/dist/contracts/teams.d.ts +9 -9
- package/dist/contracts/transfers.d.ts +1 -2
- package/dist/contracts/variables.d.ts +4 -4
- package/dist/contracts/variables.js +4 -3
- package/dist/contracts/webhooks.d.ts +2 -2
- package/dist/contracts/zero.d.ts +437 -193
- package/dist/contracts/zero.js +149 -158
- package/dist/dashboard-paths/index.d.ts +3 -3
- package/dist/dashboard-paths/index.js +1 -0
- package/dist/docs/agent-prose.js +10 -8
- package/dist/docs/agent-setup.d.ts +6 -4
- package/dist/docs/agent-setup.js +26 -16
- package/dist/docs/agent-solutions.js +1 -1
- package/dist/docs/catalog.d.ts +35 -37
- package/dist/docs/catalog.js +36 -38
- package/dist/docs/error-docs.js +108 -44
- package/dist/utils/asset-fingerprint.d.ts +11 -0
- package/dist/utils/asset-fingerprint.js +46 -0
- package/dist/utils/auth-redirect.d.ts +1 -3
- package/dist/utils/auth-redirect.js +1 -21
- package/dist/utils/build-settings.d.ts +2 -0
- package/dist/utils/build-settings.js +61 -9
- package/dist/utils/credential-policy.d.ts +1 -1
- package/dist/utils/credential-policy.js +4 -0
- package/dist/utils/dns-instructions.d.ts +29 -0
- package/dist/utils/dns-instructions.js +78 -0
- package/dist/utils/domain-diagnostics.d.ts +29 -0
- package/dist/utils/domain-diagnostics.js +63 -0
- package/dist/utils/egress-policy.fixtures.json +51 -0
- package/dist/utils/grants.d.ts +53 -0
- package/dist/utils/grants.js +160 -0
- package/dist/utils/id-hints.d.ts +1 -0
- package/dist/utils/id-hints.js +55 -13
- package/dist/utils/local-space-state.js +1 -0
- package/dist/utils/page-colors.d.ts +50 -0
- package/dist/utils/page-colors.js +167 -0
- package/dist/utils/page-fonts.d.ts +2 -0
- package/dist/utils/page-fonts.js +4 -0
- package/dist/utils/pages.d.ts +19 -6
- package/dist/utils/pages.js +122 -49
- package/dist/utils/production-runtime.d.ts +18 -0
- package/dist/utils/production-runtime.js +28 -0
- package/dist/utils/publish-policy.d.ts +34 -4
- package/dist/utils/publish-policy.js +132 -61
- package/dist/utils/query-keys.d.ts +7 -13
- package/dist/utils/query-keys.js +12 -18
- package/dist/utils/source-archive-policy.d.ts +2 -0
- package/dist/utils/source-archive-policy.js +2 -0
- package/dist/utils/space-config.d.ts +5 -1
- package/dist/utils/space-config.js +35 -0
- package/dist/utils/static-runtime-policy.d.ts +5 -0
- package/dist/utils/static-runtime-policy.fixtures.json +57 -0
- package/dist/utils/static-runtime-policy.js +54 -10
- package/dist/utils/upload-session.d.ts +3 -0
- package/dist/utils/upload-session.js +31 -0
- package/package.json +6 -4
- package/dist/contracts/annotations.d.ts +0 -327
- package/dist/contracts/annotations.js +0 -235
- package/dist/contracts/generated-feature-launch-entries.d.ts +0 -2
- package/dist/contracts/generated-feature-launch-entries.js +0 -518
- package/dist/contracts/publishes.d.ts +0 -135
- package/dist/contracts/publishes.js +0 -162
- package/dist/utils/access-match.d.ts +0 -39
- package/dist/utils/access-match.js +0 -296
- package/dist/utils/sf-config-v1.d.ts +0 -2
- package/dist/utils/sf-config-v1.js +0 -542
|
@@ -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
|
|
39
|
-
|
|
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. */
|
|
@@ -128,19 +128,230 @@ export const spaceBuildSettingsSchema = z
|
|
|
128
128
|
// (enums.ts) rather than declaring a second "shared"/"dedicated" enum —
|
|
129
129
|
// space placement mode and normalized site class are the same axis.
|
|
130
130
|
export const spacePlacementModeSchema = normalizedSiteClass;
|
|
131
|
+
// No region knob: a space binds to the site it was placed on at creation and
|
|
132
|
+
// the only placement change that relocates it is shared -> dedicated. Data
|
|
133
|
+
// location is chosen once, at creation, and never changes.
|
|
131
134
|
export const spacePlacementConfigSchema = z
|
|
132
135
|
.object({
|
|
133
|
-
region: z.string().trim().min(1).optional(),
|
|
134
136
|
mode: spacePlacementModeSchema.optional(),
|
|
135
137
|
burstable: z.boolean().optional(),
|
|
136
138
|
})
|
|
137
139
|
.strict()
|
|
138
140
|
.describe("Placement controls for this space.");
|
|
139
|
-
|
|
140
|
-
|
|
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.
|
|
141
352
|
// Meta caps are the spec parser-safety numbers (title 300, description 1000,
|
|
142
353
|
// image 2000); plan policy may lower them but never raise them.
|
|
143
|
-
|
|
354
|
+
const spaceConfigBaseSchema = z.object({
|
|
144
355
|
index: z
|
|
145
356
|
.union([z.string().min(1), z.literal(false)])
|
|
146
357
|
.optional()
|
|
@@ -252,26 +463,31 @@ export const spaceConfigSchema = z.object({
|
|
|
252
463
|
})
|
|
253
464
|
.optional(),
|
|
254
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
|
+
}
|
|
255
487
|
export function spaceSuperpowersEnabled(config) {
|
|
256
488
|
const superpowers = config?.superpowers;
|
|
257
489
|
return superpowers?.enabled !== false && superpowers?.disable_all_injection !== true;
|
|
258
490
|
}
|
|
259
|
-
// The declarative file-lane access/firewall block (internal-docs/
|
|
260
|
-
// internal-docs/access-plan.html, "file" provenance): the SAME unified Rule shape
|
|
261
|
-
// the policy document and the cloud lane use — no parallel contract. Rules
|
|
262
|
-
// committed here compile at publish finalize and version + roll back WITH the
|
|
263
|
-
// content version; live edits without a republish belong to the cloud lane
|
|
264
|
-
// (`sf access`). The file lane lives ONLY in the config FILE — the API `config`
|
|
265
|
-
// overlay never carries it (single-home, no drift).
|
|
266
|
-
export const spaceAccessConfigSchema = z
|
|
267
|
-
.object({
|
|
268
|
-
rules: z
|
|
269
|
-
.array(runtimeRuleSchema)
|
|
270
|
-
.max(SPACE_CONFIG_CAPS.accessRules)
|
|
271
|
-
.default([])
|
|
272
|
-
.describe("Ordered unified access/firewall rules; first match wins."),
|
|
273
|
-
})
|
|
274
|
-
.describe("Declarative access lane: rules compiled at publish that version and roll back with the site.");
|
|
275
491
|
// The `sf.jsonc` FILE shape (internal-docs/platform.md "Space
|
|
276
492
|
// Configuration Files"): SpaceConfig plus the editor `$schema` pointer and the
|
|
277
493
|
// optional `space` link (slug or `spc_` id) resolved by CLI/publish flows.
|
|
@@ -291,7 +507,88 @@ export const spacePublishOptionsSchema = z
|
|
|
291
507
|
"Originals keep serving unchanged. Default off."),
|
|
292
508
|
})
|
|
293
509
|
.describe("Client publish-time options consumed by the Spacefast CLI, not the runtime.");
|
|
294
|
-
|
|
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({
|
|
295
592
|
$schema: z
|
|
296
593
|
.string()
|
|
297
594
|
.optional()
|
|
@@ -301,8 +598,9 @@ export const spaceConfigFileSchema = spaceConfigSchema.extend({
|
|
|
301
598
|
.min(1)
|
|
302
599
|
.optional()
|
|
303
600
|
.describe("Optional space link: a space slug or spc_ id this project publishes to."),
|
|
304
|
-
access: spaceAccessConfigSchema.optional(),
|
|
305
601
|
publish: spacePublishOptionsSchema.optional(),
|
|
602
|
+
access: spaceConfigFileAccessSchema.optional(),
|
|
603
|
+
functions: spaceFunctionsOptionsSchema.optional(),
|
|
306
604
|
});
|
|
307
605
|
/**
|
|
308
606
|
* The published JSON Schema artifact for `sf.jsonc`, generated from the
|