@spacefast/common 0.0.24 → 0.0.26
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/brand-assets/LICENSE-Haskoy.txt +93 -0
- package/brand-assets/haskoy-latin-variable.woff2 +0 -0
- package/brand-assets/spacefast-favicon.svg +4 -4
- package/brand-assets/spacefast-sf-full-bleed.svg +3 -3
- package/brand-assets/spacefast-wordmark.svg +2 -4
- package/dist/agents/connect-targets.js +1 -1
- package/dist/agents/private-key-oauth.d.ts +0 -1
- package/dist/agents/private-key-oauth.js +46 -63
- package/dist/brand-assets-build.js +4 -6
- package/dist/brand-assets.d.ts +5 -15
- package/dist/brand-assets.js +15 -14
- package/dist/config/domains.d.ts +47 -7
- package/dist/config/domains.js +65 -10
- package/dist/contracts/access.d.ts +3 -3
- package/dist/contracts/activity.d.ts +1 -1
- package/dist/contracts/activity.js +5 -3
- package/dist/contracts/api-keys.js +2 -2
- package/dist/contracts/billing.d.ts +2 -1
- package/dist/contracts/billing.js +30 -0
- package/dist/contracts/builds.d.ts +34 -22
- package/dist/contracts/builds.js +18 -6
- package/dist/contracts/channels.js +1 -1
- package/dist/contracts/comments.d.ts +1 -1
- package/dist/contracts/common.d.ts +6 -6
- package/dist/contracts/common.js +5 -5
- package/dist/contracts/crons.d.ts +35 -0
- package/dist/contracts/crons.js +42 -0
- package/dist/contracts/device-auth.js +1 -1
- package/dist/contracts/docs.d.ts +5 -5
- package/dist/contracts/docs.js +2 -2
- package/dist/contracts/domains.d.ts +74 -63
- package/dist/contracts/domains.js +29 -2
- package/dist/contracts/enums.d.ts +3 -2
- package/dist/contracts/enums.js +10 -10
- package/dist/contracts/error-code-meta.d.ts +14 -0
- package/dist/contracts/error-code-meta.js +17 -0
- package/dist/contracts/error-codes.d.ts +1 -1
- package/dist/contracts/error-codes.js +4 -1
- package/dist/contracts/execution.d.ts +15 -12
- package/dist/contracts/execution.js +15 -12
- package/dist/contracts/feature-lifecycle.d.ts +4 -14
- package/dist/contracts/feature-lifecycle.js +0 -11
- package/dist/contracts/features.d.ts +33 -47
- package/dist/contracts/features.js +27 -20
- package/dist/contracts/functions.d.ts +169 -2
- package/dist/contracts/functions.js +160 -4
- package/dist/contracts/git.d.ts +27 -33
- package/dist/contracts/git.js +10 -11
- package/dist/contracts/grants.d.ts +2 -2
- package/dist/contracts/ids.js +2 -2
- package/dist/contracts/internal.d.ts +5 -16
- package/dist/contracts/internal.js +2 -12
- package/dist/contracts/mcp.d.ts +11 -0
- package/dist/contracts/mcp.js +5 -0
- package/dist/contracts/me.js +1 -1
- package/dist/contracts/operations.d.ts +4 -4
- package/dist/contracts/operations.js +2 -2
- package/dist/contracts/pages.d.ts +6 -6
- package/dist/contracts/plan-policy.d.ts +1 -1
- package/dist/contracts/plan-policy.js +1 -1
- package/dist/contracts/platform.d.ts +4 -4
- package/dist/contracts/publish-archive.d.ts +170 -160
- package/dist/contracts/publish-archive.js +8 -1
- package/dist/contracts/push-new.d.ts +4 -4
- package/dist/contracts/quotas.js +2 -2
- package/dist/contracts/repository-connections.d.ts +43 -30
- package/dist/contracts/repository-connections.js +11 -3
- package/dist/contracts/resources.d.ts +8 -8
- package/dist/contracts/resources.js +6 -6
- package/dist/contracts/runtime-api.d.ts +103 -56
- package/dist/contracts/runtime-api.js +67 -26
- package/dist/contracts/runtime-app.d.ts +2 -2
- package/dist/contracts/runtime-db.d.ts +6 -6
- package/dist/contracts/runtime-db.js +2 -2
- package/dist/contracts/runtime-purge.d.ts +8 -0
- package/dist/contracts/runtime-purge.js +7 -0
- package/dist/contracts/runtime-storage.d.ts +3 -2
- package/dist/contracts/runtime-storage.js +2 -1
- package/dist/contracts/sf-config-v1.d.ts +48 -10
- package/dist/contracts/space-config.d.ts +60 -3
- package/dist/contracts/space-config.js +213 -18
- package/dist/contracts/spaces.d.ts +55 -74
- package/dist/contracts/spaces.js +11 -14
- package/dist/contracts/superadmin-spaces.d.ts +9 -9
- package/dist/contracts/superadmin-teams.d.ts +2 -2
- package/dist/contracts/superadmin-tenants.d.ts +0 -6
- package/dist/contracts/superadmin-tenants.js +1 -7
- package/dist/contracts/superadmin.d.ts +135 -22
- package/dist/contracts/superadmin.js +43 -12
- package/dist/contracts/tags.d.ts +21 -21
- package/dist/contracts/tags.js +3 -3
- package/dist/contracts/teams.d.ts +14 -17
- package/dist/contracts/teams.js +6 -6
- package/dist/contracts/transfers.js +2 -2
- package/dist/contracts/variables.d.ts +4 -4
- package/dist/contracts/variables.js +3 -3
- package/dist/contracts/webhooks.d.ts +4 -4
- package/dist/contracts/zero.d.ts +15 -17
- package/dist/contracts/zero.js +12 -12
- package/dist/docs/agent-prose.d.ts +1 -1
- package/dist/docs/agent-prose.js +11 -14
- package/dist/docs/agent-setup.js +5 -2
- package/dist/docs/catalog.d.ts +7 -7
- package/dist/docs/catalog.js +7 -7
- package/dist/docs/error-docs.js +145 -133
- package/dist/docs/skills.d.ts +12 -12
- package/dist/docs/skills.js +4 -4
- package/dist/test-helpers/fetch-stub.js +1 -0
- package/dist/utils/build-settings.d.ts +5 -1
- package/dist/utils/build-settings.js +82 -10
- package/dist/utils/canonical-json.js +1 -0
- package/dist/utils/concurrency.js +1 -0
- package/dist/utils/credential-policy.js +4 -3
- package/dist/utils/dns-instructions.js +9 -9
- package/dist/utils/error-code.js +1 -0
- package/dist/utils/grant-decision.fixtures.json +2 -2
- package/dist/utils/grants.js +3 -1
- package/dist/utils/local-space-state.d.ts +8 -11
- package/dist/utils/local-space-state.js +13 -24
- package/dist/utils/one-shot-replay.d.ts +4 -1
- package/dist/utils/page-colors.d.ts +14 -12
- package/dist/utils/page-colors.js +15 -13
- package/dist/utils/page-fonts.d.ts +1 -1
- package/dist/utils/page-fonts.js +11 -18
- package/dist/utils/pages.d.ts +20 -1
- package/dist/utils/pages.js +22 -9
- package/dist/utils/publish-policy.d.ts +7 -0
- package/dist/utils/publish-policy.fixtures.json +11 -5
- package/dist/utils/publish-policy.js +9 -1
- package/dist/utils/query-keys.d.ts +4 -4
- package/dist/utils/query-keys.js +2 -2
- package/dist/utils/runtime-upload.d.ts +5 -0
- package/dist/utils/runtime-upload.js +7 -0
- package/dist/utils/secure-local-file.js +5 -16
- package/dist/utils/space-config.d.ts +1 -1
- package/dist/utils/space-config.js +3 -0
- package/dist/utils/static-runtime-policy.d.ts +9 -3
- package/dist/utils/static-runtime-policy.fixtures.json +38 -41
- package/dist/utils/static-runtime-policy.js +22 -56
- package/dist/utils/upload-session.d.ts +4 -1
- package/dist/utils/upload-session.js +33 -5
- package/dist/utils/version-path.js +1 -1
- package/dist/vocabulary.d.ts +8 -8
- package/dist/vocabulary.js +31 -20
- package/package.json +2 -17
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type { SpacePageTheme } from "../contracts/space-config.js";
|
|
2
|
-
/** Default page palette (mirrored into the shared template CSS)
|
|
2
|
+
/** Default page palette (mirrored into the shared template CSS) — the
|
|
3
|
+
* printed-matter scheme: warm canvas, ink type, marker accent (always a
|
|
4
|
+
* ground with ink on top; the link derivation handles its text-grade). */
|
|
3
5
|
export declare const PAGE_COLOR_DEFAULTS: {
|
|
4
|
-
readonly accent: "#
|
|
5
|
-
readonly bg: "#
|
|
6
|
-
readonly fg: "#
|
|
7
|
-
readonly muted: "#
|
|
8
|
-
readonly card: "#
|
|
9
|
-
readonly border: "#
|
|
10
|
-
readonly darkBg: "#
|
|
11
|
-
readonly darkFg: "#
|
|
12
|
-
readonly darkMuted: "#
|
|
13
|
-
readonly darkCard: "#
|
|
14
|
-
readonly darkBorder: "#
|
|
6
|
+
readonly accent: "#d8f24b";
|
|
7
|
+
readonly bg: "#f7f5ef";
|
|
8
|
+
readonly fg: "#0c0c0c";
|
|
9
|
+
readonly muted: "#6e6a5e";
|
|
10
|
+
readonly card: "#f7f5ef";
|
|
11
|
+
readonly border: "#e5e1d4";
|
|
12
|
+
readonly darkBg: "#0c0c0c";
|
|
13
|
+
readonly darkFg: "#f0eee6";
|
|
14
|
+
readonly darkMuted: "#a6a193";
|
|
15
|
+
readonly darkCard: "#0c0c0c";
|
|
16
|
+
readonly darkBorder: "#2b2823";
|
|
15
17
|
};
|
|
16
18
|
export type PageSchemeColors = {
|
|
17
19
|
bg: string;
|
|
@@ -18,19 +18,21 @@
|
|
|
18
18
|
// Everything is pure and deterministic: publish finalize, dashboard previews,
|
|
19
19
|
// and tests all call the same functions and get the same pixels.
|
|
20
20
|
import { formatHex, oklch, parse, wcagContrast } from "culori";
|
|
21
|
-
/** Default page palette (mirrored into the shared template CSS)
|
|
21
|
+
/** Default page palette (mirrored into the shared template CSS) — the
|
|
22
|
+
* printed-matter scheme: warm canvas, ink type, marker accent (always a
|
|
23
|
+
* ground with ink on top; the link derivation handles its text-grade). */
|
|
22
24
|
export const PAGE_COLOR_DEFAULTS = {
|
|
23
|
-
accent: "#
|
|
24
|
-
bg: "#
|
|
25
|
-
fg: "#
|
|
26
|
-
muted: "#
|
|
27
|
-
card: "#
|
|
28
|
-
border: "#
|
|
29
|
-
darkBg: "#
|
|
30
|
-
darkFg: "#
|
|
31
|
-
darkMuted: "#
|
|
32
|
-
darkCard: "#
|
|
33
|
-
darkBorder: "#
|
|
25
|
+
accent: "#d8f24b",
|
|
26
|
+
bg: "#f7f5ef",
|
|
27
|
+
fg: "#0c0c0c",
|
|
28
|
+
muted: "#6e6a5e",
|
|
29
|
+
card: "#f7f5ef",
|
|
30
|
+
border: "#e5e1d4",
|
|
31
|
+
darkBg: "#0c0c0c",
|
|
32
|
+
darkFg: "#f0eee6",
|
|
33
|
+
darkMuted: "#a6a193",
|
|
34
|
+
darkCard: "#0c0c0c",
|
|
35
|
+
darkBorder: "#2b2823",
|
|
34
36
|
};
|
|
35
37
|
/** WCAG AA for body-size text. */
|
|
36
38
|
const TEXT_CONTRAST = 4.5;
|
|
@@ -139,7 +141,7 @@ function schemeFromBackground(background) {
|
|
|
139
141
|
/** The one entry point: raw theme in, everything the templates paint with out. */
|
|
140
142
|
export function derivePageColors(theme) {
|
|
141
143
|
const accentInput = theme.accent?.trim();
|
|
142
|
-
const accent = safeParse(accentInput) ? accentInput : PAGE_COLOR_DEFAULTS.accent;
|
|
144
|
+
const accent = accentInput && safeParse(accentInput) ? accentInput : PAGE_COLOR_DEFAULTS.accent;
|
|
143
145
|
const onAccent = onAccentColor(accent);
|
|
144
146
|
const background = theme.background?.trim();
|
|
145
147
|
if (background && safeParse(background)) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const PAGE_FONT_FACES: string;
|
|
2
|
-
export declare const PAGE_FONT_PRELOAD_HTML
|
|
2
|
+
export declare const PAGE_FONT_PRELOAD_HTML = "<link rel=\"preload\" href=\"https://wordpress.com/i/fonts/recoleta/400.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>";
|
package/dist/utils/page-fonts.js
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
// The platform page fonts
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// font CDN
|
|
5
|
-
//
|
|
1
|
+
// The platform page fonts. Recoleta is the only downloaded face (DESIGN.md
|
|
2
|
+
// §4) — body and mono are system stacks, so a published page pays for exactly
|
|
3
|
+
// one typeface. It is commercial, never committed to this repo, and loads from
|
|
4
|
+
// wordpress.com's font CDN so every space hostname reuses one warm
|
|
5
|
+
// browser-cache entry instead of a cold per-space fetch.
|
|
6
6
|
// `font-display:optional` renders the fallback and never swaps mid-view when a
|
|
7
7
|
// font arrives late — cached loads (everything after the first) get the brand
|
|
8
|
-
//
|
|
8
|
+
// font with no flicker either way.
|
|
9
9
|
const RECOLETA_BASE = "https://wordpress.com/i/fonts/recoleta/";
|
|
10
|
-
const HASKOY_URL = "https://spacefast.com/assets/fonts/haskoy-latin-variable.woff2";
|
|
11
10
|
const RECOLETA_WEIGHTS = [300, 400, 500, 600, 700];
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export const
|
|
17
|
-
// Only the faces the default pages actually render: Recoleta 400 for headings,
|
|
18
|
-
// Haskoy for everything else. Fonts are a cross-origin fetch; `crossorigin` is
|
|
19
|
-
// required — font requests are CORS mode, and a mismatched preload is discarded.
|
|
20
|
-
export const PAGE_FONT_PRELOAD_HTML = [`${RECOLETA_BASE}400.woff2`, HASKOY_URL]
|
|
21
|
-
.map((url) => `<link rel="preload" href="${url}" as="font" type="font/woff2" crossorigin>`)
|
|
22
|
-
.join("");
|
|
11
|
+
export const PAGE_FONT_FACES = RECOLETA_WEIGHTS.map((weight) => `@font-face{font-family:"Recoleta";src:url("${RECOLETA_BASE}${weight}.woff2") format("woff2");font-style:normal;font-weight:${weight};font-display:optional}`).join("");
|
|
12
|
+
// Only the face the default pages actually download: Recoleta 400 for headings.
|
|
13
|
+
// Fonts are a cross-origin fetch; `crossorigin` is required — font requests are
|
|
14
|
+
// CORS mode, and a mismatched preload is discarded.
|
|
15
|
+
export const PAGE_FONT_PRELOAD_HTML = `<link rel="preload" href="${RECOLETA_BASE}400.woff2" as="font" type="font/woff2" crossorigin>`;
|
package/dist/utils/pages.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CustomPageId, type SitePageId } from "../contracts/pages.js";
|
|
2
|
+
import { type DerivedPageColors } from "./page-colors.js";
|
|
2
3
|
export { PAGE_PREVIEW_QUERY, previewPageHref } from "./page-preview.js";
|
|
3
4
|
export type PageTheme = {
|
|
4
5
|
accent?: string | undefined;
|
|
@@ -40,6 +41,18 @@ export type PageDiagnostic = {
|
|
|
40
41
|
};
|
|
41
42
|
export declare function isPagePublicPath(path: string): boolean;
|
|
42
43
|
export declare function escapePageHtml(value: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* The custom properties a theme paints with. Pushed colors are intent; the
|
|
46
|
+
* readability engine (page-colors.ts) derives what the templates actually use.
|
|
47
|
+
* A custom background pins a complete scheme for both OS modes — emitting
|
|
48
|
+
* `--sf-bg` alone used to leave dark-mode visitors with the dark foreground on
|
|
49
|
+
* the user's light background.
|
|
50
|
+
*
|
|
51
|
+
* Exported because the dashboard's live preview paints the same tokens from an
|
|
52
|
+
* unsaved draft: it passes the colors it has already derived, and gets exactly
|
|
53
|
+
* what the published page would carry.
|
|
54
|
+
*/
|
|
55
|
+
export declare function pageThemeCss(theme: PageTheme, derived?: DerivedPageColors): string;
|
|
43
56
|
export declare function expandPageHtml(source: string, context: PageExpansionContext): string;
|
|
44
57
|
export declare function validatePageSources(input: {
|
|
45
58
|
pages: Readonly<Record<string, string>>;
|
|
@@ -49,7 +62,13 @@ export declare function isCompleteHtmlDocument(source: string): boolean;
|
|
|
49
62
|
export declare function applyPageLayout(layout: string | null, content: string, context: Omit<PageExpansionContext, "page" | "content">): string;
|
|
50
63
|
export declare function inferPageThemeFromThemeJson(raw: string): Partial<PageTheme>;
|
|
51
64
|
export declare function defaultPageBody(id: SitePageId): string;
|
|
52
|
-
export declare const DEFAULT_PAGE_TEMPLATES:
|
|
65
|
+
export declare const DEFAULT_PAGE_TEMPLATES: {
|
|
66
|
+
"404": string;
|
|
67
|
+
denied: string;
|
|
68
|
+
access: string;
|
|
69
|
+
index: string;
|
|
70
|
+
preview: string;
|
|
71
|
+
};
|
|
53
72
|
export declare const DEFAULT_LAYOUT_TEMPLATE: string;
|
|
54
73
|
export declare function defaultPageTemplate(id: SitePageId): string;
|
|
55
74
|
export declare function pageIdFromFilename(filename: string): CustomPageId | null;
|
package/dist/utils/pages.js
CHANGED
|
@@ -33,15 +33,21 @@ export function escapePageHtml(value) {
|
|
|
33
33
|
.replaceAll('"', """)
|
|
34
34
|
.replaceAll("'", "'");
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
/**
|
|
37
|
+
* The custom properties a theme paints with. Pushed colors are intent; the
|
|
38
|
+
* readability engine (page-colors.ts) derives what the templates actually use.
|
|
39
|
+
* A custom background pins a complete scheme for both OS modes — emitting
|
|
40
|
+
* `--sf-bg` alone used to leave dark-mode visitors with the dark foreground on
|
|
41
|
+
* the user's light background.
|
|
42
|
+
*
|
|
43
|
+
* Exported because the dashboard's live preview paints the same tokens from an
|
|
44
|
+
* unsaved draft: it passes the colors it has already derived, and gets exactly
|
|
45
|
+
* what the published page would carry.
|
|
46
|
+
*/
|
|
47
|
+
export function pageThemeCss(theme, derived = derivePageColors(theme)) {
|
|
41
48
|
const root = [];
|
|
42
49
|
const dark = [];
|
|
43
50
|
if (theme.accent || theme.background) {
|
|
44
|
-
const derived = derivePageColors(theme);
|
|
45
51
|
// `derived.accent` is the parse-validated input (or the default), never
|
|
46
52
|
// an arbitrary string straight from the theme.
|
|
47
53
|
if (theme.accent)
|
|
@@ -59,7 +65,11 @@ function themeStyle(theme) {
|
|
|
59
65
|
if (root.length === 0)
|
|
60
66
|
return "";
|
|
61
67
|
const darkBlock = dark.length > 0 ? `@media(prefers-color-scheme:dark){:root{${dark.join(";")}}}` : "";
|
|
62
|
-
return
|
|
68
|
+
return `:root{${root.join(";")}}${darkBlock}`;
|
|
69
|
+
}
|
|
70
|
+
function themeStyle(theme) {
|
|
71
|
+
const css = pageThemeCss(theme);
|
|
72
|
+
return css ? `<style>${css}</style>` : "";
|
|
63
73
|
}
|
|
64
74
|
const runtimeSlot = (name, fallback) => `<!--sf-runtime:${name}:start-->${fallback}<!--sf-runtime:${name}:end-->`;
|
|
65
75
|
function renderFiles(files) {
|
|
@@ -84,7 +94,7 @@ function renderFile(file) {
|
|
|
84
94
|
function renderSpacefastBranding(hidden) {
|
|
85
95
|
const wordmark = hidden
|
|
86
96
|
? ""
|
|
87
|
-
: `<span class="sf-powered" tabindex="0"><svg class="sf-wordmark" viewBox="0 0
|
|
97
|
+
: `<span class="sf-powered" tabindex="0"><svg class="sf-wordmark" viewBox="0 0 152 28.9062" role="img" aria-label="Spacefast"><path fill="currentColor" 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>`;
|
|
88
98
|
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>`;
|
|
89
99
|
}
|
|
90
100
|
function renderElement(element, context) {
|
|
@@ -128,6 +138,7 @@ const ELEMENT_PATTERN = /<\s*(sf-[a-z0-9-]+)\b[^>]*>(?:\s*<\s*\/\s*\1\s*>)?|<\s*
|
|
|
128
138
|
export function expandPageHtml(source, context) {
|
|
129
139
|
let output = source.replace(ELEMENT_PATTERN, (_match, paired, unary) => {
|
|
130
140
|
const name = (paired ?? unary)?.toLowerCase();
|
|
141
|
+
// SAFETY: The set membership check admits only names from the PageElement literal set.
|
|
131
142
|
return name && knownElements.has(name) ? renderElement(name, context) : _match;
|
|
132
143
|
});
|
|
133
144
|
if (output.includes("/*sf-default-fonts*/")) {
|
|
@@ -377,7 +388,7 @@ export function defaultPageBody(id) {
|
|
|
377
388
|
// The built-in palette runs through the same readability engine as pushed
|
|
378
389
|
// themes, so buttons and links remain readable for every accepted accent.
|
|
379
390
|
const defaultPageColors = derivePageColors({});
|
|
380
|
-
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
|
|
391
|
+
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:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",system-ui,Roboto,"Helvetica Neue",Arial,sans-serif;--sf-display-font:"Recoleta",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:400 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{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{filter:brightness(1.06)}a:focus-visible,button:focus-visible{outline:2px solid var(--sf-link);outline-offset:2px}.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:14px;color:var(--sf-fg)}@media(max-width:520px){.sf-page{padding:20px}.sf-button{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{border:1px solid ButtonText}.sf-powered:focus{outline:1px solid CanvasText;outline-offset:3px}}`;
|
|
381
392
|
const accessCss = `.sf-page.access .sf-main{max-width:26rem;padding:clamp(42px,8vh,72px) 0}.sf-page.access h1{max-width:18ch;margin-bottom:14px;font:600 clamp(2rem,7vw,2.65rem)/1.12 var(--sf-font);letter-spacing:-.035em}.sf-page.access .sf-copy{font-size:15px;line-height:1.6}.sf-access-default{display:flex;width:100%;flex-direction:column;align-items:center}.sf-access-status{padding:10px 12px;border:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);border-radius:8px;background:color-mix(in srgb,var(--sf-fg) 4%,transparent);color:var(--sf-fg)}.sf-access-lanes{display:flex;flex-direction:column;gap:16px;width:min(23rem,100%);margin-top:30px;text-align:left}.sf-access-lanes form{display:flex;flex-direction:column;gap:14px;margin:0}.sf-input{box-sizing:border-box;width:100%;min-height:46px;padding:11px 12px;border:1px solid color-mix(in srgb,var(--sf-fg) 18%,transparent);border-radius:8px;outline:2px solid transparent;outline-offset:-1px;background:color-mix(in srgb,var(--sf-bg) 92%,var(--sf-fg));color:var(--sf-fg);font:500 15px/1.45 var(--sf-font);appearance:none}.sf-input::placeholder{color:color-mix(in srgb,var(--sf-muted) 82%,transparent);font:inherit}.sf-input:hover{border-color:color-mix(in srgb,var(--sf-fg) 34%,transparent)}.sf-input:focus-visible{border-color:var(--sf-accent);outline-color:var(--sf-accent)}textarea.sf-input{min-height:92px;resize:vertical}.sf-label{color:var(--sf-fg);font:600 13px/1.4 var(--sf-font)}.sf-access-lanes .sf-button{width:100%;min-height:46px;border-radius:8px}.sf-access-or{display:flex;align-items:center;gap:12px;color:var(--sf-muted);font-size:13px}.sf-access-or:before,.sf-access-or:after{content:"";flex:1;border-top:1px solid color-mix(in srgb,var(--sf-fg) 12%,transparent)}.sf-error{margin:0;color:var(--sf-error);font-size:14px}.sf-access-password-row{display:flex;gap:8px}.sf-access-password-row .sf-input{min-width:0;flex:1}.sf-access-password-row .sf-button{width:auto;flex:none}.sf-access-request{padding-top:2px}.sf-access-request summary{padding:4px 0;cursor:pointer;color:var(--sf-muted);font:550 14px/1.5 var(--sf-font)}.sf-access-request summary:hover{color:var(--sf-fg)}.sf-access-request[open] summary{color:var(--sf-fg)}.sf-access-request form{margin-top:16px}@media(max-width:520px){.sf-page.access .sf-main{padding:38px 0}.sf-page.access h1{font-size:2rem}.sf-page.access .sf-copy{font-size:16px}.sf-input{min-height:48px;font-size:16px}.sf-access-lanes .sf-button{min-height:48px}.sf-access-password-row{flex-direction:column}.sf-access-password-row .sf-button{width:100%}}@media(prefers-color-scheme:dark){.sf-access-status{background:color-mix(in srgb,var(--sf-fg) 5%,transparent)}}`;
|
|
382
393
|
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>';
|
|
383
394
|
const pageFooter = "<sf-spacefast-branding></sf-spacefast-branding>";
|
|
@@ -400,8 +411,10 @@ export function defaultPageTemplate(id) {
|
|
|
400
411
|
}
|
|
401
412
|
export function pageIdFromFilename(filename) {
|
|
402
413
|
const id = filename.replace(/\.html$/, "");
|
|
414
|
+
// SAFETY: The set membership check admits only ids from the CustomPageId literal set.
|
|
403
415
|
return customPageIds.has(id) ? id : null;
|
|
404
416
|
}
|
|
405
417
|
export function allDefaultPageTemplates() {
|
|
418
|
+
// SAFETY: SITE_PAGE_IDS maps every SitePageId to its corresponding default template entry.
|
|
406
419
|
return Object.fromEntries(SITE_PAGE_IDS.map((id) => [id, DEFAULT_PAGE_TEMPLATES[id]]));
|
|
407
420
|
}
|
|
@@ -25,6 +25,13 @@ export declare function normalizePublishPath(relativePath: string): string;
|
|
|
25
25
|
* could mint its own tokens.
|
|
26
26
|
*/
|
|
27
27
|
export declare const PUBLISHABLE_BUILD_ARTIFACT_PREFIX = "__spacefast/functions/bundles/";
|
|
28
|
+
/**
|
|
29
|
+
* The bundle carve-out's sibling for the Next-cache seed document: the same
|
|
30
|
+
* reasoning (travel through the ordinary manifest and upload path, stay
|
|
31
|
+
* unservable by default), the same deliberate narrowness (a prefix of its
|
|
32
|
+
* own, never the whole `functions/` directory — see the bundle comment).
|
|
33
|
+
*/
|
|
34
|
+
export declare const PUBLISHABLE_SEED_ARTIFACT_PREFIX = "__spacefast/functions/seeds/";
|
|
28
35
|
export declare function publishPathIgnored(relativePath: string): boolean;
|
|
29
36
|
export declare function publishPathIsConvention(relativePath: string): boolean;
|
|
30
37
|
export declare function providerRuntimeFeatureWarnings(paths: readonly string[]): PublishManifestWarning[];
|
|
@@ -146,6 +146,16 @@
|
|
|
146
146
|
"path": "__spacefast/functions/bundlesX/evil.json",
|
|
147
147
|
"expected": "static_runtime_control_path_not_supported"
|
|
148
148
|
},
|
|
149
|
+
{
|
|
150
|
+
"path": "__spacefast/functions/seeds/abc123/seed.json",
|
|
151
|
+
"expected": null,
|
|
152
|
+
"why": "the Next-cache seed document, the bundle carve-out's sibling"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"path": "__spacefast/functions/seeds.json",
|
|
156
|
+
"expected": "static_runtime_control_path_not_supported",
|
|
157
|
+
"why": "the carve-out is the seeds/ prefix, not anything starting with seeds"
|
|
158
|
+
},
|
|
149
159
|
{
|
|
150
160
|
"path": "__spacefast/zero/deploy.json",
|
|
151
161
|
"expected": null,
|
|
@@ -201,10 +211,6 @@
|
|
|
201
211
|
"path": "engine-manifest.json",
|
|
202
212
|
"expected": "static_runtime_control_path_not_supported"
|
|
203
213
|
},
|
|
204
|
-
{
|
|
205
|
-
"path": "entrypoint-shim.php",
|
|
206
|
-
"expected": "static_runtime_control_path_not_supported"
|
|
207
|
-
},
|
|
208
214
|
{
|
|
209
215
|
"path": "nested/custom-redirects.php",
|
|
210
216
|
"expected": null,
|
|
@@ -213,7 +219,7 @@
|
|
|
213
219
|
{
|
|
214
220
|
"path": "init.php",
|
|
215
221
|
"expected": null,
|
|
216
|
-
"why": "engine/init.php installs
|
|
222
|
+
"why": "engine/init.php installs inside an immutable release, not the docroot"
|
|
217
223
|
},
|
|
218
224
|
{
|
|
219
225
|
"path": ".spacefast/storage/routes/current.php",
|
|
@@ -2,7 +2,7 @@ import { BRAND } from "../brand.js";
|
|
|
2
2
|
import { WORST_CASE_QUOTAS } from "../contracts/quotas.js";
|
|
3
3
|
import { SPACE_CONFIG_PRIVATE_FILES } from "../contracts/space-config.js";
|
|
4
4
|
import { ZERO_RUNTIME_DEPLOY_PATH } from "../contracts/zero.js";
|
|
5
|
-
// The ONE static publish policy (
|
|
5
|
+
// The ONE static publish policy (the platform spec "Upload Contract"):
|
|
6
6
|
// path denials, manifest scale caps, plan caps, duplicate detection, and the
|
|
7
7
|
// free-plan content rules live here. Consumed by API manifest validation, CLI
|
|
8
8
|
// local preflight, inline archive extraction, and the runtime parity fixtures
|
|
@@ -112,6 +112,13 @@ export function normalizePublishPath(relativePath) {
|
|
|
112
112
|
* could mint its own tokens.
|
|
113
113
|
*/
|
|
114
114
|
export const PUBLISHABLE_BUILD_ARTIFACT_PREFIX = "__spacefast/functions/bundles/";
|
|
115
|
+
/**
|
|
116
|
+
* The bundle carve-out's sibling for the Next-cache seed document: the same
|
|
117
|
+
* reasoning (travel through the ordinary manifest and upload path, stay
|
|
118
|
+
* unservable by default), the same deliberate narrowness (a prefix of its
|
|
119
|
+
* own, never the whole `functions/` directory — see the bundle comment).
|
|
120
|
+
*/
|
|
121
|
+
export const PUBLISHABLE_SEED_ARTIFACT_PREFIX = "__spacefast/functions/seeds/";
|
|
115
122
|
/**
|
|
116
123
|
* The one FILE inside the control prefix a publish may write.
|
|
117
124
|
*
|
|
@@ -127,6 +134,7 @@ export const PUBLISHABLE_BUILD_ARTIFACT_PREFIX = "__spacefast/functions/bundles/
|
|
|
127
134
|
*/
|
|
128
135
|
function isPublishableBuildArtifactPath(normalizedPath, lowerPath) {
|
|
129
136
|
return (lowerPath.startsWith(PUBLISHABLE_BUILD_ARTIFACT_PREFIX) ||
|
|
137
|
+
lowerPath.startsWith(PUBLISHABLE_SEED_ARTIFACT_PREFIX) ||
|
|
130
138
|
normalizedPath === ZERO_RUNTIME_DEPLOY_PATH);
|
|
131
139
|
}
|
|
132
140
|
export function publishPathIgnored(relativePath) {
|
|
@@ -6,10 +6,10 @@ export declare const qk: {
|
|
|
6
6
|
accountInvitations: () => readonly ["account", "invitations"];
|
|
7
7
|
apiTokens: (teamId?: string) => readonly ["api-tokens"] | readonly ["api-tokens", string];
|
|
8
8
|
agentAccounts: (teamId: string) => readonly ["agent-accounts", string];
|
|
9
|
-
myShelf: () => readonly ["my-shelf"];
|
|
10
9
|
mcpDashboard: (teamId: string) => readonly ["mcp", "dashboard", string];
|
|
11
10
|
teams: () => readonly ["teams", "list"];
|
|
12
11
|
team: (ref: string) => readonly ["teams", "by-ref", string];
|
|
12
|
+
teamDetail: (id: string) => readonly ["teams", string, "detail"];
|
|
13
13
|
teamMembers: (id: string) => readonly ["teams", string, "members"];
|
|
14
14
|
teamInvites: (id: string) => readonly ["teams", string, "invites"];
|
|
15
15
|
teamBilling: (id: string) => readonly ["teams", string, "billing"];
|
|
@@ -41,7 +41,7 @@ export declare const qk: {
|
|
|
41
41
|
spaceBuilds: (id: string) => readonly ["spaces", string, "builds"];
|
|
42
42
|
spaceRepositoryConnection: (id: string, connectionType?: string) => readonly ["spaces", string, "repository-connection", string];
|
|
43
43
|
repositoryInstallations: (spaceId: string) => readonly ["spaces", string, "repository-installations"];
|
|
44
|
-
repositoryInstallationRepositories: (installationId: string) => readonly ["
|
|
44
|
+
repositoryInstallationRepositories: (spaceId: string, installationId: string) => readonly ["spaces", string, "repository-installations", string, "repositories"];
|
|
45
45
|
spaceRepositoryAnalyze: (id: string, connectionType?: string, ref?: string, root?: string) => readonly ["spaces", string, "repository-connection", string, "analyze", string, string];
|
|
46
46
|
spaceRepositoryWebhookDeliveries: (id: string, connectionType?: string) => readonly ["spaces", string, "repository-webhook-deliveries", string];
|
|
47
47
|
teamWebhooks: (teamId: string) => readonly ["teams", string, "webhooks"];
|
|
@@ -54,7 +54,7 @@ export declare const qk: {
|
|
|
54
54
|
spaceVersionFileUrlPreview: (id: string, versionId: string, path: string) => readonly ["spaces", string, "versions", string, "files", string, "url-preview"];
|
|
55
55
|
spaceVersionFileDiff: (id: string, versionId: string, baseVersionId?: string) => readonly ["spaces", string, "versions", string, "files", "diff", string];
|
|
56
56
|
spaceVersionConventionArtifacts: (id: string, versionId: string) => readonly ["spaces", string, "versions", string, "convention-artifacts"];
|
|
57
|
-
spaceVersionConventionArtifact: (id: string, versionId: string, kind: "redirects" | "headers") => readonly ["spaces", string, "versions", string, "convention-artifacts", "
|
|
57
|
+
spaceVersionConventionArtifact: (id: string, versionId: string, kind: "redirects" | "headers") => readonly ["spaces", string, "versions", string, "convention-artifacts", "headers" | "redirects"];
|
|
58
58
|
spaceVersionRoutingSummary: (id: string, versionId: string) => readonly ["spaces", string, "versions", string, "routing-summary"];
|
|
59
59
|
spaceDiagnostics: (id: string) => readonly ["spaces", string, "diagnostics"];
|
|
60
60
|
spaceDomains: (id: string) => readonly ["spaces", string, "domains"];
|
|
@@ -67,7 +67,7 @@ export declare const qk: {
|
|
|
67
67
|
spaceRuntimeRealtimeEvents: (id: string, limit: number) => readonly ["spaces", string, "runtime", "realtime", "events", number];
|
|
68
68
|
spaceVariables: (id: string) => readonly ["spaces", string, "variables"];
|
|
69
69
|
spaceCertificates: (id: string) => readonly ["spaces", string, "runtime", "certificates"];
|
|
70
|
-
spaceLogs: (id: string, kind: "build" | "access" | "runtime") => readonly ["spaces", string, "logs", "build" | "
|
|
70
|
+
spaceLogs: (id: string, kind: "build" | "access" | "runtime") => readonly ["spaces", string, "logs", "build" | "access" | "runtime"];
|
|
71
71
|
spaceAnalytics: (id: string, window: string) => readonly ["spaces", string, "analytics", string];
|
|
72
72
|
teamAgentHandoffs: (teamId: string) => readonly ["teams", string, "agent-handoffs"];
|
|
73
73
|
teamAccessDefaults: (teamId: string) => readonly ["teams", string, "access", "defaults"];
|
package/dist/utils/query-keys.js
CHANGED
|
@@ -8,13 +8,13 @@ export const qk = {
|
|
|
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
10
|
agentAccounts: (teamId) => ["agent-accounts", teamId],
|
|
11
|
-
myShelf: () => ["my-shelf"],
|
|
12
11
|
mcpDashboard: (teamId) => ["mcp", "dashboard", teamId],
|
|
13
12
|
// The teams LIST gets its own leaf so invalidating it never cascades into
|
|
14
13
|
// ["teams", id, ...] resource queries (members, spaces, domains, ...).
|
|
15
14
|
teams: () => ["teams", "list"],
|
|
16
15
|
// Keyed by slug (the URL ref), so it lives outside the id namespace.
|
|
17
16
|
team: (ref) => ["teams", "by-ref", ref],
|
|
17
|
+
teamDetail: (id) => ["teams", id, "detail"],
|
|
18
18
|
teamMembers: (id) => ["teams", id, "members"],
|
|
19
19
|
teamInvites: (id) => ["teams", id, "invites"],
|
|
20
20
|
teamBilling: (id) => ["teams", id, "billing"],
|
|
@@ -55,7 +55,7 @@ export const qk = {
|
|
|
55
55
|
spaceBuilds: (id) => ["spaces", id, "builds"],
|
|
56
56
|
spaceRepositoryConnection: (id, connectionType = "connected") => ["spaces", id, "repository-connection", connectionType],
|
|
57
57
|
repositoryInstallations: (spaceId) => [...qk.space(spaceId), "repository-installations"],
|
|
58
|
-
repositoryInstallationRepositories: (installationId) => ["repository-
|
|
58
|
+
repositoryInstallationRepositories: (spaceId, installationId) => [...qk.space(spaceId), "repository-installations", installationId, "repositories"],
|
|
59
59
|
spaceRepositoryAnalyze: (id, connectionType = "connected", ref = "", root = "") => ["spaces", id, "repository-connection", connectionType, "analyze", ref, root],
|
|
60
60
|
spaceRepositoryWebhookDeliveries: (id, connectionType = "connected") => ["spaces", id, "repository-webhook-deliveries", connectionType],
|
|
61
61
|
teamWebhooks: (teamId) => ["teams", teamId, "webhooks"],
|
|
@@ -4,6 +4,11 @@ export declare function runtimeUploadFetchUrl(input: {
|
|
|
4
4
|
uploadId: string;
|
|
5
5
|
path: string;
|
|
6
6
|
}): string;
|
|
7
|
+
export declare function runtimeUploadFileUrl(input: {
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
uploadId: string;
|
|
10
|
+
path: string;
|
|
11
|
+
}): string;
|
|
7
12
|
export declare function runtimeUploadBlobUrl(input: {
|
|
8
13
|
baseUrl: string;
|
|
9
14
|
spaceId: string;
|
|
@@ -15,6 +15,13 @@ export function runtimeUploadFetchUrl(input) {
|
|
|
15
15
|
path: runtimeUploadCanonicalPath(input.path),
|
|
16
16
|
}), input.baseUrl).toString();
|
|
17
17
|
}
|
|
18
|
+
export function runtimeUploadFileUrl(input) {
|
|
19
|
+
return new URL(runtimeUploadPath({
|
|
20
|
+
op: "file",
|
|
21
|
+
upload_id: input.uploadId,
|
|
22
|
+
path: runtimeUploadCanonicalPath(input.path),
|
|
23
|
+
}), input.baseUrl).toString();
|
|
24
|
+
}
|
|
18
25
|
export function runtimeUploadBlobUrl(input) {
|
|
19
26
|
return new URL(runtimeUploadPath({
|
|
20
27
|
route: `/spaces/${encodeURIComponent(input.spaceId)}/blobs/${encodeURIComponent(input.sha256)}`,
|
|
@@ -273,7 +273,6 @@ function lockName(leaf) {
|
|
|
273
273
|
const digest = createHash("sha256").update(identity).digest("hex").slice(0, 24);
|
|
274
274
|
return `.spacefast-${digest}.lock`;
|
|
275
275
|
}
|
|
276
|
-
const LEGACY_LEAF_LOCK_STALE_MS = 2 * 60_000;
|
|
277
276
|
function serializeLeafLockOwner(owner) {
|
|
278
277
|
const { phase, ...identity } = owner;
|
|
279
278
|
return `${phase === "active" ? "A" : "R"}${JSON.stringify(identity)}\n`;
|
|
@@ -293,6 +292,7 @@ function parseLeafLockOwner(raw) {
|
|
|
293
292
|
(value.temporaryName === undefined ||
|
|
294
293
|
(typeof value.temporaryName === "string" &&
|
|
295
294
|
/^\.spacefast-[0-9a-f-]{36}\.tmp$/.test(value.temporaryName)))) {
|
|
295
|
+
// SAFETY: The preceding checks establish every required LeafLockOwner field and the optional temporaryName field.
|
|
296
296
|
return { ...value, phase };
|
|
297
297
|
}
|
|
298
298
|
}
|
|
@@ -360,14 +360,8 @@ function processIsAlive(pid) {
|
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
362
|
async function leafLockRecordIsStale(record) {
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
// fresh legacy writer stays exclusive, while crash wreckage can recover.
|
|
366
|
-
if (record?.kind === "legacy") {
|
|
367
|
-
return Date.now() - record.modifiedAtMs > LEGACY_LEAF_LOCK_STALE_MS;
|
|
368
|
-
}
|
|
369
|
-
// A malformed non-empty record is not proof that no writer owns it. New lock
|
|
370
|
-
// records are atomically published, so corruption still needs explicit recovery.
|
|
363
|
+
// A malformed record is not proof that no writer owns it. New lock records are
|
|
364
|
+
// atomically published, so corruption still needs explicit recovery.
|
|
371
365
|
if (record?.kind !== "owned")
|
|
372
366
|
return false;
|
|
373
367
|
const { owner } = record;
|
|
@@ -399,13 +393,8 @@ async function readLeafLockRecord(filePath) {
|
|
|
399
393
|
visibleMetadata.ino !== metadata.ino) {
|
|
400
394
|
return { kind: "invalid" };
|
|
401
395
|
}
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
if (owner)
|
|
405
|
-
return { kind: "owned", owner };
|
|
406
|
-
return raw.length === 0
|
|
407
|
-
? { kind: "legacy", modifiedAtMs: metadata.mtimeMs }
|
|
408
|
-
: { kind: "invalid" };
|
|
396
|
+
const owner = parseLeafLockOwner(await file.readFile("utf8"));
|
|
397
|
+
return owner ? { kind: "owned", owner } : { kind: "invalid" };
|
|
409
398
|
}
|
|
410
399
|
finally {
|
|
411
400
|
await file.close();
|
|
@@ -3,7 +3,7 @@ import { type SpaceConfig, type SpaceConfigFile } from "../contracts/space-confi
|
|
|
3
3
|
export type SpaceConfigBlockingIssueCode = "config_file_too_large" | "config_templates_over_limit" | "config_meta_too_long" | "config_name_too_long" | "config_functions_key_removed" | "config_invalid" | "config_runtime_invalid_kind" | "config_runtime_entry_missing" | "inject_snippet_too_large" | "inject_invalid";
|
|
4
4
|
export type SpaceConfigIssue = {
|
|
5
5
|
severity: "info" | "warning" | "error";
|
|
6
|
-
code: SpaceConfigBlockingIssueCode | "single_html_index" | "files_mode_inferred";
|
|
6
|
+
code: SpaceConfigBlockingIssueCode | "single_html_index" | "files_mode_inferred" | "listing_shadows_fallback";
|
|
7
7
|
message: string;
|
|
8
8
|
/** JSON-path-lite pointer into the config shape (e.g. `meta.title`). */
|
|
9
9
|
path?: string;
|
|
@@ -57,6 +57,7 @@ function capIssues(candidate) {
|
|
|
57
57
|
}
|
|
58
58
|
else {
|
|
59
59
|
for (const key of ["head", "bodyStart", "bodyEnd", "noscript"]) {
|
|
60
|
+
// SAFETY: The enclosing branch proves inject is a non-null, non-array object.
|
|
60
61
|
const snippets = inject[key];
|
|
61
62
|
if (snippets === undefined) {
|
|
62
63
|
continue;
|
|
@@ -174,6 +175,7 @@ export function stripJsonComments(raw) {
|
|
|
174
175
|
let inLineComment = false;
|
|
175
176
|
let inBlockComment = false;
|
|
176
177
|
for (let i = 0; i < raw.length; i += 1) {
|
|
178
|
+
// SAFETY: The loop condition proves i addresses a character in raw.
|
|
177
179
|
const char = raw[i];
|
|
178
180
|
const next = raw[i + 1];
|
|
179
181
|
if (inLineComment) {
|
|
@@ -220,6 +222,7 @@ export function stripJsonComments(raw) {
|
|
|
220
222
|
// Trailing comma: skip when the next non-whitespace/non-comment char closes.
|
|
221
223
|
let j = i + 1;
|
|
222
224
|
while (j < raw.length) {
|
|
225
|
+
// SAFETY: The loop condition proves j addresses a character in raw.
|
|
223
226
|
const ahead = raw[j];
|
|
224
227
|
if (/\s/.test(ahead)) {
|
|
225
228
|
j += 1;
|
|
@@ -5,14 +5,20 @@ export declare const PLATFORM_MANAGED_RESPONSE_HEADERS: string[];
|
|
|
5
5
|
export declare const PLATFORM_MANAGED_RESPONSE_HEADER_PREFIXES: string[];
|
|
6
6
|
/** True when user rules may neither set nor remove this response header. */
|
|
7
7
|
export declare function isPlatformManagedResponseHeader(name: string): boolean;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The token grammar a `_headers` rule accepts for a header NAME. The routing
|
|
10
|
+
* compiler enforces it; anything that GENERATES `_headers` rules (the Next
|
|
11
|
+
* materializer's framework sections) must gate on the same pattern, so a
|
|
12
|
+
* generated rule can never fail the author's publish.
|
|
13
|
+
*/
|
|
14
|
+
export declare const HEADER_RULE_NAME_PATTERN: RegExp;
|
|
15
|
+
/** `content-type` → `Content-Type`: the casing compiled rules and generated `_headers` carry. */
|
|
16
|
+
export declare function canonicalHeaderName(input: string): string;
|
|
10
17
|
/** Lowercased extension without the leading dot, posix semantics. Dotfiles have no extension. */
|
|
11
18
|
export declare function pathExtension(filePath: string): string;
|
|
12
19
|
/** Basename without its extension (the "stem"), posix semantics. */
|
|
13
20
|
export declare function pathStem(filePath: string): string;
|
|
14
21
|
export declare function isPhpLikeStaticPath(filePath: string): boolean;
|
|
15
|
-
export declare function isImmutableAssetPath(filePath: string): boolean;
|
|
16
22
|
/** Forced inert serving policy for a committed static file. PHP-like files are never executable. */
|
|
17
23
|
export declare function runtimeFileSafety(filePath: string): {
|
|
18
24
|
executable: false;
|