@middag-io/licensing 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/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @middag-io/licensing
2
+
3
+ Protected code delivery for MIDDAG products.
4
+
5
+ `@middag-io/licensing` standardizes how MIDDAG plugins (Moodle, WordPress,
6
+ and future hosts) ship licensed JavaScript modules through a CDN. The
7
+ package owns the technical contract (placeholders, manifest schema, types).
8
+ The licensing worker owns the policy (which install gets which module,
9
+ when, signed with what key).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @middag-io/licensing
15
+ ```
16
+
17
+ Published to GitHub Packages (`@middag-io` scope). Configure `.npmrc` with
18
+ a token that has `read:packages` for installs and `write:packages` for
19
+ releases.
20
+
21
+ ## Subpath exports
22
+
23
+ | Import | Purpose |
24
+ |---|---|
25
+ | `@middag-io/licensing` | Shared contract types (`CONTRACT_VERSION`, `PLACEHOLDERS`, manifest interfaces). |
26
+ | `@middag-io/licensing/contract` | Same as root — explicit subpath. |
27
+ | `@middag-io/licensing/build` | Vite plugin and CLI helpers that inject placeholders and emit `manifest.template.json`. Node-only. |
28
+ | `@middag-io/licensing/client` | Runtime loader for the host. Validates the worker-issued manifest and dynamically imports protected modules. |
29
+
30
+ ## Status
31
+
32
+ v0.0.2 — first implemented release. Tracks the protected-delivery worker
33
+ contract that shipped in `worker-ts-middag-licensing` v0.1.4 (ADR-012,
34
+ T1-T6). The Vite plugin emits `manifest.template.json` with SHA-384
35
+ hashes; the runtime loader verifies the worker-issued JWS against the
36
+ worker JWKS, validates the bootstrap (install / host / product /
37
+ contract version), and dynamic-imports modules with recomputed SRI.
38
+
39
+ Locked placeholder set (worker substitutes these per install):
40
+
41
+ - `__MIDDAG_HOST_ORIGIN__`
42
+ - `__MIDDAG_INSTALL_ID__`
43
+ - `__MIDDAG_KID__`
44
+ - `__MIDDAG_BUNDLE_EXPIRES_AT__`
45
+ - `__MIDDAG_NONCE__`
46
+
47
+ Adding, removing, or renaming a token is a coordinated worker change
48
+ (`src/lib/manifest-materialize.ts:PLACEHOLDERS`) and a `CONTRACT_VERSION`
49
+ bump.
50
+
51
+ Design contract:
52
+ [`worker-ts-middag-licensing/docs/explanation/spec-licensing-js-lib.md`](https://github.com/middag-io/worker-ts-middag-licensing/blob/main/docs/explanation/spec-licensing-js-lib.md).
53
+
54
+ ## Development
55
+
56
+ ```bash
57
+ npm install
58
+ npm run build
59
+ npm run typecheck
60
+ npm run test
61
+ npm run lint
62
+ ```
63
+
64
+ ## Release
65
+
66
+ 1. `npm run changeset` to record the intent.
67
+ 2. Merge to `main`. The Release workflow opens a "Version Packages" PR.
68
+ 3. Merge that PR to publish.
69
+
70
+ ## License
71
+
72
+ Proprietary — MIDDAG.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Build-time entry point for @middag-io/licensing.
3
+ *
4
+ * Exposes a Vite plugin that:
5
+ *
6
+ * 1. Scans each protected module's emitted artifact for the locked
7
+ * `__MIDDAG_*__` placeholder tokens.
8
+ * 2. Computes a SHA-384 hash of the placeholder-bearing template bytes
9
+ * (informational — the worker re-hashes post-substitution per install).
10
+ * 3. Emits `manifest.template.json` next to the bundle so product CI can
11
+ * upload it together with the artifacts to the worker's BUILDS bucket.
12
+ *
13
+ * The plugin does NOT substitute placeholders at build time — the developer
14
+ * writes the literal token (e.g. `__MIDDAG_INSTALL_ID__`) in their source,
15
+ * and the worker substitutes per-install at materialization. Build-time
16
+ * substitution would mean every install ships the same identifying bytes,
17
+ * defeating the per-install rotation that ADR-012 §"Protection model" relies
18
+ * on.
19
+ */
20
+ import type { Plugin } from "vite";
21
+ import { CONTRACT_VERSION, PLACEHOLDERS, type ManifestTemplate, type ModuleDefinition } from "../contract/index.js";
22
+ export interface LicensingPluginOptions {
23
+ product: string;
24
+ release: string;
25
+ buildId: string;
26
+ modules: ModuleDefinition[];
27
+ /** Manifest filename (defaults to `manifest.template.json`). */
28
+ manifestFilename?: string;
29
+ /**
30
+ * Behaviour when a protected module's emitted bytes contain none of the
31
+ * locked placeholders. Default `error` because shipping a "protected"
32
+ * module that has nothing to substitute is almost always a mistake (the
33
+ * developer forgot to insert the tokens, or a minifier renamed them).
34
+ * Set `warn` for libraries that only use a subset of placeholders.
35
+ */
36
+ onMissingPlaceholders?: "error" | "warn";
37
+ }
38
+ export declare function licensingPlugin(options: LicensingPluginOptions): Plugin;
39
+ /** Convenience helper for tests / scripts that want the empty template. */
40
+ export declare function emptyManifestTemplate(opts: {
41
+ product: string;
42
+ release: string;
43
+ buildId: string;
44
+ }): ManifestTemplate;
45
+ export { CONTRACT_VERSION, PLACEHOLDERS };
46
+ export type { ManifestTemplate, ModuleDefinition };
47
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/build/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EAEtB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,gEAAgE;IAChE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC1C;AA6BD,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CA8EvE;AAmBD,2EAA2E;AAC3E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,gBAAgB,CAQnB;AAED,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,CAAC;AAC1C,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC"}
@@ -0,0 +1,113 @@
1
+ import { CONTRACT_VERSION, PLACEHOLDERS } from "../contract/index.js";
2
+ import { createHash } from "node:crypto";
3
+ import { posix } from "node:path";
4
+ //#region src/build/index.ts
5
+ /**
6
+ * Build-time entry point for @middag-io/licensing.
7
+ *
8
+ * Exposes a Vite plugin that:
9
+ *
10
+ * 1. Scans each protected module's emitted artifact for the locked
11
+ * `__MIDDAG_*__` placeholder tokens.
12
+ * 2. Computes a SHA-384 hash of the placeholder-bearing template bytes
13
+ * (informational — the worker re-hashes post-substitution per install).
14
+ * 3. Emits `manifest.template.json` next to the bundle so product CI can
15
+ * upload it together with the artifacts to the worker's BUILDS bucket.
16
+ *
17
+ * The plugin does NOT substitute placeholders at build time — the developer
18
+ * writes the literal token (e.g. `__MIDDAG_INSTALL_ID__`) in their source,
19
+ * and the worker substitutes per-install at materialization. Build-time
20
+ * substitution would mean every install ships the same identifying bytes,
21
+ * defeating the per-install rotation that ADR-012 §"Protection model" relies
22
+ * on.
23
+ */
24
+ var PLACEHOLDER_ENTRIES = Object.entries(PLACEHOLDERS);
25
+ function sha384Sri(bytes) {
26
+ return `sha384-${createHash("sha384").update(bytes).digest("base64")}`;
27
+ }
28
+ function placeholdersSeen(source) {
29
+ const seen = [];
30
+ for (const [key, token] of PLACEHOLDER_ENTRIES) if (source.includes(token)) seen.push(key);
31
+ return seen;
32
+ }
33
+ function normalizeEntry(entry) {
34
+ return entry.split(/[\\/]+/).filter(Boolean).join("/");
35
+ }
36
+ function licensingPlugin(options) {
37
+ const manifestFilename = options.manifestFilename ?? "manifest.template.json";
38
+ const onMissing = options.onMissingPlaceholders ?? "error";
39
+ return {
40
+ name: "@middag-io/licensing",
41
+ apply: "build",
42
+ enforce: "post",
43
+ async generateBundle(_outputOptions, bundle) {
44
+ const emitted = /* @__PURE__ */ new Map();
45
+ for (const [filename, chunk] of Object.entries(bundle)) {
46
+ const norm = normalizeEntry(filename);
47
+ let bytes;
48
+ if (chunk.type === "chunk") bytes = new TextEncoder().encode(chunk.code);
49
+ else if (chunk.type === "asset") {
50
+ const source = chunk.source;
51
+ bytes = typeof source === "string" ? new TextEncoder().encode(source) : new Uint8Array(source);
52
+ }
53
+ if (bytes) emitted.set(norm, bytes);
54
+ }
55
+ const moduleEntries = [];
56
+ const missing = [];
57
+ for (const mod of options.modules) {
58
+ const key = normalizeEntry(mod.entry);
59
+ const bytes = emitted.get(key) ?? emitted.get(posix.basename(key)) ?? findByBasenameSuffix(emitted, key);
60
+ if (!bytes) this.error(`@middag-io/licensing: module "${mod.name}" declared entry "${mod.entry}" was not present in this build output`);
61
+ const seen = placeholdersSeen(new TextDecoder().decode(bytes));
62
+ if (seen.length === 0) missing.push(mod.name);
63
+ moduleEntries.push({
64
+ name: mod.name,
65
+ entry: mod.entry,
66
+ extension: mod.extension,
67
+ entitlements: mod.entitlements,
68
+ filename: key,
69
+ hash_sha384: sha384Sri(bytes),
70
+ placeholders_seen: seen
71
+ });
72
+ }
73
+ if (missing.length > 0) {
74
+ const msg = `@middag-io/licensing: protected module(s) had no placeholders: ${missing.join(", ")}. Ensure your source code uses tokens from PLACEHOLDERS (e.g. __MIDDAG_INSTALL_ID__) and that no plugin renames or strips them.`;
75
+ if (onMissing === "error") this.error(msg);
76
+ else this.warn(msg);
77
+ }
78
+ const template = {
79
+ contract_version: CONTRACT_VERSION,
80
+ product: options.product,
81
+ release: options.release,
82
+ build_id: options.buildId,
83
+ modules: moduleEntries
84
+ };
85
+ this.emitFile({
86
+ type: "asset",
87
+ fileName: manifestFilename,
88
+ source: JSON.stringify(template, null, 2) + "\n"
89
+ });
90
+ }
91
+ };
92
+ }
93
+ function findByBasenameSuffix(emitted, declared) {
94
+ const base = posix.basename(declared).replace(/\.[^.]+$/, "");
95
+ for (const [filename, bytes] of emitted) {
96
+ const file = posix.basename(filename);
97
+ if (file.startsWith(base + "-") || file === posix.basename(declared)) return bytes;
98
+ }
99
+ }
100
+ /** Convenience helper for tests / scripts that want the empty template. */
101
+ function emptyManifestTemplate(opts) {
102
+ return {
103
+ contract_version: CONTRACT_VERSION,
104
+ product: opts.product,
105
+ release: opts.release,
106
+ build_id: opts.buildId,
107
+ modules: []
108
+ };
109
+ }
110
+ //#endregion
111
+ export { CONTRACT_VERSION, PLACEHOLDERS, emptyManifestTemplate, licensingPlugin };
112
+
113
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/build/index.ts"],"sourcesContent":["/**\n * Build-time entry point for @middag-io/licensing.\n *\n * Exposes a Vite plugin that:\n *\n * 1. Scans each protected module's emitted artifact for the locked\n * `__MIDDAG_*__` placeholder tokens.\n * 2. Computes a SHA-384 hash of the placeholder-bearing template bytes\n * (informational — the worker re-hashes post-substitution per install).\n * 3. Emits `manifest.template.json` next to the bundle so product CI can\n * upload it together with the artifacts to the worker's BUILDS bucket.\n *\n * The plugin does NOT substitute placeholders at build time — the developer\n * writes the literal token (e.g. `__MIDDAG_INSTALL_ID__`) in their source,\n * and the worker substitutes per-install at materialization. Build-time\n * substitution would mean every install ships the same identifying bytes,\n * defeating the per-install rotation that ADR-012 §\"Protection model\" relies\n * on.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { posix } from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport {\n CONTRACT_VERSION,\n PLACEHOLDERS,\n type ManifestTemplate,\n type ModuleDefinition,\n type PlaceholderKey,\n} from \"../contract/index.js\";\n\nexport interface LicensingPluginOptions {\n product: string;\n release: string;\n buildId: string;\n modules: ModuleDefinition[];\n /** Manifest filename (defaults to `manifest.template.json`). */\n manifestFilename?: string;\n /**\n * Behaviour when a protected module's emitted bytes contain none of the\n * locked placeholders. Default `error` because shipping a \"protected\"\n * module that has nothing to substitute is almost always a mistake (the\n * developer forgot to insert the tokens, or a minifier renamed them).\n * Set `warn` for libraries that only use a subset of placeholders.\n */\n onMissingPlaceholders?: \"error\" | \"warn\";\n}\n\nconst PLACEHOLDER_ENTRIES = Object.entries(PLACEHOLDERS) as Array<\n [PlaceholderKey, (typeof PLACEHOLDERS)[PlaceholderKey]]\n>;\n\nfunction sha384Sri(bytes: Uint8Array): string {\n const digest = createHash(\"sha384\").update(bytes).digest(\"base64\");\n return `sha384-${digest}`;\n}\n\nfunction placeholdersSeen(source: string): PlaceholderKey[] {\n const seen: PlaceholderKey[] = [];\n for (const [key, token] of PLACEHOLDER_ENTRIES) {\n if (source.includes(token)) seen.push(key);\n }\n return seen;\n}\n\nfunction normalizeEntry(entry: string): string {\n // Vite's emit pipeline keys assets by their forward-slash path relative to\n // outDir. Convert any Windows-style separators so Vite-on-Windows matches\n // the same key Vite-on-macOS would produce.\n return entry\n .split(/[\\\\/]+/)\n .filter(Boolean)\n .join(\"/\");\n}\n\nexport function licensingPlugin(options: LicensingPluginOptions): Plugin {\n const manifestFilename = options.manifestFilename ?? \"manifest.template.json\";\n const onMissing = options.onMissingPlaceholders ?? \"error\";\n\n return {\n name: \"@middag-io/licensing\",\n apply: \"build\",\n enforce: \"post\",\n\n async generateBundle(_outputOptions, bundle) {\n const emitted = new Map<string, Uint8Array>();\n for (const [filename, chunk] of Object.entries(bundle)) {\n const norm = normalizeEntry(filename);\n let bytes: Uint8Array | undefined;\n if (chunk.type === \"chunk\") {\n bytes = new TextEncoder().encode(chunk.code);\n } else if (chunk.type === \"asset\") {\n const source = chunk.source;\n bytes =\n typeof source === \"string\" ? new TextEncoder().encode(source) : new Uint8Array(source);\n }\n if (bytes) emitted.set(norm, bytes);\n }\n\n const moduleEntries: ManifestTemplate[\"modules\"] = [];\n const missing: string[] = [];\n\n for (const mod of options.modules) {\n const key = normalizeEntry(mod.entry);\n const bytes =\n emitted.get(key) ??\n emitted.get(posix.basename(key)) ??\n findByBasenameSuffix(emitted, key);\n\n if (!bytes) {\n this.error(\n `@middag-io/licensing: module \"${mod.name}\" declared entry \"${mod.entry}\" was not present in this build output`,\n );\n }\n\n const source = new TextDecoder().decode(bytes);\n const seen = placeholdersSeen(source);\n if (seen.length === 0) {\n missing.push(mod.name);\n }\n\n moduleEntries.push({\n name: mod.name,\n entry: mod.entry,\n extension: mod.extension,\n entitlements: mod.entitlements,\n filename: key,\n hash_sha384: sha384Sri(bytes),\n placeholders_seen: seen,\n });\n }\n\n if (missing.length > 0) {\n const msg = `@middag-io/licensing: protected module(s) had no placeholders: ${missing.join(\", \")}. Ensure your source code uses tokens from PLACEHOLDERS (e.g. __MIDDAG_INSTALL_ID__) and that no plugin renames or strips them.`;\n if (onMissing === \"error\") this.error(msg);\n else this.warn(msg);\n }\n\n const template: ManifestTemplate = {\n contract_version: CONTRACT_VERSION,\n product: options.product,\n release: options.release,\n build_id: options.buildId,\n modules: moduleEntries,\n };\n\n this.emitFile({\n type: \"asset\",\n fileName: manifestFilename,\n source: JSON.stringify(template, null, 2) + \"\\n\",\n });\n },\n };\n}\n\n// Some Vite/Rollup configs hash entry filenames. If the declared `entry` is\n// `dist/core.js` but Vite emits `core-abc123.js`, fall back to matching by\n// basename stem so a user-friendly entry path still resolves.\nfunction findByBasenameSuffix(\n emitted: Map<string, Uint8Array>,\n declared: string,\n): Uint8Array | undefined {\n const base = posix.basename(declared).replace(/\\.[^.]+$/, \"\");\n for (const [filename, bytes] of emitted) {\n const file = posix.basename(filename);\n if (file.startsWith(base + \"-\") || file === posix.basename(declared)) {\n return bytes;\n }\n }\n return undefined;\n}\n\n/** Convenience helper for tests / scripts that want the empty template. */\nexport function emptyManifestTemplate(opts: {\n product: string;\n release: string;\n buildId: string;\n}): ManifestTemplate {\n return {\n contract_version: CONTRACT_VERSION,\n product: opts.product,\n release: opts.release,\n build_id: opts.buildId,\n modules: [],\n };\n}\n\nexport { CONTRACT_VERSION, PLACEHOLDERS };\nexport type { ManifestTemplate, ModuleDefinition };\n"],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Runtime loader for @middag-io/licensing.
3
+ *
4
+ * Runs inside the consuming host (Moodle, WordPress, ...). The browser never
5
+ * calls the worker's HMAC-protected `/v1/protected/manifest` endpoint — the
6
+ * PHP host bridge does that, persists the `ManifestResponse`, and serves it
7
+ * to the browser at `manifest_url` (per ADR-012 §"Server-side host bridge").
8
+ *
9
+ * This loader:
10
+ *
11
+ * 1. Fetches the manifest JSON from the host-served URL (or via a caller-
12
+ * supplied `fetchManifest` for embedded use cases).
13
+ * 2. Verifies the JWS signature against the worker's JWKS endpoint
14
+ * `${workerOrigin}/api/protected/jwks` (cached in-memory per kid).
15
+ * 3. Validates the decoded payload against the bootstrap context
16
+ * (`install_id`, `host_origin`, `host_type`, `product`, `contract_version`)
17
+ * so a manifest swapped between installs is rejected client-side.
18
+ * 4. Loads a requested module by fetching its CDN URL, recomputing the
19
+ * SRI hash against the bytes, and dynamic-importing from a Blob URL.
20
+ * Native `import()` does not yet take an integrity attribute, so we
21
+ * compute SRI ourselves before handing the bytes to the JS engine.
22
+ */
23
+ import { type JWK } from "jose";
24
+ import { CONTRACT_VERSION, type AuthorizedManifest, type HostType, type ManifestResponse } from "../contract/index.js";
25
+ export interface CreateLicensingClientOptions {
26
+ hostType: HostType;
27
+ hostOrigin: string;
28
+ product: string;
29
+ installId: string;
30
+ /**
31
+ * Worker base origin used to fetch the JWKS, e.g.
32
+ * `https://licensing.middag.io`. Required unless `jwks` is supplied.
33
+ */
34
+ workerOrigin?: string;
35
+ /**
36
+ * Static JWKS document (escape hatch for tests / pre-fetched keys).
37
+ * When supplied, the loader skips the network fetch.
38
+ */
39
+ jwks?: {
40
+ keys: JWK[];
41
+ };
42
+ /**
43
+ * Manifest URL served by the host (PHP bridge). The body must be the
44
+ * JSON shape worker returns from POST /v1/protected/manifest.
45
+ */
46
+ manifestUrl?: string;
47
+ /** Caller-supplied manifest fetcher; overrides `manifestUrl` when given. */
48
+ fetchManifest?: () => Promise<ManifestResponse>;
49
+ /**
50
+ * Fetch implementation override. Defaults to `globalThis.fetch`. Tests
51
+ * can pass a mock here; SSR contexts can pass a polyfill.
52
+ */
53
+ fetch?: typeof fetch;
54
+ /**
55
+ * Clock skew tolerance in seconds when checking `bundle_expires_at`.
56
+ * Defaults to 60s.
57
+ */
58
+ clockSkewSeconds?: number;
59
+ }
60
+ export interface LicensingClient {
61
+ readonly manifest: AuthorizedManifest;
62
+ readonly response: ManifestResponse;
63
+ has(moduleName: string): boolean;
64
+ loadModule<T = unknown>(moduleName: string): Promise<T>;
65
+ refresh(): Promise<AuthorizedManifest>;
66
+ }
67
+ export declare class LicensingError extends Error {
68
+ readonly code: string;
69
+ constructor(code: string, message: string);
70
+ }
71
+ export declare function createLicensingClient(options: CreateLicensingClientOptions): Promise<LicensingClient>;
72
+ export { CONTRACT_VERSION };
73
+ export type { AuthorizedManifest, HostType, ManifestResponse };
74
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAA4B,KAAK,GAAG,EAAE,MAAM,MAAM,CAAC;AAC1D,OAAO,EACL,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAChD;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC,UAAU,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACxD,OAAO,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACxC;AA0DD,qBAAa,cAAe,SAAQ,KAAK;aAErB,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAC5B,OAAO,EAAE,MAAM;CAKlB;AA2ID,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,eAAe,CAAC,CAmE1B;AAED,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAC5B,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC"}