@lloyal-labs/rig 2.1.0 → 3.0.1
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 +107 -0
- package/LICENSE-FAQ.md +256 -0
- package/README.md +93 -74
- package/dist/bundle.d.ts +211 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +296 -0
- package/dist/bundle.js.map +1 -0
- package/dist/cancellable-fetch.d.ts +98 -0
- package/dist/cancellable-fetch.d.ts.map +1 -0
- package/dist/cancellable-fetch.js +133 -0
- package/dist/cancellable-fetch.js.map +1 -0
- package/dist/config-store.d.ts +30 -0
- package/dist/config-store.d.ts.map +1 -0
- package/dist/config-store.js +45 -0
- package/dist/config-store.js.map +1 -0
- package/dist/define-app.d.ts +98 -0
- package/dist/define-app.d.ts.map +1 -0
- package/dist/define-app.js +232 -0
- package/dist/define-app.js.map +1 -0
- package/dist/grant-store.d.ts +31 -0
- package/dist/grant-store.d.ts.map +1 -0
- package/dist/grant-store.js +49 -0
- package/dist/grant-store.js.map +1 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -11
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +3 -2
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +3 -2
- package/dist/node.js.map +1 -1
- package/dist/protocol.d.ts +155 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +184 -0
- package/dist/protocol.js.map +1 -0
- package/dist/registry.d.ts +87 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +245 -0
- package/dist/registry.js.map +1 -0
- package/dist/reranker.d.ts +25 -7
- package/dist/reranker.d.ts.map +1 -1
- package/dist/reranker.js +103 -63
- package/dist/reranker.js.map +1 -1
- package/dist/resources/types.d.ts +10 -37
- package/dist/resources/types.d.ts.map +1 -1
- package/dist/resources/types.js +12 -0
- package/dist/resources/types.js.map +1 -1
- package/dist/spine-render.d.ts +97 -0
- package/dist/spine-render.d.ts.map +1 -0
- package/dist/spine-render.js +121 -0
- package/dist/spine-render.js.map +1 -0
- package/dist/tools/index.d.ts +26 -22
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +24 -28
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/keyless-search.d.ts +67 -0
- package/dist/tools/keyless-search.d.ts.map +1 -0
- package/dist/tools/keyless-search.js +401 -0
- package/dist/tools/keyless-search.js.map +1 -0
- package/dist/tools/plan.d.ts +31 -4
- package/dist/tools/plan.d.ts.map +1 -1
- package/dist/tools/plan.js +46 -11
- package/dist/tools/plan.js.map +1 -1
- package/dist/tools/types.d.ts +12 -56
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js +17 -0
- package/dist/tools/types.js.map +1 -1
- package/dist/tools/web-search.d.ts +9 -25
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +11 -119
- package/dist/tools/web-search.js.map +1 -1
- package/package.json +10 -7
- package/dist/sources/corpus.d.ts +0 -80
- package/dist/sources/corpus.d.ts.map +0 -1
- package/dist/sources/corpus.js +0 -100
- package/dist/sources/corpus.js.map +0 -1
- package/dist/sources/index.d.ts +0 -12
- package/dist/sources/index.d.ts.map +0 -1
- package/dist/sources/index.js +0 -14
- package/dist/sources/index.js.map +0 -1
- package/dist/sources/web.d.ts +0 -67
- package/dist/sources/web.d.ts.map +0 -1
- package/dist/sources/web.js +0 -104
- package/dist/sources/web.js.map +0 -1
- package/dist/tools/fetch-page.d.ts +0 -48
- package/dist/tools/fetch-page.d.ts.map +0 -1
- package/dist/tools/fetch-page.js +0 -309
- package/dist/tools/fetch-page.js.map +0 -1
- package/dist/tools/grep.d.ts +0 -35
- package/dist/tools/grep.d.ts.map +0 -1
- package/dist/tools/grep.js +0 -84
- package/dist/tools/grep.js.map +0 -1
- package/dist/tools/read-file.d.ts +0 -74
- package/dist/tools/read-file.d.ts.map +0 -1
- package/dist/tools/read-file.js +0 -192
- package/dist/tools/read-file.js.map +0 -1
- package/dist/tools/search.d.ts +0 -34
- package/dist/tools/search.d.ts.map +0 -1
- package/dist/tools/search.js +0 -101
- package/dist/tools/search.js.map +0 -1
package/dist/bundle.d.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed-tarball App distribution — verify primitives.
|
|
3
|
+
*
|
|
4
|
+
* Apps are distributed as signed npm tarballs through the canonical
|
|
5
|
+
* channel at {@link CHANNEL_CATALOG_URL}. The `harness.dev install` CLI
|
|
6
|
+
* uses the primitives here ({@link verifyBundle}, {@link resolveAppEntry})
|
|
7
|
+
* to fetch + signature-verify a tarball against
|
|
8
|
+
* {@link CHANNEL_TRUST_ROOTS}, then shells out to `npm install <URL>` so
|
|
9
|
+
* the app lands in the harness's `node_modules` like any other npm
|
|
10
|
+
* dependency. The harness boots and imports each app with a plain static
|
|
11
|
+
* `import`; the framework provides no runtime "load app by name" verb.
|
|
12
|
+
*
|
|
13
|
+
* This module exposes the verify primitives only — the file-system and
|
|
14
|
+
* `npm install` shell-out live in the CLI package
|
|
15
|
+
* (`@lloyal-labs/harness-cli`) so this entry remains platform-agnostic
|
|
16
|
+
* (no `node:*` imports) and works in any JS runtime, including React
|
|
17
|
+
* Native harnesses that might consume `@lloyal-labs/rig` for non-install
|
|
18
|
+
* code paths.
|
|
19
|
+
*
|
|
20
|
+
* **Channel-canonical resolution.** {@link resolveAppEntry} fetches the
|
|
21
|
+
* catalog from {@link CHANNEL_CATALOG_URL}, verifies its Ed25519
|
|
22
|
+
* signature against {@link CHANNEL_TRUST_ROOTS}, and resolves a name +
|
|
23
|
+
* semver range to a {@link CatalogVersion} descriptor (manifestUrl +
|
|
24
|
+
* tarballUrl + sizeBytes). The caller never supplies a URL or a trust
|
|
25
|
+
* map — to use a different channel, fork `@lloyal-labs/rig` and edit
|
|
26
|
+
* the constants in `protocol.ts`.
|
|
27
|
+
*
|
|
28
|
+
* **Verification is the entire trust boundary.** `verifyBundle` runs
|
|
29
|
+
* before `harness.dev install` invokes `npm install <tarball-URL>`, so
|
|
30
|
+
* a tampered tarball never reaches `npm install`. Once installed, the
|
|
31
|
+
* lockfile's sha512 `integrity` field carries that trust forward for
|
|
32
|
+
* subsequent `npm ci` reproduction (immutable tarball URL → same bytes
|
|
33
|
+
* forever → same sha512 → same Ed25519 chain).
|
|
34
|
+
*
|
|
35
|
+
* @packageDocumentation
|
|
36
|
+
* @category Protocol
|
|
37
|
+
*/
|
|
38
|
+
import type { Operation } from 'effection';
|
|
39
|
+
/**
|
|
40
|
+
* Manifest describing a signed tarball, served at the `manifestUrl`
|
|
41
|
+
* listed in a catalog entry. The manifest is the publisher-of-record
|
|
42
|
+
* payload that ties (tarball bytes ↔ Ed25519 signature ↔ npm-compatible
|
|
43
|
+
* sha512 integrity ↔ identifying metadata) together.
|
|
44
|
+
*/
|
|
45
|
+
export interface AppBundleManifest {
|
|
46
|
+
/** App identifier (matches `App.manifest.name`). */
|
|
47
|
+
name: string;
|
|
48
|
+
/** Semver of this release. */
|
|
49
|
+
version: string;
|
|
50
|
+
/**
|
|
51
|
+
* Filename of the tarball relative to the channel's bundle directory
|
|
52
|
+
* (e.g., `web-1.2.0.tgz`). The canonical record of what was signed —
|
|
53
|
+
* `signature` is over the bytes of this artifact.
|
|
54
|
+
*/
|
|
55
|
+
entry: string;
|
|
56
|
+
/** Base64-encoded Ed25519 signature over the tarball bytes. */
|
|
57
|
+
signature: string;
|
|
58
|
+
/**
|
|
59
|
+
* npm-compatible Subresource Integrity hash over the tarball bytes
|
|
60
|
+
* (e.g., `sha512-<base64>`). `npm install` verifies this on extract
|
|
61
|
+
* as defense-in-depth; the Ed25519 `signature` above is the
|
|
62
|
+
* authoritative trust boundary, but the SRI hash carries trust
|
|
63
|
+
* forward into the consumer's `package-lock.json` so subsequent
|
|
64
|
+
* `npm ci` reproduces the install without re-verifying the
|
|
65
|
+
* signature.
|
|
66
|
+
*/
|
|
67
|
+
integrity: string;
|
|
68
|
+
/**
|
|
69
|
+
* Identifier of the publisher's signing key. Looked up in
|
|
70
|
+
* {@link CHANNEL_TRUST_ROOTS} to obtain the verifying key.
|
|
71
|
+
*/
|
|
72
|
+
publisherKeyId: string;
|
|
73
|
+
/** Tarball size in bytes (sanity check vs. download). */
|
|
74
|
+
sizeBytes: number;
|
|
75
|
+
/**
|
|
76
|
+
* peerDependencies of the app (e.g., `{"@lloyal-labs/rig":
|
|
77
|
+
* "^3.0.0"}`). Informational; npm enforces these on install.
|
|
78
|
+
*/
|
|
79
|
+
peerDependencies?: Record<string, string>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* One version's entry in the catalog (under an app's `versions` array).
|
|
83
|
+
*/
|
|
84
|
+
export interface CatalogVersion {
|
|
85
|
+
/** Semver of this release. */
|
|
86
|
+
version: string;
|
|
87
|
+
/** URL the manifest JSON is served from. */
|
|
88
|
+
manifestUrl: string;
|
|
89
|
+
/**
|
|
90
|
+
* URL the signed tarball (`.tgz`) is served from. This URL is
|
|
91
|
+
* immutable per version: republishing forces a new semver. The
|
|
92
|
+
* `harness.dev install` CLI passes this URL straight to
|
|
93
|
+
* `npm install`, and it lands verbatim in the consumer's
|
|
94
|
+
* `package.json` and `package-lock.json` so CI can reproduce the
|
|
95
|
+
* install with plain `npm ci` against no Lloyal tooling.
|
|
96
|
+
*/
|
|
97
|
+
tarballUrl: string;
|
|
98
|
+
/** App-protocol version this artifact targets (e.g., `'3.0'`). */
|
|
99
|
+
appProtocolVersion: string;
|
|
100
|
+
/** Tarball size in bytes (sanity check vs. download). */
|
|
101
|
+
sizeBytes: number;
|
|
102
|
+
/**
|
|
103
|
+
* npm package name as declared in the tarball's `package.json` (e.g.,
|
|
104
|
+
* `@lloyal-labs/web-app`). The catalog `name` is the scoped Lloyal
|
|
105
|
+
* identifier (`lloyal/web`); `importName` is what consumers actually
|
|
106
|
+
* `import { … } from '<importName>'` once npm has installed the tarball.
|
|
107
|
+
* Validated server-side at submission time against the tarball's
|
|
108
|
+
* embedded `package.json`.
|
|
109
|
+
*/
|
|
110
|
+
importName: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* One app's entry in the catalog.
|
|
114
|
+
*/
|
|
115
|
+
export interface CatalogEntry {
|
|
116
|
+
/** App identifier (matches `manifest.name`). */
|
|
117
|
+
name: string;
|
|
118
|
+
/** Published versions, unordered. */
|
|
119
|
+
versions: readonly CatalogVersion[];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The full signed catalog served at {@link CHANNEL_CATALOG_URL}.
|
|
123
|
+
*
|
|
124
|
+
* The signature is over a canonical-JSON encoding of
|
|
125
|
+
* `{ signedAt, entries, publisherKeyId }` (sorted keys, no whitespace).
|
|
126
|
+
*/
|
|
127
|
+
export interface SignedCatalog {
|
|
128
|
+
/** ISO-8601 timestamp of when the catalog was signed. */
|
|
129
|
+
signedAt: string;
|
|
130
|
+
/** All apps published to the channel. */
|
|
131
|
+
entries: readonly CatalogEntry[];
|
|
132
|
+
/**
|
|
133
|
+
* Identifier of the platform key that signed this catalog. Looked up
|
|
134
|
+
* in {@link CHANNEL_TRUST_ROOTS}.
|
|
135
|
+
*/
|
|
136
|
+
publisherKeyId: string;
|
|
137
|
+
/** Base64-encoded Ed25519 signature. */
|
|
138
|
+
signature: string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Raised when a tarball, manifest, or catalog fails signature, size,
|
|
142
|
+
* or trust-roots verification. Distinct from network errors raised by
|
|
143
|
+
* `cancellableFetch`.
|
|
144
|
+
*/
|
|
145
|
+
export declare class BundleVerificationError extends Error {
|
|
146
|
+
constructor(message: string);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Raised when {@link resolveAppEntry} cannot resolve the requested
|
|
150
|
+
* `(name, semver)` tuple against the catalog. Distinct from
|
|
151
|
+
* {@link BundleVerificationError}: the catalog was reached and verified,
|
|
152
|
+
* the name is just not listed (or no version matched the semver range).
|
|
153
|
+
*/
|
|
154
|
+
export declare class AppNotFoundError extends Error {
|
|
155
|
+
constructor(message: string);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Test-only: override {@link CHANNEL_TRUST_ROOTS} with a map containing
|
|
159
|
+
* exactly the (keyId, publicKey) pair given. Subsequent
|
|
160
|
+
* {@link resolveAppEntry} calls (and the internal catalog-verification
|
|
161
|
+
* path) use this override instead of the framework-vendored constant.
|
|
162
|
+
* Only active when `process.env.NODE_ENV === 'test'`.
|
|
163
|
+
*
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
export declare function setTestTrustRoot(keyId: string, key: Uint8Array): void;
|
|
167
|
+
/**
|
|
168
|
+
* Test-only: override {@link CHANNEL_CATALOG_URL} with the given URL.
|
|
169
|
+
* Useful for pointing the resolver at a `file://` or `http://localhost:N`
|
|
170
|
+
* fixture during unit tests. Only active when
|
|
171
|
+
* `process.env.NODE_ENV === 'test'`.
|
|
172
|
+
*
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
export declare function setTestCatalogUrl(url: string): void;
|
|
176
|
+
/**
|
|
177
|
+
* Test-only: clear both overrides. Call from `afterEach` to keep test
|
|
178
|
+
* isolation clean.
|
|
179
|
+
*
|
|
180
|
+
* @internal
|
|
181
|
+
*/
|
|
182
|
+
export declare function clearTestOverrides(): void;
|
|
183
|
+
/**
|
|
184
|
+
* Test-only: drop the per-process catalog cache. Use in `afterEach` to
|
|
185
|
+
* guarantee a fresh catalog fetch per test.
|
|
186
|
+
*
|
|
187
|
+
* @internal
|
|
188
|
+
*/
|
|
189
|
+
export declare function clearCatalogCache(): void;
|
|
190
|
+
/**
|
|
191
|
+
* Verify an Ed25519 signature over `bytes` using `publicKey` (32-byte
|
|
192
|
+
* raw key). Returns `true` if the signature is authentic; `false`
|
|
193
|
+
* otherwise. `crypto.subtle.verify` is async so the function returns a
|
|
194
|
+
* `Promise<boolean>`; callers `yield* call(() => verifyBundle(...))` to
|
|
195
|
+
* bridge.
|
|
196
|
+
*/
|
|
197
|
+
export declare function verifyBundle(bytes: Uint8Array, signatureBase64: string, publicKey: Uint8Array): Promise<boolean>;
|
|
198
|
+
/**
|
|
199
|
+
* Resolve a name + optional semver range against the verified catalog.
|
|
200
|
+
* Returns the highest-matching version's catalog entry, or throws
|
|
201
|
+
* {@link AppNotFoundError} if the name is absent or no version matches.
|
|
202
|
+
*
|
|
203
|
+
* Consumers (notably the `harness.dev install` CLI) then fetch the
|
|
204
|
+
* returned `manifestUrl` + `tarballUrl`, run {@link verifyBundle}
|
|
205
|
+
* against the manifest's signature over the tarball bytes, and shell
|
|
206
|
+
* out to `npm install <tarballUrl>` to install the verified package.
|
|
207
|
+
*/
|
|
208
|
+
export declare function resolveAppEntry(name: string, opts?: {
|
|
209
|
+
semver?: string;
|
|
210
|
+
}): Operation<CatalogVersion>;
|
|
211
|
+
//# sourceMappingURL=bundle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAK3C;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;OAQG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;;OAOG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;CACrC;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAcD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAErE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC;AAqCD;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAID;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,MAAM,EACvB,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC,OAAO,CAAC,CAuBlB;AA2GD;;;;;;;;;GASG;AACH,wBAAiB,eAAe,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7B,SAAS,CAAC,cAAc,CAAC,CA2B3B"}
|
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Signed-tarball App distribution — verify primitives.
|
|
4
|
+
*
|
|
5
|
+
* Apps are distributed as signed npm tarballs through the canonical
|
|
6
|
+
* channel at {@link CHANNEL_CATALOG_URL}. The `harness.dev install` CLI
|
|
7
|
+
* uses the primitives here ({@link verifyBundle}, {@link resolveAppEntry})
|
|
8
|
+
* to fetch + signature-verify a tarball against
|
|
9
|
+
* {@link CHANNEL_TRUST_ROOTS}, then shells out to `npm install <URL>` so
|
|
10
|
+
* the app lands in the harness's `node_modules` like any other npm
|
|
11
|
+
* dependency. The harness boots and imports each app with a plain static
|
|
12
|
+
* `import`; the framework provides no runtime "load app by name" verb.
|
|
13
|
+
*
|
|
14
|
+
* This module exposes the verify primitives only — the file-system and
|
|
15
|
+
* `npm install` shell-out live in the CLI package
|
|
16
|
+
* (`@lloyal-labs/harness-cli`) so this entry remains platform-agnostic
|
|
17
|
+
* (no `node:*` imports) and works in any JS runtime, including React
|
|
18
|
+
* Native harnesses that might consume `@lloyal-labs/rig` for non-install
|
|
19
|
+
* code paths.
|
|
20
|
+
*
|
|
21
|
+
* **Channel-canonical resolution.** {@link resolveAppEntry} fetches the
|
|
22
|
+
* catalog from {@link CHANNEL_CATALOG_URL}, verifies its Ed25519
|
|
23
|
+
* signature against {@link CHANNEL_TRUST_ROOTS}, and resolves a name +
|
|
24
|
+
* semver range to a {@link CatalogVersion} descriptor (manifestUrl +
|
|
25
|
+
* tarballUrl + sizeBytes). The caller never supplies a URL or a trust
|
|
26
|
+
* map — to use a different channel, fork `@lloyal-labs/rig` and edit
|
|
27
|
+
* the constants in `protocol.ts`.
|
|
28
|
+
*
|
|
29
|
+
* **Verification is the entire trust boundary.** `verifyBundle` runs
|
|
30
|
+
* before `harness.dev install` invokes `npm install <tarball-URL>`, so
|
|
31
|
+
* a tampered tarball never reaches `npm install`. Once installed, the
|
|
32
|
+
* lockfile's sha512 `integrity` field carries that trust forward for
|
|
33
|
+
* subsequent `npm ci` reproduction (immutable tarball URL → same bytes
|
|
34
|
+
* forever → same sha512 → same Ed25519 chain).
|
|
35
|
+
*
|
|
36
|
+
* @packageDocumentation
|
|
37
|
+
* @category Protocol
|
|
38
|
+
*/
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.AppNotFoundError = exports.BundleVerificationError = void 0;
|
|
41
|
+
exports.setTestTrustRoot = setTestTrustRoot;
|
|
42
|
+
exports.setTestCatalogUrl = setTestCatalogUrl;
|
|
43
|
+
exports.clearTestOverrides = clearTestOverrides;
|
|
44
|
+
exports.clearCatalogCache = clearCatalogCache;
|
|
45
|
+
exports.verifyBundle = verifyBundle;
|
|
46
|
+
exports.resolveAppEntry = resolveAppEntry;
|
|
47
|
+
const effection_1 = require("effection");
|
|
48
|
+
const semver_1 = require("semver");
|
|
49
|
+
const cancellable_fetch_1 = require("./cancellable-fetch");
|
|
50
|
+
const protocol_1 = require("./protocol");
|
|
51
|
+
/**
|
|
52
|
+
* Raised when a tarball, manifest, or catalog fails signature, size,
|
|
53
|
+
* or trust-roots verification. Distinct from network errors raised by
|
|
54
|
+
* `cancellableFetch`.
|
|
55
|
+
*/
|
|
56
|
+
class BundleVerificationError extends Error {
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = 'BundleVerificationError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.BundleVerificationError = BundleVerificationError;
|
|
63
|
+
/**
|
|
64
|
+
* Raised when {@link resolveAppEntry} cannot resolve the requested
|
|
65
|
+
* `(name, semver)` tuple against the catalog. Distinct from
|
|
66
|
+
* {@link BundleVerificationError}: the catalog was reached and verified,
|
|
67
|
+
* the name is just not listed (or no version matched the semver range).
|
|
68
|
+
*/
|
|
69
|
+
class AppNotFoundError extends Error {
|
|
70
|
+
constructor(message) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = 'AppNotFoundError';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.AppNotFoundError = AppNotFoundError;
|
|
76
|
+
// ── Test-only injection (NODE_ENV=test) ─────────────────────────────
|
|
77
|
+
//
|
|
78
|
+
// bundle.test.ts overrides the framework-vendored CHANNEL_TRUST_ROOTS +
|
|
79
|
+
// CHANNEL_CATALOG_URL via the helpers below so it can exercise the
|
|
80
|
+
// verification flow against a fresh test keypair + a local HTTP / file://
|
|
81
|
+
// catalog fixture. The overrides are inert outside NODE_ENV=test —
|
|
82
|
+
// `getTrustRoots()` / `getCatalogUrl()` consult them only when the
|
|
83
|
+
// environment names the test runner.
|
|
84
|
+
let testTrustRoots;
|
|
85
|
+
let testCatalogUrl;
|
|
86
|
+
/**
|
|
87
|
+
* Test-only: override {@link CHANNEL_TRUST_ROOTS} with a map containing
|
|
88
|
+
* exactly the (keyId, publicKey) pair given. Subsequent
|
|
89
|
+
* {@link resolveAppEntry} calls (and the internal catalog-verification
|
|
90
|
+
* path) use this override instead of the framework-vendored constant.
|
|
91
|
+
* Only active when `process.env.NODE_ENV === 'test'`.
|
|
92
|
+
*
|
|
93
|
+
* @internal
|
|
94
|
+
*/
|
|
95
|
+
function setTestTrustRoot(keyId, key) {
|
|
96
|
+
testTrustRoots = new Map([[keyId, key]]);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Test-only: override {@link CHANNEL_CATALOG_URL} with the given URL.
|
|
100
|
+
* Useful for pointing the resolver at a `file://` or `http://localhost:N`
|
|
101
|
+
* fixture during unit tests. Only active when
|
|
102
|
+
* `process.env.NODE_ENV === 'test'`.
|
|
103
|
+
*
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
106
|
+
function setTestCatalogUrl(url) {
|
|
107
|
+
testCatalogUrl = url;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Test-only: clear both overrides. Call from `afterEach` to keep test
|
|
111
|
+
* isolation clean.
|
|
112
|
+
*
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
function clearTestOverrides() {
|
|
116
|
+
testTrustRoots = undefined;
|
|
117
|
+
testCatalogUrl = undefined;
|
|
118
|
+
}
|
|
119
|
+
function isTestEnv() {
|
|
120
|
+
return (typeof process !== 'undefined' &&
|
|
121
|
+
process.env != null &&
|
|
122
|
+
process.env.NODE_ENV === 'test');
|
|
123
|
+
}
|
|
124
|
+
function getTrustRoots() {
|
|
125
|
+
if (isTestEnv() && testTrustRoots)
|
|
126
|
+
return testTrustRoots;
|
|
127
|
+
return protocol_1.CHANNEL_TRUST_ROOTS;
|
|
128
|
+
}
|
|
129
|
+
function getCatalogUrl() {
|
|
130
|
+
if (isTestEnv() && testCatalogUrl)
|
|
131
|
+
return testCatalogUrl;
|
|
132
|
+
return protocol_1.CHANNEL_CATALOG_URL;
|
|
133
|
+
}
|
|
134
|
+
const catalogCache = new Map();
|
|
135
|
+
/**
|
|
136
|
+
* Test-only: drop the per-process catalog cache. Use in `afterEach` to
|
|
137
|
+
* guarantee a fresh catalog fetch per test.
|
|
138
|
+
*
|
|
139
|
+
* @internal
|
|
140
|
+
*/
|
|
141
|
+
function clearCatalogCache() {
|
|
142
|
+
catalogCache.clear();
|
|
143
|
+
}
|
|
144
|
+
// ── Verification primitives ────────────────────────────────────────
|
|
145
|
+
/**
|
|
146
|
+
* Verify an Ed25519 signature over `bytes` using `publicKey` (32-byte
|
|
147
|
+
* raw key). Returns `true` if the signature is authentic; `false`
|
|
148
|
+
* otherwise. `crypto.subtle.verify` is async so the function returns a
|
|
149
|
+
* `Promise<boolean>`; callers `yield* call(() => verifyBundle(...))` to
|
|
150
|
+
* bridge.
|
|
151
|
+
*/
|
|
152
|
+
async function verifyBundle(bytes, signatureBase64, publicKey) {
|
|
153
|
+
let signature;
|
|
154
|
+
try {
|
|
155
|
+
signature = base64ToBytes(signatureBase64);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
if (publicKey.byteLength !== 32)
|
|
161
|
+
return false;
|
|
162
|
+
if (signature.byteLength !== 64)
|
|
163
|
+
return false;
|
|
164
|
+
const key = await crypto.subtle.importKey('raw', toArrayBuffer(publicKey), { name: 'Ed25519' }, false, ['verify']);
|
|
165
|
+
return crypto.subtle.verify({ name: 'Ed25519' }, key, toArrayBuffer(signature), toArrayBuffer(bytes));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Canonical-JSON encoding for signature payloads. Sorts object keys
|
|
169
|
+
* recursively and emits compact (no-whitespace) output. Arrays preserve
|
|
170
|
+
* insertion order. Numbers, booleans, null, and strings round-trip via
|
|
171
|
+
* `JSON.stringify`. Sufficient for `signedAt: ISO8601`, `publisherKeyId:
|
|
172
|
+
* string`, and the `entries` tree (all string / number primitives).
|
|
173
|
+
*
|
|
174
|
+
* Not a full RFC 8785 implementation — explicitly. The catalog schema
|
|
175
|
+
* is constrained to JSON types this helper handles correctly, and an
|
|
176
|
+
* RFC 8785 dep would be overkill for the surface area.
|
|
177
|
+
*/
|
|
178
|
+
function canonicalJson(value) {
|
|
179
|
+
if (value === null || typeof value !== 'object') {
|
|
180
|
+
return JSON.stringify(value);
|
|
181
|
+
}
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
184
|
+
}
|
|
185
|
+
const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
186
|
+
return `{${entries
|
|
187
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`)
|
|
188
|
+
.join(',')}}`;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Compute the signed payload bytes for a catalog: canonical-JSON of
|
|
192
|
+
* `{ signedAt, entries, publisherKeyId }`, UTF-8 encoded. Used by both
|
|
193
|
+
* the verifier (here) and the signer (out-of-repo publish tooling).
|
|
194
|
+
*/
|
|
195
|
+
function catalogSignedBytes(signedAt, entries, publisherKeyId) {
|
|
196
|
+
const json = canonicalJson({ signedAt, entries, publisherKeyId });
|
|
197
|
+
return new TextEncoder().encode(json);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Fetch the catalog from {@link CHANNEL_CATALOG_URL}, verify its
|
|
201
|
+
* signature against {@link CHANNEL_TRUST_ROOTS}, and return the verified
|
|
202
|
+
* structure. Memoized per-process per effective URL.
|
|
203
|
+
*/
|
|
204
|
+
function* fetchAndVerifyCatalog() {
|
|
205
|
+
const url = getCatalogUrl();
|
|
206
|
+
const cached = catalogCache.get(url);
|
|
207
|
+
if (cached)
|
|
208
|
+
return cached.catalog;
|
|
209
|
+
const response = yield* (0, cancellable_fetch_1.cancellableFetch)(url);
|
|
210
|
+
if (!response.ok) {
|
|
211
|
+
throw new BundleVerificationError(`Catalog fetch from ${url} returned HTTP ${response.status} ${response.statusText}.`);
|
|
212
|
+
}
|
|
213
|
+
const text = yield* (0, effection_1.call)(() => response.text());
|
|
214
|
+
let catalog;
|
|
215
|
+
try {
|
|
216
|
+
catalog = JSON.parse(text);
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
throw new BundleVerificationError(`Catalog at ${url} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
220
|
+
}
|
|
221
|
+
if (typeof catalog.signedAt !== 'string' ||
|
|
222
|
+
!Array.isArray(catalog.entries) ||
|
|
223
|
+
typeof catalog.publisherKeyId !== 'string' ||
|
|
224
|
+
typeof catalog.signature !== 'string') {
|
|
225
|
+
throw new BundleVerificationError(`Catalog at ${url} is missing required fields (signedAt, entries, publisherKeyId, signature).`);
|
|
226
|
+
}
|
|
227
|
+
const trustKey = getTrustRoots().get(catalog.publisherKeyId);
|
|
228
|
+
if (!trustKey) {
|
|
229
|
+
throw new BundleVerificationError(`Catalog at ${url} is signed by publisherKeyId="${catalog.publisherKeyId}" ` +
|
|
230
|
+
`which is not in CHANNEL_TRUST_ROOTS. The framework refuses to trust ` +
|
|
231
|
+
`keys it does not vendor.`);
|
|
232
|
+
}
|
|
233
|
+
const signedBytes = catalogSignedBytes(catalog.signedAt, catalog.entries, catalog.publisherKeyId);
|
|
234
|
+
const ok = yield* (0, effection_1.call)(() => verifyBundle(signedBytes, catalog.signature, trustKey));
|
|
235
|
+
if (!ok) {
|
|
236
|
+
throw new BundleVerificationError(`Catalog at ${url} failed Ed25519 signature verification ` +
|
|
237
|
+
`(publisherKeyId="${catalog.publisherKeyId}"). The catalog was tampered with ` +
|
|
238
|
+
`or the publisher's signing key has changed without a corresponding rig update.`);
|
|
239
|
+
}
|
|
240
|
+
catalogCache.set(url, { catalog, bytes: signedBytes });
|
|
241
|
+
return catalog;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Resolve a name + optional semver range against the verified catalog.
|
|
245
|
+
* Returns the highest-matching version's catalog entry, or throws
|
|
246
|
+
* {@link AppNotFoundError} if the name is absent or no version matches.
|
|
247
|
+
*
|
|
248
|
+
* Consumers (notably the `harness.dev install` CLI) then fetch the
|
|
249
|
+
* returned `manifestUrl` + `tarballUrl`, run {@link verifyBundle}
|
|
250
|
+
* against the manifest's signature over the tarball bytes, and shell
|
|
251
|
+
* out to `npm install <tarballUrl>` to install the verified package.
|
|
252
|
+
*/
|
|
253
|
+
function* resolveAppEntry(name, opts = {}) {
|
|
254
|
+
const catalog = yield* fetchAndVerifyCatalog();
|
|
255
|
+
const entry = catalog.entries.find((e) => e.name === name);
|
|
256
|
+
if (!entry) {
|
|
257
|
+
throw new AppNotFoundError(`App "${name}" is not listed in the catalog at ${getCatalogUrl()}.`);
|
|
258
|
+
}
|
|
259
|
+
const range = opts.semver;
|
|
260
|
+
const matching = range
|
|
261
|
+
? entry.versions.filter((v) => {
|
|
262
|
+
try {
|
|
263
|
+
return (0, semver_1.satisfies)(v.version, range);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
: [...entry.versions];
|
|
270
|
+
if (matching.length === 0) {
|
|
271
|
+
const available = entry.versions.map((v) => v.version).join(', ') || '(none published)';
|
|
272
|
+
throw new AppNotFoundError(`App "${name}" has no version matching "${range ?? '*'}". ` +
|
|
273
|
+
`Published versions: ${available}.`);
|
|
274
|
+
}
|
|
275
|
+
matching.sort((a, b) => (0, semver_1.rcompare)(a.version, b.version));
|
|
276
|
+
return matching[0];
|
|
277
|
+
}
|
|
278
|
+
// ── Byte helpers ───────────────────────────────────────────────────
|
|
279
|
+
function base64ToBytes(b64) {
|
|
280
|
+
const bin = atob(b64);
|
|
281
|
+
const out = new Uint8Array(bin.length);
|
|
282
|
+
for (let i = 0; i < bin.length; i++)
|
|
283
|
+
out[i] = bin.charCodeAt(i);
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Coerce a `Uint8Array` whose underlying buffer is `ArrayBufferLike`
|
|
288
|
+
* (could be SharedArrayBuffer-backed) into a fresh `ArrayBuffer` copy.
|
|
289
|
+
* WebCrypto's typed signature rejects `SharedArrayBuffer`-backed inputs.
|
|
290
|
+
*/
|
|
291
|
+
function toArrayBuffer(view) {
|
|
292
|
+
const buf = new ArrayBuffer(view.byteLength);
|
|
293
|
+
new Uint8Array(buf).set(view);
|
|
294
|
+
return buf;
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=bundle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle.js","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cancellableFetch(url, init, opts)` — Effection-native HTTP with
|
|
3
|
+
* scope-linked cancellation and a deadline timeout.
|
|
4
|
+
*
|
|
5
|
+
* Wraps the global `fetch` so:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Outer-scope halt aborts the in-flight request.** The Effection
|
|
8
|
+
* `useAbortSignal()` returns a signal linked to the current scope;
|
|
9
|
+
* when the scope halts (because a containing operation throws,
|
|
10
|
+
* `race` chose another leg, the harness is cancelled, etc.) the
|
|
11
|
+
* signal aborts and the underlying socket closes. The fetch is
|
|
12
|
+
* *genuinely* cancelled — not abandoned.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Timeout aborts the in-flight request.** A second leg sleeps for
|
|
15
|
+
* `opts.timeoutMs` and throws on completion. `race` halts whichever
|
|
16
|
+
* leg loses, propagating the abort the same way an outer halt does.
|
|
17
|
+
*
|
|
18
|
+
* Three current/planned consumers route through this primitive:
|
|
19
|
+
*
|
|
20
|
+
* - `@lloyal-labs/web-app/src/tools/fetch-page.ts` (post-migration —
|
|
21
|
+
* replaces raw `AbortController` + `setTimeout`, closes the latent
|
|
22
|
+
* socket-leak bug where a halted pool kept fetch-page sockets open).
|
|
23
|
+
* - `@lloyal-labs/web-app/src/tools/keyless-search.ts` (post-migration
|
|
24
|
+
* — replaces the private `fetchWithTimeout` from which this primitive
|
|
25
|
+
* was lifted; zero behavior change, just consolidation).
|
|
26
|
+
* - `@lloyal-labs/rig/src/bundle.ts` (`resolveAppEntry`) — fetches the
|
|
27
|
+
* signed catalog via `cancellableFetch` so a halted scope during
|
|
28
|
+
* resolution tears down cleanly. Used by `harness.dev install` to
|
|
29
|
+
* resolve names against the canonical channel.
|
|
30
|
+
*
|
|
31
|
+
* Third-party apps SHOULD use `cancellableFetch` for any HTTP they do
|
|
32
|
+
* under structured concurrency, rather than reinventing the
|
|
33
|
+
* `race + useAbortSignal` pattern.
|
|
34
|
+
*
|
|
35
|
+
* @packageDocumentation
|
|
36
|
+
* @category Contract
|
|
37
|
+
*/
|
|
38
|
+
import type { Operation } from 'effection';
|
|
39
|
+
/**
|
|
40
|
+
* Options accepted by {@link cancellableFetch}.
|
|
41
|
+
*/
|
|
42
|
+
export interface CancellableFetchOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Maximum time in milliseconds before the fetch is aborted with a
|
|
45
|
+
* {@link FetchTimeoutError}. Default: 30000 (30 seconds). Set to
|
|
46
|
+
* `Infinity` to disable the timeout (cancellation via outer scope
|
|
47
|
+
* still works).
|
|
48
|
+
*/
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Inject a non-default `fetch` implementation for testing or for
|
|
52
|
+
* harnesses that proxy network access. Defaults to the global `fetch`.
|
|
53
|
+
*/
|
|
54
|
+
fetchImpl?: typeof fetch;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Thrown when the timeout leg of `cancellableFetch` wins the race.
|
|
58
|
+
* Distinct from generic `Error` so consumers can catch only the
|
|
59
|
+
* timeout case (e.g., to retry with a longer timeout) without also
|
|
60
|
+
* catching network-layer errors thrown from the fetch leg.
|
|
61
|
+
*/
|
|
62
|
+
export declare class FetchTimeoutError extends Error {
|
|
63
|
+
constructor(url: string, timeoutMs: number);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Fetch a URL with Effection-scope-linked cancellation and a timeout.
|
|
67
|
+
*
|
|
68
|
+
* @param url - The URL to fetch.
|
|
69
|
+
* @param init - Standard `RequestInit` options. `init.signal` is *replaced*
|
|
70
|
+
* by the Effection-scope-linked signal; callers cannot pass their own
|
|
71
|
+
* AbortController. (If you want to compose with an external abort
|
|
72
|
+
* source, do it at the outer-scope level — halting the outer scope
|
|
73
|
+
* propagates here.)
|
|
74
|
+
* @param opts - Timeout + injection knobs.
|
|
75
|
+
* @returns The `Response` object — unread. Callers `yield* call(() => res.text())`
|
|
76
|
+
* etc. as appropriate to their use case.
|
|
77
|
+
*
|
|
78
|
+
* @throws {FetchTimeoutError} If the timeout leg wins.
|
|
79
|
+
* @throws Network-layer errors thrown by the underlying `fetch` (e.g., DNS
|
|
80
|
+
* resolution failure, TLS error, socket reset). When the abort signal
|
|
81
|
+
* fires, `fetch` throws a `DOMException` with `name === 'AbortError'` —
|
|
82
|
+
* distinguishable from real network errors by that name.
|
|
83
|
+
*
|
|
84
|
+
* **Body buffering.** The returned `Response`'s body is **fully buffered
|
|
85
|
+
* in memory** before the function returns; the caller may safely call
|
|
86
|
+
* `.text()` / `.json()` / `.arrayBuffer()` on it without further async
|
|
87
|
+
* coordination. This is load-bearing: the underlying `useAbortSignal()`
|
|
88
|
+
* aborts when the http leg's scope unwinds (after `race` resolves),
|
|
89
|
+
* which would otherwise cause the caller's body-read to throw
|
|
90
|
+
* `AbortError` mid-flight (the body is still bound to the request's
|
|
91
|
+
* signal in undici). Pre-consuming inside the http leg, before the
|
|
92
|
+
* signal aborts, sidesteps that interaction. Cost: streaming is not
|
|
93
|
+
* supported — all responses are fully resident before return. Acceptable
|
|
94
|
+
* for the consumers we have (catalog JSON, manifest JSON, signed
|
|
95
|
+
* bundles up to a few hundred KB).
|
|
96
|
+
*/
|
|
97
|
+
export declare function cancellableFetch(url: string, init?: RequestInit, opts?: CancellableFetchOptions): Operation<Response>;
|
|
98
|
+
//# sourceMappingURL=cancellable-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancellable-fetch.d.ts","sourceRoot":"","sources":["../src/cancellable-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED;;;;;GAKG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAI3C;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAiB,gBAAgB,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,WAAW,EAClB,IAAI,CAAC,EAAE,uBAAuB,GAC7B,SAAS,CAAC,QAAQ,CAAC,CAwCrB"}
|