veryfront 0.1.763 → 0.1.765
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/esm/deno.js +1 -1
- package/esm/src/cache/keys/builders/render.d.ts +8 -1
- package/esm/src/cache/keys/builders/render.d.ts.map +1 -1
- package/esm/src/cache/keys/builders/render.js +10 -2
- package/esm/src/html/html-shell-generator.d.ts.map +1 -1
- package/esm/src/html/html-shell-generator.js +24 -4
- package/esm/src/html/schemas/html.schema.d.ts +2 -0
- package/esm/src/html/schemas/html.schema.d.ts.map +1 -1
- package/esm/src/html/schemas/html.schema.js +1 -0
- package/esm/src/platform/adapters/fs/veryfront/adapter.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/adapter.js +31 -0
- package/esm/src/platform/adapters/veryfront-api-client/client.d.ts +31 -0
- package/esm/src/platform/adapters/veryfront-api-client/client.d.ts.map +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/client.js +21 -0
- package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts +6 -1
- package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts.map +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/operations.js +59 -1
- package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts +22 -0
- package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts.map +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.js +39 -0
- package/esm/src/platform/adapters/veryfront-api-client/schemas/index.d.ts +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/schemas/index.d.ts.map +1 -1
- package/esm/src/platform/adapters/veryfront-api-client/schemas/index.js +1 -1
- package/esm/src/proxy/asset-handler.d.ts +30 -0
- package/esm/src/proxy/asset-handler.d.ts.map +1 -0
- package/esm/src/proxy/asset-handler.js +108 -0
- package/esm/src/proxy/main.js +4 -0
- package/esm/src/release-assets/build-executor.d.ts +89 -0
- package/esm/src/release-assets/build-executor.d.ts.map +1 -0
- package/esm/src/release-assets/build-executor.js +405 -0
- package/esm/src/release-assets/constants.d.ts +35 -0
- package/esm/src/release-assets/constants.d.ts.map +1 -0
- package/esm/src/release-assets/constants.js +50 -0
- package/esm/src/release-assets/hash.d.ts +14 -0
- package/esm/src/release-assets/hash.d.ts.map +1 -0
- package/esm/src/release-assets/hash.js +22 -0
- package/esm/src/release-assets/html-consumption.d.ts +31 -0
- package/esm/src/release-assets/html-consumption.d.ts.map +1 -0
- package/esm/src/release-assets/html-consumption.js +64 -0
- package/esm/src/release-assets/manifest-cache.d.ts +70 -0
- package/esm/src/release-assets/manifest-cache.d.ts.map +1 -0
- package/esm/src/release-assets/manifest-cache.js +178 -0
- package/esm/src/release-assets/manifest-schema.d.ts +70 -0
- package/esm/src/release-assets/manifest-schema.d.ts.map +1 -0
- package/esm/src/release-assets/manifest-schema.js +126 -0
- package/esm/src/rendering/context/render-context.d.ts.map +1 -1
- package/esm/src/rendering/context/render-context.js +3 -1
- package/esm/src/rendering/orchestrator/html.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/html.js +16 -0
- package/esm/src/rendering/orchestrator/types.d.ts +2 -0
- package/esm/src/rendering/orchestrator/types.d.ts.map +1 -1
- package/esm/src/security/http/response/security-handler.d.ts.map +1 -1
- package/esm/src/security/http/response/security-handler.js +15 -10
- package/esm/src/server/context/enriched-context.d.ts.map +1 -1
- package/esm/src/server/context/enriched-context.js +2 -1
- package/esm/src/server/handlers/request/project-run-execute.handler.d.ts +5 -0
- package/esm/src/server/handlers/request/project-run-execute.handler.d.ts.map +1 -1
- package/esm/src/server/handlers/request/project-run-execute.handler.js +86 -3
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Asset Manifest — shared constants.
|
|
3
|
+
*
|
|
4
|
+
* @module release-assets/constants
|
|
5
|
+
*/
|
|
6
|
+
/** Current manifest body schema version. */
|
|
7
|
+
export declare const RELEASE_ASSET_MANIFEST_SCHEMA_VERSION: 1;
|
|
8
|
+
/** Public asset base path served on the project's own domain (proxy-owned). */
|
|
9
|
+
export declare const RELEASE_ASSET_BASE_PATH: "/_vf/assets";
|
|
10
|
+
/** Content types permitted for release assets. */
|
|
11
|
+
export declare const RELEASE_ASSET_CONTENT_TYPES: {
|
|
12
|
+
readonly js: "text/javascript";
|
|
13
|
+
readonly css: "text/css";
|
|
14
|
+
};
|
|
15
|
+
export type ReleaseAssetExtension = keyof typeof RELEASE_ASSET_CONTENT_TYPES;
|
|
16
|
+
export type ReleaseAssetContentType = (typeof RELEASE_ASSET_CONTENT_TYPES)[ReleaseAssetExtension];
|
|
17
|
+
/** Allowlist of accepted content types (upstream + upload validation). */
|
|
18
|
+
export declare const RELEASE_ASSET_CONTENT_TYPE_ALLOWLIST: readonly ReleaseAssetContentType[];
|
|
19
|
+
/** Maximum size (bytes) for a single uploaded asset (10 MB). */
|
|
20
|
+
export declare const RELEASE_ASSET_MAX_SIZE_BYTES: number;
|
|
21
|
+
/** Immutable cache max-age in seconds (1 year). */
|
|
22
|
+
export declare const RELEASE_ASSET_IMMUTABLE_MAX_AGE_SECONDS = 31536000;
|
|
23
|
+
/** Bounded upload concurrency when posting assets during a build. */
|
|
24
|
+
export declare const RELEASE_ASSET_UPLOAD_CONCURRENCY = 8;
|
|
25
|
+
/** Env flag that enables HTML manifest consumption in production (default OFF). */
|
|
26
|
+
export declare const RELEASE_ASSET_MANIFEST_ENV_FLAG = "VERYFRONT_RELEASE_ASSET_MANIFEST";
|
|
27
|
+
/** Map a 64-hex content hash + extension to its public asset URL. */
|
|
28
|
+
export declare function releaseAssetUrl(contentHash: string, extension: ReleaseAssetExtension): string;
|
|
29
|
+
/** Resolve the content type for an extension, or null if not allowed. */
|
|
30
|
+
export declare function contentTypeForExtension(extension: string): ReleaseAssetContentType | null;
|
|
31
|
+
/** True when the value is a valid allowlisted release asset content type. */
|
|
32
|
+
export declare function isAllowedReleaseAssetContentType(value: string | null | undefined): value is ReleaseAssetContentType;
|
|
33
|
+
/** Validate a content hash is exactly 64 lowercase hex characters. */
|
|
34
|
+
export declare function isValidContentHash(hash: string): boolean;
|
|
35
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/src/release-assets/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,4CAA4C;AAC5C,eAAO,MAAM,qCAAqC,EAAG,CAAU,CAAC;AAEhE,+EAA+E;AAC/E,eAAO,MAAM,uBAAuB,EAAG,aAAsB,CAAC;AAE9D,kDAAkD;AAClD,eAAO,MAAM,2BAA2B;;;CAG9B,CAAC;AAEX,MAAM,MAAM,qBAAqB,GAAG,MAAM,OAAO,2BAA2B,CAAC;AAC7E,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,2BAA2B,CAAC,CAAC,qBAAqB,CAAC,CAAC;AAElG,0EAA0E;AAC1E,eAAO,MAAM,oCAAoC,EAAE,SAAS,uBAAuB,EAGlF,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,4BAA4B,QAAmB,CAAC;AAE7D,mDAAmD;AACnD,eAAO,MAAM,uCAAuC,WAAa,CAAC;AAElE,qEAAqE;AACrE,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAElD,mFAAmF;AACnF,eAAO,MAAM,+BAA+B,qCAAqC,CAAC;AAElF,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,qBAAqB,GAAG,MAAM,CAE7F;AAED,yEAAyE;AACzE,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,GAChB,uBAAuB,GAAG,IAAI,CAIhC;AAED,6EAA6E;AAC7E,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,KAAK,IAAI,uBAAuB,CAIlC;AAED,sEAAsE;AACtE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Asset Manifest — shared constants.
|
|
3
|
+
*
|
|
4
|
+
* @module release-assets/constants
|
|
5
|
+
*/
|
|
6
|
+
/** Current manifest body schema version. */
|
|
7
|
+
export const RELEASE_ASSET_MANIFEST_SCHEMA_VERSION = 1;
|
|
8
|
+
/** Public asset base path served on the project's own domain (proxy-owned). */
|
|
9
|
+
export const RELEASE_ASSET_BASE_PATH = "/_vf/assets";
|
|
10
|
+
/** Content types permitted for release assets. */
|
|
11
|
+
export const RELEASE_ASSET_CONTENT_TYPES = {
|
|
12
|
+
js: "text/javascript",
|
|
13
|
+
css: "text/css",
|
|
14
|
+
};
|
|
15
|
+
/** Allowlist of accepted content types (upstream + upload validation). */
|
|
16
|
+
export const RELEASE_ASSET_CONTENT_TYPE_ALLOWLIST = [
|
|
17
|
+
RELEASE_ASSET_CONTENT_TYPES.js,
|
|
18
|
+
RELEASE_ASSET_CONTENT_TYPES.css,
|
|
19
|
+
];
|
|
20
|
+
/** Maximum size (bytes) for a single uploaded asset (10 MB). */
|
|
21
|
+
export const RELEASE_ASSET_MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
|
22
|
+
/** Immutable cache max-age in seconds (1 year). */
|
|
23
|
+
export const RELEASE_ASSET_IMMUTABLE_MAX_AGE_SECONDS = 31_536_000;
|
|
24
|
+
/** Bounded upload concurrency when posting assets during a build. */
|
|
25
|
+
export const RELEASE_ASSET_UPLOAD_CONCURRENCY = 8;
|
|
26
|
+
/** Env flag that enables HTML manifest consumption in production (default OFF). */
|
|
27
|
+
export const RELEASE_ASSET_MANIFEST_ENV_FLAG = "VERYFRONT_RELEASE_ASSET_MANIFEST";
|
|
28
|
+
/** Map a 64-hex content hash + extension to its public asset URL. */
|
|
29
|
+
export function releaseAssetUrl(contentHash, extension) {
|
|
30
|
+
return `${RELEASE_ASSET_BASE_PATH}/${contentHash}.${extension}`;
|
|
31
|
+
}
|
|
32
|
+
/** Resolve the content type for an extension, or null if not allowed. */
|
|
33
|
+
export function contentTypeForExtension(extension) {
|
|
34
|
+
if (extension === "js")
|
|
35
|
+
return RELEASE_ASSET_CONTENT_TYPES.js;
|
|
36
|
+
if (extension === "css")
|
|
37
|
+
return RELEASE_ASSET_CONTENT_TYPES.css;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** True when the value is a valid allowlisted release asset content type. */
|
|
41
|
+
export function isAllowedReleaseAssetContentType(value) {
|
|
42
|
+
if (!value)
|
|
43
|
+
return false;
|
|
44
|
+
const base = value.split(";")[0]?.trim();
|
|
45
|
+
return RELEASE_ASSET_CONTENT_TYPE_ALLOWLIST.some((allowed) => allowed === base);
|
|
46
|
+
}
|
|
47
|
+
/** Validate a content hash is exactly 64 lowercase hex characters. */
|
|
48
|
+
export function isValidContentHash(hash) {
|
|
49
|
+
return /^[0-9a-f]{64}$/.test(hash);
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Asset Manifest — SHA-256 helper.
|
|
3
|
+
*
|
|
4
|
+
* Produces lowercase hex SHA-256 digests of raw asset bytes using the Web
|
|
5
|
+
* Crypto API (`crypto.subtle`), matching the content-addressing contract.
|
|
6
|
+
*
|
|
7
|
+
* @module release-assets/hash
|
|
8
|
+
*/
|
|
9
|
+
import * as dntShim from "../../_dnt.shims.js";
|
|
10
|
+
/** Compute the lowercase hex SHA-256 digest of raw bytes. */
|
|
11
|
+
export declare function sha256HexBytes(bytes: dntShim.BufferSource): Promise<string>;
|
|
12
|
+
/** Compute the lowercase hex SHA-256 digest of a UTF-8 string. */
|
|
13
|
+
export declare function sha256Hex(input: string): Promise<string>;
|
|
14
|
+
//# sourceMappingURL=hash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/src/release-assets/hash.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAC;AAO/C,6DAA6D;AAC7D,wBAAsB,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAGjF;AAED,kEAAkE;AAClE,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG9D"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Asset Manifest — SHA-256 helper.
|
|
3
|
+
*
|
|
4
|
+
* Produces lowercase hex SHA-256 digests of raw asset bytes using the Web
|
|
5
|
+
* Crypto API (`crypto.subtle`), matching the content-addressing contract.
|
|
6
|
+
*
|
|
7
|
+
* @module release-assets/hash
|
|
8
|
+
*/
|
|
9
|
+
import * as dntShim from "../../_dnt.shims.js";
|
|
10
|
+
function toHex(buffer) {
|
|
11
|
+
return Array.from(new Uint8Array(buffer), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
12
|
+
}
|
|
13
|
+
/** Compute the lowercase hex SHA-256 digest of raw bytes. */
|
|
14
|
+
export async function sha256HexBytes(bytes) {
|
|
15
|
+
const digest = await dntShim.crypto.subtle.digest("SHA-256", bytes);
|
|
16
|
+
return toHex(digest);
|
|
17
|
+
}
|
|
18
|
+
/** Compute the lowercase hex SHA-256 digest of a UTF-8 string. */
|
|
19
|
+
export async function sha256Hex(input) {
|
|
20
|
+
const digest = await dntShim.crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
21
|
+
return toHex(digest);
|
|
22
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Asset Manifest — HTML consumption helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers used by the HTML shell generator to rewrite module URLs and
|
|
5
|
+
* preload hints to content-addressed `/_vf/assets/{hash}.js` URLs when a ready
|
|
6
|
+
* manifest covers the entry. Misses fall back to the existing URL (per-entry)
|
|
7
|
+
* and are counted via structured debug logs.
|
|
8
|
+
*
|
|
9
|
+
* @module release-assets/html-consumption
|
|
10
|
+
*/
|
|
11
|
+
import type { ReleaseAssetManifest } from "./manifest-schema.js";
|
|
12
|
+
/**
|
|
13
|
+
* Normalize a logical module path to the manifest's key convention.
|
|
14
|
+
*
|
|
15
|
+
* The HTML shell works with relative source paths like `pages/index.tsx` and
|
|
16
|
+
* `/_vf_modules/pages/index.js` URLs. Manifest module keys use the logical
|
|
17
|
+
* source path (e.g. `pages/index.tsx`). This strips a leading `/_vf_modules/`
|
|
18
|
+
* and any `.js` URL extension so both forms resolve.
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeManifestModuleKey(path: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a module URL through the manifest.
|
|
23
|
+
*
|
|
24
|
+
* Returns the content-addressed asset URL on a hit, or null on a miss (caller
|
|
25
|
+
* keeps the existing URL). The manifest is consulted by both the logical key
|
|
26
|
+
* and its `.js`-stripped form to tolerate either input shape.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveManifestModuleUrl(manifest: ReleaseAssetManifest, logicalPath: string): string | null;
|
|
29
|
+
/** Resolve the route closure module URLs for preload hints from the manifest. */
|
|
30
|
+
export declare function resolveManifestRoutePreloadUrls(manifest: ReleaseAssetManifest, route: string): string[];
|
|
31
|
+
//# sourceMappingURL=html-consumption.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-consumption.d.ts","sourceRoot":"","sources":["../../../src/src/release-assets/html-consumption.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAIjE;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/D;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,oBAAoB,EAC9B,WAAW,EAAE,MAAM,GAClB,MAAM,GAAG,IAAI,CAcf;AAED,iFAAiF;AACjF,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,oBAAoB,EAC9B,KAAK,EAAE,MAAM,GACZ,MAAM,EAAE,CAcV"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Asset Manifest — HTML consumption helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers used by the HTML shell generator to rewrite module URLs and
|
|
5
|
+
* preload hints to content-addressed `/_vf/assets/{hash}.js` URLs when a ready
|
|
6
|
+
* manifest covers the entry. Misses fall back to the existing URL (per-entry)
|
|
7
|
+
* and are counted via structured debug logs.
|
|
8
|
+
*
|
|
9
|
+
* @module release-assets/html-consumption
|
|
10
|
+
*/
|
|
11
|
+
import { serverLogger } from "../utils/index.js";
|
|
12
|
+
import { releaseAssetUrl } from "./constants.js";
|
|
13
|
+
const logger = serverLogger.component("release-asset-consume");
|
|
14
|
+
/**
|
|
15
|
+
* Normalize a logical module path to the manifest's key convention.
|
|
16
|
+
*
|
|
17
|
+
* The HTML shell works with relative source paths like `pages/index.tsx` and
|
|
18
|
+
* `/_vf_modules/pages/index.js` URLs. Manifest module keys use the logical
|
|
19
|
+
* source path (e.g. `pages/index.tsx`). This strips a leading `/_vf_modules/`
|
|
20
|
+
* and any `.js` URL extension so both forms resolve.
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeManifestModuleKey(path) {
|
|
23
|
+
let key = path.replace(/^\/?_vf_modules\//, "");
|
|
24
|
+
key = key.replace(/^\/+/, "");
|
|
25
|
+
return key;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a module URL through the manifest.
|
|
29
|
+
*
|
|
30
|
+
* Returns the content-addressed asset URL on a hit, or null on a miss (caller
|
|
31
|
+
* keeps the existing URL). The manifest is consulted by both the logical key
|
|
32
|
+
* and its `.js`-stripped form to tolerate either input shape.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveManifestModuleUrl(manifest, logicalPath) {
|
|
35
|
+
const key = normalizeManifestModuleKey(logicalPath);
|
|
36
|
+
const direct = manifest.modules[key];
|
|
37
|
+
if (direct)
|
|
38
|
+
return releaseAssetUrl(direct.contentHash, "js");
|
|
39
|
+
// Tolerate keys that differ only by extension (e.g. ".js" vs source ext).
|
|
40
|
+
const withoutExt = key.replace(/\.(tsx|ts|jsx|mdx|js)$/, "");
|
|
41
|
+
for (const candidateExt of [".tsx", ".ts", ".jsx", ".mdx", ".js"]) {
|
|
42
|
+
const candidate = manifest.modules[withoutExt + candidateExt];
|
|
43
|
+
if (candidate)
|
|
44
|
+
return releaseAssetUrl(candidate.contentHash, "js");
|
|
45
|
+
}
|
|
46
|
+
logger.debug("manifest module miss", { key });
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/** Resolve the route closure module URLs for preload hints from the manifest. */
|
|
50
|
+
export function resolveManifestRoutePreloadUrls(manifest, route) {
|
|
51
|
+
const entry = manifest.routes[route] ?? manifest.routes[`/${route}`] ??
|
|
52
|
+
manifest.routes[route.replace(/^\//, "")];
|
|
53
|
+
if (!entry) {
|
|
54
|
+
logger.debug("manifest route miss", { route });
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const urls = [];
|
|
58
|
+
for (const modulePath of entry.modules) {
|
|
59
|
+
const asset = manifest.modules[modulePath];
|
|
60
|
+
if (asset)
|
|
61
|
+
urls.push(releaseAssetUrl(asset.contentHash, "js"));
|
|
62
|
+
}
|
|
63
|
+
return urls;
|
|
64
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Asset Manifest — in-process consumption cache (production HTML).
|
|
3
|
+
*
|
|
4
|
+
* Fetches manifests from the project-scoped GET endpoint once per release and
|
|
5
|
+
* caches them keyed by `${releaseId}:${manifestVersion}`. Ready manifests are
|
|
6
|
+
* cached for a bounded TTL (15 min) so superseded manifests are eventually
|
|
7
|
+
* replaced; non-ready / missing results are cached for a short TTL so the
|
|
8
|
+
* common "no manifest" case stays cheap.
|
|
9
|
+
*
|
|
10
|
+
* Consumption is gated by `VERYFRONT_RELEASE_ASSET_MANIFEST=1` (default OFF).
|
|
11
|
+
* When the flag is off, `getReadyManifestForRender` always returns null so the
|
|
12
|
+
* HTML output is byte-identical to today.
|
|
13
|
+
*
|
|
14
|
+
* The HTML shell is synchronous, so reads are non-blocking: a cache miss kicks
|
|
15
|
+
* off a background fetch and returns null for the current render. Subsequent
|
|
16
|
+
* renders for the same release pick up the cached result.
|
|
17
|
+
*
|
|
18
|
+
* Multi-tenancy: each releaseId is served by the fetcher registered for that
|
|
19
|
+
* specific releaseId (the adapter that owns it). There is no cross-project
|
|
20
|
+
* token reuse. If no per-releaseId fetcher is registered, the call returns null
|
|
21
|
+
* (byte-identical JIT fallback).
|
|
22
|
+
*
|
|
23
|
+
* @module release-assets/manifest-cache
|
|
24
|
+
*/
|
|
25
|
+
import { type ReleaseAssetManifest } from "./manifest-schema.js";
|
|
26
|
+
/**
|
|
27
|
+
* Fetcher used to retrieve a manifest for a release. Registered per-releaseId
|
|
28
|
+
* by the runtime adapter that owns that release, so the correct project-scoped
|
|
29
|
+
* token is always used. Returns null when the manifest is unavailable.
|
|
30
|
+
*/
|
|
31
|
+
export interface ReleaseAssetManifestFetcher {
|
|
32
|
+
(releaseId: string): Promise<{
|
|
33
|
+
state: string;
|
|
34
|
+
manifest: ReleaseAssetManifest | null;
|
|
35
|
+
} | null>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Register a project-scoped manifest fetcher for the given releaseId.
|
|
39
|
+
*
|
|
40
|
+
* Called by the FS adapter when its content context is set to a release.
|
|
41
|
+
* Overwrites any previous registration for the same releaseId (safe — the
|
|
42
|
+
* latest adapter for a release is the authoritative owner).
|
|
43
|
+
*/
|
|
44
|
+
export declare function registerManifestFetcherForRelease(releaseId: string, fetcher: ReleaseAssetManifestFetcher): void;
|
|
45
|
+
/**
|
|
46
|
+
* Remove the manifest fetcher for the given releaseId.
|
|
47
|
+
*
|
|
48
|
+
* Called when an adapter transitions away from a release context.
|
|
49
|
+
*/
|
|
50
|
+
export declare function unregisterManifestFetcherForRelease(releaseId: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* Register a single global fetcher (for tests / simple single-project setups).
|
|
53
|
+
*
|
|
54
|
+
* In production multi-tenant mode prefer `registerManifestFetcherForRelease`.
|
|
55
|
+
* Passing `undefined` clears the global fallback.
|
|
56
|
+
*/
|
|
57
|
+
export declare function configureReleaseAssetManifestFetcher(fetcher: ReleaseAssetManifestFetcher | undefined): void;
|
|
58
|
+
/** True when production manifest consumption is enabled via env flag. */
|
|
59
|
+
export declare function isReleaseAssetManifestEnabled(): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Return a ready manifest for `releaseId` if one is cached, else null.
|
|
62
|
+
*
|
|
63
|
+
* Non-blocking: on a cache miss (or expired entry) it schedules a background
|
|
64
|
+
* fetch and returns null for the current render. Returns null immediately when
|
|
65
|
+
* the flag is off or no fetcher is registered for this releaseId.
|
|
66
|
+
*/
|
|
67
|
+
export declare function getReadyManifestForRender(releaseId: string | null | undefined): ReleaseAssetManifest | null;
|
|
68
|
+
/** Clear the cache (deployment / memory pressure / tests). */
|
|
69
|
+
export declare function clearReleaseAssetManifestCache(): void;
|
|
70
|
+
//# sourceMappingURL=manifest-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-cache.d.ts","sourceRoot":"","sources":["../../../src/src/release-assets/manifest-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAOH,OAAO,EAA6B,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAuB5F;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IAC1C,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAC1B;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAChE,CAAC;CACH;AAKD;;;;;;GAMG;AACH,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,2BAA2B,GACnC,IAAI,CAEN;AAED;;;;GAIG;AACH,wBAAgB,mCAAmC,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE3E;AAED;;;;;GAKG;AACH,wBAAgB,oCAAoC,CAClD,OAAO,EAAE,2BAA2B,GAAG,SAAS,GAC/C,IAAI,CAMN;AASD,yEAAyE;AACzE,wBAAgB,6BAA6B,IAAI,OAAO,CAEvD;AAOD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACnC,oBAAoB,GAAG,IAAI,CA+B7B;AA+CD,8DAA8D;AAC9D,wBAAgB,8BAA8B,IAAI,IAAI,CAIrD"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Asset Manifest — in-process consumption cache (production HTML).
|
|
3
|
+
*
|
|
4
|
+
* Fetches manifests from the project-scoped GET endpoint once per release and
|
|
5
|
+
* caches them keyed by `${releaseId}:${manifestVersion}`. Ready manifests are
|
|
6
|
+
* cached for a bounded TTL (15 min) so superseded manifests are eventually
|
|
7
|
+
* replaced; non-ready / missing results are cached for a short TTL so the
|
|
8
|
+
* common "no manifest" case stays cheap.
|
|
9
|
+
*
|
|
10
|
+
* Consumption is gated by `VERYFRONT_RELEASE_ASSET_MANIFEST=1` (default OFF).
|
|
11
|
+
* When the flag is off, `getReadyManifestForRender` always returns null so the
|
|
12
|
+
* HTML output is byte-identical to today.
|
|
13
|
+
*
|
|
14
|
+
* The HTML shell is synchronous, so reads are non-blocking: a cache miss kicks
|
|
15
|
+
* off a background fetch and returns null for the current render. Subsequent
|
|
16
|
+
* renders for the same release pick up the cached result.
|
|
17
|
+
*
|
|
18
|
+
* Multi-tenancy: each releaseId is served by the fetcher registered for that
|
|
19
|
+
* specific releaseId (the adapter that owns it). There is no cross-project
|
|
20
|
+
* token reuse. If no per-releaseId fetcher is registered, the call returns null
|
|
21
|
+
* (byte-identical JIT fallback).
|
|
22
|
+
*
|
|
23
|
+
* @module release-assets/manifest-cache
|
|
24
|
+
*/
|
|
25
|
+
import { serverLogger } from "../utils/index.js";
|
|
26
|
+
import { LRUCache } from "../utils/lru-wrapper.js";
|
|
27
|
+
import { registerLRUCache } from "../cache/index.js";
|
|
28
|
+
import { getEnv } from "../platform/compat/process.js";
|
|
29
|
+
import { RELEASE_ASSET_MANIFEST_ENV_FLAG } from "./constants.js";
|
|
30
|
+
import { parseReleaseAssetManifest } from "./manifest-schema.js";
|
|
31
|
+
const logger = serverLogger.component("release-asset-manifest");
|
|
32
|
+
/** Bound on cached manifests (per releaseId:manifestVersion). */
|
|
33
|
+
const MAX_CACHED_MANIFESTS = 500;
|
|
34
|
+
/** Short TTL for non-ready / missing results (ms). */
|
|
35
|
+
const NON_READY_TTL_MS = 30_000;
|
|
36
|
+
/** TTL for ready manifests — long but finite so superseded entries are picked up (15 min). */
|
|
37
|
+
const READY_TTL_MS = 15 * 60 * 1000;
|
|
38
|
+
const manifestCache = new LRUCache({ maxEntries: MAX_CACHED_MANIFESTS });
|
|
39
|
+
registerLRUCache("release-asset-manifest-cache", manifestCache);
|
|
40
|
+
/** In-flight fetches, deduped per releaseId. */
|
|
41
|
+
const inFlight = new Map();
|
|
42
|
+
/** Per-releaseId fetcher registry (keyed by releaseId). */
|
|
43
|
+
const fetcherRegistry = new Map();
|
|
44
|
+
/**
|
|
45
|
+
* Register a project-scoped manifest fetcher for the given releaseId.
|
|
46
|
+
*
|
|
47
|
+
* Called by the FS adapter when its content context is set to a release.
|
|
48
|
+
* Overwrites any previous registration for the same releaseId (safe — the
|
|
49
|
+
* latest adapter for a release is the authoritative owner).
|
|
50
|
+
*/
|
|
51
|
+
export function registerManifestFetcherForRelease(releaseId, fetcher) {
|
|
52
|
+
fetcherRegistry.set(releaseId, fetcher);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Remove the manifest fetcher for the given releaseId.
|
|
56
|
+
*
|
|
57
|
+
* Called when an adapter transitions away from a release context.
|
|
58
|
+
*/
|
|
59
|
+
export function unregisterManifestFetcherForRelease(releaseId) {
|
|
60
|
+
fetcherRegistry.delete(releaseId);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Register a single global fetcher (for tests / simple single-project setups).
|
|
64
|
+
*
|
|
65
|
+
* In production multi-tenant mode prefer `registerManifestFetcherForRelease`.
|
|
66
|
+
* Passing `undefined` clears the global fallback.
|
|
67
|
+
*/
|
|
68
|
+
export function configureReleaseAssetManifestFetcher(fetcher) {
|
|
69
|
+
if (fetcher) {
|
|
70
|
+
fetcherRegistry.set("*", fetcher);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
fetcherRegistry.delete("*");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Resolve the fetcher for a releaseId: prefer per-releaseId, then global fallback. */
|
|
77
|
+
function resolveFetcher(releaseId) {
|
|
78
|
+
return fetcherRegistry.get(releaseId) ?? fetcherRegistry.get("*");
|
|
79
|
+
}
|
|
80
|
+
/** True when production manifest consumption is enabled via env flag. */
|
|
81
|
+
export function isReleaseAssetManifestEnabled() {
|
|
82
|
+
return getEnv(RELEASE_ASSET_MANIFEST_ENV_FLAG) === "1";
|
|
83
|
+
}
|
|
84
|
+
/** Build the cache key from releaseId + the latest known manifestVersion. */
|
|
85
|
+
function cacheKey(releaseId, manifestVersion) {
|
|
86
|
+
return manifestVersion !== undefined ? `${releaseId}:${manifestVersion}` : releaseId;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Return a ready manifest for `releaseId` if one is cached, else null.
|
|
90
|
+
*
|
|
91
|
+
* Non-blocking: on a cache miss (or expired entry) it schedules a background
|
|
92
|
+
* fetch and returns null for the current render. Returns null immediately when
|
|
93
|
+
* the flag is off or no fetcher is registered for this releaseId.
|
|
94
|
+
*/
|
|
95
|
+
export function getReadyManifestForRender(releaseId) {
|
|
96
|
+
if (!releaseId)
|
|
97
|
+
return null;
|
|
98
|
+
if (!isReleaseAssetManifestEnabled())
|
|
99
|
+
return null;
|
|
100
|
+
if (!resolveFetcher(releaseId))
|
|
101
|
+
return null;
|
|
102
|
+
// Look up the most recent cached entry for this release. We do a prefix scan
|
|
103
|
+
// of the LRU to find any `releaseId:*` entry; if we find a live ready one we
|
|
104
|
+
// return it without scheduling another fetch.
|
|
105
|
+
let best = null;
|
|
106
|
+
for (const [k, v] of manifestCache.entries()) {
|
|
107
|
+
if (k !== releaseId && !k.startsWith(`${releaseId}:`))
|
|
108
|
+
continue;
|
|
109
|
+
if (v.expiresAt > Date.now() && v.manifest) {
|
|
110
|
+
// Prefer the entry with the highest manifest version.
|
|
111
|
+
const existingVersion = best?.manifest?.manifestVersion ?? -1;
|
|
112
|
+
if ((v.manifest.manifestVersion ?? 0) > existingVersion) {
|
|
113
|
+
best = v;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (best)
|
|
118
|
+
return best.manifest;
|
|
119
|
+
// Also check the plain releaseId slot (non-ready / null entry).
|
|
120
|
+
const plain = manifestCache.get(releaseId);
|
|
121
|
+
if (plain && plain.expiresAt > Date.now()) {
|
|
122
|
+
// Non-ready entry still warm — return null without scheduling another fetch.
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
scheduleFetch(releaseId);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function scheduleFetch(releaseId) {
|
|
129
|
+
if (inFlight.has(releaseId))
|
|
130
|
+
return;
|
|
131
|
+
const active = resolveFetcher(releaseId);
|
|
132
|
+
if (!active)
|
|
133
|
+
return;
|
|
134
|
+
const promise = (async () => {
|
|
135
|
+
try {
|
|
136
|
+
const result = await active(releaseId);
|
|
137
|
+
if (!result) {
|
|
138
|
+
manifestCache.set(releaseId, { manifest: null, expiresAt: Date.now() + NON_READY_TTL_MS });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const manifest = result.state === "ready" && result.manifest
|
|
142
|
+
? parseReleaseAssetManifest(result.manifest)
|
|
143
|
+
: null;
|
|
144
|
+
if (manifest) {
|
|
145
|
+
const key = cacheKey(releaseId, manifest.manifestVersion);
|
|
146
|
+
manifestCache.set(key, { manifest, expiresAt: Date.now() + READY_TTL_MS });
|
|
147
|
+
logger.debug("Cached ready manifest", {
|
|
148
|
+
releaseId,
|
|
149
|
+
manifestVersion: manifest.manifestVersion,
|
|
150
|
+
ttlMs: READY_TTL_MS,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
manifestCache.set(releaseId, {
|
|
155
|
+
manifest: null,
|
|
156
|
+
expiresAt: Date.now() + NON_READY_TTL_MS,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
logger.debug("Manifest fetch failed", {
|
|
162
|
+
releaseId,
|
|
163
|
+
error: error instanceof Error ? error.message : String(error),
|
|
164
|
+
});
|
|
165
|
+
manifestCache.set(releaseId, { manifest: null, expiresAt: Date.now() + NON_READY_TTL_MS });
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
inFlight.delete(releaseId);
|
|
169
|
+
}
|
|
170
|
+
})();
|
|
171
|
+
inFlight.set(releaseId, promise);
|
|
172
|
+
}
|
|
173
|
+
/** Clear the cache (deployment / memory pressure / tests). */
|
|
174
|
+
export function clearReleaseAssetManifestCache() {
|
|
175
|
+
manifestCache.clear();
|
|
176
|
+
inFlight.clear();
|
|
177
|
+
fetcherRegistry.clear();
|
|
178
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Asset Manifest — v1 body schema, types, and validator.
|
|
3
|
+
*
|
|
4
|
+
* The manifest body is content-addressed metadata describing the transformed
|
|
5
|
+
* browser modules and compiled CSS for a release, plus the per-route closure
|
|
6
|
+
* used to drive preload hints and asset URL rewriting.
|
|
7
|
+
*
|
|
8
|
+
* Validation follows the repo's `defineSchema` convention (zod via the
|
|
9
|
+
* `SchemaValidator` extension contract). The schema is materialized lazily so
|
|
10
|
+
* core modules can import these types without pulling in the validator.
|
|
11
|
+
*
|
|
12
|
+
* @module release-assets/manifest-schema
|
|
13
|
+
*/
|
|
14
|
+
import type { InferSchema } from "../extensions/schema/index.js";
|
|
15
|
+
export declare const getReleaseAssetManifestSchema: () => import("../internal-agents/schema.js").Schema<import("../extensions/schema/schema-validator.js").InferShape<{
|
|
16
|
+
schemaVersion: import("../internal-agents/schema.js").Schema<1>;
|
|
17
|
+
projectId: import("../internal-agents/schema.js").Schema<string>;
|
|
18
|
+
releaseId: import("../internal-agents/schema.js").Schema<string>;
|
|
19
|
+
releaseVersion: import("../internal-agents/schema.js").Schema<number>;
|
|
20
|
+
manifestVersion: import("../internal-agents/schema.js").Schema<number>;
|
|
21
|
+
builderVersion: import("../internal-agents/schema.js").Schema<string>;
|
|
22
|
+
sourceContentHash: import("../internal-agents/schema.js").Schema<string>;
|
|
23
|
+
createdAt: import("../internal-agents/schema.js").Schema<string>;
|
|
24
|
+
assetBasePath: import("../internal-agents/schema.js").Schema<string>;
|
|
25
|
+
modules: import("../internal-agents/schema.js").Schema<Record<string, import("../extensions/schema/schema-validator.js").InferShape<{
|
|
26
|
+
contentHash: import("../internal-agents/schema.js").Schema<string>;
|
|
27
|
+
size: import("../internal-agents/schema.js").Schema<number>;
|
|
28
|
+
contentType: import("../internal-agents/schema.js").Schema<string>;
|
|
29
|
+
}>>>;
|
|
30
|
+
css: import("../internal-agents/schema.js").Schema<import("../extensions/schema/schema-validator.js").InferShape<{
|
|
31
|
+
contentHash: import("../internal-agents/schema.js").Schema<string>;
|
|
32
|
+
size: import("../internal-agents/schema.js").Schema<number>;
|
|
33
|
+
contentType: import("../internal-agents/schema.js").Schema<string>;
|
|
34
|
+
styleProfileHash: import("../internal-agents/schema.js").Schema<string | null>;
|
|
35
|
+
}>[]>;
|
|
36
|
+
routes: import("../internal-agents/schema.js").Schema<Record<string, import("../extensions/schema/schema-validator.js").InferShape<{
|
|
37
|
+
modules: import("../internal-agents/schema.js").Schema<string[]>;
|
|
38
|
+
css: import("../internal-agents/schema.js").Schema<string[]>;
|
|
39
|
+
}>>>;
|
|
40
|
+
dependencies: import("../internal-agents/schema.js").Schema<Record<string, import("../extensions/schema/schema-validator.js").InferShape<{
|
|
41
|
+
contentHash: import("../internal-agents/schema.js").Schema<string>;
|
|
42
|
+
size: import("../internal-agents/schema.js").Schema<number>;
|
|
43
|
+
contentType: import("../internal-agents/schema.js").Schema<string>;
|
|
44
|
+
}>>>;
|
|
45
|
+
fallback: import("../internal-agents/schema.js").Schema<import("../extensions/schema/schema-validator.js").InferShape<{
|
|
46
|
+
mode: import("../internal-agents/schema.js").Schema<"jit">;
|
|
47
|
+
gaps: import("../internal-agents/schema.js").Schema<string[]>;
|
|
48
|
+
}>>;
|
|
49
|
+
}>>;
|
|
50
|
+
export type ReleaseAssetManifest = InferSchema<ReturnType<typeof getReleaseAssetManifestSchema>>;
|
|
51
|
+
export type ReleaseAssetEntry = ReleaseAssetManifest["modules"][string];
|
|
52
|
+
export type ReleaseAssetCssEntry = ReleaseAssetManifest["css"][number];
|
|
53
|
+
export type ReleaseAssetRouteEntry = ReleaseAssetManifest["routes"][string];
|
|
54
|
+
/** Manifest lifecycle states (DB-owned; mirrored here for runtime checks). */
|
|
55
|
+
export type ReleaseAssetManifestState = "queued" | "building" | "partial" | "ready" | "failed" | "superseded";
|
|
56
|
+
/** Response shape for the GET asset-manifest endpoint. */
|
|
57
|
+
export interface ReleaseAssetManifestResponse {
|
|
58
|
+
state: ReleaseAssetManifestState;
|
|
59
|
+
manifest_version: number;
|
|
60
|
+
manifest: ReleaseAssetManifest | null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Hand-rolled structural validator.
|
|
64
|
+
*
|
|
65
|
+
* Used on consumption paths (HTML/proxy) where the `SchemaValidator` extension
|
|
66
|
+
* may not be registered. Returns the typed manifest on success, or null. Does
|
|
67
|
+
* not throw — consumption is always best-effort with a JIT fallback.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseReleaseAssetManifest(value: unknown): ReleaseAssetManifest | null;
|
|
70
|
+
//# sourceMappingURL=manifest-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-schema.d.ts","sourceRoot":"","sources":["../../../src/src/release-assets/manifest-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAmB,MAAM,+BAA+B,CAAC;AAkClF,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmBzC,CAAC;AAMF,MAAM,MAAM,oBAAoB,GAAG,WAAW,CAC5C,UAAU,CAAC,OAAO,6BAA6B,CAAC,CACjD,CAAC;AACF,MAAM,MAAM,iBAAiB,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AACxE,MAAM,MAAM,oBAAoB,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;AACvE,MAAM,MAAM,sBAAsB,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5E,8EAA8E;AAC9E,MAAM,MAAM,yBAAyB,GACjC,QAAQ,GACR,UAAU,GACV,SAAS,GACT,OAAO,GACP,QAAQ,GACR,YAAY,CAAC;AAEjB,0DAA0D;AAC1D,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,yBAAyB,CAAC;IACjC,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;CACvC;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,GAAG,IAAI,CAuBrF"}
|