@spacefast/common 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts/access.d.ts +4 -4
- package/dist/contracts/activity.d.ts +4 -4
- package/dist/contracts/annotations.d.ts +1 -1
- package/dist/contracts/api-keys.d.ts +6 -6
- package/dist/contracts/archives.d.ts +14 -14
- package/dist/contracts/billing.d.ts +21 -21
- package/dist/contracts/builds.d.ts +8 -8
- package/dist/contracts/channels.d.ts +2 -2
- package/dist/contracts/common.d.ts +5 -5
- package/dist/contracts/continuation.d.ts +66 -0
- package/dist/contracts/continuation.js +91 -0
- package/dist/contracts/deployments.d.ts +18 -18
- package/dist/contracts/device-auth.d.ts +6 -6
- package/dist/contracts/domains.d.ts +247 -247
- package/dist/contracts/enums.d.ts +2 -2
- package/dist/contracts/error-code-meta.d.ts +12 -0
- package/dist/contracts/error-code-meta.js +3 -0
- package/dist/contracts/error-codes.d.ts +1 -1
- package/dist/contracts/error-codes.js +3 -0
- package/dist/contracts/events.d.ts +12 -12
- package/dist/contracts/features.d.ts +17 -17
- package/dist/contracts/git.d.ts +3 -3
- package/dist/contracts/internal.d.ts +3 -3
- package/dist/contracts/mcp.d.ts +34 -34
- package/dist/contracts/me.d.ts +8 -8
- package/dist/contracts/operations.d.ts +2 -2
- package/dist/contracts/plan-policy.d.ts +4 -4
- package/dist/contracts/platform.d.ts +3 -3
- package/dist/contracts/repository-connections.d.ts +5 -5
- package/dist/contracts/resources.d.ts +15 -15
- package/dist/contracts/runtime-api.d.ts +4 -4
- package/dist/contracts/sites.d.ts +26 -26
- package/dist/contracts/spaces.d.ts +179 -89
- package/dist/contracts/spaces.js +57 -3
- package/dist/contracts/superadmin-emails.d.ts +3 -3
- package/dist/contracts/superadmin-queues.d.ts +58 -58
- package/dist/contracts/superadmin-spaces.d.ts +275 -9
- package/dist/contracts/superadmin-spaces.js +24 -0
- package/dist/contracts/superadmin-tenants.d.ts +8 -8
- package/dist/contracts/superadmin.d.ts +12 -12
- package/dist/contracts/tags.d.ts +38 -38
- package/dist/contracts/teams.d.ts +26 -8
- package/dist/contracts/teams.js +20 -0
- package/dist/contracts/transfers.d.ts +9 -9
- package/dist/contracts/variables.d.ts +14 -14
- package/dist/contracts/webhooks.d.ts +4 -4
- package/dist/contracts/zero.d.ts +8 -8
- package/dist/docs/agent-prose.js +8 -4
- package/dist/docs/agent-setup.js +3 -2
- package/dist/docs/error-docs.js +16 -4
- package/dist/utils/query-keys.d.ts +2 -2
- package/dist/utils/static-runtime-policy.d.ts +21 -0
- package/dist/utils/static-runtime-policy.js +119 -3
- package/package.json +2 -1
package/dist/contracts/zero.d.ts
CHANGED
|
@@ -183,9 +183,9 @@ export type ZeroMigrationOperation = z.infer<typeof zeroMigrationOperationSchema
|
|
|
183
183
|
export declare const zeroMigrationPlanSchema: z.ZodObject<{
|
|
184
184
|
format: z.ZodLiteral<"stattic.zero.migrations.v1">;
|
|
185
185
|
mode: z.ZodDefault<z.ZodEnum<{
|
|
186
|
+
safe: "safe";
|
|
186
187
|
drop: "drop";
|
|
187
188
|
rename: "rename";
|
|
188
|
-
safe: "safe";
|
|
189
189
|
}>>;
|
|
190
190
|
schemaHash: z.ZodString;
|
|
191
191
|
operations: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -480,8 +480,8 @@ export declare const zeroCapsuleArtifactSchema: z.ZodObject<{
|
|
|
480
480
|
}, z.core.$strict>;
|
|
481
481
|
limits: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
482
482
|
futureCapabilities: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
483
|
-
blob: "blob";
|
|
484
483
|
ai: "ai";
|
|
484
|
+
blob: "blob";
|
|
485
485
|
"generalized-js": "generalized-js";
|
|
486
486
|
}>>>;
|
|
487
487
|
sourceManifest: z.ZodObject<{
|
|
@@ -641,8 +641,8 @@ export declare const zeroRuntimeFinalizePayloadSchema: z.ZodObject<{
|
|
|
641
641
|
}, z.core.$strict>;
|
|
642
642
|
limits: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
643
643
|
futureCapabilities: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
644
|
-
blob: "blob";
|
|
645
644
|
ai: "ai";
|
|
645
|
+
blob: "blob";
|
|
646
646
|
"generalized-js": "generalized-js";
|
|
647
647
|
}>>>;
|
|
648
648
|
sourceManifest: z.ZodObject<{
|
|
@@ -705,9 +705,9 @@ export declare const zeroRuntimeFinalizePayloadSchema: z.ZodObject<{
|
|
|
705
705
|
migrations: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
706
706
|
format: z.ZodLiteral<"stattic.zero.migrations.v1">;
|
|
707
707
|
mode: z.ZodDefault<z.ZodEnum<{
|
|
708
|
+
safe: "safe";
|
|
708
709
|
drop: "drop";
|
|
709
710
|
rename: "rename";
|
|
710
|
-
safe: "safe";
|
|
711
711
|
}>>;
|
|
712
712
|
schemaHash: z.ZodString;
|
|
713
713
|
operations: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -916,8 +916,8 @@ export declare const zeroDeployMetadataSchema: z.ZodObject<{
|
|
|
916
916
|
}, z.core.$strict>;
|
|
917
917
|
limits: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
918
918
|
futureCapabilities: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
919
|
-
blob: "blob";
|
|
920
919
|
ai: "ai";
|
|
920
|
+
blob: "blob";
|
|
921
921
|
"generalized-js": "generalized-js";
|
|
922
922
|
}>>>;
|
|
923
923
|
sourceManifest: z.ZodObject<{
|
|
@@ -980,9 +980,9 @@ export declare const zeroDeployMetadataSchema: z.ZodObject<{
|
|
|
980
980
|
migrations: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
981
981
|
format: z.ZodLiteral<"stattic.zero.migrations.v1">;
|
|
982
982
|
mode: z.ZodDefault<z.ZodEnum<{
|
|
983
|
+
safe: "safe";
|
|
983
984
|
drop: "drop";
|
|
984
985
|
rename: "rename";
|
|
985
|
-
safe: "safe";
|
|
986
986
|
}>>;
|
|
987
987
|
schemaHash: z.ZodString;
|
|
988
988
|
operations: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -1234,9 +1234,9 @@ export declare const zeroDbInspectResponseSchema: z.ZodObject<{
|
|
|
1234
1234
|
artifactId: z.ZodNullable<z.ZodString>;
|
|
1235
1235
|
backend: z.ZodNullable<z.ZodLiteral<"wpcloud-mysql">>;
|
|
1236
1236
|
migrationMode: z.ZodNullable<z.ZodEnum<{
|
|
1237
|
+
safe: "safe";
|
|
1237
1238
|
drop: "drop";
|
|
1238
1239
|
rename: "rename";
|
|
1239
|
-
safe: "safe";
|
|
1240
1240
|
}>>;
|
|
1241
1241
|
schemaHash: z.ZodNullable<z.ZodString>;
|
|
1242
1242
|
tables: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -1304,9 +1304,9 @@ export declare const zeroDbInspectPublicResponseSchema: z.ZodObject<{
|
|
|
1304
1304
|
artifactId: z.ZodNullable<z.ZodString>;
|
|
1305
1305
|
backend: z.ZodNullable<z.ZodLiteral<"wpcloud-mysql">>;
|
|
1306
1306
|
migrationMode: z.ZodNullable<z.ZodEnum<{
|
|
1307
|
+
safe: "safe";
|
|
1307
1308
|
drop: "drop";
|
|
1308
1309
|
rename: "rename";
|
|
1309
|
-
safe: "safe";
|
|
1310
1310
|
}>>;
|
|
1311
1311
|
schemaHash: z.ZodNullable<z.ZodString>;
|
|
1312
1312
|
tables: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
package/dist/docs/agent-prose.js
CHANGED
|
@@ -91,16 +91,20 @@ export const RECEIPT_SHAPE_PROSE = [
|
|
|
91
91
|
"The JSON receipt is the source of truth:",
|
|
92
92
|
"",
|
|
93
93
|
"- `data.space.liveUrl` — the live URL.",
|
|
94
|
-
"- `data.version.immutableUrl` — the permanent, frozen version URL
|
|
94
|
+
"- `data.version.immutableUrl` — the permanent, frozen version URL; share it for reviews.",
|
|
95
|
+
" It is reserved in the initial receipt. If the publish is still finalizing, poll `data.links.status`",
|
|
96
|
+
" with the claim token until the status is `ready`.",
|
|
95
97
|
"- `data.shareBlurb` — a paste-ready one-liner with the URL and claim nudge; relay it verbatim when present.",
|
|
96
98
|
"- `data.upload` — signed upload instructions for manifest publishes when file bytes were not sent",
|
|
97
99
|
" with the request. Send each `data.upload.targets[]` instruction exactly as returned, treat",
|
|
98
100
|
" authorization headers and upload tokens as secrets, then finalize.",
|
|
99
101
|
"- `data.claim.url` and `data.claim.expiresAt` — the space is anonymous and expires unless claimed;",
|
|
100
102
|
" always show the user the claim link and the deadline so the space becomes theirs.",
|
|
101
|
-
"- `data.claim.token` — a secret capability.
|
|
102
|
-
" `Authorization: Bearer <claim-token>`
|
|
103
|
-
"
|
|
103
|
+
"- `data.claim.token` — a secret capability. For anonymous spaces, use it as",
|
|
104
|
+
" `Authorization: Bearer <claim-token>` when polling `data.links.status`, finalizing uploads,",
|
|
105
|
+
" or updating the same `spaceId`.",
|
|
106
|
+
" Never print it back to the user.",
|
|
107
|
+
"- `data.links.status`, `resume`, `finalize`, and `promote` — follow-up API paths.",
|
|
104
108
|
"- `error.code`, `error.docsUrl`, and `error.hint` — stable error code plus exact recovery docs and a next step.",
|
|
105
109
|
"",
|
|
106
110
|
"If no files changed, the API may omit `upload`; report the receipt and diagnostics rather than",
|
package/dist/docs/agent-setup.js
CHANGED
|
@@ -120,8 +120,9 @@ export function aiInstructionsText() {
|
|
|
120
120
|
"",
|
|
121
121
|
"## After publishing",
|
|
122
122
|
"",
|
|
123
|
-
"- Share the live URL, the immutable version URL, and — for anonymous spaces —
|
|
124
|
-
" claim link with its expiry.",
|
|
123
|
+
"- Share the live URL, the immutable version URL when ready, and — for anonymous spaces —",
|
|
124
|
+
" the claim link with its expiry. If the receipt's immutable URL is null, poll the",
|
|
125
|
+
" operation and version links with the claim token as bearer auth until the version is ready.",
|
|
125
126
|
"- Never print access tokens, claim tokens, auth files, or `.stattic/state.json`.",
|
|
126
127
|
"",
|
|
127
128
|
"## Space conventions",
|
package/dist/docs/error-docs.js
CHANGED
|
@@ -483,6 +483,10 @@ export const ERROR_DOCS = {
|
|
|
483
483
|
summary: "The claim token is invalid or does not match this space.",
|
|
484
484
|
fix: "Use the exact claim URL returned by the publish that created the space.",
|
|
485
485
|
},
|
|
486
|
+
invalid_continuation_token: {
|
|
487
|
+
summary: "The continuation token is invalid, expired, or already finished.",
|
|
488
|
+
fix: "Restart the originating flow to obtain a fresh requiresAction envelope and continuation token.",
|
|
489
|
+
},
|
|
486
490
|
invalid_data_location: {
|
|
487
491
|
summary: "The requested data location is not a supported region.",
|
|
488
492
|
fix: "Pick a region from the documented data locations, or use auto.",
|
|
@@ -571,6 +575,14 @@ export const ERROR_DOCS = {
|
|
|
571
575
|
summary: "An invitation for this email already exists on the team.",
|
|
572
576
|
fix: "Resend the existing invitation instead of creating a new one.",
|
|
573
577
|
},
|
|
578
|
+
invitation_email_mismatch: {
|
|
579
|
+
summary: "This invitation was addressed to a different email than the accepting user.",
|
|
580
|
+
fix: "Sign in as the invited email, or ask an admin to invite your current address.",
|
|
581
|
+
},
|
|
582
|
+
invitation_expired: {
|
|
583
|
+
summary: "The invitation has expired and can no longer be accepted.",
|
|
584
|
+
fix: "Ask a team admin to send a fresh invitation, then accept the new one.",
|
|
585
|
+
},
|
|
574
586
|
invitation_not_pending: {
|
|
575
587
|
summary: "The invitation is no longer pending.",
|
|
576
588
|
fix: "Create a new invitation if the member still needs to join.",
|
|
@@ -808,8 +820,8 @@ export const ERROR_DOCS = {
|
|
|
808
820
|
fix: "Refresh the runtime JWKS; the signing keys may have rotated.",
|
|
809
821
|
},
|
|
810
822
|
runtime_logs_unavailable: {
|
|
811
|
-
summary: "
|
|
812
|
-
fix: "
|
|
823
|
+
summary: "Server logs are not available because Spacefast serves your space as static files.",
|
|
824
|
+
fix: "There is no running server process to log. Use the request logs in your dashboard to inspect recent traffic.",
|
|
813
825
|
},
|
|
814
826
|
runtime_management_unavailable: {
|
|
815
827
|
summary: "The runtime management surface is unavailable for this site.",
|
|
@@ -1548,8 +1560,8 @@ export const ERROR_DOCS = {
|
|
|
1548
1560
|
fix: "Use the existing shared site.",
|
|
1549
1561
|
},
|
|
1550
1562
|
wp_cloud_webhook_secret_not_configured: {
|
|
1551
|
-
summary: "The
|
|
1552
|
-
fix: "Set the
|
|
1563
|
+
summary: "The infra webhook secret is not configured on this deployment.",
|
|
1564
|
+
fix: "Set the infra webhook secret in the control-plane environment.",
|
|
1553
1565
|
},
|
|
1554
1566
|
};
|
|
1555
1567
|
const PRODUCT = BRAND.productName;
|
|
@@ -21,7 +21,7 @@ export declare const qk: {
|
|
|
21
21
|
teamDomains: (id: string) => readonly ["teams", string, "domains"];
|
|
22
22
|
teamManagedDomains: (id: string) => readonly ["teams", string, "managed-domains"];
|
|
23
23
|
domainBindings: () => readonly ["domain-bindings"];
|
|
24
|
-
slugAvailability: (kind: "team" | "space", slug: string, teamId?: string, currentSlug?: string) => readonly ["slugs", "availability", "
|
|
24
|
+
slugAvailability: (kind: "team" | "space", slug: string, teamId?: string, currentSlug?: string) => readonly ["slugs", "availability", "space" | "team", string, string, string];
|
|
25
25
|
teamVariables: (id: string) => readonly ["teams", string, "variables"];
|
|
26
26
|
teamActivity: (id: string, limit: number) => readonly ["teams", string, "activity", number];
|
|
27
27
|
space: (id: string) => readonly ["spaces", string];
|
|
@@ -63,7 +63,7 @@ export declare const qk: {
|
|
|
63
63
|
spaceZeroRealtimeEvents: (id: string, limit: number) => readonly ["spaces", string, "zero", "realtime", "events", number];
|
|
64
64
|
spaceVariables: (id: string) => readonly ["spaces", string, "variables"];
|
|
65
65
|
spaceCertificates: (id: string) => readonly ["spaces", string, "runtime", "certificates"];
|
|
66
|
-
spaceLogs: (id: string, kind: "build" | "access" | "runtime") => readonly ["spaces", string, "logs", "build" | "
|
|
66
|
+
spaceLogs: (id: string, kind: "build" | "access" | "runtime") => readonly ["spaces", string, "logs", "build" | "runtime" | "access"];
|
|
67
67
|
spaceAnalytics: (id: string, window: string) => readonly ["spaces", string, "analytics", string];
|
|
68
68
|
domain: (teamId: string, ref: string) => readonly ["teams", string, "domains", string];
|
|
69
69
|
domainDiagnostics: (teamId: string, domainId: string) => readonly ["teams", string, "domains", string, "diagnostics"];
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
export declare const PHP_LIKE_EXTENSIONS: string[];
|
|
2
2
|
export declare const PLATFORM_MANAGED_RESPONSE_HEADERS: string[];
|
|
3
3
|
export declare const NON_IMMUTABLE_EXTENSIONS: string[];
|
|
4
|
+
export declare const GUARANTEED_IMMUTABLE_PREFIXES: string[];
|
|
4
5
|
/** Lowercased extension without the leading dot, posix semantics. Dotfiles have no extension. */
|
|
5
6
|
export declare function pathExtension(filePath: string): string;
|
|
6
7
|
/** Basename without its extension (the "stem"), posix semantics. */
|
|
7
8
|
export declare function pathStem(filePath: string): string;
|
|
8
9
|
export declare function isPhpLikeStaticPath(filePath: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Classifies a candidate hash token — the final `.`/`-`-delimited tail of a
|
|
12
|
+
* filename stem (see isImmutableAssetPath) — as a real bundler content hash.
|
|
13
|
+
*
|
|
14
|
+
* Immutable caching is a one-year pin, so the test is asymmetric on purpose: a
|
|
15
|
+
* missed hash costs a cheap 304 revalidation, a false positive serves stale
|
|
16
|
+
* content for a year. When a token is ambiguous we say no. The discriminator is
|
|
17
|
+
* NOT character case — modern bundlers emit BOTH lowercase-hex and mixed-case
|
|
18
|
+
* base64url hashes — it is entropy plus the absence of dictionary-word shape.
|
|
19
|
+
*
|
|
20
|
+
* IRREDUCIBLE LIMIT: an 8-char base64url token is the same size as a short
|
|
21
|
+
* CamelCase-with-version-digit human name, and every default Vite/Rollup hash we
|
|
22
|
+
* must accept carries only one digit (BzeDGMM3, 4A_iFExJ). So a token like
|
|
23
|
+
* `D3GeoMap`, `McKayCV2`, or `v2-WebGL` is character-for-character indistinguishable
|
|
24
|
+
* from a real hash and is accepted by clause B. No filename-only rule can separate
|
|
25
|
+
* these without also rejecting the bundler hashes the spec mandates. The durable
|
|
26
|
+
* fix is to gate immutable caching on path convention (Vite `assets/`, Next
|
|
27
|
+
* `_next/static/`, SvelteKit `_app/immutable/`, …) rather than guessing from the
|
|
28
|
+
* stem; this heuristic is the best-effort fallback when that context is absent.
|
|
29
|
+
*/
|
|
9
30
|
export declare function looksLikeBundlerFingerprint(segment: string): boolean;
|
|
10
31
|
export declare function isImmutableAssetPath(filePath: string): boolean;
|
|
11
32
|
/** Forced inert serving policy for a committed static file. PHP-like files are never executable. */
|
|
@@ -46,7 +46,50 @@ export const PLATFORM_MANAGED_RESPONSE_HEADERS = [
|
|
|
46
46
|
export const NON_IMMUTABLE_EXTENSIONS = ["html", "htm", "php", "txt", "xml"];
|
|
47
47
|
const PHP_LIKE_EXTENSION_SET = new Set(PHP_LIKE_EXTENSIONS);
|
|
48
48
|
const NON_IMMUTABLE_EXTENSION_SET = new Set(NON_IMMUTABLE_EXTENSIONS);
|
|
49
|
-
|
|
49
|
+
// Two real bundler content-hash shapes. (A) lowercase hex (git/webpack short
|
|
50
|
+
// SHAs, many tools): >=7 chars of [0-9a-f]. (B) base64url (Vite/Rollup/esbuild/
|
|
51
|
+
// Next): >=8 chars of [A-Za-z0-9_-]. Both forms are recognized; the entropy
|
|
52
|
+
// guards in looksLikeBundlerFingerprint are what keep human names out.
|
|
53
|
+
const HEX_FINGERPRINT = /^[0-9a-f]{7,}$/;
|
|
54
|
+
const BASE64URL_FINGERPRINT = /^[A-Za-z0-9_-]{8,}$/;
|
|
55
|
+
// Durable path-convention layer over the filename-entropy heuristic
|
|
56
|
+
// (looksLikeBundlerFingerprint). These are reserved build-output directories that the
|
|
57
|
+
// framework OWNS and fills exclusively with content-addressed ([contenthash]) output by
|
|
58
|
+
// construction — so every asset served from under them is safe to pin immutable for a
|
|
59
|
+
// year regardless of how its individual filename reads. This recovers the real hashes the
|
|
60
|
+
// filename heuristic conservatively MISSES (no-digit / single-case base64url draws like
|
|
61
|
+
// `oton0Guf`, `DYyuCeEr`) without loosening the heuristic itself.
|
|
62
|
+
//
|
|
63
|
+
// Each prefix is HIGH-CONFIDENCE guaranteed-hashed:
|
|
64
|
+
// - `_next/static/` Next.js immutable build assets (chunks/css/media), all
|
|
65
|
+
// [contenthash]; Next itself serves this dir with `immutable`.
|
|
66
|
+
// - `_app/immutable/` SvelteKit (Vite) — the directory is literally named `immutable`
|
|
67
|
+
// and holds only fingerprinted chunks/assets.
|
|
68
|
+
// - `_nuxt/` Nuxt build dir — Vite-hashed client bundle, served from a
|
|
69
|
+
// reserved app-owned path.
|
|
70
|
+
// - `_astro/` Astro bundled assets — all content-hashed.
|
|
71
|
+
//
|
|
72
|
+
// DELIBERATELY EXCLUDED: generic user-writable dirs (Vite's `assets/`, CRA/Gatsby's
|
|
73
|
+
// `static/`). Users drop NON-hashed files there, so widening to them would create
|
|
74
|
+
// year-long false positives. Those stay gated by the filename heuristic only. This layer
|
|
75
|
+
// is purely ADDITIVE: it can only turn a heuristic-`false` into `true` for assets under a
|
|
76
|
+
// guaranteed-hashed dir; it never flips a `true` to `false`.
|
|
77
|
+
export const GUARANTEED_IMMUTABLE_PREFIXES = [
|
|
78
|
+
"_next/static/",
|
|
79
|
+
"_app/immutable/",
|
|
80
|
+
"_nuxt/",
|
|
81
|
+
"_astro/",
|
|
82
|
+
];
|
|
83
|
+
/** True when the path lives under a framework dir where every emitted asset is content-hashed. */
|
|
84
|
+
function isUnderGuaranteedImmutablePrefix(filePath) {
|
|
85
|
+
const normalized = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
|
86
|
+
for (const prefix of GUARANTEED_IMMUTABLE_PREFIXES) {
|
|
87
|
+
if (normalized.startsWith(prefix)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
50
93
|
function lastSegment(filePath) {
|
|
51
94
|
const slash = filePath.lastIndexOf("/");
|
|
52
95
|
return slash >= 0 ? filePath.slice(slash + 1) : filePath;
|
|
@@ -66,14 +109,87 @@ export function pathStem(filePath) {
|
|
|
66
109
|
export function isPhpLikeStaticPath(filePath) {
|
|
67
110
|
return PHP_LIKE_EXTENSION_SET.has(pathExtension(filePath));
|
|
68
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Classifies a candidate hash token — the final `.`/`-`-delimited tail of a
|
|
114
|
+
* filename stem (see isImmutableAssetPath) — as a real bundler content hash.
|
|
115
|
+
*
|
|
116
|
+
* Immutable caching is a one-year pin, so the test is asymmetric on purpose: a
|
|
117
|
+
* missed hash costs a cheap 304 revalidation, a false positive serves stale
|
|
118
|
+
* content for a year. When a token is ambiguous we say no. The discriminator is
|
|
119
|
+
* NOT character case — modern bundlers emit BOTH lowercase-hex and mixed-case
|
|
120
|
+
* base64url hashes — it is entropy plus the absence of dictionary-word shape.
|
|
121
|
+
*
|
|
122
|
+
* IRREDUCIBLE LIMIT: an 8-char base64url token is the same size as a short
|
|
123
|
+
* CamelCase-with-version-digit human name, and every default Vite/Rollup hash we
|
|
124
|
+
* must accept carries only one digit (BzeDGMM3, 4A_iFExJ). So a token like
|
|
125
|
+
* `D3GeoMap`, `McKayCV2`, or `v2-WebGL` is character-for-character indistinguishable
|
|
126
|
+
* from a real hash and is accepted by clause B. No filename-only rule can separate
|
|
127
|
+
* these without also rejecting the bundler hashes the spec mandates. The durable
|
|
128
|
+
* fix is to gate immutable caching on path convention (Vite `assets/`, Next
|
|
129
|
+
* `_next/static/`, SvelteKit `_app/immutable/`, …) rather than guessing from the
|
|
130
|
+
* stem; this heuristic is the best-effort fallback when that context is absent.
|
|
131
|
+
*/
|
|
69
132
|
export function looksLikeBundlerFingerprint(segment) {
|
|
70
|
-
|
|
71
|
-
|
|
133
|
+
// (A) Lowercase-hex hash (git short-SHA, webpack/Vite/Gatsby `[contenthash]`):
|
|
134
|
+
// a1b2c3d, 3f9a2b7e, d41d8cd98f00b204e980. Always require >=1 hex letter and
|
|
135
|
+
// >=1 digit so all-letter words (deadbeef) and digit-only runs (1234567) are out.
|
|
136
|
+
if (HEX_FINGERPRINT.test(segment) && /[a-f]/.test(segment) && /[0-9]/.test(segment)) {
|
|
137
|
+
// A long all-hex token is unambiguous: no human authors a 16+ char hex name, so
|
|
138
|
+
// length alone is entropy proof and the anti-word guards would only lose real
|
|
139
|
+
// hashes (most 16/20-char contenthashes contain an incidental 4-run).
|
|
140
|
+
if (segment.length >= 16) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
// Short hex (7-15) overlaps with hex-spelled words/years/colors, so require >=2
|
|
144
|
+
// digits and forbid a run of 4+ hex letters (deadbeef, cafe, face, decade) or
|
|
145
|
+
// 4+ digits (2024, 1234) — low-entropy human false positives.
|
|
146
|
+
if ((segment.match(/[0-9]/g) ?? []).length >= 2 &&
|
|
147
|
+
!/[a-f]{4}/.test(segment) &&
|
|
148
|
+
!/[0-9]{4}/.test(segment)) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// (B) Base64url hash (Vite/Rollup/esbuild/Next): BzeDGMM3, 4A_iFExJ, P1Tx-Cyu,
|
|
153
|
+
// Z-cBR0Xb, 9-ygPMte. Random base64url draws — but immutable is a one-year pin, so
|
|
154
|
+
// against the human names that mimic this shape (acronym data/UI names, version
|
|
155
|
+
// and fiscal labels, dates, resolutions) we demand the full BALANCED-entropy
|
|
156
|
+
// signature of a real hash, and accept the safe cost of missing the minority of
|
|
157
|
+
// genuine hashes that fall short (a cheap 304):
|
|
158
|
+
// - >=2 lowercase AND >=2 uppercase letters. Real base64url hashes carry several
|
|
159
|
+
// of each; acronym-dominant human names (USMap-AKHIORWA, Map-USA_CA_TX,
|
|
160
|
+
// MyCSS2DUI) have at most one lowercase token and are rejected here. This also
|
|
161
|
+
// conservatively drops single-case and no-/one-lowercase hashes (DYyuCeEr,
|
|
162
|
+
// ChJ_j-JJ) — the safe miss direction.
|
|
163
|
+
// - at least one digit. ~1/4 of base64url hashes lack one and become cheap 304s,
|
|
164
|
+
// but requiring it removes the all-letter acronym names that are otherwise
|
|
165
|
+
// indistinguishable from a hash (USMap-AKHIORWA has no digit).
|
|
166
|
+
// - no run of 3+ lowercase letters: a dictionary-word body (MyComponent
|
|
167
|
+
// "omponent", LoginOAuth2 "ogin", HTTPClient2 "lient"). A packed hash breaks
|
|
168
|
+
// lowercase up with caps/digits.
|
|
169
|
+
// - no run of 2+ digits: clustered digits are human years/resolutions/sequences/
|
|
170
|
+
// dates (2024, 1080, Win10, 01); a hash scatters single digits between letters.
|
|
171
|
+
// <8 chars (Widget2, API2, v2) never qualifies via the regex length floor.
|
|
172
|
+
if (BASE64URL_FINGERPRINT.test(segment) &&
|
|
173
|
+
(segment.match(/[a-z]/g) ?? []).length >= 2 &&
|
|
174
|
+
(segment.match(/[A-Z]/g) ?? []).length >= 2 &&
|
|
175
|
+
/[0-9]/.test(segment) &&
|
|
176
|
+
!/[a-z]{3}/.test(segment) &&
|
|
177
|
+
!/[0-9]{2}/.test(segment)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
72
181
|
}
|
|
73
182
|
export function isImmutableAssetPath(filePath) {
|
|
74
183
|
if (NON_IMMUTABLE_EXTENSION_SET.has(pathExtension(filePath))) {
|
|
75
184
|
return false;
|
|
76
185
|
}
|
|
186
|
+
// Path-convention layer (additive): anything under a guaranteed-content-hashed
|
|
187
|
+
// framework build dir is immutable regardless of filename. The extension gate above
|
|
188
|
+
// still applies, so an `index.html` accidentally under `_next/static/` is never pinned.
|
|
189
|
+
if (isUnderGuaranteedImmutablePrefix(filePath)) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
// Fallback for assets OUTSIDE those dirs: the filename-entropy heuristic.
|
|
77
193
|
const stem = pathStem(filePath);
|
|
78
194
|
for (let index = 0; index < stem.length; index += 1) {
|
|
79
195
|
if (index > 0 && stem[index - 1] !== "." && stem[index - 1] !== "-") {
|