@spacefast/common 0.0.11 → 0.0.12
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/agents/connect-targets.d.ts +4 -4
- package/dist/agents/connect-targets.js +3 -3
- package/dist/agents/private-key-oauth.d.ts +37 -0
- package/dist/agents/private-key-oauth.js +158 -0
- package/dist/config/domains.js +1 -1
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +1 -0
- package/dist/config/postgres-budget.d.ts +126 -0
- package/dist/config/postgres-budget.js +135 -0
- package/dist/config/postgres-budget.json +50 -0
- package/dist/contracts/access.d.ts +42 -24
- package/dist/contracts/access.js +73 -34
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +5 -5
- package/dist/contracts/api-keys.d.ts +53 -45
- package/dist/contracts/api-keys.js +1 -0
- package/dist/contracts/archives.d.ts +8 -8
- package/dist/contracts/builds.d.ts +8 -8
- package/dist/contracts/builds.js +28 -48
- package/dist/contracts/channels.js +1 -2
- package/dist/contracts/comments.d.ts +55 -19
- package/dist/contracts/comments.js +56 -17
- package/dist/contracts/common.d.ts +4 -3
- package/dist/contracts/common.js +2 -0
- package/dist/contracts/deployments.d.ts +3 -3
- package/dist/contracts/device-auth.d.ts +14 -12
- package/dist/contracts/docs.d.ts +5 -5
- package/dist/contracts/domains.d.ts +261 -1330
- package/dist/contracts/domains.js +39 -301
- package/dist/contracts/email-preferences.d.ts +50 -0
- package/dist/contracts/email-preferences.js +44 -0
- package/dist/contracts/enums.d.ts +1 -1
- package/dist/contracts/error-code-meta.d.ts +84 -15
- package/dist/contracts/error-code-meta.js +28 -4
- package/dist/contracts/error-codes.d.ts +1 -1
- package/dist/contracts/error-codes.js +24 -6
- package/dist/contracts/events.d.ts +13 -13
- package/dist/contracts/feature-lifecycle.d.ts +644 -2
- package/dist/contracts/feature-lifecycle.js +685 -2
- package/dist/contracts/features.d.ts +236 -24
- package/dist/contracts/features.js +3 -272
- package/dist/contracts/ids.d.ts +1 -2
- package/dist/contracts/ids.js +1 -2
- package/dist/contracts/internal.d.ts +3 -3
- package/dist/contracts/mcp.d.ts +6 -6
- package/dist/contracts/me.js +1 -1
- package/dist/contracts/mounts.d.ts +142 -0
- package/dist/contracts/mounts.js +56 -0
- package/dist/contracts/oauth-resources.d.ts +5 -5
- package/dist/contracts/oauth-resources.js +1 -0
- package/dist/contracts/operations.d.ts +2 -2
- package/dist/contracts/pages.d.ts +2 -2
- package/dist/contracts/pages.js +8 -3
- package/dist/contracts/plan-policy.d.ts +1 -1
- package/dist/contracts/platform.d.ts +3 -3
- package/dist/contracts/resources.d.ts +12 -20
- package/dist/contracts/resources.js +13 -37
- package/dist/contracts/runtime-api.d.ts +330 -16
- package/dist/contracts/runtime-api.js +58 -14
- package/dist/contracts/sf-config-v1.js +1 -1
- package/dist/contracts/sites.d.ts +12 -12
- package/dist/contracts/sites.js +18 -11
- package/dist/contracts/space-config.d.ts +2 -3
- package/dist/contracts/space-config.js +3 -1
- package/dist/contracts/spaces.d.ts +143 -102
- package/dist/contracts/spaces.js +109 -43
- package/dist/contracts/superadmin-emails.d.ts +25 -1
- package/dist/contracts/superadmin-emails.js +10 -15
- package/dist/contracts/superadmin-runtime.d.ts +2 -2
- package/dist/contracts/superadmin-spaces.d.ts +6 -14
- package/dist/contracts/tags.d.ts +43 -13
- package/dist/contracts/tags.js +195 -11
- package/dist/contracts/transfers.d.ts +1 -1
- package/dist/contracts/variables.d.ts +4 -4
- package/dist/contracts/variables.js +4 -3
- package/dist/contracts/webhooks.d.ts +2 -2
- package/dist/contracts/zero.d.ts +329 -7
- package/dist/contracts/zero.js +121 -2
- package/dist/dashboard-paths/index.d.ts +1 -1
- package/dist/docs/agent-setup.d.ts +4 -2
- package/dist/docs/agent-setup.js +15 -7
- package/dist/docs/agent-solutions.js +1 -1
- package/dist/docs/catalog.d.ts +8 -10
- package/dist/docs/catalog.js +8 -10
- package/dist/docs/error-docs.js +86 -18
- package/dist/utils/access-grant.d.ts +14 -0
- package/dist/utils/access-grant.js +88 -0
- package/dist/utils/access-match.d.ts +7 -0
- package/dist/utils/access-match.fixtures.json +513 -0
- package/dist/utils/access-match.js +32 -16
- package/dist/utils/access-verdict.fixtures.json +421 -0
- package/dist/utils/credential-policy.d.ts +1 -1
- package/dist/utils/dns-instructions.d.ts +29 -0
- package/dist/utils/dns-instructions.js +78 -0
- package/dist/utils/domain-diagnostics.d.ts +29 -0
- package/dist/utils/domain-diagnostics.js +63 -0
- package/dist/utils/egress-policy.fixtures.json +51 -0
- package/dist/utils/id-hints.d.ts +1 -0
- package/dist/utils/id-hints.js +55 -13
- package/dist/utils/page-colors.d.ts +50 -0
- package/dist/utils/page-colors.js +167 -0
- package/dist/utils/page-fonts.d.ts +2 -0
- package/dist/utils/page-fonts.js +4 -0
- package/dist/utils/pages.d.ts +8 -1
- package/dist/utils/pages.js +123 -37
- package/dist/utils/production-runtime.d.ts +18 -0
- package/dist/utils/production-runtime.js +28 -0
- package/dist/utils/query-keys.d.ts +6 -7
- package/dist/utils/query-keys.js +11 -12
- package/dist/utils/sf-config-v1.fixtures.json +9 -0
- package/dist/utils/sf-config-v1.js +28 -19
- package/dist/utils/source-archive-policy.d.ts +2 -0
- package/dist/utils/source-archive-policy.js +2 -0
- package/dist/utils/static-runtime-policy.fixtures.json +57 -0
- package/dist/utils/static-runtime-policy.js +1 -1
- package/package.json +5 -4
- package/dist/contracts/annotations.d.ts +0 -327
- package/dist/contracts/annotations.js +0 -235
- package/dist/contracts/generated-feature-launch-entries.d.ts +0 -2
- package/dist/contracts/generated-feature-launch-entries.js +0 -518
- package/dist/contracts/publishes.d.ts +0 -135
- package/dist/contracts/publishes.js +0 -162
package/dist/utils/pages.js
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SPACEFAST_WORDMARK_PATH } from "../brand-assets.js";
|
|
2
|
+
import { CUSTOM_PAGE_IDS, PAGE_CATALOG, PAGE_ELEMENTS, PAGE_LAYOUT_FILE, PAGE_SOURCE_MAX_BYTES, SITE_PAGE_IDS, } from "../contracts/pages.js";
|
|
3
|
+
import { SPACE_CONFIG_ACCEPTED_FILES } from "../contracts/space-config.js";
|
|
4
|
+
import { derivePageColors, PAGE_COLOR_DEFAULTS } from "./page-colors.js";
|
|
5
|
+
import { PAGE_EMBEDDED_FONT_FACES, PAGE_FONT_FACES } from "./page-fonts.js";
|
|
2
6
|
const encoder = new TextEncoder();
|
|
3
7
|
const knownElements = new Set(PAGE_ELEMENTS);
|
|
4
8
|
const customPageIds = new Set(CUSTOM_PAGE_IDS);
|
|
9
|
+
export function isPagePublicPath(path) {
|
|
10
|
+
const lower = path.toLowerCase();
|
|
11
|
+
if (lower === "theme.json" ||
|
|
12
|
+
lower === "_redirects" ||
|
|
13
|
+
lower === "_headers" ||
|
|
14
|
+
lower === "_config.json" ||
|
|
15
|
+
lower === "_routes.json" ||
|
|
16
|
+
SPACE_CONFIG_ACCEPTED_FILES.some((candidate) => candidate === lower) ||
|
|
17
|
+
lower.split("/").includes("_pages") ||
|
|
18
|
+
lower === "_layout.html" ||
|
|
19
|
+
lower.endsWith("/_layout.html"))
|
|
20
|
+
return false;
|
|
21
|
+
return !lower
|
|
22
|
+
.split("/")
|
|
23
|
+
.some((segment, index) => segment.startsWith(".") && !(index === 0 && segment === ".well-known"));
|
|
24
|
+
}
|
|
5
25
|
export function escapePageHtml(value) {
|
|
6
26
|
return value
|
|
7
27
|
.replaceAll("&", "&")
|
|
@@ -10,38 +30,92 @@ export function escapePageHtml(value) {
|
|
|
10
30
|
.replaceAll('"', """)
|
|
11
31
|
.replaceAll("'", "'");
|
|
12
32
|
}
|
|
33
|
+
// Pushed colors are intent; the readability engine (page-colors.ts) derives
|
|
34
|
+
// what the templates actually paint with. A custom background pins a complete
|
|
35
|
+
// scheme for both OS modes — emitting `--sf-bg` alone used to leave dark-mode
|
|
36
|
+
// visitors with the dark foreground on the user's light background.
|
|
13
37
|
function themeStyle(theme) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
38
|
+
const root = [];
|
|
39
|
+
const dark = [];
|
|
40
|
+
if (theme.accent || theme.background) {
|
|
41
|
+
const derived = derivePageColors(theme);
|
|
42
|
+
// `derived.accent` is the parse-validated input (or the default), never
|
|
43
|
+
// an arbitrary string straight from the theme.
|
|
44
|
+
if (theme.accent)
|
|
45
|
+
root.push(`--sf-accent:${derived.accent}`);
|
|
46
|
+
root.push(`--sf-on-accent:${derived.onAccent}`, `--sf-link:${derived.link}`);
|
|
47
|
+
if (derived.pinned) {
|
|
48
|
+
root.push(`--sf-bg:${derived.pinned.bg}`, `--sf-fg:${derived.pinned.fg}`, `--sf-muted:${derived.pinned.muted}`, `--sf-card:${derived.pinned.card}`, `--sf-border:${derived.pinned.border}`, `--sf-error:${derived.pinned.error}`, `color-scheme:${derived.scheme}`);
|
|
49
|
+
}
|
|
50
|
+
else if (derived.linkDark) {
|
|
51
|
+
dark.push(`--sf-link:${derived.linkDark}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (theme.font)
|
|
55
|
+
root.push(`--sf-font:${theme.font}`, `--sf-display-font:${theme.font}`);
|
|
56
|
+
if (root.length === 0)
|
|
57
|
+
return "";
|
|
58
|
+
const darkBlock = dark.length > 0 ? `@media(prefers-color-scheme:dark){:root{${dark.join(";")}}}` : "";
|
|
59
|
+
return `<style>:root{${root.join(";")}}${darkBlock}</style>`;
|
|
20
60
|
}
|
|
61
|
+
const runtimeSlot = (name, fallback) => `<!--sf-runtime:${name}:start-->${fallback}<!--sf-runtime:${name}:end-->`;
|
|
21
62
|
function renderFiles(files) {
|
|
22
63
|
if (files.length === 0)
|
|
23
64
|
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 ?
|
|
65
|
+
return `<ul class="sf-files" role="list">${files
|
|
66
|
+
.map((file) => `<li><a href="${escapePageHtml(file.href)}"><span class="sf-file-name">${escapePageHtml(file.name)}${file.directory ? '<span aria-hidden="true">/</span>' : ""}</span></a>${file.size ? `<span class="sf-file-size">${escapePageHtml(file.size)}</span>` : ""}</li>`)
|
|
26
67
|
.join("")}</ul>`;
|
|
27
68
|
}
|
|
28
69
|
function renderFile(file) {
|
|
29
70
|
if (!file)
|
|
30
71
|
return '<p class="sf-empty">File unavailable.</p>';
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
72
|
+
const rawHref = `${file.href}${file.href.includes("?") ? "&" : "?"}spacefast_raw=1`;
|
|
73
|
+
const href = escapePageHtml(rawHref);
|
|
74
|
+
const name = escapePageHtml(file.name);
|
|
75
|
+
const media = file.mime?.startsWith("image/")
|
|
76
|
+
? `<img class="sf-preview" src="${href}" alt="">`
|
|
77
|
+
: `<iframe class="sf-preview" src="${href}" title="Preview of ${name}" loading="lazy" sandbox></iframe>`;
|
|
78
|
+
const downloadHref = file.downloadHref
|
|
79
|
+
? `${file.downloadHref}${file.downloadHref.includes("?") ? "&" : "?"}spacefast_raw=1`
|
|
80
|
+
: rawHref;
|
|
81
|
+
return `<div class="sf-preview-wrap">${media}</div><dl class="sf-file-meta"><div><dt>Name</dt><dd>${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><div class="sf-actions"><a class="sf-button" href="${escapePageHtml(downloadHref)}" download>Download</a><a class="sf-ghost" href="${href}">View raw</a></div>`;
|
|
82
|
+
}
|
|
83
|
+
function renderSpacefastBranding(hidden) {
|
|
84
|
+
const wordmark = hidden
|
|
85
|
+
? ""
|
|
86
|
+
: `<span class="sf-powered" tabindex="0"><svg class="sf-wordmark" viewBox="0 0 326 62" role="img" aria-label="Spacefast"><rect class="sf-wordmark-shadow" x="14" y="12" width="306" height="48"></rect><rect class="sf-wordmark-plate" x="10" y="8" width="306" height="48"></rect><path class="sf-wordmark-paper" d="${SPACEFAST_WORDMARK_PATH}"></path></svg><span class="sf-powered-line"><a href="https://spacefast.com">Best way to share what your agent made</a></span></span>`;
|
|
87
|
+
return `<footer class="sf-colophon"><div class="sf-colophon-row">${wordmark}<a class="sf-help" href="https://spacefast.com/help">Need help?</a></div></footer>`;
|
|
36
88
|
}
|
|
37
89
|
function renderElement(element, context) {
|
|
38
90
|
switch (element) {
|
|
39
91
|
case "sf-content":
|
|
40
92
|
return context.content ?? "";
|
|
41
|
-
case "sf-password-form":
|
|
42
|
-
|
|
93
|
+
case "sf-password-form": {
|
|
94
|
+
const hasError = Boolean(context.passwordError);
|
|
95
|
+
const error = hasError
|
|
96
|
+
? `<p class="sf-error" id="sf-password-error" role="alert">${escapePageHtml(context.passwordError ?? "")}</p>`
|
|
97
|
+
: "";
|
|
98
|
+
const form = `<form class="sf-form" method="post" action="${escapePageHtml(context.passwordAction ?? "")}"><label for="sf-password">Password</label><div class="sf-form-row"><input id="sf-password" name="_pw" type="password" required autocomplete="current-password"${hasError ? ' aria-invalid="true" aria-describedby="sf-password-error"' : ""}><button type="submit">Continue</button></div>${error}</form>`;
|
|
99
|
+
return runtimeSlot("challenge", form);
|
|
100
|
+
}
|
|
43
101
|
case "sf-login":
|
|
44
|
-
return `<a class="sf-button" href="${escapePageHtml(context.loginHref ?? "/__spacefast/access/login")}">
|
|
102
|
+
return runtimeSlot("challenge", `<div class="sf-actions"><a class="sf-button" href="${escapePageHtml(context.loginHref ?? "/__spacefast/access/login")}">Continue with Spacefast</a></div>`);
|
|
103
|
+
case "sf-denial-context":
|
|
104
|
+
return runtimeSlot("denial", '<p class="sf-copy">Access to this space is restricted.</p>');
|
|
105
|
+
case "sf-page-path":
|
|
106
|
+
return context.displayPath
|
|
107
|
+
? escapePageHtml(context.displayPath)
|
|
108
|
+
: runtimeSlot("request-path", "this address");
|
|
109
|
+
case "sf-page-title":
|
|
110
|
+
return escapePageHtml(context.displayTitle ?? context.theme?.name ?? "This space");
|
|
111
|
+
case "sf-page-summary": {
|
|
112
|
+
const count = context.itemCount ?? context.files?.length ?? 0;
|
|
113
|
+
const visible = context.visibleItemCount ?? context.files?.length ?? count;
|
|
114
|
+
const label = `${count} ${count === 1 ? "item" : "items"}`;
|
|
115
|
+
return escapePageHtml(visible < count ? `${label} · showing first ${visible}` : label);
|
|
116
|
+
}
|
|
117
|
+
case "sf-spacefast-branding":
|
|
118
|
+
return renderSpacefastBranding(context.hideSpacefastBranding);
|
|
45
119
|
case "sf-files":
|
|
46
120
|
return renderFiles(context.files ?? []);
|
|
47
121
|
case "sf-file":
|
|
@@ -61,6 +135,7 @@ export function expandPageHtml(source, context) {
|
|
|
61
135
|
const name = (paired ?? unary)?.toLowerCase();
|
|
62
136
|
return name && knownElements.has(name) ? renderElement(name, context) : _match;
|
|
63
137
|
});
|
|
138
|
+
output = output.replaceAll("/*sf-default-fonts*/", context.embedPageFonts ? PAGE_EMBEDDED_FONT_FACES : PAGE_FONT_FACES);
|
|
64
139
|
const tokens = themeStyle(context.theme ?? {});
|
|
65
140
|
if (tokens) {
|
|
66
141
|
output = /<\s*\/\s*head\s*>/i.test(output)
|
|
@@ -115,9 +190,17 @@ export function validatePageSources(input) {
|
|
|
115
190
|
});
|
|
116
191
|
const allowed = element === "sf-logo" ||
|
|
117
192
|
element === "sf-name" ||
|
|
193
|
+
element === "sf-spacefast-branding" ||
|
|
194
|
+
(id === "404" && element === "sf-page-path") ||
|
|
118
195
|
(id === "password" && element === "sf-password-form") ||
|
|
119
|
-
(id === "
|
|
120
|
-
(id === "
|
|
196
|
+
(id === "denied" && element === "sf-denial-context") ||
|
|
197
|
+
(id === "index" &&
|
|
198
|
+
(element === "sf-files" ||
|
|
199
|
+
element === "sf-page-path" ||
|
|
200
|
+
element === "sf-page-title" ||
|
|
201
|
+
element === "sf-page-summary")) ||
|
|
202
|
+
(id === "preview" &&
|
|
203
|
+
(element === "sf-file" || element === "sf-page-path" || element === "sf-page-title"));
|
|
121
204
|
if (knownElements.has(element) && !allowed)
|
|
122
205
|
diagnostics.push({
|
|
123
206
|
severity: "error",
|
|
@@ -169,7 +252,11 @@ export function validatePageSources(input) {
|
|
|
169
252
|
message: `Unknown Pages element <${element}>.`,
|
|
170
253
|
path,
|
|
171
254
|
});
|
|
172
|
-
else if (element !== "sf-content" &&
|
|
255
|
+
else if (element !== "sf-content" &&
|
|
256
|
+
element !== "sf-logo" &&
|
|
257
|
+
element !== "sf-name" &&
|
|
258
|
+
element !== "sf-page-title" &&
|
|
259
|
+
element !== "sf-spacefast-branding")
|
|
173
260
|
diagnostics.push({
|
|
174
261
|
severity: "error",
|
|
175
262
|
code: "page_element_not_allowed",
|
|
@@ -242,26 +329,25 @@ export function defaultPageBody(id) {
|
|
|
242
329
|
const source = DEFAULT_PAGE_TEMPLATES[id];
|
|
243
330
|
return source.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] ?? source;
|
|
244
331
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
332
|
+
// The built-in palette runs through the same readability engine as pushed
|
|
333
|
+
// themes, so buttons and links remain readable for every accepted accent.
|
|
334
|
+
const defaultPageColors = derivePageColors({});
|
|
335
|
+
const sharedCss = `/*sf-default-fonts*/:root{color-scheme:light dark;--sf-accent:${PAGE_COLOR_DEFAULTS.accent};--sf-bg:${PAGE_COLOR_DEFAULTS.bg};--sf-fg:${PAGE_COLOR_DEFAULTS.fg};--sf-muted:${PAGE_COLOR_DEFAULTS.muted};--sf-card:${PAGE_COLOR_DEFAULTS.card};--sf-border:${PAGE_COLOR_DEFAULTS.border};--sf-link:${defaultPageColors.link};--sf-on-accent:${defaultPageColors.onAccent};--sf-error:#a82d1a;--sf-font:"Haskoy",-apple-system,BlinkMacSystemFont,ui-sans-serif,"Segoe UI",sans-serif;--sf-display-font:"Merriweather",Georgia,serif;--sf-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace}*{box-sizing:border-box}html{min-width:320px;background:var(--sf-bg);-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh;background:var(--sf-bg);color:var(--sf-fg);font:14px/1.65 var(--sf-font)}a{color:inherit}.sf-page{min-height:100vh;display:flex;flex-direction:column;padding:clamp(22px,4vw,44px)}.sf-header{display:flex;align-items:center;min-height:32px}.sf-page-center .sf-header{justify-content:center}.sf-brand{display:inline-flex;min-width:0;align-items:center;gap:10px;color:var(--sf-fg);text-decoration:none}.sf-brand-mark{width:7px;height:7px;flex:none;background:var(--sf-accent)}.sf-logo{width:28px;height:28px;flex:none;object-fit:contain}.sf-brand strong{min-width:0;overflow-wrap:anywhere;font-size:13px;font-weight:600}.sf-main{flex:1;width:100%;padding:clamp(34px,9vh,84px) 0}.sf-page-plain .sf-main{max-width:680px}.sf-page-center .sf-main{max-width:620px;margin:auto;display:flex;flex-direction:column;align-items:center;text-align:center}.sf-eyebrow{margin:0 0 14px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.12em;text-transform:uppercase;overflow-wrap:anywhere}.sf-page-center .sf-eyebrow{margin-bottom:16px}h1{max-width:20ch;margin:0 0 12px;color:var(--sf-fg);font:430 clamp(1.9rem,5vw,2.8rem)/1.22 var(--sf-display-font);letter-spacing:-.012em;text-wrap:balance}.sf-copy,.sf-sub{max-width:52ch;margin:0;color:var(--sf-muted);text-wrap:pretty}.sf-inline-path{font-family:var(--sf-mono);font-size:.9em;color:var(--sf-fg);overflow-wrap:anywhere}.sf-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:22px}.sf-page-center .sf-actions{justify-content:center}.sf-button,.sf-form button,.sf-login{display:inline-flex;min-height:44px;align-items:center;justify-content:center;border:0;background:var(--sf-accent);color:var(--sf-on-accent);padding:10px 16px;font:600 13px/1.4 var(--sf-font);text-decoration:none;cursor:pointer}.sf-ghost{color:var(--sf-muted);text-underline-offset:3px}.sf-button:hover,.sf-form button:hover,.sf-login:hover{filter:brightness(1.06)}a:focus-visible,button:focus-visible{outline:2px solid var(--sf-link);outline-offset:2px}.sf-form{width:100%;max-width:360px;margin-top:22px;text-align:left}.sf-form label,.sf-password label{display:block;margin-bottom:8px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.1em;text-transform:uppercase}.sf-password label{flex-basis:100%}.sf-form-row{display:flex;gap:10px}.sf-password form{display:flex;flex-wrap:wrap;gap:10px}.sf-form input,.sf-pw{min-width:0;flex:1;border:1px solid var(--sf-border);background:transparent;color:var(--sf-fg);padding:9px 11px;font:14px/1.5 var(--sf-font)}.sf-form input:focus-visible,.sf-pw:focus-visible{outline:2px solid var(--sf-link);outline-offset:-2px}.sf-form input[aria-invalid=true],.sf-pw[aria-invalid=true]{border-color:var(--sf-error)}.sf-form button,.sf-password button{margin:0}.sf-error{margin:9px 0 0;color:var(--sf-error);font-size:14px}.sf-password .sf-error{flex-basis:100%;margin:0}.sf-block{width:100%}.sf-login-block{display:flex;justify-content:center;margin-top:20px}.sf-copy+.sf-copy{margin-top:12px}.sf-files{width:100%;margin:24px 0 0;padding:0;list-style:none}.sf-files li{display:flex;min-width:0;align-items:baseline;gap:18px;border-bottom:1px solid var(--sf-border)}.sf-files a{min-width:0;flex:1;padding:10px 2px;color:var(--sf-fg);font-weight:500;text-decoration:none}.sf-files a:hover{color:var(--sf-link)}.sf-file-name{overflow-wrap:anywhere}.sf-file-size{flex:none;padding:10px 2px;color:var(--sf-muted);font:12px/1.5 var(--sf-mono);white-space:nowrap}.sf-empty{max-width:420px;margin:24px 0 0;border:1px dashed var(--sf-border);padding:18px 20px;color:var(--sf-muted)}.sf-preview-wrap{width:min(100%,560px);aspect-ratio:16/9;margin:10px 0 18px;overflow:hidden;background:#fff;outline:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);outline-offset:-1px}.sf-preview{display:block;width:100%;height:100%;border:0;object-fit:contain}.sf-file-meta{width:min(100%,560px);margin:0}.sf-file-meta div{display:flex;min-width:0;justify-content:space-between;gap:20px;border-bottom:1px solid var(--sf-border);padding:9px 2px}.sf-file-meta dt{color:var(--sf-fg);font-weight:600}.sf-file-meta dd{min-width:0;margin:0;color:var(--sf-muted);font-family:var(--sf-mono);overflow-wrap:anywhere;text-align:right}.sf-colophon{display:flex;flex-direction:column;align-items:center;gap:10px;padding:44px 0 26px;margin-top:auto;color:var(--sf-muted)}.sf-page-plain .sf-colophon{align-items:flex-start}.sf-colophon-row{display:flex;align-items:center;gap:20px;flex-wrap:wrap}.sf-powered{display:inline-flex;align-items:center;gap:10px;cursor:pointer;outline:none}.sf-powered-line{display:none;font-size:12px;color:var(--sf-muted)}.sf-powered:hover .sf-powered-line,.sf-powered:focus .sf-powered-line,.sf-powered:focus-within .sf-powered-line{display:inline}.sf-powered-line a{color:var(--sf-link);text-decoration:underline;text-underline-offset:3px}.sf-help{font-size:12px;color:var(--sf-muted);text-decoration:underline;text-underline-offset:3px}.sf-help:hover{color:var(--sf-fg)}.sf-wordmark{display:block;flex:none;width:auto;height:11px}.sf-wordmark-shadow{fill:var(--sf-accent)}.sf-wordmark-plate{fill:#141419}.sf-wordmark-paper{fill:#fbf8f1}@media(max-width:520px){.sf-page{padding:20px}.sf-form-row,.sf-password form{flex-direction:column}.sf-button,.sf-form button,.sf-login{min-height:48px}.sf-file-meta div{flex-direction:column;gap:2px}.sf-file-meta dd{text-align:left}.sf-colophon{padding-top:32px}}@media(prefers-color-scheme:dark){:root{--sf-bg:${PAGE_COLOR_DEFAULTS.darkBg};--sf-fg:${PAGE_COLOR_DEFAULTS.darkFg};--sf-muted:${PAGE_COLOR_DEFAULTS.darkMuted};--sf-card:${PAGE_COLOR_DEFAULTS.darkCard};--sf-border:${PAGE_COLOR_DEFAULTS.darkBorder};--sf-link:${defaultPageColors.linkDark};--sf-error:#ff8a7a}.sf-preview-wrap{outline-color:#ffffff1a}}@media(forced-colors:active){.sf-brand-mark{background:CanvasText}.sf-button,.sf-form button,.sf-login{border:1px solid ButtonText}.sf-powered:focus{outline:1px solid CanvasText;outline-offset:3px}}`;
|
|
336
|
+
const pageHeader = '<header class="sf-header"><a class="sf-brand" href="/" aria-label="Homepage"><span class="sf-brand-mark" aria-hidden="true"></span><sf-logo></sf-logo><strong><sf-name></sf-name></strong></a></header>';
|
|
337
|
+
const pageFooter = "<sf-spacefast-branding></sf-spacefast-branding>";
|
|
338
|
+
function document(id, title, body, layout) {
|
|
339
|
+
const noindex = PAGE_CATALOG[id].noindex ? '<meta name="robots" content="noindex">' : "";
|
|
340
|
+
return `<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${noindex}<title>${title}</title><style>${sharedCss}</style></head>\n<body><div class="sf-page sf-page-${layout}">${pageHeader}<main class="sf-main">${body}</main>${pageFooter}</div></body>\n</html>\n`;
|
|
248
341
|
}
|
|
249
|
-
const badge = '<p class="sf-badge">Hosted with <a href="https://spacefast.com">Spacefast</a></p>';
|
|
250
342
|
export const DEFAULT_PAGE_TEMPLATES = {
|
|
251
|
-
"404": document("Page not found", '<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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),
|
|
343
|
+
"404": document("404", "Page not found", '<p class="sf-eyebrow">404 · Not Found</p><h1>Page not found</h1><p class="sf-copy">There’s nothing at <span class="sf-inline-path"><sf-page-path></sf-page-path></span>.</p><div class="sf-actions"><a class="sf-button" href="/">Go to the homepage</a></div>', "center"),
|
|
344
|
+
password: document("password", "Password required", '<h1>This space is protected</h1><p class="sf-sub">Enter the password to continue.</p><sf-password-form></sf-password-form>', "center"),
|
|
345
|
+
denied: document("denied", "Access denied", "<h1>Access denied</h1><sf-denial-context></sf-denial-context>", "center"),
|
|
346
|
+
login: document("login", "Sign in", '<h1>This space is for members</h1><p class="sf-sub">Sign in to continue — you’ll come right back here.</p><sf-login></sf-login>', "center"),
|
|
347
|
+
index: document("index", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><p class="sf-sub"><sf-page-summary></sf-page-summary></p><sf-files></sf-files>', "plain"),
|
|
348
|
+
preview: document("preview", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><sf-file></sf-file>', "plain"),
|
|
263
349
|
};
|
|
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-
|
|
350
|
+
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-page-title></sf-page-title></title><style>${sharedCss}</style></head>\n<body><sf-content></sf-content></body>\n</html>\n`;
|
|
265
351
|
export function defaultPageTemplate(id) {
|
|
266
352
|
return DEFAULT_PAGE_TEMPLATES[id];
|
|
267
353
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type RuntimeEnvironment = Record<string, string | undefined>;
|
|
2
|
+
type RailwayRuntimeEvidence = {
|
|
3
|
+
kind: "railway";
|
|
4
|
+
runtimeEnvironment: string | undefined;
|
|
5
|
+
env: RuntimeEnvironment;
|
|
6
|
+
};
|
|
7
|
+
type BrowserRuntimeEvidence = {
|
|
8
|
+
kind: "browser";
|
|
9
|
+
currentUrl: string;
|
|
10
|
+
productionOrigin: string;
|
|
11
|
+
};
|
|
12
|
+
export type ProductionRuntimeEvidence = RailwayRuntimeEvidence | BrowserRuntimeEvidence;
|
|
13
|
+
/**
|
|
14
|
+
* Proves that telemetry is executing in a deployed production runtime.
|
|
15
|
+
* Environment labels alone are configuration, not runtime evidence.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isProductionRuntime(evidence: ProductionRuntimeEvidence): boolean;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const RAILWAY_RUNTIME_IDENTITY_KEYS = [
|
|
2
|
+
"RAILWAY_PROJECT_ID",
|
|
3
|
+
"RAILWAY_ENVIRONMENT_ID",
|
|
4
|
+
"RAILWAY_SERVICE_ID",
|
|
5
|
+
"RAILWAY_DEPLOYMENT_ID",
|
|
6
|
+
];
|
|
7
|
+
function present(value) {
|
|
8
|
+
return Boolean(value?.trim());
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Proves that telemetry is executing in a deployed production runtime.
|
|
12
|
+
* Environment labels alone are configuration, not runtime evidence.
|
|
13
|
+
*/
|
|
14
|
+
export function isProductionRuntime(evidence) {
|
|
15
|
+
if (evidence.kind === "railway") {
|
|
16
|
+
return (evidence.runtimeEnvironment?.trim().toLowerCase() === "production" &&
|
|
17
|
+
evidence.env.RAILWAY_ENVIRONMENT_NAME?.trim().toLowerCase() === "production" &&
|
|
18
|
+
RAILWAY_RUNTIME_IDENTITY_KEYS.every((key) => present(evidence.env[key])));
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const current = new URL(evidence.currentUrl);
|
|
22
|
+
const production = new URL(evidence.productionOrigin);
|
|
23
|
+
return production.protocol === "https:" && current.origin === production.origin;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -21,7 +21,6 @@ export declare const qk: {
|
|
|
21
21
|
teamSpaces: (id: string) => readonly ["teams", string, "spaces"];
|
|
22
22
|
teamDomains: (id: string) => readonly ["teams", string, "domains"];
|
|
23
23
|
teamManagedDomains: (id: string) => readonly ["teams", string, "managed-domains"];
|
|
24
|
-
domainBindings: () => readonly ["domain-bindings"];
|
|
25
24
|
slugAvailability: (kind: "team" | "space", slug: string, teamId?: string, currentSlug?: string) => readonly ["slugs", "availability", "space" | "team", string, string, string];
|
|
26
25
|
teamVariables: (id: string) => readonly ["teams", string, "variables"];
|
|
27
26
|
teamActivity: (id: string, limit: number) => readonly ["teams", string, "activity", number];
|
|
@@ -30,8 +29,8 @@ export declare const qk: {
|
|
|
30
29
|
spaceVersions: (id: string) => readonly ["spaces", string, "versions"];
|
|
31
30
|
spaceVersionsHistory: (id: string) => readonly ["spaces", string, "versions", "history"];
|
|
32
31
|
spaceCollaboration: (id: string) => readonly ["spaces", string, "collaboration"];
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
spaceComments: (id: string) => readonly ["spaces", string, "comments"];
|
|
33
|
+
spaceCommentThread: (id: string, threadId: string) => readonly ["spaces", string, "comments", string];
|
|
35
34
|
spaceActivity: (id: string, limit: number) => readonly ["spaces", string, "activity", number];
|
|
36
35
|
spaceDeployments: (id: string) => readonly ["spaces", string, "deployments"];
|
|
37
36
|
spaceTags: (id: string) => readonly ["spaces", string, "tags"];
|
|
@@ -39,11 +38,11 @@ export declare const qk: {
|
|
|
39
38
|
spaceTagDiff: (id: string, versionId: string) => readonly ["spaces", string, "tags", string, "diff"];
|
|
40
39
|
spaceTagReleases: (id: string) => readonly ["spaces", string, "tags", "releases"];
|
|
41
40
|
spaceTagsEffective: (id: string) => readonly ["spaces", string, "tags", "effective"];
|
|
42
|
-
build: (id: string) => readonly ["builds", string];
|
|
43
|
-
buildLogs: (id: string, limit?: number) => readonly ["builds", string, "logs"] | readonly ["builds", string, "logs", number];
|
|
41
|
+
build: (spaceId: string, id: string) => readonly ["spaces", string, "builds", string];
|
|
42
|
+
buildLogs: (spaceId: string, id: string, limit?: number) => readonly ["spaces", string, "builds", string, "logs"] | readonly ["spaces", string, "builds", string, "logs", number];
|
|
44
43
|
spaceBuilds: (id: string) => readonly ["spaces", string, "builds"];
|
|
45
44
|
spaceRepositoryConnection: (id: string, connectionType?: string) => readonly ["spaces", string, "repository-connection", string];
|
|
46
|
-
repositoryInstallations: (spaceId: string) => readonly ["
|
|
45
|
+
repositoryInstallations: (spaceId: string) => readonly ["spaces", string, "repository-installations"];
|
|
47
46
|
repositoryInstallationRepositories: (installationId: string) => readonly ["repository-connections", "installations", string, "repositories"];
|
|
48
47
|
spaceRepositoryAnalyze: (id: string, connectionType?: string, ref?: string, root?: string) => readonly ["spaces", string, "repository-connection", string, "analyze", string, string];
|
|
49
48
|
spaceRepositoryWebhookDeliveries: (id: string, connectionType?: string) => readonly ["spaces", string, "repository-webhook-deliveries", string];
|
|
@@ -51,7 +50,7 @@ export declare const qk: {
|
|
|
51
50
|
webhook: (webhookId: string) => readonly ["webhooks", string];
|
|
52
51
|
webhookDeliveries: (webhookId: string) => readonly ["webhooks", string, "deliveries"];
|
|
53
52
|
spaceVersion: (id: string, versionId: string) => readonly ["spaces", string, "versions", string];
|
|
54
|
-
|
|
53
|
+
spaceVersionComments: (id: string, versionId: string, status?: string) => readonly ["spaces", string, "versions", string, "comments"] | readonly ["spaces", string, "versions", string, "comments", string];
|
|
55
54
|
spaceVersionFiles: (id: string, versionId: string) => readonly ["spaces", string, "versions", string, "files"];
|
|
56
55
|
spaceVersionFile: (id: string, versionId: string, path: string) => readonly ["spaces", string, "versions", string, "files", string];
|
|
57
56
|
spaceVersionFileDirectUrl: (id: string, versionId: string, path: string) => readonly ["spaces", string, "versions", string, "files", string, "direct-url"];
|
package/dist/utils/query-keys.js
CHANGED
|
@@ -26,7 +26,6 @@ export const qk = {
|
|
|
26
26
|
teamSpaces: (id) => ["teams", id, "spaces"],
|
|
27
27
|
teamDomains: (id) => ["teams", id, "domains"],
|
|
28
28
|
teamManagedDomains: (id) => ["teams", id, "managed-domains"],
|
|
29
|
-
domainBindings: () => ["domain-bindings"],
|
|
30
29
|
slugAvailability: (kind, slug, teamId, currentSlug) => ["slugs", "availability", kind, slug, teamId ?? "", currentSlug ?? ""],
|
|
31
30
|
teamVariables: (id) => ["teams", id, "variables"],
|
|
32
31
|
teamActivity: (id, limit) => ["teams", id, "activity", limit],
|
|
@@ -38,8 +37,8 @@ export const qk = {
|
|
|
38
37
|
// `invalidateQueries({ queryKey: qk.spaceVersions(id) })` still reaches it.
|
|
39
38
|
spaceVersionsHistory: (id) => [...qk.spaceVersions(id), "history"],
|
|
40
39
|
spaceCollaboration: (id) => ["spaces", id, "collaboration"],
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
spaceComments: (id) => ["spaces", id, "comments"],
|
|
41
|
+
spaceCommentThread: (id, threadId) => [...qk.spaceComments(id), threadId],
|
|
43
42
|
spaceActivity: (id, limit) => ["spaces", id, "activity", limit],
|
|
44
43
|
spaceDeployments: (id) => ["spaces", id, "deployments"],
|
|
45
44
|
spaceTags: (id) => ["spaces", id, "tags"],
|
|
@@ -49,15 +48,15 @@ export const qk = {
|
|
|
49
48
|
// The merged (system+team+space) read-only view; never a write base — see
|
|
50
49
|
// `fetchSpaceReleasedGraph` in apps/my/src/lib/space-queries.ts.
|
|
51
50
|
spaceTagsEffective: (id) => ["spaces", id, "tags", "effective"],
|
|
52
|
-
build: (id) => [
|
|
51
|
+
build: (spaceId, id) => [...qk.spaceBuilds(spaceId), id],
|
|
53
52
|
// Observers with different limits must not share a cache entry; calling with
|
|
54
53
|
// no limit yields the bare prefix for invalidating every limit at once.
|
|
55
|
-
buildLogs: (id, limit) => limit === undefined
|
|
56
|
-
? [
|
|
57
|
-
: [
|
|
54
|
+
buildLogs: (spaceId, id, limit) => limit === undefined
|
|
55
|
+
? [...qk.build(spaceId, id), "logs"]
|
|
56
|
+
: [...qk.build(spaceId, id), "logs", limit],
|
|
58
57
|
spaceBuilds: (id) => ["spaces", id, "builds"],
|
|
59
58
|
spaceRepositoryConnection: (id, connectionType = "connected") => ["spaces", id, "repository-connection", connectionType],
|
|
60
|
-
repositoryInstallations: (spaceId) => ["repository-
|
|
59
|
+
repositoryInstallations: (spaceId) => [...qk.space(spaceId), "repository-installations"],
|
|
61
60
|
repositoryInstallationRepositories: (installationId) => ["repository-connections", "installations", installationId, "repositories"],
|
|
62
61
|
spaceRepositoryAnalyze: (id, connectionType = "connected", ref = "", root = "") => ["spaces", id, "repository-connection", connectionType, "analyze", ref, root],
|
|
63
62
|
spaceRepositoryWebhookDeliveries: (id, connectionType = "connected") => ["spaces", id, "repository-webhook-deliveries", connectionType],
|
|
@@ -66,10 +65,10 @@ export const qk = {
|
|
|
66
65
|
webhookDeliveries: (webhookId) => ["webhooks", webhookId, "deliveries"],
|
|
67
66
|
spaceVersion: (id, versionId) => ["spaces", id, "versions", versionId],
|
|
68
67
|
// Calling with no status yields the bare prefix, for invalidating every
|
|
69
|
-
// status-scoped
|
|
70
|
-
|
|
71
|
-
? ["spaces", id, "versions", versionId, "
|
|
72
|
-
: ["spaces", id, "versions", versionId, "
|
|
68
|
+
// status-scoped version comment list at once.
|
|
69
|
+
spaceVersionComments: (id, versionId, status) => status === undefined
|
|
70
|
+
? ["spaces", id, "versions", versionId, "comments"]
|
|
71
|
+
: ["spaces", id, "versions", versionId, "comments", status],
|
|
73
72
|
spaceVersionFiles: (id, versionId) => ["spaces", id, "versions", versionId, "files"],
|
|
74
73
|
spaceVersionFile: (id, versionId, path) => ["spaces", id, "versions", versionId, "files", path],
|
|
75
74
|
spaceVersionFileDirectUrl: (id, versionId, path) => ["spaces", id, "versions", versionId, "files", path, "direct-url"],
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"minimal.jsonc": "{\n // The canonical starter is intentionally tiny.\n \"$schema\": \"https://spacefast.com/schemas/sf.v1.json\",\n \"version\": 1,\n}\n",
|
|
3
|
+
"full.jsonc": "{\n \"version\": 1,\n \"build\": {\n \"installRoot\": \"packages/site\",\n \"installCommand\": \"bun install --frozen-lockfile\",\n \"command\": \"bun run build\",\n \"output\": \"dist\",\n \"timeoutSeconds\": 600\n },\n \"serve\": {\n \"index\": \"index.html\",\n \"spaFallback\": \"index.html\",\n \"cleanUrls\": false,\n \"directoryListing\": false\n },\n \"metadata\": {\n \"title\": \"Docs\",\n \"description\": \"The useful bits.\",\n \"image\": \"/og.png\"\n },\n \"substitute\": { \"files\": [\"assets/runtime.js\"] }\n}\n",
|
|
4
|
+
"invalid-values.jsonc": "{\n \"version\": 1,\n \"build\": {\n \"installRoot\": \"/outside\",\n \"timeoutSeconds\": 30\n },\n \"serve\": { \"cleanUrls\": null },\n \"substitute\": { \"files\": \"index.html\" }\n}\n",
|
|
5
|
+
"removed-identity.jsonc": "{\n \"version\": 1,\n \"space\": \"docs\",\n \"placement\": \"dedicated\",\n \"inject\": { \"head\": \"nope\" }\n}\n",
|
|
6
|
+
"retired-template.jsonc": "{\n \"version\": 1,\n \"substitute\": { \"files\": [\"runtime.js\"] }\n}\n",
|
|
7
|
+
"syntax-error.jsonc": "{\n \"version\": 1,\n \"serve\": { \"index\": \"index.html\" }\n",
|
|
8
|
+
"unknown-nested.jsonc": "{\n \"version\": 1,\n \"serve\": {\n \"directoryListings\": true\n }\n}\n"
|
|
9
|
+
}
|
|
@@ -127,7 +127,7 @@ class JsoncReader {
|
|
|
127
127
|
}
|
|
128
128
|
this.offset += 4;
|
|
129
129
|
}
|
|
130
|
-
this.fail("Unterminated string", start);
|
|
130
|
+
return this.fail("Unterminated string", start);
|
|
131
131
|
}
|
|
132
132
|
trivia() {
|
|
133
133
|
while (this.offset < this.source.length) {
|
|
@@ -286,16 +286,20 @@ export function compileSfConfigV1(source, options = {}) {
|
|
|
286
286
|
config,
|
|
287
287
|
effective: {
|
|
288
288
|
version: 1,
|
|
289
|
-
...(config.build
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
...(config.build
|
|
290
|
+
? {
|
|
291
|
+
build: {
|
|
292
|
+
...config.build,
|
|
293
|
+
installRoot: config.build.installRoot ?? ".",
|
|
294
|
+
timeoutSeconds: config.build.timeoutSeconds ?? 900,
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
: {}),
|
|
296
298
|
serve: {
|
|
297
299
|
index,
|
|
298
|
-
...(config.serve?.spaFallback !== undefined
|
|
300
|
+
...(config.serve?.spaFallback !== undefined
|
|
301
|
+
? { spaFallback: config.serve.spaFallback }
|
|
302
|
+
: {}),
|
|
299
303
|
cleanUrls: config.serve?.cleanUrls ?? true,
|
|
300
304
|
directoryListing: config.serve?.directoryListing ?? index === false,
|
|
301
305
|
},
|
|
@@ -311,14 +315,16 @@ function invalidResult(message, line, column) {
|
|
|
311
315
|
success: false,
|
|
312
316
|
config: null,
|
|
313
317
|
effective: null,
|
|
314
|
-
issues: [
|
|
318
|
+
issues: [
|
|
319
|
+
{
|
|
315
320
|
code: "config_invalid",
|
|
316
321
|
severity: "error",
|
|
317
322
|
path: "$",
|
|
318
323
|
line,
|
|
319
324
|
column,
|
|
320
325
|
message,
|
|
321
|
-
}
|
|
326
|
+
},
|
|
327
|
+
],
|
|
322
328
|
locations: new Map([["$", { line, column }]]),
|
|
323
329
|
};
|
|
324
330
|
}
|
|
@@ -374,9 +380,8 @@ function validateShape(value, issue) {
|
|
|
374
380
|
}
|
|
375
381
|
if (isObject(value.build)) {
|
|
376
382
|
const timeout = value.build.timeoutSeconds;
|
|
377
|
-
if (typeof timeout === "number" &&
|
|
378
|
-
|| timeout < 60
|
|
379
|
-
|| timeout > 7200)) {
|
|
383
|
+
if (typeof timeout === "number" &&
|
|
384
|
+
(!Number.isInteger(timeout) || timeout < 60 || timeout > 7200)) {
|
|
380
385
|
issue("config_invalid", "$.build.timeoutSeconds", "build.timeoutSeconds must be an integer from 60 to 7200.");
|
|
381
386
|
}
|
|
382
387
|
const installRoot = value.build.installRoot;
|
|
@@ -470,9 +475,9 @@ function editDistance(left, right) {
|
|
|
470
475
|
return previous[right.length] ?? right.length;
|
|
471
476
|
}
|
|
472
477
|
function isCommittedPath(candidate) {
|
|
473
|
-
return candidate.length > 0
|
|
474
|
-
|
|
475
|
-
|
|
478
|
+
return (candidate.length > 0 &&
|
|
479
|
+
!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(candidate) &&
|
|
480
|
+
!isAbsoluteOrEscaping(candidate));
|
|
476
481
|
}
|
|
477
482
|
function isCommittedImagePath(candidate) {
|
|
478
483
|
return /^\/(?!\/)/.test(candidate) && !candidate.split(/[\\/]/).includes("..");
|
|
@@ -505,14 +510,18 @@ function validateArtifactReferences(value, artifactPaths, issue) {
|
|
|
505
510
|
if (isObject(value.serve)) {
|
|
506
511
|
for (const key of ["index", "spaFallback"]) {
|
|
507
512
|
const candidate = value.serve[key];
|
|
508
|
-
if (typeof candidate === "string" &&
|
|
513
|
+
if (typeof candidate === "string" &&
|
|
514
|
+
isCommittedPath(candidate) &&
|
|
515
|
+
!artifacts.has(candidate)) {
|
|
509
516
|
issue("config_invalid", `$.serve.${key}`, `serve.${key} must reference a committed artifact.`);
|
|
510
517
|
}
|
|
511
518
|
}
|
|
512
519
|
}
|
|
513
520
|
if (isObject(value.substitute) && Array.isArray(value.substitute.files)) {
|
|
514
521
|
value.substitute.files.forEach((candidate, index) => {
|
|
515
|
-
if (typeof candidate === "string" &&
|
|
522
|
+
if (typeof candidate === "string" &&
|
|
523
|
+
isCommittedPath(candidate) &&
|
|
524
|
+
!artifacts.has(candidate)) {
|
|
516
525
|
issue("config_invalid", `$.substitute.files[${index}]`, "substitute.files entries must reference committed artifacts.");
|
|
517
526
|
}
|
|
518
527
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"phpLike": [
|
|
3
|
+
{ "path": "index.php", "expected": true },
|
|
4
|
+
{ "path": "nested/app.phtml", "expected": true },
|
|
5
|
+
{ "path": "lib.phar", "expected": true },
|
|
6
|
+
{ "path": "legacy.php5", "expected": true },
|
|
7
|
+
{ "path": "index.html", "expected": false },
|
|
8
|
+
{ "path": "assets/main.js", "expected": false },
|
|
9
|
+
{ "path": ".env", "expected": false }
|
|
10
|
+
],
|
|
11
|
+
"immutable": [
|
|
12
|
+
{ "path": "_next/static/chunks/main-app-4A_iFExJ.js", "expected": true },
|
|
13
|
+
{ "path": "_next/static/chunks/framework-2c1d4f8a.js", "expected": true },
|
|
14
|
+
{ "path": "_next/static/chunks/pages/index-a1b2c3d4e5f6.js", "expected": true },
|
|
15
|
+
{ "path": "_next/static/chunks/oton0Guf.js", "expected": true },
|
|
16
|
+
{ "path": "_next/static/css/app.plainname.css", "expected": true },
|
|
17
|
+
{ "path": "/_next/static/chunks/oton0Guf.js", "expected": true },
|
|
18
|
+
{ "path": "_app/immutable/entry/start.DYyuCeEr.js", "expected": true },
|
|
19
|
+
{ "path": "_nuxt/index.oton0Guf.js", "expected": true },
|
|
20
|
+
{ "path": "_astro/about.DYyuCeEr.css", "expected": true },
|
|
21
|
+
{ "path": "_next/static/index.html", "expected": false },
|
|
22
|
+
{ "path": "_next/static/sitemap.xml", "expected": false },
|
|
23
|
+
{ "path": "_app/immutable/manifest.txt", "expected": false },
|
|
24
|
+
{ "path": "_nextish/chunks/oton0Guf.js", "expected": false },
|
|
25
|
+
{ "path": "nested/_next/static/chunks/oton0Guf.js", "expected": false },
|
|
26
|
+
{ "path": "assets/index-BzeDGMM3.js", "expected": false },
|
|
27
|
+
{ "path": "assets/auth-client-P1Tx-Cyu.js", "expected": false },
|
|
28
|
+
{ "path": "assets/index-DYyuCeEr.js", "expected": false },
|
|
29
|
+
{ "path": "assets/index-9f8e7d6c.js", "expected": false },
|
|
30
|
+
{ "path": "assets/D3GeoMap.js", "expected": false },
|
|
31
|
+
{ "path": "assets/index.js", "expected": false },
|
|
32
|
+
{ "path": "assets/client-widget.js", "expected": false },
|
|
33
|
+
{ "path": "app.1a2b3c4d.css", "expected": false },
|
|
34
|
+
{ "path": "vendor.3f9a2b7e.js", "expected": false },
|
|
35
|
+
{ "path": "static/js/main.d41d8cd98f00b204e9800998.js", "expected": false },
|
|
36
|
+
{ "path": "index.html", "expected": false },
|
|
37
|
+
{ "path": "robots.txt", "expected": false },
|
|
38
|
+
{ "path": "config.xml", "expected": false },
|
|
39
|
+
{ "path": "page.php", "expected": false },
|
|
40
|
+
{ "path": "MyComponent.js", "expected": false },
|
|
41
|
+
{ "path": "image01.png", "expected": false }
|
|
42
|
+
],
|
|
43
|
+
"safeContentType": [
|
|
44
|
+
{
|
|
45
|
+
"path": "index.php",
|
|
46
|
+
"mime": "application/x-httpd-php",
|
|
47
|
+
"expected": "text/plain; charset=utf-8"
|
|
48
|
+
},
|
|
49
|
+
{ "path": "lib.phar", "mime": "application/zip", "expected": "application/octet-stream" },
|
|
50
|
+
{
|
|
51
|
+
"path": "assets/main.js",
|
|
52
|
+
"mime": "text/javascript; charset=utf-8",
|
|
53
|
+
"expected": "text/javascript; charset=utf-8"
|
|
54
|
+
},
|
|
55
|
+
{ "path": "logo.svg", "mime": "image/svg+xml", "expected": "image/svg+xml" }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
@@ -11,7 +11,7 @@ export const PHP_LIKE_EXTENSIONS = ["php", "php3", "php4", "php5", "php7", "php8
|
|
|
11
11
|
// Headers"): user `_headers` rules can never set or remove these. The list includes the
|
|
12
12
|
// internal-redirect/sendfile family (X-Accel-Redirect, X-Sendfile, X-LIGHTTPD-send-file,
|
|
13
13
|
// X-Accel-Buffering) because under nginx+php-fpm those headers turn a response into a
|
|
14
|
-
// server-side file read — a private-path disclosure primitive. The
|
|
14
|
+
// server-side file read — a private-path disclosure primitive. The native Rust compiler
|
|
15
15
|
// rejects them at publish; the PHP runtime refuses them again at apply time so a compiler
|
|
16
16
|
// gap can never emit one.
|
|
17
17
|
export const PLATFORM_MANAGED_RESPONSE_HEADERS = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spacefast/common",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Shared TypeScript contracts and utilities for Spacefast packages.",
|
|
6
6
|
"repository": {
|
|
@@ -91,8 +91,9 @@
|
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
93
|
"dependencies": {
|
|
94
|
-
"@tanstack/react-query": "^5.101.
|
|
95
|
-
"
|
|
96
|
-
"
|
|
94
|
+
"@tanstack/react-query": "^5.101.4",
|
|
95
|
+
"culori": "^4.0.2",
|
|
96
|
+
"smol-toml": "^1.7.0",
|
|
97
|
+
"zod": "^4.4.3"
|
|
97
98
|
}
|
|
98
99
|
}
|