@caelo-cms/shared 0.10.48 → 0.10.49
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/ai-tools.d.ts +4 -3
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +5 -4
- package/dist/ai-tools.js.map +1 -1
- package/dist/auto-update.d.ts +124 -0
- package/dist/auto-update.d.ts.map +1 -0
- package/dist/auto-update.js +228 -0
- package/dist/auto-update.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/platform-updates.d.ts +6 -1
- package/dist/platform-updates.d.ts.map +1 -1
- package/dist/platform-updates.js +17 -2
- package/dist/platform-updates.js.map +1 -1
- package/dist/release-manifest.d.ts +99 -0
- package/dist/release-manifest.d.ts.map +1 -0
- package/dist/release-manifest.js +161 -0
- package/dist/release-manifest.js.map +1 -0
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +4 -1
- package/dist/seo.js.map +1 -1
- package/dist/static-cache-policy.d.ts +7 -4
- package/dist/static-cache-policy.d.ts.map +1 -1
- package/dist/static-cache-policy.js +7 -4
- package/dist/static-cache-policy.js.map +1 -1
- package/dist/subagents.d.ts +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/ai-tools.ts +5 -4
- package/src/auto-update.test.ts +321 -0
- package/src/auto-update.ts +315 -0
- package/src/index.ts +2 -0
- package/src/platform-updates.test.ts +5 -0
- package/src/platform-updates.ts +24 -1
- package/src/release-manifest.ts +212 -0
- package/src/seo.ts +4 -1
- package/src/static-cache-policy.test.ts +6 -2
- package/src/static-cache-policy.ts +7 -4
- package/src/version.ts +1 -1
package/src/platform-updates.ts
CHANGED
|
@@ -163,7 +163,15 @@ export const updatePolicySchema = z
|
|
|
163
163
|
windowStart: z.string().regex(WALL_CLOCK, "expected HH:MM").nullable(),
|
|
164
164
|
windowEnd: z.string().regex(WALL_CLOCK, "expected HH:MM").nullable(),
|
|
165
165
|
windowTimezone: z.string().min(1).max(64).refine(isKnownTimeZone, "unknown time zone"),
|
|
166
|
+
/** Hours after publication before a non-security release installs automatically. */
|
|
166
167
|
soakHours: z.number().int().min(0).max(720),
|
|
168
|
+
/**
|
|
169
|
+
* The same for a release whose signed manifest names a published GitHub
|
|
170
|
+
* Security Advisory (auto-update.ts `isSecurityRelease`). Shorter by
|
|
171
|
+
* default (2 h vs 24 h): the fix is public, so every hour unpatched is
|
|
172
|
+
* exposure, while two hours still let a broken release be pulled first.
|
|
173
|
+
*/
|
|
174
|
+
securitySoakHours: z.number().int().min(0).max(720),
|
|
167
175
|
})
|
|
168
176
|
.strict()
|
|
169
177
|
.refine((p) => (p.windowStart === null) === (p.windowEnd === null), {
|
|
@@ -171,6 +179,9 @@ export const updatePolicySchema = z
|
|
|
171
179
|
})
|
|
172
180
|
.refine((p) => p.mode !== "auto" || p.channel === "stable", {
|
|
173
181
|
message: "automatic installs follow the stable channel only",
|
|
182
|
+
})
|
|
183
|
+
.refine((p) => p.securitySoakHours <= p.soakHours, {
|
|
184
|
+
message: "security fixes cannot wait longer than other releases",
|
|
174
185
|
});
|
|
175
186
|
export type UpdatePolicy = z.infer<typeof updatePolicySchema>;
|
|
176
187
|
|
|
@@ -179,6 +190,10 @@ export interface StoredUpdatePolicy extends UpdatePolicy {
|
|
|
179
190
|
readonly paused: boolean;
|
|
180
191
|
readonly pausedReason: string | null;
|
|
181
192
|
readonly pausedAt: string | null;
|
|
193
|
+
/** End of the Owner's pause; null = until resumed (or not paused). */
|
|
194
|
+
readonly pausedUntil: string | null;
|
|
195
|
+
/** True when the Owner paused; false when the upgrader paused after a failure. */
|
|
196
|
+
readonly pausedByOwner: boolean;
|
|
182
197
|
readonly updatedAt: string;
|
|
183
198
|
}
|
|
184
199
|
|
|
@@ -238,6 +253,7 @@ export function autoInstallRefusal(
|
|
|
238
253
|
| "autoPatch"
|
|
239
254
|
| "autoMinor"
|
|
240
255
|
| "paused"
|
|
256
|
+
| "pausedUntil"
|
|
241
257
|
| "windowStart"
|
|
242
258
|
| "windowEnd"
|
|
243
259
|
| "windowTimezone"
|
|
@@ -246,7 +262,14 @@ export function autoInstallRefusal(
|
|
|
246
262
|
at: Date,
|
|
247
263
|
): string | null {
|
|
248
264
|
if (policy.mode !== "auto") return "automatic installs are switched off in the update policy";
|
|
249
|
-
|
|
265
|
+
// An Owner pause with an end that has passed no longer holds (the
|
|
266
|
+
// scheduler lifts it on its next tick; this keeps the job consistent).
|
|
267
|
+
if (
|
|
268
|
+
policy.paused &&
|
|
269
|
+
(policy.pausedUntil === null || Date.parse(policy.pausedUntil) > at.getTime())
|
|
270
|
+
) {
|
|
271
|
+
return "automatic installs are paused";
|
|
272
|
+
}
|
|
250
273
|
if (candidate.channel !== "stable") return "only stable releases install automatically";
|
|
251
274
|
if (candidate.contractMigrations) {
|
|
252
275
|
return "the release carries destructive database changes; the Owner installs it by hand";
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The release manifest's shape and where it lives (epic #696) — shared by
|
|
5
|
+
* the provisioning CLI / upgrader job, which verify its Sigstore signature
|
|
6
|
+
* with cosign before trusting it (provisioning release-manifest.ts), and
|
|
7
|
+
* the admin's auto-update scheduler, which only *reads* it to decide when
|
|
8
|
+
* to queue an install (the admin image ships no cosign; the job verifies).
|
|
9
|
+
*
|
|
10
|
+
* The artifact is one OCI manifest with two layers — the JSON manifest and
|
|
11
|
+
* its Sigstore bundle — pushed by `release.yml` on the release tag.
|
|
12
|
+
* {@link fetchReleaseArtifact} fetches both blobs and checks each against
|
|
13
|
+
* its content digest; it does NOT check the signature.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
/** Release channels. Pre-release versions name theirs (`0.11.0-rc.1` → `rc`). */
|
|
19
|
+
export const RELEASE_CHANNELS = ["stable", "rc", "beta", "alpha"] as const;
|
|
20
|
+
export type ReleaseChannel = (typeof RELEASE_CHANNELS)[number];
|
|
21
|
+
|
|
22
|
+
/** The services a release ships images for. */
|
|
23
|
+
export const RELEASE_IMAGES = ["admin", "gateway", "ops"] as const;
|
|
24
|
+
export type ReleaseImage = (typeof RELEASE_IMAGES)[number];
|
|
25
|
+
|
|
26
|
+
/** A `sha256:<64 hex>` image / blob digest. */
|
|
27
|
+
export const IMAGE_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
28
|
+
|
|
29
|
+
const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
30
|
+
const GHSA_ID = /^GHSA(-[23456789cfghjmpqrvwx]{4}){3}$/;
|
|
31
|
+
|
|
32
|
+
const digest = z.string().regex(IMAGE_DIGEST, "expected sha256:<64 hex>");
|
|
33
|
+
|
|
34
|
+
/** Zod schema of `release-manifest.json` (manifestVersion 1). */
|
|
35
|
+
export const ReleaseManifestSchema = z.object({
|
|
36
|
+
manifestVersion: z.literal(1),
|
|
37
|
+
version: z.string().regex(SEMVER, "expected a SemVer version without a leading v"),
|
|
38
|
+
channel: z.enum(RELEASE_CHANNELS),
|
|
39
|
+
digests: z.object({ admin: digest, gateway: digest, ops: digest }),
|
|
40
|
+
minFromVersion: z.string().regex(SEMVER),
|
|
41
|
+
infraContractVersion: z.number().int().positive(),
|
|
42
|
+
migrations: z.object({
|
|
43
|
+
/** True when the release carries destructive (contract) migrations. */
|
|
44
|
+
contract: z.boolean(),
|
|
45
|
+
/**
|
|
46
|
+
* Newest release up to and including this one that carried contract
|
|
47
|
+
* migrations (null: none known). An install below it that jumps
|
|
48
|
+
* straight here would run those destructive steps while its old image
|
|
49
|
+
* still serves — so it is manual too, even when `contract` is false.
|
|
50
|
+
*/
|
|
51
|
+
lastContractVersion: z.string().regex(SEMVER).nullable(),
|
|
52
|
+
/** Newest cms_admin migration in the release, e.g. `0262_tool_approval_branch`. */
|
|
53
|
+
head: z.string().min(1),
|
|
54
|
+
}),
|
|
55
|
+
/** Set only via the release-cut input, backed by published GitHub Security Advisories. */
|
|
56
|
+
security: z.boolean(),
|
|
57
|
+
advisories: z.array(z.string().regex(GHSA_ID, "expected a GHSA id")),
|
|
58
|
+
});
|
|
59
|
+
export type ReleaseManifest = z.infer<typeof ReleaseManifestSchema>;
|
|
60
|
+
|
|
61
|
+
/** Registry the manifests are read from — the public AR mirror Cloud Run pulls from. */
|
|
62
|
+
export const RELEASE_MANIFEST_REGISTRY = "europe-west1-docker.pkg.dev";
|
|
63
|
+
/** Repository path of the manifest artifact inside {@link RELEASE_MANIFEST_REGISTRY}. */
|
|
64
|
+
export const RELEASE_MANIFEST_REPOSITORY = "caelo-website/caelo-cms-images/release-manifest";
|
|
65
|
+
|
|
66
|
+
/** OCI artifact type + layer media types of the pushed manifest. */
|
|
67
|
+
export const RELEASE_MANIFEST_ARTIFACT_TYPE = "application/vnd.caelo.release-manifest.v1";
|
|
68
|
+
export const RELEASE_MANIFEST_MEDIA_TYPE = "application/vnd.caelo.release-manifest.v1+json";
|
|
69
|
+
export const SIGSTORE_BUNDLE_MEDIA_TYPE = "application/vnd.dev.sigstore.bundle.v0.3+json";
|
|
70
|
+
|
|
71
|
+
/** Why fetching the artifact failed. */
|
|
72
|
+
export type ReleaseArtifactErrorCode = "registry" | "artifact-shape" | "blob-digest";
|
|
73
|
+
|
|
74
|
+
export type ReleaseArtifactResult =
|
|
75
|
+
| { readonly ok: true; readonly manifestBytes: Uint8Array; readonly bundleBytes: Uint8Array }
|
|
76
|
+
| {
|
|
77
|
+
readonly ok: false;
|
|
78
|
+
readonly error: { readonly code: ReleaseArtifactErrorCode; readonly message: string };
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Where and how {@link fetchReleaseArtifact} reaches the registry. */
|
|
82
|
+
export interface ReleaseArtifactDeps {
|
|
83
|
+
readonly fetch: (url: string, init?: RequestInit) => Promise<Response>;
|
|
84
|
+
/** Registry host + repository; default to the fixed public mirror. */
|
|
85
|
+
readonly registry?: string;
|
|
86
|
+
readonly repository?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const OciManifestSchema = z.object({
|
|
90
|
+
artifactType: z.string().optional(),
|
|
91
|
+
layers: z.array(z.object({ mediaType: z.string(), digest: z.string().regex(IMAGE_DIGEST) })),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
async function sha256Digest(bytes: Uint8Array<ArrayBuffer>): Promise<string> {
|
|
95
|
+
const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
96
|
+
return `sha256:${[...hash].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const fail = (code: ReleaseArtifactErrorCode, message: string): ReleaseArtifactResult => ({
|
|
100
|
+
ok: false,
|
|
101
|
+
error: { code, message },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fetch the release manifest artifact of `version` and both its layers,
|
|
106
|
+
* each checked against its content digest. Verifies NO signature: callers
|
|
107
|
+
* that act on the manifest verify the bundle (provisioning
|
|
108
|
+
* fetchVerifiedReleaseManifest); the admin's scheduler only plans with it.
|
|
109
|
+
*
|
|
110
|
+
* @param version - SemVer without a leading `v` (the caller validates it).
|
|
111
|
+
*/
|
|
112
|
+
export async function fetchReleaseArtifact(
|
|
113
|
+
version: string,
|
|
114
|
+
deps: ReleaseArtifactDeps,
|
|
115
|
+
): Promise<ReleaseArtifactResult> {
|
|
116
|
+
const base = `https://${deps.registry ?? RELEASE_MANIFEST_REGISTRY}/v2/${deps.repository ?? RELEASE_MANIFEST_REPOSITORY}`;
|
|
117
|
+
let artifact: z.infer<typeof OciManifestSchema>;
|
|
118
|
+
try {
|
|
119
|
+
const res = await deps.fetch(`${base}/manifests/${version}`, {
|
|
120
|
+
headers: { Accept: "application/vnd.oci.image.manifest.v1+json" },
|
|
121
|
+
});
|
|
122
|
+
if (!res.ok) {
|
|
123
|
+
return fail(
|
|
124
|
+
"registry",
|
|
125
|
+
`no release manifest for v${version} at ${base} (HTTP ${res.status}). Releases before the signed manifest (epic #696) have none; otherwise check the version exists.`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const parsed = OciManifestSchema.safeParse(await res.json());
|
|
129
|
+
if (!parsed.success) {
|
|
130
|
+
return fail("artifact-shape", `not an OCI manifest: ${parsed.error.message}`);
|
|
131
|
+
}
|
|
132
|
+
artifact = parsed.data;
|
|
133
|
+
} catch (e) {
|
|
134
|
+
return fail("registry", `registry unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (artifact.artifactType !== RELEASE_MANIFEST_ARTIFACT_TYPE) {
|
|
138
|
+
return fail(
|
|
139
|
+
"artifact-shape",
|
|
140
|
+
`artifactType is ${artifact.artifactType ?? "(none)"}, expected ${RELEASE_MANIFEST_ARTIFACT_TYPE}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const manifestLayers = artifact.layers.filter((l) => l.mediaType === RELEASE_MANIFEST_MEDIA_TYPE);
|
|
144
|
+
const bundleLayers = artifact.layers.filter((l) => l.mediaType === SIGSTORE_BUNDLE_MEDIA_TYPE);
|
|
145
|
+
const [manifestLayer] = manifestLayers;
|
|
146
|
+
const [bundleLayer] = bundleLayers;
|
|
147
|
+
if (manifestLayers.length !== 1 || bundleLayers.length !== 1 || !manifestLayer || !bundleLayer) {
|
|
148
|
+
return fail(
|
|
149
|
+
"artifact-shape",
|
|
150
|
+
"the artifact must hold exactly one release-manifest layer and one Sigstore bundle layer",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const blob = async (layerDigest: string): Promise<Uint8Array | ReleaseArtifactResult> => {
|
|
155
|
+
const res = await deps.fetch(`${base}/blobs/${layerDigest}`);
|
|
156
|
+
if (!res.ok) return fail("registry", `blob ${layerDigest}: HTTP ${res.status}`);
|
|
157
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
158
|
+
const actual = await sha256Digest(bytes);
|
|
159
|
+
if (actual !== layerDigest) {
|
|
160
|
+
return fail(
|
|
161
|
+
"blob-digest",
|
|
162
|
+
`blob content does not match its digest (${layerDigest} expected, ${actual} served) — the registry or a proxy altered it`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return bytes;
|
|
166
|
+
};
|
|
167
|
+
try {
|
|
168
|
+
const manifestBytes = await blob(manifestLayer.digest);
|
|
169
|
+
if (!(manifestBytes instanceof Uint8Array)) return manifestBytes;
|
|
170
|
+
const bundleBytes = await blob(bundleLayer.digest);
|
|
171
|
+
if (!(bundleBytes instanceof Uint8Array)) return bundleBytes;
|
|
172
|
+
return { ok: true, manifestBytes, bundleBytes };
|
|
173
|
+
} catch (e) {
|
|
174
|
+
return fail("registry", `registry unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Parse manifest bytes against {@link ReleaseManifestSchema} and check they
|
|
180
|
+
* describe `version`.
|
|
181
|
+
*
|
|
182
|
+
* @returns the manifest, or a plain-words problem
|
|
183
|
+
*/
|
|
184
|
+
export function parseReleaseManifest(
|
|
185
|
+
bytes: Uint8Array,
|
|
186
|
+
version: string,
|
|
187
|
+
):
|
|
188
|
+
| { readonly ok: true; readonly manifest: ReleaseManifest }
|
|
189
|
+
| { readonly ok: false; readonly code: "schema" | "version-mismatch"; readonly message: string } {
|
|
190
|
+
let json: unknown;
|
|
191
|
+
try {
|
|
192
|
+
json = JSON.parse(new TextDecoder().decode(bytes));
|
|
193
|
+
} catch {
|
|
194
|
+
return { ok: false, code: "schema", message: "the manifest is not valid JSON" };
|
|
195
|
+
}
|
|
196
|
+
const parsed = ReleaseManifestSchema.safeParse(json);
|
|
197
|
+
if (!parsed.success) {
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
code: "schema",
|
|
201
|
+
message: `the manifest does not match manifestVersion 1 — this Caelo may be too old to read it: ${parsed.error.message}`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (parsed.data.version !== version) {
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
code: "version-mismatch",
|
|
208
|
+
message: `asked for v${version} but the manifest describes v${parsed.data.version}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return { ok: true, manifest: parsed.data };
|
|
212
|
+
}
|
package/src/seo.ts
CHANGED
|
@@ -44,7 +44,10 @@ export type SeoSetInput = z.infer<typeof seoSetInputSchema>;
|
|
|
44
44
|
export const seoAutofillInputSchema = z
|
|
45
45
|
.object({
|
|
46
46
|
pageId: z.string().uuid(),
|
|
47
|
-
|
|
47
|
+
// Trimmed before the length check: a whitespace-only description would
|
|
48
|
+
// stamp `autofilled_at` (the fill-once marker) while the page still
|
|
49
|
+
// ships without a description — and then refuse every later autofill.
|
|
50
|
+
metaDescription: z.string().trim().min(1).max(SEO_DESCRIPTION_HARD_MAX),
|
|
48
51
|
ogImageAssetId: z.string().uuid().nullable().optional(),
|
|
49
52
|
})
|
|
50
53
|
.strict();
|
|
@@ -8,8 +8,8 @@ const HASHED_PATHS = [
|
|
|
8
8
|
// Lighthouse report on gcp-firebase staging (v0.10.28).
|
|
9
9
|
"/_assets/fonts/inter/29ede7bd4be32ab0.woff2",
|
|
10
10
|
"/_assets/fonts/manrope/f3a06e9b32049b82.woff2",
|
|
11
|
-
`/_assets/fonts/pinned/${"a".repeat(64)}.woff2`,
|
|
12
|
-
`/_assets/fonts/pinned/${"0".repeat(64)}.
|
|
11
|
+
`/_assets/fonts/pinned/${"a".repeat(64)}.latin.0123456789ab.woff2`,
|
|
12
|
+
`/_assets/fonts/pinned/${"0".repeat(64)}.cyrillic-ext.0123456789ab.woff2`,
|
|
13
13
|
"/_caelo/plugin/consent-manager/runtime.0123456789ab.js",
|
|
14
14
|
"/_caelo/plugin/consent-manager/banner.abcdef012345.css",
|
|
15
15
|
"/_app/immutable/chunks/abc.js",
|
|
@@ -40,6 +40,10 @@ const STABLE_PATHS = [
|
|
|
40
40
|
// Pinned-font license file is id-named, not hashed.
|
|
41
41
|
"/_assets/fonts/pinned/0b1f6c2e-9d7a-4c1e-8f3a-2b6d9e0c1a4f.license.txt",
|
|
42
42
|
// Look-alikes that must not slip through.
|
|
43
|
+
// The registry source itself is never published; were a raw pinned file to
|
|
44
|
+
// appear, it must not be pinned in caches for a year.
|
|
45
|
+
`/_assets/fonts/pinned/${"0".repeat(64)}.ttf`,
|
|
46
|
+
`/_assets/fonts/pinned/${"a".repeat(64)}.latin.woff2`,
|
|
43
47
|
"/_assets/fonts/inter/not-a-hash.woff2",
|
|
44
48
|
"/_assets/fonts/inter/29ede7bd4be32ab0.woff2.html",
|
|
45
49
|
"/_caelo/plugin/consent-manager/runtime.js",
|
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
* Google Fonts faces; the name is a hash of the upstream face URL,
|
|
18
18
|
* which Google versions per file (a changed face gets a new URL,
|
|
19
19
|
* hence a new name).
|
|
20
|
-
* - `_assets/fonts/pinned/<sha256>.<
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* - `_assets/fonts/pinned/<sha256>.<slice>.<12 hex>.woff2` — WOFF2
|
|
21
|
+
* unicode-range slices derived from a registry font: the sha256 of
|
|
22
|
+
* the pinned source plus the delivery-toolchain tag
|
|
23
|
+
* (`font-service/src/delivery.ts`), which together determine the
|
|
24
|
+
* bytes. (The sibling `pinned/<font-id>.license.txt` is id-named,
|
|
25
|
+
* NOT hashed.)
|
|
23
26
|
* - `_caelo/plugin/<slug>/<stem>.<12 hex>.<js|css>` — plugin client
|
|
24
27
|
* assets, sha256 of the content in the name
|
|
25
28
|
* (`plugin-host/src/client-assets.ts`).
|
|
@@ -52,7 +55,7 @@ export const HTML_CACHE_CONTROL = "public, max-age=60, stale-while-revalidate=86
|
|
|
52
55
|
* matcher and Caddy's `path_regexp` (both RE2) and to `RegExp` here.
|
|
53
56
|
*/
|
|
54
57
|
export const CONTENT_HASHED_PATH_PATTERN =
|
|
55
|
-
"^/(?:_app/immutable/.+|_assets/fonts/[^/]+/[0-9a-f]{16
|
|
58
|
+
"^/(?:_app/immutable/.+|_assets/fonts/[^/]+/[0-9a-f]{16}\\.woff2|_assets/fonts/pinned/[0-9a-f]{64}\\.[a-z]+(?:-[a-z]+)?\\.[0-9a-f]{12}\\.woff2|_assets/[a-z0-9][a-z0-9-]*(?:/[a-z][a-z0-9-]*)?\\.[0-9a-f]{16}\\.[a-z0-9]{2,5}|_caelo/plugin/[^/]+/[^/]+\\.[0-9a-f]{12}\\.(?:js|css))$";
|
|
56
59
|
|
|
57
60
|
/** Hex length of the content hash in published media names (#667). */
|
|
58
61
|
export const MEDIA_CONTENT_HASH_HEX_LENGTH = 16;
|