@rhinestone/shared-configs 1.7.16 → 1.8.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.
@@ -0,0 +1,460 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const bun_test_1 = require("bun:test");
4
+ const node_crypto_1 = require("node:crypto");
5
+ const chainFactsClient_1 = require("../chainFactsClient");
6
+ const index_1 = require("../index");
7
+ const packageVersion_1 = require("../generated/packageVersion");
8
+ const REMOTE_URL = "https://example.invalid/chain-facts.json";
9
+ const SIGNATURE_URL = `${REMOTE_URL}.sig`;
10
+ const ETHEREUM = String(index_1.MainnetNetwork.ETHEREUM);
11
+ // A real P-256 keypair, signing in DER exactly as KMS ECDSA_SHA_256 does, so
12
+ // these tests exercise the same bytes production will see rather than a
13
+ // convenient stand-in.
14
+ function makeSigner() {
15
+ const { publicKey, privateKey } = (0, node_crypto_1.generateKeyPairSync)("ec", {
16
+ namedCurve: "P-256",
17
+ });
18
+ return {
19
+ publicKey: publicKey.export({ type: "spki", format: "der" }).toString("base64"),
20
+ sign: (bytes) => (0, node_crypto_1.sign)("sha256", Buffer.from(bytes), privateKey).toString("base64"),
21
+ };
22
+ }
23
+ const signer = makeSigner();
24
+ const realFetch = globalThis.fetch;
25
+ (0, bun_test_1.afterEach)(() => {
26
+ globalThis.fetch = realFetch;
27
+ });
28
+ /** Routes by URL, since a read now fetches the artifact and its detached signature. */
29
+ function stubFetch(handler) {
30
+ globalThis.fetch = ((input, init) => handler(String(input), init));
31
+ }
32
+ /** Serves `body` with a valid signature over its exact serialized bytes. */
33
+ function serveSigned(body, signWith = signer) {
34
+ const text = JSON.stringify(body);
35
+ const signature = signWith.sign(text);
36
+ stubFetch(async (url) => url === SIGNATURE_URL ? new Response(signature) : new Response(text));
37
+ return text;
38
+ }
39
+ // Defaults to the FULL bundled registry: a payload missing chains the bundled
40
+ // registry has is rejected as stale, so a partial fixture would be testing the
41
+ // staleness guard rather than whatever the test is actually about.
42
+ // Version defaults to the bundled PACKAGE_VERSION: an artifact older than the
43
+ // bundled registry is rejected as stale, so a fixture with an arbitrary version
44
+ // would be testing the staleness guard rather than its actual subject.
45
+ function payload(chains = index_1.chainRegistry, version = packageVersion_1.PACKAGE_VERSION) {
46
+ return { version, chains };
47
+ }
48
+ function bumpedVersion() {
49
+ const [major, minor, patch] = packageVersion_1.PACKAGE_VERSION.split("-")[0].split(".");
50
+ return `${major}.${minor}.${Number(patch) + 1}`;
51
+ }
52
+ function olderVersion() {
53
+ const [major, minor, patch] = packageVersion_1.PACKAGE_VERSION.split("-")[0].split(".");
54
+ return `${major}.${minor}.${Math.max(0, Number(patch) - 1)}`;
55
+ }
56
+ function withEthereum(overrides) {
57
+ return {
58
+ ...index_1.chainRegistry,
59
+ [ETHEREUM]: { ...index_1.chainRegistry[ETHEREUM], ...overrides },
60
+ };
61
+ }
62
+ /** The bundled registry minus one chain — the shape a stale artifact has. */
63
+ function registryMissingOneChain() {
64
+ const partial = { ...index_1.chainRegistry };
65
+ delete partial[ETHEREUM];
66
+ return partial;
67
+ }
68
+ function clientWith(extra = {}) {
69
+ return (0, chainFactsClient_1.createChainFactsClient)({
70
+ remoteUrl: REMOTE_URL,
71
+ publicKey: signer.publicKey,
72
+ ...extra,
73
+ });
74
+ }
75
+ (0, bun_test_1.describe)("createChainFactsClient", () => {
76
+ (0, bun_test_1.describe)("tiers", () => {
77
+ (0, bun_test_1.it)("serves bundled before any refresh", () => {
78
+ const snapshot = (0, chainFactsClient_1.createChainFactsClient)().getSnapshot();
79
+ (0, bun_test_1.expect)(snapshot.source).toBe("bundled");
80
+ (0, bun_test_1.expect)(snapshot.version).toBeNull();
81
+ (0, bun_test_1.expect)(snapshot.chains).toBe(index_1.chainRegistry);
82
+ });
83
+ (0, bun_test_1.it)("serves bundled when remoteUrl is explicitly disabled", async () => {
84
+ let fetched = false;
85
+ stubFetch(async () => {
86
+ fetched = true;
87
+ return new Response("{}");
88
+ });
89
+ const client = (0, chainFactsClient_1.createChainFactsClient)({ remoteUrl: null });
90
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
91
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
92
+ (0, bun_test_1.expect)(fetched).toBe(false);
93
+ });
94
+ (0, bun_test_1.it)("defaults to the published artifact URL and bundled key", async () => {
95
+ // Asserts the wiring without reaching the network: the stub records where
96
+ // an unconfigured client would have gone.
97
+ const seen = [];
98
+ stubFetch(async (url) => {
99
+ seen.push(url);
100
+ throw new Error("no network in tests");
101
+ });
102
+ const client = (0, chainFactsClient_1.createChainFactsClient)();
103
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
104
+ (0, bun_test_1.expect)(seen).toContain("https://facts.rhinestone.dev/latest.json");
105
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
106
+ });
107
+ (0, bun_test_1.it)("installs the remote payload on a successful refresh", async () => {
108
+ serveSigned(payload());
109
+ const client = clientWith();
110
+ (0, bun_test_1.expect)(await client.refresh()).toBe(true);
111
+ const snapshot = client.getSnapshot();
112
+ (0, bun_test_1.expect)(snapshot.source).toBe("remote");
113
+ (0, bun_test_1.expect)(snapshot.version).toBe(packageVersion_1.PACKAGE_VERSION);
114
+ (0, bun_test_1.expect)(Object.keys(client.getChainRegistry())).toEqual(Object.keys(index_1.chainRegistry));
115
+ });
116
+ (0, bun_test_1.it)("accepts a remote payload that adds chains beyond the bundled set", async () => {
117
+ serveSigned(payload({ ...index_1.chainRegistry, "424242": index_1.chainRegistry[ETHEREUM] }));
118
+ const client = clientWith();
119
+ (0, bun_test_1.expect)(await client.refresh()).toBe(true);
120
+ (0, bun_test_1.expect)(client.getChainRegistry()["424242"]).toBeDefined();
121
+ });
122
+ (0, bun_test_1.it)("keeps an installed remote snapshot when a later refresh fails", async () => {
123
+ const text = JSON.stringify(payload());
124
+ const signature = signer.sign(text);
125
+ let call = 0;
126
+ stubFetch(async (url) => {
127
+ call += 1;
128
+ if (call > 2)
129
+ throw new Error("network blip");
130
+ return url === SIGNATURE_URL
131
+ ? new Response(signature)
132
+ : new Response(text);
133
+ });
134
+ const client = clientWith();
135
+ (0, bun_test_1.expect)(await client.refresh()).toBe(true);
136
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
137
+ // The tier only moves forwards — no downgrade back to bundled.
138
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("remote");
139
+ (0, bun_test_1.expect)(client.getSnapshot().version).toBe(packageVersion_1.PACKAGE_VERSION);
140
+ });
141
+ });
142
+ (0, bun_test_1.describe)("concurrent refreshes", () => {
143
+ // Without single-flight this reorders: the first call's fetch is held open
144
+ // while the second completes and installs v2, then the first resolves and
145
+ // puts v1 back — silently hiding whatever v2 added.
146
+ (0, bun_test_1.it)("does not let an overlapping refresh install a stale snapshot", async () => {
147
+ const v1 = JSON.stringify(payload());
148
+ const v2 = JSON.stringify(payload(index_1.chainRegistry, bumpedVersion()));
149
+ const sig1 = signer.sign(v1);
150
+ const sig2 = signer.sign(v2);
151
+ let releaseFirst;
152
+ const firstHeld = new Promise((resolve) => {
153
+ releaseFirst = resolve;
154
+ });
155
+ let payloadFetches = 0;
156
+ stubFetch(async (url) => {
157
+ if (url === SIGNATURE_URL) {
158
+ return new Response(payloadFetches <= 1 ? sig1 : sig2);
159
+ }
160
+ payloadFetches += 1;
161
+ if (payloadFetches === 1) {
162
+ await firstHeld; // hold the first read open
163
+ return new Response(v1);
164
+ }
165
+ return new Response(v2);
166
+ });
167
+ const client = clientWith();
168
+ const first = client.refresh();
169
+ const second = client.refresh();
170
+ // Single-flight: the second caller joins the first rather than racing it.
171
+ releaseFirst?.();
172
+ const [a, b] = await Promise.all([first, second]);
173
+ (0, bun_test_1.expect)(a).toBe(true);
174
+ (0, bun_test_1.expect)(b).toBe(true);
175
+ (0, bun_test_1.expect)(payloadFetches).toBe(1);
176
+ // Whatever won, the snapshot is never left older than what was installed.
177
+ (0, bun_test_1.expect)(client.getSnapshot().version).toBe(packageVersion_1.PACKAGE_VERSION);
178
+ });
179
+ (0, bun_test_1.it)("fetches again on a sequential refresh, once the previous one settled", async () => {
180
+ let fetches = 0;
181
+ const text = JSON.stringify(payload());
182
+ const signature = signer.sign(text);
183
+ stubFetch(async (url) => {
184
+ if (url === SIGNATURE_URL)
185
+ return new Response(signature);
186
+ fetches += 1;
187
+ return new Response(text);
188
+ });
189
+ const client = clientWith();
190
+ (0, bun_test_1.expect)(await client.refresh()).toBe(true);
191
+ (0, bun_test_1.expect)(await client.refresh()).toBe(true);
192
+ (0, bun_test_1.expect)(fetches).toBe(2);
193
+ });
194
+ });
195
+ (0, bun_test_1.describe)("staleness", () => {
196
+ // A signature proves provenance, not freshness. If the CDN publish fails
197
+ // while npm succeeds, a consumer that bumps has a bundled registry newer
198
+ // than latest.json — validly signed, and it would otherwise override the
199
+ // newer bundled data and hide the change behind a successful read.
200
+ (0, bun_test_1.it)("rejects a validly signed artifact from an older release", async () => {
201
+ serveSigned(payload(index_1.chainRegistry, olderVersion()));
202
+ const phases = [];
203
+ const errors = [];
204
+ const client = clientWith({
205
+ onError: (e, phase) => {
206
+ phases.push(phase);
207
+ errors.push(e);
208
+ },
209
+ });
210
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
211
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
212
+ (0, bun_test_1.expect)(phases).toEqual(["stale"]);
213
+ (0, bun_test_1.expect)(String(errors[0])).toContain(packageVersion_1.PACKAGE_VERSION);
214
+ });
215
+ // The case chain-id comparison missed: same chains, older content.
216
+ (0, bun_test_1.it)("rejects an older release even when it carries every bundled chain id", async () => {
217
+ const sameKeysOlder = { ...index_1.chainRegistry };
218
+ serveSigned(payload(sameKeysOlder, olderVersion()));
219
+ const client = clientWith();
220
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
221
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
222
+ });
223
+ (0, bun_test_1.it)("accepts an artifact from the same release as the bundled registry", async () => {
224
+ serveSigned(payload());
225
+ const client = clientWith();
226
+ (0, bun_test_1.expect)(await client.refresh()).toBe(true);
227
+ });
228
+ (0, bun_test_1.it)("accepts a newer release", async () => {
229
+ serveSigned(payload(index_1.chainRegistry, bumpedVersion()));
230
+ const client = clientWith();
231
+ (0, bun_test_1.expect)(await client.refresh()).toBe(true);
232
+ (0, bun_test_1.expect)(client.getSnapshot().version).toBe(bumpedVersion());
233
+ });
234
+ // An unparseable version is a pipeline bug to observe, not a reason to stop
235
+ // taking chain updates — the signature already establishes provenance.
236
+ (0, bun_test_1.it)("accepts an artifact whose version cannot be compared", async () => {
237
+ serveSigned(payload(index_1.chainRegistry, "not-a-semver"));
238
+ const client = clientWith();
239
+ (0, bun_test_1.expect)(await client.refresh()).toBe(true);
240
+ });
241
+ });
242
+ (0, bun_test_1.describe)("signature verification", () => {
243
+ (0, bun_test_1.it)("refuses to read remotely when the publicKey is explicitly cleared", async () => {
244
+ serveSigned(payload());
245
+ const phases = [];
246
+ const client = (0, chainFactsClient_1.createChainFactsClient)({
247
+ remoteUrl: REMOTE_URL,
248
+ publicKey: null,
249
+ onError: (_error, phase) => phases.push(phase),
250
+ });
251
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
252
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
253
+ (0, bun_test_1.expect)(phases).toEqual(["config"]);
254
+ });
255
+ (0, bun_test_1.it)("rejects a payload signed by a key other than the bundled one", async () => {
256
+ // The default publicKey is the real KMS key, so a locally-signed payload
257
+ // must fail — this is what stops a rogue endpoint being trusted.
258
+ serveSigned(payload());
259
+ const phases = [];
260
+ const client = (0, chainFactsClient_1.createChainFactsClient)({
261
+ remoteUrl: REMOTE_URL,
262
+ onError: (_error, phase) => phases.push(phase),
263
+ });
264
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
265
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
266
+ (0, bun_test_1.expect)(phases).toEqual(["verify"]);
267
+ });
268
+ (0, bun_test_1.it)("rejects a payload signed by a different key", async () => {
269
+ serveSigned(payload(), makeSigner());
270
+ const phases = [];
271
+ const client = clientWith({
272
+ onError: (_e, phase) => phases.push(phase),
273
+ });
274
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
275
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
276
+ (0, bun_test_1.expect)(phases).toEqual(["verify"]);
277
+ });
278
+ (0, bun_test_1.it)("rejects a payload whose bytes were tampered with after signing", async () => {
279
+ const original = JSON.stringify(payload());
280
+ const signature = signer.sign(original);
281
+ // Flip a byte that certainly exists, so the test really tampers.
282
+ const tampered = original.replace('"name":"Ethereum"', '"name":"Ethereuq"');
283
+ (0, bun_test_1.expect)(tampered).not.toBe(original);
284
+ stubFetch(async (url) => url === SIGNATURE_URL
285
+ ? new Response(signature)
286
+ : new Response(tampered));
287
+ const phases = [];
288
+ const client = clientWith({
289
+ onError: (_e, phase) => phases.push(phase),
290
+ });
291
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
292
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
293
+ (0, bun_test_1.expect)(phases).toEqual(["verify"]);
294
+ });
295
+ (0, bun_test_1.it)("rejects a malformed signature without throwing", async () => {
296
+ stubFetch(async (url) => url === SIGNATURE_URL
297
+ ? new Response("bm90LWEtc2lnbmF0dXJl")
298
+ : new Response(JSON.stringify(payload())));
299
+ const phases = [];
300
+ const client = clientWith({
301
+ onError: (_e, phase) => phases.push(phase),
302
+ });
303
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
304
+ (0, bun_test_1.expect)(phases).toEqual(["verify"]);
305
+ });
306
+ (0, bun_test_1.it)("degrades to bundled when the signature is missing", async () => {
307
+ stubFetch(async (url) => url === SIGNATURE_URL
308
+ ? new Response("", { status: 404, statusText: "Not Found" })
309
+ : new Response(JSON.stringify(payload())));
310
+ const phases = [];
311
+ const client = clientWith({
312
+ onError: (_e, phase) => phases.push(phase),
313
+ });
314
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
315
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
316
+ (0, bun_test_1.expect)(phases).toEqual(["fetch"]);
317
+ });
318
+ // ECDSA is randomised, so r and s vary per signature: roughly half have a
319
+ // high bit set (DER prepends 0x00) and some are shorter than 32 bytes
320
+ // (leading zeroes dropped). Both need left-padding into the fixed-width r‖s
321
+ // form WebCrypto wants, and getting it wrong fails only for *some*
322
+ // signatures — so sign repeatedly rather than once.
323
+ (0, bun_test_1.it)("verifies across many signatures, covering DER padding variants", async () => {
324
+ for (let i = 0; i < 25; i += 1) {
325
+ const body = payload();
326
+ body.version = `v${i}`;
327
+ serveSigned(body);
328
+ const client = clientWith();
329
+ (0, bun_test_1.expect)(await client.refresh()).toBe(true);
330
+ (0, bun_test_1.expect)(client.getSnapshot().version).toBe(`v${i}`);
331
+ }
332
+ });
333
+ });
334
+ (0, bun_test_1.describe)("never throws", () => {
335
+ bun_test_1.it.each([
336
+ [
337
+ "a network error",
338
+ () => stubFetch(async () => {
339
+ throw new Error("ECONNREFUSED");
340
+ }),
341
+ "fetch",
342
+ ],
343
+ [
344
+ "a non-2xx response",
345
+ () => stubFetch(async () => new Response("", { status: 404, statusText: "Not Found" })),
346
+ "fetch",
347
+ ],
348
+ [
349
+ "a body that isn't JSON",
350
+ () => serveSigned("<html>502</html>"),
351
+ "parse",
352
+ ],
353
+ ])("degrades to bundled on %s", async (_label, arrange, expectedPhase) => {
354
+ arrange();
355
+ const phases = [];
356
+ const client = clientWith({
357
+ onError: (_e, phase) => phases.push(phase),
358
+ });
359
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
360
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
361
+ (0, bun_test_1.expect)(phases).toEqual([expectedPhase]);
362
+ });
363
+ bun_test_1.it.each([
364
+ ["a payload with no version", { chains: index_1.chainRegistry }],
365
+ ["a payload with no chains", { version: "v1" }],
366
+ ])("degrades to bundled on %s", async (_label, body) => {
367
+ serveSigned(body);
368
+ const phases = [];
369
+ const client = clientWith({
370
+ onError: (_e, phase) => phases.push(phase),
371
+ });
372
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
373
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
374
+ (0, bun_test_1.expect)(phases).toEqual(["parse"]);
375
+ });
376
+ (0, bun_test_1.it)("aborts a stalled response body rather than hanging", async () => {
377
+ // Headers land immediately; the body stream never completes and only
378
+ // errors when the request's AbortSignal fires, as real fetch does.
379
+ stubFetch(async (_url, init) => new Response(new ReadableStream({
380
+ start(controller) {
381
+ init?.signal?.addEventListener("abort", () => controller.error(new DOMException("Aborted", "AbortError")));
382
+ },
383
+ })));
384
+ const errors = [];
385
+ const client = clientWith({
386
+ fetchTimeoutMs: 20,
387
+ onError: (error) => errors.push(error),
388
+ });
389
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
390
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
391
+ (0, bun_test_1.expect)(errors).toHaveLength(1);
392
+ }, 1000);
393
+ (0, bun_test_1.it)("survives an onError callback that itself throws", async () => {
394
+ stubFetch(async () => {
395
+ throw new Error("ECONNREFUSED");
396
+ });
397
+ const client = clientWith({
398
+ onError: () => {
399
+ throw new Error("buggy logging hook");
400
+ },
401
+ });
402
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
403
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
404
+ });
405
+ });
406
+ (0, bun_test_1.describe)("payload shape", () => {
407
+ // Guards against the shape check drifting stricter than the real generated
408
+ // data: if it ever rejected chains.json, every consumer would silently sit
409
+ // on bundled forever.
410
+ (0, bun_test_1.it)("accepts the bundled registry itself, so the check matches real data", async () => {
411
+ serveSigned({ version: "mirrors-bundled", chains: index_1.chainRegistry });
412
+ const errors = [];
413
+ const client = clientWith({ onError: (e) => errors.push(e) });
414
+ (0, bun_test_1.expect)(await client.refresh()).toBe(true);
415
+ (0, bun_test_1.expect)(errors).toEqual([]);
416
+ (0, bun_test_1.expect)(client.getSnapshot().version).toBe("mirrors-bundled");
417
+ });
418
+ // Enum *values* are deliberately not validated, so a new settlement layer
419
+ // or quoter doesn't require every consumer to ship a release first.
420
+ (0, bun_test_1.it)("accepts unknown enum values and unknown keys, for forward compatibility", async () => {
421
+ serveSigned(payload(withEthereum({
422
+ settlementLayers: ["ACROSS", "SOME_FUTURE_LAYER"],
423
+ swapQuoters: ["some-future-quoter"],
424
+ someFieldThisBuildPredates: { nested: true },
425
+ })));
426
+ const client = clientWith();
427
+ (0, bun_test_1.expect)(await client.refresh()).toBe(true);
428
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("remote");
429
+ });
430
+ bun_test_1.it.each([
431
+ ["chains is an empty object", {}],
432
+ ["chains is an array", []],
433
+ ["a key is not a chain id", { "not-a-chain-id": index_1.chainRegistry[ETHEREUM] }],
434
+ ["a key has a leading zero", { "01": index_1.chainRegistry[ETHEREUM] }],
435
+ ["an entry is not an object", { "1": "nope" }],
436
+ ])("rejects a registry where %s", async (_label, chains) => {
437
+ serveSigned(payload(chains));
438
+ const client = clientWith();
439
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
440
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
441
+ });
442
+ bun_test_1.it.each([
443
+ ["name", { name: 5 }],
444
+ ["tokens", { tokens: "not-an-array" }],
445
+ ])("rejects an entry with a malformed %s", async (_label, overrides) => {
446
+ serveSigned(payload(withEthereum(overrides)));
447
+ const client = clientWith();
448
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
449
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
450
+ });
451
+ // The realistic misconfiguration: pointing the URL at another artifact that
452
+ // is also keyed by chain id.
453
+ (0, bun_test_1.it)("rejects a payload that is a different chain-id-keyed blob", async () => {
454
+ serveSigned(payload({ "1": { providers: ["DRPC", "Alchemy"] } }));
455
+ const client = clientWith();
456
+ (0, bun_test_1.expect)(await client.refresh()).toBe(false);
457
+ (0, bun_test_1.expect)(client.getSnapshot().source).toBe("bundled");
458
+ });
459
+ });
460
+ });
@@ -0,0 +1,57 @@
1
+ import type { ChainRegistry } from "./types";
2
+ /** Wire format of the published facts artifact. */
3
+ interface ChainFactsPayload {
4
+ version: string;
5
+ chains: ChainRegistry;
6
+ }
7
+ type ChainFactsSource = "remote" | "bundled";
8
+ type ChainFactsErrorPhase = "config" | "fetch" | "verify" | "parse" | "stale";
9
+ interface ChainFactsSnapshot {
10
+ chains: ChainRegistry;
11
+ source: ChainFactsSource;
12
+ /** null for "bundled", which ships with the package and has no version stamp of its own. */
13
+ version: string | null;
14
+ }
15
+ interface ChainFactsClientOptions {
16
+ /**
17
+ * URL serving the facts JSON, with its detached signature at `<url>.sig`.
18
+ * Defaults to the published artifact; pass `null` to disable remote reads and
19
+ * always serve the bundled registry.
20
+ */
21
+ remoteUrl?: string | null;
22
+ /**
23
+ * Base64 SPKI DER of the ECDSA P-256 public key the artifact is signed with —
24
+ * i.e. exactly what `aws kms get-public-key --query PublicKey --output text`
25
+ * returns.
26
+ *
27
+ * Defaults to the key bundled with this package, so verification needs no
28
+ * configuration. Because the payload checks are only structural, this
29
+ * signature is what establishes that the facts came from the release
30
+ * pipeline; without it a remote read would be trusting whatever answered the
31
+ * URL. Explicitly clearing it therefore disables the remote tier rather than
32
+ * reading unverified — there is deliberately no way to opt out of
33
+ * verification while still reading remotely.
34
+ */
35
+ publicKey?: string | null;
36
+ fetchTimeoutMs?: number;
37
+ /** Called on every rejected read. Reads never throw, so this is the only way to observe one. */
38
+ onError?: (error: unknown, phase: ChainFactsErrorPhase) => void;
39
+ }
40
+ interface ChainFactsClient {
41
+ /** Fetches and installs the remote facts if usable. Resolves to whether they were installed; never rejects. */
42
+ refresh(): Promise<boolean>;
43
+ /** The active facts: remote once a refresh has succeeded, otherwise bundled. */
44
+ getSnapshot(): ChainFactsSnapshot;
45
+ getChainRegistry(): ChainRegistry;
46
+ }
47
+ /**
48
+ * Reads chain facts remote-first, falling back to the registry bundled with
49
+ * this package at publish time. Reads never throw — a failed or malformed
50
+ * fetch leaves the previous tier in place, so callers always get a usable
51
+ * registry. `getSnapshot().source` reports which tier served it, so consumers
52
+ * can alert on having run bundled for longer than expected.
53
+ */
54
+ declare function createChainFactsClient(options?: ChainFactsClientOptions): ChainFactsClient;
55
+ export { createChainFactsClient };
56
+ export type { ChainFactsClient, ChainFactsClientOptions, ChainFactsErrorPhase, ChainFactsPayload, ChainFactsSnapshot, ChainFactsSource, };
57
+ //# sourceMappingURL=chainFactsClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chainFactsClient.d.ts","sourceRoot":"","sources":["../../src/chainFactsClient.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAI7C,mDAAmD;AACnD,UAAU,iBAAiB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,KAAK,gBAAgB,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE7C,KAAK,oBAAoB,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;AAE9E,UAAU,kBAAkB;IAC1B,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,gBAAgB,CAAC;IACzB,4FAA4F;IAC5F,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,UAAU,uBAAuB;IAC/B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gGAAgG;IAChG,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;CACjE;AAED,UAAU,gBAAgB;IACxB,+GAA+G;IAC/G,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5B,gFAAgF;IAChF,WAAW,IAAI,kBAAkB,CAAC;IAClC,gBAAgB,IAAI,aAAa,CAAC;CACnC;AAyKD;;;;;;GAMG;AACH,iBAAS,sBAAsB,CAC7B,OAAO,GAAE,uBAA4B,GACpC,gBAAgB,CAyKlB;AAED,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAClC,YAAY,EACV,gBAAgB,EAChB,uBAAuB,EACvB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,GACjB,CAAC"}