@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.
Files changed (183) hide show
  1. package/brand-assets/spacefast-favicon.svg +16 -0
  2. package/brand-assets/spacefast-sf-full-bleed.svg +13 -0
  3. package/brand-assets/spacefast-wordmark.svg +5 -0
  4. package/dist/agents/connect-targets.d.ts +138 -0
  5. package/dist/agents/connect-targets.js +243 -0
  6. package/dist/brand-assets-build.d.ts +26 -0
  7. package/dist/brand-assets-build.js +81 -0
  8. package/dist/brand-assets.d.ts +42 -0
  9. package/dist/brand-assets.js +30 -0
  10. package/dist/brand.d.ts +1 -0
  11. package/dist/brand.js +5 -0
  12. package/dist/config/domains.d.ts +12 -6
  13. package/dist/config/domains.js +30 -38
  14. package/dist/contracts/access.d.ts +967 -124
  15. package/dist/contracts/access.js +804 -148
  16. package/dist/contracts/activity.d.ts +1 -1
  17. package/dist/contracts/activity.js +28 -2
  18. package/dist/contracts/annotations.d.ts +2 -4
  19. package/dist/contracts/api-keys.d.ts +126 -4
  20. package/dist/contracts/api-keys.js +74 -6
  21. package/dist/contracts/archives.d.ts +8 -2
  22. package/dist/contracts/archives.js +11 -4
  23. package/dist/contracts/billing.d.ts +0 -9
  24. package/dist/contracts/builds.d.ts +65 -21
  25. package/dist/contracts/builds.js +29 -0
  26. package/dist/contracts/common.d.ts +2 -0
  27. package/dist/contracts/common.js +4 -0
  28. package/dist/contracts/continuation.d.ts +0 -3
  29. package/dist/contracts/countries.d.ts +2 -0
  30. package/dist/contracts/countries.js +261 -0
  31. package/dist/contracts/deployments.d.ts +34 -34
  32. package/dist/contracts/device-auth.d.ts +4 -11
  33. package/dist/contracts/device-auth.js +4 -0
  34. package/dist/contracts/docs.d.ts +119 -0
  35. package/dist/contracts/docs.js +51 -0
  36. package/dist/contracts/domains.d.ts +96 -54
  37. package/dist/contracts/domains.js +26 -4
  38. package/dist/contracts/enums.d.ts +15 -16
  39. package/dist/contracts/enums.js +26 -14
  40. package/dist/contracts/error-code-meta.d.ts +184 -0
  41. package/dist/contracts/error-code-meta.js +124 -10
  42. package/dist/contracts/error-codes.d.ts +1 -1
  43. package/dist/contracts/error-codes.js +42 -0
  44. package/dist/contracts/events.d.ts +1 -1
  45. package/dist/contracts/feature-lifecycle-core.d.ts +36 -0
  46. package/dist/contracts/feature-lifecycle-core.js +220 -0
  47. package/dist/contracts/feature-lifecycle.d.ts +7 -0
  48. package/dist/contracts/feature-lifecycle.js +31 -0
  49. package/dist/contracts/features.d.ts +76 -6
  50. package/dist/contracts/features.js +178 -13
  51. package/dist/contracts/generated-feature-launch-entries.d.ts +2 -0
  52. package/dist/contracts/generated-feature-launch-entries.js +518 -0
  53. package/dist/contracts/git.d.ts +2 -8
  54. package/dist/contracts/ids.d.ts +9 -0
  55. package/dist/contracts/ids.js +13 -0
  56. package/dist/contracts/mcp.d.ts +8 -10
  57. package/dist/contracts/me.js +1 -1
  58. package/dist/contracts/oauth-resources.d.ts +19 -8
  59. package/dist/contracts/oauth-resources.js +44 -13
  60. package/dist/contracts/operations.d.ts +161 -1
  61. package/dist/contracts/operations.js +49 -1
  62. package/dist/contracts/platform.d.ts +6 -102
  63. package/dist/contracts/platform.js +6 -50
  64. package/dist/contracts/publishes.d.ts +130 -0
  65. package/dist/contracts/publishes.js +145 -0
  66. package/dist/contracts/push-new.d.ts +4 -4
  67. package/dist/contracts/quotas.d.ts +2 -0
  68. package/dist/contracts/quotas.js +8 -0
  69. package/dist/contracts/repository-connections.d.ts +9 -8
  70. package/dist/contracts/repository-connections.js +13 -0
  71. package/dist/contracts/resources.d.ts +99 -13
  72. package/dist/contracts/resources.js +36 -4
  73. package/dist/contracts/routes.d.ts +0 -2
  74. package/dist/contracts/runtime-api.d.ts +363 -39
  75. package/dist/contracts/runtime-api.js +276 -6
  76. package/dist/contracts/sf-config-v1.d.ts +75 -0
  77. package/dist/contracts/sf-config-v1.js +6 -0
  78. package/dist/contracts/sites.d.ts +19 -2
  79. package/dist/contracts/sites.js +21 -0
  80. package/dist/contracts/space-config.d.ts +201 -2
  81. package/dist/contracts/space-config.js +135 -5
  82. package/dist/contracts/spaces.d.ts +376 -196
  83. package/dist/contracts/spaces.js +62 -10
  84. package/dist/contracts/superadmin-emails.d.ts +4 -2
  85. package/dist/contracts/superadmin-emails.js +8 -0
  86. package/dist/contracts/superadmin-queues.d.ts +26 -26
  87. package/dist/contracts/superadmin-spaces.d.ts +172 -42
  88. package/dist/contracts/superadmin-tenants.d.ts +6 -6
  89. package/dist/contracts/superadmin-tenants.js +6 -11
  90. package/dist/contracts/superadmin.d.ts +6 -93
  91. package/dist/contracts/superadmin.js +6 -14
  92. package/dist/contracts/tags.d.ts +10 -9
  93. package/dist/contracts/tags.js +2 -1
  94. package/dist/contracts/teams.d.ts +26 -30
  95. package/dist/contracts/teams.js +11 -17
  96. package/dist/contracts/theme-json.d.ts +30 -0
  97. package/dist/contracts/theme-json.js +48 -0
  98. package/dist/contracts/variables.d.ts +0 -2
  99. package/dist/contracts/variables.js +0 -5
  100. package/dist/contracts/webhooks.d.ts +2 -0
  101. package/dist/contracts/webhooks.js +1 -1
  102. package/dist/contracts/zero.d.ts +5 -10
  103. package/dist/docs/agent-prose.d.ts +43 -3
  104. package/dist/docs/agent-prose.js +146 -7
  105. package/dist/docs/agent-setup.d.ts +51 -0
  106. package/dist/docs/agent-setup.js +211 -14
  107. package/dist/docs/agent-solutions.d.ts +5 -5
  108. package/dist/docs/agent-solutions.js +2 -2
  109. package/dist/docs/catalog.d.ts +650 -0
  110. package/dist/docs/catalog.js +758 -0
  111. package/dist/docs/error-docs.d.ts +0 -2
  112. package/dist/docs/error-docs.js +189 -35
  113. package/dist/docs/index-build.d.ts +17 -0
  114. package/dist/docs/index-build.js +24 -0
  115. package/dist/docs/search.d.ts +54 -0
  116. package/dist/docs/search.js +96 -0
  117. package/dist/docs/skill-distribution.d.ts +5 -3
  118. package/dist/docs/skill-distribution.js +8 -8
  119. package/dist/slug-policy/blocklist.d.ts +4 -0
  120. package/dist/slug-policy/blocklist.js +122 -0
  121. package/dist/slug-policy/index.d.ts +17 -0
  122. package/dist/slug-policy/index.js +86 -0
  123. package/dist/utils/access-match.d.ts +39 -0
  124. package/dist/utils/access-match.js +296 -0
  125. package/dist/utils/auth-redirect.d.ts +3 -1
  126. package/dist/utils/auth-redirect.js +21 -1
  127. package/dist/utils/build-settings.d.ts +6 -0
  128. package/dist/utils/build-settings.js +429 -46
  129. package/dist/utils/claim-token.d.ts +8 -0
  130. package/dist/utils/claim-token.js +23 -2
  131. package/dist/utils/concurrency.d.ts +1 -0
  132. package/dist/utils/concurrency.js +22 -0
  133. package/dist/utils/content-type.d.ts +1 -0
  134. package/dist/utils/content-type.js +1 -1
  135. package/dist/utils/credential-policy.d.ts +54 -0
  136. package/dist/utils/{access-policy.js → credential-policy.js} +28 -2
  137. package/dist/utils/email.d.ts +1 -0
  138. package/dist/utils/email.js +3 -0
  139. package/dist/utils/gate-theme.d.ts +61 -0
  140. package/dist/utils/gate-theme.js +217 -0
  141. package/dist/utils/git-repository.d.ts +0 -1
  142. package/dist/utils/git-repository.js +0 -3
  143. package/dist/utils/id-hints.d.ts +8 -0
  144. package/dist/utils/id-hints.js +61 -0
  145. package/dist/utils/local-space-state.d.ts +91 -0
  146. package/dist/utils/local-space-state.js +251 -0
  147. package/dist/utils/oauth-signed-query.d.ts +8 -0
  148. package/dist/utils/oauth-signed-query.js +26 -0
  149. package/dist/utils/publish-detection.d.ts +0 -8
  150. package/dist/utils/publish-detection.js +0 -93
  151. package/dist/utils/publish-policy.d.ts +6 -5
  152. package/dist/utils/publish-policy.js +71 -7
  153. package/dist/utils/query-keys.d.ts +16 -4
  154. package/dist/utils/query-keys.js +31 -4
  155. package/dist/utils/runtime-paths.d.ts +0 -1
  156. package/dist/utils/runtime-paths.js +0 -1
  157. package/dist/utils/runtime-upload-batch.d.ts +18 -0
  158. package/dist/utils/runtime-upload-batch.js +90 -0
  159. package/dist/utils/runtime-upload.d.ts +2 -0
  160. package/dist/utils/runtime-upload.js +5 -0
  161. package/dist/utils/sf-config-v1.d.ts +2 -0
  162. package/dist/utils/sf-config-v1.js +542 -0
  163. package/dist/utils/space-config.d.ts +4 -0
  164. package/dist/utils/space-config.js +10 -1
  165. package/dist/utils/space-theme.d.ts +2 -0
  166. package/dist/utils/space-theme.js +9 -0
  167. package/dist/utils/static-runtime-policy.d.ts +0 -21
  168. package/dist/utils/static-runtime-policy.js +19 -103
  169. package/dist/utils/upload-session.d.ts +1 -1
  170. package/dist/utils/wpcom-auth-redirects.d.ts +32 -2
  171. package/dist/utils/wpcom-auth-redirects.js +26 -10
  172. package/dist/vocabulary.d.ts +130 -30
  173. package/dist/vocabulary.js +77 -36
  174. package/package.json +35 -1
  175. package/dist/contracts/account.d.ts +0 -28
  176. package/dist/contracts/account.js +0 -54
  177. package/dist/contracts/intercom.d.ts +0 -6
  178. package/dist/contracts/intercom.js +0 -5
  179. package/dist/contracts/telemetry.d.ts +0 -2
  180. package/dist/contracts/telemetry.js +0 -1
  181. package/dist/utils/access-policy.d.ts +0 -39
  182. package/dist/utils/error-display.d.ts +0 -7
  183. 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) || path === ".well-known/stattic-runtime") {
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): {