@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.
Files changed (100) hide show
  1. package/LICENSE +107 -0
  2. package/LICENSE-FAQ.md +256 -0
  3. package/README.md +93 -74
  4. package/dist/bundle.d.ts +211 -0
  5. package/dist/bundle.d.ts.map +1 -0
  6. package/dist/bundle.js +296 -0
  7. package/dist/bundle.js.map +1 -0
  8. package/dist/cancellable-fetch.d.ts +98 -0
  9. package/dist/cancellable-fetch.d.ts.map +1 -0
  10. package/dist/cancellable-fetch.js +133 -0
  11. package/dist/cancellable-fetch.js.map +1 -0
  12. package/dist/config-store.d.ts +30 -0
  13. package/dist/config-store.d.ts.map +1 -0
  14. package/dist/config-store.js +45 -0
  15. package/dist/config-store.js.map +1 -0
  16. package/dist/define-app.d.ts +98 -0
  17. package/dist/define-app.d.ts.map +1 -0
  18. package/dist/define-app.js +232 -0
  19. package/dist/define-app.js.map +1 -0
  20. package/dist/grant-store.d.ts +31 -0
  21. package/dist/grant-store.d.ts.map +1 -0
  22. package/dist/grant-store.js +49 -0
  23. package/dist/grant-store.js.map +1 -0
  24. package/dist/index.d.ts +13 -6
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +37 -11
  27. package/dist/index.js.map +1 -1
  28. package/dist/node.d.ts +3 -2
  29. package/dist/node.d.ts.map +1 -1
  30. package/dist/node.js +3 -2
  31. package/dist/node.js.map +1 -1
  32. package/dist/protocol.d.ts +155 -0
  33. package/dist/protocol.d.ts.map +1 -0
  34. package/dist/protocol.js +184 -0
  35. package/dist/protocol.js.map +1 -0
  36. package/dist/registry.d.ts +87 -0
  37. package/dist/registry.d.ts.map +1 -0
  38. package/dist/registry.js +245 -0
  39. package/dist/registry.js.map +1 -0
  40. package/dist/reranker.d.ts +25 -7
  41. package/dist/reranker.d.ts.map +1 -1
  42. package/dist/reranker.js +103 -63
  43. package/dist/reranker.js.map +1 -1
  44. package/dist/resources/types.d.ts +10 -37
  45. package/dist/resources/types.d.ts.map +1 -1
  46. package/dist/resources/types.js +12 -0
  47. package/dist/resources/types.js.map +1 -1
  48. package/dist/spine-render.d.ts +97 -0
  49. package/dist/spine-render.d.ts.map +1 -0
  50. package/dist/spine-render.js +121 -0
  51. package/dist/spine-render.js.map +1 -0
  52. package/dist/tools/index.d.ts +26 -22
  53. package/dist/tools/index.d.ts.map +1 -1
  54. package/dist/tools/index.js +24 -28
  55. package/dist/tools/index.js.map +1 -1
  56. package/dist/tools/keyless-search.d.ts +67 -0
  57. package/dist/tools/keyless-search.d.ts.map +1 -0
  58. package/dist/tools/keyless-search.js +401 -0
  59. package/dist/tools/keyless-search.js.map +1 -0
  60. package/dist/tools/plan.d.ts +31 -4
  61. package/dist/tools/plan.d.ts.map +1 -1
  62. package/dist/tools/plan.js +46 -11
  63. package/dist/tools/plan.js.map +1 -1
  64. package/dist/tools/types.d.ts +12 -56
  65. package/dist/tools/types.d.ts.map +1 -1
  66. package/dist/tools/types.js +17 -0
  67. package/dist/tools/types.js.map +1 -1
  68. package/dist/tools/web-search.d.ts +9 -25
  69. package/dist/tools/web-search.d.ts.map +1 -1
  70. package/dist/tools/web-search.js +11 -119
  71. package/dist/tools/web-search.js.map +1 -1
  72. package/package.json +10 -7
  73. package/dist/sources/corpus.d.ts +0 -80
  74. package/dist/sources/corpus.d.ts.map +0 -1
  75. package/dist/sources/corpus.js +0 -100
  76. package/dist/sources/corpus.js.map +0 -1
  77. package/dist/sources/index.d.ts +0 -12
  78. package/dist/sources/index.d.ts.map +0 -1
  79. package/dist/sources/index.js +0 -14
  80. package/dist/sources/index.js.map +0 -1
  81. package/dist/sources/web.d.ts +0 -67
  82. package/dist/sources/web.d.ts.map +0 -1
  83. package/dist/sources/web.js +0 -104
  84. package/dist/sources/web.js.map +0 -1
  85. package/dist/tools/fetch-page.d.ts +0 -48
  86. package/dist/tools/fetch-page.d.ts.map +0 -1
  87. package/dist/tools/fetch-page.js +0 -309
  88. package/dist/tools/fetch-page.js.map +0 -1
  89. package/dist/tools/grep.d.ts +0 -35
  90. package/dist/tools/grep.d.ts.map +0 -1
  91. package/dist/tools/grep.js +0 -84
  92. package/dist/tools/grep.js.map +0 -1
  93. package/dist/tools/read-file.d.ts +0 -74
  94. package/dist/tools/read-file.d.ts.map +0 -1
  95. package/dist/tools/read-file.js +0 -192
  96. package/dist/tools/read-file.js.map +0 -1
  97. package/dist/tools/search.d.ts +0 -34
  98. package/dist/tools/search.d.ts.map +0 -1
  99. package/dist/tools/search.js +0 -101
  100. package/dist/tools/search.js.map +0 -1
@@ -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"}