@cortexkit/common-auth 0.2.5 → 0.2.7
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/claustrum/consumer.d.ts +93 -0
- package/dist/claustrum/consumer.js +276 -0
- package/dist/claustrum/custody.d.ts +128 -0
- package/dist/claustrum/custody.js +321 -0
- package/dist/claustrum/enrollment.d.ts +121 -0
- package/dist/claustrum/enrollment.js +579 -0
- package/dist/claustrum/errors.d.ts +16 -0
- package/dist/claustrum/errors.js +8 -0
- package/dist/claustrum/host-slot.d.ts +39 -0
- package/dist/claustrum/host-slot.js +72 -0
- package/dist/claustrum/index.d.ts +18 -1
- package/dist/claustrum/index.js +22 -2
- package/dist/claustrum/interlock.d.ts +29 -0
- package/dist/claustrum/interlock.js +36 -0
- package/dist/claustrum/roster.d.ts +103 -0
- package/dist/claustrum/roster.js +334 -0
- package/dist/commands/builtins.d.ts +71 -0
- package/dist/commands/builtins.js +508 -0
- package/dist/commands/index.d.ts +10 -1
- package/dist/commands/index.js +5 -2
- package/dist/commands/menu.d.ts +39 -0
- package/dist/commands/menu.js +249 -0
- package/dist/commands/model.d.ts +188 -0
- package/dist/commands/model.js +16 -0
- package/dist/commands/pi.d.ts +22 -0
- package/dist/commands/pi.js +178 -0
- package/dist/commands/seam.d.ts +36 -0
- package/dist/commands/seam.js +185 -0
- package/dist/store/errors.d.ts +1 -1
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.js +1 -0
- package/dist/store/pool.d.ts +12 -0
- package/dist/store/pool.js +3 -0
- package/dist/store/settings.d.ts +63 -0
- package/dist/store/settings.js +120 -0
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ClaustrumReporterSource } from '@cortexkit/claustrum-client';
|
|
2
|
+
import type { QuotaObservation } from '../quota/index.js';
|
|
3
|
+
import type { RoutingRow } from '../routing/index.js';
|
|
4
|
+
import { type ClaustrumFamily, type ClaustrumScopedAttempt, type ClaustrumScopedClient, type IdentityParser } from './custody.js';
|
|
5
|
+
import { type ClaustrumLogger } from './errors.js';
|
|
6
|
+
import { type AccountMapper, type VaultRosterFile } from './roster.js';
|
|
7
|
+
export interface ClaustrumConsumerOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The roster file: one row per vault account (route id, credential id,
|
|
10
|
+
* account identity, state, quota), plus the accounts the user declined. It
|
|
11
|
+
* never holds a token.
|
|
12
|
+
*/
|
|
13
|
+
rosterPath: string;
|
|
14
|
+
/** This host's enrollment token, written by setup. */
|
|
15
|
+
tokenPath: string;
|
|
16
|
+
family: ClaustrumFamily;
|
|
17
|
+
connect: () => Promise<ClaustrumScopedClient>;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the plugin currently takes its accounts from the vault (custody
|
|
20
|
+
* mode) rather than from local logins. Checked before every discovery,
|
|
21
|
+
* commit and dispatch; false closes the vault connection and refuses sends.
|
|
22
|
+
*/
|
|
23
|
+
isCustodyActive?: () => boolean | Promise<boolean>;
|
|
24
|
+
/** Ids of the plugin's local pool rows; no vault route id may equal one. */
|
|
25
|
+
reservedRouteIds?: () => Iterable<string>;
|
|
26
|
+
routePrefix?: string;
|
|
27
|
+
mapAccount?: AccountMapper;
|
|
28
|
+
parseIdentity?: IdentityParser;
|
|
29
|
+
/**
|
|
30
|
+
* Fired once per change of the vault's view cursor, including the first
|
|
31
|
+
* roster. A poll that sees the same view does not fire, and neither does a
|
|
32
|
+
* routine token refresh, which never moves the view.
|
|
33
|
+
*/
|
|
34
|
+
onRoster?: (roster: VaultRosterFile) => void;
|
|
35
|
+
onError?: (error: unknown) => void;
|
|
36
|
+
pollIntervalMs?: number;
|
|
37
|
+
setTimeoutImpl?: typeof setTimeout;
|
|
38
|
+
clearTimeoutImpl?: typeof clearTimeout;
|
|
39
|
+
now?: () => number;
|
|
40
|
+
logger?: ClaustrumLogger;
|
|
41
|
+
}
|
|
42
|
+
export interface SendOptions {
|
|
43
|
+
/**
|
|
44
|
+
* What kind of request this is (for example `model`, `quota`, `profile`),
|
|
45
|
+
* recorded in the 401 retry log line so a retry can be traced to its caller.
|
|
46
|
+
*/
|
|
47
|
+
site: string;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
reporterSource?: ClaustrumReporterSource;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One plugin host's view of its vault accounts. It turns vault credentials
|
|
53
|
+
* into rows for `/routing`, authorizes each physical send with the vault, and
|
|
54
|
+
* reports a 401 against the exact record version that send used. Metadata
|
|
55
|
+
* (the connection and discovery) is shared and coalesced; credential reads
|
|
56
|
+
* never are. It never enrolls: a missing token is an error here, and setup
|
|
57
|
+
* is where enrollment happens.
|
|
58
|
+
*/
|
|
59
|
+
export declare class ClaustrumConsumer {
|
|
60
|
+
#private;
|
|
61
|
+
constructor(options: ClaustrumConsumerOptions);
|
|
62
|
+
/** The last committed roster this instance saw, without any I/O. */
|
|
63
|
+
snapshot(): VaultRosterFile | undefined;
|
|
64
|
+
/** Rows for `/routing`, from the last committed roster. */
|
|
65
|
+
routingRows(): RoutingRow[];
|
|
66
|
+
refresh(): Promise<VaultRosterFile | undefined>;
|
|
67
|
+
start(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Authorize one physical send on a vault route. The roster file is re-read
|
|
70
|
+
* first, so a decline committed by another process applies before the next
|
|
71
|
+
* poll, and a route whose credential or account changed is refused.
|
|
72
|
+
*/
|
|
73
|
+
authorize(routeId: string, signal?: AbortSignal): Promise<ClaustrumScopedAttempt>;
|
|
74
|
+
reportFailure(attempt: ClaustrumScopedAttempt, status: number, source: ClaustrumReporterSource): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Send on a vault route. `dispatch` builds and sends the request with the
|
|
77
|
+
* receipt's token and may be called twice, so it must be able to rebuild
|
|
78
|
+
* its body. A 401 is retried once, and only when the vault now serves a new
|
|
79
|
+
* record version of the same credential and account; the final 401 is
|
|
80
|
+
* reported against the version that send actually used. Each call to
|
|
81
|
+
* `dispatch` gets its own receipt.
|
|
82
|
+
*/
|
|
83
|
+
send(routeId: string, dispatch: (attempt: ClaustrumScopedAttempt, signal?: AbortSignal) => Promise<Response>, options: SendOptions): Promise<Response>;
|
|
84
|
+
/** Decline a vault account: it stays listed and never routes until accepted. */
|
|
85
|
+
decline(routeId: string): Promise<void>;
|
|
86
|
+
accept(routeId: string): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Store a quota observation for a vault route. Pass the receipt the reading
|
|
89
|
+
* was taken with, so an observation for a replaced account is dropped.
|
|
90
|
+
*/
|
|
91
|
+
recordQuota(routeId: string, observation: QuotaObservation, attempt?: Pick<ClaustrumScopedAttempt, 'accountIdentity'>): Promise<boolean>;
|
|
92
|
+
close(): void;
|
|
93
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { createLogger } from '../logger/index.js';
|
|
2
|
+
import { ClaustrumScopedCustody, decideScopedRetryAfter401, } from './custody.js';
|
|
3
|
+
import { ClaustrumConsumerError } from './errors.js';
|
|
4
|
+
import { acceptVaultRoute, declineVaultRoute, readVaultRoster, recordVaultQuota, refreshVaultRoster, vaultRoutingRows, } from './roster.js';
|
|
5
|
+
/**
|
|
6
|
+
* One plugin host's view of its vault accounts. It turns vault credentials
|
|
7
|
+
* into rows for `/routing`, authorizes each physical send with the vault, and
|
|
8
|
+
* reports a 401 against the exact record version that send used. Metadata
|
|
9
|
+
* (the connection and discovery) is shared and coalesced; credential reads
|
|
10
|
+
* never are. It never enrolls: a missing token is an error here, and setup
|
|
11
|
+
* is where enrollment happens.
|
|
12
|
+
*/
|
|
13
|
+
export class ClaustrumConsumer {
|
|
14
|
+
#options;
|
|
15
|
+
#shutdown = new AbortController();
|
|
16
|
+
#logger;
|
|
17
|
+
#custody;
|
|
18
|
+
#connecting;
|
|
19
|
+
#refreshing;
|
|
20
|
+
#roster;
|
|
21
|
+
#timer;
|
|
22
|
+
#started = false;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.#options = options;
|
|
25
|
+
this.#logger = options.logger ?? createLogger('claustrum');
|
|
26
|
+
}
|
|
27
|
+
#assertOpen(signal) {
|
|
28
|
+
this.#shutdown.signal.throwIfAborted();
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
}
|
|
31
|
+
async #active() {
|
|
32
|
+
return (await this.#options.isCustodyActive?.()) ?? true;
|
|
33
|
+
}
|
|
34
|
+
#wait(pending, signal) {
|
|
35
|
+
const combined = AbortSignal.any([
|
|
36
|
+
this.#shutdown.signal,
|
|
37
|
+
...(signal ? [signal] : []),
|
|
38
|
+
]);
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const cleanup = () => combined.removeEventListener('abort', abort);
|
|
41
|
+
const abort = () => {
|
|
42
|
+
cleanup();
|
|
43
|
+
reject(combined.reason);
|
|
44
|
+
};
|
|
45
|
+
combined.addEventListener('abort', abort, { once: true });
|
|
46
|
+
// Observe the operation even after cancellation: shared metadata work may
|
|
47
|
+
// finish for other callers, and a late connection must still be closed.
|
|
48
|
+
pending.then((value) => {
|
|
49
|
+
cleanup();
|
|
50
|
+
if (combined.aborted)
|
|
51
|
+
reject(combined.reason);
|
|
52
|
+
else
|
|
53
|
+
resolve(value);
|
|
54
|
+
}, (error) => {
|
|
55
|
+
cleanup();
|
|
56
|
+
reject(error);
|
|
57
|
+
});
|
|
58
|
+
if (combined.aborted)
|
|
59
|
+
abort();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async #getCustody() {
|
|
63
|
+
this.#assertOpen();
|
|
64
|
+
if (this.#custody)
|
|
65
|
+
return this.#custody;
|
|
66
|
+
if (!this.#connecting) {
|
|
67
|
+
this.#connecting = this.#options
|
|
68
|
+
.connect()
|
|
69
|
+
.then((client) => {
|
|
70
|
+
if (this.#shutdown.signal.aborted) {
|
|
71
|
+
client.close();
|
|
72
|
+
this.#assertOpen();
|
|
73
|
+
}
|
|
74
|
+
this.#custody = new ClaustrumScopedCustody({
|
|
75
|
+
client,
|
|
76
|
+
family: this.#options.family,
|
|
77
|
+
tokenPath: this.#options.tokenPath,
|
|
78
|
+
parseIdentity: this.#options.parseIdentity,
|
|
79
|
+
now: this.#options.now,
|
|
80
|
+
logger: this.#logger,
|
|
81
|
+
});
|
|
82
|
+
return this.#custody;
|
|
83
|
+
})
|
|
84
|
+
.finally(() => {
|
|
85
|
+
this.#connecting = undefined;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return this.#wait(this.#connecting);
|
|
89
|
+
}
|
|
90
|
+
/** The last committed roster this instance saw, without any I/O. */
|
|
91
|
+
snapshot() {
|
|
92
|
+
return this.#roster;
|
|
93
|
+
}
|
|
94
|
+
/** Rows for `/routing`, from the last committed roster. */
|
|
95
|
+
routingRows() {
|
|
96
|
+
return vaultRoutingRows(this.#roster);
|
|
97
|
+
}
|
|
98
|
+
refresh() {
|
|
99
|
+
this.#assertOpen();
|
|
100
|
+
if (this.#refreshing)
|
|
101
|
+
return this.#refreshing;
|
|
102
|
+
this.#refreshing = (async () => {
|
|
103
|
+
if (!(await this.#active())) {
|
|
104
|
+
this.#roster = undefined;
|
|
105
|
+
this.#custody?.close();
|
|
106
|
+
this.#custody = undefined;
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const custody = await this.#getCustody();
|
|
110
|
+
const roster = await refreshVaultRoster({
|
|
111
|
+
path: this.#options.rosterPath,
|
|
112
|
+
custody,
|
|
113
|
+
isActive: () => this.#active(),
|
|
114
|
+
signal: this.#shutdown.signal,
|
|
115
|
+
projection: () => ({
|
|
116
|
+
reservedRouteIds: new Set(this.#options.reservedRouteIds?.() ?? []),
|
|
117
|
+
routePrefix: this.#options.routePrefix,
|
|
118
|
+
mapAccount: this.#options.mapAccount,
|
|
119
|
+
now: (this.#options.now ?? Date.now)(),
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
this.#assertOpen();
|
|
123
|
+
const changed = roster?.view !== this.#roster?.view;
|
|
124
|
+
this.#roster = roster;
|
|
125
|
+
// Notify on view changes only: subscribers rewrite shared state on each
|
|
126
|
+
// notification, and the poll runs every few seconds.
|
|
127
|
+
if (roster && changed)
|
|
128
|
+
this.#options.onRoster?.(roster);
|
|
129
|
+
return roster;
|
|
130
|
+
})().finally(() => {
|
|
131
|
+
this.#refreshing = undefined;
|
|
132
|
+
});
|
|
133
|
+
return this.#refreshing;
|
|
134
|
+
}
|
|
135
|
+
start() {
|
|
136
|
+
if (this.#started)
|
|
137
|
+
return;
|
|
138
|
+
this.#assertOpen();
|
|
139
|
+
this.#started = true;
|
|
140
|
+
const tick = async () => {
|
|
141
|
+
try {
|
|
142
|
+
await this.refresh();
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (!this.#shutdown.signal.aborted)
|
|
146
|
+
this.#options.onError?.(error);
|
|
147
|
+
}
|
|
148
|
+
if (this.#shutdown.signal.aborted)
|
|
149
|
+
return;
|
|
150
|
+
const delay = this.#options.pollIntervalMs ?? 5_000;
|
|
151
|
+
if (delay <= 0)
|
|
152
|
+
return;
|
|
153
|
+
this.#timer = (this.#options.setTimeoutImpl ?? setTimeout)(() => {
|
|
154
|
+
this.#timer = undefined;
|
|
155
|
+
void tick();
|
|
156
|
+
}, delay);
|
|
157
|
+
this.#timer.unref?.();
|
|
158
|
+
};
|
|
159
|
+
void tick();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Authorize one physical send on a vault route. The roster file is re-read
|
|
163
|
+
* first, so a decline committed by another process applies before the next
|
|
164
|
+
* poll, and a route whose credential or account changed is refused.
|
|
165
|
+
*/
|
|
166
|
+
async authorize(routeId, signal) {
|
|
167
|
+
this.#assertOpen(signal);
|
|
168
|
+
const roster = this.#roster ?? (await this.#wait(this.refresh(), signal));
|
|
169
|
+
if (!roster || !(await this.#active()))
|
|
170
|
+
throw new ClaustrumConsumerError('not-active', 'Claustrum scoped custody is not active');
|
|
171
|
+
const current = await readVaultRoster(this.#options.rosterPath);
|
|
172
|
+
this.#assertOpen(signal);
|
|
173
|
+
const observed = roster.rows.find((row) => row.routeId === routeId);
|
|
174
|
+
const configured = current?.rows.find((row) => row.routeId === routeId);
|
|
175
|
+
if (configured && !configured.enabled)
|
|
176
|
+
throw new ClaustrumConsumerError('route-declined', 'Claustrum route is disabled');
|
|
177
|
+
if (!observed ||
|
|
178
|
+
!configured ||
|
|
179
|
+
configured.state !== 'active' ||
|
|
180
|
+
configured.credentialId !== observed.credentialId ||
|
|
181
|
+
configured.accountIdentity !== observed.accountIdentity)
|
|
182
|
+
throw new ClaustrumConsumerError('route-unavailable', 'Claustrum route is disabled, removed or changed');
|
|
183
|
+
return (await this.#wait(this.#getCustody(), signal)).authorize({
|
|
184
|
+
credentialId: configured.credentialId,
|
|
185
|
+
credentialType: configured.credentialType,
|
|
186
|
+
...(configured.accountIdentity !== undefined && {
|
|
187
|
+
accountIdentity: configured.accountIdentity,
|
|
188
|
+
}),
|
|
189
|
+
}, AbortSignal.any([this.#shutdown.signal, ...(signal ? [signal] : [])]));
|
|
190
|
+
}
|
|
191
|
+
async reportFailure(attempt, status, source) {
|
|
192
|
+
this.#assertOpen();
|
|
193
|
+
if (!this.#custody)
|
|
194
|
+
throw new ClaustrumConsumerError('no-receipt', 'Claustrum dispatch receipt has no active owner');
|
|
195
|
+
await this.#custody.reportFailure(attempt, status, source);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Send on a vault route. `dispatch` builds and sends the request with the
|
|
199
|
+
* receipt's token and may be called twice, so it must be able to rebuild
|
|
200
|
+
* its body. A 401 is retried once, and only when the vault now serves a new
|
|
201
|
+
* record version of the same credential and account; the final 401 is
|
|
202
|
+
* reported against the version that send actually used. Each call to
|
|
203
|
+
* `dispatch` gets its own receipt.
|
|
204
|
+
*/
|
|
205
|
+
async send(routeId, dispatch, options) {
|
|
206
|
+
const signal = options.signal;
|
|
207
|
+
let served = await this.authorize(routeId, signal);
|
|
208
|
+
let response = await dispatch(served, signal);
|
|
209
|
+
if (response.status === 401 && !signal?.aborted) {
|
|
210
|
+
let current;
|
|
211
|
+
try {
|
|
212
|
+
current = await this.authorize(routeId, signal);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Re-authorization failed: keep this 401 and report it against the
|
|
216
|
+
// receipt (served record version) this request was sent with.
|
|
217
|
+
}
|
|
218
|
+
if (decideScopedRetryAfter401(options.site, served, current, this.#logger)) {
|
|
219
|
+
await response.body?.cancel().catch(() => { });
|
|
220
|
+
served = current;
|
|
221
|
+
response = await dispatch(current, signal);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (response.status === 401) {
|
|
225
|
+
await this.reportFailure(served, 401, options.reporterSource ?? 'direct').catch((error) => this.#options.onError?.(error));
|
|
226
|
+
// After the report the vault may mark the account as needing a new
|
|
227
|
+
// login; refresh now so routing drops it without waiting for the next poll. Deferred so a consumer closed in
|
|
228
|
+
// the meantime reports to onError instead of throwing from this send.
|
|
229
|
+
Promise.resolve()
|
|
230
|
+
.then(() => this.refresh())
|
|
231
|
+
.catch((error) => {
|
|
232
|
+
if (!this.#shutdown.signal.aborted)
|
|
233
|
+
this.#options.onError?.(error);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return response;
|
|
237
|
+
}
|
|
238
|
+
/** Decline a vault account: it stays listed and never routes until accepted. */
|
|
239
|
+
async decline(routeId) {
|
|
240
|
+
this.#assertOpen();
|
|
241
|
+
await declineVaultRoute(this.#options.rosterPath, routeId);
|
|
242
|
+
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
243
|
+
}
|
|
244
|
+
async accept(routeId) {
|
|
245
|
+
this.#assertOpen();
|
|
246
|
+
await acceptVaultRoute(this.#options.rosterPath, routeId);
|
|
247
|
+
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Store a quota observation for a vault route. Pass the receipt the reading
|
|
251
|
+
* was taken with, so an observation for a replaced account is dropped.
|
|
252
|
+
*/
|
|
253
|
+
async recordQuota(routeId, observation, attempt) {
|
|
254
|
+
this.#assertOpen();
|
|
255
|
+
const kept = await recordVaultQuota(this.#options.rosterPath, {
|
|
256
|
+
routeId,
|
|
257
|
+
observation,
|
|
258
|
+
...(attempt?.accountIdentity !== undefined && {
|
|
259
|
+
accountIdentity: attempt.accountIdentity,
|
|
260
|
+
}),
|
|
261
|
+
});
|
|
262
|
+
if (kept)
|
|
263
|
+
this.#roster = await readVaultRoster(this.#options.rosterPath);
|
|
264
|
+
return kept;
|
|
265
|
+
}
|
|
266
|
+
close() {
|
|
267
|
+
if (this.#shutdown.signal.aborted)
|
|
268
|
+
return;
|
|
269
|
+
this.#shutdown.abort(new ClaustrumConsumerError('closed', 'Claustrum consumer is closed'));
|
|
270
|
+
if (this.#timer)
|
|
271
|
+
(this.#options.clearTimeoutImpl ?? clearTimeout)(this.#timer);
|
|
272
|
+
this.#timer = undefined;
|
|
273
|
+
this.#custody?.close();
|
|
274
|
+
this.#roster = undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { type ClaustrumClient, type ClaustrumReporterSource, type EnrollmentTokenFile } from '@cortexkit/claustrum-client';
|
|
2
|
+
import { type ClaustrumLogger } from './errors.js';
|
|
3
|
+
export type ClaustrumScopedClient = Pick<ClaustrumClient, 'listScoped' | 'getScoped' | 'reportAuthFailureScoped' | 'close'>;
|
|
4
|
+
/** The two credential types a plugin's family can hold; routing differs by type. */
|
|
5
|
+
export type VaultCredentialType = 'oauth' | 'api_key';
|
|
6
|
+
/**
|
|
7
|
+
* Which vault rows belong to this plugin. `refreshAdapter` classifies an OAuth
|
|
8
|
+
* row by the protocol it speaks; `category` is the grant that authorizes this
|
|
9
|
+
* consumer to read it. Static API keys carry no refresh adapter, so they are
|
|
10
|
+
* admitted only when `apiKeys` is set and only by category.
|
|
11
|
+
*/
|
|
12
|
+
export interface ClaustrumFamily {
|
|
13
|
+
refreshAdapter: string;
|
|
14
|
+
category: string;
|
|
15
|
+
apiKeys?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** One vault credential this consumer may serve. Never carries bearer material. */
|
|
18
|
+
export interface VaultCredential {
|
|
19
|
+
readonly credentialId: string;
|
|
20
|
+
readonly credentialType: VaultCredentialType;
|
|
21
|
+
/**
|
|
22
|
+
* The provider account the credential logs into, when the vault's adapter
|
|
23
|
+
* claims one. Absent means the adapter makes no claim, not a mismatch.
|
|
24
|
+
*/
|
|
25
|
+
readonly accountIdentity?: string;
|
|
26
|
+
readonly state: string;
|
|
27
|
+
readonly email?: string;
|
|
28
|
+
readonly orgName?: string;
|
|
29
|
+
}
|
|
30
|
+
/** A vault record this consumer could not use, kept for the warning it raises. */
|
|
31
|
+
export interface SkippedVaultRecord {
|
|
32
|
+
readonly credentialId?: string;
|
|
33
|
+
readonly reason: string;
|
|
34
|
+
}
|
|
35
|
+
export interface VaultInventory {
|
|
36
|
+
/**
|
|
37
|
+
* The vault's change cursor: a digest over exactly what this consumer can see
|
|
38
|
+
* (ids, grants, state, identity), never over record versions, so a routine
|
|
39
|
+
* token refresh does not move it. Only equality is meaningful.
|
|
40
|
+
*/
|
|
41
|
+
readonly view: string;
|
|
42
|
+
readonly credentials: readonly VaultCredential[];
|
|
43
|
+
readonly skipped: readonly SkippedVaultRecord[];
|
|
44
|
+
}
|
|
45
|
+
export interface ClaustrumScopedIdentity {
|
|
46
|
+
readonly credentialId: string;
|
|
47
|
+
readonly credentialType: VaultCredentialType;
|
|
48
|
+
readonly accountIdentity?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A receipt: what the vault served for one physical send. Each attempt gets
|
|
52
|
+
* its own. It records the exact record version served, because a 401 for
|
|
53
|
+
* this send is reported to the vault against that version.
|
|
54
|
+
*/
|
|
55
|
+
export interface ClaustrumScopedAttempt {
|
|
56
|
+
readonly credentialId: string;
|
|
57
|
+
readonly credentialType: VaultCredentialType;
|
|
58
|
+
readonly accountIdentity?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Kept in memory only and hidden from JSON.stringify and object spreads, so
|
|
61
|
+
* logging a receipt never leaks it. Authorize again for every dispatch and retry.
|
|
62
|
+
*/
|
|
63
|
+
readonly accessToken: string;
|
|
64
|
+
readonly recordVersion: number;
|
|
65
|
+
readonly expiresAtMs: number | null;
|
|
66
|
+
}
|
|
67
|
+
/** Reads the provider identity a served token executes under, when the plugin can tell. */
|
|
68
|
+
export type IdentityParser = (accessToken: string) => string | undefined;
|
|
69
|
+
/** Only a new version of the same account may replace an in-flight 401. */
|
|
70
|
+
export declare function isScopedCredentialRotation(served: ClaustrumScopedAttempt, current: ClaustrumScopedAttempt | undefined): current is ClaustrumScopedAttempt;
|
|
71
|
+
/** Why a scoped 401 did or did not retry. Never carries credential material. */
|
|
72
|
+
export type ScopedRetryReason = 'rotated' | 'reauthorize-failed' | 'credential-changed' | 'account-changed' | 'version-unchanged';
|
|
73
|
+
/**
|
|
74
|
+
* Decide whether a request that got a 401 should retry with the freshly
|
|
75
|
+
* re-authorized receipt, and log the decision. The vault can refresh a
|
|
76
|
+
* credential between the send and the 401; the log line lets that refresh be
|
|
77
|
+
* matched against the consumer's retry. `site` names the kind of request.
|
|
78
|
+
*/
|
|
79
|
+
export declare function decideScopedRetryAfter401(site: string, served: ClaustrumScopedAttempt, current: ClaustrumScopedAttempt | undefined, logger?: ClaustrumLogger): current is ClaustrumScopedAttempt;
|
|
80
|
+
/**
|
|
81
|
+
* How long a served OAuth token must stay valid after the vault hands it out,
|
|
82
|
+
* so it cannot expire while a long request is still using it.
|
|
83
|
+
*/
|
|
84
|
+
export declare const SERVING_MARGIN_MS = 300000;
|
|
85
|
+
/**
|
|
86
|
+
* Reads this consumer's vault credentials (list, fetch, 401 report), with the
|
|
87
|
+
* enrollment token as authorization. Used by every host of a plugin. There is
|
|
88
|
+
* deliberately no credential cache and no single-flight of credential reads:
|
|
89
|
+
* each physical send is authorized by the vault, so a revoked enrollment or a
|
|
90
|
+
* changed record takes effect on the next send. The enrollment token is
|
|
91
|
+
* re-read per operation so an operator's reissue on disk is picked up.
|
|
92
|
+
*/
|
|
93
|
+
export declare class ClaustrumScopedCustody {
|
|
94
|
+
#private;
|
|
95
|
+
constructor(options: {
|
|
96
|
+
client: ClaustrumScopedClient;
|
|
97
|
+
family: ClaustrumFamily;
|
|
98
|
+
tokenPath?: string;
|
|
99
|
+
readToken?: () => Promise<EnrollmentTokenFile>;
|
|
100
|
+
parseIdentity?: IdentityParser;
|
|
101
|
+
now?: () => number;
|
|
102
|
+
logger?: ClaustrumLogger;
|
|
103
|
+
});
|
|
104
|
+
/**
|
|
105
|
+
* List this consumer's credentials. A record that cannot be used is skipped
|
|
106
|
+
* and warned about rather than failing the whole list, so one bad record
|
|
107
|
+
* never hides every other account. Records outside the family are not
|
|
108
|
+
* skipped records: they simply are not this consumer's.
|
|
109
|
+
*/
|
|
110
|
+
discover(signal?: AbortSignal): Promise<VaultInventory>;
|
|
111
|
+
/**
|
|
112
|
+
* Fetch the credential for one physical send and wrap it in a fresh receipt.
|
|
113
|
+
* Identity is checked only where both sides assert one: the vault's served
|
|
114
|
+
* identity (or, without one, the plugin's parse of the token) must equal the
|
|
115
|
+
* roster's when both are present; absence on either side proves nothing and
|
|
116
|
+
* does not refuse.
|
|
117
|
+
*/
|
|
118
|
+
authorize(identity: ClaustrumScopedIdentity, signal?: AbortSignal): Promise<ClaustrumScopedAttempt>;
|
|
119
|
+
/**
|
|
120
|
+
* Report that a send this consumer actually made was rejected. Only a 401
|
|
121
|
+
* is reported (the vault refuses 429, 402 and 5xx reports), addressed by
|
|
122
|
+
* credential id with the exact record version that send was served, and
|
|
123
|
+
* signed with the enrollment token that authorized it. A receipt this
|
|
124
|
+
* custody did not issue is refused rather than reported.
|
|
125
|
+
*/
|
|
126
|
+
reportFailure(attempt: ClaustrumScopedAttempt, status: number, reporterSource: ClaustrumReporterSource): Promise<void>;
|
|
127
|
+
close(): void;
|
|
128
|
+
}
|