@absol-labs/agent 0.7.3 → 0.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.
Files changed (37) hide show
  1. package/README.md +19 -4
  2. package/dist/gateway/caller-auth-gateway.d.ts +103 -2
  3. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  4. package/dist/gateway/caller-auth-gateway.js +176 -19
  5. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  6. package/dist/gateway/http-server.d.ts +12 -0
  7. package/dist/gateway/http-server.d.ts.map +1 -1
  8. package/dist/gateway/http-server.js +45 -1
  9. package/dist/gateway/http-server.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/wallet/autonomous-wallet-store.d.ts +123 -0
  15. package/dist/wallet/autonomous-wallet-store.d.ts.map +1 -0
  16. package/dist/wallet/autonomous-wallet-store.js +318 -0
  17. package/dist/wallet/autonomous-wallet-store.js.map +1 -0
  18. package/dist/wallet/autonomous-wallet.d.ts +14 -39
  19. package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
  20. package/dist/wallet/autonomous-wallet.js +12 -145
  21. package/dist/wallet/autonomous-wallet.js.map +1 -1
  22. package/dist/wallet/encrypted-file-credential-store.d.ts +41 -0
  23. package/dist/wallet/encrypted-file-credential-store.d.ts.map +1 -0
  24. package/dist/wallet/encrypted-file-credential-store.js +221 -0
  25. package/dist/wallet/encrypted-file-credential-store.js.map +1 -0
  26. package/dist/wallet/secret-service-probe.d.ts +56 -0
  27. package/dist/wallet/secret-service-probe.d.ts.map +1 -0
  28. package/dist/wallet/secret-service-probe.js +407 -0
  29. package/dist/wallet/secret-service-probe.js.map +1 -0
  30. package/package.json +1 -1
  31. package/src/gateway/caller-auth-gateway.ts +281 -15
  32. package/src/gateway/http-server.ts +64 -0
  33. package/src/index.ts +19 -0
  34. package/src/wallet/autonomous-wallet-store.ts +487 -0
  35. package/src/wallet/autonomous-wallet.ts +57 -224
  36. package/src/wallet/encrypted-file-credential-store.ts +341 -0
  37. package/src/wallet/secret-service-probe.ts +487 -0
@@ -22,6 +22,36 @@ import {
22
22
  * called. Revocation is automatic: once a stream is closed/expired/reclaimed,
23
23
  * the on-chain read step below fails closed and access stops with no extra
24
24
  * bookkeeping.
25
+ *
26
+ * ## Stream state must never be trusted from a single unverified view (#87)
27
+ *
28
+ * A capability presented seconds after the buyer's `close()` was mined was
29
+ * served, because authorization read the stream from ONE unpinned `latest`
30
+ * view and believed it unconditionally: the exposure window was exactly
31
+ * "however stale that view happened to be" - unbounded and unobservable.
32
+ * Provider replica lag, an unsafe-head reorg, provider response caching, or a
33
+ * cache added later for performance all land in the same failure mode, and it
34
+ * fails OPEN on the one transition that must be immediate.
35
+ *
36
+ * Three fail-closed properties remove that trust:
37
+ *
38
+ * 1. **Corroboration** - every configured view is read (bounded: one read per
39
+ * view, in parallel) and the request is authorized ONLY if EVERY view
40
+ * authorizes. Any view reporting a non-authorizable stream - or failing to
41
+ * read at all - denies. Independent endpoints go stale independently, so
42
+ * configuring one corroborating RPC collapses the window to the freshest of
43
+ * them.
44
+ * 2. **Freshness bound** - a view that reports the block it observed
45
+ * ({@link GatewayStreamView.observedAt}) is rejected outright when that
46
+ * block is older than {@link CallerAuthGatewayConfig.maxStateStalenessSeconds},
47
+ * turning silent staleness into an explicit `stream-state-stale` denial.
48
+ * 3. **Monotonic revocation** - `StreamEscrowV2` only ever assigns
49
+ * `status = Closed` (never resets it), only ever decreases `deposit` (there
50
+ * is no top-up), and fixes `expiresAt` at open. "Not active", "expired" and
51
+ * "fully claimed" are therefore irreversible, so an observed terminal state
52
+ * is remembered and a later view reporting ACTIVE - which is provably stale
53
+ * - can never re-authorize. This is the only correctness-preserving caching
54
+ * direction here: negative, never positive.
25
55
  */
26
56
 
27
57
  /** Machine-readable, closed set of rejection reasons - every failure mode is distinct. */
@@ -38,7 +68,49 @@ export type CallerAuthDenialReason =
38
68
  | "buyer-mismatch"
39
69
  | "stream-not-active"
40
70
  | "stream-expired"
41
- | "stream-not-funded";
71
+ | "stream-not-funded"
72
+ | "stream-state-stale";
73
+
74
+ /**
75
+ * Default freshness bound for on-chain stream state, in seconds. Base blocks
76
+ * are ~2s, so a healthy endpoint is a few seconds stale at most; this is a
77
+ * generous ceiling whose job is to reject GROSS staleness rather than to
78
+ * micro-manage a healthy provider.
79
+ */
80
+ export const DEFAULT_MAX_STATE_STALENESS_SECONDS = 30;
81
+
82
+ /** Maximum corroborating views the bounded hot path accepts (excludes the primary). */
83
+ export const MAX_CORROBORATING_STREAM_READERS = 3;
84
+
85
+ /**
86
+ * Denial precedence when views disagree: the most authoritative statement about
87
+ * the stream wins, so a single fresh "closed" is never masked by another view's
88
+ * read error or staleness.
89
+ */
90
+ const DENIAL_PRECEDENCE: readonly CallerAuthDenialReason[] = [
91
+ "stream-not-active",
92
+ "stream-expired",
93
+ "buyer-mismatch",
94
+ "service-ref-mismatch",
95
+ "stream-not-funded",
96
+ "stream-state-stale",
97
+ "stream-not-found",
98
+ ];
99
+
100
+ /**
101
+ * Denials that reflect an IRREVERSIBLE on-chain state (see the class docs):
102
+ * once observed, they hold forever, so they are remembered and a stale ACTIVE
103
+ * read can never re-authorize.
104
+ *
105
+ * Deliberately limited to states derived PURELY from on-chain values. Expiry is
106
+ * equally irreversible but is derived from the local clock, and a skewed clock
107
+ * must not be able to permanently revoke a live stream - expiry re-denies on
108
+ * every request anyway, at no extra cost.
109
+ */
110
+ const TERMINAL_DENIALS: ReadonlySet<CallerAuthDenialReason> = new Set([
111
+ "stream-not-active",
112
+ "stream-not-funded",
113
+ ]);
42
114
 
43
115
  export interface CallerAuthDecision {
44
116
  readonly authorized: boolean;
@@ -59,6 +131,18 @@ export interface GatewayStreamView {
59
131
  readonly claimedCumulative: bigint;
60
132
  readonly expiresAt: number;
61
133
  readonly status: "active" | "closed";
134
+ /**
135
+ * The chain head this view was observed against. Readers SHOULD report it:
136
+ * when present, the gateway rejects the view outright if the block is older
137
+ * than its freshness bound (`stream-state-stale`) instead of trusting
138
+ * unbounded staleness. Optional only so `StreamReader` implementations
139
+ * predating this field keep working; the default SDK reader always reports it.
140
+ */
141
+ readonly observedAt?: {
142
+ readonly blockNumber: bigint;
143
+ /** Unix seconds of the observed block. */
144
+ readonly blockTimestamp: number;
145
+ };
62
146
  }
63
147
 
64
148
  /** Pluggable on-chain stream reader, so the gateway is testable without a live RPC. */
@@ -66,6 +150,48 @@ export interface StreamReader {
66
150
  getStream(streamId: Hex): Promise<GatewayStreamView>;
67
151
  }
68
152
 
153
+ /**
154
+ * Remembers streams observed in an irreversible terminal state, so a later
155
+ * stale view cannot re-authorize them. Negative-only by construction: nothing
156
+ * that would GRANT access is ever cached.
157
+ */
158
+ export interface StreamRevocationSet {
159
+ /** The remembered terminal denial for this stream, if any. */
160
+ get(streamId: Hex): CallerAuthDenialReason | undefined;
161
+ /** Records an irreversible terminal denial for this stream. */
162
+ record(streamId: Hex, reason: CallerAuthDenialReason): void;
163
+ }
164
+
165
+ /**
166
+ * In-memory {@link StreamRevocationSet}. Bounded (oldest entries are evicted
167
+ * past {@link maxEntries}) so a hostile caller cannot grow it without limit;
168
+ * eviction is safe because the on-chain read still denies an evicted stream -
169
+ * it only loses the short-circuit.
170
+ */
171
+ export class InMemoryStreamRevocationSet implements StreamRevocationSet {
172
+ private readonly revoked = new Map<string, CallerAuthDenialReason>();
173
+
174
+ constructor(private readonly maxEntries = 10_000) {}
175
+
176
+ get(streamId: Hex): CallerAuthDenialReason | undefined {
177
+ return this.revoked.get(streamId.toLowerCase());
178
+ }
179
+
180
+ record(streamId: Hex, reason: CallerAuthDenialReason): void {
181
+ const key = streamId.toLowerCase();
182
+ if (this.revoked.has(key)) {
183
+ return;
184
+ }
185
+ if (this.revoked.size >= this.maxEntries) {
186
+ const oldest = this.revoked.keys().next();
187
+ if (!oldest.done) {
188
+ this.revoked.delete(oldest.value);
189
+ }
190
+ }
191
+ this.revoked.set(key, reason);
192
+ }
193
+ }
194
+
69
195
  export class UnknownStreamGatewayError extends Error {
70
196
  constructor(message: string, options?: { readonly cause?: unknown }) {
71
197
  super(message, options);
@@ -85,7 +211,14 @@ export function createSdkStreamReader(options: {
85
211
  return {
86
212
  async getStream(streamId) {
87
213
  try {
88
- const stream = await metrik.getStreamV2(streamId);
214
+ // Read the stream and the chain head this endpoint is serving in one
215
+ // round trip (concurrent, same transport) so the view can be
216
+ // freshness-checked. A failed head read is treated as an unusable view
217
+ // (fail closed) rather than silently dropping the freshness signal.
218
+ const [stream, block] = await Promise.all([
219
+ metrik.getStreamV2(streamId),
220
+ metrik.publicClient.getBlock({ blockTag: "latest" }),
221
+ ]);
89
222
  return {
90
223
  buyer: stream.buyer,
91
224
  serviceRef: stream.serviceRef,
@@ -93,6 +226,10 @@ export function createSdkStreamReader(options: {
93
226
  claimedCumulative: stream.claimedCumulative,
94
227
  expiresAt: stream.expiresAt,
95
228
  status: stream.status,
229
+ observedAt: {
230
+ blockNumber: block.number,
231
+ blockTimestamp: Number(block.timestamp),
232
+ },
96
233
  };
97
234
  } catch (cause) {
98
235
  throw new UnknownStreamGatewayError(
@@ -153,6 +290,28 @@ export interface CallerAuthGatewayConfig {
153
290
  readonly headerName?: string;
154
291
  /** Override the on-chain reader (tests inject a fake; default is the real SDK reader). */
155
292
  readonly streamReader?: StreamReader;
293
+ /**
294
+ * Independent corroborating views of the SAME escrow. Every view is read per
295
+ * request (in parallel) and the request is authorized only if EVERY view
296
+ * authorizes - so a stale primary cannot serve a closed stream. Bounded to
297
+ * {@link MAX_CORROBORATING_STREAM_READERS}.
298
+ */
299
+ readonly corroboratingStreamReaders?: readonly StreamReader[];
300
+ /**
301
+ * Corroborating RPC endpoints for the default SDK reader path. One reader is
302
+ * built per URL; ignored when `corroboratingStreamReaders` is supplied.
303
+ * Use endpoints on DIFFERENT providers - two URLs on one provider share its
304
+ * staleness.
305
+ */
306
+ readonly corroboratingRpcUrls?: readonly string[];
307
+ /**
308
+ * Reject stream state observed more than this many seconds behind the wall
309
+ * clock. Default {@link DEFAULT_MAX_STATE_STALENESS_SECONDS}. Enforced only
310
+ * for views that report {@link GatewayStreamView.observedAt}.
311
+ */
312
+ readonly maxStateStalenessSeconds?: number;
313
+ /** Override the terminal-state memory (default in-memory, bounded). */
314
+ readonly revocationSet?: StreamRevocationSet;
156
315
  /** Override the replay cache (tests inject a fake; default is in-memory). */
157
316
  readonly nonceCache?: NonceReplayCache;
158
317
  /** Injectable clock (unix seconds), for tests. */
@@ -174,7 +333,9 @@ export interface CallerAuthRequestInput {
174
333
  * See `./http-server.ts` for a ready-to-run reverse-proxy reference server.
175
334
  */
176
335
  export class CallerAuthGateway {
177
- private readonly streamReader: StreamReader;
336
+ private readonly streamReaders: readonly StreamReader[];
337
+ private readonly maxStateStalenessSeconds: number;
338
+ private readonly revocationSet: StreamRevocationSet;
178
339
  private readonly nonceCache: NonceReplayCache;
179
340
  private readonly headerName: string;
180
341
  private readonly domain: InvocationCapabilityDomainInput;
@@ -201,12 +362,35 @@ export class CallerAuthGateway {
201
362
  this.headerName = (
202
363
  config.headerName ?? CAPABILITY_HEADER_NAME
203
364
  ).toLowerCase();
204
- this.streamReader =
365
+ const primaryReader =
205
366
  config.streamReader ??
206
367
  createSdkStreamReader({
207
368
  escrowAddress: config.escrowAddress,
208
369
  rpcUrl: config.rpcUrl as string,
209
370
  });
371
+ const corroborating =
372
+ config.corroboratingStreamReaders ??
373
+ (config.corroboratingRpcUrls ?? []).map((rpcUrl) =>
374
+ createSdkStreamReader({
375
+ escrowAddress: config.escrowAddress,
376
+ rpcUrl,
377
+ }),
378
+ );
379
+ if (corroborating.length > MAX_CORROBORATING_STREAM_READERS) {
380
+ throw new Error(
381
+ `at most ${MAX_CORROBORATING_STREAM_READERS} corroborating stream readers are supported (bounded hot path)`,
382
+ );
383
+ }
384
+ this.streamReaders = [primaryReader, ...corroborating];
385
+
386
+ const maxStaleness =
387
+ config.maxStateStalenessSeconds ?? DEFAULT_MAX_STATE_STALENESS_SECONDS;
388
+ if (!Number.isFinite(maxStaleness) || maxStaleness <= 0) {
389
+ throw new Error("maxStateStalenessSeconds must be a positive number");
390
+ }
391
+ this.maxStateStalenessSeconds = maxStaleness;
392
+ this.revocationSet =
393
+ config.revocationSet ?? new InMemoryStreamRevocationSet();
210
394
  this.nonceCache = config.nonceCache ?? new InMemoryNonceReplayCache();
211
395
  this.domain = {
212
396
  chainId: config.chainId ?? 84532,
@@ -272,33 +456,115 @@ export class CallerAuthGateway {
272
456
  return deny("nonce-replayed", 403, capability);
273
457
  }
274
458
 
275
- let stream: GatewayStreamView;
276
- try {
277
- stream = await this.streamReader.getStream(capability.streamId as Hex);
278
- } catch {
279
- return deny("stream-not-found", 403, capability);
459
+ const streamId = capability.streamId as Hex;
460
+
461
+ // An irreversible terminal state already observed for this stream holds
462
+ // forever: no read can un-close it, so short-circuit (and never re-read a
463
+ // dead stream).
464
+ const revoked = this.revocationSet.get(streamId);
465
+ if (revoked !== undefined) {
466
+ return deny(revoked, httpStatusFor(revoked), capability);
280
467
  }
281
468
 
469
+ // Read EVERY configured view (bounded: one read per view, all in parallel).
470
+ const results = await Promise.allSettled(
471
+ this.streamReaders.map((reader) => reader.getStream(streamId)),
472
+ );
473
+
474
+ // Strictest view wins. A view that fails to read, is measurably stale, or
475
+ // reports a non-authorizable stream denies the request - authorization
476
+ // requires unanimous consent from every view.
477
+ const denials: CallerAuthDenialReason[] = [];
478
+ const views: GatewayStreamView[] = [];
479
+ for (const result of results) {
480
+ if (result.status === "rejected") {
481
+ denials.push("stream-not-found");
482
+ continue;
483
+ }
484
+ const reason = this.evaluateView(result.value, capability, nowSeconds);
485
+ if (reason === undefined) {
486
+ views.push(result.value);
487
+ } else {
488
+ denials.push(reason);
489
+ }
490
+ }
491
+
492
+ if (denials.length > 0) {
493
+ const reason = strictestDenial(denials);
494
+ if (TERMINAL_DENIALS.has(reason)) {
495
+ this.revocationSet.record(streamId, reason);
496
+ }
497
+ return deny(reason, httpStatusFor(reason), capability);
498
+ }
499
+
500
+ // Report the freshest corroborated view to the caller.
501
+ return { authorized: true, stream: freshestView(views), capability };
502
+ }
503
+
504
+ /** Returns the denial reason this single view produces, or `undefined` if it authorizes. */
505
+ private evaluateView(
506
+ stream: GatewayStreamView,
507
+ capability: SignedInvocationCapability,
508
+ nowSeconds: number,
509
+ ): CallerAuthDenialReason | undefined {
510
+ // Freshness first: a measurably stale view is not evidence of anything, so
511
+ // it must not be able to vouch for an ACTIVE stream.
512
+ if (
513
+ stream.observedAt !== undefined &&
514
+ nowSeconds - stream.observedAt.blockTimestamp >
515
+ this.maxStateStalenessSeconds
516
+ ) {
517
+ return "stream-state-stale";
518
+ }
282
519
  if (stream.buyer.toLowerCase() !== capability.buyer.toLowerCase()) {
283
- return deny("buyer-mismatch", 403, capability);
520
+ return "buyer-mismatch";
284
521
  }
285
522
  if (
286
523
  stream.serviceRef.toLowerCase() !== capability.serviceRef.toLowerCase()
287
524
  ) {
288
- return deny("service-ref-mismatch", 403, capability);
525
+ return "service-ref-mismatch";
289
526
  }
290
527
  if (stream.status !== "active") {
291
- return deny("stream-not-active", 403, capability);
528
+ return "stream-not-active";
292
529
  }
293
530
  if (nowSeconds >= stream.expiresAt) {
294
- return deny("stream-expired", 403, capability);
531
+ return "stream-expired";
295
532
  }
296
533
  if (stream.deposit <= stream.claimedCumulative) {
297
- return deny("stream-not-funded", 402, capability);
534
+ return "stream-not-funded";
535
+ }
536
+ return undefined;
537
+ }
538
+ }
539
+
540
+ function httpStatusFor(reason: CallerAuthDenialReason): 402 | 403 {
541
+ return reason === "stream-not-funded" ? 402 : 403;
542
+ }
543
+
544
+ function strictestDenial(
545
+ denials: readonly CallerAuthDenialReason[],
546
+ ): CallerAuthDenialReason {
547
+ for (const candidate of DENIAL_PRECEDENCE) {
548
+ if (denials.includes(candidate)) {
549
+ return candidate;
298
550
  }
551
+ }
552
+ return denials[0] as CallerAuthDenialReason;
553
+ }
299
554
 
300
- return { authorized: true, stream, capability };
555
+ function freshestView(views: readonly GatewayStreamView[]): GatewayStreamView {
556
+ let freshest = views[0] as GatewayStreamView;
557
+ for (const view of views.slice(1)) {
558
+ const current = freshest.observedAt?.blockNumber;
559
+ const candidate = view.observedAt?.blockNumber;
560
+ if (
561
+ candidate !== undefined &&
562
+ (current === undefined || candidate > current)
563
+ ) {
564
+ freshest = view;
565
+ }
301
566
  }
567
+ return freshest;
302
568
  }
303
569
 
304
570
  function deny(
@@ -10,6 +10,8 @@ import { isAddress, isHex, type Address, type Hex } from "viem";
10
10
 
11
11
  import {
12
12
  CallerAuthGateway,
13
+ DEFAULT_MAX_STATE_STALENESS_SECONDS,
14
+ MAX_CORROBORATING_STREAM_READERS,
13
15
  type CallerAuthDenialReason,
14
16
  } from "./caller-auth-gateway.js";
15
17
 
@@ -236,6 +238,10 @@ export interface CallerAuthGatewayEnvConfig {
236
238
  readonly chainId: number;
237
239
  readonly upstreamUrl: string;
238
240
  readonly headerName?: string;
241
+ /** Independent RPCs used to corroborate stream state (see `CallerAuthGateway`). */
242
+ readonly corroboratingRpcUrls: readonly string[];
243
+ /** Freshness bound for on-chain stream state, in seconds. */
244
+ readonly maxStateStalenessSeconds: number;
239
245
  }
240
246
 
241
247
  /**
@@ -246,6 +252,14 @@ export interface CallerAuthGatewayEnvConfig {
246
252
  * - `METRIK_GATEWAY_SERVICE_REF` (required) - primary bytes32 service ref.
247
253
  * - `METRIK_GATEWAY_SERVICE_REFS` (optional) - comma-separated migration aliases.
248
254
  * - `METRIK_GATEWAY_UPSTREAM_URL` (required) - the real service to proxy to on pass.
255
+ * - `METRIK_GATEWAY_CORROBORATING_RPC_URLS` (optional, STRONGLY recommended) -
256
+ * comma-separated INDEPENDENT RPC endpoints for the same escrow. Stream state
257
+ * is authorized only if every configured view agrees, so a stale primary view
258
+ * cannot serve a stream that has just been closed. At most
259
+ * {@link MAX_CORROBORATING_STREAM_READERS}.
260
+ * - `METRIK_GATEWAY_MAX_STATE_STALENESS_SECONDS` (optional) - reject stream
261
+ * state observed more than N seconds behind the wall clock. Default
262
+ * {@link DEFAULT_MAX_STATE_STALENESS_SECONDS}.
249
263
  * - `METRIK_GATEWAY_CHAIN_ID` (optional) - default `84532` (Base Sepolia).
250
264
  * - `METRIK_GATEWAY_HEADER_NAME` (optional) - default `x-metrik-capability`.
251
265
  * - `PORT` / `METRIK_GATEWAY_PORT` (optional) - default `8787`.
@@ -295,6 +309,43 @@ export function parseCallerAuthGatewayEnvConfig(
295
309
  const host = env.METRIK_GATEWAY_HOST ?? "0.0.0.0";
296
310
  const headerName = env.METRIK_GATEWAY_HEADER_NAME;
297
311
 
312
+ const corroboratingRpcUrls = [
313
+ ...new Set(
314
+ (env.METRIK_GATEWAY_CORROBORATING_RPC_URLS ?? "")
315
+ .split(",")
316
+ .map((value) => value.trim())
317
+ .filter((value) => value.length > 0),
318
+ ),
319
+ ];
320
+ for (const candidate of corroboratingRpcUrls) {
321
+ try {
322
+ // eslint-disable-next-line no-new -- validates the URL is well-formed.
323
+ new URL(candidate);
324
+ } catch {
325
+ throw new Error(
326
+ `METRIK_GATEWAY_CORROBORATING_RPC_URLS must contain only valid URLs (got ${candidate})`,
327
+ );
328
+ }
329
+ }
330
+ if (corroboratingRpcUrls.length > MAX_CORROBORATING_STREAM_READERS) {
331
+ throw new Error(
332
+ `METRIK_GATEWAY_CORROBORATING_RPC_URLS accepts at most ${MAX_CORROBORATING_STREAM_READERS} URLs (bounded hot path)`,
333
+ );
334
+ }
335
+
336
+ const stalenessRaw =
337
+ env.METRIK_GATEWAY_MAX_STATE_STALENESS_SECONDS ??
338
+ String(DEFAULT_MAX_STATE_STALENESS_SECONDS);
339
+ const maxStateStalenessSeconds = Number(stalenessRaw);
340
+ if (
341
+ !Number.isInteger(maxStateStalenessSeconds) ||
342
+ maxStateStalenessSeconds <= 0
343
+ ) {
344
+ throw new Error(
345
+ `invalid METRIK_GATEWAY_MAX_STATE_STALENESS_SECONDS: ${stalenessRaw}`,
346
+ );
347
+ }
348
+
298
349
  return {
299
350
  host,
300
351
  port,
@@ -306,6 +357,8 @@ export function parseCallerAuthGatewayEnvConfig(
306
357
  ] as Hex[],
307
358
  chainId,
308
359
  upstreamUrl,
360
+ corroboratingRpcUrls,
361
+ maxStateStalenessSeconds,
309
362
  ...(headerName === undefined ? {} : { headerName }),
310
363
  };
311
364
  }
@@ -329,10 +382,21 @@ export async function startCallerAuthGatewayServerFromEnv(
329
382
  serviceRef: config.serviceRef,
330
383
  serviceRefs: config.serviceRefs,
331
384
  chainId: config.chainId,
385
+ corroboratingRpcUrls: config.corroboratingRpcUrls,
386
+ maxStateStalenessSeconds: config.maxStateStalenessSeconds,
332
387
  ...(config.headerName === undefined
333
388
  ? {}
334
389
  : { headerName: config.headerName }),
335
390
  });
391
+ if (config.corroboratingRpcUrls.length === 0) {
392
+ console.error(
393
+ "[metrik-caller-auth-gateway] WARNING: no METRIK_GATEWAY_CORROBORATING_RPC_URLS " +
394
+ "configured — stream state is authorized from a single RPC view. A stale view " +
395
+ "can authorize a stream that has just been closed (bounded to " +
396
+ `${config.maxStateStalenessSeconds}s by the freshness check). Configure at least ` +
397
+ "one INDEPENDENT provider to close that window.",
398
+ );
399
+ }
336
400
  const server = createCallerAuthGatewayServer({
337
401
  gateway,
338
402
  upstreamUrl: config.upstreamUrl,
package/src/index.ts CHANGED
@@ -154,26 +154,41 @@ export {
154
154
  export {
155
155
  AUTONOMOUS_WALLET_CAIP2,
156
156
  AUTONOMOUS_WALLET_CHAIN_ID,
157
+ AUTONOMOUS_WALLET_KEYCHAIN_SERVICE,
157
158
  AUTONOMOUS_WALLET_VERSION,
158
159
  METRIK_AUTONOMOUS_WALLET_BROKER_URL,
160
+ METRIK_WALLET_ENCRYPTION_KEY_ENV,
161
+ METRIK_WALLET_STORE_DIR_ENV,
159
162
  AutonomousWalletError,
163
+ EncryptedFileCredentialStore,
160
164
  HttpAutonomousWalletBroker,
161
165
  InMemoryAutonomousWalletStore,
162
166
  OsCredentialStore,
167
+ assertDurableCredentialStore,
163
168
  autonomousProofBytes,
164
169
  createAutonomousWalletAccount,
165
170
  createAutonomousWalletProvider,
166
171
  createDefaultAutonomousWalletStore,
172
+ probeSecretServiceAvailable,
167
173
  provisionAutonomousWallet,
168
174
  provisionMetrikAutonomousWallet,
175
+ type AssertDurableCredentialStoreOptions,
169
176
  type AutonomousWalletBroker as AutonomousWalletClientBroker,
170
177
  type AutonomousWalletCredentialStore,
171
178
  type AutonomousWalletProof,
172
179
  type AutonomousWalletRecord,
173
180
  type CreateAutonomousWalletProviderOptions,
181
+ type CredentialStoreDurability,
182
+ type CredentialStoreDurabilityReport,
183
+ type EncryptedFileCredentialStoreOptions,
184
+ type OsCredentialStoreOptions,
174
185
  type PreparedPrivyRequest,
186
+ type ProbeSecretServiceOptions,
175
187
  type ProvisionAutonomousWalletOptions,
176
188
  type ProvisionMetrikAutonomousWalletOptions,
189
+ type SecretEntryFactory,
190
+ type SecretEntryLike,
191
+ type SecretServiceProbeResult,
177
192
  } from "./wallet/autonomous-wallet.js";
178
193
 
179
194
  export {
@@ -271,7 +286,10 @@ export {
271
286
 
272
287
  export {
273
288
  CallerAuthGateway,
289
+ DEFAULT_MAX_STATE_STALENESS_SECONDS,
274
290
  InMemoryNonceReplayCache,
291
+ InMemoryStreamRevocationSet,
292
+ MAX_CORROBORATING_STREAM_READERS,
275
293
  UnknownStreamGatewayError,
276
294
  createSdkStreamReader,
277
295
  type CallerAuthDecision,
@@ -281,6 +299,7 @@ export {
281
299
  type GatewayStreamView,
282
300
  type NonceReplayCache,
283
301
  type StreamReader,
302
+ type StreamRevocationSet,
284
303
  } from "./gateway/caller-auth-gateway.js";
285
304
 
286
305
  export {