@spacefast/common 0.0.6 → 0.0.8

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 (65) 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 +6 -1
  5. package/dist/agents/connect-targets.js +19 -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/contracts/access.d.ts +143 -0
  11. package/dist/contracts/access.js +33 -0
  12. package/dist/contracts/api-keys.d.ts +123 -0
  13. package/dist/contracts/api-keys.js +65 -0
  14. package/dist/contracts/builds.d.ts +40 -0
  15. package/dist/contracts/builds.js +17 -0
  16. package/dist/contracts/enums.js +5 -4
  17. package/dist/contracts/error-code-meta.d.ts +116 -4
  18. package/dist/contracts/error-code-meta.js +29 -1
  19. package/dist/contracts/error-codes.d.ts +1 -1
  20. package/dist/contracts/error-codes.js +28 -0
  21. package/dist/contracts/feature-lifecycle-core.d.ts +36 -0
  22. package/dist/contracts/feature-lifecycle-core.js +220 -0
  23. package/dist/contracts/feature-lifecycle.d.ts +7 -0
  24. package/dist/contracts/feature-lifecycle.js +31 -0
  25. package/dist/contracts/generated-feature-launch-entries.d.ts +2 -0
  26. package/dist/contracts/generated-feature-launch-entries.js +518 -0
  27. package/dist/contracts/ids.d.ts +4 -0
  28. package/dist/contracts/ids.js +8 -0
  29. package/dist/contracts/operations.d.ts +161 -1
  30. package/dist/contracts/operations.js +49 -1
  31. package/dist/contracts/platform.d.ts +6 -0
  32. package/dist/contracts/platform.js +6 -0
  33. package/dist/contracts/publishes.d.ts +130 -0
  34. package/dist/contracts/publishes.js +145 -0
  35. package/dist/contracts/repository-connections.d.ts +3 -0
  36. package/dist/contracts/repository-connections.js +13 -0
  37. package/dist/contracts/resources.d.ts +2 -0
  38. package/dist/contracts/resources.js +10 -2
  39. package/dist/contracts/runtime-api.d.ts +2 -0
  40. package/dist/contracts/runtime-api.js +1 -0
  41. package/dist/contracts/sf-config-v1.d.ts +75 -0
  42. package/dist/contracts/sf-config-v1.js +6 -0
  43. package/dist/contracts/spaces.d.ts +12 -3
  44. package/dist/contracts/spaces.js +22 -2
  45. package/dist/contracts/superadmin-spaces.d.ts +2 -0
  46. package/dist/contracts/superadmin-tenants.d.ts +6 -6
  47. package/dist/contracts/superadmin-tenants.js +6 -11
  48. package/dist/contracts/teams.d.ts +8 -11
  49. package/dist/contracts/teams.js +11 -16
  50. package/dist/docs/agent-setup.d.ts +51 -0
  51. package/dist/docs/agent-setup.js +168 -3
  52. package/dist/docs/error-docs.js +113 -1
  53. package/dist/slug-policy/blocklist.d.ts +4 -0
  54. package/dist/slug-policy/blocklist.js +122 -0
  55. package/dist/slug-policy/index.d.ts +17 -0
  56. package/dist/slug-policy/index.js +86 -0
  57. package/dist/utils/concurrency.d.ts +1 -0
  58. package/dist/utils/concurrency.js +22 -0
  59. package/dist/utils/email.d.ts +1 -0
  60. package/dist/utils/email.js +3 -0
  61. package/dist/utils/query-keys.d.ts +1 -0
  62. package/dist/utils/query-keys.js +4 -0
  63. package/dist/utils/sf-config-v1.d.ts +2 -0
  64. package/dist/utils/sf-config-v1.js +542 -0
  65. package/package.json +20 -1
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spacefast/common",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "license": "MIT",
5
5
  "description": "Shared TypeScript contracts and utilities for Spacefast packages.",
6
6
  "repository": {
@@ -17,6 +17,7 @@
17
17
  "node": ">=20"
18
18
  },
19
19
  "files": [
20
+ "brand-assets",
20
21
  "dist"
21
22
  ],
22
23
  "exports": {
@@ -25,6 +26,19 @@
25
26
  "import": "./dist/brand.js",
26
27
  "default": "./dist/brand.js"
27
28
  },
29
+ "./brand-assets": {
30
+ "types": "./dist/brand-assets.d.ts",
31
+ "import": "./dist/brand-assets.js",
32
+ "default": "./dist/brand-assets.js"
33
+ },
34
+ "./brand-assets/build": {
35
+ "types": "./dist/brand-assets-build.d.ts",
36
+ "import": "./dist/brand-assets-build.js",
37
+ "default": "./dist/brand-assets-build.js"
38
+ },
39
+ "./brand-assets/spacefast-wordmark.svg": "./brand-assets/spacefast-wordmark.svg",
40
+ "./brand-assets/spacefast-sf-full-bleed.svg": "./brand-assets/spacefast-sf-full-bleed.svg",
41
+ "./brand-assets/spacefast-favicon.svg": "./brand-assets/spacefast-favicon.svg",
28
42
  "./vocabulary": {
29
43
  "types": "./dist/vocabulary.d.ts",
30
44
  "import": "./dist/vocabulary.js",
@@ -45,6 +59,11 @@
45
59
  "import": "./dist/utils/*.js",
46
60
  "default": "./dist/utils/*.js"
47
61
  },
62
+ "./slug-policy": {
63
+ "types": "./dist/slug-policy/index.d.ts",
64
+ "import": "./dist/slug-policy/index.js",
65
+ "default": "./dist/slug-policy/index.js"
66
+ },
48
67
  "./agents/*": {
49
68
  "types": "./dist/agents/*.d.ts",
50
69
  "import": "./dist/agents/*.js",