@spacefast/common 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +11 -6
- package/dist/contracts/annotations.d.ts +2 -0
- package/dist/contracts/annotations.js +2 -1
- package/dist/contracts/api-keys.d.ts +4 -0
- package/dist/contracts/api-keys.js +4 -0
- package/dist/contracts/archives.d.ts +2 -8
- package/dist/contracts/archives.js +3 -8
- package/dist/contracts/builds.d.ts +1 -0
- package/dist/contracts/builds.js +5 -0
- package/dist/contracts/comments.d.ts +309 -0
- package/dist/contracts/comments.js +200 -0
- package/dist/contracts/device-auth.d.ts +221 -1
- package/dist/contracts/device-auth.js +106 -4
- package/dist/contracts/device-team-scope.d.ts +10 -0
- package/dist/contracts/device-team-scope.js +17 -0
- package/dist/contracts/error-code-meta.d.ts +52 -0
- package/dist/contracts/error-code-meta.js +13 -0
- package/dist/contracts/error-codes.d.ts +1 -1
- package/dist/contracts/error-codes.js +13 -0
- package/dist/contracts/features.d.ts +37 -38
- package/dist/contracts/features.js +51 -59
- package/dist/contracts/generated-feature-launch-entries.d.ts +2 -2
- package/dist/contracts/generated-feature-launch-entries.js +6 -6
- package/dist/contracts/ids.d.ts +1 -1
- package/dist/contracts/ids.js +1 -1
- package/dist/contracts/oauth-resources.d.ts +7 -0
- package/dist/contracts/oauth-resources.js +10 -2
- package/dist/contracts/operations.d.ts +1 -3
- package/dist/contracts/operations.js +5 -1
- package/dist/contracts/pages.d.ts +70 -0
- package/dist/contracts/pages.js +89 -0
- package/dist/contracts/publishes.d.ts +5 -0
- package/dist/contracts/publishes.js +25 -8
- package/dist/contracts/resources.d.ts +14 -62
- package/dist/contracts/runtime-api.d.ts +13 -15
- package/dist/contracts/runtime-api.js +12 -70
- package/dist/contracts/space-config.d.ts +32 -90
- package/dist/contracts/space-config.js +49 -54
- package/dist/contracts/spaces.d.ts +47 -187
- package/dist/contracts/spaces.js +19 -3
- package/dist/contracts/superadmin-spaces.d.ts +14 -62
- package/dist/contracts/superadmin-teams.d.ts +79 -0
- package/dist/contracts/superadmin-teams.js +36 -0
- package/dist/contracts/superadmin.d.ts +2 -0
- package/dist/contracts/superadmin.js +1 -0
- package/dist/contracts/teams.d.ts +1 -0
- package/dist/contracts/teams.js +8 -0
- package/dist/contracts/theme-json.js +4 -5
- package/dist/dashboard-paths/index.d.ts +31 -0
- package/dist/dashboard-paths/index.js +80 -0
- package/dist/docs/agent-handoff-document.d.ts +15 -0
- package/dist/docs/agent-handoff-document.js +179 -0
- package/dist/docs/agent-setup.d.ts +12 -1
- package/dist/docs/agent-setup.js +30 -16
- package/dist/docs/catalog.d.ts +2 -2
- package/dist/docs/catalog.js +2 -2
- package/dist/docs/error-docs.js +52 -0
- package/dist/utils/asset-fingerprint.d.ts +53 -0
- package/dist/utils/asset-fingerprint.js +574 -0
- package/dist/utils/credential-policy.d.ts +5 -0
- package/dist/utils/credential-policy.js +31 -8
- package/dist/utils/pages.d.ts +53 -0
- package/dist/utils/pages.js +274 -0
- package/dist/utils/query-keys.d.ts +4 -3
- package/dist/utils/query-keys.js +4 -3
- package/dist/utils/space-config.d.ts +2 -2
- package/dist/utils/space-config.js +1 -1
- package/dist/utils/wpcom-auth-redirects.d.ts +0 -4
- package/dist/utils/wpcom-auth-redirects.js +0 -6
- package/dist/vocabulary.d.ts +2 -2
- package/package.json +6 -1
- package/dist/contracts/space-theme.d.ts +0 -25
- package/dist/contracts/space-theme.js +0 -49
- package/dist/utils/gate-theme.d.ts +0 -61
- package/dist/utils/gate-theme.js +0 -217
- package/dist/utils/space-theme.d.ts +0 -18
- 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("&", "&")
|
|
8
|
+
.replaceAll("<", "<")
|
|
9
|
+
.replaceAll(">", ">")
|
|
10
|
+
.replaceAll('"', """)
|
|
11
|
+
.replaceAll("'", "'");
|
|
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];
|
package/dist/utils/query-keys.js
CHANGED
|
@@ -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
|
-
/**
|
|
46
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/vocabulary.d.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.0.10",
|
|
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;
|