@spacefast/common 0.0.9 → 0.0.11

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 (78) hide show
  1. package/dist/contracts/activity.d.ts +1 -1
  2. package/dist/contracts/activity.js +11 -6
  3. package/dist/contracts/annotations.d.ts +2 -0
  4. package/dist/contracts/annotations.js +2 -1
  5. package/dist/contracts/api-keys.d.ts +4 -0
  6. package/dist/contracts/api-keys.js +4 -0
  7. package/dist/contracts/archives.d.ts +2 -8
  8. package/dist/contracts/archives.js +3 -8
  9. package/dist/contracts/builds.d.ts +1 -0
  10. package/dist/contracts/builds.js +5 -0
  11. package/dist/contracts/comments.d.ts +309 -0
  12. package/dist/contracts/comments.js +200 -0
  13. package/dist/contracts/device-auth.d.ts +221 -1
  14. package/dist/contracts/device-auth.js +106 -4
  15. package/dist/contracts/device-team-scope.d.ts +10 -0
  16. package/dist/contracts/device-team-scope.js +17 -0
  17. package/dist/contracts/error-code-meta.d.ts +52 -0
  18. package/dist/contracts/error-code-meta.js +13 -0
  19. package/dist/contracts/error-codes.d.ts +1 -1
  20. package/dist/contracts/error-codes.js +13 -0
  21. package/dist/contracts/features.d.ts +37 -38
  22. package/dist/contracts/features.js +51 -59
  23. package/dist/contracts/generated-feature-launch-entries.d.ts +2 -2
  24. package/dist/contracts/generated-feature-launch-entries.js +6 -6
  25. package/dist/contracts/ids.d.ts +1 -1
  26. package/dist/contracts/ids.js +1 -1
  27. package/dist/contracts/oauth-resources.d.ts +7 -0
  28. package/dist/contracts/oauth-resources.js +10 -2
  29. package/dist/contracts/operations.d.ts +1 -3
  30. package/dist/contracts/operations.js +5 -1
  31. package/dist/contracts/pages.d.ts +70 -0
  32. package/dist/contracts/pages.js +89 -0
  33. package/dist/contracts/publishes.d.ts +5 -0
  34. package/dist/contracts/publishes.js +25 -8
  35. package/dist/contracts/resources.d.ts +14 -62
  36. package/dist/contracts/runtime-api.d.ts +13 -15
  37. package/dist/contracts/runtime-api.js +12 -70
  38. package/dist/contracts/space-config.d.ts +32 -90
  39. package/dist/contracts/space-config.js +49 -54
  40. package/dist/contracts/spaces.d.ts +47 -187
  41. package/dist/contracts/spaces.js +19 -3
  42. package/dist/contracts/superadmin-spaces.d.ts +14 -62
  43. package/dist/contracts/superadmin-teams.d.ts +79 -0
  44. package/dist/contracts/superadmin-teams.js +36 -0
  45. package/dist/contracts/superadmin.d.ts +2 -0
  46. package/dist/contracts/superadmin.js +1 -0
  47. package/dist/contracts/teams.d.ts +1 -0
  48. package/dist/contracts/teams.js +8 -0
  49. package/dist/contracts/theme-json.js +4 -5
  50. package/dist/dashboard-paths/index.d.ts +31 -0
  51. package/dist/dashboard-paths/index.js +80 -0
  52. package/dist/docs/agent-handoff-document.d.ts +15 -0
  53. package/dist/docs/agent-handoff-document.js +179 -0
  54. package/dist/docs/agent-setup.d.ts +12 -1
  55. package/dist/docs/agent-setup.js +30 -16
  56. package/dist/docs/catalog.d.ts +2 -2
  57. package/dist/docs/catalog.js +2 -2
  58. package/dist/docs/error-docs.js +52 -0
  59. package/dist/utils/asset-fingerprint.d.ts +53 -0
  60. package/dist/utils/asset-fingerprint.js +574 -0
  61. package/dist/utils/credential-policy.d.ts +5 -0
  62. package/dist/utils/credential-policy.js +31 -8
  63. package/dist/utils/pages.d.ts +53 -0
  64. package/dist/utils/pages.js +274 -0
  65. package/dist/utils/query-keys.d.ts +4 -3
  66. package/dist/utils/query-keys.js +4 -3
  67. package/dist/utils/space-config.d.ts +2 -2
  68. package/dist/utils/space-config.js +1 -1
  69. package/dist/utils/wpcom-auth-redirects.d.ts +0 -4
  70. package/dist/utils/wpcom-auth-redirects.js +0 -6
  71. package/dist/vocabulary.d.ts +2 -2
  72. package/package.json +6 -1
  73. package/dist/contracts/space-theme.d.ts +0 -25
  74. package/dist/contracts/space-theme.js +0 -49
  75. package/dist/utils/gate-theme.d.ts +0 -61
  76. package/dist/utils/gate-theme.js +0 -217
  77. package/dist/utils/space-theme.d.ts +0 -18
  78. package/dist/utils/space-theme.js +0 -98
@@ -0,0 +1,53 @@
1
+ import { type CustomPageId, type SitePageId } from "../contracts/pages.js";
2
+ export type PageTheme = {
3
+ accent?: string | undefined;
4
+ background?: string | undefined;
5
+ font?: string | undefined;
6
+ logo?: string | undefined;
7
+ name?: string | undefined;
8
+ hideSpacefastBranding?: boolean | undefined;
9
+ };
10
+ export type PageFileEntry = {
11
+ name: string;
12
+ href: string;
13
+ size?: string;
14
+ directory?: boolean;
15
+ };
16
+ export type PagePreviewFile = {
17
+ name: string;
18
+ href: string;
19
+ downloadHref?: string;
20
+ size?: string;
21
+ mime?: string;
22
+ };
23
+ export type PageExpansionContext = {
24
+ page: SitePageId | "layout";
25
+ theme?: PageTheme;
26
+ content?: string;
27
+ passwordAction?: string;
28
+ passwordError?: string;
29
+ loginHref?: string;
30
+ files?: readonly PageFileEntry[];
31
+ file?: PagePreviewFile;
32
+ };
33
+ export type PageDiagnostic = {
34
+ severity: "warning" | "error";
35
+ code: "page_source_too_large" | "unknown_page_template" | "unknown_page_element" | "page_element_not_allowed" | "page_document_required" | "layout_missing_content" | "password_form_missing" | "index_files_missing" | "page_indexable";
36
+ message: string;
37
+ path: string;
38
+ };
39
+ export declare function escapePageHtml(value: string): string;
40
+ export declare function expandPageHtml(source: string, context: PageExpansionContext): string;
41
+ export declare function validatePageSources(input: {
42
+ pages: Readonly<Record<string, string>>;
43
+ layouts?: Readonly<Record<string, string>>;
44
+ }): PageDiagnostic[];
45
+ export declare function isCompleteHtmlDocument(source: string): boolean;
46
+ export declare function applyPageLayout(layout: string | null, content: string, context: Omit<PageExpansionContext, "page" | "content">): string;
47
+ export declare function inferPageThemeFromThemeJson(raw: string): Partial<PageTheme>;
48
+ export declare function defaultPageBody(id: SitePageId): string;
49
+ export declare const DEFAULT_PAGE_TEMPLATES: Readonly<Record<SitePageId, string>>;
50
+ export declare const DEFAULT_LAYOUT_TEMPLATE = "<!doctype html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title><sf-name></sf-name></title></head>\n<body>\n <header><a href=\"/\"><sf-logo></sf-logo><sf-name></sf-name></a></header>\n <main><sf-content></sf-content></main>\n</body>\n</html>\n";
51
+ export declare function defaultPageTemplate(id: SitePageId): string;
52
+ export declare function pageIdFromFilename(filename: string): CustomPageId | null;
53
+ export declare function allDefaultPageTemplates(): Readonly<Record<SitePageId, string>>;
@@ -0,0 +1,274 @@
1
+ import { CUSTOM_PAGE_IDS, PAGE_ELEMENTS, PAGE_LAYOUT_FILE, PAGE_SOURCE_MAX_BYTES, SITE_PAGE_IDS, } from "../contracts/pages.js";
2
+ const encoder = new TextEncoder();
3
+ const knownElements = new Set(PAGE_ELEMENTS);
4
+ const customPageIds = new Set(CUSTOM_PAGE_IDS);
5
+ export function escapePageHtml(value) {
6
+ return value
7
+ .replaceAll("&", "&amp;")
8
+ .replaceAll("<", "&lt;")
9
+ .replaceAll(">", "&gt;")
10
+ .replaceAll('"', "&quot;")
11
+ .replaceAll("'", "&#039;");
12
+ }
13
+ function themeStyle(theme) {
14
+ const declarations = [
15
+ theme.accent ? `--sf-accent:${theme.accent}` : "",
16
+ theme.background ? `--sf-bg:${theme.background}` : "",
17
+ theme.font ? `--sf-font:${theme.font}` : "",
18
+ ].filter(Boolean);
19
+ return declarations.length > 0 ? `<style>:root{${declarations.join(";")}}</style>` : "";
20
+ }
21
+ function renderFiles(files) {
22
+ if (files.length === 0)
23
+ return '<p class="sf-empty">Nothing here yet.</p>';
24
+ return `<ul class="sf-files">${files
25
+ .map((file) => `<li><a href="${escapePageHtml(file.href)}">${file.directory ? "↳ " : ""}${escapePageHtml(file.name)}</a>${file.size ? `<span>${escapePageHtml(file.size)}</span>` : ""}</li>`)
26
+ .join("")}</ul>`;
27
+ }
28
+ function renderFile(file) {
29
+ if (!file)
30
+ return '<p class="sf-empty">File unavailable.</p>';
31
+ const href = escapePageHtml(file.href);
32
+ const image = file.mime?.startsWith("image/")
33
+ ? `<img class="sf-preview" src="${href}" alt="${escapePageHtml(file.name)}">`
34
+ : `<iframe class="sf-preview" src="${href}" title="${escapePageHtml(file.name)}"></iframe>`;
35
+ return `${image}<dl class="sf-file-meta"><div><dt>Name</dt><dd>${escapePageHtml(file.name)}</dd></div>${file.size ? `<div><dt>Size</dt><dd>${escapePageHtml(file.size)}</dd></div>` : ""}${file.mime ? `<div><dt>Type</dt><dd>${escapePageHtml(file.mime)}</dd></div>` : ""}</dl><a class="sf-button" href="${escapePageHtml(file.downloadHref ?? file.href)}" download>Download</a>`;
36
+ }
37
+ function renderElement(element, context) {
38
+ switch (element) {
39
+ case "sf-content":
40
+ return context.content ?? "";
41
+ case "sf-password-form":
42
+ return `${context.passwordError ? `<p class="sf-error" role="alert">${escapePageHtml(context.passwordError)}</p>` : ""}<form class="sf-form" method="post" action="${escapePageHtml(context.passwordAction ?? "")}"><label>Password<input name="_pw" type="password" required autocomplete="current-password"></label><button type="submit">Continue</button></form>`;
43
+ case "sf-login":
44
+ return `<a class="sf-button" href="${escapePageHtml(context.loginHref ?? "/__spacefast/access/login")}">Log in</a>`;
45
+ case "sf-files":
46
+ return renderFiles(context.files ?? []);
47
+ case "sf-file":
48
+ return renderFile(context.file);
49
+ case "sf-logo":
50
+ return context.theme?.logo
51
+ ? `<img class="sf-logo" src="${escapePageHtml(context.theme.logo)}" alt="">`
52
+ : "";
53
+ case "sf-name":
54
+ return escapePageHtml(context.theme?.name ?? "This space");
55
+ }
56
+ return "";
57
+ }
58
+ const ELEMENT_PATTERN = /<\s*(sf-[a-z0-9-]+)\b[^>]*>(?:\s*<\s*\/\s*\1\s*>)?|<\s*(sf-[a-z0-9-]+)\b[^>]*\/\s*>/gi;
59
+ export function expandPageHtml(source, context) {
60
+ let output = source.replace(ELEMENT_PATTERN, (_match, paired, unary) => {
61
+ const name = (paired ?? unary)?.toLowerCase();
62
+ return name && knownElements.has(name) ? renderElement(name, context) : _match;
63
+ });
64
+ const tokens = themeStyle(context.theme ?? {});
65
+ if (tokens) {
66
+ output = /<\s*\/\s*head\s*>/i.test(output)
67
+ ? output.replace(/<\s*\/\s*head\s*>/i, `${tokens}</head>`)
68
+ : `${tokens}${output}`;
69
+ }
70
+ return output;
71
+ }
72
+ function foundElements(source) {
73
+ const result = new Set();
74
+ for (const match of source.matchAll(/<\s*(sf-[a-z0-9-]+)\b/gi))
75
+ result.add((match[1] ?? "").toLowerCase());
76
+ return [...result];
77
+ }
78
+ export function validatePageSources(input) {
79
+ const diagnostics = [];
80
+ for (const [sourcePath, source] of Object.entries(input.pages)) {
81
+ const path = sourcePath.includes("_pages/") ? sourcePath : `_pages/${sourcePath}`;
82
+ const filename = path.slice(path.lastIndexOf("_pages/") + 7);
83
+ const id = filename.replace(/\.html$/i, "");
84
+ if (!customPageIds.has(id) || filename !== `${id}.html`) {
85
+ diagnostics.push({
86
+ severity: "error",
87
+ code: "unknown_page_template",
88
+ message: `${path} is not a supported page template.`,
89
+ path,
90
+ });
91
+ continue;
92
+ }
93
+ if (encoder.encode(source).length > PAGE_SOURCE_MAX_BYTES) {
94
+ diagnostics.push({
95
+ severity: "error",
96
+ code: "page_source_too_large",
97
+ message: `${path} exceeds the ${PAGE_SOURCE_MAX_BYTES} byte limit.`,
98
+ path,
99
+ });
100
+ }
101
+ if (!isCompleteHtmlDocument(source))
102
+ diagnostics.push({
103
+ severity: "error",
104
+ code: "page_document_required",
105
+ message: `${path} must be a complete HTML document beginning with <!doctype html>.`,
106
+ path,
107
+ });
108
+ for (const element of foundElements(source)) {
109
+ if (!knownElements.has(element))
110
+ diagnostics.push({
111
+ severity: "error",
112
+ code: "unknown_page_element",
113
+ message: `Unknown Pages element <${element}>.`,
114
+ path,
115
+ });
116
+ const allowed = element === "sf-logo" ||
117
+ element === "sf-name" ||
118
+ (id === "password" && element === "sf-password-form") ||
119
+ (id === "index" && element === "sf-files") ||
120
+ (id === "preview" && element === "sf-file");
121
+ if (knownElements.has(element) && !allowed)
122
+ diagnostics.push({
123
+ severity: "error",
124
+ code: "page_element_not_allowed",
125
+ message: `<${element}> is not valid in the ${id} page.`,
126
+ path,
127
+ });
128
+ }
129
+ if (id === "password" &&
130
+ !/<\s*sf-password-form\b/i.test(source) &&
131
+ !/<\s*input\b[^>]*\bname\s*=\s*["']_pw["']/i.test(source)) {
132
+ diagnostics.push({
133
+ severity: "error",
134
+ code: "password_form_missing",
135
+ message: "The password page needs <sf-password-form> or a form input named _pw.",
136
+ path,
137
+ });
138
+ }
139
+ if (id === "index" && !/<\s*sf-files\b/i.test(source))
140
+ diagnostics.push({
141
+ severity: "warning",
142
+ code: "index_files_missing",
143
+ message: "The index page does not render <sf-files>.",
144
+ path,
145
+ });
146
+ if ((id === "password" || id === "denied") &&
147
+ /<\s*meta\b[^>]*\bname\s*=\s*["']robots["'][^>]*\bcontent\s*=\s*["'][^"']*\bindex\b/i.test(source))
148
+ diagnostics.push({
149
+ severity: "warning",
150
+ code: "page_indexable",
151
+ message: "Gate pages are served with noindex and should not declare themselves indexable.",
152
+ path,
153
+ });
154
+ }
155
+ for (const [path, source] of Object.entries(input.layouts ?? {})) {
156
+ if (encoder.encode(source).length > PAGE_SOURCE_MAX_BYTES)
157
+ diagnostics.push({
158
+ severity: "error",
159
+ code: "page_source_too_large",
160
+ message: `${path} exceeds the ${PAGE_SOURCE_MAX_BYTES} byte limit.`,
161
+ path,
162
+ });
163
+ const elements = foundElements(source);
164
+ for (const element of elements) {
165
+ if (!knownElements.has(element))
166
+ diagnostics.push({
167
+ severity: "error",
168
+ code: "unknown_page_element",
169
+ message: `Unknown Pages element <${element}>.`,
170
+ path,
171
+ });
172
+ else if (element !== "sf-content" && element !== "sf-logo" && element !== "sf-name")
173
+ diagnostics.push({
174
+ severity: "error",
175
+ code: "page_element_not_allowed",
176
+ message: `<${element}> is not valid in _layout.html.`,
177
+ path,
178
+ });
179
+ }
180
+ const contentCount = [...source.matchAll(/<\s*sf-content\b/gi)].length;
181
+ if (contentCount !== 1)
182
+ diagnostics.push({
183
+ severity: "error",
184
+ code: "layout_missing_content",
185
+ message: `${PAGE_LAYOUT_FILE} needs exactly one <sf-content> element.`,
186
+ path,
187
+ });
188
+ }
189
+ return diagnostics;
190
+ }
191
+ export function isCompleteHtmlDocument(source) {
192
+ return /^\s*<!doctype\s+html\b/i.test(source);
193
+ }
194
+ export function applyPageLayout(layout, content, context) {
195
+ if (!layout || isCompleteHtmlDocument(content))
196
+ return expandPageHtml(content, { ...context, page: "layout" });
197
+ return expandPageHtml(layout, { ...context, page: "layout", content });
198
+ }
199
+ export function inferPageThemeFromThemeJson(raw) {
200
+ try {
201
+ const value = JSON.parse(raw);
202
+ if (!value || typeof value !== "object")
203
+ return {};
204
+ const settings = value.settings;
205
+ if (!settings || typeof settings !== "object")
206
+ return {};
207
+ const color = settings.color;
208
+ const typography = settings.typography;
209
+ const palette = color && typeof color === "object" ? color.palette : null;
210
+ const fonts = typography && typeof typography === "object"
211
+ ? typography.fontFamilies
212
+ : null;
213
+ const inferred = {};
214
+ if (Array.isArray(palette)) {
215
+ for (const entry of palette) {
216
+ if (!entry || typeof entry !== "object")
217
+ continue;
218
+ const slug = entry.slug;
219
+ const entryColor = entry.color;
220
+ if (typeof entryColor !== "string")
221
+ continue;
222
+ if ((slug === "accent" || slug === "primary") && inferred.accent === undefined)
223
+ inferred.accent = entryColor;
224
+ if (slug === "background")
225
+ inferred.background = entryColor;
226
+ }
227
+ }
228
+ if (Array.isArray(fonts)) {
229
+ const first = fonts.find((entry) => entry &&
230
+ typeof entry === "object" &&
231
+ typeof entry.fontFamily === "string");
232
+ if (first)
233
+ inferred.font = first.fontFamily;
234
+ }
235
+ return inferred;
236
+ }
237
+ catch {
238
+ return {};
239
+ }
240
+ }
241
+ export function defaultPageBody(id) {
242
+ const source = DEFAULT_PAGE_TEMPLATES[id];
243
+ return source.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] ?? source;
244
+ }
245
+ const sharedCss = `:root{color-scheme:light dark;--sf-accent:#ff603d;--sf-bg:#faf9f7;--sf-fg:#1c1b1a;--sf-muted:#6b6862;--sf-card:#fff;--sf-border:#e5e2dc;--sf-font:Inter,ui-sans-serif,system-ui,sans-serif}*{box-sizing:border-box}body{margin:0;background:var(--sf-bg);color:var(--sf-fg);font:16px/1.55 var(--sf-font);min-height:100vh;display:grid;place-items:center;padding:24px}.sf-shell{width:min(100%,640px)}.sf-card{background:var(--sf-card);border:1px solid var(--sf-border);border-radius:18px;padding:clamp(24px,5vw,48px);box-shadow:0 18px 55px #1c1b1a12}h1{font-size:clamp(2rem,7vw,3.5rem);line-height:1;letter-spacing:-.05em;margin:16px 0}p{color:var(--sf-muted)}a{color:var(--sf-accent)}.sf-brand{display:flex;align-items:center;gap:10px;font-weight:650}.sf-logo{width:28px;height:28px;object-fit:contain}.sf-badge{margin-top:28px;font-size:12px;color:var(--sf-muted)}.sf-badge a{text-decoration:none}.sf-form label{display:grid;gap:8px;font-weight:600}.sf-form input{width:100%;margin-top:6px;padding:12px 14px;border:1px solid var(--sf-border);border-radius:10px;background:transparent;color:inherit;font:inherit}.sf-form button,.sf-button{display:inline-block;border:0;border-radius:10px;background:var(--sf-accent);color:#fff;padding:12px 16px;font:600 14px/1 var(--sf-font);text-decoration:none;margin-top:14px;cursor:pointer}.sf-error{color:#b42318}.sf-files{list-style:none;padding:0;margin:24px 0}.sf-files li{display:flex;border-top:1px solid var(--sf-border)}.sf-files a{padding:12px 0;flex:1;text-decoration:none}.sf-files span{padding:12px 0;color:var(--sf-muted);font-size:13px}.sf-preview{width:100%;min-height:320px;border:1px solid var(--sf-border);border-radius:10px;object-fit:contain;background:#fff}.sf-file-meta div{display:flex;justify-content:space-between;border-bottom:1px solid var(--sf-border);padding:8px 0}.sf-file-meta dt{color:var(--sf-muted)}.sf-file-meta dd{margin:0}@media(prefers-color-scheme:dark){:root{--sf-bg:#16151a;--sf-fg:#e8e6e3;--sf-muted:#a8a49c;--sf-card:#1e1d23;--sf-border:#343239}.sf-card{box-shadow:0 18px 55px #0005}}`;
246
+ function document(title, body, noindex = false) {
247
+ return `<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${noindex ? '<meta name="robots" content="noindex">' : ""}<title>${title}</title><style>${sharedCss}</style></head>\n<body><main class="sf-shell"><section class="sf-card">${body}</section></main></body>\n</html>\n`;
248
+ }
249
+ const badge = '<p class="sf-badge">Hosted with <a href="https://spacefast.com">Spacefast</a></p>';
250
+ export const DEFAULT_PAGE_TEMPLATES = {
251
+ "404": document("Page not found", '<div class="sf-brand"><sf-logo></sf-logo><sf-name></sf-name></div><h1>Lost in space.</h1><p>This page doesn’t exist. Check the address or head back home.</p><a class="sf-button" href="/">Go home</a>' +
252
+ badge),
253
+ password: document("Password required", '<div class="sf-brand"><sf-logo></sf-logo><sf-name></sf-name></div><h1>This one’s private.</h1><p>Enter the password to keep going.</p><sf-password-form></sf-password-form>' +
254
+ badge, true),
255
+ denied: document("Access denied", '<div class="sf-brand"><sf-logo></sf-logo><sf-name></sf-name></div><h1>Not for this account.</h1><p>You don’t have access to this page.</p>' +
256
+ badge, true),
257
+ login: document("Log in", '<div class="sf-brand"><sf-logo></sf-logo><sf-name></sf-name></div><h1>Log in to continue.</h1><p>This space knows its people.</p><sf-login></sf-login>' +
258
+ badge, true),
259
+ index: document("Files", '<div class="sf-brand"><sf-logo></sf-logo><sf-name></sf-name></div><h1>Files</h1><sf-files></sf-files>' +
260
+ badge),
261
+ preview: document("File preview", '<div class="sf-brand"><sf-logo></sf-logo><sf-name></sf-name></div><h1>Preview</h1><sf-file></sf-file>' +
262
+ badge),
263
+ };
264
+ export const DEFAULT_LAYOUT_TEMPLATE = `<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title><sf-name></sf-name></title></head>\n<body>\n <header><a href="/"><sf-logo></sf-logo><sf-name></sf-name></a></header>\n <main><sf-content></sf-content></main>\n</body>\n</html>\n`;
265
+ export function defaultPageTemplate(id) {
266
+ return DEFAULT_PAGE_TEMPLATES[id];
267
+ }
268
+ export function pageIdFromFilename(filename) {
269
+ const id = filename.replace(/\.html$/, "");
270
+ return customPageIds.has(id) ? id : null;
271
+ }
272
+ export function allDefaultPageTemplates() {
273
+ return Object.fromEntries(SITE_PAGE_IDS.map((id) => [id, DEFAULT_PAGE_TEMPLATES[id]]));
274
+ }
@@ -5,10 +5,10 @@ export declare const qk: {
5
5
  accountConnections: () => readonly ["account", "connections"];
6
6
  accountInvitations: () => readonly ["account", "invitations"];
7
7
  apiTokens: (teamId?: string) => readonly ["api-tokens"] | readonly ["api-tokens", string];
8
- agentAccounts: () => readonly ["agent-accounts"];
8
+ agentAccounts: (teamId: string) => readonly ["agent-accounts", string];
9
9
  myShelf: () => readonly ["my-shelf"];
10
- mcpDashboard: () => readonly ["mcp", "dashboard"];
11
- mcpApproval: (id: string) => readonly ["mcp", "approval", string];
10
+ mcpDashboard: (teamId: string) => readonly ["mcp", "dashboard", string];
11
+ mcpApproval: (teamId: string, id: string) => readonly ["mcp", "approval", string, string];
12
12
  teams: () => readonly ["teams", "list"];
13
13
  team: (ref: string) => readonly ["teams", "by-ref", string];
14
14
  teamMembers: (id: string) => readonly ["teams", string, "members"];
@@ -75,6 +75,7 @@ export declare const qk: {
75
75
  spaceFirewall: (id: string) => readonly ["spaces", string, "access", "firewall"];
76
76
  spaceAccessViews: (id: string, since?: string) => readonly ["spaces", string, "access", "views", string];
77
77
  teamServiceTokens: (teamId: string) => readonly ["teams", string, "access", "service-tokens"];
78
+ teamAgentHandoffs: (teamId: string) => readonly ["teams", string, "agent-handoffs"];
78
79
  teamAccessDefaults: (teamId: string) => readonly ["teams", string, "access", "defaults"];
79
80
  teamAccessConnections: (teamId: string) => readonly ["teams", string, "access", "connections"];
80
81
  domain: (teamId: string, ref: string) => readonly ["teams", string, "domains", string];
@@ -7,10 +7,10 @@ export const qk = {
7
7
  // Results are team-filtered, so the team lands in the key; calling with no
8
8
  // teamId yields the bare prefix (personal scope, and invalidate-all sweeps).
9
9
  apiTokens: (teamId) => teamId === undefined ? ["api-tokens"] : ["api-tokens", teamId],
10
- agentAccounts: () => ["agent-accounts"],
10
+ agentAccounts: (teamId) => ["agent-accounts", teamId],
11
11
  myShelf: () => ["my-shelf"],
12
- mcpDashboard: () => ["mcp", "dashboard"],
13
- mcpApproval: (id) => ["mcp", "approval", id],
12
+ mcpDashboard: (teamId) => ["mcp", "dashboard", teamId],
13
+ mcpApproval: (teamId, id) => ["mcp", "approval", teamId, id],
14
14
  // The teams LIST gets its own leaf so invalidating it never cascades into
15
15
  // ["teams", id, ...] resource queries (members, spaces, domains, ...).
16
16
  teams: () => ["teams", "list"],
@@ -93,6 +93,7 @@ export const qk = {
93
93
  spaceFirewall: (id) => ["spaces", id, "access", "firewall"],
94
94
  spaceAccessViews: (id, since) => ["spaces", id, "access", "views", since ?? ""],
95
95
  teamServiceTokens: (teamId) => ["teams", teamId, "access", "service-tokens"],
96
+ teamAgentHandoffs: (teamId) => ["teams", teamId, "agent-handoffs"],
96
97
  teamAccessDefaults: (teamId) => ["teams", teamId, "access", "defaults"],
97
98
  teamAccessConnections: (teamId) => ["teams", teamId, "access", "connections"],
98
99
  domain: (teamId, ref) => ["teams", teamId, "domains", ref],
@@ -42,8 +42,8 @@ export type ResolvedServingConfig = {
42
42
  listing: boolean;
43
43
  viewer: boolean;
44
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;
45
+ /** Theme tokens for every Spacefast-authored page; null when unset. */
46
+ pageTheme: NonNullable<SpaceConfig["theme"]> | null;
47
47
  /** Hidden experimental build-pipeline flag; false keeps today's behavior. */
48
48
  experimentalGutenberg: boolean;
49
49
  /** Resolved inject snippets; only honored when experimentalGutenberg is true. */
@@ -393,7 +393,7 @@ export function resolveEffectiveSpaceConfig(input) {
393
393
  // The single-file viewer rides the listing bundle (artifact-style spaces).
394
394
  viewer: listing,
395
395
  meta: merged.meta ?? null,
396
- pagesTheme: merged.pages?.theme && Object.keys(merged.pages.theme).length > 0 ? merged.pages.theme : null,
396
+ pageTheme: merged.theme && Object.keys(merged.theme).length > 0 ? merged.theme : null,
397
397
  experimentalGutenberg: merged.experimental_gutenberg === true,
398
398
  inject: merged.inject ?? null,
399
399
  };
@@ -1,8 +1,6 @@
1
1
  export type LoginIntentSearch = {
2
2
  direct?: boolean | undefined;
3
3
  returnTo?: string | undefined;
4
- /** Route straight to WordPress.com's signup page instead of its login page. */
5
- signup?: boolean | undefined;
6
4
  };
7
5
  /**
8
6
  * Optional query params WordPress.com's OAuth2 authorize endpoint understands
@@ -21,8 +19,6 @@ export type WpcomAuthorizeExtras = {
21
19
  * our side loops them straight back into the same account.
22
20
  */
23
21
  switchAccount?: boolean | undefined;
24
- /** `new-user=1`: routes straight to the WP.com signup page. */
25
- signup?: boolean | undefined;
26
22
  };
27
23
  export declare function decorateWpcomAuthorizeUrl(authorizeUrl: string, extras?: WpcomAuthorizeExtras): string;
28
24
  export declare function buildDashboardLoginPath(input: LoginIntentSearch): string;
@@ -14,9 +14,6 @@ export function decorateWpcomAuthorizeUrl(authorizeUrl, extras = {}) {
14
14
  if (extras.switchAccount) {
15
15
  url.searchParams.set("implicit", "false");
16
16
  }
17
- if (extras.signup) {
18
- url.searchParams.set("new-user", "1");
19
- }
20
17
  return url.toString();
21
18
  }
22
19
  export function buildDashboardLoginPath(input) {
@@ -36,9 +33,6 @@ export function buildWpcomAuthStartPath(input) {
36
33
  if (returnTo) {
37
34
  url.searchParams.set("returnTo", returnTo);
38
35
  }
39
- if (input.signup) {
40
- url.searchParams.set("signup", "1");
41
- }
42
36
  if (input.oauthQuery) {
43
37
  url.searchParams.set("oauth_query", input.oauthQuery);
44
38
  }
@@ -150,7 +150,7 @@ export declare const EXTERNAL_TERM_ALLOWLIST: readonly [{
150
150
  }];
151
151
  /**
152
152
  * AMENDMENT (DECIDED 2026-07-02): the deploy vocabulary stays, as documented,
153
- * first-class ALIASES of the canonical vocabulary — `deploy` is an alias of
153
+ * first-class ALIASES of the canonical vocabulary — `deploy` is an exact alias of
154
154
  * `publish` (the verb) and `deployments` is an alias of `versions` (the noun).
155
155
  * The canonical lifecycle nouns are unchanged: version / channel / build.
156
156
  *
@@ -158,7 +158,7 @@ export declare const EXTERNAL_TERM_ALLOWLIST: readonly [{
158
158
  *
159
159
  * - `sf deploy` / `sf deployments` stay listed and documented; every surface
160
160
  * that presents them names the canonical command first and marks the
161
- * deploy form as an alias.
161
+ * deploy form as an exact alias with no independent behavior.
162
162
  * - Wherever the alias is introduced, attach DEPLOY_ALIAS_DISAMBIGUATION
163
163
  * (the one sanctioned disambiguation sentence) so readers land on the
164
164
  * canonical nouns.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spacefast/common",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "license": "MIT",
5
5
  "description": "Shared TypeScript contracts and utilities for Spacefast packages.",
6
6
  "repository": {
@@ -64,6 +64,11 @@
64
64
  "import": "./dist/slug-policy/index.js",
65
65
  "default": "./dist/slug-policy/index.js"
66
66
  },
67
+ "./dashboard-paths": {
68
+ "types": "./dist/dashboard-paths/index.d.ts",
69
+ "import": "./dist/dashboard-paths/index.js",
70
+ "default": "./dist/dashboard-paths/index.js"
71
+ },
67
72
  "./agents/*": {
68
73
  "types": "./dist/agents/*.d.ts",
69
74
  "import": "./dist/agents/*.js",
@@ -1,25 +0,0 @@
1
- import { z } from "zod";
2
- export declare const spaceThemeSchema: z.ZodObject<{
3
- color: z.ZodOptional<z.ZodObject<{
4
- palette: z.ZodOptional<z.ZodArray<z.ZodObject<{
5
- slug: z.ZodString;
6
- color: z.ZodString;
7
- name: z.ZodOptional<z.ZodString>;
8
- }, z.core.$strip>>>;
9
- }, z.core.$strip>>;
10
- typography: z.ZodOptional<z.ZodObject<{
11
- fontSizes: z.ZodOptional<z.ZodArray<z.ZodObject<{
12
- slug: z.ZodString;
13
- size: z.ZodString;
14
- name: z.ZodOptional<z.ZodString>;
15
- }, z.core.$strip>>>;
16
- fontFamily: z.ZodOptional<z.ZodString>;
17
- }, z.core.$strip>>;
18
- spacing: z.ZodOptional<z.ZodObject<{
19
- spacingScale: z.ZodOptional<z.ZodArray<z.ZodObject<{
20
- slug: z.ZodString;
21
- size: z.ZodString;
22
- }, z.core.$strip>>>;
23
- }, z.core.$strip>>;
24
- }, z.core.$strip>;
25
- export type SpaceTheme = z.infer<typeof spaceThemeSchema>;
@@ -1,49 +0,0 @@
1
- import { z } from "zod";
2
- // SpaceTheme (internal-docs/platform.md "SpaceTheme"): a validated preset
3
- // subset of WordPress theme.json. Presets only — no arbitrary CSS. The
4
- // finalize-time compiler (wave B1) additionally validates every value against
5
- // the safe-value grammar (no url()/@import/expression()/data: URIs) and drops
6
- // invalid keys with a warning diagnostic.
7
- const presetSlug = z
8
- .string()
9
- .min(1)
10
- .max(64)
11
- .regex(/^[a-z0-9-]+$/, { message: "Preset slugs are lowercase alphanumerics and dashes." });
12
- export const spaceThemeSchema = z.object({
13
- color: z
14
- .object({
15
- palette: z
16
- .array(z.object({
17
- slug: presetSlug,
18
- color: z.string().min(1).max(128),
19
- name: z.string().min(1).max(128).optional(),
20
- }))
21
- .max(64)
22
- .optional(),
23
- })
24
- .optional(),
25
- typography: z
26
- .object({
27
- fontSizes: z
28
- .array(z.object({
29
- slug: presetSlug,
30
- size: z.string().min(1).max(64),
31
- name: z.string().min(1).max(128).optional(),
32
- }))
33
- .max(64)
34
- .optional(),
35
- fontFamily: z.string().min(1).max(256).optional(),
36
- })
37
- .optional(),
38
- spacing: z
39
- .object({
40
- spacingScale: z
41
- .array(z.object({
42
- slug: presetSlug,
43
- size: z.string().min(1).max(64),
44
- }))
45
- .max(64)
46
- .optional(),
47
- })
48
- .optional(),
49
- });
@@ -1,61 +0,0 @@
1
- import { type SpacePagesTheme } from "../contracts/space-config.js";
2
- export type GateThemeDiagnostic = {
3
- severity: "info" | "warning" | "error";
4
- code: string;
5
- message: string;
6
- path?: string | undefined;
7
- details?: Record<string, unknown> | undefined;
8
- };
9
- /** Bound on the raw `theme.json` / `sf.theme.jsonc` file size at finalize. */
10
- export declare const GATE_THEME_FILE_MAX_BYTES: number;
11
- /** Bound on the compiled additive CSS custom-property block. */
12
- export declare const GATE_THEME_VARS_MAX_BYTES: number;
13
- export type ParsedThemeJson = {
14
- /** Gate-page tokens derivable from named palette slugs (accent/primary/background). */
15
- theme: Partial<SpacePagesTheme>;
16
- /** `--wp--preset--color--{slug}` / `--wp--preset--font-family--{slug}` declarations. */
17
- themeVars: string | null;
18
- diagnostics: GateThemeDiagnostic[];
19
- };
20
- /**
21
- * Parses a `theme.json` file's `settings.color.palette` /
22
- * `settings.typography.fontFamilies` into gate-page tokens (accent/background
23
- * derived from conventionally-named slugs: `accent`|`primary`, `background`)
24
- * plus a full CSS custom-property block for every safe palette/font-family
25
- * entry. Any problem (oversized, invalid JSON, invalid shape) warns and
26
- * yields an empty result — the next tier down still applies.
27
- */
28
- export declare function parseThemeJsonFile(raw: string): ParsedThemeJson;
29
- export type ParsedSfThemeJsonc = {
30
- theme: Partial<SpacePagesTheme>;
31
- diagnostics: GateThemeDiagnostic[];
32
- };
33
- /**
34
- * Parses the `sf.theme.jsonc` escape hatch: same JSONC tolerance and the same
35
- * validated token grammar as `spacefast.jsonc "pages.theme"`
36
- * (`spacePagesThemeSchema`), just from a dedicated file. Any problem warns
37
- * and yields an empty result.
38
- */
39
- export declare function parseSfThemeJsonc(raw: string): ParsedSfThemeJsonc;
40
- export type CompiledGateTheme = {
41
- /** Merged accent/background/logo/name/fontFamily; empty object when every tier is unset. */
42
- theme: Partial<SpacePagesTheme>;
43
- /** Additive `--wp--preset--*` declarations from theme.json (advanced surface for custom pages). */
44
- themeVars: string | null;
45
- diagnostics: GateThemeDiagnostic[];
46
- };
47
- /**
48
- * Compiles the full tier-3 cascade into the flat token shape the runtime
49
- * already reads. `base` is the existing free-tier `spacefast.jsonc
50
- * "pages.theme"` (already validated by `spaceConfigSchema` upstream — passed
51
- * through as-is). `sf.theme.jsonc` only applies when `advancedThemingEntitled`
52
- * is true; when it is present but NOT entitled, the tier is skipped with an
53
- * info diagnostic (fail-safe by omission — the compiled artifact simply
54
- * reflects the lower, entitled tiers, per access-plan §9).
55
- */
56
- export declare function mergeGateTheme(input: {
57
- base: SpacePagesTheme | null;
58
- themeJsonRaw: string | null;
59
- sfThemeJsoncRaw: string | null;
60
- advancedThemingEntitled: boolean;
61
- }): CompiledGateTheme;