@cortexkit/common-auth 0.3.0 → 0.4.1
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.
- package/dist/cachekeep/manager.d.ts +18 -6
- package/dist/cachekeep/manager.js +40 -10
- package/dist/claustrum/consumer.d.ts +13 -4
- package/dist/claustrum/consumer.js +11 -3
- package/dist/claustrum/custody.d.ts +47 -6
- package/dist/claustrum/custody.js +37 -7
- package/dist/claustrum/errors.d.ts +1 -1
- package/dist/claustrum/index.d.ts +3 -3
- package/dist/claustrum/index.js +2 -2
- package/dist/claustrum/interlock.d.ts +15 -17
- package/dist/claustrum/interlock.js +19 -26
- package/dist/claustrum/roster.d.ts +96 -6
- package/dist/claustrum/roster.js +237 -45
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +6 -1
- package/dist/commands/index.d.ts +2 -2
- package/dist/commands/index.js +1 -1
- package/dist/commands/menu.d.ts +8 -0
- package/dist/commands/menu.js +34 -13
- package/dist/commands/model.d.ts +9 -0
- package/dist/commands/seam.d.ts +40 -4
- package/dist/commands/seam.js +132 -19
- package/dist/dump/index.d.ts +94 -0
- package/dist/dump/index.js +236 -9
- package/dist/logger/engine.d.ts +52 -17
- package/dist/logger/engine.js +178 -135
- package/dist/logger/index.d.ts +2 -2
- package/dist/logger/index.js +1 -1
- package/dist/opencode2/install.d.ts +8 -3
- package/dist/opencode2/install.js +18 -9
- package/dist/opencode2/types.d.ts +22 -3
- package/dist/quota/projection.d.ts +11 -4
- package/dist/quota/projection.js +11 -4
- package/dist/routing/admission.js +3 -1
- package/dist/routing/index.d.ts +2 -2
- package/dist/routing/index.js +1 -1
- package/dist/routing/sticky.d.ts +19 -6
- package/dist/routing/sticky.js +34 -23
- package/dist/rpc/notifications.d.ts +20 -0
- package/dist/rpc/notifications.js +21 -0
- package/dist/rpc/rpc-server.d.ts +9 -1
- package/dist/rpc/rpc-server.js +8 -1
- package/dist/sidebar-file/index.d.ts +1 -1
- package/dist/sidebar-file/sidebar-file.d.ts +50 -2
- package/dist/sidebar-file/sidebar-file.js +92 -21
- package/dist/store/attribution.js +14 -2
- package/dist/store/errors.d.ts +6 -3
- package/dist/store/identity.d.ts +13 -4
- package/dist/store/index.d.ts +1 -1
- package/dist/store/mutate.d.ts +27 -4
- package/dist/store/mutate.js +43 -26
- package/dist/store/pool.d.ts +16 -3
- package/dist/store/pool.js +7 -2
- package/dist/store/rows.d.ts +20 -6
- package/dist/store/rows.js +141 -46
- package/dist/store/schema.d.ts +74 -5
- package/dist/store/schema.js +112 -10
- package/dist/store/torn.d.ts +29 -0
- package/dist/store/torn.js +113 -0
- package/package.json +1 -1
|
@@ -151,12 +151,18 @@ export interface CacheKeepStatus {
|
|
|
151
151
|
* Keeps idle prompt caches warm: one target per session holding the latest
|
|
152
152
|
* request body, replayed just before the provider's cache would expire.
|
|
153
153
|
*
|
|
154
|
-
* Bounds, in the order they act:
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
154
|
+
* Bounds, in the order they act: a target retires once its last confirmed
|
|
155
|
+
* cache lifetime ends (`cacheExpiresAt`, set by its capture or its last
|
|
156
|
+
* successful warm), whatever else holds, because replaying a history whose
|
|
157
|
+
* cache has expired rebuilds a cold cache instead of keeping a warm one
|
|
158
|
+
* alive; the clock window gates capture and warming (targets captured earlier
|
|
159
|
+
* survive outside it until their lifetime ends); idle caps prune a target its
|
|
160
|
+
* session stopped using (`sustain` lifts only the main-session idle cap, not
|
|
161
|
+
* the lifetime); the target-count and byte caps evict the least recently
|
|
162
|
+
* touched target; a per-target warm cap retires short-lived sessions; and a
|
|
163
|
+
* failed warm backs that target off without touching the others. A retry
|
|
164
|
+
* after a failure is sent only while the lifetime lasts, so a backoff that
|
|
165
|
+
* runs past it retires the target instead.
|
|
160
166
|
*/
|
|
161
167
|
export declare class CacheKeepManager<M = undefined> {
|
|
162
168
|
private readonly targets;
|
|
@@ -203,6 +209,12 @@ export declare class CacheKeepManager<M = undefined> {
|
|
|
203
209
|
*/
|
|
204
210
|
tick(): Promise<void>;
|
|
205
211
|
private runTick;
|
|
212
|
+
/**
|
|
213
|
+
* Retires `target` when its last confirmed cache lifetime has ended; true
|
|
214
|
+
* when it did. Called before every send, since earlier warms in the same
|
|
215
|
+
* tick, the adapter's account lookup and body build all take time.
|
|
216
|
+
*/
|
|
217
|
+
private retireIfExpired;
|
|
206
218
|
private isCurrent;
|
|
207
219
|
private view;
|
|
208
220
|
private fail;
|
|
@@ -19,12 +19,18 @@ function errorMessage(error) {
|
|
|
19
19
|
* Keeps idle prompt caches warm: one target per session holding the latest
|
|
20
20
|
* request body, replayed just before the provider's cache would expire.
|
|
21
21
|
*
|
|
22
|
-
* Bounds, in the order they act:
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* Bounds, in the order they act: a target retires once its last confirmed
|
|
23
|
+
* cache lifetime ends (`cacheExpiresAt`, set by its capture or its last
|
|
24
|
+
* successful warm), whatever else holds, because replaying a history whose
|
|
25
|
+
* cache has expired rebuilds a cold cache instead of keeping a warm one
|
|
26
|
+
* alive; the clock window gates capture and warming (targets captured earlier
|
|
27
|
+
* survive outside it until their lifetime ends); idle caps prune a target its
|
|
28
|
+
* session stopped using (`sustain` lifts only the main-session idle cap, not
|
|
29
|
+
* the lifetime); the target-count and byte caps evict the least recently
|
|
30
|
+
* touched target; a per-target warm cap retires short-lived sessions; and a
|
|
31
|
+
* failed warm backs that target off without touching the others. A retry
|
|
32
|
+
* after a failure is sent only while the lifetime lasts, so a backoff that
|
|
33
|
+
* runs past it retires the target instead.
|
|
28
34
|
*/
|
|
29
35
|
export class CacheKeepManager {
|
|
30
36
|
targets = new Map();
|
|
@@ -233,8 +239,9 @@ export class CacheKeepManager {
|
|
|
233
239
|
}
|
|
234
240
|
async runTick() {
|
|
235
241
|
this.pruneStale();
|
|
236
|
-
// Outside the window nothing fires, but captured targets stay
|
|
237
|
-
//
|
|
242
|
+
// Outside the window nothing fires, but captured targets stay until
|
|
243
|
+
// their cache lifetime ends, so one still alive when the window reopens
|
|
244
|
+
// is warmed again.
|
|
238
245
|
const window = this.getWindow?.();
|
|
239
246
|
if (window && !isWithinCacheKeepWindow(window, new Date(this.now()))) {
|
|
240
247
|
return;
|
|
@@ -254,6 +261,23 @@ export class CacheKeepManager {
|
|
|
254
261
|
await this.warm(sessionKey, target);
|
|
255
262
|
}
|
|
256
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Retires `target` when its last confirmed cache lifetime has ended; true
|
|
266
|
+
* when it did. Called before every send, since earlier warms in the same
|
|
267
|
+
* tick, the adapter's account lookup and body build all take time.
|
|
268
|
+
*/
|
|
269
|
+
retireIfExpired(sessionKey, target) {
|
|
270
|
+
if (this.now() < target.cacheExpiresAt)
|
|
271
|
+
return false;
|
|
272
|
+
this.log?.debug('cachekeep retired target (cache lifetime ended)', {
|
|
273
|
+
sessionKey,
|
|
274
|
+
accountId: target.accountId,
|
|
275
|
+
cacheExpiresAt: target.cacheExpiresAt,
|
|
276
|
+
failures: target.failures,
|
|
277
|
+
});
|
|
278
|
+
this.drop(sessionKey);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
257
281
|
isCurrent(sessionKey, target) {
|
|
258
282
|
return !this.disposed && this.targets.get(sessionKey) === target;
|
|
259
283
|
}
|
|
@@ -286,6 +310,8 @@ export class CacheKeepManager {
|
|
|
286
310
|
});
|
|
287
311
|
}
|
|
288
312
|
async warm(sessionKey, target) {
|
|
313
|
+
if (this.retireIfExpired(sessionKey, target))
|
|
314
|
+
return;
|
|
289
315
|
const view = this.view(sessionKey, target);
|
|
290
316
|
if (this.adapter.activeAccount) {
|
|
291
317
|
let active;
|
|
@@ -322,6 +348,8 @@ export class CacheKeepManager {
|
|
|
322
348
|
}
|
|
323
349
|
if (!this.isCurrent(sessionKey, target))
|
|
324
350
|
return;
|
|
351
|
+
if (this.retireIfExpired(sessionKey, target))
|
|
352
|
+
return;
|
|
325
353
|
const signal = AbortSignal.any([
|
|
326
354
|
AbortSignal.timeout(this.warmTimeoutMs),
|
|
327
355
|
this.abortController.signal,
|
|
@@ -383,8 +411,10 @@ export class CacheKeepManager {
|
|
|
383
411
|
const now = this.now();
|
|
384
412
|
const sustain = this.getSustain?.() === true;
|
|
385
413
|
for (const [sessionKey, target] of [...this.targets]) {
|
|
386
|
-
|
|
387
|
-
|
|
414
|
+
if (this.retireIfExpired(sessionKey, target))
|
|
415
|
+
continue;
|
|
416
|
+
// Sustain exempts main sessions from the idle cap only; the lifetime,
|
|
417
|
+
// window, count, byte and warm caps still apply to them.
|
|
388
418
|
if (sustain && !target.isSubagent)
|
|
389
419
|
continue;
|
|
390
420
|
const maxIdleMs = target.maxIdleMs ?? this.idleDefault(target.isSubagent);
|
|
@@ -3,7 +3,7 @@ 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 } from './custody.js';
|
|
5
5
|
import { type ClaustrumLogger } from './errors.js';
|
|
6
|
-
import { type AccountMapper, type VaultRosterFile } from './roster.js';
|
|
6
|
+
import { type AccountMapper, type QuotaReceipt, type VaultRosterFile } from './roster.js';
|
|
7
7
|
export interface ClaustrumConsumerOptions {
|
|
8
8
|
/**
|
|
9
9
|
* The roster file: one row per vault account (route id, credential id,
|
|
@@ -26,6 +26,13 @@ export interface ClaustrumConsumerOptions {
|
|
|
26
26
|
routePrefix?: string;
|
|
27
27
|
mapAccount?: AccountMapper;
|
|
28
28
|
parseIdentity?: IdentityParser;
|
|
29
|
+
/**
|
|
30
|
+
* Issue a receipt only when the vault's served reply itself names the
|
|
31
|
+
* credential id and the roster's account identity (see
|
|
32
|
+
* `ClaustrumScopedCustody`). Set it for providers whose tokens do not reveal
|
|
33
|
+
* their account; leave it unset to accept a reply that asserts no identity.
|
|
34
|
+
*/
|
|
35
|
+
requireAssertion?: boolean;
|
|
29
36
|
/**
|
|
30
37
|
* Fired once per change of the vault's view cursor, including the first
|
|
31
38
|
* roster. A poll that sees the same view does not fire, and neither does a
|
|
@@ -85,9 +92,11 @@ export declare class ClaustrumConsumer {
|
|
|
85
92
|
decline(routeId: string): Promise<void>;
|
|
86
93
|
accept(routeId: string): Promise<void>;
|
|
87
94
|
/**
|
|
88
|
-
* Store a quota observation for a vault route.
|
|
89
|
-
* was taken with
|
|
95
|
+
* Store a quota or profile observation for a vault route. The receipt the
|
|
96
|
+
* reading was taken with is required: the observation lands only while the
|
|
97
|
+
* route still holds the credential and account that receipt was served for
|
|
98
|
+
* (see `recordVaultQuota`), so a reading for a replaced account is dropped.
|
|
90
99
|
*/
|
|
91
|
-
recordQuota(routeId: string, observation: QuotaObservation, attempt
|
|
100
|
+
recordQuota(routeId: string, observation: QuotaObservation, attempt: QuotaReceipt): Promise<boolean>;
|
|
92
101
|
close(): void;
|
|
93
102
|
}
|
|
@@ -76,6 +76,7 @@ export class ClaustrumConsumer {
|
|
|
76
76
|
family: this.#options.family,
|
|
77
77
|
tokenPath: this.#options.tokenPath,
|
|
78
78
|
parseIdentity: this.#options.parseIdentity,
|
|
79
|
+
requireAssertion: this.#options.requireAssertion,
|
|
79
80
|
now: this.#options.now,
|
|
80
81
|
logger: this.#logger,
|
|
81
82
|
});
|
|
@@ -247,17 +248,24 @@ export class ClaustrumConsumer {
|
|
|
247
248
|
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
248
249
|
}
|
|
249
250
|
/**
|
|
250
|
-
* Store a quota observation for a vault route.
|
|
251
|
-
* was taken with
|
|
251
|
+
* Store a quota or profile observation for a vault route. The receipt the
|
|
252
|
+
* reading was taken with is required: the observation lands only while the
|
|
253
|
+
* route still holds the credential and account that receipt was served for
|
|
254
|
+
* (see `recordVaultQuota`), so a reading for a replaced account is dropped.
|
|
252
255
|
*/
|
|
253
256
|
async recordQuota(routeId, observation, attempt) {
|
|
254
257
|
this.#assertOpen();
|
|
255
258
|
const kept = await recordVaultQuota(this.#options.rosterPath, {
|
|
256
259
|
routeId,
|
|
257
260
|
observation,
|
|
258
|
-
|
|
261
|
+
credentialId: attempt.credentialId,
|
|
262
|
+
accountIdentitySource: attempt.accountIdentitySource,
|
|
263
|
+
...(attempt.accountIdentity !== undefined && {
|
|
259
264
|
accountIdentity: attempt.accountIdentity,
|
|
260
265
|
}),
|
|
266
|
+
...(attempt.expectedAccountIdentity !== undefined && {
|
|
267
|
+
expectedAccountIdentity: attempt.expectedAccountIdentity,
|
|
268
|
+
}),
|
|
261
269
|
});
|
|
262
270
|
if (kept)
|
|
263
271
|
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
@@ -27,10 +27,19 @@ export interface VaultCredential {
|
|
|
27
27
|
readonly email?: string;
|
|
28
28
|
readonly orgName?: string;
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Why a listed vault record could not be used. A closed set of fixed codes, so
|
|
32
|
+
* a log line or a roster file that carries one never echoes vault data.
|
|
33
|
+
*/
|
|
34
|
+
export type SkippedVaultReason = 'empty credential id' | 'duplicate credential id' | 'blank account identity' | 'empty state';
|
|
35
|
+
/**
|
|
36
|
+
* A vault record this consumer could not use. `credentialId` is absent when
|
|
37
|
+
* the record's id itself was unusable, so nothing can say which account the
|
|
38
|
+
* record was.
|
|
39
|
+
*/
|
|
31
40
|
export interface SkippedVaultRecord {
|
|
32
41
|
readonly credentialId?: string;
|
|
33
|
-
readonly reason:
|
|
42
|
+
readonly reason: SkippedVaultReason;
|
|
34
43
|
}
|
|
35
44
|
export interface VaultInventory {
|
|
36
45
|
/**
|
|
@@ -47,6 +56,13 @@ export interface ClaustrumScopedIdentity {
|
|
|
47
56
|
readonly credentialType: VaultCredentialType;
|
|
48
57
|
readonly accountIdentity?: string;
|
|
49
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Where a receipt's `accountIdentity` came from: the vault asserted it in the
|
|
61
|
+
* served reply, the plugin's `parseIdentity` read it from the served token, or
|
|
62
|
+
* neither did and it is only the roster's expectation (`none`: no identity at
|
|
63
|
+
* all).
|
|
64
|
+
*/
|
|
65
|
+
export type AccountIdentitySource = 'asserted' | 'parsed' | 'expected' | 'none';
|
|
50
66
|
/**
|
|
51
67
|
* A receipt: what the vault served for one physical send. Each attempt gets
|
|
52
68
|
* its own. It records the exact record version served, because a 401 for
|
|
@@ -55,7 +71,22 @@ export interface ClaustrumScopedIdentity {
|
|
|
55
71
|
export interface ClaustrumScopedAttempt {
|
|
56
72
|
readonly credentialId: string;
|
|
57
73
|
readonly credentialType: VaultCredentialType;
|
|
74
|
+
/**
|
|
75
|
+
* The account this receipt is bound to: the vault's assertion, else the
|
|
76
|
+
* plugin's parse of the token, else the roster's expectation. Check
|
|
77
|
+
* `accountIdentitySource` before treating it as proof.
|
|
78
|
+
*/
|
|
58
79
|
readonly accountIdentity?: string;
|
|
80
|
+
readonly accountIdentitySource: AccountIdentitySource;
|
|
81
|
+
/** The account the roster row named when this receipt was requested. */
|
|
82
|
+
readonly expectedAccountIdentity?: string;
|
|
83
|
+
/** The credential id the vault itself put in the served reply, if any. */
|
|
84
|
+
readonly assertedCredentialId?: string;
|
|
85
|
+
/**
|
|
86
|
+
* The account the vault itself put in the served reply, if any. Never filled
|
|
87
|
+
* in from the roster or from a token parse.
|
|
88
|
+
*/
|
|
89
|
+
readonly assertedAccountIdentity?: string;
|
|
59
90
|
/**
|
|
60
91
|
* Kept in memory only and hidden from JSON.stringify and object spreads, so
|
|
61
92
|
* logging a receipt never leaks it. Authorize again for every dispatch and retry.
|
|
@@ -98,6 +129,14 @@ export declare class ClaustrumScopedCustody {
|
|
|
98
129
|
tokenPath?: string;
|
|
99
130
|
readToken?: () => Promise<EnrollmentTokenFile>;
|
|
100
131
|
parseIdentity?: IdentityParser;
|
|
132
|
+
/**
|
|
133
|
+
* Issue a receipt only when the vault's served reply itself names the
|
|
134
|
+
* requested credential id and the roster's (known) account identity. For
|
|
135
|
+
* providers whose tokens are opaque, where nothing else can prove which
|
|
136
|
+
* account a token belongs to. Off by default: then an absent assertion is
|
|
137
|
+
* no claim, and the receipt says where its identity came from.
|
|
138
|
+
*/
|
|
139
|
+
requireAssertion?: boolean;
|
|
101
140
|
now?: () => number;
|
|
102
141
|
logger?: ClaustrumLogger;
|
|
103
142
|
});
|
|
@@ -110,10 +149,12 @@ export declare class ClaustrumScopedCustody {
|
|
|
110
149
|
discover(signal?: AbortSignal): Promise<VaultInventory>;
|
|
111
150
|
/**
|
|
112
151
|
* Fetch the credential for one physical send and wrap it in a fresh receipt.
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
152
|
+
* The vault's served identity (or, without one, the plugin's parse of the
|
|
153
|
+
* token) must equal the roster's when both are present. Without
|
|
154
|
+
* `requireAssertion`, absence on either side proves nothing and does not
|
|
155
|
+
* refuse; the receipt records what the vault asserted separately from what
|
|
156
|
+
* the roster expected. With it, a reply that does not itself name the
|
|
157
|
+
* credential id and the expected account is refused.
|
|
117
158
|
*/
|
|
118
159
|
authorize(identity: ClaustrumScopedIdentity, signal?: AbortSignal): Promise<ClaustrumScopedAttempt>;
|
|
119
160
|
/**
|
|
@@ -97,6 +97,7 @@ export class ClaustrumScopedCustody {
|
|
|
97
97
|
#now;
|
|
98
98
|
#family;
|
|
99
99
|
#parseIdentity;
|
|
100
|
+
#requireAssertion;
|
|
100
101
|
#logger;
|
|
101
102
|
#provenance = new WeakMap();
|
|
102
103
|
#reports = new WeakMap();
|
|
@@ -115,6 +116,7 @@ export class ClaustrumScopedCustody {
|
|
|
115
116
|
this.#client = options.client;
|
|
116
117
|
this.#family = options.family;
|
|
117
118
|
this.#parseIdentity = options.parseIdentity;
|
|
119
|
+
this.#requireAssertion = options.requireAssertion ?? false;
|
|
118
120
|
this.#now = options.now ?? Date.now;
|
|
119
121
|
this.#logger = options.logger ?? defaultLogger;
|
|
120
122
|
}
|
|
@@ -223,16 +225,20 @@ export class ClaustrumScopedCustody {
|
|
|
223
225
|
}
|
|
224
226
|
/**
|
|
225
227
|
* Fetch the credential for one physical send and wrap it in a fresh receipt.
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
228
|
+
* The vault's served identity (or, without one, the plugin's parse of the
|
|
229
|
+
* token) must equal the roster's when both are present. Without
|
|
230
|
+
* `requireAssertion`, absence on either side proves nothing and does not
|
|
231
|
+
* refuse; the receipt records what the vault asserted separately from what
|
|
232
|
+
* the roster expected. With it, a reply that does not itself name the
|
|
233
|
+
* credential id and the expected account is refused.
|
|
230
234
|
*/
|
|
231
235
|
async authorize(identity, signal) {
|
|
232
236
|
if (!identity.credentialId)
|
|
233
237
|
throw new ClaustrumConsumerError('route-unavailable', 'Claustrum dispatch requires a credential id');
|
|
234
238
|
// Capture the caller's fields before yielding so a later mutation cannot move the fence.
|
|
235
239
|
const { credentialId, credentialType, accountIdentity } = identity;
|
|
240
|
+
if (this.#requireAssertion && accountIdentity === undefined)
|
|
241
|
+
throw new ClaustrumConsumerError('identity-unasserted', 'Claustrum dispatch requires a known account identity');
|
|
236
242
|
const token = await this.#token(signal);
|
|
237
243
|
const served = await this.#call(() => this.#client.getScoped({
|
|
238
244
|
credentialId,
|
|
@@ -253,21 +259,45 @@ export class ClaustrumScopedCustody {
|
|
|
253
259
|
throw new ClaustrumConsumerError('insufficient-validity', 'Claustrum served credential has insufficient validity');
|
|
254
260
|
}
|
|
255
261
|
const accessToken = accessTokenFromMaterial(served.material);
|
|
256
|
-
const
|
|
262
|
+
const assertedIdentity = served.accountId?.trim()
|
|
257
263
|
? served.accountId
|
|
258
|
-
:
|
|
264
|
+
: undefined;
|
|
265
|
+
if (this.#requireAssertion &&
|
|
266
|
+
(served.credentialId === undefined || assertedIdentity === undefined))
|
|
267
|
+
throw new ClaustrumConsumerError('identity-unasserted', 'Claustrum served credential did not assert its identity');
|
|
268
|
+
const parsedIdentity = assertedIdentity === undefined
|
|
269
|
+
? this.#parseIdentity?.(accessToken)
|
|
270
|
+
: undefined;
|
|
271
|
+
const servedIdentity = assertedIdentity ?? parsedIdentity;
|
|
259
272
|
if (accountIdentity !== undefined &&
|
|
260
273
|
servedIdentity !== undefined &&
|
|
261
274
|
servedIdentity !== accountIdentity) {
|
|
262
275
|
throw new ClaustrumConsumerError('identity-changed', 'Claustrum served credential identity changed');
|
|
263
276
|
}
|
|
264
|
-
const resolvedIdentity =
|
|
277
|
+
const resolvedIdentity = servedIdentity ?? accountIdentity;
|
|
278
|
+
const source = assertedIdentity !== undefined
|
|
279
|
+
? 'asserted'
|
|
280
|
+
: parsedIdentity !== undefined
|
|
281
|
+
? 'parsed'
|
|
282
|
+
: accountIdentity !== undefined
|
|
283
|
+
? 'expected'
|
|
284
|
+
: 'none';
|
|
265
285
|
const attempt = Object.freeze(Object.defineProperty({
|
|
266
286
|
credentialId,
|
|
267
287
|
credentialType,
|
|
268
288
|
...(resolvedIdentity !== undefined && {
|
|
269
289
|
accountIdentity: resolvedIdentity,
|
|
270
290
|
}),
|
|
291
|
+
accountIdentitySource: source,
|
|
292
|
+
...(accountIdentity !== undefined && {
|
|
293
|
+
expectedAccountIdentity: accountIdentity,
|
|
294
|
+
}),
|
|
295
|
+
...(served.credentialId !== undefined && {
|
|
296
|
+
assertedCredentialId: served.credentialId,
|
|
297
|
+
}),
|
|
298
|
+
...(assertedIdentity !== undefined && {
|
|
299
|
+
assertedAccountIdentity: assertedIdentity,
|
|
300
|
+
}),
|
|
271
301
|
recordVersion: served.recordVersion,
|
|
272
302
|
expiresAtMs,
|
|
273
303
|
}, 'accessToken', { value: accessToken, enumerable: false }));
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `code`, a `class` and an `action`), so callers can tell the vault's verdict
|
|
5
5
|
* apart from a check made on this side.
|
|
6
6
|
*/
|
|
7
|
-
export type ClaustrumConsumerFailureKind = 'closed' | 'not-enrolled' | 'invalid-token' | 'unavailable' | 'identity-changed' | 'insufficient-validity' | 'invalid-material' | 'not-active' | 'route-unavailable' | 'route-declined' | 'no-receipt' | 'unsafe-file' | 'invalid-state' | 'wrong-consumer' | 'roster-busy' | 'host-slot-login' | 'host-slot-placeholder' | 'placeholder-refresh';
|
|
7
|
+
export type ClaustrumConsumerFailureKind = 'closed' | 'not-enrolled' | 'invalid-token' | 'unavailable' | 'identity-changed' | 'identity-unasserted' | 'insufficient-validity' | 'invalid-material' | 'not-active' | 'route-unavailable' | 'route-declined' | 'no-receipt' | 'unsafe-file' | 'invalid-state' | 'wrong-consumer' | 'roster-busy' | 'host-slot-login' | 'host-slot-placeholder' | 'placeholder-refresh';
|
|
8
8
|
export declare class ClaustrumConsumerError extends Error {
|
|
9
9
|
readonly kind: ClaustrumConsumerFailureKind;
|
|
10
10
|
constructor(kind: ClaustrumConsumerFailureKind, message: string);
|
|
@@ -2,12 +2,12 @@ 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 { ClaustrumConsumer, type ClaustrumConsumerOptions, type SendOptions, } from './consumer.js';
|
|
5
|
-
export { type ClaustrumFamily, type ClaustrumScopedAttempt, type ClaustrumScopedClient, ClaustrumScopedCustody, type ClaustrumScopedIdentity, decideScopedRetryAfter401, type IdentityParser, isScopedCredentialRotation, type ScopedRetryReason, SERVING_MARGIN_MS, type SkippedVaultRecord, type VaultCredential, type VaultCredentialType, type VaultInventory, } from './custody.js';
|
|
5
|
+
export { type AccountIdentitySource, type ClaustrumFamily, type ClaustrumScopedAttempt, type ClaustrumScopedClient, ClaustrumScopedCustody, type ClaustrumScopedIdentity, decideScopedRetryAfter401, type IdentityParser, isScopedCredentialRotation, type ScopedRetryReason, SERVING_MARGIN_MS, type SkippedVaultReason, type SkippedVaultRecord, type VaultCredential, type VaultCredentialType, type VaultInventory, } from './custody.js';
|
|
6
6
|
export { type ClaustrumEnrollmentClient, type ClaustrumEnrollmentConnection, ClaustrumEnrollmentManager, type ClaustrumEnrollmentPaths, type ClaustrumEnrollmentResetResult, type ClaustrumEnrollmentStatus, classifyEnrollmentError, connectClaustrumEnrollmentClient, type EnrollmentDisposition, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, } from './enrollment.js';
|
|
7
7
|
export { ClaustrumConsumerError, type ClaustrumConsumerFailureKind, type ClaustrumLogger, } from './errors.js';
|
|
8
8
|
export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, type HostSlotContent, isCustodyPlaceholder, isCustodyPlaceholderValue, } from './host-slot.js';
|
|
9
|
-
export { acceptAccount, type DeclinedAccount, declineAccount, isDeclined,
|
|
10
|
-
export { type AccountMapper, acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, type ProjectionOptions, projectVaultRoster, readVaultRoster, recordVaultQuota, refreshVaultRoster, type VaultRosterFile, type VaultRosterRow, vaultRoutingRows, } from './roster.js';
|
|
9
|
+
export { acceptAccount, type DeclinedAccount, declineAccount, isDeclined, } from './interlock.js';
|
|
10
|
+
export { type AccountMapper, acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, type ProjectionOptions, projectVaultRoster, type QuotaReceipt, readVaultRoster, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, type VaultPrimary, type VaultPrimaryBinding, type VaultPrimaryUnavailableReason, type VaultRosterFile, type VaultRosterRow, vaultRoutingRows, } from './roster.js';
|
|
11
11
|
/**
|
|
12
12
|
* Connect the client that lists and fetches this consumer's vault credentials
|
|
13
13
|
* on the request path. `connectionFile` is required: this library
|
package/dist/claustrum/index.js
CHANGED
|
@@ -10,8 +10,8 @@ export { ClaustrumScopedCustody, decideScopedRetryAfter401, isScopedCredentialRo
|
|
|
10
10
|
export { ClaustrumEnrollmentManager, classifyEnrollmentError, connectClaustrumEnrollmentClient, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, } from './enrollment.js';
|
|
11
11
|
export { ClaustrumConsumerError, } from './errors.js';
|
|
12
12
|
export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, isCustodyPlaceholder, isCustodyPlaceholderValue, } from './host-slot.js';
|
|
13
|
-
export { acceptAccount, declineAccount, isDeclined,
|
|
14
|
-
export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultQuota, refreshVaultRoster, vaultRoutingRows, } from './roster.js';
|
|
13
|
+
export { acceptAccount, declineAccount, isDeclined, } from './interlock.js';
|
|
14
|
+
export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, vaultRoutingRows, } from './roster.js';
|
|
15
15
|
/**
|
|
16
16
|
* Connect the client that lists and fetches this consumer's vault credentials
|
|
17
17
|
* on the request path. `connectionFile` is required: this library
|
|
@@ -2,28 +2,26 @@
|
|
|
2
2
|
* The declined-account interlock: the user's "do not use this vault account",
|
|
3
3
|
* kept on the consumer side because the vault has no notion of it.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* A decline follows the provider account, not the vault record: an entry made
|
|
6
|
+
* while the account was known matches any credential id or alias that logs
|
|
7
|
+
* into that same account, so relabelling, adding an alias or removing and
|
|
8
|
+
* re-adding the account never lifts it. A credential id that now logs into a
|
|
9
|
+
* different known account does not match, because that account has its own
|
|
10
|
+
* policy. While either side's account is unknown the entry falls back to the
|
|
11
|
+
* credential id, so an adapter that makes no identity claim cannot slip past
|
|
12
|
+
* it. Never keyed on the record version, so a token refresh cannot lift it.
|
|
12
13
|
*/
|
|
13
14
|
export interface DeclinedAccount {
|
|
14
15
|
readonly credentialId: string;
|
|
15
16
|
readonly accountIdentity?: string;
|
|
16
17
|
}
|
|
17
18
|
export declare function isDeclined(entries: readonly DeclinedAccount[], credentialId: string, accountIdentity?: string): boolean;
|
|
19
|
+
export declare function declineAccount(entries: readonly DeclinedAccount[], credentialId: string, accountIdentity?: string): DeclinedAccount[];
|
|
18
20
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Lift every entry that declines a row: the row's credential ids (the
|
|
22
|
+
* representative and its aliases) under the row's account. An entry for a
|
|
23
|
+
* different known account is left alone even when it names one of these
|
|
24
|
+
* credential ids, so accepting a replacement never re-enables the account the
|
|
25
|
+
* user declined.
|
|
23
26
|
*/
|
|
24
|
-
export declare function
|
|
25
|
-
credentialId: string;
|
|
26
|
-
accountIdentity?: string;
|
|
27
|
-
}[]): DeclinedAccount[];
|
|
28
|
-
export declare function declineAccount(entries: readonly DeclinedAccount[], credentialId: string, accountIdentity?: string): DeclinedAccount[];
|
|
29
|
-
export declare function acceptAccount(entries: readonly DeclinedAccount[], credentialIds: readonly string[]): DeclinedAccount[];
|
|
27
|
+
export declare function acceptAccount(entries: readonly DeclinedAccount[], credentialIds: readonly string[], accountIdentity?: string): DeclinedAccount[];
|
|
@@ -1,36 +1,29 @@
|
|
|
1
1
|
function matches(entry, credentialId, accountIdentity) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
entry.accountIdentity === accountIdentity));
|
|
2
|
+
if (entry.accountIdentity !== undefined && accountIdentity !== undefined)
|
|
3
|
+
return entry.accountIdentity === accountIdentity;
|
|
4
|
+
return entry.credentialId === credentialId;
|
|
6
5
|
}
|
|
7
6
|
export function isDeclined(entries, credentialId, accountIdentity) {
|
|
8
7
|
return entries.some((entry) => matches(entry, credentialId, accountIdentity));
|
|
9
8
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Drop entries the vault has proven stale: the same credential id now listed
|
|
12
|
-
* with a known identity that differs from the declined one. Entries for
|
|
13
|
-
* credentials that are not listed stay, so an account that leaves and comes
|
|
14
|
-
* back is still declined.
|
|
15
|
-
*/
|
|
16
|
-
export function pruneDeclined(entries, listed) {
|
|
17
|
-
return entries.filter((entry) => {
|
|
18
|
-
const current = listed.find((credential) => credential.credentialId === entry.credentialId);
|
|
19
|
-
return !(current &&
|
|
20
|
-
entry.accountIdentity !== undefined &&
|
|
21
|
-
current.accountIdentity !== undefined &&
|
|
22
|
-
current.accountIdentity !== entry.accountIdentity);
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
9
|
export function declineAccount(entries, credentialId, accountIdentity) {
|
|
10
|
+
const entry = {
|
|
11
|
+
credentialId,
|
|
12
|
+
...(accountIdentity !== undefined && { accountIdentity }),
|
|
13
|
+
};
|
|
26
14
|
return [
|
|
27
|
-
...entries.filter((
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
...(accountIdentity !== undefined && { accountIdentity }),
|
|
31
|
-
},
|
|
15
|
+
...entries.filter((existing) => existing.credentialId !== credentialId ||
|
|
16
|
+
existing.accountIdentity !== accountIdentity),
|
|
17
|
+
entry,
|
|
32
18
|
];
|
|
33
19
|
}
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Lift every entry that declines a row: the row's credential ids (the
|
|
22
|
+
* representative and its aliases) under the row's account. An entry for a
|
|
23
|
+
* different known account is left alone even when it names one of these
|
|
24
|
+
* credential ids, so accepting a replacement never re-enables the account the
|
|
25
|
+
* user declined.
|
|
26
|
+
*/
|
|
27
|
+
export function acceptAccount(entries, credentialIds, accountIdentity) {
|
|
28
|
+
return entries.filter((entry) => !credentialIds.some((id) => matches(entry, id, accountIdentity)));
|
|
36
29
|
}
|