@grimoire-rs/indexer 0.1.0
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/LICENSE +201 -0
- package/README.md +48 -0
- package/dist/cli/build.d.ts +6 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +29 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/exit.d.ts +28 -0
- package/dist/cli/exit.d.ts.map +1 -0
- package/dist/cli/exit.js +33 -0
- package/dist/cli/exit.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +40 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +317 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +112 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/validate.d.ts +18 -0
- package/dist/cli/validate.d.ts.map +1 -0
- package/dist/cli/validate.js +139 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/config.d.ts +76 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +134 -0
- package/dist/config.js.map +1 -0
- package/dist/data/index.d.ts +40 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +103 -0
- package/dist/data/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +2 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +4 -0
- package/dist/integration.js.map +1 -0
- package/dist/renderer/astro/components/Catalog.d.ts +6 -0
- package/dist/renderer/astro/components/Catalog.d.ts.map +1 -0
- package/dist/renderer/astro/components/Catalog.js +73 -0
- package/dist/renderer/astro/components/Catalog.js.map +1 -0
- package/dist/renderer/astro/components/Catalog.tsx +256 -0
- package/dist/renderer/astro/components/CopyButton.astro +24 -0
- package/dist/renderer/astro/components/VersionMenu.astro +105 -0
- package/dist/renderer/astro/content.config.ts +30 -0
- package/dist/renderer/astro/layouts/Base.astro +623 -0
- package/dist/renderer/astro/lib/catalog.d.ts +7 -0
- package/dist/renderer/astro/lib/catalog.d.ts.map +1 -0
- package/dist/renderer/astro/lib/catalog.js +61 -0
- package/dist/renderer/astro/lib/catalog.js.map +1 -0
- package/dist/renderer/astro/lib/catalog.ts +69 -0
- package/dist/renderer/astro/lib/data.d.ts +3 -0
- package/dist/renderer/astro/lib/data.d.ts.map +1 -0
- package/dist/renderer/astro/lib/data.js +4 -0
- package/dist/renderer/astro/lib/data.js.map +1 -0
- package/dist/renderer/astro/lib/data.ts +12 -0
- package/dist/renderer/astro/pages/index.astro +69 -0
- package/dist/renderer/astro/pages/p/[...slug].astro +367 -0
- package/dist/renderer/index.d.ts +20 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +192 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/renderer/public/favicon.svg +4 -0
- package/dist/renderer/types.d.ts +34 -0
- package/dist/renderer/types.d.ts.map +1 -0
- package/dist/renderer/types.js +4 -0
- package/dist/renderer/types.js.map +1 -0
- package/dist/validate/adapters/files.d.ts +17 -0
- package/dist/validate/adapters/files.d.ts.map +1 -0
- package/dist/validate/adapters/files.js +50 -0
- package/dist/validate/adapters/files.js.map +1 -0
- package/dist/validate/adapters/forge.d.ts +25 -0
- package/dist/validate/adapters/forge.d.ts.map +1 -0
- package/dist/validate/adapters/forge.js +100 -0
- package/dist/validate/adapters/forge.js.map +1 -0
- package/dist/validate/adapters/http.d.ts +24 -0
- package/dist/validate/adapters/http.d.ts.map +1 -0
- package/dist/validate/adapters/http.js +75 -0
- package/dist/validate/adapters/http.js.map +1 -0
- package/dist/validate/adapters/registry.d.ts +16 -0
- package/dist/validate/adapters/registry.d.ts.map +1 -0
- package/dist/validate/adapters/registry.js +91 -0
- package/dist/validate/adapters/registry.js.map +1 -0
- package/dist/validate/core/metadata.d.ts +67 -0
- package/dist/validate/core/metadata.d.ts.map +1 -0
- package/dist/validate/core/metadata.js +149 -0
- package/dist/validate/core/metadata.js.map +1 -0
- package/dist/validate/core/ownership.d.ts +43 -0
- package/dist/validate/core/ownership.d.ts.map +1 -0
- package/dist/validate/core/ownership.js +33 -0
- package/dist/validate/core/ownership.js.map +1 -0
- package/dist/validate/core/paths.d.ts +21 -0
- package/dist/validate/core/paths.d.ts.map +1 -0
- package/dist/validate/core/paths.js +34 -0
- package/dist/validate/core/paths.js.map +1 -0
- package/dist/validate/index.d.ts +40 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +206 -0
- package/dist/validate/index.js.map +1 -0
- package/package.json +50 -0
- package/templates/README.md +48 -0
- package/templates/github-pages.yml +24 -0
- package/templates/github-validate.yml +36 -0
- package/templates/gitignore +9 -0
- package/templates/gitlab-ci.yml +13 -0
- package/templates/publish.toml +17 -0
- package/templates/reusable/gitlab-index-ci.yml +61 -0
- package/templates/reusable/index-pages.yml +80 -0
- package/templates/reusable/index-validate.yml +88 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
/**
|
|
4
|
+
* OCI registry reachability. A pointer is only eligible once the thing it
|
|
5
|
+
* points at actually exists — the index never publishes a claim it did not
|
|
6
|
+
* confirm against registry truth.
|
|
7
|
+
*
|
|
8
|
+
* The registry is an untrusted endpoint chosen by the PR author, so its replies
|
|
9
|
+
* steer nothing: the bearer-token realm it names must be HTTPS on a public DNS
|
|
10
|
+
* host, and a configured credential is sent there only when that host is the
|
|
11
|
+
* registry itself or is on the committed allowlist. (The Python this ports
|
|
12
|
+
* followed any realm and sent Basic auth to it — a metadata-endpoint SSRF and a
|
|
13
|
+
* credential leak in one.)
|
|
14
|
+
*/
|
|
15
|
+
import { isPublicDnsHost } from "../core/metadata.js";
|
|
16
|
+
import { request, USER_AGENT } from "./http.js";
|
|
17
|
+
function quoted(challenge, key) {
|
|
18
|
+
const match = new RegExp(`${key}="([^"]*)"`).exec(challenge);
|
|
19
|
+
return match?.[1] ?? null;
|
|
20
|
+
}
|
|
21
|
+
export function tokenRequest(challenge, registryHost, repository, allowlist) {
|
|
22
|
+
const realm = quoted(challenge, "realm");
|
|
23
|
+
if (realm === null)
|
|
24
|
+
return null;
|
|
25
|
+
let url;
|
|
26
|
+
try {
|
|
27
|
+
url = new URL(realm);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (url.protocol !== "https:")
|
|
33
|
+
return null;
|
|
34
|
+
if (url.username !== "" || url.password !== "")
|
|
35
|
+
return null;
|
|
36
|
+
if (!isPublicDnsHost(url.host))
|
|
37
|
+
return null;
|
|
38
|
+
url.searchParams.set("scope", `repository:${repository}:pull`);
|
|
39
|
+
const service = quoted(challenge, "service");
|
|
40
|
+
if (service !== null)
|
|
41
|
+
url.searchParams.set("service", service);
|
|
42
|
+
const sameHost = url.host.toLowerCase() === registryHost.toLowerCase();
|
|
43
|
+
const listed = allowlist?.some((entry) => entry.toLowerCase() === url.host.toLowerCase()) ??
|
|
44
|
+
false;
|
|
45
|
+
return { url: url.toString(), credentialed: sameHost || listed };
|
|
46
|
+
}
|
|
47
|
+
async function pullToken(challenge, host, repository, config) {
|
|
48
|
+
const target = tokenRequest(challenge, host, repository, config.allowlist);
|
|
49
|
+
if (target === null)
|
|
50
|
+
return null;
|
|
51
|
+
const headers = { "User-Agent": USER_AGENT };
|
|
52
|
+
if (config.auth && target.credentialed) {
|
|
53
|
+
headers.Authorization = `Basic ${Buffer.from(config.auth).toString("base64")}`;
|
|
54
|
+
}
|
|
55
|
+
const response = await request(target.url, headers);
|
|
56
|
+
if (response.status !== 200)
|
|
57
|
+
return null;
|
|
58
|
+
try {
|
|
59
|
+
const token = JSON.parse(response.body)
|
|
60
|
+
.token;
|
|
61
|
+
return typeof token === "string" && token !== "" ? token : null;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** True when `<host>/v2/<repository>/tags/list` lists at least one tag. */
|
|
68
|
+
export async function refReachable(host, repository, config = {}) {
|
|
69
|
+
const url = `https://${host}/v2/${repository}/tags/list`;
|
|
70
|
+
let response = await request(url);
|
|
71
|
+
if (response.status === 401) {
|
|
72
|
+
const challenge = response.header("WWW-Authenticate");
|
|
73
|
+
const token = challenge
|
|
74
|
+
? await pullToken(challenge, host, repository, config)
|
|
75
|
+
: null;
|
|
76
|
+
if (token !== null) {
|
|
77
|
+
response = await request(url, { Authorization: `Bearer ${token}` });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (response.status !== 200)
|
|
81
|
+
return false;
|
|
82
|
+
try {
|
|
83
|
+
const tags = JSON.parse(response.body)
|
|
84
|
+
.tags;
|
|
85
|
+
return Array.isArray(tags) && tags.length > 0;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/validate/adapters/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure checks on the submitted `metadata.json` — schema shape, package-name
|
|
3
|
+
* agreement, owner shape, and OCI-ref syntax/policy.
|
|
4
|
+
*
|
|
5
|
+
* Everything here treats its input as hostile: the document comes from the PR
|
|
6
|
+
* head and is consumed as data only. Nothing is executed, nothing is merged
|
|
7
|
+
* into an existing object, and no value reaches a shell or a file path.
|
|
8
|
+
*/
|
|
9
|
+
export declare const KINDS: readonly ["skill", "rule", "agent", "mcp", "bundle"];
|
|
10
|
+
export type Kind = (typeof KINDS)[number];
|
|
11
|
+
export interface Owner {
|
|
12
|
+
/** Numeric forge account id — the authorization key (immutable, rename-proof). */
|
|
13
|
+
id: number;
|
|
14
|
+
/** GitHub login. Display only. */
|
|
15
|
+
github?: string;
|
|
16
|
+
/** Generic login for non-GitHub forges. Display only. */
|
|
17
|
+
login?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface Metadata {
|
|
20
|
+
schema: number;
|
|
21
|
+
name: string;
|
|
22
|
+
kind: Kind;
|
|
23
|
+
ref: string;
|
|
24
|
+
description: string;
|
|
25
|
+
owner: Owner;
|
|
26
|
+
}
|
|
27
|
+
/** A metadata document larger than this is refused before it is parsed. */
|
|
28
|
+
export declare const MAX_METADATA_BYTES: number;
|
|
29
|
+
export type Parsed<T> = {
|
|
30
|
+
ok: true;
|
|
31
|
+
value: T;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
reason: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* `JSON.parse` with a reviver that refuses prototype-pollution keys anywhere in
|
|
38
|
+
* the document. Nothing downstream merges this object, so the keys are inert on
|
|
39
|
+
* their own — refusing them keeps it that way for future callers and makes the
|
|
40
|
+
* property testable.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseMetadataJson(text: string): Parsed<unknown>;
|
|
43
|
+
/** Port of `build.validate` — the schema/kind/name checks, plus type strictness. */
|
|
44
|
+
export declare function validateMetadata(value: unknown, pkg: string): Parsed<Metadata>;
|
|
45
|
+
/** The declared owner login — display only, never the authorization key. */
|
|
46
|
+
export declare function ownerLogin(owner: Owner): string;
|
|
47
|
+
export interface RegistryRef {
|
|
48
|
+
host: string;
|
|
49
|
+
repository: string;
|
|
50
|
+
}
|
|
51
|
+
/** Split `<host>/<repository>` and check both halves against their grammar. */
|
|
52
|
+
export declare function parseRef(ref: string): Parsed<RegistryRef>;
|
|
53
|
+
/**
|
|
54
|
+
* A dotted DNS name, not an IP literal and not a dotless internal name — the
|
|
55
|
+
* shape a public registry always has, and loopback/link-local never do.
|
|
56
|
+
*/
|
|
57
|
+
export declare function isPublicDnsHost(host: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Registry-host policy, evaluated *before* any network call (SSRF ordering).
|
|
60
|
+
*
|
|
61
|
+
* A committed allowlist is authoritative — an operator who lists a host has
|
|
62
|
+
* accepted it, IP literal or not. With no allowlist the check falls back to
|
|
63
|
+
* "public DNS name": no IP literals, no dotless hosts, which keeps a hostile
|
|
64
|
+
* `ref` from steering CI at loopback or a cloud metadata endpoint.
|
|
65
|
+
*/
|
|
66
|
+
export declare function registryHostReason(host: string, allowlist?: readonly string[]): string | null;
|
|
67
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../../src/validate/core/metadata.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH,eAAO,MAAM,KAAK,sDAAuD,CAAC;AAC1E,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1C,MAAM,WAAW,KAAK;IACpB,kFAAkF;IAClF,EAAE,EAAE,MAAM,CAAC;IACX,kCAAkC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,KAAK,CAAC;CACd;AAcD,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,QAAY,CAAC;AAE5C,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/E;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAe/D;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,CAoD9E;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAE/C;AASD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,+EAA+E;AAC/E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,CAYzD;AAOD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAIrD;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,GAC5B,MAAM,GAAG,IAAI,CAUf"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
/**
|
|
4
|
+
* Pure checks on the submitted `metadata.json` — schema shape, package-name
|
|
5
|
+
* agreement, owner shape, and OCI-ref syntax/policy.
|
|
6
|
+
*
|
|
7
|
+
* Everything here treats its input as hostile: the document comes from the PR
|
|
8
|
+
* head and is consumed as data only. Nothing is executed, nothing is merged
|
|
9
|
+
* into an existing object, and no value reaches a shell or a file path.
|
|
10
|
+
*/
|
|
11
|
+
export const KINDS = ["skill", "rule", "agent", "mcp", "bundle"];
|
|
12
|
+
const REQUIRED = [
|
|
13
|
+
"schema",
|
|
14
|
+
"name",
|
|
15
|
+
"kind",
|
|
16
|
+
"ref",
|
|
17
|
+
"description",
|
|
18
|
+
"owner",
|
|
19
|
+
];
|
|
20
|
+
/** Keys that turn a later merge/assign into prototype pollution. */
|
|
21
|
+
const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
22
|
+
/** A metadata document larger than this is refused before it is parsed. */
|
|
23
|
+
export const MAX_METADATA_BYTES = 64 * 1024;
|
|
24
|
+
/**
|
|
25
|
+
* `JSON.parse` with a reviver that refuses prototype-pollution keys anywhere in
|
|
26
|
+
* the document. Nothing downstream merges this object, so the keys are inert on
|
|
27
|
+
* their own — refusing them keeps it that way for future callers and makes the
|
|
28
|
+
* property testable.
|
|
29
|
+
*/
|
|
30
|
+
export function parseMetadataJson(text) {
|
|
31
|
+
if (text.length > MAX_METADATA_BYTES) {
|
|
32
|
+
return { ok: false, reason: `larger than ${MAX_METADATA_BYTES} bytes` };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const value = JSON.parse(text, (key, val) => {
|
|
36
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
37
|
+
throw new SyntaxError(`forbidden key ${JSON.stringify(key)}`);
|
|
38
|
+
}
|
|
39
|
+
return val;
|
|
40
|
+
});
|
|
41
|
+
return { ok: true, value };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return { ok: false, reason: `invalid JSON: ${error.message}` };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Port of `build.validate` — the schema/kind/name checks, plus type strictness. */
|
|
48
|
+
export function validateMetadata(value, pkg) {
|
|
49
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
50
|
+
return { ok: false, reason: "metadata must be a JSON object" };
|
|
51
|
+
}
|
|
52
|
+
const meta = value;
|
|
53
|
+
const missing = REQUIRED.filter((key) => !Object.prototype.hasOwnProperty.call(meta, key));
|
|
54
|
+
if (missing.length > 0) {
|
|
55
|
+
return { ok: false, reason: `missing keys: ${missing.join(", ")}` };
|
|
56
|
+
}
|
|
57
|
+
if (meta.schema !== 1) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
reason: `unsupported schema version ${JSON.stringify(meta.schema)}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (typeof meta.kind !== "string" ||
|
|
64
|
+
!KINDS.includes(meta.kind)) {
|
|
65
|
+
return { ok: false, reason: `kind must be one of ${KINDS.join(", ")}` };
|
|
66
|
+
}
|
|
67
|
+
if (typeof meta.name !== "string" || meta.name !== pkg) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
reason: `name ${JSON.stringify(meta.name)} != directory ${JSON.stringify(pkg)}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (typeof meta.ref !== "string" || meta.ref === "") {
|
|
74
|
+
return { ok: false, reason: "ref must be a non-empty string" };
|
|
75
|
+
}
|
|
76
|
+
if (typeof meta.description !== "string") {
|
|
77
|
+
return { ok: false, reason: "description must be a string" };
|
|
78
|
+
}
|
|
79
|
+
const owner = meta.owner;
|
|
80
|
+
if (typeof owner !== "object" || owner === null || Array.isArray(owner)) {
|
|
81
|
+
return { ok: false, reason: "owner must be an object" };
|
|
82
|
+
}
|
|
83
|
+
const fields = owner;
|
|
84
|
+
// `github` names a GitHub login; `login` is the generic key other forges use.
|
|
85
|
+
const login = fields.github ?? fields.login;
|
|
86
|
+
if (typeof login !== "string" || login === "") {
|
|
87
|
+
return { ok: false, reason: "owner needs a 'github' or 'login' string" };
|
|
88
|
+
}
|
|
89
|
+
if (typeof fields.id !== "number" || !Number.isInteger(fields.id)) {
|
|
90
|
+
return { ok: false, reason: "owner.id must be an integer account id" };
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, value: meta };
|
|
93
|
+
}
|
|
94
|
+
/** The declared owner login — display only, never the authorization key. */
|
|
95
|
+
export function ownerLogin(owner) {
|
|
96
|
+
return owner.github ?? owner.login ?? "";
|
|
97
|
+
}
|
|
98
|
+
/** Hostname with optional port. Rejects userinfo, paths, and unicode. */
|
|
99
|
+
const HOST = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*(:\d{1,5})?$/i;
|
|
100
|
+
/** OCI distribution-spec repository name grammar. */
|
|
101
|
+
const REPOSITORY = /^[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*(?:\/[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*)*$/;
|
|
102
|
+
/** Split `<host>/<repository>` and check both halves against their grammar. */
|
|
103
|
+
export function parseRef(ref) {
|
|
104
|
+
const slash = ref.indexOf("/");
|
|
105
|
+
if (slash <= 0)
|
|
106
|
+
return { ok: false, reason: `ref ${JSON.stringify(ref)} has no repository part` };
|
|
107
|
+
const host = ref.slice(0, slash);
|
|
108
|
+
const repository = ref.slice(slash + 1);
|
|
109
|
+
if (!HOST.test(host)) {
|
|
110
|
+
return { ok: false, reason: `ref ${JSON.stringify(ref)} has an invalid registry host` };
|
|
111
|
+
}
|
|
112
|
+
if (!REPOSITORY.test(repository)) {
|
|
113
|
+
return { ok: false, reason: `ref ${JSON.stringify(ref)} has an invalid repository name` };
|
|
114
|
+
}
|
|
115
|
+
return { ok: true, value: { host, repository } };
|
|
116
|
+
}
|
|
117
|
+
function stripPort(host) {
|
|
118
|
+
const colon = host.lastIndexOf(":");
|
|
119
|
+
return colon === -1 ? host : host.slice(0, colon);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A dotted DNS name, not an IP literal and not a dotless internal name — the
|
|
123
|
+
* shape a public registry always has, and loopback/link-local never do.
|
|
124
|
+
*/
|
|
125
|
+
export function isPublicDnsHost(host) {
|
|
126
|
+
const bare = host.startsWith("[") ? host : stripPort(host);
|
|
127
|
+
if (bare.startsWith("[") || /^\d{1,3}(\.\d{1,3}){3}$/.test(bare))
|
|
128
|
+
return false;
|
|
129
|
+
return bare.includes(".");
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Registry-host policy, evaluated *before* any network call (SSRF ordering).
|
|
133
|
+
*
|
|
134
|
+
* A committed allowlist is authoritative — an operator who lists a host has
|
|
135
|
+
* accepted it, IP literal or not. With no allowlist the check falls back to
|
|
136
|
+
* "public DNS name": no IP literals, no dotless hosts, which keeps a hostile
|
|
137
|
+
* `ref` from steering CI at loopback or a cloud metadata endpoint.
|
|
138
|
+
*/
|
|
139
|
+
export function registryHostReason(host, allowlist) {
|
|
140
|
+
const listed = allowlist?.some((entry) => entry.toLowerCase() === host.toLowerCase()) ?? false;
|
|
141
|
+
if (allowlist !== undefined && allowlist.length > 0) {
|
|
142
|
+
return listed ? null : `registry host ${JSON.stringify(host)} is not in the allowlist`;
|
|
143
|
+
}
|
|
144
|
+
if (!isPublicDnsHost(host)) {
|
|
145
|
+
return `registry host ${JSON.stringify(host)} must be a public DNS name`;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../../../src/validate/core/metadata.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespace-ownership decisions — pure, given facts the adapters probed.
|
|
3
|
+
*
|
|
4
|
+
* Identity is the forge account that authored the PR; authorization is the
|
|
5
|
+
* *numeric* account id. The login string is display only: ids are immutable and
|
|
6
|
+
* rename-proof, so every comparison that decides anything compares ids.
|
|
7
|
+
*/
|
|
8
|
+
import type { NamespaceIdentity } from "../adapters/forge.js";
|
|
9
|
+
/**
|
|
10
|
+
* A bot trusted to announce on someone else's behalf (first-party publish CI).
|
|
11
|
+
*
|
|
12
|
+
* The object form pins the numeric account id and scopes the namespaces the bot
|
|
13
|
+
* may touch — use it. The bare-string form is login-only: unpinned id, every
|
|
14
|
+
* namespace. It exists because config files are usually written by hand, and it
|
|
15
|
+
* is safe only where logins cannot be re-registered (GitHub app bots own their
|
|
16
|
+
* `...[bot]` suffix; humans cannot register brackets).
|
|
17
|
+
*/
|
|
18
|
+
export type TrustedBot = string | {
|
|
19
|
+
login: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
namespaces?: string[];
|
|
22
|
+
};
|
|
23
|
+
export interface BotGrant {
|
|
24
|
+
login: string;
|
|
25
|
+
/** Numeric account id the author must present. Absent = unpinned. */
|
|
26
|
+
id?: string;
|
|
27
|
+
/** Namespaces the bot may announce for; `"*"` means all. */
|
|
28
|
+
namespaces: string[];
|
|
29
|
+
}
|
|
30
|
+
export declare function findTrustedBot(bots: readonly TrustedBot[] | undefined, login: string): BotGrant | null;
|
|
31
|
+
export declare function botOwnsNamespace(grant: BotGrant, namespace: string, authorId: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Does the author own this namespace, given the identity the forge reported?
|
|
34
|
+
*
|
|
35
|
+
* A user namespace is owned only by the account it *is* — login match plus id
|
|
36
|
+
* match, because a login match alone is a rename away from being someone else.
|
|
37
|
+
* Org/group namespaces delegate to the membership probe the caller ran.
|
|
38
|
+
*/
|
|
39
|
+
export declare function namespaceOwned(namespace: string, identity: NamespaceIdentity, author: {
|
|
40
|
+
login: string;
|
|
41
|
+
id: string;
|
|
42
|
+
}, isMember: boolean): boolean;
|
|
43
|
+
//# sourceMappingURL=ownership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../../../src/validate/core/ownership.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAClB,MAAM,GACN;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE1D,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,4DAA4D;IAC5D,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,SAAS,UAAU,EAAE,GAAG,SAAS,EACvC,KAAK,EAAE,MAAM,GACZ,QAAQ,GAAG,IAAI,CASjB;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAKT;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,EACrC,QAAQ,EAAE,OAAO,GAChB,OAAO,CAST"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
export function findTrustedBot(bots, login) {
|
|
4
|
+
for (const bot of bots ?? []) {
|
|
5
|
+
const grant = typeof bot === "string"
|
|
6
|
+
? { login: bot, namespaces: ["*"] }
|
|
7
|
+
: { login: bot.login, id: bot.id, namespaces: bot.namespaces ?? ["*"] };
|
|
8
|
+
if (grant.login.toLowerCase() === login.toLowerCase())
|
|
9
|
+
return grant;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
export function botOwnsNamespace(grant, namespace, authorId) {
|
|
14
|
+
if (grant.id !== undefined && grant.id !== authorId)
|
|
15
|
+
return false;
|
|
16
|
+
return grant.namespaces.some((allowed) => allowed === "*" || allowed.toLowerCase() === namespace.toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Does the author own this namespace, given the identity the forge reported?
|
|
20
|
+
*
|
|
21
|
+
* A user namespace is owned only by the account it *is* — login match plus id
|
|
22
|
+
* match, because a login match alone is a rename away from being someone else.
|
|
23
|
+
* Org/group namespaces delegate to the membership probe the caller ran.
|
|
24
|
+
*/
|
|
25
|
+
export function namespaceOwned(namespace, identity, author, isMember) {
|
|
26
|
+
if (identity.kind === "user") {
|
|
27
|
+
return (identity.path.toLowerCase() === author.login.toLowerCase() &&
|
|
28
|
+
namespace.toLowerCase() === author.login.toLowerCase() &&
|
|
29
|
+
identity.id === author.id);
|
|
30
|
+
}
|
|
31
|
+
return isMember;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=ownership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownership.js","sourceRoot":"","sources":["../../../src/validate/core/ownership.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,sCAAsC;AAiCtC,MAAM,UAAU,cAAc,CAC5B,IAAuC,EACvC,KAAa;IAEb,KAAK,MAAM,GAAG,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;QAC7B,MAAM,KAAK,GACT,OAAO,GAAG,KAAK,QAAQ;YACrB,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,EAAE;YACnC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5E,IAAI,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,WAAW,EAAE;YAAE,OAAO,KAAK,CAAC;IACtE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,KAAe,EACf,SAAiB,EACjB,QAAgB;IAEhB,IAAI,KAAK,CAAC,EAAE,KAAK,SAAS,IAAI,KAAK,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAClE,OAAO,KAAK,CAAC,UAAU,CAAC,IAAI,CAC1B,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,KAAK,GAAG,IAAI,OAAO,CAAC,WAAW,EAAE,KAAK,SAAS,CAAC,WAAW,EAAE,CAClF,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,SAAiB,EACjB,QAA2B,EAC3B,MAAqC,EACrC,QAAiB;IAEjB,IAAI,QAAQ,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC7B,OAAO,CACL,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE;YAC1D,SAAS,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE;YACtD,QAAQ,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAC1B,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Changed-path gate — the first and cheapest filter on an untrusted PR.
|
|
3
|
+
*
|
|
4
|
+
* Every changed path must be `index/<host>/<namespace...>/<pkg>/metadata.json`.
|
|
5
|
+
* Anything else means manual review. Path strings arrive from the forge API and
|
|
6
|
+
* are attacker-chosen, so segments are charset-checked rather than merely
|
|
7
|
+
* pattern-matched: `..`, empty segments, absolute paths, backslashes and NUL
|
|
8
|
+
* never survive, which is what makes the later `resolve()` in
|
|
9
|
+
* `adapters/files.ts` a belt-and-braces check rather than the only one.
|
|
10
|
+
*/
|
|
11
|
+
export type Forge = "github" | "gitlab";
|
|
12
|
+
export interface IndexPath {
|
|
13
|
+
/** The path exactly as the forge reported it. */
|
|
14
|
+
file: string;
|
|
15
|
+
/** Namespace segment(s) — one on GitHub, possibly nested on GitLab. */
|
|
16
|
+
namespace: string;
|
|
17
|
+
/** Package directory name; `metadata.json`'s `name` must equal it. */
|
|
18
|
+
pkg: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function parseIndexPath(file: string, forge: Forge, host: string): IndexPath | null;
|
|
21
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../src/validate/core/paths.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AAEH,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAExC,MAAM,WAAW,SAAS;IACxB,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;CACb;AAQD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,MAAM,GACX,SAAS,GAAG,IAAI,CAmBlB"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
/**
|
|
4
|
+
* A single path segment: starts alphanumeric, then alphanumeric/`.`/`_`/`-`.
|
|
5
|
+
* Rejects `.`, `..`, hidden files, and anything needing URL escaping.
|
|
6
|
+
*/
|
|
7
|
+
const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
8
|
+
export function parseIndexPath(file, forge, host) {
|
|
9
|
+
if (file.includes("\0") || file.includes("\\"))
|
|
10
|
+
return null;
|
|
11
|
+
const parts = file.split("/");
|
|
12
|
+
// index / <host> / <ns...> / <pkg> / metadata.json
|
|
13
|
+
if (parts.length < 5)
|
|
14
|
+
return null;
|
|
15
|
+
if (parts[0] !== "index")
|
|
16
|
+
return null;
|
|
17
|
+
if ((parts[1] ?? "").toLowerCase() !== host.toLowerCase())
|
|
18
|
+
return null;
|
|
19
|
+
if (parts[parts.length - 1] !== "metadata.json")
|
|
20
|
+
return null;
|
|
21
|
+
const middle = parts.slice(2, -1);
|
|
22
|
+
if (middle.length < 2)
|
|
23
|
+
return null;
|
|
24
|
+
// GitHub namespaces are a single login or org; GitLab groups nest.
|
|
25
|
+
if (forge === "github" && middle.length !== 2)
|
|
26
|
+
return null;
|
|
27
|
+
if (!middle.every((segment) => SEGMENT.test(segment)))
|
|
28
|
+
return null;
|
|
29
|
+
const pkg = middle[middle.length - 1];
|
|
30
|
+
if (pkg === undefined)
|
|
31
|
+
return null;
|
|
32
|
+
return { file, namespace: middle.slice(0, -1).join("/"), pkg };
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/validate/core/paths.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,sCAAsC;AAwBtC;;;GAGG;AACH,MAAM,OAAO,GAAG,8BAA8B,CAAC;AAE/C,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,KAAY,EACZ,IAAY;IAEZ,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAE5D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9B,mDAAmD;IACnD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IACtC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,WAAW,EAAE;QAAE,OAAO,IAAI,CAAC;IACvE,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,eAAe;QAAE,OAAO,IAAI,CAAC;IAE7D,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAClC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,mEAAmE;IACnE,IAAI,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3D,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAEnE,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACtC,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC;AACjE,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type TrustedBot } from "./core/ownership.js";
|
|
2
|
+
export type { TrustedBot } from "./core/ownership.js";
|
|
3
|
+
export type { Metadata } from "./core/metadata.js";
|
|
4
|
+
export interface ValidateOptions {
|
|
5
|
+
forge: "github" | "gitlab";
|
|
6
|
+
/** Trusted base checkout — the committed state the PR proposes to change. */
|
|
7
|
+
root: string;
|
|
8
|
+
changedFiles: string[];
|
|
9
|
+
author: {
|
|
10
|
+
login: string;
|
|
11
|
+
id: string;
|
|
12
|
+
};
|
|
13
|
+
/** Directory holding the PR head's files. DATA ONLY. */
|
|
14
|
+
prTree: string;
|
|
15
|
+
/**
|
|
16
|
+
* Accounts trusted to announce for a namespace they do not own. Bare strings
|
|
17
|
+
* are login-only; the object form pins the account id and scopes namespaces.
|
|
18
|
+
*/
|
|
19
|
+
trustedBots?: TrustedBot[];
|
|
20
|
+
/** Committed registry-host allowlist. Empty/absent = any public DNS host. */
|
|
21
|
+
registryHosts?: string[];
|
|
22
|
+
/** Namespaces nobody may claim through a PR, whoever owns them upstream. */
|
|
23
|
+
reservedNamespaces?: string[];
|
|
24
|
+
/** Forge host pinning the `index/<host>/` segment. Default: the forge's own. */
|
|
25
|
+
host?: string;
|
|
26
|
+
/** API base URL. Default: the CI-predefined one for the forge. */
|
|
27
|
+
apiBase?: string;
|
|
28
|
+
/** Forge API token. Default: the CI-conventional environment variable. */
|
|
29
|
+
token?: string;
|
|
30
|
+
/** `user:token` for registries without anonymous pull tokens. */
|
|
31
|
+
registryAuth?: string;
|
|
32
|
+
/** GitLab only: membership threshold (30 = Developer). */
|
|
33
|
+
minAccessLevel?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface ValidateResult {
|
|
36
|
+
eligible: boolean;
|
|
37
|
+
reasons: string[];
|
|
38
|
+
}
|
|
39
|
+
export declare function runValidate(options: ValidateOptions): Promise<ValidateResult>;
|
|
40
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/validate/index.ts"],"names":[],"mappings":"AAkCA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAE7B,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,YAAY,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC3B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AA+BD,wBAAsB,WAAW,CAC/B,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,cAAc,CAAC,CAWzB"}
|