@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.
- package/dist/agents/client-registry.d.ts +22 -0
- package/dist/agents/client-registry.js +41 -0
- package/dist/agents/connect-targets.d.ts +51 -6
- package/dist/agents/connect-targets.js +93 -28
- package/dist/brand.d.ts +0 -11
- package/dist/brand.js +0 -20
- package/dist/config/domains.d.ts +15 -6
- package/dist/config/domains.js +24 -10
- package/dist/config/postgres-budget.d.ts +3 -0
- package/dist/config/postgres-budget.js +6 -1
- package/dist/config/postgres-budget.json +2 -1
- package/dist/contracts/abuse.js +1 -1
- package/dist/contracts/access.d.ts +486 -1196
- package/dist/contracts/access.js +323 -1054
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +11 -15
- package/dist/contracts/api-keys.d.ts +16 -0
- package/dist/contracts/api-keys.js +11 -9
- package/dist/contracts/archives.d.ts +1 -2
- package/dist/contracts/archives.js +15 -17
- package/dist/contracts/beta.js +1 -1
- package/dist/contracts/builds.js +14 -14
- package/dist/contracts/channels.js +2 -2
- package/dist/contracts/comments.d.ts +143 -99
- package/dist/contracts/comments.js +194 -79
- package/dist/contracts/common.d.ts +12 -12
- package/dist/contracts/common.js +33 -30
- package/dist/contracts/continuation.js +5 -5
- 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/device-auth.d.ts +13 -9
- package/dist/contracts/device-auth.js +12 -12
- package/dist/contracts/docs.d.ts +10 -10
- package/dist/contracts/domains.js +39 -33
- package/dist/contracts/enums.d.ts +4 -14
- package/dist/contracts/enums.js +3 -10
- package/dist/contracts/error-code-meta.d.ts +122 -56
- package/dist/contracts/error-code-meta.js +43 -19
- package/dist/contracts/error-codes.d.ts +7 -4
- package/dist/contracts/error-codes.js +48 -24
- package/dist/contracts/events.d.ts +22 -15
- package/dist/contracts/events.js +33 -5
- package/dist/contracts/execution.d.ts +238 -0
- package/dist/contracts/execution.js +309 -0
- package/dist/contracts/feature-lifecycle.d.ts +39 -188
- package/dist/contracts/feature-lifecycle.js +52 -196
- package/dist/contracts/features.d.ts +13 -115
- package/dist/contracts/features.js +5 -35
- package/dist/contracts/functions.d.ts +597 -0
- package/dist/contracts/functions.js +475 -0
- package/dist/contracts/grants.d.ts +1175 -0
- package/dist/contracts/grants.js +783 -0
- package/dist/contracts/ids.d.ts +7 -5
- package/dist/contracts/ids.js +7 -7
- package/dist/contracts/internal.js +2 -2
- package/dist/contracts/mcp.d.ts +17 -556
- package/dist/contracts/mcp.js +9 -103
- package/dist/contracts/me.js +3 -4
- package/dist/contracts/notifications.d.ts +108 -0
- package/dist/contracts/notifications.js +54 -0
- package/dist/contracts/oauth-resources.d.ts +22 -5
- package/dist/contracts/oauth-resources.js +23 -2
- package/dist/contracts/operations.d.ts +0 -48
- package/dist/contracts/operations.js +0 -19
- package/dist/contracts/pages.d.ts +10 -14
- package/dist/contracts/pages.js +17 -21
- package/dist/contracts/plan-policy.d.ts +1 -8
- package/dist/contracts/plan-policy.js +1 -4
- package/dist/contracts/platform.d.ts +78 -30
- package/dist/contracts/platform.js +69 -20
- package/dist/contracts/problem-document.d.ts +14 -0
- package/dist/contracts/problem-document.js +35 -0
- package/dist/contracts/push-new.d.ts +41 -2
- package/dist/contracts/push-new.js +4 -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 +16 -71
- package/dist/contracts/resources.js +13 -16
- package/dist/contracts/runtime-api.d.ts +666 -303
- package/dist/contracts/runtime-api.js +272 -113
- package/dist/contracts/runtime-app.d.ts +168 -0
- package/dist/contracts/runtime-app.js +42 -0
- package/dist/contracts/runtime-db.d.ts +424 -0
- package/dist/contracts/runtime-db.js +108 -0
- package/dist/contracts/runtime-storage.d.ts +120 -0
- package/dist/contracts/runtime-storage.js +40 -0
- package/dist/contracts/sf-config-v1.d.ts +54 -1
- package/dist/contracts/sf-config-v1.js +2 -2
- package/dist/contracts/sites.d.ts +6 -0
- package/dist/contracts/sites.js +30 -11
- package/dist/contracts/space-config.d.ts +91 -121
- package/dist/contracts/space-config.js +464 -42
- package/dist/contracts/spaces.d.ts +300 -171
- package/dist/contracts/spaces.js +128 -98
- package/dist/contracts/superadmin-emails.d.ts +2 -2
- package/dist/contracts/superadmin-spaces.d.ts +26 -130
- package/dist/contracts/superadmin-spaces.js +22 -0
- package/dist/contracts/superadmin-tenants.js +4 -4
- package/dist/contracts/superadmin.d.ts +4 -0
- package/dist/contracts/superadmin.js +14 -1
- package/dist/contracts/tags.d.ts +6 -6
- package/dist/contracts/tags.js +1 -1
- package/dist/contracts/teams.d.ts +9 -9
- package/dist/contracts/teams.js +25 -16
- package/dist/contracts/theme-json.js +1 -1
- package/dist/contracts/transfers.d.ts +0 -1
- package/dist/contracts/transfers.js +4 -4
- package/dist/contracts/variables.js +3 -3
- package/dist/contracts/zero.d.ts +384 -596
- package/dist/contracts/zero.js +167 -299
- package/dist/dashboard-paths/index.d.ts +18 -3
- package/dist/dashboard-paths/index.js +19 -0
- package/dist/docs/agent-handoff-document.d.ts +2 -2
- package/dist/docs/agent-handoff-document.js +8 -7
- package/dist/docs/agent-prose.d.ts +22 -3
- package/dist/docs/agent-prose.js +227 -55
- package/dist/docs/agent-setup.d.ts +14 -11
- package/dist/docs/agent-setup.js +289 -61
- package/dist/docs/agent-solutions.d.ts +6 -6
- package/dist/docs/agent-solutions.js +24 -22
- package/dist/docs/catalog.d.ts +53 -98
- package/dist/docs/catalog.js +90 -117
- package/dist/docs/error-docs.js +304 -236
- package/dist/docs/skill-distribution.d.ts +2 -0
- package/dist/docs/skill-distribution.js +7 -4
- package/dist/docs/skills.d.ts +546 -0
- package/dist/docs/skills.js +340 -0
- package/dist/utils/auth-redirect.d.ts +1 -3
- package/dist/utils/auth-redirect.js +1 -21
- package/dist/utils/browser-credential.d.ts +19 -0
- package/dist/utils/browser-credential.js +62 -0
- package/dist/utils/build-settings.d.ts +2 -0
- package/dist/utils/build-settings.js +61 -9
- package/dist/utils/comment-avatars.d.ts +7 -0
- package/dist/utils/comment-avatars.js +42 -0
- package/dist/utils/content-type.d.ts +3 -3
- package/dist/utils/content-type.js +38 -61
- package/dist/utils/credential-policy.d.ts +1 -1
- package/dist/utils/credential-policy.js +4 -0
- package/dist/utils/domain-diagnostics.d.ts +5 -5
- package/dist/utils/domain-diagnostics.js +21 -10
- package/dist/utils/egress-policy.fixtures.json +1 -1
- package/dist/utils/grants.d.ts +53 -0
- package/dist/utils/grants.js +160 -0
- package/dist/utils/local-space-state.d.ts +78 -4
- package/dist/utils/local-space-state.js +384 -82
- package/dist/utils/one-shot-replay.d.ts +14 -0
- package/dist/utils/one-shot-replay.js +21 -0
- package/dist/utils/page-preview.d.ts +7 -0
- package/dist/utils/page-preview.js +9 -0
- package/dist/utils/pages.d.ts +2 -5
- package/dist/utils/pages.js +64 -33
- package/dist/utils/publish-form-data.d.ts +22 -0
- package/dist/utils/publish-form-data.js +26 -0
- package/dist/utils/publish-policy.d.ts +34 -4
- package/dist/utils/publish-policy.js +149 -62
- package/dist/utils/query-keys.d.ts +8 -10
- package/dist/utils/query-keys.js +14 -10
- package/dist/utils/runtime-paths.d.ts +3 -0
- package/dist/utils/runtime-paths.js +7 -0
- package/dist/utils/secure-local-file.d.ts +73 -0
- package/dist/utils/secure-local-file.js +1373 -0
- package/dist/utils/space-config.d.ts +17 -4
- package/dist/utils/space-config.js +151 -8
- package/dist/utils/static-runtime-policy.d.ts +5 -0
- package/dist/utils/static-runtime-policy.js +54 -10
- package/dist/utils/storage-policy.d.ts +20 -0
- package/dist/utils/storage-policy.fixtures.json +174 -0
- package/dist/utils/storage-policy.js +193 -0
- package/dist/utils/upload-session.d.ts +15 -1
- package/dist/utils/upload-session.js +68 -2
- package/dist/utils/version-file-browser.d.ts +13 -0
- package/dist/utils/version-file-browser.js +59 -0
- package/package.json +20 -1
- package/dist/contracts/mounts.d.ts +0 -142
- package/dist/contracts/mounts.js +0 -56
- package/dist/utils/access-grant.d.ts +0 -14
- package/dist/utils/access-grant.js +0 -88
- package/dist/utils/access-match.d.ts +0 -46
- package/dist/utils/access-match.fixtures.json +0 -513
- package/dist/utils/access-match.js +0 -312
- package/dist/utils/access-verdict.fixtures.json +0 -421
- package/dist/utils/asset-fingerprint.d.ts +0 -53
- package/dist/utils/asset-fingerprint.js +0 -574
- package/dist/utils/sf-config-v1.d.ts +0 -2
- package/dist/utils/sf-config-v1.fixtures.json +0 -9
- package/dist/utils/sf-config-v1.js +0 -551
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
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
|
|
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. */
|
|
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 = `${
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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("
|
|
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("
|
|
296
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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
|