@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.
Files changed (54) hide show
  1. package/dist/contracts/access.d.ts +4 -4
  2. package/dist/contracts/activity.d.ts +4 -4
  3. package/dist/contracts/annotations.d.ts +1 -1
  4. package/dist/contracts/api-keys.d.ts +6 -6
  5. package/dist/contracts/archives.d.ts +14 -14
  6. package/dist/contracts/billing.d.ts +21 -21
  7. package/dist/contracts/builds.d.ts +8 -8
  8. package/dist/contracts/channels.d.ts +2 -2
  9. package/dist/contracts/common.d.ts +5 -5
  10. package/dist/contracts/continuation.d.ts +66 -0
  11. package/dist/contracts/continuation.js +91 -0
  12. package/dist/contracts/deployments.d.ts +18 -18
  13. package/dist/contracts/device-auth.d.ts +6 -6
  14. package/dist/contracts/domains.d.ts +247 -247
  15. package/dist/contracts/enums.d.ts +2 -2
  16. package/dist/contracts/error-code-meta.d.ts +12 -0
  17. package/dist/contracts/error-code-meta.js +3 -0
  18. package/dist/contracts/error-codes.d.ts +1 -1
  19. package/dist/contracts/error-codes.js +3 -0
  20. package/dist/contracts/events.d.ts +12 -12
  21. package/dist/contracts/features.d.ts +17 -17
  22. package/dist/contracts/git.d.ts +3 -3
  23. package/dist/contracts/internal.d.ts +3 -3
  24. package/dist/contracts/mcp.d.ts +34 -34
  25. package/dist/contracts/me.d.ts +8 -8
  26. package/dist/contracts/operations.d.ts +2 -2
  27. package/dist/contracts/plan-policy.d.ts +4 -4
  28. package/dist/contracts/platform.d.ts +3 -3
  29. package/dist/contracts/repository-connections.d.ts +5 -5
  30. package/dist/contracts/resources.d.ts +15 -15
  31. package/dist/contracts/runtime-api.d.ts +4 -4
  32. package/dist/contracts/sites.d.ts +26 -26
  33. package/dist/contracts/spaces.d.ts +179 -89
  34. package/dist/contracts/spaces.js +57 -3
  35. package/dist/contracts/superadmin-emails.d.ts +3 -3
  36. package/dist/contracts/superadmin-queues.d.ts +58 -58
  37. package/dist/contracts/superadmin-spaces.d.ts +275 -9
  38. package/dist/contracts/superadmin-spaces.js +24 -0
  39. package/dist/contracts/superadmin-tenants.d.ts +8 -8
  40. package/dist/contracts/superadmin.d.ts +12 -12
  41. package/dist/contracts/tags.d.ts +38 -38
  42. package/dist/contracts/teams.d.ts +26 -8
  43. package/dist/contracts/teams.js +20 -0
  44. package/dist/contracts/transfers.d.ts +9 -9
  45. package/dist/contracts/variables.d.ts +14 -14
  46. package/dist/contracts/webhooks.d.ts +4 -4
  47. package/dist/contracts/zero.d.ts +8 -8
  48. package/dist/docs/agent-prose.js +8 -4
  49. package/dist/docs/agent-setup.js +3 -2
  50. package/dist/docs/error-docs.js +16 -4
  51. package/dist/utils/query-keys.d.ts +2 -2
  52. package/dist/utils/static-runtime-policy.d.ts +21 -0
  53. package/dist/utils/static-runtime-policy.js +119 -3
  54. package/package.json +2 -1
@@ -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<{
@@ -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 when available; share it for reviews.",
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. Save it only when needed to update the space later, send it as",
102
- " `Authorization: Bearer <claim-token>` with the same `spaceId`, and never print it back to the user.",
103
- "- `data.links.inspect`, `resume`, `finalize`, and `promote` — follow-up API paths.",
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",
@@ -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 — the",
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",
@@ -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: "Runtime logs are not available for this space right now.",
812
- fix: "Retry shortly; logs may lag behind serving.",
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 WP.Cloud webhook secret is not configured on this deployment.",
1552
- fix: "Set the WP.Cloud webhook secret in the control-plane environment.",
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", "team" | "space", string, string, string];
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" | "access" | "runtime"];
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
- const FINGERPRINTED_ASSET_SEGMENT = /^[A-Za-z0-9_-]{8,}$/;
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
- return (FINGERPRINTED_ASSET_SEGMENT.test(segment) &&
71
- (/\d/.test(segment) || /_/.test(segment) || (/[a-z]/.test(segment) && /[A-Z]/.test(segment))));
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] !== "-") {
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@spacefast/common",
3
- "version": "0.0.2",
3
+ "version": "0.0.5",
4
+ "license": "MIT",
4
5
  "description": "Shared TypeScript contracts and utilities for Spacefast packages.",
5
6
  "type": "module",
6
7
  "publishConfig": {