@cortexkit/anthropic-auth-core 2.2.8 → 2.3.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.
@@ -3,8 +3,9 @@ import type { QuotaObservation } from '../quota/index.js';
3
3
  import type { RoutingRow } from '../routing/index.js';
4
4
  import { type ClaustrumFamily, type ClaustrumScopedAttempt, type ClaustrumScopedClient, type IdentityParser, type KeptReceiptState, type VaultGrantReach } from './custody.js';
5
5
  import { type ClaustrumLogger } from './errors.js';
6
+ import type { CustodyLease } from './hub.js';
6
7
  import { type InsulaQuotaOptions, type InsulaQuotaResult } from './insula.js';
7
- import { type AccountMapper, type QuotaReceipt, type ReorderVaultRosterResult, type VaultRosterFile } from './roster.js';
8
+ import { type AccountMapper, type ProjectionOptions, type QuotaReceipt, type ReorderVaultRosterResult, type VaultRosterFile } from './roster.js';
8
9
  import { type WatchMode } from './watch.js';
9
10
  export interface ClaustrumConsumerOptions<Q = QuotaObservation> {
10
11
  /**
@@ -17,6 +18,14 @@ export interface ClaustrumConsumerOptions<Q = QuotaObservation> {
17
18
  tokenPath: string;
18
19
  family: ClaustrumFamily;
19
20
  connect: () => Promise<ClaustrumScopedClient>;
21
+ /**
22
+ * How long one `connect()` attempt may take before this consumer treats the
23
+ * vault as unreachable (default 10 s). A connect that never settles would
24
+ * otherwise leave every later `refresh()` waiting on the same promise. After
25
+ * the limit the waiters get an unreachable error, the next `refresh()`
26
+ * connects again, and a client that arrives late is closed.
27
+ */
28
+ connectTimeoutMs?: number;
20
29
  /**
21
30
  * Whether the plugin currently takes its accounts from the vault (custody
22
31
  * mode) rather than from local logins. Checked before every discovery,
@@ -65,6 +74,72 @@ export interface ClaustrumWatchState {
65
74
  readonly vault: WatchMode;
66
75
  readonly insula?: WatchMode;
67
76
  }
77
+ /**
78
+ * What a hub member asks of the `ClaustrumDiscoveryHub` that built it.
79
+ * Package-internal: the hub implements it, and `/claustrum` exports neither
80
+ * it nor `createHubMember`.
81
+ */
82
+ export interface HubMemberPort {
83
+ readonly provider: string;
84
+ /** The member's `start()`: make sure the hub's own driver runs. */
85
+ start(): void;
86
+ /**
87
+ * One hub refresh, coalesced across members (one list, one fan-out).
88
+ * Undefined while vault mode is off, or when the connection it ran on was
89
+ * ended before its fan-out finished. `errors` holds each member's own
90
+ * commit error; a failed list rejects instead.
91
+ */
92
+ refresh(): Promise<HubRefreshOutcome | undefined>;
93
+ /**
94
+ * The hub's authority read. The hub, not the member, decides what a false
95
+ * read means: it runs its inactive transition on the first one.
96
+ */
97
+ readAuthority(): Promise<boolean>;
98
+ acquire(signal?: AbortSignal): Promise<CustodyLease>;
99
+ /** False once a lease was released or its connection ended. */
100
+ leaseLive(lease: CustodyLease): boolean;
101
+ /** The hub's grant reach, only while `view` is the view it was read with. */
102
+ grantReach(view: string | undefined): VaultGrantReach | undefined;
103
+ watchMode(): WatchMode;
104
+ keptReceiptState(): KeptReceiptState;
105
+ /** Apply the union of every member's roster in memory to kept receipts. */
106
+ retainUnion(): void;
107
+ forget(credentialId: string): void;
108
+ /** The member closed: the hub stops committing and pushing to it. */
109
+ detach(): void;
110
+ }
111
+ export interface HubRefreshOutcome {
112
+ readonly errors: ReadonlyMap<string, unknown>;
113
+ }
114
+ /** What the hub may do to one member's memory. Package-internal. */
115
+ export interface HubMemberHooks {
116
+ /**
117
+ * Set the roster in memory as a consumer's own refresh does (pending quota
118
+ * readings included) and notify `onRoster` when its view changed.
119
+ */
120
+ install(roster: VaultRosterFile): void;
121
+ /** Vault mode went off: forget the roster. */
122
+ reset(): void;
123
+ roster(): VaultRosterFile | undefined;
124
+ projection(): ProjectionOptions;
125
+ onError(error: unknown): void;
126
+ }
127
+ /**
128
+ * How long one connect attempt may take by default. It bounds a connect that
129
+ * never settles; the client's own handshake limit is not guaranteed to cover
130
+ * every stall, and callers waiting on discovery need an answer either way.
131
+ */
132
+ export declare const DEFAULT_CONNECT_TIMEOUT_MS = 10000;
133
+ /**
134
+ * Build the consumer for one hub member. `options` holds only what the member
135
+ * uses: the hub's roster path for this member and the member's own fields.
136
+ * Hub-mode paths never read `tokenPath`, `family` or `connect`, which the hub
137
+ * owns. Package-internal: only `ClaustrumDiscoveryHub.member` calls it.
138
+ */
139
+ export declare function createHubMember<Q>(port: HubMemberPort, options: ClaustrumConsumerOptions<Q>): {
140
+ consumer: ClaustrumConsumer<Q>;
141
+ hooks: HubMemberHooks;
142
+ };
68
143
  export interface SendOptions {
69
144
  /**
70
145
  * What kind of request this is (for example `model`, `quota`, `profile`),
@@ -179,6 +254,6 @@ export declare class ClaustrumConsumer<Q = QuotaObservation> {
179
254
  */
180
255
  recordAccountQuota(routeId: string, accountIdentity: string, observation: QuotaObservation): Promise<boolean>;
181
256
  close(): void;
182
- /** Close immediately; the last consumer also waits for the writer's bounded drain. */
257
+ /** Close immediately; background work gets 2 s and the quota writer its own 1 s. */
183
258
  dispose(): Promise<void>;
184
259
  }
@@ -22,10 +22,35 @@ export interface ClaustrumFamily {
22
22
  category: string;
23
23
  apiKeys?: boolean;
24
24
  }
25
+ /** Non-secret classification facts, including rows decoded by older clients. */
26
+ export interface VaultRowFacts {
27
+ readonly credentialId: string;
28
+ readonly credentialType: string;
29
+ readonly authMethod?: string;
30
+ readonly refreshAdapter?: string;
31
+ readonly providerIds?: readonly string[];
32
+ readonly categories: readonly string[];
33
+ }
34
+ /** A policy that partitions one category without requiring one OAuth adapter. */
35
+ export interface ClassifyingClaustrumFamily<Code extends string> {
36
+ category: string;
37
+ refusalCodes: readonly Code[];
38
+ classify(row: VaultRowFacts): {
39
+ kind: 'not-mine';
40
+ } | {
41
+ kind: 'mine';
42
+ member: string;
43
+ } | {
44
+ kind: 'refused';
45
+ code: Code;
46
+ };
47
+ }
48
+ export type ClaustrumFamilyInput = ClaustrumFamily | ClassifyingClaustrumFamily<string>;
25
49
  /** One vault credential this consumer may serve. Never carries bearer material. */
26
50
  export interface VaultCredential {
27
51
  readonly credentialId: string;
28
52
  readonly credentialType: VaultCredentialType;
53
+ readonly member?: string;
29
54
  /**
30
55
  * The provider account the credential logs into, when the vault's adapter
31
56
  * claims one. Absent means the adapter makes no claim, not a mismatch.
@@ -47,6 +72,7 @@ export type SkippedVaultReason = 'empty credential id' | 'duplicate credential i
47
72
  */
48
73
  export interface SkippedVaultRecord {
49
74
  readonly credentialId?: string;
75
+ readonly member?: string;
50
76
  readonly reason: SkippedVaultReason;
51
77
  }
52
78
  /**
@@ -82,6 +108,13 @@ export interface VaultInventory {
82
108
  readonly view: string;
83
109
  readonly credentials: readonly VaultCredential[];
84
110
  readonly skipped: readonly SkippedVaultRecord[];
111
+ /** Classifying families only: fixed refusal codes, never classifier errors. */
112
+ readonly refused?: readonly {
113
+ credentialId?: string;
114
+ code: string;
115
+ }[];
116
+ /** Classifying families only: rows outside this family's responsibility. */
117
+ readonly notMine?: number;
85
118
  /**
86
119
  * The consumer's grants, as the vault's list reply named them. Undefined means unknown
87
120
  * (the vault or client predates the fields), never zero grants.
@@ -221,7 +254,7 @@ export declare class ClaustrumScopedCustody {
221
254
  #private;
222
255
  constructor(options: {
223
256
  client: ClaustrumScopedClient;
224
- family: ClaustrumFamily;
257
+ family: ClaustrumFamilyInput;
225
258
  tokenPath?: string;
226
259
  readToken?: () => Promise<EnrollmentTokenFile>;
227
260
  parseIdentity?: IdentityParser;
@@ -296,4 +329,6 @@ export declare class ClaustrumScopedCustody {
296
329
  */
297
330
  watchRoster(options: ClaustrumRosterWatchOptions): ClaustrumRosterWatch;
298
331
  close(): void;
332
+ /** Stop immediately; allow token reads 2 s and share an owner's timeout warning. */
333
+ dispose(onTimeout?: (pendingOperations: number) => void): Promise<void>;
299
334
  }
@@ -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
+ }
@@ -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, type ReorderVaultRosterOptions, type ReorderVaultRosterResult, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, reorderVaultRoster, 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>;
@@ -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 {};
@@ -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;
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
+ }[];
65
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 {
@@ -190,6 +214,16 @@ export declare function applyVaultQuota(current: VaultRosterFile | undefined, in
190
214
  kept: boolean;
191
215
  };
192
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;
193
227
  /**
194
228
  * Discover and commit the roster. The lease covers the list call and the
195
229
  * commit, not just the write: an opaque view cannot tell a delayed old reply
@@ -197,7 +231,8 @@ export declare function forgetPendingVaultDiscovery(path: string): void;
197
231
  * failed list, or custody switching off mid-run, never writes anything; a
198
232
  * peer holding the lease means its last committed roster is served instead.
199
233
  * A write-lock timeout keeps the discovered inventory for the next poll,
200
- * 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.
201
236
  */
202
237
  export declare function refreshVaultRoster(options: {
203
238
  path: string;
@@ -206,6 +241,19 @@ export declare function refreshVaultRoster(options: {
206
241
  signal?: AbortSignal;
207
242
  projection?: ProjectionOptions | (() => ProjectionOptions);
208
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
+ };
209
257
  }): Promise<VaultRosterFile | undefined>;
210
258
  /**
211
259
  * A host-owned main account as last verified from the vault: the plugin's
@@ -28,7 +28,7 @@ type RefreshFileLockOptions = {
28
28
  onContended?: () => void;
29
29
  /** Synchronous, never awaited; throws and returned promise/thenable rejections are isolated. */
30
30
  onRenewalTimer?: (event: 'scheduled' | 'cancelled') => void;
31
- onStep?: (step: 'stale-marker-stat' | 'stale-marker-claimed' | 'stale-lock-observed' | 'lock-read-ready' | 'lock-missing' | 'stale-lock-confirmed' | 'stale-lock-removed' | 'stale-lock-recreated' | 'eviction-marker-acquired' | 'renewal-owner-confirmed' | 'renewal-marker-unavailable' | 'renewal-write-fenced' | 'renewal-write-ready' | 'renewal-write-finished' | 'relinquish-read' | 'renewal-finished' | 'release-owner-confirmed') => void | Promise<void>;
31
+ onStep?: (step: 'stale-marker-stat' | 'stale-marker-claimed' | 'stale-lock-observed' | 'lease-contention-read-ready' | 'lock-read-ready' | 'lock-missing' | 'stale-lock-confirmed' | 'stale-lock-removed' | 'stale-lock-recreated' | 'eviction-marker-acquired' | 'renewal-owner-confirmed' | 'renewal-marker-unavailable' | 'renewal-write-fenced' | 'renewal-write-ready' | 'renewal-write-finished' | 'relinquish-read' | 'renewal-finished' | 'release-owner-confirmed') => void | Promise<void>;
32
32
  };
33
33
  /** Filesystem leases share wall time across all callers and processes. */
34
34
  export declare function acquireRefreshFileLock(options: RefreshFileLockOptions): Promise<RefreshFileLock | null>;
@@ -0,0 +1,9 @@
1
+ export declare const QUOTA_COMMIT_INTERVAL_MS = 30000;
2
+ export interface QuotaCommitTimer {
3
+ now(): number;
4
+ schedule(delayMs: number, run: () => Promise<void>): {
5
+ cancel(): void;
6
+ unref?(): void;
7
+ };
8
+ }
9
+ export declare const quotaCommitTimer: QuotaCommitTimer;
@@ -3,6 +3,8 @@ export interface PlannedWrite {
3
3
  path: string;
4
4
  content: string;
5
5
  changed: boolean;
6
+ existingLocalEntries?: string[];
7
+ existingNpmEntries?: string[];
6
8
  }
7
9
  /** Edit only array entries so unrelated JSONC text, including comments, survives. */
8
10
  export declare function updatePluginConfig(text: string, key: 'plugin' | 'plugins', pkg: string, version: string): string;
@@ -1,4 +1,6 @@
1
+ import { type StoreContext } from './mutate.js';
1
2
  import { type StoreRuntime } from './runtime.js';
3
+ import { type PoolRow } from './schema.js';
2
4
  /**
3
5
  * What a pull or refresh captured about its row (named by id alongside) when
4
6
  * it was issued. A result applies only while the row with that id still has
@@ -20,6 +22,9 @@ export interface Attribution {
20
22
  credentialEpoch: number;
21
23
  identity?: string;
22
24
  }
25
+ export declare function assertQuotaEpoch(id: string, attribution: Attribution): void;
26
+ export declare function assertQuotaRow(id: string, attribution: Attribution, row: PoolRow | undefined): asserts row is PoolRow;
27
+ export declare function mergedQuota(ctx: StoreContext, id: string, stored: unknown, observation: unknown): unknown;
23
28
  /**
24
29
  * Merges a quota observation into a row's stored map under the store locks,
25
30
  * after attribution passes. Needs no row lock, so it is permitted from
@@ -52,6 +52,8 @@ export type ReadResult = ({
52
52
  kind?: 'snapshot-contended';
53
53
  retryable?: true;
54
54
  };
55
+ /** Settings need only the config classification, not a joined roster snapshot. */
56
+ export declare function readPoolConfig(path: string): Promise<import("./schema.js").ConfigClassification>;
55
57
  /**
56
58
  * Validate config/state/config bytes without locks. Roster replacement writes
57
59
  * config before deleting old state credentials. Re-read a changed config so an
@@ -1,3 +1,4 @@
1
+ import type { QuotaCommitTimer } from '../quota/commit-interval.js';
1
2
  import { type Attribution } from './attribution.js';
2
3
  import type { PoolOperationError } from './errors.js';
3
4
  import type { PoolLogger } from './hooks.js';
@@ -16,6 +17,23 @@ export interface OpenPoolStoreOptions {
16
17
  configPath: string;
17
18
  statePath: string;
18
19
  quota: QuotaCodec;
20
+ /**
21
+ * Opt in to detached, coalesced quota publication, with any `quota` codec.
22
+ * Each accepted observation is queued whole for its row and binding, and
23
+ * the queue is applied in arrival order through `quota.merge` under the
24
+ * store locks, so the codec's own merge decides which fields a newer or
25
+ * older reading replaces. Accepted readings appear in read/load
26
+ * immediately, merged the same way; the returned promise still rejects
27
+ * current-row refusals before queueing. Await dispose or close at shutdown
28
+ * to attempt a bounded drain; readings still pending at its timeout are
29
+ * discarded. Normal quota commits are at least 30 seconds apart; a full
30
+ * queue commits early so response handling never waits for that interval.
31
+ */
32
+ coalesceQuota?: boolean;
33
+ /** Bounded quota drain on close/dispose, in milliseconds; defaults to 1,000. */
34
+ quotaDrainTimeoutMs?: number;
35
+ /** Monotonic clock and scheduling seam for detached quota commits. */
36
+ quotaCommitTimer?: QuotaCommitTimer;
19
37
  /**
20
38
  * The codec of the provider state kept beside each row's credential (see
21
39
  * `ProviderStateCodec`). Without it no row shows a provider state, and
@@ -189,6 +207,10 @@ export interface PoolStore {
189
207
  }>;
190
208
  refresh(id: string, provider: ProviderRefresh, options?: RefreshOptions): Promise<RefreshOutcome>;
191
209
  recordQuota(id: string, attribution: Attribution, observation: unknown): Promise<void>;
210
+ /** Drain opted-in quota publication; subsequent coalesced readings refuse. Idempotent. */
211
+ close(): Promise<void>;
212
+ /** Same bounded quota drain as close(). Does not wait for provider pull hooks. */
213
+ dispose(): Promise<void>;
192
214
  /** Fires a pull for a row admission refused for want of a reading. */
193
215
  requestReading(id: string): void;
194
216
  /** Resolves once every pull fired so far has settled. */
@@ -1,3 +1,4 @@
1
+ import type { Attribution } from './attribution.js';
1
2
  import { type PoolOperation, PoolOperationError } from './errors.js';
2
3
  import { type ReadResult, type StoreContext } from './mutate.js';
3
4
  import type { PoolLockOptions, PoolLockSpec } from './refresh-lock.js';
@@ -10,6 +11,7 @@ export interface StoreRuntime {
10
11
  providerLock: PoolLockSpec;
11
12
  rowLockOptions: Partial<PoolLockOptions>;
12
13
  firePull(id: string, reason: PullReason): void;
14
+ publishQuota?: (id: string, attribution: Attribution, observation: unknown) => Promise<void>;
13
15
  }
14
16
  /**
15
17
  * The row lock's (name, path): keyed by the row's recorded wire identity when
@@ -50,7 +50,7 @@ export type UpdateSettingsResult = {
50
50
  /** `unchanged` when the mutator left the settings as they were; nothing was written. */
51
51
  outcome: 'updated' | 'unchanged';
52
52
  };
53
- /** Reads the settings without taking a lock or writing anything. */
53
+ /** Reads only the config's settings, without taking a lock or writing anything. */
54
54
  export declare function readPoolSettings(rt: StoreRuntime): Promise<SettingsRead>;
55
55
  /**
56
56
  * One locked read-modify-write of the plugin's settings in the config file