@spacefast/common 0.0.5 → 0.0.7
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/brand-assets/spacefast-favicon.svg +16 -0
- package/brand-assets/spacefast-sf-full-bleed.svg +13 -0
- package/brand-assets/spacefast-wordmark.svg +5 -0
- package/dist/agents/connect-targets.d.ts +138 -0
- package/dist/agents/connect-targets.js +243 -0
- package/dist/brand-assets-build.d.ts +26 -0
- package/dist/brand-assets-build.js +81 -0
- package/dist/brand-assets.d.ts +42 -0
- package/dist/brand-assets.js +30 -0
- package/dist/brand.d.ts +1 -0
- package/dist/brand.js +5 -0
- package/dist/config/domains.d.ts +12 -6
- package/dist/config/domains.js +30 -38
- package/dist/contracts/access.d.ts +967 -124
- package/dist/contracts/access.js +804 -148
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +28 -2
- package/dist/contracts/annotations.d.ts +2 -4
- package/dist/contracts/api-keys.d.ts +126 -4
- package/dist/contracts/api-keys.js +74 -6
- package/dist/contracts/archives.d.ts +8 -2
- package/dist/contracts/archives.js +11 -4
- package/dist/contracts/billing.d.ts +0 -9
- package/dist/contracts/builds.d.ts +65 -21
- package/dist/contracts/builds.js +29 -0
- package/dist/contracts/common.d.ts +2 -0
- package/dist/contracts/common.js +4 -0
- package/dist/contracts/continuation.d.ts +0 -3
- package/dist/contracts/countries.d.ts +2 -0
- package/dist/contracts/countries.js +261 -0
- package/dist/contracts/deployments.d.ts +34 -34
- package/dist/contracts/device-auth.d.ts +4 -11
- package/dist/contracts/device-auth.js +4 -0
- package/dist/contracts/docs.d.ts +119 -0
- package/dist/contracts/docs.js +51 -0
- package/dist/contracts/domains.d.ts +96 -54
- package/dist/contracts/domains.js +26 -4
- package/dist/contracts/enums.d.ts +15 -16
- package/dist/contracts/enums.js +26 -14
- package/dist/contracts/error-code-meta.d.ts +184 -0
- package/dist/contracts/error-code-meta.js +124 -10
- package/dist/contracts/error-codes.d.ts +1 -1
- package/dist/contracts/error-codes.js +42 -0
- package/dist/contracts/events.d.ts +1 -1
- package/dist/contracts/feature-lifecycle-core.d.ts +36 -0
- package/dist/contracts/feature-lifecycle-core.js +220 -0
- package/dist/contracts/feature-lifecycle.d.ts +7 -0
- package/dist/contracts/feature-lifecycle.js +31 -0
- package/dist/contracts/features.d.ts +76 -6
- package/dist/contracts/features.js +178 -13
- package/dist/contracts/generated-feature-launch-entries.d.ts +2 -0
- package/dist/contracts/generated-feature-launch-entries.js +518 -0
- package/dist/contracts/git.d.ts +2 -8
- package/dist/contracts/ids.d.ts +9 -0
- package/dist/contracts/ids.js +13 -0
- package/dist/contracts/mcp.d.ts +8 -10
- package/dist/contracts/me.js +1 -1
- package/dist/contracts/oauth-resources.d.ts +19 -8
- package/dist/contracts/oauth-resources.js +44 -13
- package/dist/contracts/operations.d.ts +161 -1
- package/dist/contracts/operations.js +49 -1
- package/dist/contracts/platform.d.ts +6 -102
- package/dist/contracts/platform.js +6 -50
- package/dist/contracts/publishes.d.ts +130 -0
- package/dist/contracts/publishes.js +145 -0
- package/dist/contracts/push-new.d.ts +4 -4
- package/dist/contracts/quotas.d.ts +2 -0
- package/dist/contracts/quotas.js +8 -0
- package/dist/contracts/repository-connections.d.ts +9 -8
- package/dist/contracts/repository-connections.js +13 -0
- package/dist/contracts/resources.d.ts +99 -13
- package/dist/contracts/resources.js +36 -4
- package/dist/contracts/routes.d.ts +0 -2
- package/dist/contracts/runtime-api.d.ts +363 -39
- package/dist/contracts/runtime-api.js +276 -6
- package/dist/contracts/sf-config-v1.d.ts +75 -0
- package/dist/contracts/sf-config-v1.js +6 -0
- package/dist/contracts/sites.d.ts +19 -2
- package/dist/contracts/sites.js +21 -0
- package/dist/contracts/space-config.d.ts +201 -2
- package/dist/contracts/space-config.js +135 -5
- package/dist/contracts/spaces.d.ts +376 -196
- package/dist/contracts/spaces.js +62 -10
- package/dist/contracts/superadmin-emails.d.ts +4 -2
- package/dist/contracts/superadmin-emails.js +8 -0
- package/dist/contracts/superadmin-queues.d.ts +26 -26
- package/dist/contracts/superadmin-spaces.d.ts +172 -42
- package/dist/contracts/superadmin-tenants.d.ts +6 -6
- package/dist/contracts/superadmin-tenants.js +6 -11
- package/dist/contracts/superadmin.d.ts +6 -93
- package/dist/contracts/superadmin.js +6 -14
- package/dist/contracts/tags.d.ts +10 -9
- package/dist/contracts/tags.js +2 -1
- package/dist/contracts/teams.d.ts +26 -30
- package/dist/contracts/teams.js +11 -17
- package/dist/contracts/theme-json.d.ts +30 -0
- package/dist/contracts/theme-json.js +48 -0
- package/dist/contracts/variables.d.ts +0 -2
- package/dist/contracts/variables.js +0 -5
- package/dist/contracts/webhooks.d.ts +2 -0
- package/dist/contracts/webhooks.js +1 -1
- package/dist/contracts/zero.d.ts +5 -10
- package/dist/docs/agent-prose.d.ts +43 -3
- package/dist/docs/agent-prose.js +146 -7
- package/dist/docs/agent-setup.d.ts +51 -0
- package/dist/docs/agent-setup.js +211 -14
- package/dist/docs/agent-solutions.d.ts +5 -5
- package/dist/docs/agent-solutions.js +2 -2
- package/dist/docs/catalog.d.ts +650 -0
- package/dist/docs/catalog.js +758 -0
- package/dist/docs/error-docs.d.ts +0 -2
- package/dist/docs/error-docs.js +189 -35
- package/dist/docs/index-build.d.ts +17 -0
- package/dist/docs/index-build.js +24 -0
- package/dist/docs/search.d.ts +54 -0
- package/dist/docs/search.js +96 -0
- package/dist/docs/skill-distribution.d.ts +5 -3
- package/dist/docs/skill-distribution.js +8 -8
- package/dist/slug-policy/blocklist.d.ts +4 -0
- package/dist/slug-policy/blocklist.js +122 -0
- package/dist/slug-policy/index.d.ts +17 -0
- package/dist/slug-policy/index.js +86 -0
- package/dist/utils/access-match.d.ts +39 -0
- package/dist/utils/access-match.js +296 -0
- package/dist/utils/auth-redirect.d.ts +3 -1
- package/dist/utils/auth-redirect.js +21 -1
- package/dist/utils/build-settings.d.ts +6 -0
- package/dist/utils/build-settings.js +429 -46
- package/dist/utils/claim-token.d.ts +8 -0
- package/dist/utils/claim-token.js +23 -2
- package/dist/utils/concurrency.d.ts +1 -0
- package/dist/utils/concurrency.js +22 -0
- package/dist/utils/content-type.d.ts +1 -0
- package/dist/utils/content-type.js +1 -1
- package/dist/utils/credential-policy.d.ts +54 -0
- package/dist/utils/{access-policy.js → credential-policy.js} +28 -2
- package/dist/utils/email.d.ts +1 -0
- package/dist/utils/email.js +3 -0
- package/dist/utils/gate-theme.d.ts +61 -0
- package/dist/utils/gate-theme.js +217 -0
- package/dist/utils/git-repository.d.ts +0 -1
- package/dist/utils/git-repository.js +0 -3
- package/dist/utils/id-hints.d.ts +8 -0
- package/dist/utils/id-hints.js +61 -0
- package/dist/utils/local-space-state.d.ts +91 -0
- package/dist/utils/local-space-state.js +251 -0
- package/dist/utils/oauth-signed-query.d.ts +8 -0
- package/dist/utils/oauth-signed-query.js +26 -0
- package/dist/utils/publish-detection.d.ts +0 -8
- package/dist/utils/publish-detection.js +0 -93
- package/dist/utils/publish-policy.d.ts +6 -5
- package/dist/utils/publish-policy.js +71 -7
- package/dist/utils/query-keys.d.ts +16 -4
- package/dist/utils/query-keys.js +31 -4
- package/dist/utils/runtime-paths.d.ts +0 -1
- package/dist/utils/runtime-paths.js +0 -1
- package/dist/utils/runtime-upload-batch.d.ts +18 -0
- package/dist/utils/runtime-upload-batch.js +90 -0
- package/dist/utils/runtime-upload.d.ts +2 -0
- package/dist/utils/runtime-upload.js +5 -0
- package/dist/utils/sf-config-v1.d.ts +2 -0
- package/dist/utils/sf-config-v1.js +542 -0
- package/dist/utils/space-config.d.ts +4 -0
- package/dist/utils/space-config.js +10 -1
- package/dist/utils/space-theme.d.ts +2 -0
- package/dist/utils/space-theme.js +9 -0
- package/dist/utils/static-runtime-policy.d.ts +0 -21
- package/dist/utils/static-runtime-policy.js +19 -103
- package/dist/utils/upload-session.d.ts +1 -1
- package/dist/utils/wpcom-auth-redirects.d.ts +32 -2
- package/dist/utils/wpcom-auth-redirects.js +26 -10
- package/dist/vocabulary.d.ts +130 -30
- package/dist/vocabulary.js +77 -36
- package/package.json +35 -1
- package/dist/contracts/account.d.ts +0 -28
- package/dist/contracts/account.js +0 -54
- package/dist/contracts/intercom.d.ts +0 -6
- package/dist/contracts/intercom.js +0 -5
- package/dist/contracts/telemetry.d.ts +0 -2
- package/dist/contracts/telemetry.js +0 -1
- package/dist/utils/access-policy.d.ts +0 -39
- package/dist/utils/error-display.d.ts +0 -7
- package/dist/utils/error-display.js +0 -42
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { SF_CONFIG_V1_SCHEMA_URL, } from "../contracts/sf-config-v1.js";
|
|
2
|
+
import { SPACE_CONFIG_CAPS } from "../contracts/space-config.js";
|
|
3
|
+
class JsoncReader {
|
|
4
|
+
source;
|
|
5
|
+
offset = 0;
|
|
6
|
+
keyOffsets = new Map();
|
|
7
|
+
constructor(source) {
|
|
8
|
+
this.source = source;
|
|
9
|
+
}
|
|
10
|
+
read() {
|
|
11
|
+
const result = this.value("$");
|
|
12
|
+
this.trivia();
|
|
13
|
+
if (this.offset !== this.source.length)
|
|
14
|
+
this.fail("Unexpected content");
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
value(jsonPath) {
|
|
18
|
+
this.trivia();
|
|
19
|
+
const char = this.source[this.offset];
|
|
20
|
+
if (char === "{")
|
|
21
|
+
return this.object(jsonPath);
|
|
22
|
+
if (char === "[")
|
|
23
|
+
return this.array(jsonPath);
|
|
24
|
+
if (char === '"')
|
|
25
|
+
return this.string();
|
|
26
|
+
const token = /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(this.source.slice(this.offset))?.[0];
|
|
27
|
+
if (!token)
|
|
28
|
+
this.fail("Expected a JSON value");
|
|
29
|
+
this.offset += token.length;
|
|
30
|
+
return JSON.parse(token);
|
|
31
|
+
}
|
|
32
|
+
object(jsonPath) {
|
|
33
|
+
const result = Object.create(null);
|
|
34
|
+
const keys = new Set();
|
|
35
|
+
this.offset++;
|
|
36
|
+
this.trivia();
|
|
37
|
+
if (this.source[this.offset] === "}") {
|
|
38
|
+
this.offset++;
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
while (true) {
|
|
42
|
+
this.trivia();
|
|
43
|
+
const keyOffset = this.offset;
|
|
44
|
+
if (this.source[this.offset] !== '"')
|
|
45
|
+
this.fail("Expected an object key");
|
|
46
|
+
const key = this.string();
|
|
47
|
+
if (keys.has(key))
|
|
48
|
+
this.fail(`Duplicate key ${JSON.stringify(key)}`, keyOffset);
|
|
49
|
+
keys.add(key);
|
|
50
|
+
const childPath = `${jsonPath}.${key}`;
|
|
51
|
+
this.keyOffsets.set(childPath, keyOffset);
|
|
52
|
+
this.trivia();
|
|
53
|
+
if (this.source[this.offset] !== ":")
|
|
54
|
+
this.fail("Expected ':' after object key");
|
|
55
|
+
this.offset++;
|
|
56
|
+
result[key] = this.value(childPath);
|
|
57
|
+
this.trivia();
|
|
58
|
+
const separator = this.source[this.offset];
|
|
59
|
+
if (separator === "}") {
|
|
60
|
+
this.offset++;
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
if (separator !== ",")
|
|
64
|
+
this.fail("Expected ',' or '}'");
|
|
65
|
+
this.offset++;
|
|
66
|
+
this.trivia();
|
|
67
|
+
if (this.source[this.offset] === "}") {
|
|
68
|
+
this.offset++;
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
array(jsonPath) {
|
|
74
|
+
const result = [];
|
|
75
|
+
this.offset++;
|
|
76
|
+
this.trivia();
|
|
77
|
+
if (this.source[this.offset] === "]") {
|
|
78
|
+
this.offset++;
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
while (true) {
|
|
82
|
+
const itemPath = `${jsonPath}[${result.length}]`;
|
|
83
|
+
this.keyOffsets.set(itemPath, this.offset);
|
|
84
|
+
result.push(this.value(itemPath));
|
|
85
|
+
this.trivia();
|
|
86
|
+
const separator = this.source[this.offset];
|
|
87
|
+
if (separator === "]") {
|
|
88
|
+
this.offset++;
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
if (separator !== ",")
|
|
92
|
+
this.fail("Expected ',' or ']'");
|
|
93
|
+
this.offset++;
|
|
94
|
+
this.trivia();
|
|
95
|
+
if (this.source[this.offset] === "]") {
|
|
96
|
+
this.offset++;
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
string() {
|
|
102
|
+
const start = this.offset;
|
|
103
|
+
this.offset++;
|
|
104
|
+
while (this.offset < this.source.length) {
|
|
105
|
+
const charOffset = this.offset;
|
|
106
|
+
const char = this.source[this.offset++];
|
|
107
|
+
if (char === '"')
|
|
108
|
+
return JSON.parse(this.source.slice(start, this.offset));
|
|
109
|
+
if (this.source.charCodeAt(charOffset) < 0x20) {
|
|
110
|
+
this.fail("Unescaped control character in string", charOffset);
|
|
111
|
+
}
|
|
112
|
+
if (char !== "\\")
|
|
113
|
+
continue;
|
|
114
|
+
const escapeOffset = this.offset;
|
|
115
|
+
const escaped = this.source[this.offset++];
|
|
116
|
+
if (escaped === undefined)
|
|
117
|
+
this.fail("Unterminated string", start);
|
|
118
|
+
if ('"\\/bfnrt'.includes(escaped))
|
|
119
|
+
continue;
|
|
120
|
+
if (escaped !== "u")
|
|
121
|
+
this.fail("Invalid escape sequence in string", escapeOffset);
|
|
122
|
+
for (let index = 0; index < 4; index++) {
|
|
123
|
+
const hexOffset = this.offset + index;
|
|
124
|
+
if (!/[0-9a-fA-F]/.test(this.source[hexOffset] ?? "")) {
|
|
125
|
+
this.fail("Invalid Unicode escape sequence in string", hexOffset);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
this.offset += 4;
|
|
129
|
+
}
|
|
130
|
+
this.fail("Unterminated string", start);
|
|
131
|
+
}
|
|
132
|
+
trivia() {
|
|
133
|
+
while (this.offset < this.source.length) {
|
|
134
|
+
if ([" ", "\t", "\r", "\n"].includes(this.source[this.offset] ?? "")) {
|
|
135
|
+
this.offset++;
|
|
136
|
+
}
|
|
137
|
+
else if (this.source.startsWith("//", this.offset)) {
|
|
138
|
+
const newline = this.source.indexOf("\n", this.offset + 2);
|
|
139
|
+
this.offset = newline === -1 ? this.source.length : newline + 1;
|
|
140
|
+
}
|
|
141
|
+
else if (this.source.startsWith("/*", this.offset)) {
|
|
142
|
+
const end = this.source.indexOf("*/", this.offset + 2);
|
|
143
|
+
if (end === -1)
|
|
144
|
+
this.fail("Unterminated block comment");
|
|
145
|
+
this.offset = end + 2;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
fail(message, offset = this.offset) {
|
|
153
|
+
throw new JsoncSyntaxError(message.endsWith(".") ? message : `${message}.`, offset);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
class JsoncSyntaxError extends Error {
|
|
157
|
+
offset;
|
|
158
|
+
constructor(message, offset) {
|
|
159
|
+
super(message);
|
|
160
|
+
this.offset = offset;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function locationFinder(source) {
|
|
164
|
+
const lineStarts = [0];
|
|
165
|
+
for (let index = 0; index < source.length; index++) {
|
|
166
|
+
if (source[index] === "\n")
|
|
167
|
+
lineStarts.push(index + 1);
|
|
168
|
+
}
|
|
169
|
+
return (offset) => {
|
|
170
|
+
let low = 0;
|
|
171
|
+
let high = lineStarts.length;
|
|
172
|
+
while (low + 1 < high) {
|
|
173
|
+
const middle = Math.floor((low + high) / 2);
|
|
174
|
+
if ((lineStarts[middle] ?? 0) <= offset)
|
|
175
|
+
low = middle;
|
|
176
|
+
else
|
|
177
|
+
high = middle;
|
|
178
|
+
}
|
|
179
|
+
return { line: low + 1, column: offset - (lineStarts[low] ?? 0) + 1 };
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function isObject(value) {
|
|
183
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
184
|
+
}
|
|
185
|
+
const CONFIG_FIELDS = {
|
|
186
|
+
build: {
|
|
187
|
+
installRoot: { type: "string" },
|
|
188
|
+
installCommand: { type: "string" },
|
|
189
|
+
command: { type: "string" },
|
|
190
|
+
output: { type: "string" },
|
|
191
|
+
timeoutSeconds: { type: "number" },
|
|
192
|
+
},
|
|
193
|
+
serve: {
|
|
194
|
+
index: { type: "string-or-false" },
|
|
195
|
+
spaFallback: { type: "string" },
|
|
196
|
+
cleanUrls: { type: "boolean" },
|
|
197
|
+
directoryListing: { type: "boolean" },
|
|
198
|
+
},
|
|
199
|
+
metadata: {
|
|
200
|
+
title: { type: "string" },
|
|
201
|
+
description: { type: "string" },
|
|
202
|
+
image: { type: "string" },
|
|
203
|
+
},
|
|
204
|
+
substitute: {
|
|
205
|
+
files: { type: "string-array" },
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
const ROOT_KEYS = new Set(["$schema", "version", ...Object.keys(CONFIG_FIELDS)]);
|
|
209
|
+
const LEGACY_KEYS = {
|
|
210
|
+
index: { kind: "renamed", suggestion: "serve.index" },
|
|
211
|
+
fallback: { kind: "renamed", suggestion: "serve.spaFallback" },
|
|
212
|
+
cleanUrls: { kind: "renamed", suggestion: "serve.cleanUrls" },
|
|
213
|
+
meta: { kind: "renamed", suggestion: "metadata" },
|
|
214
|
+
listing: { kind: "removed" },
|
|
215
|
+
superpowers: { kind: "removed" },
|
|
216
|
+
templates: { kind: "removed" },
|
|
217
|
+
theme: { kind: "removed" },
|
|
218
|
+
pages: { kind: "removed" },
|
|
219
|
+
placement: { kind: "removed" },
|
|
220
|
+
markdownNegotiation: { kind: "removed" },
|
|
221
|
+
inject: { kind: "removed" },
|
|
222
|
+
access: { kind: "removed" },
|
|
223
|
+
};
|
|
224
|
+
export function compileSfConfigV1(source, options = {}) {
|
|
225
|
+
const locate = locationFinder(source);
|
|
226
|
+
if (new TextEncoder().encode(source).byteLength > SPACE_CONFIG_CAPS.fileBytes) {
|
|
227
|
+
return invalidResult(`Config supports up to ${SPACE_CONFIG_CAPS.fileBytes} bytes.`, 1, 1);
|
|
228
|
+
}
|
|
229
|
+
const reader = new JsoncReader(source);
|
|
230
|
+
let value;
|
|
231
|
+
try {
|
|
232
|
+
value = reader.read();
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
const syntax = error instanceof JsoncSyntaxError ? error : new JsoncSyntaxError("Invalid JSONC.", 0);
|
|
236
|
+
const located = locate(syntax.offset);
|
|
237
|
+
return invalidResult(syntax.message, located.line, located.column);
|
|
238
|
+
}
|
|
239
|
+
const issues = [];
|
|
240
|
+
const locations = new Map();
|
|
241
|
+
for (const [jsonPath, offset] of reader.keyOffsets)
|
|
242
|
+
locations.set(jsonPath, locate(offset));
|
|
243
|
+
locations.set("$", { line: 1, column: 1 });
|
|
244
|
+
const issue = (code, jsonPath, message, suggestion) => issues.push({
|
|
245
|
+
code,
|
|
246
|
+
severity: "error",
|
|
247
|
+
path: jsonPath,
|
|
248
|
+
...(locations.get(jsonPath) ?? { line: 1, column: 1 }),
|
|
249
|
+
message,
|
|
250
|
+
...(suggestion ? { suggestion } : {}),
|
|
251
|
+
});
|
|
252
|
+
if (!isObject(value)) {
|
|
253
|
+
issue("config_invalid", "$", "Config must be an object.");
|
|
254
|
+
return { success: false, config: null, effective: null, issues, locations };
|
|
255
|
+
}
|
|
256
|
+
validateKeys(value, issue);
|
|
257
|
+
if (value.version !== 1)
|
|
258
|
+
issue("config_invalid", "$.version", "version must be 1.");
|
|
259
|
+
if (value.$schema !== undefined && value.$schema !== SF_CONFIG_V1_SCHEMA_URL) {
|
|
260
|
+
issue("config_invalid", "$.$schema", "$schema must identify the sf.jsonc v1 schema.");
|
|
261
|
+
}
|
|
262
|
+
validateShape(value, issue);
|
|
263
|
+
validateTemplateSources(value, options.templateSources ?? [], issue);
|
|
264
|
+
validateArtifactReferences(value, options.artifactPaths, issue);
|
|
265
|
+
if (issues.length > 0)
|
|
266
|
+
return { success: false, config: null, effective: null, issues, locations };
|
|
267
|
+
const config = projectConfig(value);
|
|
268
|
+
const artifacts = options.artifactPaths ?? [];
|
|
269
|
+
const rootHtml = artifacts.filter((entry) => !entry.includes("/") && entry.endsWith(".html"));
|
|
270
|
+
const explicitIndex = config.serve?.index;
|
|
271
|
+
let index;
|
|
272
|
+
if (explicitIndex !== undefined)
|
|
273
|
+
index = explicitIndex;
|
|
274
|
+
else if (artifacts.includes("index.html"))
|
|
275
|
+
index = "index.html";
|
|
276
|
+
else if (rootHtml.length === 1)
|
|
277
|
+
index = rootHtml[0] ?? false;
|
|
278
|
+
else if (rootHtml.length > 1) {
|
|
279
|
+
issue("serve_index_required", "$.serve.index", "Multiple root HTML files require serve.index.");
|
|
280
|
+
return { success: false, config: null, effective: null, issues, locations };
|
|
281
|
+
}
|
|
282
|
+
else
|
|
283
|
+
index = false;
|
|
284
|
+
return {
|
|
285
|
+
success: true,
|
|
286
|
+
config,
|
|
287
|
+
effective: {
|
|
288
|
+
version: 1,
|
|
289
|
+
...(config.build ? {
|
|
290
|
+
build: {
|
|
291
|
+
...config.build,
|
|
292
|
+
installRoot: config.build.installRoot ?? ".",
|
|
293
|
+
timeoutSeconds: config.build.timeoutSeconds ?? 900,
|
|
294
|
+
},
|
|
295
|
+
} : {}),
|
|
296
|
+
serve: {
|
|
297
|
+
index,
|
|
298
|
+
...(config.serve?.spaFallback !== undefined ? { spaFallback: config.serve.spaFallback } : {}),
|
|
299
|
+
cleanUrls: config.serve?.cleanUrls ?? true,
|
|
300
|
+
directoryListing: config.serve?.directoryListing ?? index === false,
|
|
301
|
+
},
|
|
302
|
+
...(config.metadata ? { metadata: config.metadata } : {}),
|
|
303
|
+
...(config.substitute ? { substitute: config.substitute } : {}),
|
|
304
|
+
},
|
|
305
|
+
issues,
|
|
306
|
+
locations,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function invalidResult(message, line, column) {
|
|
310
|
+
return {
|
|
311
|
+
success: false,
|
|
312
|
+
config: null,
|
|
313
|
+
effective: null,
|
|
314
|
+
issues: [{
|
|
315
|
+
code: "config_invalid",
|
|
316
|
+
severity: "error",
|
|
317
|
+
path: "$",
|
|
318
|
+
line,
|
|
319
|
+
column,
|
|
320
|
+
message,
|
|
321
|
+
}],
|
|
322
|
+
locations: new Map([["$", { line, column }]]),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function validateKeys(value, issue) {
|
|
326
|
+
for (const key of Object.keys(value)) {
|
|
327
|
+
if (ROOT_KEYS.has(key))
|
|
328
|
+
continue;
|
|
329
|
+
const keyPath = `$.${key}`;
|
|
330
|
+
if (key === "space") {
|
|
331
|
+
issue("config_identity_moved", keyPath, "Space identity belongs in .spacefast/space.json.");
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const legacy = LEGACY_KEYS[key];
|
|
335
|
+
if (legacy?.kind === "removed") {
|
|
336
|
+
issue("config_key_removed", keyPath, `${key} is not part of sf.jsonc v1.`);
|
|
337
|
+
}
|
|
338
|
+
else if (legacy?.kind === "renamed") {
|
|
339
|
+
issue("config_unknown_key", keyPath, `Unknown config key ${key}.`, legacy.suggestion);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
issue("config_unknown_key", keyPath, `Unknown config key ${key}.`, nearestKey(key, [...ROOT_KEYS]));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
for (const [section, fields] of Object.entries(CONFIG_FIELDS)) {
|
|
346
|
+
const object = value[section];
|
|
347
|
+
if (object === undefined)
|
|
348
|
+
continue;
|
|
349
|
+
if (!isObject(object)) {
|
|
350
|
+
issue("config_invalid", `$.${section}`, `${section} must be an object.`);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
for (const key of Object.keys(object)) {
|
|
354
|
+
if (Object.hasOwn(fields, key))
|
|
355
|
+
continue;
|
|
356
|
+
const keyPath = `$.${section}.${key}`;
|
|
357
|
+
issue("config_unknown_key", keyPath, `Unknown config key ${key}.`, nearestKey(key, Object.keys(fields)));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function validateShape(value, issue) {
|
|
362
|
+
for (const [section, fields] of Object.entries(CONFIG_FIELDS)) {
|
|
363
|
+
const object = value[section];
|
|
364
|
+
if (!isObject(object))
|
|
365
|
+
continue;
|
|
366
|
+
for (const [key, descriptor] of Object.entries(fields)) {
|
|
367
|
+
const fieldValue = object[key];
|
|
368
|
+
if (fieldValue === undefined)
|
|
369
|
+
continue;
|
|
370
|
+
if (!matchesType(fieldValue, descriptor.type)) {
|
|
371
|
+
issue("config_invalid", `$.${section}.${key}`, typeMessage(section, key, descriptor.type));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (isObject(value.build)) {
|
|
376
|
+
const timeout = value.build.timeoutSeconds;
|
|
377
|
+
if (typeof timeout === "number" && (!Number.isInteger(timeout)
|
|
378
|
+
|| timeout < 60
|
|
379
|
+
|| timeout > 7200)) {
|
|
380
|
+
issue("config_invalid", "$.build.timeoutSeconds", "build.timeoutSeconds must be an integer from 60 to 7200.");
|
|
381
|
+
}
|
|
382
|
+
const installRoot = value.build.installRoot;
|
|
383
|
+
if (typeof installRoot === "string" && isAbsolutePath(installRoot)) {
|
|
384
|
+
issue("config_invalid", "$.build.installRoot", "build.installRoot must be a relative path.");
|
|
385
|
+
}
|
|
386
|
+
const output = value.build.output;
|
|
387
|
+
if (typeof output === "string" && isAbsoluteOrEscaping(output)) {
|
|
388
|
+
issue("config_invalid", "$.build.output", "build.output must be a relative path that does not escape the project root.");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (isObject(value.serve)) {
|
|
392
|
+
for (const key of ["index", "spaFallback"]) {
|
|
393
|
+
const candidate = value.serve[key];
|
|
394
|
+
if (typeof candidate === "string" && !isCommittedPath(candidate)) {
|
|
395
|
+
issue("config_invalid", `$.serve.${key}`, `serve.${key} must be a non-empty committed relative path.`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (isObject(value.substitute) && value.substitute.files === undefined) {
|
|
400
|
+
issue("config_invalid", "$.substitute", "substitute.files is required when substitute is present.");
|
|
401
|
+
}
|
|
402
|
+
if (isObject(value.substitute) && Array.isArray(value.substitute.files)) {
|
|
403
|
+
if (value.substitute.files.length > SPACE_CONFIG_CAPS.templates) {
|
|
404
|
+
issue("config_invalid", "$.substitute.files", `substitute.files supports up to ${SPACE_CONFIG_CAPS.templates} entries.`);
|
|
405
|
+
}
|
|
406
|
+
value.substitute.files.forEach((candidate, index) => {
|
|
407
|
+
if (typeof candidate === "string" && !isCommittedPath(candidate)) {
|
|
408
|
+
issue("config_invalid", `$.substitute.files[${index}]`, "substitute.files entries must be non-empty committed relative paths.");
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (isObject(value.metadata)) {
|
|
413
|
+
for (const [key, cap] of [
|
|
414
|
+
["title", SPACE_CONFIG_CAPS.metaTitleChars],
|
|
415
|
+
["description", SPACE_CONFIG_CAPS.metaDescriptionChars],
|
|
416
|
+
["image", SPACE_CONFIG_CAPS.metaImageChars],
|
|
417
|
+
]) {
|
|
418
|
+
const candidate = value.metadata[key];
|
|
419
|
+
if (typeof candidate === "string" && candidate.length > cap) {
|
|
420
|
+
issue("config_invalid", `$.metadata.${key}`, `metadata.${key} supports up to ${cap} characters.`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const image = value.metadata.image;
|
|
424
|
+
if (typeof image === "string" && !isCommittedImagePath(image) && !isHttpsUrl(image)) {
|
|
425
|
+
issue("config_invalid", "$.metadata.image", "metadata.image must be a committed root-relative path or https: URL.");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function matchesType(value, type) {
|
|
430
|
+
if (type === "string-array") {
|
|
431
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
432
|
+
}
|
|
433
|
+
if (type === "string-or-false")
|
|
434
|
+
return typeof value === "string" || value === false;
|
|
435
|
+
return typeof value === type;
|
|
436
|
+
}
|
|
437
|
+
function typeMessage(section, key, type) {
|
|
438
|
+
if (type === "string-array")
|
|
439
|
+
return `${section}.${key} must be an array of strings.`;
|
|
440
|
+
if (type === "string-or-false")
|
|
441
|
+
return `${section}.${key} must be a string or false.`;
|
|
442
|
+
if (type === "boolean")
|
|
443
|
+
return `${section}.${key} must be boolean.`;
|
|
444
|
+
return `${section}.${key} must be a ${type}.`;
|
|
445
|
+
}
|
|
446
|
+
function isAbsolutePath(candidate) {
|
|
447
|
+
return /^(?:[\\/]|[A-Za-z]:[\\/])/.test(candidate);
|
|
448
|
+
}
|
|
449
|
+
function isAbsoluteOrEscaping(candidate) {
|
|
450
|
+
return isAbsolutePath(candidate) || candidate.split(/[\\/]/).includes("..");
|
|
451
|
+
}
|
|
452
|
+
function nearestKey(candidate, keys) {
|
|
453
|
+
let nearest;
|
|
454
|
+
for (const key of keys) {
|
|
455
|
+
const distance = editDistance(candidate, key);
|
|
456
|
+
if (distance <= 2 && (!nearest || distance < nearest.distance))
|
|
457
|
+
nearest = { key, distance };
|
|
458
|
+
}
|
|
459
|
+
return nearest?.key;
|
|
460
|
+
}
|
|
461
|
+
function editDistance(left, right) {
|
|
462
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
463
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex++) {
|
|
464
|
+
const current = [leftIndex];
|
|
465
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex++) {
|
|
466
|
+
current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (previous[rightIndex] ?? 0) + 1, (previous[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1));
|
|
467
|
+
}
|
|
468
|
+
previous = current;
|
|
469
|
+
}
|
|
470
|
+
return previous[right.length] ?? right.length;
|
|
471
|
+
}
|
|
472
|
+
function isCommittedPath(candidate) {
|
|
473
|
+
return candidate.length > 0
|
|
474
|
+
&& !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(candidate)
|
|
475
|
+
&& !isAbsoluteOrEscaping(candidate);
|
|
476
|
+
}
|
|
477
|
+
function isCommittedImagePath(candidate) {
|
|
478
|
+
return /^\/(?!\/)/.test(candidate) && !candidate.split(/[\\/]/).includes("..");
|
|
479
|
+
}
|
|
480
|
+
function isHttpsUrl(candidate) {
|
|
481
|
+
try {
|
|
482
|
+
return new URL(candidate).protocol === "https:";
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function validateTemplateSources(value, templates, issue) {
|
|
489
|
+
const files = isObject(value.substitute) && Array.isArray(value.substitute.files)
|
|
490
|
+
? value.substitute.files
|
|
491
|
+
: [];
|
|
492
|
+
for (const template of templates) {
|
|
493
|
+
const index = files.indexOf(template.path);
|
|
494
|
+
if (index < 0)
|
|
495
|
+
continue;
|
|
496
|
+
if (!/\{\{\s*vars\./.test(template.source))
|
|
497
|
+
continue;
|
|
498
|
+
issue("template_syntax_retired", `$.substitute.files[${index}]`, `Use {{ env.NAME }} instead of {{ vars.NAME }} in ${template.path}.`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function validateArtifactReferences(value, artifactPaths, issue) {
|
|
502
|
+
if (artifactPaths === undefined)
|
|
503
|
+
return;
|
|
504
|
+
const artifacts = new Set(artifactPaths);
|
|
505
|
+
if (isObject(value.serve)) {
|
|
506
|
+
for (const key of ["index", "spaFallback"]) {
|
|
507
|
+
const candidate = value.serve[key];
|
|
508
|
+
if (typeof candidate === "string" && isCommittedPath(candidate) && !artifacts.has(candidate)) {
|
|
509
|
+
issue("config_invalid", `$.serve.${key}`, `serve.${key} must reference a committed artifact.`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (isObject(value.substitute) && Array.isArray(value.substitute.files)) {
|
|
514
|
+
value.substitute.files.forEach((candidate, index) => {
|
|
515
|
+
if (typeof candidate === "string" && isCommittedPath(candidate) && !artifacts.has(candidate)) {
|
|
516
|
+
issue("config_invalid", `$.substitute.files[${index}]`, "substitute.files entries must reference committed artifacts.");
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
const image = isObject(value.metadata) ? value.metadata.image : undefined;
|
|
521
|
+
if (typeof image === "string" && isCommittedImagePath(image) && !artifacts.has(image.slice(1))) {
|
|
522
|
+
issue("config_invalid", "$.metadata.image", "metadata.image must reference a committed artifact.");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function projectConfig(value) {
|
|
526
|
+
const config = {
|
|
527
|
+
version: 1,
|
|
528
|
+
...(value.$schema !== undefined ? { $schema: SF_CONFIG_V1_SCHEMA_URL } : {}),
|
|
529
|
+
};
|
|
530
|
+
for (const [section, fields] of Object.entries(CONFIG_FIELDS)) {
|
|
531
|
+
const source = value[section];
|
|
532
|
+
if (!isObject(source))
|
|
533
|
+
continue;
|
|
534
|
+
const projected = {};
|
|
535
|
+
for (const key of Object.keys(fields)) {
|
|
536
|
+
if (source[key] !== undefined)
|
|
537
|
+
projected[key] = source[key];
|
|
538
|
+
}
|
|
539
|
+
config[section] = projected;
|
|
540
|
+
}
|
|
541
|
+
return config;
|
|
542
|
+
}
|
|
@@ -37,9 +37,13 @@ export type ResolvedServingConfig = {
|
|
|
37
37
|
path: string;
|
|
38
38
|
status: 200 | 404;
|
|
39
39
|
} | null;
|
|
40
|
+
/** W7.1 clean URLs: serve flat `<name>.html` at its extensionless URL. */
|
|
41
|
+
cleanUrls: boolean;
|
|
40
42
|
listing: boolean;
|
|
41
43
|
viewer: boolean;
|
|
42
44
|
meta: NonNullable<SpaceConfig["meta"]> | null;
|
|
45
|
+
/** Gate-page theme tokens (access-plan §5.7 tier 1); null when unset. */
|
|
46
|
+
pagesTheme: NonNullable<NonNullable<SpaceConfig["pages"]>["theme"]> | null;
|
|
43
47
|
/** Hidden experimental build-pipeline flag; false keeps today's behavior. */
|
|
44
48
|
experimentalGutenberg: boolean;
|
|
45
49
|
/** Resolved inject snippets; only honored when experimentalGutenberg is true. */
|
|
@@ -303,7 +303,9 @@ const PRIVATE_SERVING_FILES = new Set([
|
|
|
303
303
|
...SPACE_CONFIG_PRIVATE_FILES,
|
|
304
304
|
]);
|
|
305
305
|
function privateServingPath(path) {
|
|
306
|
-
if (PRIVATE_SERVING_FILES.has(path) ||
|
|
306
|
+
if (PRIVATE_SERVING_FILES.has(path) ||
|
|
307
|
+
path === ".well-known/spacefast-runtime" ||
|
|
308
|
+
path === ".well-known/stattic-runtime") {
|
|
307
309
|
return true;
|
|
308
310
|
}
|
|
309
311
|
const segments = path.toLowerCase().split("/");
|
|
@@ -381,10 +383,17 @@ export function resolveEffectiveSpaceConfig(input) {
|
|
|
381
383
|
const serving = {
|
|
382
384
|
index: merged.index ?? inferredRootHtmlIndex ?? false,
|
|
383
385
|
fallback,
|
|
386
|
+
// Clean URLs (runtime W7.1): an explicit config value wins; the default is
|
|
387
|
+
// ON for plain static sites and OFF when a 200-status SPA fallback owns
|
|
388
|
+
// extensionless routes (the fallback answers them instead). The runtime
|
|
389
|
+
// resolver (management.php) applies the same default for artifacts
|
|
390
|
+
// finalized without the key.
|
|
391
|
+
cleanUrls: merged.cleanUrls ?? !(fallback && fallback.status === 200),
|
|
384
392
|
listing,
|
|
385
393
|
// The single-file viewer rides the listing bundle (artifact-style spaces).
|
|
386
394
|
viewer: listing,
|
|
387
395
|
meta: merged.meta ?? null,
|
|
396
|
+
pagesTheme: merged.pages?.theme && Object.keys(merged.pages.theme).length > 0 ? merged.pages.theme : null,
|
|
388
397
|
experimentalGutenberg: merged.experimental_gutenberg === true,
|
|
389
398
|
inject: merged.inject ?? null,
|
|
390
399
|
};
|
|
@@ -13,4 +13,6 @@ export type CompiledSpaceTheme = {
|
|
|
13
13
|
};
|
|
14
14
|
/** Engine-enforced ceiling for the compiled declaration block. */
|
|
15
15
|
export declare const SPACE_THEME_CSS_MAX_BYTES: number;
|
|
16
|
+
export declare function isSafeThemeColor(value: string): boolean;
|
|
17
|
+
export declare function isSafeThemeFontFamily(value: string): boolean;
|
|
16
18
|
export declare function compileSpaceTheme(theme: SpaceTheme | null | undefined): CompiledSpaceTheme;
|
|
@@ -25,6 +25,15 @@ function safeValue(value, pattern) {
|
|
|
25
25
|
const trimmed = value.trim();
|
|
26
26
|
return trimmed.length > 0 && !FORBIDDEN_VALUE_PATTERN.test(trimmed) && pattern.test(trimmed);
|
|
27
27
|
}
|
|
28
|
+
// Exported so other compilers that consume the same preset grammar (the gate-page
|
|
29
|
+
// theme cascade's `theme.json`/`sf.theme.jsonc` tiers, access-plan §8) reuse this
|
|
30
|
+
// ONE allowlist instead of re-deriving it — same posture, same forbidden set.
|
|
31
|
+
export function isSafeThemeColor(value) {
|
|
32
|
+
return safeValue(value, COLOR_PATTERN);
|
|
33
|
+
}
|
|
34
|
+
export function isSafeThemeFontFamily(value) {
|
|
35
|
+
return safeValue(value, FONT_FAMILY_PATTERN);
|
|
36
|
+
}
|
|
28
37
|
export function compileSpaceTheme(theme) {
|
|
29
38
|
if (!theme) {
|
|
30
39
|
return { css: null, warnings: [] };
|
|
@@ -7,27 +7,6 @@ export declare function pathExtension(filePath: string): string;
|
|
|
7
7
|
/** Basename without its extension (the "stem"), posix semantics. */
|
|
8
8
|
export declare function pathStem(filePath: string): string;
|
|
9
9
|
export declare function isPhpLikeStaticPath(filePath: string): boolean;
|
|
10
|
-
/**
|
|
11
|
-
* Classifies a candidate hash token — the final `.`/`-`-delimited tail of a
|
|
12
|
-
* filename stem (see isImmutableAssetPath) — as a real bundler content hash.
|
|
13
|
-
*
|
|
14
|
-
* Immutable caching is a one-year pin, so the test is asymmetric on purpose: a
|
|
15
|
-
* missed hash costs a cheap 304 revalidation, a false positive serves stale
|
|
16
|
-
* content for a year. When a token is ambiguous we say no. The discriminator is
|
|
17
|
-
* NOT character case — modern bundlers emit BOTH lowercase-hex and mixed-case
|
|
18
|
-
* base64url hashes — it is entropy plus the absence of dictionary-word shape.
|
|
19
|
-
*
|
|
20
|
-
* IRREDUCIBLE LIMIT: an 8-char base64url token is the same size as a short
|
|
21
|
-
* CamelCase-with-version-digit human name, and every default Vite/Rollup hash we
|
|
22
|
-
* must accept carries only one digit (BzeDGMM3, 4A_iFExJ). So a token like
|
|
23
|
-
* `D3GeoMap`, `McKayCV2`, or `v2-WebGL` is character-for-character indistinguishable
|
|
24
|
-
* from a real hash and is accepted by clause B. No filename-only rule can separate
|
|
25
|
-
* these without also rejecting the bundler hashes the spec mandates. The durable
|
|
26
|
-
* fix is to gate immutable caching on path convention (Vite `assets/`, Next
|
|
27
|
-
* `_next/static/`, SvelteKit `_app/immutable/`, …) rather than guessing from the
|
|
28
|
-
* stem; this heuristic is the best-effort fallback when that context is absent.
|
|
29
|
-
*/
|
|
30
|
-
export declare function looksLikeBundlerFingerprint(segment: string): boolean;
|
|
31
10
|
export declare function isImmutableAssetPath(filePath: string): boolean;
|
|
32
11
|
/** Forced inert serving policy for a committed static file. PHP-like files are never executable. */
|
|
33
12
|
export declare function runtimeFileSafety(filePath: string): {
|