@caelo-cms/shared 0.10.48 → 0.10.50
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 +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/platform-updates.d.ts +13 -7
- package/dist/platform-updates.d.ts.map +1 -1
- package/dist/platform-updates.js +24 -8
- package/dist/platform-updates.js.map +1 -1
- package/dist/redirect-emit-caddy.d.ts +44 -0
- package/dist/redirect-emit-caddy.d.ts.map +1 -0
- package/dist/redirect-emit-caddy.js +76 -0
- package/dist/redirect-emit-caddy.js.map +1 -0
- package/dist/redirect-emit-cloudfront.d.ts +55 -0
- package/dist/redirect-emit-cloudfront.d.ts.map +1 -0
- package/dist/redirect-emit-cloudfront.js +99 -0
- package/dist/redirect-emit-cloudfront.js.map +1 -0
- package/dist/redirect-emit-front-door.d.ts +63 -0
- package/dist/redirect-emit-front-door.d.ts.map +1 -0
- package/dist/redirect-emit-front-door.js +79 -0
- package/dist/redirect-emit-front-door.js.map +1 -0
- package/dist/redirect-emit-gcp.d.ts +89 -0
- package/dist/redirect-emit-gcp.d.ts.map +1 -0
- package/dist/redirect-emit-gcp.js +91 -0
- package/dist/redirect-emit-gcp.js.map +1 -0
- package/dist/redirect-plan.d.ts +69 -0
- package/dist/redirect-plan.d.ts.map +1 -0
- package/dist/redirect-plan.js +218 -0
- package/dist/redirect-plan.js.map +1 -0
- 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 +7 -0
- package/src/platform-updates.test.ts +5 -0
- package/src/platform-updates.ts +31 -7
- package/src/redirect-emit-caddy.ts +103 -0
- package/src/redirect-emit-cloudfront.ts +145 -0
- package/src/redirect-emit-front-door.ts +140 -0
- package/src/redirect-emit-gcp.ts +165 -0
- package/src/redirect-emit.test.ts +291 -0
- package/src/redirect-plan.test.ts +143 -0
- package/src/redirect-plan.ts +298 -0
- 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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The redirect plan as GCP URL-map path rules (provider `gcp`: GCS backend
|
|
3
|
+
* bucket behind the global external Application Load Balancer, #587).
|
|
4
|
+
*
|
|
5
|
+
* Why URL-map rules: the load balancer answers a `urlRedirect` path rule
|
|
6
|
+
* itself, at the edge, before the backend bucket is asked — no compute on
|
|
7
|
+
* the static request path (CLAUDE.md §11.B Tier 1). The alternatives do not
|
|
8
|
+
* exist on this stack: GCS has no per-object redirects, and the URL map's
|
|
9
|
+
* custom error responses can only point at a backend bucket, so a bucket
|
|
10
|
+
* 404 cannot be handed to the gateway for a `redirects.lookup`.
|
|
11
|
+
*
|
|
12
|
+
* Limits (https://cloud.google.com/load-balancing/docs/quotas, "URL maps",
|
|
13
|
+
* fixed — they cannot be raised): per path matcher at most 1,000 path
|
|
14
|
+
* rules and 1,000 predicates, where every path of a path rule is one
|
|
15
|
+
* predicate. Every host has exactly one path matcher, so the site host's
|
|
16
|
+
* redirects share that budget with its own routing rules (`/api/*`). A plan
|
|
17
|
+
* that does not fit is refused with {@link GcpUrlMapBudgetError} BEFORE
|
|
18
|
+
* anything changes — the load balancer keeps the previous rules, and the
|
|
19
|
+
* error names the counts and the ways out. Dropping redirects silently
|
|
20
|
+
* would turn linked URLs into 404s without anyone noticing. Going past the
|
|
21
|
+
* quota is tracked in #751.
|
|
22
|
+
*
|
|
23
|
+
* Rules this module owns are recognised by their `urlRedirect` (the stack's
|
|
24
|
+
* own rules route to a service), so re-applying a plan replaces exactly the
|
|
25
|
+
* previous plan's rules and keeps everything else in the path matcher.
|
|
26
|
+
*/
|
|
27
|
+
import type { RedirectPlan } from "./redirect-plan.js";
|
|
28
|
+
/** Path rules per path matcher (URL map quota, global external ALB). */
|
|
29
|
+
export declare const GCP_URL_MAP_MAX_PATH_RULES = 1000;
|
|
30
|
+
/** Predicates (paths across all path rules) per path matcher. */
|
|
31
|
+
export declare const GCP_URL_MAP_MAX_PREDICATES = 1000;
|
|
32
|
+
export type GcpRedirectResponseCode = "MOVED_PERMANENTLY_DEFAULT" | "FOUND" | "SEE_OTHER" | "TEMPORARY_REDIRECT" | "PERMANENT_REDIRECT";
|
|
33
|
+
/** A path rule of a URL map path matcher (Compute REST shape). */
|
|
34
|
+
export interface GcpPathRule {
|
|
35
|
+
readonly paths: readonly string[];
|
|
36
|
+
readonly service?: string;
|
|
37
|
+
readonly urlRedirect?: {
|
|
38
|
+
readonly pathRedirect?: string;
|
|
39
|
+
readonly redirectResponseCode?: GcpRedirectResponseCode;
|
|
40
|
+
readonly stripQuery?: boolean;
|
|
41
|
+
readonly [key: string]: unknown;
|
|
42
|
+
};
|
|
43
|
+
readonly [key: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
/** A URL map path matcher (Compute REST shape; other fields kept as-is). */
|
|
46
|
+
export interface GcpPathMatcher {
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly pathRules?: readonly GcpPathRule[];
|
|
49
|
+
readonly routeRules?: readonly unknown[];
|
|
50
|
+
readonly [key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
/** The plan does not fit the path matcher's quota; nothing was changed. */
|
|
53
|
+
export declare class GcpUrlMapBudgetError extends Error {
|
|
54
|
+
readonly redirectCount: number;
|
|
55
|
+
readonly predicatesNeeded: number;
|
|
56
|
+
readonly rulesNeeded: number;
|
|
57
|
+
constructor(redirectCount: number, predicatesNeeded: number, rulesNeeded: number);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* One `urlRedirect` path rule per planned redirect. 410 entries have no
|
|
61
|
+
* URL-map equivalent and answer the bucket's 404.
|
|
62
|
+
*
|
|
63
|
+
* @throws when a planned redirect cannot be expressed as a URL-map rule
|
|
64
|
+
* (a `*` in the from path would turn it into a wildcard; a target
|
|
65
|
+
* with a query or fragment cannot be a `pathRedirect`).
|
|
66
|
+
*/
|
|
67
|
+
export declare function gcpRedirectPathRules(plan: RedirectPlan): GcpPathRule[];
|
|
68
|
+
/** Whether a path rule is one a redirect plan wrote (see module comment). */
|
|
69
|
+
export declare function isGcpRedirectRule(rule: GcpPathRule): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The path matcher with its redirect rules replaced by the plan's. Rules
|
|
72
|
+
* that are not redirects stay first, in their order — `/api/*` keeps
|
|
73
|
+
* routing to the gateway. Path rules match the longest path, so the order
|
|
74
|
+
* between them does not change routing.
|
|
75
|
+
*
|
|
76
|
+
* @throws {GcpUrlMapBudgetError} when the result exceeds the quota.
|
|
77
|
+
*/
|
|
78
|
+
export declare function withGcpRedirects(matcher: GcpPathMatcher, plan: RedirectPlan): GcpPathMatcher;
|
|
79
|
+
/**
|
|
80
|
+
* Check a plan against the budget a path matcher with `baseRules` leaves,
|
|
81
|
+
* without a URL map at hand (Stage runs it so an over-quota redirect set
|
|
82
|
+
* is reported before Confirm-publish).
|
|
83
|
+
*
|
|
84
|
+
* @throws {GcpUrlMapBudgetError} when the plan does not fit.
|
|
85
|
+
*/
|
|
86
|
+
export declare function assertGcpRedirectBudget(plan: RedirectPlan, baseRules: readonly GcpPathRule[]): void;
|
|
87
|
+
/** The site's own routing in the `public` path matcher (stack + budget). */
|
|
88
|
+
export declare const GCP_PUBLIC_BASE_PATHS: readonly string[];
|
|
89
|
+
//# sourceMappingURL=redirect-emit-gcp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect-emit-gcp.d.ts","sourceRoot":"","sources":["../src/redirect-emit-gcp.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAmB,YAAY,EAAsB,MAAM,oBAAoB,CAAC;AAE5F,wEAAwE;AACxE,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAC/C,iEAAiE;AACjE,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAU/C,MAAM,MAAM,uBAAuB,GAC/B,2BAA2B,GAC3B,OAAO,GACP,WAAW,GACX,oBAAoB,GACpB,oBAAoB,CAAC;AAEzB,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAC/B,QAAQ,CAAC,oBAAoB,CAAC,EAAE,uBAAuB,CAAC;QACxD,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;QAC9B,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACjC,CAAC;IACF,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IACzC,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,2EAA2E;AAC3E,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,aAAa,EAAE,MAAM;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,MAAM;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM;gBAFnB,aAAa,EAAE,MAAM,EACrB,gBAAgB,EAAE,MAAM,EACxB,WAAW,EAAE,MAAM;CAO/B;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,EAAE,CAEtE;AAwBD,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAE5D;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,YAAY,GAAG,cAAc,CAc5F;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,SAAS,WAAW,EAAE,GAChC,IAAI,CAEN;AAED,4EAA4E;AAC5E,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAAe,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
/** Path rules per path matcher (URL map quota, global external ALB). */
|
|
3
|
+
export const GCP_URL_MAP_MAX_PATH_RULES = 1000;
|
|
4
|
+
/** Predicates (paths across all path rules) per path matcher. */
|
|
5
|
+
export const GCP_URL_MAP_MAX_PREDICATES = 1000;
|
|
6
|
+
/** `urlRedirect.redirectResponseCode` per stored status. */
|
|
7
|
+
const RESPONSE_CODE = {
|
|
8
|
+
301: "MOVED_PERMANENTLY_DEFAULT",
|
|
9
|
+
302: "FOUND",
|
|
10
|
+
307: "TEMPORARY_REDIRECT",
|
|
11
|
+
308: "PERMANENT_REDIRECT",
|
|
12
|
+
};
|
|
13
|
+
/** The plan does not fit the path matcher's quota; nothing was changed. */
|
|
14
|
+
export class GcpUrlMapBudgetError extends Error {
|
|
15
|
+
redirectCount;
|
|
16
|
+
predicatesNeeded;
|
|
17
|
+
rulesNeeded;
|
|
18
|
+
constructor(redirectCount, predicatesNeeded, rulesNeeded) {
|
|
19
|
+
super(`The GCP load balancer serves at most ${GCP_URL_MAP_MAX_PREDICATES} redirect paths and ${GCP_URL_MAP_MAX_PATH_RULES} rules per site (a fixed URL-map quota); this site's ${redirectCount} redirect(s) need ${predicatesNeeded} paths and ${rulesNeeded} rules, counting the site's own routing. Nothing was published and the live redirects are unchanged. Next step: delete redirects nobody links to any more (find_redirects lists them, bulk_delete_redirects removes them) — every redirect whose old URL ends in '/' or not costs two paths, a file-like one ('/old.html') costs one — then publish again. Serving larger redirect sets on this provider is tracked in https://github.com/caelo-cms/caelo-cms/issues/751.`);
|
|
20
|
+
this.redirectCount = redirectCount;
|
|
21
|
+
this.predicatesNeeded = predicatesNeeded;
|
|
22
|
+
this.rulesNeeded = rulesNeeded;
|
|
23
|
+
this.name = "GcpUrlMapBudgetError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* One `urlRedirect` path rule per planned redirect. 410 entries have no
|
|
28
|
+
* URL-map equivalent and answer the bucket's 404.
|
|
29
|
+
*
|
|
30
|
+
* @throws when a planned redirect cannot be expressed as a URL-map rule
|
|
31
|
+
* (a `*` in the from path would turn it into a wildcard; a target
|
|
32
|
+
* with a query or fragment cannot be a `pathRedirect`).
|
|
33
|
+
*/
|
|
34
|
+
export function gcpRedirectPathRules(plan) {
|
|
35
|
+
return plan.redirects.map(toPathRule);
|
|
36
|
+
}
|
|
37
|
+
function toPathRule(r) {
|
|
38
|
+
if (r.matchPaths.some((p) => p.includes("*"))) {
|
|
39
|
+
throw new Error(`redirect-emit-gcp: the redirect for ${JSON.stringify(r.fromPath)} contains '*', which a GCP URL map reads as a wildcard. Percent-encode it ('%2A') with bulk_create_redirects (upsert), then publish again.`);
|
|
40
|
+
}
|
|
41
|
+
if (!r.preserveQuery) {
|
|
42
|
+
throw new Error(`redirect-emit-gcp: the redirect ${r.fromPath} → ${r.location} targets a URL with a query string or fragment, which a GCP URL-map redirect cannot carry. Point it at the page's path with bulk_create_redirects (upsert), then publish again.`);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
paths: [...r.matchPaths],
|
|
46
|
+
urlRedirect: {
|
|
47
|
+
pathRedirect: r.location,
|
|
48
|
+
redirectResponseCode: RESPONSE_CODE[r.statusCode],
|
|
49
|
+
// The visitor's query string rides along, like on every provider.
|
|
50
|
+
stripQuery: false,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Whether a path rule is one a redirect plan wrote (see module comment). */
|
|
55
|
+
export function isGcpRedirectRule(rule) {
|
|
56
|
+
return rule.urlRedirect !== undefined && rule.service === undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The path matcher with its redirect rules replaced by the plan's. Rules
|
|
60
|
+
* that are not redirects stay first, in their order — `/api/*` keeps
|
|
61
|
+
* routing to the gateway. Path rules match the longest path, so the order
|
|
62
|
+
* between them does not change routing.
|
|
63
|
+
*
|
|
64
|
+
* @throws {GcpUrlMapBudgetError} when the result exceeds the quota.
|
|
65
|
+
*/
|
|
66
|
+
export function withGcpRedirects(matcher, plan) {
|
|
67
|
+
if (matcher.routeRules && matcher.routeRules.length > 0) {
|
|
68
|
+
throw new Error(`redirect-emit-gcp: path matcher ${JSON.stringify(matcher.name)} uses routeRules; a path matcher holds either pathRules or routeRules, and Caelo's stack writes pathRules. The URL map was changed outside the stack — restore it with the installer, then publish again.`);
|
|
69
|
+
}
|
|
70
|
+
const kept = (matcher.pathRules ?? []).filter((r) => !isGcpRedirectRule(r));
|
|
71
|
+
const redirects = gcpRedirectPathRules(plan);
|
|
72
|
+
const pathRules = [...kept, ...redirects];
|
|
73
|
+
const predicates = pathRules.reduce((n, r) => n + r.paths.length, 0);
|
|
74
|
+
if (predicates > GCP_URL_MAP_MAX_PREDICATES || pathRules.length > GCP_URL_MAP_MAX_PATH_RULES) {
|
|
75
|
+
throw new GcpUrlMapBudgetError(redirects.length, predicates, pathRules.length);
|
|
76
|
+
}
|
|
77
|
+
return { ...matcher, pathRules };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Check a plan against the budget a path matcher with `baseRules` leaves,
|
|
81
|
+
* without a URL map at hand (Stage runs it so an over-quota redirect set
|
|
82
|
+
* is reported before Confirm-publish).
|
|
83
|
+
*
|
|
84
|
+
* @throws {GcpUrlMapBudgetError} when the plan does not fit.
|
|
85
|
+
*/
|
|
86
|
+
export function assertGcpRedirectBudget(plan, baseRules) {
|
|
87
|
+
withGcpRedirects({ name: "budget-check", pathRules: baseRules }, plan);
|
|
88
|
+
}
|
|
89
|
+
/** The site's own routing in the `public` path matcher (stack + budget). */
|
|
90
|
+
export const GCP_PUBLIC_BASE_PATHS = ["/api/*"];
|
|
91
|
+
//# sourceMappingURL=redirect-emit-gcp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect-emit-gcp.js","sourceRoot":"","sources":["../src/redirect-emit-gcp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** The redirect statuses the redirects table stores (besides 410). */
|
|
2
|
+
export type RedirectStatusCode = 301 | 302 | 307 | 308;
|
|
3
|
+
/** A row of the redirects table, as the static generator dumps it. */
|
|
4
|
+
export interface RedirectTableRow {
|
|
5
|
+
readonly fromPath: string;
|
|
6
|
+
readonly toPath: string;
|
|
7
|
+
readonly statusCode: number;
|
|
8
|
+
}
|
|
9
|
+
/** How the deploy target emits page URLs (`DeployTarget.pageUrlStyle`). */
|
|
10
|
+
export type RedirectPageUrlStyle = "directory" | "no-extension";
|
|
11
|
+
/** One redirect the serving layer answers. */
|
|
12
|
+
export interface PlannedRedirect {
|
|
13
|
+
/** The stored from path without a trailing slash (`/` for the root). */
|
|
14
|
+
readonly fromPath: string;
|
|
15
|
+
/** Every request path this entry answers, exactly (`/old`, `/old/`). */
|
|
16
|
+
readonly matchPaths: readonly string[];
|
|
17
|
+
/** The Location header value (a same-site path, see module comment). */
|
|
18
|
+
readonly location: string;
|
|
19
|
+
readonly statusCode: RedirectStatusCode;
|
|
20
|
+
/** Append the request's query string to {@link location}. */
|
|
21
|
+
readonly preserveQuery: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** One path the serving layer answers with 410 Gone (where it can). */
|
|
24
|
+
export interface PlannedGone {
|
|
25
|
+
readonly fromPath: string;
|
|
26
|
+
readonly matchPaths: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
/** Everything a serving layer needs to answer the redirects table. */
|
|
29
|
+
export interface RedirectPlan {
|
|
30
|
+
readonly redirects: readonly PlannedRedirect[];
|
|
31
|
+
readonly gone: readonly PlannedGone[];
|
|
32
|
+
/** From paths left out because a build file serves them (#575). */
|
|
33
|
+
readonly shadowedByBuild: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse the generator's `_redirects` file (`<from> <to> <status>` per line,
|
|
37
|
+
* `#` comments). Throws on a malformed line — a rule silently dropped is a
|
|
38
|
+
* 404 on a URL someone links to.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* parseRedirectsFile("# generated\n/old /new 301\n")
|
|
42
|
+
* // → [{ fromPath: "/old", toPath: "/new", statusCode: 301 }]
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseRedirectsFile(text: string): RedirectTableRow[];
|
|
45
|
+
/**
|
|
46
|
+
* Serialise rows into the `_redirects` file format {@link parseRedirectsFile}
|
|
47
|
+
* reads (also the Netlify / Cloudflare Pages format).
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatRedirectsFile(rows: readonly RedirectTableRow[]): string;
|
|
50
|
+
/** Arguments of {@link buildRedirectPlan}. */
|
|
51
|
+
export interface BuildRedirectPlanArgs {
|
|
52
|
+
readonly rows: readonly RedirectTableRow[];
|
|
53
|
+
readonly pageUrlStyle: RedirectPageUrlStyle;
|
|
54
|
+
/**
|
|
55
|
+
* Every file of the build, relative to the build root without a leading
|
|
56
|
+
* slash (`de/preise/index.html`). A from path one of them serves is left
|
|
57
|
+
* out of the plan.
|
|
58
|
+
*/
|
|
59
|
+
readonly servedFiles: ReadonlySet<string>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build the redirect plan every provider serves (see the module comment for
|
|
63
|
+
* the semantics). Pure and deterministic: entries keep the rows' order.
|
|
64
|
+
* Throws with an actionable message on rows no edge layer can serve
|
|
65
|
+
* correctly — a malformed path, an unsafe target, conflicting duplicates, a
|
|
66
|
+
* loop.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildRedirectPlan(args: BuildRedirectPlanArgs): RedirectPlan;
|
|
69
|
+
//# sourceMappingURL=redirect-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect-plan.d.ts","sourceRoot":"","sources":["../src/redirect-plan.ts"],"names":[],"mappings":"AAwCA,sEAAsE;AACtE,MAAM,MAAM,kBAAkB,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEvD,sEAAsE;AACtE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,2EAA2E;AAC3E,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,cAAc,CAAC;AAEhE,8CAA8C;AAC9C,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;IACxC,6DAA6D;IAC7D,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;CACjC;AAED,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,SAAS,eAAe,EAAE,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,SAAS,WAAW,EAAE,CAAC;IACtC,mEAAmE;IACnE,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAKD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAgBnE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAM7E;AAED,8CAA8C;AAC9C,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC3C,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC3C;AAQD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,YAAY,CA+B3E"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
/**
|
|
3
|
+
* The redirect plan: what every serving layer answers for the rows of the
|
|
4
|
+
* `redirects` table (#587).
|
|
5
|
+
*
|
|
6
|
+
* Each provider serves redirects through its own edge mechanism (Caddy
|
|
7
|
+
* `map` + `redir`, a GCP URL map, Firebase Hosting `config.redirects`, a
|
|
8
|
+
* CloudFront Function with a KeyValueStore, Front Door rules). They all
|
|
9
|
+
* consume this one plan, so a redirect means the same thing on every
|
|
10
|
+
* provider:
|
|
11
|
+
*
|
|
12
|
+
* - **Exact paths.** A row matches its `from` path exactly (case-sensitive)
|
|
13
|
+
* and the same path with a trailing slash — in the directory page style
|
|
14
|
+
* `/old/` is the URL people link to. A from path whose last segment
|
|
15
|
+
* looks like a file (`/old.html`) matches only as written.
|
|
16
|
+
* - **The build wins.** A row whose from path a file of the build serves
|
|
17
|
+
* is left out (#575): a live page always beats a recorded redirect, as
|
|
18
|
+
* in the admin's 404 fallback (`redirects.lookup`). The edge layers
|
|
19
|
+
* evaluate redirects BEFORE static files, so without this a slug moved
|
|
20
|
+
* back to its old path would be shadowed (or loop with the reverse row).
|
|
21
|
+
* - **Chains collapse.** `/a → /b` plus `/b → /c` serves `/a → /c` in one
|
|
22
|
+
* hop; crawlers follow few hops and every hop costs a round-trip. A hop
|
|
23
|
+
* is followed only through rows that are themselves in the plan. The
|
|
24
|
+
* collapsed status is permanent only when every hop is, and preserves
|
|
25
|
+
* the method (307/308) only when every hop does.
|
|
26
|
+
* - **Loops fail the build.** A cycle can only ever answer "too many
|
|
27
|
+
* redirects"; the error names it.
|
|
28
|
+
* - **Location.** In the directory style a page's canonical URL ends in
|
|
29
|
+
* `/`; the Location points there directly (the host would otherwise add
|
|
30
|
+
* a second hop). The visitor's query string is carried over unless the
|
|
31
|
+
* target has its own query or fragment.
|
|
32
|
+
* - **410.** Rows with status 410 become `gone` entries. Layers that can
|
|
33
|
+
* answer a bare status (Caddy, CloudFront) send 410; the others answer
|
|
34
|
+
* the 404 a path without a file gets anyway.
|
|
35
|
+
*/
|
|
36
|
+
import { redirectTargetIssue } from "./redirect-target.js";
|
|
37
|
+
import { trimTrailingSlashes } from "./url.js";
|
|
38
|
+
const REDIRECT_STATUSES = new Set([301, 302, 307, 308]);
|
|
39
|
+
const GONE_STATUS = 410;
|
|
40
|
+
/**
|
|
41
|
+
* Parse the generator's `_redirects` file (`<from> <to> <status>` per line,
|
|
42
|
+
* `#` comments). Throws on a malformed line — a rule silently dropped is a
|
|
43
|
+
* 404 on a URL someone links to.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* parseRedirectsFile("# generated\n/old /new 301\n")
|
|
47
|
+
* // → [{ fromPath: "/old", toPath: "/new", statusCode: 301 }]
|
|
48
|
+
*/
|
|
49
|
+
export function parseRedirectsFile(text) {
|
|
50
|
+
const rows = [];
|
|
51
|
+
for (const [index, raw] of text.split("\n").entries()) {
|
|
52
|
+
const line = raw.trim();
|
|
53
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
54
|
+
continue;
|
|
55
|
+
const parts = line.split(" ");
|
|
56
|
+
const [fromPath, toPath, status] = parts;
|
|
57
|
+
const statusCode = Number(status);
|
|
58
|
+
if (parts.length !== 3 || !fromPath || !toPath || !Number.isInteger(statusCode)) {
|
|
59
|
+
throw new Error(`redirect-plan: _redirects line ${index + 1} is not "<from> <to> <status>": ${JSON.stringify(raw)}. The static generator writes this file from the redirects table — fix the row there (find_redirects / bulk_create_redirects) and rebuild.`);
|
|
60
|
+
}
|
|
61
|
+
rows.push({ fromPath, toPath, statusCode });
|
|
62
|
+
}
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Serialise rows into the `_redirects` file format {@link parseRedirectsFile}
|
|
67
|
+
* reads (also the Netlify / Cloudflare Pages format).
|
|
68
|
+
*/
|
|
69
|
+
export function formatRedirectsFile(rows) {
|
|
70
|
+
return [
|
|
71
|
+
"# Auto-generated by Caelo static-generator. Do not edit by hand.",
|
|
72
|
+
...rows.map((r) => `${r.fromPath} ${r.toPath} ${r.statusCode}`),
|
|
73
|
+
"",
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build the redirect plan every provider serves (see the module comment for
|
|
78
|
+
* the semantics). Pure and deterministic: entries keep the rows' order.
|
|
79
|
+
* Throws with an actionable message on rows no edge layer can serve
|
|
80
|
+
* correctly — a malformed path, an unsafe target, conflicting duplicates, a
|
|
81
|
+
* loop.
|
|
82
|
+
*/
|
|
83
|
+
export function buildRedirectPlan(args) {
|
|
84
|
+
const rows = normaliseRows(args.rows);
|
|
85
|
+
const shadowedByBuild = [];
|
|
86
|
+
const live = new Map();
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
if (servedByBuild(row.bare, args.servedFiles)) {
|
|
89
|
+
shadowedByBuild.push(row.bare);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
live.set(row.bare, row);
|
|
93
|
+
}
|
|
94
|
+
const redirects = [];
|
|
95
|
+
const gone = [];
|
|
96
|
+
for (const row of live.values()) {
|
|
97
|
+
const matchPaths = matchPathsFor(row.bare);
|
|
98
|
+
if (row.statusCode === GONE_STATUS) {
|
|
99
|
+
gone.push({ fromPath: row.bare, matchPaths });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const { target, statusCode } = collapseChain(row, live);
|
|
103
|
+
const location = locationFor(target, args.pageUrlStyle);
|
|
104
|
+
redirects.push({
|
|
105
|
+
fromPath: row.bare,
|
|
106
|
+
matchPaths,
|
|
107
|
+
location,
|
|
108
|
+
statusCode,
|
|
109
|
+
preserveQuery: !/[?#]/.test(location),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return { redirects, gone, shadowedByBuild };
|
|
113
|
+
}
|
|
114
|
+
function normaliseRows(rows) {
|
|
115
|
+
const byBare = new Map();
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const row of rows) {
|
|
118
|
+
const { fromPath, toPath, statusCode } = row;
|
|
119
|
+
const shown = JSON.stringify(fromPath);
|
|
120
|
+
if (!fromPath.startsWith("/") || /[\s\p{Cc}]/u.test(fromPath)) {
|
|
121
|
+
throw new Error(`redirect-plan: from path ${shown} must start with '/' and contain no whitespace or control characters. Fix the row with bulk_create_redirects (upsert) or delete it with bulk_delete_redirects, then rebuild.`);
|
|
122
|
+
}
|
|
123
|
+
if (/[?#]/.test(fromPath)) {
|
|
124
|
+
throw new Error(`redirect-plan: from path ${shown} contains '?' or '#'. Redirects match the URL path only, so this row can never match. Re-create it for the path alone (e.g. '${pathBeforeQuery(fromPath)}') or delete it with bulk_delete_redirects, then rebuild.`);
|
|
125
|
+
}
|
|
126
|
+
if (statusCode !== GONE_STATUS && !REDIRECT_STATUSES.has(statusCode)) {
|
|
127
|
+
throw new Error(`redirect-plan: the redirect for ${shown} has status ${statusCode}; the redirects table allows 301, 302, 307, 308 or 410. Fix the row and rebuild.`);
|
|
128
|
+
}
|
|
129
|
+
if (statusCode !== GONE_STATUS) {
|
|
130
|
+
const targetIssue = redirectTargetIssue(toPath);
|
|
131
|
+
if (targetIssue)
|
|
132
|
+
throw new Error(`redirect-plan: the redirect for ${shown}: ${targetIssue}`);
|
|
133
|
+
}
|
|
134
|
+
const bare = stripTrailingSlash(fromPath);
|
|
135
|
+
const normalised = { bare, toPath, statusCode };
|
|
136
|
+
const existing = byBare.get(bare);
|
|
137
|
+
if (existing) {
|
|
138
|
+
if (existing.toPath === toPath && existing.statusCode === statusCode)
|
|
139
|
+
continue;
|
|
140
|
+
throw new Error(`redirect-plan: two redirects answer ${JSON.stringify(bare)} (with and without the trailing slash) differently: → ${existing.toPath} (${existing.statusCode}) and → ${toPath} (${statusCode}). Delete one with bulk_delete_redirects, then rebuild.`);
|
|
141
|
+
}
|
|
142
|
+
byBare.set(bare, normalised);
|
|
143
|
+
out.push(normalised);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/** Follow `row`'s target through the plan's own rows to the final hop. */
|
|
148
|
+
function collapseChain(row, live) {
|
|
149
|
+
const visited = [row.bare];
|
|
150
|
+
let target = row.toPath;
|
|
151
|
+
let permanent = isPermanent(row.statusCode);
|
|
152
|
+
let preservesMethod = preservesRequestMethod(row.statusCode);
|
|
153
|
+
for (;;) {
|
|
154
|
+
// A target with its own query or fragment is a different URL than the
|
|
155
|
+
// path the next row matches; the chain ends there.
|
|
156
|
+
if (/[?#]/.test(target))
|
|
157
|
+
break;
|
|
158
|
+
const next = live.get(stripTrailingSlash(target));
|
|
159
|
+
if (!next || next.statusCode === GONE_STATUS)
|
|
160
|
+
break;
|
|
161
|
+
if (visited.includes(next.bare)) {
|
|
162
|
+
const cycle = [...visited.slice(visited.indexOf(next.bare)), next.bare].join(" → ");
|
|
163
|
+
throw new Error(`redirect-plan: the redirects ${cycle} form a loop, which browsers answer with "too many redirects". Delete the row that should no longer apply with bulk_delete_redirects (find_redirects lists them), then rebuild.`);
|
|
164
|
+
}
|
|
165
|
+
visited.push(next.bare);
|
|
166
|
+
permanent &&= isPermanent(next.statusCode);
|
|
167
|
+
preservesMethod &&= preservesRequestMethod(next.statusCode);
|
|
168
|
+
target = next.toPath;
|
|
169
|
+
}
|
|
170
|
+
return { target, statusCode: combinedStatus(permanent, preservesMethod) };
|
|
171
|
+
}
|
|
172
|
+
function isPermanent(status) {
|
|
173
|
+
return status === 301 || status === 308;
|
|
174
|
+
}
|
|
175
|
+
function preservesRequestMethod(status) {
|
|
176
|
+
return status === 307 || status === 308;
|
|
177
|
+
}
|
|
178
|
+
function combinedStatus(permanent, preservesMethod) {
|
|
179
|
+
if (permanent)
|
|
180
|
+
return preservesMethod ? 308 : 301;
|
|
181
|
+
return preservesMethod ? 307 : 302;
|
|
182
|
+
}
|
|
183
|
+
function stripTrailingSlash(path) {
|
|
184
|
+
return path.length > 1 ? trimTrailingSlashes(path) || "/" : path;
|
|
185
|
+
}
|
|
186
|
+
/** The part of a stored path before its first `?` or `#` (`/` if empty). */
|
|
187
|
+
function pathBeforeQuery(path) {
|
|
188
|
+
const cut = path.search(/[?#]/);
|
|
189
|
+
return (cut === -1 ? path : path.slice(0, cut)) || "/";
|
|
190
|
+
}
|
|
191
|
+
/** Whether the last path segment names a file (`/a/b.html`). */
|
|
192
|
+
function looksLikeFile(path) {
|
|
193
|
+
return path.slice(path.lastIndexOf("/") + 1).includes(".");
|
|
194
|
+
}
|
|
195
|
+
function matchPathsFor(bare) {
|
|
196
|
+
if (bare === "/" || looksLikeFile(bare))
|
|
197
|
+
return [bare];
|
|
198
|
+
return [bare, `${bare}/`];
|
|
199
|
+
}
|
|
200
|
+
/** Does a file of the build answer `bare`? Hosts serve `/x` from the file
|
|
201
|
+
* `x` and from the directory index `x/index.html`. */
|
|
202
|
+
function servedByBuild(bare, servedFiles) {
|
|
203
|
+
if (bare === "/")
|
|
204
|
+
return servedFiles.has("index.html");
|
|
205
|
+
const rel = bare.slice(1);
|
|
206
|
+
return servedFiles.has(rel) || servedFiles.has(`${rel}/index.html`);
|
|
207
|
+
}
|
|
208
|
+
/** In the directory style, internal page paths end in "/"; files (anything
|
|
209
|
+
* with an extension in its last segment) and URLs with a query or fragment
|
|
210
|
+
* stay as they are. */
|
|
211
|
+
function locationFor(toPath, pageUrlStyle) {
|
|
212
|
+
if (pageUrlStyle !== "directory" || toPath.endsWith("/"))
|
|
213
|
+
return toPath;
|
|
214
|
+
if (/[?#]/.test(toPath) || looksLikeFile(toPath))
|
|
215
|
+
return toPath;
|
|
216
|
+
return `${toPath}/`;
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=redirect-plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect-plan.js","sourceRoot":"","sources":["../src/redirect-plan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release manifest's shape and where it lives (epic #696) — shared by
|
|
3
|
+
* the provisioning CLI / upgrader job, which verify its Sigstore signature
|
|
4
|
+
* with cosign before trusting it (provisioning release-manifest.ts), and
|
|
5
|
+
* the admin's auto-update scheduler, which only *reads* it to decide when
|
|
6
|
+
* to queue an install (the admin image ships no cosign; the job verifies).
|
|
7
|
+
*
|
|
8
|
+
* The artifact is one OCI manifest with two layers — the JSON manifest and
|
|
9
|
+
* its Sigstore bundle — pushed by `release.yml` on the release tag.
|
|
10
|
+
* {@link fetchReleaseArtifact} fetches both blobs and checks each against
|
|
11
|
+
* its content digest; it does NOT check the signature.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
/** Release channels. Pre-release versions name theirs (`0.11.0-rc.1` → `rc`). */
|
|
15
|
+
export declare const RELEASE_CHANNELS: readonly ["stable", "rc", "beta", "alpha"];
|
|
16
|
+
export type ReleaseChannel = (typeof RELEASE_CHANNELS)[number];
|
|
17
|
+
/** The services a release ships images for. */
|
|
18
|
+
export declare const RELEASE_IMAGES: readonly ["admin", "gateway", "ops"];
|
|
19
|
+
export type ReleaseImage = (typeof RELEASE_IMAGES)[number];
|
|
20
|
+
/** A `sha256:<64 hex>` image / blob digest. */
|
|
21
|
+
export declare const IMAGE_DIGEST: RegExp;
|
|
22
|
+
/** Zod schema of `release-manifest.json` (manifestVersion 1). */
|
|
23
|
+
export declare const ReleaseManifestSchema: z.ZodObject<{
|
|
24
|
+
manifestVersion: z.ZodLiteral<1>;
|
|
25
|
+
version: z.ZodString;
|
|
26
|
+
channel: z.ZodEnum<{
|
|
27
|
+
stable: "stable";
|
|
28
|
+
rc: "rc";
|
|
29
|
+
beta: "beta";
|
|
30
|
+
alpha: "alpha";
|
|
31
|
+
}>;
|
|
32
|
+
digests: z.ZodObject<{
|
|
33
|
+
admin: z.ZodString;
|
|
34
|
+
gateway: z.ZodString;
|
|
35
|
+
ops: z.ZodString;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
minFromVersion: z.ZodString;
|
|
38
|
+
infraContractVersion: z.ZodNumber;
|
|
39
|
+
migrations: z.ZodObject<{
|
|
40
|
+
contract: z.ZodBoolean;
|
|
41
|
+
lastContractVersion: z.ZodNullable<z.ZodString>;
|
|
42
|
+
head: z.ZodString;
|
|
43
|
+
}, z.core.$strip>;
|
|
44
|
+
security: z.ZodBoolean;
|
|
45
|
+
advisories: z.ZodArray<z.ZodString>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
export type ReleaseManifest = z.infer<typeof ReleaseManifestSchema>;
|
|
48
|
+
/** Registry the manifests are read from — the public AR mirror Cloud Run pulls from. */
|
|
49
|
+
export declare const RELEASE_MANIFEST_REGISTRY = "europe-west1-docker.pkg.dev";
|
|
50
|
+
/** Repository path of the manifest artifact inside {@link RELEASE_MANIFEST_REGISTRY}. */
|
|
51
|
+
export declare const RELEASE_MANIFEST_REPOSITORY = "caelo-website/caelo-cms-images/release-manifest";
|
|
52
|
+
/** OCI artifact type + layer media types of the pushed manifest. */
|
|
53
|
+
export declare const RELEASE_MANIFEST_ARTIFACT_TYPE = "application/vnd.caelo.release-manifest.v1";
|
|
54
|
+
export declare const RELEASE_MANIFEST_MEDIA_TYPE = "application/vnd.caelo.release-manifest.v1+json";
|
|
55
|
+
export declare const SIGSTORE_BUNDLE_MEDIA_TYPE = "application/vnd.dev.sigstore.bundle.v0.3+json";
|
|
56
|
+
/** Why fetching the artifact failed. */
|
|
57
|
+
export type ReleaseArtifactErrorCode = "registry" | "artifact-shape" | "blob-digest";
|
|
58
|
+
export type ReleaseArtifactResult = {
|
|
59
|
+
readonly ok: true;
|
|
60
|
+
readonly manifestBytes: Uint8Array;
|
|
61
|
+
readonly bundleBytes: Uint8Array;
|
|
62
|
+
} | {
|
|
63
|
+
readonly ok: false;
|
|
64
|
+
readonly error: {
|
|
65
|
+
readonly code: ReleaseArtifactErrorCode;
|
|
66
|
+
readonly message: string;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/** Where and how {@link fetchReleaseArtifact} reaches the registry. */
|
|
70
|
+
export interface ReleaseArtifactDeps {
|
|
71
|
+
readonly fetch: (url: string, init?: RequestInit) => Promise<Response>;
|
|
72
|
+
/** Registry host + repository; default to the fixed public mirror. */
|
|
73
|
+
readonly registry?: string;
|
|
74
|
+
readonly repository?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fetch the release manifest artifact of `version` and both its layers,
|
|
78
|
+
* each checked against its content digest. Verifies NO signature: callers
|
|
79
|
+
* that act on the manifest verify the bundle (provisioning
|
|
80
|
+
* fetchVerifiedReleaseManifest); the admin's scheduler only plans with it.
|
|
81
|
+
*
|
|
82
|
+
* @param version - SemVer without a leading `v` (the caller validates it).
|
|
83
|
+
*/
|
|
84
|
+
export declare function fetchReleaseArtifact(version: string, deps: ReleaseArtifactDeps): Promise<ReleaseArtifactResult>;
|
|
85
|
+
/**
|
|
86
|
+
* Parse manifest bytes against {@link ReleaseManifestSchema} and check they
|
|
87
|
+
* describe `version`.
|
|
88
|
+
*
|
|
89
|
+
* @returns the manifest, or a plain-words problem
|
|
90
|
+
*/
|
|
91
|
+
export declare function parseReleaseManifest(bytes: Uint8Array, version: string): {
|
|
92
|
+
readonly ok: true;
|
|
93
|
+
readonly manifest: ReleaseManifest;
|
|
94
|
+
} | {
|
|
95
|
+
readonly ok: false;
|
|
96
|
+
readonly code: "schema" | "version-mismatch";
|
|
97
|
+
readonly message: string;
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=release-manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release-manifest.d.ts","sourceRoot":"","sources":["../src/release-manifest.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,4CAA6C,CAAC;AAC3E,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/D,+CAA+C;AAC/C,eAAO,MAAM,cAAc,sCAAuC,CAAC;AACnE,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,+CAA+C;AAC/C,eAAO,MAAM,YAAY,QAA0B,CAAC;AAOpD,iEAAiE;AACjE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;iBAuBhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,wFAAwF;AACxF,eAAO,MAAM,yBAAyB,gCAAgC,CAAC;AACvE,yFAAyF;AACzF,eAAO,MAAM,2BAA2B,oDAAoD,CAAC;AAE7F,oEAAoE;AACpE,eAAO,MAAM,8BAA8B,8CAA8C,CAAC;AAC1F,eAAO,MAAM,2BAA2B,mDAAmD,CAAC;AAC5F,eAAO,MAAM,0BAA0B,kDAAkD,CAAC;AAE1F,wCAAwC;AACxC,MAAM,MAAM,wBAAwB,GAAG,UAAU,GAAG,gBAAgB,GAAG,aAAa,CAAC;AAErF,MAAM,MAAM,qBAAqB,GAC7B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,aAAa,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAA;CAAE,GAC3F;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACvF,CAAC;AAEN,uEAAuE;AACvE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvE,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAiBD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA6DhC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,MAAM,GAEb;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,kBAAkB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAuBjG"}
|