@cortexkit/antigravity-auth-core 3.0.12 → 3.0.13

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 (51) hide show
  1. package/dist/account-selector.d.ts +9 -1
  2. package/dist/account-selector.d.ts.map +1 -1
  3. package/dist/account-selector.js +23 -8
  4. package/dist/account-selector.js.map +1 -1
  5. package/dist/common-auth-embedded/claustrum/consumer.d.ts +77 -2
  6. package/dist/common-auth-embedded/claustrum/consumer.js +320 -35
  7. package/dist/common-auth-embedded/claustrum/custody.d.ts +36 -1
  8. package/dist/common-auth-embedded/claustrum/custody.js +95 -11
  9. package/dist/common-auth-embedded/claustrum/enrollment.js +37 -12
  10. package/dist/common-auth-embedded/claustrum/hub.d.ts +195 -0
  11. package/dist/common-auth-embedded/claustrum/hub.js +988 -0
  12. package/dist/common-auth-embedded/claustrum/index.d.ts +13 -3
  13. package/dist/common-auth-embedded/claustrum/index.js +37 -4
  14. package/dist/common-auth-embedded/claustrum/quota-writer.d.ts +8 -1
  15. package/dist/common-auth-embedded/claustrum/quota-writer.js +34 -12
  16. package/dist/common-auth-embedded/claustrum/roster.d.ts +49 -1
  17. package/dist/common-auth-embedded/claustrum/roster.js +187 -12
  18. package/dist/common-auth-embedded/claustrum/shutdown.js +42 -0
  19. package/dist/common-auth-embedded/fs/file-read-cache.js +57 -0
  20. package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +1 -1
  21. package/dist/common-auth-embedded/fs/refresh-file-lock.js +20 -11
  22. package/dist/common-auth-embedded/quota/commit-interval.d.ts +9 -0
  23. package/dist/common-auth-embedded/quota/commit-interval.js +18 -0
  24. package/dist/common-auth-embedded/source-output.json +115 -67
  25. package/dist/common-auth-embedded/store/mutate.d.ts +2 -0
  26. package/dist/common-auth-embedded/store/mutate.js +4 -0
  27. package/dist/common-auth-embedded/store/pool.d.ts +6 -1
  28. package/dist/common-auth-embedded/store/pool.js +1 -1
  29. package/dist/common-auth-embedded/store/quota-writer.js +27 -13
  30. package/dist/common-auth-embedded/store/settings.d.ts +1 -1
  31. package/dist/common-auth-embedded/store/settings.js +16 -6
  32. package/dist/model-registry.d.ts +2 -2
  33. package/dist/model-registry.d.ts.map +1 -1
  34. package/dist/model-registry.js +6 -4
  35. package/dist/model-registry.js.map +1 -1
  36. package/dist/quota-manager.d.ts +1 -1
  37. package/dist/quota-manager.d.ts.map +1 -1
  38. package/dist/quota-manager.js +12 -4
  39. package/dist/quota-manager.js.map +1 -1
  40. package/dist/quota-presentation.d.ts.map +1 -1
  41. package/dist/quota-presentation.js +8 -0
  42. package/dist/quota-presentation.js.map +1 -1
  43. package/dist/quota-types.d.ts +1 -1
  44. package/dist/quota-types.d.ts.map +1 -1
  45. package/dist/quota-types.js +5 -0
  46. package/dist/quota-types.js.map +1 -1
  47. package/dist/vault-account-source.d.ts +6 -1
  48. package/dist/vault-account-source.d.ts.map +1 -1
  49. package/dist/vault-account-source.js +9 -0
  50. package/dist/vault-account-source.js.map +1 -1
  51. package/package.json +1 -1
@@ -4,6 +4,7 @@ import { ClaustrumUnreachableError, classifyVaultFailure, } from './availability
4
4
  import { readClaustrumEnrollmentToken } from './enrollment.js';
5
5
  import { ClaustrumConsumerError } from './errors.js';
6
6
  import { CUSTODY_PLACEHOLDER_PREFIX } from './host-slot.js';
7
+ import { drainShutdown, shutdownWarning } from './shutdown.js';
7
8
  import { ScopedRosterWatch, } from './watch.js';
8
9
  /** Only a new version of the same account may replace an in-flight 401. */
9
10
  export function isScopedCredentialRotation(served, current) {
@@ -79,15 +80,56 @@ function accessTokenFromMaterial(material) {
79
80
  return access;
80
81
  }
81
82
  function credentialTypeOf(row, family) {
83
+ const notMine = { kind: 'not-mine' };
82
84
  if (!row.operations.includes('read'))
83
- return undefined;
85
+ return notMine;
84
86
  if (!row.categories.includes(family.category))
85
- return undefined;
87
+ return notMine;
88
+ if (row.credentialType !== 'oauth' && row.credentialType !== 'api_key')
89
+ return notMine;
90
+ if ('classify' in family) {
91
+ // Older clients omit these facts entirely; do not call a policy that needs them.
92
+ const facts = {
93
+ credentialId: row.id,
94
+ credentialType: row.credentialType,
95
+ categories: row.categories,
96
+ refreshAdapter: row.refreshAdapter,
97
+ ...rowFactsOf(row),
98
+ };
99
+ if (facts.providerIds === undefined)
100
+ return { kind: 'refused', code: 'unmapped' };
101
+ try {
102
+ const result = family.classify(facts);
103
+ if (result.kind === 'not-mine')
104
+ return notMine;
105
+ if (result.kind === 'mine' && typeof result.member === 'string')
106
+ return { kind: 'mine', type: row.credentialType, member: result.member };
107
+ if (result.kind === 'refused' &&
108
+ typeof result.code === 'string' &&
109
+ /^[a-z][a-z0-9-]{0,31}$/.test(result.code) &&
110
+ !result.code.includes('\n') &&
111
+ family.refusalCodes.includes(result.code))
112
+ return { kind: 'refused', code: result.code };
113
+ }
114
+ catch {
115
+ // A classifier error may contain vault data; expose only a fixed code.
116
+ }
117
+ return { kind: 'refused', code: 'unclassified' };
118
+ }
86
119
  if (row.credentialType === 'oauth')
87
- return row.refreshAdapter === family.refreshAdapter ? 'oauth' : undefined;
120
+ return row.refreshAdapter === family.refreshAdapter
121
+ ? { kind: 'mine', type: 'oauth' }
122
+ : notMine;
88
123
  if (row.credentialType === 'api_key' && family.apiKeys)
89
- return row.refreshAdapter === undefined ? 'api_key' : undefined;
90
- return undefined;
124
+ return row.refreshAdapter === undefined
125
+ ? { kind: 'mine', type: 'api_key' }
126
+ : notMine;
127
+ return notMine;
128
+ }
129
+ /** Structural access keeps the public facts independent of client declarations. */
130
+ function rowFactsOf(row) {
131
+ const metadata = row;
132
+ return { providerIds: metadata.providerIds, authMethod: metadata.authMethod };
91
133
  }
92
134
  /**
93
135
  * The grants named in the vault's list reply, or undefined when it names none.
@@ -136,6 +178,9 @@ export class ClaustrumScopedCustody {
136
178
  /** The view of the last successful `discover()`, for `watchRoster`. */
137
179
  #discoveredView;
138
180
  #watches = new Set();
181
+ #tokenReads = new Set();
182
+ #warnShutdown;
183
+ #disposing;
139
184
  constructor(options) {
140
185
  const tokenPath = options.tokenPath;
141
186
  if (options.readToken) {
@@ -153,6 +198,7 @@ export class ClaustrumScopedCustody {
153
198
  this.#requireAssertion = options.requireAssertion ?? false;
154
199
  this.#now = options.now ?? Date.now;
155
200
  this.#logger = options.logger ?? defaultLogger;
201
+ this.#warnShutdown = shutdownWarning(this.#logger);
156
202
  this.#setTimeout = options.setTimeoutImpl ?? setTimeout;
157
203
  this.#clearTimeout = options.clearTimeoutImpl ?? clearTimeout;
158
204
  }
@@ -220,7 +266,15 @@ export class ClaustrumScopedCustody {
220
266
  }
221
267
  async #token(signal) {
222
268
  this.#check(signal);
223
- const value = await this.#readToken();
269
+ const pending = this.#readToken();
270
+ this.#tokenReads.add(pending);
271
+ let value;
272
+ try {
273
+ value = await pending;
274
+ }
275
+ finally {
276
+ this.#tokenReads.delete(pending);
277
+ }
224
278
  this.#check(signal);
225
279
  if (!/^[0-9a-f]{64}$/.test(value.token) ||
226
280
  !Number.isSafeInteger(value.token_generation) ||
@@ -299,17 +353,32 @@ export class ClaustrumScopedCustody {
299
353
  throw error;
300
354
  }
301
355
  const skipped = [];
356
+ const refused = [];
357
+ let notMine = 0;
302
358
  const candidates = [];
303
359
  const counts = new Map();
304
360
  for (const row of inventory.rows) {
305
- const type = credentialTypeOf(row, this.#family);
306
- if (!type)
361
+ const classified = credentialTypeOf(row, this.#family);
362
+ if (classified.kind === 'not-mine') {
363
+ notMine++;
364
+ continue;
365
+ }
366
+ if (classified.kind === 'refused') {
367
+ refused.push({
368
+ ...(row.id.trim() && { credentialId: row.id }),
369
+ code: classified.code,
370
+ });
307
371
  continue;
308
- candidates.push({ row, type });
372
+ }
373
+ candidates.push({
374
+ row,
375
+ type: classified.type,
376
+ ...(classified.member !== undefined && { member: classified.member }),
377
+ });
309
378
  counts.set(row.id, (counts.get(row.id) ?? 0) + 1);
310
379
  }
311
380
  const credentials = [];
312
- for (const { row, type } of candidates) {
381
+ for (const { row, type, member } of candidates) {
313
382
  let reason;
314
383
  if (!row.id.trim())
315
384
  reason = 'empty credential id';
@@ -320,12 +389,17 @@ export class ClaustrumScopedCustody {
320
389
  else if (!row.state.trim())
321
390
  reason = 'empty state';
322
391
  if (reason) {
323
- skipped.push({ ...(row.id.trim() && { credentialId: row.id }), reason });
392
+ skipped.push({
393
+ ...(row.id.trim() && { credentialId: row.id }),
394
+ ...(member !== undefined && { member }),
395
+ reason,
396
+ });
324
397
  continue;
325
398
  }
326
399
  credentials.push(Object.freeze({
327
400
  credentialId: row.id,
328
401
  credentialType: type,
402
+ ...(member !== undefined && { member }),
329
403
  ...(row.accountId !== undefined && {
330
404
  accountIdentity: row.accountId,
331
405
  }),
@@ -341,6 +415,10 @@ export class ClaustrumScopedCustody {
341
415
  view: inventory.view,
342
416
  credentials: Object.freeze(credentials),
343
417
  skipped: Object.freeze(skipped),
418
+ ...('classify' in this.#family && {
419
+ refused: Object.freeze(refused),
420
+ notMine,
421
+ }),
344
422
  ...(grantReach && { grantReach }),
345
423
  };
346
424
  }
@@ -646,4 +724,10 @@ export class ClaustrumScopedCustody {
646
724
  this.#kept.clear();
647
725
  this.#client.close();
648
726
  }
727
+ /** Stop immediately; allow token reads 2 s and share an owner's timeout warning. */
728
+ async dispose(onTimeout = this.#warnShutdown) {
729
+ this.close();
730
+ this.#disposing ??= drainShutdown(this.#tokenReads, onTimeout);
731
+ await this.#disposing;
732
+ }
649
733
  }
@@ -3,6 +3,7 @@ import { constants as fsConstants } from 'node:fs';
3
3
  import { chmod, mkdir, open, readFile, realpath, rename, stat, unlink, } from 'node:fs/promises';
4
4
  import { basename, dirname, extname, isAbsolute, join, resolve, } from 'node:path';
5
5
  import { ClaustrumCredentialError, ClaustrumClient as ClaustrumWireClient, writeEnrollmentTokenFile, } from '@cortexkit/claustrum-client';
6
+ import { FileReadCache } from '../fs/file-read-cache.js';
6
7
  import { acquireRefreshFileLock } from '../fs/index.js';
7
8
  import { ensurePrivateDirectory as ensureWindowsDirectory, PrivateFileError, readPrivateText, verifySafeDirectory, windowsPrivateFiles, writePrivateAtomic, } from '../fs/private-files.js';
8
9
  import { ClaustrumConsumerError } from './errors.js';
@@ -402,32 +403,56 @@ async function writeStateAtomic(path, state, ancestorOptions = {}) {
402
403
  await unlink(temporary).catch(() => { });
403
404
  }
404
405
  }
406
+ const enrollmentStatusCache = new FileReadCache();
405
407
  export async function readClaustrumEnrollmentStatus(paths, proposedName) {
408
+ // Windows ACL, owner and ancestry checks are not represented by stat identity.
409
+ // Keep the native private-file validation and full read on every call there.
410
+ const result = windowsPrivateFiles
411
+ ? await readEnrollmentStatus(paths, proposedName)
412
+ : await enrollmentStatusCache.read([paths.tokenPath, paths.statePath], async ([tokenPath = paths.tokenPath, statePath = paths.statePath]) => readEnrollmentStatus({ tokenPath, statePath }, proposedName), (identities) => {
413
+ // Recheck the current effective user as well as mode and regular-file kind.
414
+ // lstat probes also prevent a link to the old inode from becoming a hit.
415
+ for (const metadata of identities)
416
+ if (metadata !== 'absent')
417
+ validateReadableSecretFile(metadata);
418
+ });
419
+ const status = { ...result.status };
420
+ if (result.usesProposedName && status.state === 'approved')
421
+ status.proposedName = proposedName;
422
+ return status;
423
+ }
424
+ async function readEnrollmentStatus(paths, proposedName) {
406
425
  const tokenValue = await readBoundedJson(paths.tokenPath);
407
426
  const token = tokenValue === undefined ? undefined : decodeTokenFile(tokenValue);
408
427
  const stateValue = await readBoundedJson(paths.statePath);
409
428
  const state = stateValue === undefined ? undefined : decodeEnrollmentState(stateValue);
410
429
  if (token) {
411
430
  return {
412
- state: 'approved',
413
- proposedName: state?.proposedName ?? proposedName,
414
- ...(state?.phase === 'approved' &&
415
- state.approvedName !== undefined && {
416
- approvedName: state.approvedName,
417
- }),
418
- tokenGeneration: token.token_generation,
431
+ usesProposedName: state === undefined,
432
+ status: {
433
+ state: 'approved',
434
+ proposedName: state?.proposedName ?? proposedName,
435
+ ...(state?.phase === 'approved' &&
436
+ state.approvedName !== undefined && {
437
+ approvedName: state.approvedName,
438
+ }),
439
+ tokenGeneration: token.token_generation,
440
+ },
419
441
  };
420
442
  }
421
443
  if (!state)
422
- return { state: 'idle' };
444
+ return { status: { state: 'idle' }, usesProposedName: false };
423
445
  if (state.phase === 'approved') {
424
446
  return {
425
- state: 'blocked',
426
- proposedName: state.proposedName,
427
- code: 'missing_token',
447
+ usesProposedName: false,
448
+ status: {
449
+ state: 'blocked',
450
+ proposedName: state.proposedName,
451
+ code: 'missing_token',
452
+ },
428
453
  };
429
454
  }
430
- return statusFromState(state);
455
+ return { status: statusFromState(state), usesProposedName: false };
431
456
  }
432
457
  /**
433
458
  * Resolve the host's account mode from its enrollment files. It never changes
@@ -0,0 +1,195 @@
1
+ import type { QuotaObservation } from '../quota/index.js';
2
+ import { type ClaustrumConsumer } from './consumer.js';
3
+ import { type ClassifyingClaustrumFamily, type ClaustrumScopedClient, ClaustrumScopedCustody, type IdentityParser, type KeptReceiptState, type VaultGrantReach } from './custody.js';
4
+ import { type ClaustrumLogger } from './errors.js';
5
+ import { type AccountMapper, type VaultRosterFile } from './roster.js';
6
+ import { type WatchMode } from './watch.js';
7
+ export interface ClaustrumDiscoveryHubOptions {
8
+ /**
9
+ * The plugin's owner-only state directory. Member rosters live under
10
+ * `<stateDir>/rosters/<hostGeneration>/<fingerprint>/<provider>.json`.
11
+ */
12
+ stateDir: string;
13
+ /**
14
+ * Which host generation this process runs (for example `opencode1`,
15
+ * `opencode2` or `pi`). Hosts that classify rows differently keep separate
16
+ * roster directories.
17
+ */
18
+ hostGeneration: string;
19
+ /**
20
+ * Everything the plugin's classifier decides with, as plain JSON: the
21
+ * exact object it hands the classifier (its version, the supported-table
22
+ * entries, the reserved provider set and every host fact it reads). The
23
+ * hub hashes it, with the member set, `routePrefix` and `mapAccount`, into
24
+ * the roster key, so processes whose classifiers or projections could
25
+ * disagree never share a roster file. Configuration that `mapAccount`
26
+ * reads, beyond its own source text, belongs here too.
27
+ */
28
+ classifierInput: unknown;
29
+ /**
30
+ * Every provider the plugin serves on this host, fixed for the hub's life.
31
+ * Each is the `member` a classified row names and a roster file name, so
32
+ * it is lowercase ASCII: letters, digits, `.`, `_` and `-`.
33
+ */
34
+ members: readonly string[];
35
+ /** Prefix of every member's vault route ids; part of the roster key. */
36
+ routePrefix?: string;
37
+ /**
38
+ * How every member presents a vault credential (the credential names its
39
+ * member). Part of the roster key by its source text.
40
+ */
41
+ mapAccount?: AccountMapper;
42
+ /** This host's enrollment token, written by setup. */
43
+ tokenPath: string;
44
+ /**
45
+ * The plugin's classifying family. A legacy `ClaustrumFamily` names no
46
+ * member for any row and is refused.
47
+ */
48
+ family: ClassifyingClaustrumFamily<string>;
49
+ connect: () => Promise<ClaustrumScopedClient>;
50
+ /**
51
+ * How long one `connect()` attempt may take before the hub treats the vault
52
+ * as unreachable (default 10 s, as `ClaustrumConsumerOptions`). The hub's
53
+ * connect is shared by every member waiting for a lease, so an attempt that
54
+ * never settles would hold every one of them; after the limit they get an
55
+ * unreachable error and the next acquire connects again.
56
+ */
57
+ connectTimeoutMs?: number;
58
+ /**
59
+ * Whether the plugin takes its accounts from the vault. Read from local
60
+ * state only, never from the vault: on every hub tick, and when a member
61
+ * refreshes or authorizes. The first false read revokes every lease,
62
+ * closes the connection and clears every member.
63
+ */
64
+ isCustodyActive?: () => boolean | Promise<boolean>;
65
+ parseIdentity?: IdentityParser;
66
+ /** As `ClaustrumConsumerOptions.requireAssertion`, for every member. */
67
+ requireAssertion?: boolean;
68
+ /**
69
+ * How often the hub ticks (default 5 s). A tick reads authority, installs
70
+ * member roster files another writer changed under the same view, and
71
+ * lists when a list is due: on every tick while no vault watch is open,
72
+ * when a pending member's retry is due (a member the last list did not
73
+ * commit, retried with a backoff), otherwise every five minutes as a
74
+ * safety net.
75
+ */
76
+ pollIntervalMs?: number;
77
+ setTimeoutImpl?: typeof setTimeout;
78
+ clearTimeoutImpl?: typeof clearTimeout;
79
+ now?: () => number;
80
+ logger?: ClaustrumLogger;
81
+ /**
82
+ * Errors of the hub's own tick, list and watch. A failed list and a vault
83
+ * refusal of the watch also reach every member's `onError`.
84
+ */
85
+ onError?: (error: unknown) => void;
86
+ }
87
+ /**
88
+ * What one hub member takes: only what is per provider. The hub owns the
89
+ * roster path, the enrollment token, the family, the connection, the
90
+ * authority check, the poll timer and the projection inputs that are part of
91
+ * the roster key (`routePrefix`, `mapAccount`), so none of them can be given
92
+ * here. The type parameter matches the consumer `member` returns; members
93
+ * have no insula source.
94
+ */
95
+ export interface ClaustrumHubMemberOptions<_Q = QuotaObservation> {
96
+ /**
97
+ * Ids of the plugin's local pool rows; no vault route id may equal one.
98
+ * Read on every projection and install, so it may change at any time.
99
+ */
100
+ reservedRouteIds?: () => Iterable<string>;
101
+ /** As `ClaustrumConsumerOptions.onRoster`: once per change of view. */
102
+ onRoster?: (roster: VaultRosterFile) => void;
103
+ /** This member's own failures, such as a roster commit that failed. */
104
+ onError?: (error: unknown) => void;
105
+ logger?: ClaustrumLogger;
106
+ }
107
+ /** What a lease may do with the shared custody. */
108
+ export type ClaustrumLeasedCustody = Pick<ClaustrumScopedCustody, 'authorize' | 'reportFailure' | 'forget' | 'keptReceiptState'>;
109
+ /**
110
+ * One acquisition of the hub's shared custody. `custody` throws `closed`
111
+ * after `release()`, and once the connection it belongs to ended for any
112
+ * reason, vault mode going off included. `release()` is idempotent.
113
+ */
114
+ export interface CustodyLease {
115
+ readonly custody: ClaustrumLeasedCustody;
116
+ /** The connection this lease belongs to; each new connection counts up. */
117
+ readonly generation: number;
118
+ release(): void;
119
+ }
120
+ /** The rows the hub's last complete discovery refused or left to others. */
121
+ export interface ClaustrumHubRefusals {
122
+ readonly view: string;
123
+ readonly refused: readonly {
124
+ credentialId?: string;
125
+ code: string;
126
+ }[];
127
+ readonly notMine: number;
128
+ }
129
+ /**
130
+ * One discovery hub per host process: the only vault connection and the only
131
+ * `ClaustrumScopedCustody`, one vault watch and one list per refresh, fanned
132
+ * out to one roster per provider. Members (`member()`) are ordinary
133
+ * `ClaustrumConsumer`s that never connect, list or watch themselves; each
134
+ * holds a custody lease. The connection stays open while vault mode is on,
135
+ * because the hub's own discovery holds a lease of its own, and closes when
136
+ * the last lease of a connection is released, when vault mode goes off, or
137
+ * at `close()`.
138
+ */
139
+ export declare class ClaustrumDiscoveryHub {
140
+ #private;
141
+ constructor(options: ClaustrumDiscoveryHubOptions);
142
+ /** The roster file member `provider` reads and the hub commits. */
143
+ rosterPath(provider: string): string;
144
+ /**
145
+ * Build the consumer for `provider`, once. It is an ordinary
146
+ * `ClaustrumConsumer` whose roster path is `rosterPath(provider)`; it
147
+ * connects, lists and watches only through this hub.
148
+ */
149
+ member<Q = QuotaObservation>(provider: string, options: ClaustrumHubMemberOptions<Q>): ClaustrumConsumer<Q>;
150
+ /**
151
+ * Take a lease on the current connection, connecting first when there is
152
+ * none. The acquirer's count is taken before anything is awaited, so a
153
+ * release elsewhere meanwhile cannot end the connection under it. An abort
154
+ * or a failed connect gives that count back; an acquirer whose connection
155
+ * ended while it waited tries once more on the current one. Rejects with
156
+ * `not-active` while vault mode is off.
157
+ */
158
+ acquire(signal?: AbortSignal): Promise<CustodyLease>;
159
+ /** The current connection and its lease count, for status and tests. */
160
+ leaseState(): {
161
+ readonly generation?: number;
162
+ readonly count: number;
163
+ };
164
+ /**
165
+ * Start the hub's tick, which runs until `close()`. While vault mode is
166
+ * off a tick only reads authority and makes no vault request.
167
+ */
168
+ start(): void;
169
+ /**
170
+ * List once and commit every member, as a tick does. Resolves once every
171
+ * member's memory holds its new roster. Rejects when the list fails; a
172
+ * member's own commit error goes to that member's `onError`.
173
+ */
174
+ refresh(): Promise<void>;
175
+ /**
176
+ * Rows the last discovery every member committed refused, with their fixed
177
+ * codes, and how many rows it left to other plugins. Undefined while
178
+ * unknown: before a complete discovery, and while vault mode is off.
179
+ */
180
+ refusals(): ClaustrumHubRefusals | undefined;
181
+ /** The grant reach of the same discovery as `refusals()`. */
182
+ grantReach(): VaultGrantReach | undefined;
183
+ /** How the hub learns about changes; `polling` until a watch is open. */
184
+ watchState(): WatchMode;
185
+ /** The shared custody's kept receipts, host-wide. */
186
+ keptReceiptState(): KeptReceiptState;
187
+ /**
188
+ * Stop the tick and the watch and close the connection. Members are not
189
+ * closed; they fail `closed` from then on. Use `dispose()` to also allow
190
+ * in-flight discovery up to two seconds to release its file handles.
191
+ */
192
+ close(): void;
193
+ /** Close immediately, then join background work for at most two seconds. */
194
+ dispose(): Promise<void>;
195
+ }