@cortexkit/antigravity-auth-core 3.0.11 → 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 (66) 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/agy-transport.d.ts.map +1 -1
  6. package/dist/agy-transport.js +4 -0
  7. package/dist/agy-transport.js.map +1 -1
  8. package/dist/common-auth-embedded/claustrum/consumer.d.ts +83 -2
  9. package/dist/common-auth-embedded/claustrum/consumer.js +337 -37
  10. package/dist/common-auth-embedded/claustrum/custody.d.ts +36 -1
  11. package/dist/common-auth-embedded/claustrum/custody.js +95 -11
  12. package/dist/common-auth-embedded/claustrum/enrollment.js +37 -12
  13. package/dist/common-auth-embedded/claustrum/errors.d.ts +1 -1
  14. package/dist/common-auth-embedded/claustrum/hub.d.ts +195 -0
  15. package/dist/common-auth-embedded/claustrum/hub.js +988 -0
  16. package/dist/common-auth-embedded/claustrum/index.d.ts +13 -3
  17. package/dist/common-auth-embedded/claustrum/index.js +37 -4
  18. package/dist/common-auth-embedded/claustrum/quota-writer.d.ts +8 -1
  19. package/dist/common-auth-embedded/claustrum/quota-writer.js +38 -66
  20. package/dist/common-auth-embedded/claustrum/roster.d.ts +80 -1
  21. package/dist/common-auth-embedded/claustrum/roster.js +259 -12
  22. package/dist/common-auth-embedded/claustrum/shutdown.js +42 -0
  23. package/dist/common-auth-embedded/commands/builtins.d.ts +37 -0
  24. package/dist/common-auth-embedded/commands/builtins.js +95 -13
  25. package/dist/common-auth-embedded/commands/index.d.ts +1 -1
  26. package/dist/common-auth-embedded/fs/file-read-cache.js +57 -0
  27. package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +1 -1
  28. package/dist/common-auth-embedded/fs/refresh-file-lock.js +20 -11
  29. package/dist/common-auth-embedded/quota/commit-interval.d.ts +9 -0
  30. package/dist/common-auth-embedded/quota/commit-interval.js +18 -0
  31. package/dist/common-auth-embedded/quota/pending.js +48 -0
  32. package/dist/common-auth-embedded/source-output.json +152 -88
  33. package/dist/common-auth-embedded/store/attribution.d.ts +5 -0
  34. package/dist/common-auth-embedded/store/attribution.js +39 -25
  35. package/dist/common-auth-embedded/store/mutate.d.ts +2 -0
  36. package/dist/common-auth-embedded/store/mutate.js +4 -0
  37. package/dist/common-auth-embedded/store/pool.d.ts +22 -0
  38. package/dist/common-auth-embedded/store/pool.js +18 -3
  39. package/dist/common-auth-embedded/store/pull.js +3 -1
  40. package/dist/common-auth-embedded/store/quota-writer.js +312 -0
  41. package/dist/common-auth-embedded/store/runtime.d.ts +2 -0
  42. package/dist/common-auth-embedded/store/settings.d.ts +1 -1
  43. package/dist/common-auth-embedded/store/settings.js +16 -6
  44. package/dist/model-registry.d.ts +2 -2
  45. package/dist/model-registry.d.ts.map +1 -1
  46. package/dist/model-registry.js +6 -4
  47. package/dist/model-registry.js.map +1 -1
  48. package/dist/pre-response-deadline.d.ts.map +1 -1
  49. package/dist/pre-response-deadline.js +6 -5
  50. package/dist/pre-response-deadline.js.map +1 -1
  51. package/dist/quota-manager.d.ts +1 -1
  52. package/dist/quota-manager.d.ts.map +1 -1
  53. package/dist/quota-manager.js +12 -4
  54. package/dist/quota-manager.js.map +1 -1
  55. package/dist/quota-presentation.d.ts.map +1 -1
  56. package/dist/quota-presentation.js +8 -0
  57. package/dist/quota-presentation.js.map +1 -1
  58. package/dist/quota-types.d.ts +1 -1
  59. package/dist/quota-types.d.ts.map +1 -1
  60. package/dist/quota-types.js +5 -0
  61. package/dist/quota-types.js.map +1 -1
  62. package/dist/vault-account-source.d.ts +6 -1
  63. package/dist/vault-account-source.d.ts.map +1 -1
  64. package/dist/vault-account-source.js +9 -0
  65. package/dist/vault-account-source.js.map +1 -1
  66. package/package.json +1 -1
@@ -2,21 +2,31 @@ import { type ClaustrumClientOptions } from '@cortexkit/claustrum-client';
2
2
  import type { ClaustrumScopedClient } from './custody.js';
3
3
  export { ClaustrumCredentialError, type ClaustrumReporterSource, type EnrollmentTokenFile, } from '@cortexkit/claustrum-client';
4
4
  export { ClaustrumUnreachableError, classifyVaultFailure, VAULT_FAILURE_CLASSES, type VaultFailureClass, } from './availability.js';
5
- export { ClaustrumConsumer, type ClaustrumConsumerOptions, type ClaustrumWatchState, type SendOptions, } from './consumer.js';
6
- export { type AccountIdentitySource, type ClaustrumFamily, type ClaustrumRosterWatch, type ClaustrumRosterWatchOptions, type ClaustrumScopedAttempt, type ClaustrumScopedClient, ClaustrumScopedCustody, type ClaustrumScopedIdentity, decideScopedRetryAfter401, type IdentityParser, isScopedCredentialRotation, KEPT_RECEIPT_MARGIN_MS, type KeptReceiptState, type ScopedRetryReason, SERVING_MARGIN_MS, type SkippedVaultReason, type SkippedVaultRecord, type VaultCredential, type VaultCredentialType, type VaultGrantReach, type VaultGrantTuple, type VaultInventory, } from './custody.js';
5
+ export { ClaustrumConsumer, type ClaustrumConsumerOptions, type ClaustrumWatchState, DEFAULT_CONNECT_TIMEOUT_MS, type SendOptions, } from './consumer.js';
6
+ export { type AccountIdentitySource, type ClassifyingClaustrumFamily, type ClaustrumFamily, type ClaustrumFamilyInput, type ClaustrumRosterWatch, type ClaustrumRosterWatchOptions, type ClaustrumScopedAttempt, type ClaustrumScopedClient, ClaustrumScopedCustody, type ClaustrumScopedIdentity, decideScopedRetryAfter401, type IdentityParser, isScopedCredentialRotation, KEPT_RECEIPT_MARGIN_MS, type KeptReceiptState, type ScopedRetryReason, SERVING_MARGIN_MS, type SkippedVaultReason, type SkippedVaultRecord, type VaultCredential, type VaultCredentialType, type VaultGrantReach, type VaultGrantTuple, type VaultInventory, type VaultRowFacts, } from './custody.js';
7
7
  export { type ClaustrumEnrollmentClient, type ClaustrumEnrollmentConnection, type ClaustrumEnrollmentDisconnectResult, ClaustrumEnrollmentManager, type ClaustrumEnrollmentPaths, type ClaustrumEnrollmentResetResult, type ClaustrumEnrollmentStatus, classifyEnrollmentError, connectClaustrumEnrollmentClient, disconnectClaustrumEnrollment, type EnrollmentDisposition, enrollmentAuthority, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, } from './enrollment.js';
8
8
  export { ClaustrumConsumerError, type ClaustrumConsumerFailureKind, type ClaustrumLogger, } from './errors.js';
9
9
  export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, type HostSlotContent, isCustodyPlaceholder, isCustodyPlaceholderValue, } from './host-slot.js';
10
+ export { ClaustrumDiscoveryHub, type ClaustrumDiscoveryHubOptions, type ClaustrumHubMemberOptions, type ClaustrumHubRefusals, type ClaustrumLeasedCustody, type CustodyLease, } from './hub.js';
10
11
  export { acquireInsulaQuotaSource, type InsulaQuotaContext, type InsulaQuotaLookup, type InsulaQuotaMiss, type InsulaQuotaOptions, type InsulaQuotaReading, type InsulaQuotaResult, type InsulaQuotaSource, type InsulaUsageEntry, type InsulaUsageTransport, type InsulaUsageWatch, type InsulaUsageWindow, type InsulaWatchOptions, } from './insula.js';
11
12
  export { acceptAccount, type DeclinedAccount, declineAccount, isDeclined, } from './interlock.js';
12
13
  export { drainVaultQuota } from './quota-writer.js';
13
- export { type AccountMapper, acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, type MutateVaultRosterOptions, mutateVaultRoster, type ProjectionOptions, projectVaultRoster, type QuotaReceipt, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, type VaultPrimary, type VaultPrimaryBinding, type VaultPrimaryUnavailableReason, type VaultRosterFile, type VaultRosterLockContext, type VaultRosterRow, vaultRoutingRows, } from './roster.js';
14
+ export { type AccountMapper, acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, type MutateVaultRosterOptions, mutateVaultRoster, type ProjectionOptions, projectVaultRoster, type QuotaReceipt, type ReorderVaultRosterOptions, type ReorderVaultRosterResult, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, reorderVaultRoster, resolveVaultPrimary, type VaultPrimary, type VaultPrimaryBinding, type VaultPrimaryUnavailableReason, type VaultRosterFile, type VaultRosterLockContext, type VaultRosterRow, type VaultRosterSeed, vaultRosterSeed, vaultRoutingRows, } from './roster.js';
14
15
  export { type ChangeNotice, ChangeNoticeReader, RESUBSCRIBE_BACKOFF_MS, SAFETY_POLL_MS, ScopedRosterWatch, type ScopedRosterWatchOptions, type ScopedWatchEnd, type ScopedWatchHandle, type ScopedWatchNoticeShape, type ScopedWatchSource, WATCH_RETRY_MS, type WatchMode, } from './watch.js';
16
+ /** How long `connectClaustrumScopedClient` waits by default. */
17
+ export declare const SCOPED_CONNECT_TIMEOUT_MS = 10000;
15
18
  /**
16
19
  * Connect the client that lists and fetches this consumer's vault credentials
17
20
  * on the request path. `connectionFile` is required: this library
18
21
  * reads no environment, so the plugin resolves the vault's connection file.
22
+ *
23
+ * The wait is bounded by `connectTimeoutMs` (default 10 s) so a connect that
24
+ * never settles cannot leave a consumer with a pending connection forever; the
25
+ * caller then gets an unreachable error and may try again. A socket handshake
26
+ * has its own limit, but local reads before it (the connection file) do not,
27
+ * and a client that arrives after this deadline is closed, not returned.
19
28
  */
20
29
  export declare function connectClaustrumScopedClient(options: ClaustrumClientOptions & {
21
30
  connectionFile: string;
31
+ connectTimeoutMs?: number;
22
32
  }): Promise<ClaustrumScopedClient>;
@@ -4,25 +4,58 @@
4
4
  // declined-account interlock and the host-slot guard. Needs the optional
5
5
  // peer `@cortexkit/claustrum-client`.
6
6
  import { ClaustrumClient, } from '@cortexkit/claustrum-client';
7
+ import { ClaustrumUnreachableError } from './availability.js';
7
8
  import { insulaUsageTransport } from './insula.js';
8
9
  export { ClaustrumCredentialError, } from '@cortexkit/claustrum-client';
9
10
  export { ClaustrumUnreachableError, classifyVaultFailure, VAULT_FAILURE_CLASSES, } from './availability.js';
10
- export { ClaustrumConsumer, } from './consumer.js';
11
+ export { ClaustrumConsumer, DEFAULT_CONNECT_TIMEOUT_MS, } from './consumer.js';
11
12
  export { ClaustrumScopedCustody, decideScopedRetryAfter401, isScopedCredentialRotation, KEPT_RECEIPT_MARGIN_MS, SERVING_MARGIN_MS, } from './custody.js';
12
13
  export { ClaustrumEnrollmentManager, classifyEnrollmentError, connectClaustrumEnrollmentClient, disconnectClaustrumEnrollment, enrollmentAuthority, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, } from './enrollment.js';
13
14
  export { ClaustrumConsumerError, } from './errors.js';
14
15
  export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, isCustodyPlaceholder, isCustodyPlaceholderValue, } from './host-slot.js';
16
+ export { ClaustrumDiscoveryHub, } from './hub.js';
15
17
  export { acquireInsulaQuotaSource, } from './insula.js';
16
18
  export { acceptAccount, declineAccount, isDeclined, } from './interlock.js';
17
19
  export { drainVaultQuota } from './quota-writer.js';
18
- export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, vaultRoutingRows, } from './roster.js';
20
+ export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, reorderVaultRoster, resolveVaultPrimary, vaultRosterSeed, vaultRoutingRows, } from './roster.js';
19
21
  export { ChangeNoticeReader, RESUBSCRIBE_BACKOFF_MS, SAFETY_POLL_MS, ScopedRosterWatch, WATCH_RETRY_MS, } from './watch.js';
22
+ /** How long `connectClaustrumScopedClient` waits by default. */
23
+ export const SCOPED_CONNECT_TIMEOUT_MS = 10_000;
20
24
  /**
21
25
  * Connect the client that lists and fetches this consumer's vault credentials
22
26
  * on the request path. `connectionFile` is required: this library
23
27
  * reads no environment, so the plugin resolves the vault's connection file.
28
+ *
29
+ * The wait is bounded by `connectTimeoutMs` (default 10 s) so a connect that
30
+ * never settles cannot leave a consumer with a pending connection forever; the
31
+ * caller then gets an unreachable error and may try again. A socket handshake
32
+ * has its own limit, but local reads before it (the connection file) do not,
33
+ * and a client that arrives after this deadline is closed, not returned.
24
34
  */
25
35
  export async function connectClaustrumScopedClient(options) {
26
- const client = await ClaustrumClient.connect(options);
27
- return Object.assign(client, { insula: insulaUsageTransport(options) });
36
+ const { connectTimeoutMs, ...client } = options;
37
+ const limit = connectTimeoutMs ?? SCOPED_CONNECT_TIMEOUT_MS;
38
+ const connecting = ClaustrumClient.connect(client);
39
+ let expired = false;
40
+ let timer;
41
+ const deadline = new Promise((_resolve, reject) => {
42
+ timer = setTimeout(() => {
43
+ expired = true;
44
+ reject(new ClaustrumUnreachableError());
45
+ }, limit);
46
+ timer.unref?.();
47
+ });
48
+ try {
49
+ const client = await Promise.race([connecting, deadline]);
50
+ return Object.assign(client, { insula: insulaUsageTransport(options) });
51
+ }
52
+ catch (error) {
53
+ // A connection that arrives after the deadline still owns a socket.
54
+ void connecting.then((client) => client.close(), () => { });
55
+ throw error;
56
+ }
57
+ finally {
58
+ if (!expired && timer)
59
+ clearTimeout(timer);
60
+ }
28
61
  }
@@ -1,3 +1,4 @@
1
+ import { type QuotaCommitTimer } from '../quota/commit-interval.js';
1
2
  import { type QuotaObservation } from '../quota/index.js';
2
3
  import { type QuotaReceipt, type VaultRosterFile } from './roster.js';
3
4
  export type VaultQuotaPublication = {
@@ -8,11 +9,17 @@ export type VaultQuotaPublication = {
8
9
  };
9
10
  type Listener = (publication: VaultQuotaPublication) => void;
10
11
  /** Retain the shared writer; the last consumer's close drains it. */
11
- export declare function retainVaultQuotaWriter(path: string, listener: Listener): () => Promise<void>;
12
+ export declare function retainVaultQuotaWriter(path: string, listener: Listener, clock?: QuotaCommitTimer): () => Promise<void>;
12
13
  /** Enqueue without filesystem work; each quota field retains its own receipt fence. */
13
14
  export declare function enqueueVaultQuota(path: string, publication: VaultQuotaPublication): Promise<boolean>;
14
15
  /** Overlay uncommitted observations when a consumer reloads the durable roster. */
15
16
  export declare function pendingVaultQuota(path: string, roster: VaultRosterFile | undefined): VaultRosterFile | undefined;
17
+ /**
18
+ * Whether this process holds quota readings for `path` that no commit of the
19
+ * durable roster has written yet, so a reader of the file alone is not seeing
20
+ * everything this process would publish.
21
+ */
22
+ export declare function hasPendingVaultQuota(path: string): boolean;
16
23
  /** Wait at most timeoutMs for detached publication; shutdown loss is acceptable. */
17
24
  export declare function drainVaultQuota(path: string, timeoutMs?: number): Promise<void>;
18
25
  export {};
@@ -1,12 +1,14 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { createLogger } from '../logger/index.js';
3
+ import { QUOTA_COMMIT_INTERVAL_MS, quotaCommitTimer, } from '../quota/commit-interval.js';
3
4
  import { isQuotaObservation } from '../quota/index.js';
5
+ import { quotaFields } from '../quota/pending.js';
4
6
  import { applyVaultQuota, mutateVaultRoster, } from './roster.js';
5
7
  // Every consumer of the same file shares one mailbox and one lock waiter.
6
8
  const writers = new Map();
7
9
  const logger = createLogger('claustrum');
8
10
  const COALESCE_MS = 100;
9
- function writerFor(path) {
11
+ function writerFor(path, clock = quotaCommitTimer) {
10
12
  const key = resolve(path);
11
13
  let writer = writers.get(key);
12
14
  if (!writer) {
@@ -16,6 +18,7 @@ function writerFor(path) {
16
18
  listeners: new Set(),
17
19
  wake: new Set(),
18
20
  abort: new AbortController(),
21
+ clock,
19
22
  failures: 0,
20
23
  draining: false,
21
24
  order: 0,
@@ -25,8 +28,8 @@ function writerFor(path) {
25
28
  return writer;
26
29
  }
27
30
  /** Retain the shared writer; the last consumer's close drains it. */
28
- export function retainVaultQuotaWriter(path, listener) {
29
- const writer = writerFor(path);
31
+ export function retainVaultQuotaWriter(path, listener, clock) {
32
+ const writer = writerFor(path, clock);
30
33
  writer.listeners.add(listener);
31
34
  let released;
32
35
  return () => (released ??= (() => {
@@ -34,65 +37,14 @@ export function retainVaultQuotaWriter(path, listener) {
34
37
  return writer.listeners.size ? Promise.resolve() : drainVaultQuota(path);
35
38
  })());
36
39
  }
37
- // Split one observation into its independently dated parts: each rate-limit
38
- // window (scope and label), each covered-but-unread window, the budget, each
39
- // counter and the credits record. The mailbox keeps the newest of each part on
40
- // its own, so a later observation that carries only some parts can't erase an
41
- // older but still newest reading of another part, which is how
42
- // mergeQuotaObservation treats them on disk. Each key also carries the
43
- // receipt's identity, so the identity fence still applies to every part.
40
+ // Each field retains the receipt fence as well as its independent timestamp.
44
41
  function fields(publication) {
45
- const result = new Map();
46
- const { observation } = publication;
47
- const witness = JSON.stringify([
42
+ return quotaFields(publication, JSON.stringify([
48
43
  publication.routeId,
49
44
  publication.receipt?.credentialId,
50
45
  publication.receipt?.accountIdentitySource,
51
46
  publication.receipt?.expectedAccountIdentity,
52
- ]);
53
- const add = (key, at, part) => result.set(`${witness}:${key}`, {
54
- at,
55
- order: 0,
56
- publication: { ...publication, observation: part },
57
- });
58
- const keyFor = (scope, label) => JSON.stringify([scope ?? 'all', label]);
59
- const read = new Set();
60
- for (const reading of observation.readings ?? []) {
61
- const key = keyFor(reading.scope, reading.label);
62
- read.add(key);
63
- add(key, observation.checkedAt, {
64
- checkedAt: observation.checkedAt,
65
- readings: [{ ...reading }],
66
- });
67
- }
68
- for (const pair of observation.coverage ?? []) {
69
- const key = keyFor(pair.scope, pair.label);
70
- if (!read.has(key))
71
- add(key, observation.checkedAt, {
72
- checkedAt: observation.checkedAt,
73
- coverage: [{ ...pair }],
74
- });
75
- }
76
- if (observation.budget)
77
- add('budget', observation.checkedAt, {
78
- checkedAt: observation.checkedAt,
79
- budget: { ...observation.budget },
80
- });
81
- for (const [name, counter] of Object.entries(observation.counters ?? {})) {
82
- add(`counter:${name}`, counter.checkedAt, {
83
- checkedAt: observation.checkedAt,
84
- counters: { [name]: { ...counter } },
85
- });
86
- }
87
- if (observation.credits)
88
- add('credits', observation.credits.checkedAt, {
89
- checkedAt: observation.checkedAt,
90
- credits: { ...observation.credits },
91
- });
92
- // Even an empty observation can bind an account proved by its receipt.
93
- if (!result.size)
94
- add('empty', observation.checkedAt, { checkedAt: observation.checkedAt });
95
- return result;
47
+ ]));
96
48
  }
97
49
  /** Enqueue without filesystem work; each quota field retains its own receipt fence. */
98
50
  export function enqueueVaultQuota(path, publication) {
@@ -143,37 +95,57 @@ export function pendingVaultQuota(path, roster) {
143
95
  }
144
96
  return roster;
145
97
  }
98
+ /**
99
+ * Whether this process holds quota readings for `path` that no commit of the
100
+ * durable roster has written yet, so a reader of the file alone is not seeing
101
+ * everything this process would publish.
102
+ */
103
+ export function hasPendingVaultQuota(path) {
104
+ return !!writers.get(resolve(path))?.pending.size;
105
+ }
146
106
  function schedule(writer, delay) {
147
107
  if (writer.running ||
148
108
  writer.timer ||
149
109
  !writer.pending.size ||
150
110
  writer.abort.signal.aborted)
151
111
  return;
152
- writer.timer = setTimeout(() => {
112
+ const remaining = writer.draining
113
+ ? 0
114
+ : (writer.committedAt ?? -Infinity) +
115
+ QUOTA_COMMIT_INTERVAL_MS -
116
+ writer.clock.now();
117
+ writer.timer = writer.clock.schedule(Math.max(delay, remaining), () => {
153
118
  writer.timer = undefined;
154
- flush(writer);
155
- }, delay);
119
+ return flush(writer);
120
+ });
156
121
  if (!writer.draining)
157
122
  writer.timer.unref?.();
158
123
  }
124
+ function cancelTimer(writer) {
125
+ writer.timer?.cancel();
126
+ writer.timer = undefined;
127
+ }
159
128
  function flush(writer) {
160
129
  if (writer.running || writer.abort.signal.aborted)
161
- return;
130
+ return writer.running ?? Promise.resolve();
162
131
  const batch = new Map([...writer.pending].map(([account, pending]) => [
163
132
  account,
164
133
  new Map(pending),
165
134
  ]));
166
135
  writer.running = (async () => {
167
136
  try {
168
- await mutateVaultRoster(writer.path, (current) => {
137
+ const committed = await mutateVaultRoster(writer.path, (current) => {
169
138
  let next = current;
170
139
  for (const field of orderedFields(batch)) {
171
140
  if (field.priorReading)
172
141
  next = applyVaultQuota(next, field.priorReading).roster;
173
142
  next = applyVaultQuota(next, field.publication).roster;
174
143
  }
175
- return { ...(next !== current && { next }), result: undefined };
144
+ const changed = JSON.stringify(next) !== JSON.stringify(current);
145
+ return { ...(changed && { next }), result: changed };
176
146
  }, { signal: writer.abort.signal });
147
+ if (committed)
148
+ writer.committedAt = writer.clock.now();
177
149
  for (const [account, fields] of batch) {
178
150
  const pending = writer.pending.get(account);
179
151
  for (const [key, field] of fields) {
@@ -205,6 +177,7 @@ function flush(writer) {
205
177
  schedule(writer, backoff);
206
178
  }
207
179
  })();
180
+ return writer.running;
208
181
  }
209
182
  /** Wait at most timeoutMs for detached publication; shutdown loss is acceptable. */
210
183
  export async function drainVaultQuota(path, timeoutMs = 1_000) {
@@ -213,8 +186,7 @@ export async function drainVaultQuota(path, timeoutMs = 1_000) {
213
186
  if (!writer)
214
187
  return;
215
188
  writer.draining = true;
216
- clearTimeout(writer.timer);
217
- writer.timer = undefined;
189
+ cancelTimer(writer);
218
190
  let deadline;
219
191
  let wake;
220
192
  const drained = new Promise((done) => {
@@ -244,7 +216,7 @@ export async function drainVaultQuota(path, timeoutMs = 1_000) {
244
216
  // must not discard its observations or stop its retries.
245
217
  if (!writer.listeners.size) {
246
218
  writer.abort.abort();
247
- clearTimeout(writer.timer);
219
+ cancelTimer(writer);
248
220
  writer.pending.clear();
249
221
  if (writers.get(key) === writer)
250
222
  writers.delete(key);
@@ -62,7 +62,25 @@ export interface ProjectionOptions {
62
62
  routePrefix?: string;
63
63
  mapAccount?: AccountMapper;
64
64
  now?: number;
65
+ /**
66
+ * Preferences carried over from another roster file, used only when there
67
+ * is no previous roster: its declines become this roster's declines, and
68
+ * its account order sorts the rows this inventory produces. No row of the
69
+ * seed is ever projected, so a seed cannot make an account routable.
70
+ */
71
+ seed?: VaultRosterSeed;
65
72
  }
73
+ /** The preferences one roster file can pass to a first projection elsewhere. */
74
+ export interface VaultRosterSeed {
75
+ readonly declined: readonly DeclinedAccount[];
76
+ /** Accounts in the seed roster's order, matched by account, then by id. */
77
+ readonly order: readonly {
78
+ readonly credentialId: string;
79
+ readonly accountIdentity?: string;
80
+ }[];
81
+ }
82
+ /** Declines and account order of `roster`, and nothing else of it. */
83
+ export declare function vaultRosterSeed(roster: VaultRosterFile): VaultRosterSeed;
66
84
  export declare const DEFAULT_ROUTE_PREFIX = "vault:";
67
85
  /**
68
86
  * Project the vault's list onto the previous roster. Pure: callers serialize
@@ -87,6 +105,12 @@ export declare function projectVaultRoster(previous: VaultRosterFile | undefined
87
105
  * reaches admission.
88
106
  */
89
107
  export declare function vaultRoutingRows(roster: VaultRosterFile | undefined): RoutingRow[];
108
+ /**
109
+ * `roster` as `readVaultRoster` decodes it from the file that holds it, so a
110
+ * committed roster and a later read of its file compare equal. Package
111
+ * internal: not exported from `/claustrum`.
112
+ */
113
+ export declare function decodedVaultRoster(roster: VaultRosterFile): VaultRosterFile;
90
114
  export declare function readVaultRoster(path: string): Promise<VaultRosterFile | undefined>;
91
115
  /** The ownership assertion available to a roster mutation callback. */
92
116
  export interface VaultRosterLockContext {
@@ -119,6 +143,37 @@ export declare function mutateVaultRoster<T>(path: string, change: (current: Vau
119
143
  export declare function declineVaultRoute(path: string, routeId: string): Promise<undefined>;
120
144
  /** Lift the user's decline for a vault account and every alias of it. */
121
145
  export declare function acceptVaultRoute(path: string, routeId: string): Promise<undefined>;
146
+ /** Options of `reorderVaultRoster`. */
147
+ export interface ReorderVaultRosterOptions {
148
+ /** Aborts the wait for the roster's write lock. */
149
+ signal?: AbortSignal;
150
+ }
151
+ export interface ReorderVaultRosterResult {
152
+ /** The route id order now on disk. */
153
+ ids: string[];
154
+ /** `unchanged` when the roster was already in this order; nothing was written. */
155
+ outcome: 'reordered' | 'unchanged';
156
+ }
157
+ /**
158
+ * Set the order vault accounts are tried in. Routing reads that order from
159
+ * the roster's row order (`vaultRoutingRows`), and a later discovery keeps
160
+ * each account at its position, so the order lasts until the next reorder.
161
+ * The order is this host's routing preference: the vault has no order of its
162
+ * own, and this never contacts it.
163
+ *
164
+ * `routeIds` must name every roster route id exactly once; anything else
165
+ * (an unknown, missing or repeated id) refuses with `invalid-order` and
166
+ * writes nothing. It runs under the roster's write lock, like every other
167
+ * roster write, so a quota reading or decline committed meanwhile is kept.
168
+ * Each row is kept as the same object, so only the positions change: every
169
+ * row's fields and the rest of the file stay as they were. An order equal to
170
+ * the current one writes nothing. A host with no roster file yet has no
171
+ * routes, so only the empty order is accepted.
172
+ *
173
+ * A `ClaustrumConsumer` in the same process does not see the new order until
174
+ * it re-reads the file: call `consumer.reorder` instead to update both.
175
+ */
176
+ export declare function reorderVaultRoster(path: string, routeIds: readonly string[], options?: ReorderVaultRosterOptions): Promise<ReorderVaultRosterResult>;
122
177
  /**
123
178
  * The receipt fields a quota or profile observation must carry to say which
124
179
  * send (credential and account) it came from. Pass the
@@ -159,6 +214,16 @@ export declare function applyVaultQuota(current: VaultRosterFile | undefined, in
159
214
  kept: boolean;
160
215
  };
161
216
  export declare function forgetPendingVaultDiscovery(path: string): void;
217
+ /**
218
+ * Whether `error` from a roster commit at `path` means another writer had the
219
+ * roster at that moment, not that the commit itself failed: the discovery
220
+ * lease was busy before any roster existed, the write lock stayed busy, or a
221
+ * lock this commit held was lost. On Windows the same lock steps can reject
222
+ * with a PrivateFileError instead (a lock file that could not be read or
223
+ * replaced while another process held it), so a PrivateFileError naming one
224
+ * of this roster's lock files counts the same on every OS.
225
+ */
226
+ export declare function isVaultRosterLockRefusal(path: string, error: unknown): boolean;
162
227
  /**
163
228
  * Discover and commit the roster. The lease covers the list call and the
164
229
  * commit, not just the write: an opaque view cannot tell a delayed old reply
@@ -166,7 +231,8 @@ export declare function forgetPendingVaultDiscovery(path: string): void;
166
231
  * failed list, or custody switching off mid-run, never writes anything; a
167
232
  * peer holding the lease means its last committed roster is served instead.
168
233
  * A write-lock timeout keeps the discovered inventory for the next poll,
169
- * unless a peer has superseded its base view in the meantime.
234
+ * unless a peer has superseded its base view in the meantime. A list that
235
+ * would write the file the roster already holds takes no write lock at all.
170
236
  */
171
237
  export declare function refreshVaultRoster(options: {
172
238
  path: string;
@@ -175,6 +241,19 @@ export declare function refreshVaultRoster(options: {
175
241
  signal?: AbortSignal;
176
242
  projection?: ProjectionOptions | (() => ProjectionOptions);
177
243
  onInventory?: (inventory: VaultInventory) => void;
244
+ /**
245
+ * For a caller that listed before taking this path's discovery lease (the
246
+ * discovery hub lists once for several rosters): the view this roster file
247
+ * held before that list. `custody.discover` then returns that list. It is
248
+ * held to the rule a kept discovery is held to below: it is projected only
249
+ * while the file still holds this view or the list's own view. Otherwise
250
+ * another process committed since the list, and the file is returned
251
+ * unchanged. No discovery is kept for a later call, because such a caller
252
+ * lists again.
253
+ */
254
+ listedOver?: {
255
+ readonly view: string | undefined;
256
+ };
178
257
  }): Promise<VaultRosterFile | undefined>;
179
258
  /**
180
259
  * A host-owned main account as last verified from the vault: the plugin's