@spacefast/common 0.0.13 → 0.0.17
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/client-registry.d.ts +22 -0
- package/dist/agents/client-registry.js +41 -0
- package/dist/agents/connect-targets.d.ts +51 -6
- package/dist/agents/connect-targets.js +93 -28
- package/dist/brand.d.ts +0 -11
- package/dist/brand.js +0 -20
- package/dist/config/domains.d.ts +15 -6
- package/dist/config/domains.js +24 -10
- package/dist/config/postgres-budget.d.ts +3 -0
- package/dist/config/postgres-budget.js +6 -1
- package/dist/config/postgres-budget.json +2 -1
- package/dist/contracts/abuse.js +1 -1
- package/dist/contracts/access.d.ts +149 -44
- package/dist/contracts/access.js +94 -8
- package/dist/contracts/activity.js +5 -1
- package/dist/contracts/api-keys.js +9 -9
- package/dist/contracts/archives.js +11 -11
- package/dist/contracts/beta.js +1 -1
- package/dist/contracts/builds.js +14 -14
- package/dist/contracts/channels.js +2 -2
- package/dist/contracts/comments.d.ts +86 -15
- package/dist/contracts/comments.js +116 -20
- package/dist/contracts/common.d.ts +12 -12
- package/dist/contracts/common.js +33 -30
- package/dist/contracts/continuation.js +5 -5
- package/dist/contracts/data-source-routes.js +1 -1
- package/dist/contracts/device-auth.d.ts +6 -6
- package/dist/contracts/device-auth.js +12 -12
- package/dist/contracts/docs.d.ts +10 -10
- package/dist/contracts/domains.js +39 -33
- package/dist/contracts/enums.d.ts +2 -10
- package/dist/contracts/enums.js +0 -2
- package/dist/contracts/error-code-meta.d.ts +116 -47
- package/dist/contracts/error-code-meta.js +37 -16
- package/dist/contracts/error-codes.d.ts +7 -4
- package/dist/contracts/error-codes.js +46 -21
- package/dist/contracts/events.d.ts +10 -3
- package/dist/contracts/events.js +33 -5
- package/dist/contracts/execution.d.ts +26 -0
- package/dist/contracts/execution.js +21 -0
- package/dist/contracts/feature-lifecycle.d.ts +21 -24
- package/dist/contracts/feature-lifecycle.js +28 -26
- package/dist/contracts/features.js +4 -4
- package/dist/contracts/functions.d.ts +8 -140
- package/dist/contracts/functions.js +9 -19
- package/dist/contracts/git.d.ts +1 -1
- package/dist/contracts/grants.d.ts +96 -79
- package/dist/contracts/grants.js +130 -43
- package/dist/contracts/ids.d.ts +2 -2
- package/dist/contracts/ids.js +2 -4
- package/dist/contracts/internal.js +2 -2
- package/dist/contracts/mcp.d.ts +17 -556
- package/dist/contracts/mcp.js +9 -103
- package/dist/contracts/me.js +3 -4
- package/dist/contracts/notifications.d.ts +108 -0
- package/dist/contracts/notifications.js +54 -0
- package/dist/contracts/oauth-resources.d.ts +18 -1
- package/dist/contracts/oauth-resources.js +22 -2
- package/dist/contracts/operations.d.ts +2 -50
- package/dist/contracts/operations.js +0 -19
- package/dist/contracts/pages.d.ts +8 -4
- package/dist/contracts/pages.js +16 -3
- package/dist/contracts/platform.js +10 -8
- package/dist/contracts/problem-document.d.ts +14 -0
- package/dist/contracts/problem-document.js +35 -0
- package/dist/contracts/push-new.d.ts +21 -6
- package/dist/contracts/push-new.js +3 -7
- package/dist/contracts/resources.d.ts +8 -7
- package/dist/contracts/resources.js +9 -6
- package/dist/contracts/runtime-api.d.ts +162 -92
- package/dist/contracts/runtime-api.js +99 -27
- package/dist/contracts/runtime-app.d.ts +41 -11
- package/dist/contracts/runtime-app.js +22 -1
- package/dist/contracts/runtime-db.d.ts +424 -0
- package/dist/contracts/runtime-db.js +108 -0
- package/dist/contracts/runtime-storage.d.ts +120 -0
- package/dist/contracts/runtime-storage.js +40 -0
- package/dist/contracts/sf-config-v1.d.ts +49 -1
- package/dist/contracts/sf-config-v1.js +2 -2
- package/dist/contracts/sites.d.ts +1 -0
- package/dist/contracts/sites.js +20 -12
- package/dist/contracts/space-config.d.ts +62 -22
- package/dist/contracts/space-config.js +173 -47
- package/dist/contracts/spaces.d.ts +79 -45
- package/dist/contracts/spaces.js +66 -50
- package/dist/contracts/superadmin-spaces.d.ts +13 -2
- package/dist/contracts/superadmin-spaces.js +15 -0
- package/dist/contracts/superadmin-tenants.js +4 -4
- package/dist/contracts/superadmin.d.ts +4 -0
- package/dist/contracts/superadmin.js +14 -1
- package/dist/contracts/tags.d.ts +17 -17
- package/dist/contracts/tags.js +1 -1
- package/dist/contracts/teams.js +25 -16
- package/dist/contracts/theme-json.js +1 -1
- package/dist/contracts/transfers.js +4 -4
- package/dist/contracts/variables.js +3 -3
- package/dist/contracts/zero.d.ts +204 -338
- package/dist/contracts/zero.js +94 -98
- package/dist/dashboard-paths/index.d.ts +16 -1
- package/dist/dashboard-paths/index.js +18 -0
- package/dist/docs/agent-handoff-document.d.ts +2 -2
- package/dist/docs/agent-handoff-document.js +8 -7
- package/dist/docs/agent-prose.d.ts +22 -3
- package/dist/docs/agent-prose.js +221 -51
- package/dist/docs/agent-setup.d.ts +13 -10
- package/dist/docs/agent-setup.js +279 -53
- package/dist/docs/agent-solutions.d.ts +6 -6
- package/dist/docs/agent-solutions.js +23 -21
- package/dist/docs/catalog.d.ts +29 -74
- package/dist/docs/catalog.js +65 -92
- package/dist/docs/error-docs.js +293 -221
- package/dist/docs/skill-distribution.d.ts +2 -0
- package/dist/docs/skill-distribution.js +7 -4
- package/dist/docs/skills.d.ts +546 -0
- package/dist/docs/skills.js +340 -0
- package/dist/utils/browser-credential.d.ts +19 -0
- package/dist/utils/browser-credential.js +62 -0
- package/dist/utils/comment-avatars.d.ts +7 -0
- package/dist/utils/comment-avatars.js +42 -0
- package/dist/utils/content-type.d.ts +3 -3
- package/dist/utils/content-type.js +38 -61
- package/dist/utils/domain-diagnostics.d.ts +5 -5
- package/dist/utils/domain-diagnostics.js +21 -10
- package/dist/utils/local-space-state.d.ts +78 -4
- package/dist/utils/local-space-state.js +383 -82
- package/dist/utils/one-shot-replay.d.ts +14 -0
- package/dist/utils/one-shot-replay.js +21 -0
- package/dist/utils/page-preview.d.ts +7 -0
- package/dist/utils/page-preview.js +9 -0
- package/dist/utils/pages.d.ts +2 -11
- package/dist/utils/pages.js +58 -14
- package/dist/utils/publish-form-data.d.ts +22 -0
- package/dist/utils/publish-form-data.js +26 -0
- package/dist/utils/publish-policy.js +20 -4
- package/dist/utils/query-keys.d.ts +7 -4
- package/dist/utils/query-keys.js +13 -4
- package/dist/utils/runtime-paths.d.ts +3 -0
- package/dist/utils/runtime-paths.js +7 -0
- package/dist/utils/secure-local-file.d.ts +73 -0
- package/dist/utils/secure-local-file.js +1373 -0
- package/dist/utils/space-config.d.ts +13 -4
- package/dist/utils/space-config.js +123 -15
- package/dist/utils/storage-policy.d.ts +20 -0
- package/dist/utils/storage-policy.fixtures.json +174 -0
- package/dist/utils/storage-policy.js +193 -0
- package/dist/utils/upload-session.d.ts +12 -1
- package/dist/utils/upload-session.js +37 -2
- package/dist/utils/version-file-browser.d.ts +13 -0
- package/dist/utils/version-file-browser.js +59 -0
- package/package.json +19 -1
- package/dist/contracts/mounts.d.ts +0 -142
- package/dist/contracts/mounts.js +0 -56
- package/dist/utils/asset-fingerprint.d.ts +0 -64
- package/dist/utils/asset-fingerprint.js +0 -620
|
@@ -1,620 +0,0 @@
|
|
|
1
|
-
// Publish-time asset fingerprinting (caching plan §6): content-address stable
|
|
2
|
-
// assets by adding a `name.<sha256-prefix-8>.ext` alias alongside the original
|
|
3
|
-
// path and rewriting exact static references (HTML attributes, CSS url()/
|
|
4
|
-
// @import) to the alias. Both paths ship in the version — CAS dedupes the
|
|
5
|
-
// bytes — so every reference we do NOT rewrite (JS, dynamic URLs, hotlinks)
|
|
6
|
-
// keeps working on the original path. The fingerprint IS the sha256 prefix we
|
|
7
|
-
// compute ourselves: immutability is self-verifying by construction, never
|
|
8
|
-
// inferred from pre-existing filenames (see static-runtime-policy.ts for the
|
|
9
|
-
// incident that rule comes from).
|
|
10
|
-
//
|
|
11
|
-
// This module is intentionally pure (no node:path, no I/O, hashing injected)
|
|
12
|
-
// so the exact same pass can be exercised by the CLI, the MCP server, and the
|
|
13
|
-
// builds product against one behavior. JavaScript content is never parsed and
|
|
14
|
-
// never rewritten; HTML is never fingerprinted (it is the freshness anchor).
|
|
15
|
-
import { GUARANTEED_IMMUTABLE_PREFIXES } from "./static-runtime-policy.js";
|
|
16
|
-
export const FINGERPRINT_IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
17
|
-
export const FINGERPRINT_HASH_LENGTH = 8;
|
|
18
|
-
/**
|
|
19
|
-
* Stand-in hash used to probe user `_headers` coverage of a would-be alias
|
|
20
|
-
* path before the real hash exists. Compiled `_headers` patterns are wildcard/
|
|
21
|
-
* placeholder based and cannot discriminate on hash content, so any 8-char
|
|
22
|
-
* value is decision-equivalent to the real one.
|
|
23
|
-
*/
|
|
24
|
-
const FINGERPRINT_PROBE_HASH = "0".repeat(FINGERPRINT_HASH_LENGTH);
|
|
25
|
-
// css/js/images/fonts only — never html (freshness anchor), never data files
|
|
26
|
-
// (json/xml/txt), never .well-known/robots material (no extension match).
|
|
27
|
-
const FINGERPRINT_ASSET_EXTENSIONS = new Set([
|
|
28
|
-
"css",
|
|
29
|
-
"js",
|
|
30
|
-
"mjs",
|
|
31
|
-
"png",
|
|
32
|
-
"jpg",
|
|
33
|
-
"jpeg",
|
|
34
|
-
"gif",
|
|
35
|
-
"webp",
|
|
36
|
-
"avif",
|
|
37
|
-
"svg",
|
|
38
|
-
"ico",
|
|
39
|
-
"bmp",
|
|
40
|
-
"woff",
|
|
41
|
-
"woff2",
|
|
42
|
-
"ttf",
|
|
43
|
-
"otf",
|
|
44
|
-
"eot",
|
|
45
|
-
]);
|
|
46
|
-
// Only fingerprint paths that survive URL/_headers round-trips verbatim: no
|
|
47
|
-
// percent-encoding ambiguity, no `_headers` pattern metacharacters (`*`, `:`),
|
|
48
|
-
// no whitespace. Anything else stays on the default lane.
|
|
49
|
-
const SAFE_PATH_PATTERN = /^[A-Za-z0-9/_.-]+$/;
|
|
50
|
-
const SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
51
|
-
const HTML_ANCHOR_HREF_PATTERN = /<a\b[^>]*\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/giu;
|
|
52
|
-
const JS_STATIC_REFERENCE_PATTERN = /(?:\b(?:import|export)\s+(?:[^"'()]*?\s+from\s+)?|\bimport\s*\(|\bnew\s+URL\s*\()\s*["']([^"']+)["']/gu;
|
|
53
|
-
const SOURCE_MAP_REFERENCE_PATTERN = /[#@]\s*sourceMappingURL=([^\s*]+)/gu;
|
|
54
|
-
/**
|
|
55
|
-
* Advisory-only dependency discovery used by finalize diagnostics. It shares
|
|
56
|
-
* the exact HTML/CSS tokenizer and URL resolver with asset fingerprinting, then
|
|
57
|
-
* adds conservative static JS imports and page links. It never rewrites bytes
|
|
58
|
-
* and never changes access automatically.
|
|
59
|
-
*/
|
|
60
|
-
export function scanStaticDocumentDependencies(sourcePath, text) {
|
|
61
|
-
const extension = pathExtensionOf(sourcePath);
|
|
62
|
-
const rawUrls = [];
|
|
63
|
-
if (extension === "html" || extension === "htm") {
|
|
64
|
-
const html = scanHtml(text);
|
|
65
|
-
rawUrls.push(...html.refs.map((ref) => ref.url));
|
|
66
|
-
for (const range of html.cssRanges) {
|
|
67
|
-
rawUrls.push(...scanCssRefs(range.css, range.offset).map((ref) => ref.url));
|
|
68
|
-
}
|
|
69
|
-
for (const match of text.matchAll(HTML_ANCHOR_HREF_PATTERN)) {
|
|
70
|
-
const url = match[1] ?? match[2] ?? match[3];
|
|
71
|
-
if (url)
|
|
72
|
-
rawUrls.push(url);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
else if (extension === "css") {
|
|
76
|
-
rawUrls.push(...scanCssRefs(text, 0).map((ref) => ref.url));
|
|
77
|
-
}
|
|
78
|
-
else if (extension === "js" || extension === "mjs" || extension === "cjs") {
|
|
79
|
-
for (const match of text.matchAll(JS_STATIC_REFERENCE_PATTERN)) {
|
|
80
|
-
if (match[1])
|
|
81
|
-
rawUrls.push(match[1]);
|
|
82
|
-
}
|
|
83
|
-
for (const match of text.matchAll(SOURCE_MAP_REFERENCE_PATTERN)) {
|
|
84
|
-
if (match[1])
|
|
85
|
-
rawUrls.push(match[1]);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const baseDir = parentDir(sourcePath);
|
|
89
|
-
return [
|
|
90
|
-
...new Set(rawUrls
|
|
91
|
-
.map((url) => resolveReference(baseDir, url))
|
|
92
|
-
.filter((targetPath) => targetPath !== null)),
|
|
93
|
-
]
|
|
94
|
-
.toSorted()
|
|
95
|
-
.map((targetPath) => ({ sourcePath, targetPath }));
|
|
96
|
-
}
|
|
97
|
-
export function planAssetFingerprints(input) {
|
|
98
|
-
const covered = input.pathCoveredByUserCacheControl ?? (() => false);
|
|
99
|
-
const excluded = input.excludePath ?? (() => false);
|
|
100
|
-
const fileMap = new Map(input.files.map((file) => [file.path, file]));
|
|
101
|
-
const skipped = [];
|
|
102
|
-
const eligibleTarget = (path) => {
|
|
103
|
-
const file = fileMap.get(path);
|
|
104
|
-
if (!file?.sha256)
|
|
105
|
-
return false;
|
|
106
|
-
if (!FINGERPRINT_ASSET_EXTENSIONS.has(pathExtensionOf(path)))
|
|
107
|
-
return false;
|
|
108
|
-
if (!SAFE_PATH_PATTERN.test(path))
|
|
109
|
-
return false;
|
|
110
|
-
if (GUARANTEED_IMMUTABLE_PREFIXES.some((prefix) => path.startsWith(prefix)))
|
|
111
|
-
return false;
|
|
112
|
-
const probeAlias = insertBeforeExtension(path, FINGERPRINT_PROBE_HASH);
|
|
113
|
-
if (probeAlias === null)
|
|
114
|
-
return false;
|
|
115
|
-
if (excluded(path))
|
|
116
|
-
return false;
|
|
117
|
-
if (covered(`/${path}`) || covered(`/${probeAlias}`)) {
|
|
118
|
-
skippedOnce(skipped, path, "user_cache_control");
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
return true;
|
|
122
|
-
};
|
|
123
|
-
// ---- reference extraction --------------------------------------------
|
|
124
|
-
const htmlDocs = input.files.filter((file) => isExtension(file.path, "html", "htm") && file.text !== undefined);
|
|
125
|
-
const cssDocs = input.files.filter((file) => pathExtensionOf(file.path) === "css" && file.text !== undefined);
|
|
126
|
-
const resolveDocRefs = (docPath, text, rawRefs, baseDir) => {
|
|
127
|
-
const refs = [];
|
|
128
|
-
for (const ref of rawRefs) {
|
|
129
|
-
const target = resolveReference(baseDir ?? parentDir(docPath), ref.url);
|
|
130
|
-
if (target === null || !fileMap.has(target) || target === docPath)
|
|
131
|
-
continue;
|
|
132
|
-
refs.push({ ...ref, target });
|
|
133
|
-
}
|
|
134
|
-
return { path: docPath, text, refs };
|
|
135
|
-
};
|
|
136
|
-
const htmlRefs = [];
|
|
137
|
-
for (const doc of htmlDocs) {
|
|
138
|
-
const text = doc.text;
|
|
139
|
-
const scan = scanHtml(text);
|
|
140
|
-
const base = resolveBaseDir(doc.path, scan.baseHref);
|
|
141
|
-
if (base.skipDocument)
|
|
142
|
-
continue;
|
|
143
|
-
const rawRefs = [...scan.refs];
|
|
144
|
-
for (const range of scan.cssRanges) {
|
|
145
|
-
rawRefs.push(...scanCssRefs(range.css, range.offset));
|
|
146
|
-
}
|
|
147
|
-
htmlRefs.push(resolveDocRefs(doc.path, text, rawRefs, base.dir));
|
|
148
|
-
}
|
|
149
|
-
const cssRefs = new Map();
|
|
150
|
-
for (const doc of cssDocs) {
|
|
151
|
-
const text = doc.text;
|
|
152
|
-
cssRefs.set(doc.path, resolveDocRefs(doc.path, text, scanCssRefs(text, 0), null));
|
|
153
|
-
}
|
|
154
|
-
// ---- referenced, eligible targets ------------------------------------
|
|
155
|
-
const referenced = new Set();
|
|
156
|
-
for (const doc of [...htmlRefs, ...cssRefs.values()]) {
|
|
157
|
-
for (const ref of doc.refs) {
|
|
158
|
-
if (eligibleTarget(ref.target))
|
|
159
|
-
referenced.add(ref.target);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// ---- CSS dependency order (leaves hash first; cycles drop out) --------
|
|
163
|
-
const cycleMembers = new Set();
|
|
164
|
-
const cssOrder = [];
|
|
165
|
-
const state = new Map();
|
|
166
|
-
const stack = [];
|
|
167
|
-
const visit = (path) => {
|
|
168
|
-
const current = state.get(path);
|
|
169
|
-
if (current === "done")
|
|
170
|
-
return;
|
|
171
|
-
if (current === "visiting") {
|
|
172
|
-
const from = stack.indexOf(path);
|
|
173
|
-
for (const member of stack.slice(from === -1 ? 0 : from))
|
|
174
|
-
cycleMembers.add(member);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
state.set(path, "visiting");
|
|
178
|
-
stack.push(path);
|
|
179
|
-
for (const ref of cssRefs.get(path)?.refs ?? []) {
|
|
180
|
-
if (cssRefs.has(ref.target))
|
|
181
|
-
visit(ref.target);
|
|
182
|
-
}
|
|
183
|
-
stack.pop();
|
|
184
|
-
state.set(path, "done");
|
|
185
|
-
cssOrder.push(path);
|
|
186
|
-
};
|
|
187
|
-
for (const path of cssRefs.keys())
|
|
188
|
-
visit(path);
|
|
189
|
-
for (const member of cycleMembers) {
|
|
190
|
-
if (referenced.delete(member))
|
|
191
|
-
skippedOnce(skipped, member, "css_cycle");
|
|
192
|
-
}
|
|
193
|
-
// ---- alias assignment + content rewriting ----------------------------
|
|
194
|
-
const aliasHash = new Map();
|
|
195
|
-
const claimedAliases = new Set();
|
|
196
|
-
const aliases = [];
|
|
197
|
-
const rewrites = [];
|
|
198
|
-
const assignAlias = (path, fullSha) => {
|
|
199
|
-
const hash = fullSha.slice(0, FINGERPRINT_HASH_LENGTH);
|
|
200
|
-
const aliasPath = insertBeforeExtension(path, hash);
|
|
201
|
-
if (aliasPath === null || fileMap.has(aliasPath) || claimedAliases.has(aliasPath)) {
|
|
202
|
-
referenced.delete(path);
|
|
203
|
-
skippedOnce(skipped, path, "alias_collision");
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
claimedAliases.add(aliasPath);
|
|
207
|
-
aliasHash.set(path, hash);
|
|
208
|
-
aliases.push({ path: aliasPath, sourcePath: path });
|
|
209
|
-
return true;
|
|
210
|
-
};
|
|
211
|
-
// Leaf assets (everything that is not a rewritable css doc): the given
|
|
212
|
-
// sha256 already addresses the exact published bytes.
|
|
213
|
-
for (const path of [...referenced].toSorted()) {
|
|
214
|
-
if (cssRefs.has(path))
|
|
215
|
-
continue;
|
|
216
|
-
assignAlias(path, fileMap.get(path).sha256);
|
|
217
|
-
}
|
|
218
|
-
const rewriteDocText = (doc) => spliceRefs(doc.text, doc.refs.flatMap((ref) => {
|
|
219
|
-
const hash = aliasHash.get(ref.target);
|
|
220
|
-
if (hash === undefined)
|
|
221
|
-
return [];
|
|
222
|
-
const replacement = fingerprintedUrl(ref.url, hash);
|
|
223
|
-
return replacement === null ? [] : [{ start: ref.start, end: ref.end, replacement }];
|
|
224
|
-
}));
|
|
225
|
-
// CSS in dependency order: rewrite to fingerprinted leaves, THEN hash the
|
|
226
|
-
// rewritten content — the alias name addresses the bytes that actually ship.
|
|
227
|
-
for (const path of cssOrder) {
|
|
228
|
-
if (cycleMembers.has(path))
|
|
229
|
-
continue;
|
|
230
|
-
const doc = cssRefs.get(path);
|
|
231
|
-
const rewritten = rewriteDocText(doc);
|
|
232
|
-
const changed = rewritten !== doc.text;
|
|
233
|
-
const fullSha = changed
|
|
234
|
-
? input.hashHex(rewritten)
|
|
235
|
-
: fileMap.get(path).sha256;
|
|
236
|
-
if (referenced.has(path))
|
|
237
|
-
assignAlias(path, fullSha);
|
|
238
|
-
if (changed)
|
|
239
|
-
rewrites.push({ path, text: rewritten, sha256: fullSha });
|
|
240
|
-
}
|
|
241
|
-
// HTML last: never fingerprinted, always points at the freshest aliases.
|
|
242
|
-
for (const doc of htmlRefs) {
|
|
243
|
-
const rewritten = rewriteDocText(doc);
|
|
244
|
-
if (rewritten !== doc.text) {
|
|
245
|
-
rewrites.push({ path: doc.path, text: rewritten, sha256: input.hashHex(rewritten) });
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
aliases.sort((left, right) => left.sourcePath.localeCompare(right.sourcePath));
|
|
249
|
-
return { aliases, rewrites, skipped };
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Merges the generated immutable pins into user `_headers` content. The user
|
|
253
|
-
* content is preserved byte-for-byte; generated exact-path rules are appended
|
|
254
|
-
* (per-path Cache-Control conflicts are impossible — files covered by a user
|
|
255
|
-
* Cache-Control rule are never fingerprinted).
|
|
256
|
-
*/
|
|
257
|
-
export function mergeFingerprintHeaders(userHeaders, aliasPaths) {
|
|
258
|
-
const generated = [
|
|
259
|
-
"# Generated by the Spacefast publish fingerprint pass: each path below is a",
|
|
260
|
-
"# content-hashed alias of a file in this version, so its bytes can never change.",
|
|
261
|
-
...[...aliasPaths]
|
|
262
|
-
.toSorted()
|
|
263
|
-
.flatMap((path) => [`/${path}`, ` Cache-Control: ${FINGERPRINT_IMMUTABLE_CACHE_CONTROL}`]),
|
|
264
|
-
].join("\n");
|
|
265
|
-
if (userHeaders === undefined || userHeaders === "") {
|
|
266
|
-
return `${generated}\n`;
|
|
267
|
-
}
|
|
268
|
-
return `${userHeaders}${userHeaders.endsWith("\n") ? "" : "\n"}\n${generated}\n`;
|
|
269
|
-
}
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
// Reference resolution
|
|
272
|
-
function skippedOnce(skipped, path, reason) {
|
|
273
|
-
if (!skipped.some((entry) => entry.path === path))
|
|
274
|
-
skipped.push({ path, reason });
|
|
275
|
-
}
|
|
276
|
-
function pathExtensionOf(path) {
|
|
277
|
-
const slash = path.lastIndexOf("/");
|
|
278
|
-
const base = slash === -1 ? path : path.slice(slash + 1);
|
|
279
|
-
const dot = base.lastIndexOf(".");
|
|
280
|
-
return dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
|
|
281
|
-
}
|
|
282
|
-
function isExtension(path, ...extensions) {
|
|
283
|
-
return extensions.includes(pathExtensionOf(path));
|
|
284
|
-
}
|
|
285
|
-
function parentDir(path) {
|
|
286
|
-
const slash = path.lastIndexOf("/");
|
|
287
|
-
return slash === -1 ? "" : path.slice(0, slash);
|
|
288
|
-
}
|
|
289
|
-
function normalizeSegments(pathText) {
|
|
290
|
-
const stack = [];
|
|
291
|
-
for (const segment of pathText.split("/")) {
|
|
292
|
-
if (segment === "" || segment === ".")
|
|
293
|
-
continue;
|
|
294
|
-
if (segment === "..") {
|
|
295
|
-
if (stack.length === 0)
|
|
296
|
-
return null;
|
|
297
|
-
stack.pop();
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
stack.push(segment);
|
|
301
|
-
}
|
|
302
|
-
return stack.join("/");
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Resolves a raw document reference to a canonical manifest path, or null for
|
|
306
|
-
* anything out of scope: external origins, protocol-relative URLs, data:/other
|
|
307
|
-
* schemes, query strings, bare fragments, whitespace, undecodable escapes, or
|
|
308
|
-
* traversal above the site root. Exact resolution only — no guessing.
|
|
309
|
-
*/
|
|
310
|
-
function resolveReference(docDir, url) {
|
|
311
|
-
if (url === "" || /\s/.test(url))
|
|
312
|
-
return null;
|
|
313
|
-
if (url.startsWith("#") || url.includes("?"))
|
|
314
|
-
return null;
|
|
315
|
-
if (url.startsWith("//") || SCHEME_PATTERN.test(url))
|
|
316
|
-
return null;
|
|
317
|
-
const hashIndex = url.indexOf("#");
|
|
318
|
-
const pathPart = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
319
|
-
if (pathPart === "")
|
|
320
|
-
return null;
|
|
321
|
-
let decoded;
|
|
322
|
-
try {
|
|
323
|
-
decoded = decodeURIComponent(pathPart);
|
|
324
|
-
}
|
|
325
|
-
catch {
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
return normalizeSegments(decoded.startsWith("/") ? decoded : docDir ? `${docDir}/${decoded}` : decoded);
|
|
329
|
-
}
|
|
330
|
-
function resolveBaseDir(docPath, baseHref) {
|
|
331
|
-
if (baseHref === null)
|
|
332
|
-
return { dir: null, skipDocument: false };
|
|
333
|
-
const href = baseHref.trim();
|
|
334
|
-
if (href === "")
|
|
335
|
-
return { dir: null, skipDocument: false };
|
|
336
|
-
// An external or unparseable <base> makes every ref resolve off-site:
|
|
337
|
-
// rewrite nothing in this document.
|
|
338
|
-
if (href.startsWith("//") || SCHEME_PATTERN.test(href) || href.includes("?") || /\s/.test(href)) {
|
|
339
|
-
return { dir: null, skipDocument: true };
|
|
340
|
-
}
|
|
341
|
-
const withoutFragment = href.split("#", 1)[0];
|
|
342
|
-
let decoded;
|
|
343
|
-
try {
|
|
344
|
-
decoded = decodeURIComponent(withoutFragment);
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
return { dir: null, skipDocument: true };
|
|
348
|
-
}
|
|
349
|
-
const combined = decoded.startsWith("/")
|
|
350
|
-
? decoded
|
|
351
|
-
: `${parentDir(docPath)}${parentDir(docPath) ? "/" : ""}${decoded}`;
|
|
352
|
-
const normalized = normalizeSegments(combined);
|
|
353
|
-
if (normalized === null)
|
|
354
|
-
return { dir: null, skipDocument: true };
|
|
355
|
-
// "/assets/" is the directory itself; "/assets/page.html" bases on its parent.
|
|
356
|
-
return { dir: decoded.endsWith("/") ? normalized : parentDir(normalized), skipDocument: false };
|
|
357
|
-
}
|
|
358
|
-
/** Inserts ".hash" before the final extension of a URL/path string. */
|
|
359
|
-
function insertBeforeExtension(value, hash) {
|
|
360
|
-
const slash = value.lastIndexOf("/");
|
|
361
|
-
const dot = value.lastIndexOf(".");
|
|
362
|
-
if (dot <= slash + 1 || dot >= value.length - 1)
|
|
363
|
-
return null;
|
|
364
|
-
return `${value.slice(0, dot)}.${hash}${value.slice(dot)}`;
|
|
365
|
-
}
|
|
366
|
-
/** Rewrites a raw reference string to its fingerprinted form, keeping any fragment. */
|
|
367
|
-
function fingerprintedUrl(url, hash) {
|
|
368
|
-
const hashIndex = url.indexOf("#");
|
|
369
|
-
const base = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
370
|
-
const suffix = hashIndex === -1 ? "" : url.slice(hashIndex);
|
|
371
|
-
const rewritten = insertBeforeExtension(base, hash);
|
|
372
|
-
return rewritten === null ? null : `${rewritten}${suffix}`;
|
|
373
|
-
}
|
|
374
|
-
function spliceRefs(text, replacements) {
|
|
375
|
-
let output = text;
|
|
376
|
-
for (const ref of [...replacements].toSorted((left, right) => right.start - left.start)) {
|
|
377
|
-
output = `${output.slice(0, ref.start)}${ref.replacement}${output.slice(ref.end)}`;
|
|
378
|
-
}
|
|
379
|
-
return output;
|
|
380
|
-
}
|
|
381
|
-
// ---------------------------------------------------------------------------
|
|
382
|
-
// CSS reference scanning (url() and @import string forms; comments masked)
|
|
383
|
-
const CSS_URL_PATTERN = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)"'\s]+))\s*\)/gi;
|
|
384
|
-
const CSS_IMPORT_PATTERN = /@import\s+(?:"([^"]*)"|'([^']*)')/gi;
|
|
385
|
-
const CSS_COMMENT_PATTERN = /\/\*[\s\S]*?(?:\*\/|$)/g;
|
|
386
|
-
function maskCssComments(css) {
|
|
387
|
-
return css.replace(CSS_COMMENT_PATTERN, (comment) => " ".repeat(comment.length));
|
|
388
|
-
}
|
|
389
|
-
function scanCssRefs(css, offset) {
|
|
390
|
-
const masked = maskCssComments(css);
|
|
391
|
-
const refs = [];
|
|
392
|
-
for (const [pattern, searchFrom] of [
|
|
393
|
-
[CSS_URL_PATTERN, 4],
|
|
394
|
-
[CSS_IMPORT_PATTERN, 7],
|
|
395
|
-
]) {
|
|
396
|
-
pattern.lastIndex = 0;
|
|
397
|
-
for (const match of masked.matchAll(pattern)) {
|
|
398
|
-
const quoted = match[1] ?? match[2];
|
|
399
|
-
const value = quoted ?? match[3];
|
|
400
|
-
if (value === undefined || value === "")
|
|
401
|
-
continue;
|
|
402
|
-
const valueStart = quoted !== undefined
|
|
403
|
-
? match.index + match[0].search(/["']/) + 1
|
|
404
|
-
: match.index + match[0].indexOf(value, searchFrom);
|
|
405
|
-
refs.push({
|
|
406
|
-
start: offset + valueStart,
|
|
407
|
-
end: offset + valueStart + value.length,
|
|
408
|
-
url: value,
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
return refs;
|
|
413
|
-
}
|
|
414
|
-
// ---------------------------------------------------------------------------
|
|
415
|
-
// HTML reference scanning: a real tokenizer (tags, attributes, comments,
|
|
416
|
-
// rawtext elements) that yields exact source offsets, so rewriting is an
|
|
417
|
-
// offset splice and every untouched byte survives verbatim. References are
|
|
418
|
-
// only ever rewritten when they exactly resolve to a published file, so a
|
|
419
|
-
// conservative bail-out (malformed markup, entities in values) simply leaves
|
|
420
|
-
// the reference — and the working original path — alone.
|
|
421
|
-
const HTML_URL_ATTRIBUTES = {
|
|
422
|
-
link: ["href"],
|
|
423
|
-
script: ["src"],
|
|
424
|
-
img: ["src"],
|
|
425
|
-
source: ["src"],
|
|
426
|
-
video: ["src", "poster"],
|
|
427
|
-
audio: ["src"],
|
|
428
|
-
};
|
|
429
|
-
const HTML_SRCSET_TAGS = new Set(["img", "source"]);
|
|
430
|
-
const HTML_RAWTEXT_TAGS = new Set(["script", "style", "textarea", "title"]);
|
|
431
|
-
const META_IMAGE_KEYS = new Set([
|
|
432
|
-
"og:image",
|
|
433
|
-
"og:image:url",
|
|
434
|
-
"og:image:secure_url",
|
|
435
|
-
"twitter:image",
|
|
436
|
-
"twitter:image:src",
|
|
437
|
-
]);
|
|
438
|
-
function scanHtml(text) {
|
|
439
|
-
const refs = [];
|
|
440
|
-
const cssRanges = [];
|
|
441
|
-
const lower = text.toLowerCase();
|
|
442
|
-
let baseHref = null;
|
|
443
|
-
let baseSeen = false;
|
|
444
|
-
const processTag = (tagName, attrs) => {
|
|
445
|
-
const byName = new Map();
|
|
446
|
-
for (const attr of attrs) {
|
|
447
|
-
if (!byName.has(attr.name))
|
|
448
|
-
byName.set(attr.name, attr);
|
|
449
|
-
}
|
|
450
|
-
if (tagName === "base") {
|
|
451
|
-
if (!baseSeen && byName.has("href")) {
|
|
452
|
-
baseSeen = true;
|
|
453
|
-
baseHref = byName.get("href").value;
|
|
454
|
-
}
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
// Attribute values carrying entities ("&") are skipped rather than decoded:
|
|
458
|
-
// exact-match-only means ambiguity always resolves to "leave it alone".
|
|
459
|
-
const pushRef = (attr) => {
|
|
460
|
-
if (attr && attr.value !== "" && !attr.value.includes("&")) {
|
|
461
|
-
refs.push({ start: attr.start, end: attr.end, url: attr.value });
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
for (const attrName of HTML_URL_ATTRIBUTES[tagName] ?? []) {
|
|
465
|
-
pushRef(byName.get(attrName));
|
|
466
|
-
}
|
|
467
|
-
if (HTML_SRCSET_TAGS.has(tagName)) {
|
|
468
|
-
const srcset = byName.get("srcset");
|
|
469
|
-
if (srcset && srcset.value !== "" && !srcset.value.includes("&")) {
|
|
470
|
-
refs.push(...srcsetUrlSpans(srcset.value, srcset.start));
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
if (tagName === "meta") {
|
|
474
|
-
const key = (byName.get("property")?.value ?? byName.get("name")?.value ?? "").toLowerCase();
|
|
475
|
-
if (META_IMAGE_KEYS.has(key))
|
|
476
|
-
pushRef(byName.get("content"));
|
|
477
|
-
}
|
|
478
|
-
const style = byName.get("style");
|
|
479
|
-
if (style && style.value !== "" && !style.value.includes("&")) {
|
|
480
|
-
cssRanges.push({ offset: style.start, css: style.value });
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
let i = 0;
|
|
484
|
-
scan: while (i < text.length) {
|
|
485
|
-
const lt = text.indexOf("<", i);
|
|
486
|
-
if (lt === -1)
|
|
487
|
-
break;
|
|
488
|
-
if (text.startsWith("<!--", lt)) {
|
|
489
|
-
const close = text.indexOf("-->", lt + 4);
|
|
490
|
-
if (close === -1)
|
|
491
|
-
break;
|
|
492
|
-
i = close + 3;
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
const next = text[lt + 1] ?? "";
|
|
496
|
-
if (next === "!" || next === "?" || next === "/") {
|
|
497
|
-
const close = text.indexOf(">", lt + 1);
|
|
498
|
-
if (close === -1)
|
|
499
|
-
break;
|
|
500
|
-
i = close + 1;
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
if (!/[a-zA-Z]/.test(next)) {
|
|
504
|
-
i = lt + 1;
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
let p = lt + 1;
|
|
508
|
-
while (p < text.length && /[a-zA-Z0-9-]/.test(text[p]))
|
|
509
|
-
p += 1;
|
|
510
|
-
const tagName = lower.slice(lt + 1, p);
|
|
511
|
-
const attrs = [];
|
|
512
|
-
let selfClosing = false;
|
|
513
|
-
let closed = false;
|
|
514
|
-
while (p < text.length) {
|
|
515
|
-
const char = text[p];
|
|
516
|
-
if (/\s/.test(char)) {
|
|
517
|
-
p += 1;
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
if (char === "/") {
|
|
521
|
-
selfClosing = true;
|
|
522
|
-
p += 1;
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
if (char === ">") {
|
|
526
|
-
closed = true;
|
|
527
|
-
p += 1;
|
|
528
|
-
break;
|
|
529
|
-
}
|
|
530
|
-
selfClosing = false;
|
|
531
|
-
const nameStart = p;
|
|
532
|
-
while (p < text.length && !/[\s=/>]/.test(text[p]))
|
|
533
|
-
p += 1;
|
|
534
|
-
const name = lower.slice(nameStart, p);
|
|
535
|
-
while (p < text.length && /\s/.test(text[p]))
|
|
536
|
-
p += 1;
|
|
537
|
-
if (text[p] === "=") {
|
|
538
|
-
p += 1;
|
|
539
|
-
while (p < text.length && /\s/.test(text[p]))
|
|
540
|
-
p += 1;
|
|
541
|
-
const quote = text[p];
|
|
542
|
-
if (quote === '"' || quote === "'") {
|
|
543
|
-
const valueStart = p + 1;
|
|
544
|
-
const valueEnd = text.indexOf(quote, valueStart);
|
|
545
|
-
// Unterminated attribute: stop scanning — never guess at structure.
|
|
546
|
-
if (valueEnd === -1)
|
|
547
|
-
break scan;
|
|
548
|
-
attrs.push({
|
|
549
|
-
name,
|
|
550
|
-
value: text.slice(valueStart, valueEnd),
|
|
551
|
-
start: valueStart,
|
|
552
|
-
end: valueEnd,
|
|
553
|
-
});
|
|
554
|
-
p = valueEnd + 1;
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
const valueStart = p;
|
|
558
|
-
while (p < text.length && !/[\s>]/.test(text[p]))
|
|
559
|
-
p += 1;
|
|
560
|
-
attrs.push({ name, value: text.slice(valueStart, p), start: valueStart, end: p });
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
else if (name !== "") {
|
|
564
|
-
attrs.push({ name, value: "", start: nameStart, end: nameStart });
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
p += 1;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
if (!closed)
|
|
571
|
-
break;
|
|
572
|
-
processTag(tagName, attrs);
|
|
573
|
-
if (!selfClosing && HTML_RAWTEXT_TAGS.has(tagName)) {
|
|
574
|
-
const closeIndex = lower.indexOf(`</${tagName}`, p);
|
|
575
|
-
const contentEnd = closeIndex === -1 ? text.length : closeIndex;
|
|
576
|
-
if (tagName === "style") {
|
|
577
|
-
cssRanges.push({ offset: p, css: text.slice(p, contentEnd) });
|
|
578
|
-
}
|
|
579
|
-
if (closeIndex === -1)
|
|
580
|
-
break;
|
|
581
|
-
i = contentEnd;
|
|
582
|
-
continue;
|
|
583
|
-
}
|
|
584
|
-
i = p;
|
|
585
|
-
}
|
|
586
|
-
return { refs, cssRanges, baseHref };
|
|
587
|
-
}
|
|
588
|
-
/** WHATWG-lite srcset candidate parser: URL, then descriptor up to the next comma. */
|
|
589
|
-
function srcsetUrlSpans(value, valueStart) {
|
|
590
|
-
const spans = [];
|
|
591
|
-
let i = 0;
|
|
592
|
-
while (i < value.length) {
|
|
593
|
-
while (i < value.length && /[\s,]/.test(value[i]))
|
|
594
|
-
i += 1;
|
|
595
|
-
if (i >= value.length)
|
|
596
|
-
break;
|
|
597
|
-
let j = i;
|
|
598
|
-
const dataUrl = value.slice(i, i + 5).toLowerCase() === "data:";
|
|
599
|
-
while (j < value.length &&
|
|
600
|
-
!(dataUrl ? /\s/.test(value[j]) : /[\s,]/.test(value[j]))) {
|
|
601
|
-
j += 1;
|
|
602
|
-
}
|
|
603
|
-
let url = value.slice(i, j);
|
|
604
|
-
let end = j;
|
|
605
|
-
let endedWithComma = false;
|
|
606
|
-
while (url.endsWith(",")) {
|
|
607
|
-
url = url.slice(0, -1);
|
|
608
|
-
end -= 1;
|
|
609
|
-
endedWithComma = true;
|
|
610
|
-
}
|
|
611
|
-
if (url !== "")
|
|
612
|
-
spans.push({ start: valueStart + i, end: valueStart + end, url });
|
|
613
|
-
i = j;
|
|
614
|
-
if (endedWithComma)
|
|
615
|
-
continue;
|
|
616
|
-
while (i < value.length && value[i] !== ",")
|
|
617
|
-
i += 1;
|
|
618
|
-
}
|
|
619
|
-
return spans;
|
|
620
|
-
}
|