@cortexkit/opencode-openai-auth 1.2.0 → 1.4.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 (90) hide show
  1. package/dist/bin.js +8 -8
  2. package/dist/{codex-request-CjXETpTA.d.ts → codex-request-hw0XcldE.d.ts} +146 -29
  3. package/dist/index-1eq2s1cp.js +2 -0
  4. package/dist/{index-qp5hdpw8.js → index-1tdnrt68.js} +1 -1
  5. package/dist/index-5230x6s8.js +2 -0
  6. package/dist/{index-qxbchh2d.js → index-ag04nyv7.js} +7 -7
  7. package/dist/index-brbqqer5.js +90 -0
  8. package/dist/{index-xwvze96g.js → index-d98f1xdp.js} +1 -1
  9. package/dist/index-jcyrm58f.js +65 -0
  10. package/dist/{index-htvcmtcx.js → index-rsvnabbe.js} +2 -2
  11. package/dist/{index-ftkhhr6d.js → index-s88n2bjn.js} +1 -1
  12. package/dist/{index-bc89akak.js → index-t5m0dhmq.js} +1 -1
  13. package/dist/index-vy729a0b.js +69 -0
  14. package/dist/index-wgwze391.js +2 -0
  15. package/dist/{index-ffg0sgg5.js → index-yhw3hnsh.js} +2 -2
  16. package/dist/index.d.ts +11 -3
  17. package/dist/index.js +1 -1
  18. package/dist/{model-CQcw10oW.d.ts → model-DphbMQce.d.ts} +1 -1
  19. package/dist/{port-file-BDJnzgHH.d.ts → port-file-Cx-wwe5Q.d.ts} +1 -1
  20. package/dist/{protocol-C-kagE6O.d.ts → protocol-CDqdjh74.d.ts} +1 -1
  21. package/dist/rpc/port-file.d.ts +1 -1
  22. package/dist/rpc/port-file.js +1 -1
  23. package/dist/rpc/protocol.d.ts +1 -1
  24. package/dist/rpc/protocol.js +1 -1
  25. package/dist/rpc/rpc-client.d.ts +3 -3
  26. package/dist/rpc/rpc-client.js +1 -1
  27. package/dist/rpc/rpc-dir.js +1 -1
  28. package/dist/{sidebar-state-BfQ4mpZO.d.ts → sidebar-state-pTd9gg9W.d.ts} +71 -7
  29. package/dist/sidebar-state.d.ts +2 -2
  30. package/dist/sidebar-state.js +1 -1
  31. package/dist/{token-z6qq7a1d.js → token-0z5g59p8.js} +1 -1
  32. package/dist/token-util-jf9g6stk.js +1 -0
  33. package/dist/tui-preferences.js +1 -1
  34. package/dist/tui.d.ts +30 -3
  35. package/dist/v2/server.d.ts +2 -2
  36. package/dist/v2/server.js +2 -2
  37. package/package.json +6 -6
  38. package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +119 -6
  39. package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +294 -16
  40. package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +178 -26
  41. package/src/tui-compiled/raw/shared/2577ec69f9973ae9-quota-normalize.js +45 -1
  42. package/src/tui-compiled/raw/shared/30f5bc480f31f51c-roster.js +100 -43
  43. package/src/tui-compiled/raw/shared/375d9cca2def037d-private-files.js +10 -2
  44. package/src/tui-compiled/raw/shared/39c3cb05738fcf6d-projection.js +3 -1
  45. package/src/tui-compiled/raw/shared/42ae93ab554fcec2-open-url.js +7 -9
  46. package/src/tui-compiled/raw/shared/4825e03209d6ae3e-with-lock.js +2 -0
  47. package/src/tui-compiled/raw/shared/4f36d333d081d040-merge.js +17 -3
  48. package/src/tui-compiled/raw/shared/81dddd203f1b92a3-quota-writer.js +253 -0
  49. package/src/tui-compiled/raw/shared/905bffc0220f43d0-map.js +31 -6
  50. package/src/tui-compiled/raw/shared/94f53cfd572ba4bb-availability.js +106 -0
  51. package/src/tui-compiled/raw/shared/abd062d633e29fd2-pool-routing.js +116 -12
  52. package/src/tui-compiled/raw/shared/bfbdd0f9aae56b47-pool-quota.js +15 -3
  53. package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +348 -21
  54. package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +2 -2
  55. package/src/tui-compiled/raw/shared/dbc61cd405941828-accounts.js +5 -4
  56. package/src/tui-compiled/raw/shared/dc9290c2a7552ba6-watch.js +254 -0
  57. package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +5 -1
  58. package/src/tui-compiled/raw/sidebar-state.js +32 -3
  59. package/src/tui-compiled/raw/tui/v2-dialog.js +2 -1
  60. package/src/tui-compiled/raw/tui.tsx +170 -26
  61. package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +119 -6
  62. package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +294 -16
  63. package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +178 -26
  64. package/src/tui-compiled/runtime/shared/2577ec69f9973ae9-quota-normalize.js +45 -1
  65. package/src/tui-compiled/runtime/shared/30f5bc480f31f51c-roster.js +100 -43
  66. package/src/tui-compiled/runtime/shared/375d9cca2def037d-private-files.js +10 -2
  67. package/src/tui-compiled/runtime/shared/39c3cb05738fcf6d-projection.js +3 -1
  68. package/src/tui-compiled/runtime/shared/42ae93ab554fcec2-open-url.js +7 -9
  69. package/src/tui-compiled/runtime/shared/4825e03209d6ae3e-with-lock.js +2 -0
  70. package/src/tui-compiled/runtime/shared/4f36d333d081d040-merge.js +17 -3
  71. package/src/tui-compiled/runtime/shared/81dddd203f1b92a3-quota-writer.js +253 -0
  72. package/src/tui-compiled/runtime/shared/905bffc0220f43d0-map.js +31 -6
  73. package/src/tui-compiled/runtime/shared/94f53cfd572ba4bb-availability.js +106 -0
  74. package/src/tui-compiled/runtime/shared/abd062d633e29fd2-pool-routing.js +116 -12
  75. package/src/tui-compiled/runtime/shared/bfbdd0f9aae56b47-pool-quota.js +15 -3
  76. package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +348 -21
  77. package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +2 -2
  78. package/src/tui-compiled/runtime/shared/dbc61cd405941828-accounts.js +5 -4
  79. package/src/tui-compiled/runtime/shared/dc9290c2a7552ba6-watch.js +254 -0
  80. package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +5 -1
  81. package/src/tui-compiled/runtime/sidebar-state.js +32 -3
  82. package/src/tui-compiled/runtime/tui/v2-dialog.js +2 -1
  83. package/src/tui-compiled/runtime/tui.js +350 -219
  84. package/dist/index-03em8hv1.js +0 -69
  85. package/dist/index-6n4pnfkg.js +0 -2
  86. package/dist/index-ehjj89rx.js +0 -2
  87. package/dist/index-f21addzm.js +0 -90
  88. package/dist/index-r2s51ac1.js +0 -2
  89. package/dist/index-rwbrhkse.js +0 -65
  90. package/dist/token-util-cf0nyh7n.js +0 -1
@@ -1,11 +1,15 @@
1
1
  import { ClaustrumCredentialError, } from "@cortexkit/claustrum-client";
2
2
  import { createLogger } from "./cc9e0702a752cea7-index.js";
3
+ import { ClaustrumUnreachableError, classifyVaultFailure, } from "./94f53cfd572ba4bb-availability.js";
3
4
  import { readClaustrumEnrollmentToken } from "./b9588ad4725c1d10-enrollment.js";
4
5
  import { ClaustrumConsumerError } from "./ca0fccaba1d87af4-errors.js";
5
6
  import { CUSTODY_PLACEHOLDER_PREFIX } from "./d9d2892a4c371301-host-slot.js";
7
+ import { ScopedRosterWatch, } from "./dc9290c2a7552ba6-watch.js";
6
8
  /** Only a new version of the same account may replace an in-flight 401. */
7
9
  export function isScopedCredentialRotation(served, current) {
8
10
  return (current !== undefined &&
11
+ !current.fromKeptReceipt &&
12
+ !served.fromKeptReceipt &&
9
13
  current.credentialId === served.credentialId &&
10
14
  current.accountIdentity === served.accountIdentity &&
11
15
  current.recordVersion !== served.recordVersion);
@@ -45,6 +49,8 @@ export function decideScopedRetryAfter401(site, served, current, logger = defaul
45
49
  * so it cannot expire while a long request is still using it.
46
50
  */
47
51
  export const SERVING_MARGIN_MS = 300_000;
52
+ /** A kept receipt needs a minute of headroom, not the vault's refresh demand. */
53
+ export const KEPT_RECEIPT_MARGIN_MS = 60_000;
48
54
  function invalidMaterial() {
49
55
  return new ClaustrumConsumerError('invalid-material', 'Claustrum returned invalid credential material');
50
56
  }
@@ -100,11 +106,11 @@ function grantReachOf(listed) {
100
106
  }
101
107
  /**
102
108
  * Reads this consumer's vault credentials (list, fetch, 401 report), with the
103
- * enrollment token as authorization. Used by every host of a plugin. There is
104
- * deliberately no credential cache and no single-flight of credential reads:
105
- * each physical send is authorized by the vault, so a revoked enrollment or a
106
- * changed record takes effect on the next send. The enrollment token is
107
- * re-read per operation so an operator's reissue on disk is picked up.
109
+ * enrollment token as authorization. Used by every host of a plugin. Every
110
+ * physical send asks the vault first while reachable. Only an unreachable vault permits
111
+ * reuse of the last validated receipt, in memory and bounded by its expiry.
112
+ * The enrollment token is re-read per operation so revocation and disconnect
113
+ * still take effect without a process restart.
108
114
  */
109
115
  export class ClaustrumScopedCustody {
110
116
  #client;
@@ -116,7 +122,20 @@ export class ClaustrumScopedCustody {
116
122
  #logger;
117
123
  #provenance = new WeakMap();
118
124
  #reports = new WeakMap();
125
+ #kept = new Map();
126
+ #keptAttempts = new WeakMap();
127
+ #setTimeout;
128
+ #clearTimeout;
129
+ #outage = false;
130
+ #outageError = new ClaustrumUnreachableError();
131
+ #probeTimer;
132
+ #probing = false;
133
+ #lastProbeAt = -Infinity;
134
+ #probeDelay = 2_000;
119
135
  #closed = false;
136
+ /** The view of the last successful `discover()`, for `watchRoster`. */
137
+ #discoveredView;
138
+ #watches = new Set();
120
139
  constructor(options) {
121
140
  const tokenPath = options.tokenPath;
122
141
  if (options.readToken) {
@@ -134,6 +153,65 @@ export class ClaustrumScopedCustody {
134
153
  this.#requireAssertion = options.requireAssertion ?? false;
135
154
  this.#now = options.now ?? Date.now;
136
155
  this.#logger = options.logger ?? defaultLogger;
156
+ this.#setTimeout = options.setTimeoutImpl ?? setTimeout;
157
+ this.#clearTimeout = options.clearTimeoutImpl ?? clearTimeout;
158
+ }
159
+ /** A watch losing its transport is the same outage as an unreachable read. */
160
+ transportLost(error) {
161
+ if (this.#closed || this.#outage)
162
+ return;
163
+ this.#outage = true;
164
+ this.#outageError =
165
+ error instanceof ClaustrumCredentialError
166
+ ? error
167
+ : new ClaustrumUnreachableError();
168
+ this.#logger.warn('serving kept vault receipt', { reason: 'unreachable' });
169
+ this.#scheduleProbe();
170
+ }
171
+ /** Any vault answer ends transport fallback, even when it refuses access. */
172
+ reachable() {
173
+ if (this.#closed || !this.#outage)
174
+ return;
175
+ this.#outage = false;
176
+ if (this.#probeTimer)
177
+ this.#clearTimeout(this.#probeTimer);
178
+ this.#probeTimer = undefined;
179
+ this.#probeDelay = 2_000;
180
+ for (const kept of this.#kept.values())
181
+ kept.since = null;
182
+ (this.#logger.info ?? defaultLogger.info)('vault receipt serving resumed', { reason: 'reachable' });
183
+ }
184
+ #scheduleProbe() {
185
+ if (this.#closed || !this.#outage || this.#probeTimer || this.#probing)
186
+ return;
187
+ this.#probeTimer = this.#setTimeout(() => {
188
+ this.#probeTimer = undefined;
189
+ this.#probe(true);
190
+ }, this.#probeDelay);
191
+ this.#probeTimer.unref?.();
192
+ }
193
+ #probe(idle = false) {
194
+ if (this.#closed || !this.#outage || this.#probing)
195
+ return;
196
+ if (this.#now() - this.#lastProbeAt < 1_000) {
197
+ this.#scheduleProbe();
198
+ return;
199
+ }
200
+ this.#lastProbeAt = this.#now();
201
+ this.#probing = true;
202
+ // Both triggers share list_scoped discovery, including its receipt-drop rules.
203
+ // Sends never await discovery or let idle backoff delay the next send probe.
204
+ // A probe commits no roster, so it is not the host's discovery and does
205
+ // not move the view a roster watch compares notices against.
206
+ void this.#list()
207
+ .catch(() => {
208
+ if (idle && this.#outage)
209
+ this.#probeDelay = Math.min(this.#probeDelay * 2, 30_000);
210
+ })
211
+ .finally(() => {
212
+ this.#probing = false;
213
+ this.#scheduleProbe();
214
+ });
137
215
  }
138
216
  #check(signal) {
139
217
  if (this.#closed)
@@ -172,17 +250,24 @@ export class ClaustrumScopedCustody {
172
250
  }
173
251
  catch (error) {
174
252
  this.#check(signal);
253
+ if (classifyVaultFailure(error) === 'unreachable')
254
+ this.transportLost(error);
255
+ else
256
+ this.reachable();
175
257
  // Keep the vault's own refusals (ClaustrumCredentialError, carrying
176
258
  // code, class and action); replace any other error, whose text may echo
177
259
  // request params that include the enrollment token.
178
260
  if (error instanceof ClaustrumCredentialError)
179
261
  throw error;
262
+ if (classifyVaultFailure(error) === 'unreachable')
263
+ throw new ClaustrumUnreachableError();
180
264
  throw new ClaustrumConsumerError('unavailable', 'Claustrum scoped operation unavailable');
181
265
  }
182
266
  finally {
183
267
  removeAbortListener?.();
184
268
  }
185
269
  this.#check(signal);
270
+ this.reachable();
186
271
  return result;
187
272
  }
188
273
  /**
@@ -192,8 +277,27 @@ export class ClaustrumScopedCustody {
192
277
  * skipped records: they simply are not this consumer's.
193
278
  */
194
279
  async discover(signal) {
195
- const token = await this.#token(signal);
196
- const inventory = await this.#call(() => this.#client.listScoped(token), signal);
280
+ const inventory = await this.#list(signal);
281
+ this.#discoveredView = inventory.view;
282
+ return inventory;
283
+ }
284
+ async #list(signal) {
285
+ let inventory;
286
+ let token;
287
+ try {
288
+ token = await this.#token(signal);
289
+ }
290
+ catch (error) {
291
+ this.#kept.clear();
292
+ throw error;
293
+ }
294
+ try {
295
+ inventory = await this.#call(() => this.#client.listScoped(token), signal);
296
+ }
297
+ catch (error) {
298
+ this.listFailed(error);
299
+ throw error;
300
+ }
197
301
  const skipped = [];
198
302
  const candidates = [];
199
303
  const counts = new Map();
@@ -241,22 +345,119 @@ export class ClaustrumScopedCustody {
241
345
  };
242
346
  }
243
347
  /**
244
- * Fetch the credential for one physical send and wrap it in a fresh receipt.
245
- * The vault's served identity (or, without one, the plugin's parse of the
246
- * token) must equal the roster's when both are present. Without
247
- * `requireAssertion`, absence on either side proves nothing and does not
248
- * refuse; the receipt records what the vault asserted separately from what
249
- * the roster expected. With it, a reply that does not itself name the
250
- * credential id and the expected account is refused.
348
+ * Apply a failed account listing: any answer from the vault (a refusal such
349
+ * as a revoked enrollment's `not_found`) drops every kept receipt, while an
350
+ * unreachable vault keeps them. A vault watch that ends with the same
351
+ * refusal calls this too, so both paths treat it alike.
251
352
  */
252
- async authorize(identity, signal) {
353
+ listFailed(error) {
354
+ if (classifyVaultFailure(error) === 'unreachable')
355
+ this.transportLost(error);
356
+ else {
357
+ this.reachable();
358
+ this.#kept.clear();
359
+ }
360
+ }
361
+ /** Discard bearer material when a route is removed, declined or rebound. */
362
+ forget(credentialId) {
363
+ this.#kept.delete(credentialId);
364
+ }
365
+ /** Apply an already-held roster; this never fetches credentials or writes it. */
366
+ retain(identities) {
367
+ const current = new Map(identities.map((identity) => [identity.credentialId, identity]));
368
+ for (const [id, kept] of this.#kept) {
369
+ const identity = current.get(id);
370
+ if (!identity?.enabled ||
371
+ identity.state !== 'active' ||
372
+ identity.credentialType !== kept.attempt.credentialType ||
373
+ identity.accountIdentity !== kept.attempt.expectedAccountIdentity)
374
+ this.forget(id);
375
+ }
376
+ }
377
+ /** A frozen, non-secret summary for a plugin's status display. */
378
+ keptReceiptState() {
379
+ let since = null;
380
+ for (const [id, kept] of this.#kept) {
381
+ if (!this.#usable(kept.attempt)) {
382
+ this.forget(id);
383
+ continue;
384
+ }
385
+ if (kept.since !== null)
386
+ since = Math.min(since ?? kept.since, kept.since);
387
+ }
388
+ return Object.freeze({ active: since !== null, since });
389
+ }
390
+ #usable(attempt) {
391
+ return attempt.expiresAtMs === null
392
+ ? attempt.credentialType === 'api_key'
393
+ : attempt.expiresAtMs - this.#now() > KEPT_RECEIPT_MARGIN_MS;
394
+ }
395
+ /** Ask the vault while reachable; retain one validated receipt in memory only. */
396
+ async authorize(identity, signal, routeId = 'vault') {
397
+ this.#check(signal);
398
+ const { credentialId, credentialType, accountIdentity } = identity;
399
+ let kept = this.#kept.get(credentialId);
400
+ if (kept &&
401
+ (kept.attempt.credentialType !== credentialType ||
402
+ kept.attempt.expectedAccountIdentity !== accountIdentity)) {
403
+ this.forget(credentialId);
404
+ kept = undefined;
405
+ }
406
+ // A missing or invalid enrollment is not a transport outage.
407
+ let token;
408
+ try {
409
+ token = await this.#token(signal);
410
+ }
411
+ catch (error) {
412
+ this.#kept.clear();
413
+ throw error;
414
+ }
415
+ if (this.#outage && kept) {
416
+ this.#probe();
417
+ return this.#serveKept(credentialId, kept, token, routeId);
418
+ }
419
+ try {
420
+ const attempt = await this.#authorizeFresh({ credentialId, credentialType, accountIdentity }, token, signal);
421
+ this.#kept.set(credentialId, { attempt, routeId, since: null });
422
+ return attempt;
423
+ }
424
+ catch (error) {
425
+ this.#check(signal);
426
+ if (classifyVaultFailure(error) !== 'unreachable') {
427
+ this.forget(credentialId);
428
+ throw error;
429
+ }
430
+ return this.#serveKept(credentialId, kept, token, routeId, error);
431
+ }
432
+ }
433
+ #serveKept(credentialId, kept, token, routeId, error = this.#outageError) {
434
+ // A concurrent refusal or 401 may already have discarded this entry.
435
+ if (!kept ||
436
+ this.#kept.get(credentialId) !== kept ||
437
+ !this.#usable(kept.attempt)) {
438
+ this.forget(credentialId);
439
+ throw error;
440
+ }
441
+ if (kept.since === null)
442
+ kept.since = this.#now();
443
+ const attempt = Object.freeze(Object.defineProperty({ ...kept.attempt, fromKeptReceipt: true }, 'accessToken', { value: kept.attempt.accessToken, enumerable: false }));
444
+ this.#provenance.set(attempt, token);
445
+ this.#keptAttempts.set(attempt, routeId);
446
+ return attempt;
447
+ }
448
+ /**
449
+ * Fetch and validate a fresh receipt before retaining it. The vault assertion
450
+ * (or the plugin's token parse) must agree with the roster when both exist.
451
+ * With requireAssertion, an absent assertion also refuses rather than
452
+ * treating the roster's expectation as proof of the served account.
453
+ */
454
+ async #authorizeFresh(identity, token, signal) {
253
455
  if (!identity.credentialId)
254
456
  throw new ClaustrumConsumerError('route-unavailable', 'Claustrum dispatch requires a credential id');
255
457
  // Capture the caller's fields before yielding so a later mutation cannot move the fence.
256
458
  const { credentialId, credentialType, accountIdentity } = identity;
257
459
  if (this.#requireAssertion && accountIdentity === undefined)
258
460
  throw new ClaustrumConsumerError('identity-unasserted', 'Claustrum dispatch requires a known account identity');
259
- const token = await this.#token(signal);
260
461
  const served = await this.#call(() => this.#client.getScoped({
261
462
  credentialId,
262
463
  enrollmentToken: token,
@@ -338,6 +539,21 @@ export class ClaustrumScopedCustody {
338
539
  const token = this.#provenance.get(attempt);
339
540
  if (!token)
340
541
  throw new ClaustrumConsumerError('no-receipt', 'Claustrum failure report requires an original dispatch receipt');
542
+ const keptRoute = this.#keptAttempts.get(attempt);
543
+ if (keptRoute !== undefined) {
544
+ this.forget(attempt.credentialId);
545
+ this.#logger.debug('scoped 401 not reported', {
546
+ routeId: keptRoute,
547
+ reason: 'kept-receipt',
548
+ });
549
+ return;
550
+ }
551
+ // The provider rejected this token, so it must not come back as a kept
552
+ // receipt during a later outage. Drop it before reporting, so a report that
553
+ // fails because the vault is unreachable still drops it.
554
+ const kept = this.#kept.get(attempt.credentialId);
555
+ if (kept && kept.attempt.recordVersion === attempt.recordVersion)
556
+ this.forget(attempt.credentialId);
341
557
  const pending = this.#reports.get(attempt);
342
558
  if (pending)
343
559
  return pending;
@@ -362,10 +578,72 @@ export class ClaustrumScopedCustody {
362
578
  throw error;
363
579
  }
364
580
  }
581
+ /**
582
+ * Watch this consumer's visible vault accounts instead of listing them on a
583
+ * short timer. Opens one `watchScoped` on this custody's client and calls
584
+ * `onView` when the host must run its discovery (see
585
+ * `ClaustrumRosterWatchOptions`). A claustrum-client without `watchScoped`
586
+ * (before 0.12.0) or a vault that answers `unknown_method` reports
587
+ * `fallback` and opens nothing. A watch that closes before any notice was
588
+ * never accepted: the client reads once to learn why, and the watch reports
589
+ * `fallback` and tries one new watch after `WATCH_RETRY_MS`, never in a
590
+ * loop. A watch that closes after notices is re-opened after
591
+ * `RESUBSCRIBE_BACKOFF_MS`, doubling per close in a row. A lost transport
592
+ * starts this custody's outage, exactly as an unreachable call does, and
593
+ * the first notice of a watch ends it. Timers are unreferenced; `close()`
594
+ * on the returned watch or on this custody stops it.
595
+ *
596
+ * The watch keeps no history, so the host still lists on a slow timer while
597
+ * the mode is `watching`: every `SAFETY_POLL_MS` (five minutes). In any
598
+ * other mode the host keeps its usual list timer.
599
+ */
600
+ watchRoster(options) {
601
+ this.#check();
602
+ const watch = new ScopedRosterWatch({
603
+ client: this.#client,
604
+ enrollmentToken: () => this.#token(),
605
+ view: options.view ?? (() => this.#discoveredView),
606
+ discover: async (view) => {
607
+ await options.onView(view);
608
+ },
609
+ onRefused: (error) => {
610
+ // Apply a refused watch as discover() applies a refused account
611
+ // listing: every kept receipt is dropped.
612
+ this.listFailed(error);
613
+ options.onRefused?.(error);
614
+ },
615
+ onTransportLost: () => this.transportLost(),
616
+ onReachable: () => this.reachable(),
617
+ onError: options.onError,
618
+ onMode: options.onMode,
619
+ logger: this.#logger,
620
+ now: this.#now,
621
+ setTimeoutImpl: this.#setTimeout,
622
+ clearTimeoutImpl: this.#clearTimeout,
623
+ });
624
+ this.#watches.add(watch);
625
+ watch.start();
626
+ return {
627
+ get mode() {
628
+ return watch.mode;
629
+ },
630
+ close: () => {
631
+ this.#watches.delete(watch);
632
+ watch.stop();
633
+ },
634
+ };
635
+ }
365
636
  close() {
366
637
  if (this.#closed)
367
638
  return;
368
639
  this.#closed = true;
640
+ if (this.#probeTimer)
641
+ this.#clearTimeout(this.#probeTimer);
642
+ this.#probeTimer = undefined;
643
+ for (const watch of this.#watches)
644
+ watch.stop();
645
+ this.#watches.clear();
646
+ this.#kept.clear();
369
647
  this.#client.close();
370
648
  }
371
649
  }