@cortexkit/anthropic-auth-core 2.0.2 → 2.1.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.
- package/dist/accounts.d.ts +7 -1
- package/dist/cache1h.d.ts +2 -0
- package/dist/cachekeep-registry.d.ts +3 -0
- package/dist/cachekeep.d.ts +2 -0
- package/dist/cch.d.ts +19 -0
- package/dist/claude-code.d.ts +8 -2
- package/dist/claustrum-enrollment.d.ts +5 -1
- package/dist/constants.d.ts +15 -3
- package/dist/dump.d.ts +4 -0
- package/dist/fast.d.ts +2 -0
- package/dist/index.d.ts +15 -8
- package/dist/index.js +29647 -25296
- package/dist/insula-quota.d.ts +25 -0
- package/dist/internal-types/auth-menu/terminal.d.ts +43 -0
- package/dist/internal-types/claustrum/availability.d.ts +89 -0
- package/dist/internal-types/claustrum/consumer.d.ts +172 -0
- package/dist/internal-types/claustrum/custody.d.ts +299 -0
- package/dist/internal-types/claustrum/enrollment.d.ts +164 -0
- package/dist/internal-types/claustrum/errors.d.ts +17 -0
- package/dist/internal-types/claustrum/host-slot.d.ts +39 -0
- package/dist/internal-types/claustrum/index.d.ts +21 -0
- package/dist/internal-types/claustrum/insula.d.ts +117 -0
- package/dist/internal-types/claustrum/interlock.d.ts +27 -0
- package/dist/internal-types/claustrum/roster.d.ts +221 -0
- package/dist/internal-types/claustrum/watch.d.ts +141 -0
- package/dist/internal-types/fs/refresh-file-lock.d.ts +8 -2
- package/dist/internal-types/quota/format.d.ts +43 -3
- package/dist/internal-types/quota/index.d.ts +3 -3
- package/dist/internal-types/quota/projection.d.ts +16 -0
- package/dist/internal-types/routing/admission.d.ts +93 -0
- package/dist/internal-types/routing/index.d.ts +8 -0
- package/dist/internal-types/routing/ordered.d.ts +47 -0
- package/dist/internal-types/routing/pins.d.ts +22 -0
- package/dist/internal-types/routing/sticky.d.ts +158 -0
- package/dist/internal-types/setup/command-error.d.ts +32 -0
- package/dist/internal-types/setup/command.d.ts +3 -0
- package/dist/internal-types/setup/detect.d.ts +6 -0
- package/dist/internal-types/setup/index.d.ts +6 -0
- package/dist/internal-types/setup/process-fence.d.ts +43 -0
- package/dist/internal-types/setup/register.d.ts +11 -0
- package/dist/internal-types/setup/types.d.ts +150 -0
- package/dist/internal-types/store/pool.d.ts +1 -1
- package/dist/internal-types/store/refresh-lock.d.ts +0 -1
- package/dist/logging.d.ts +2 -0
- package/dist/mid-conversation-output-config.d.ts +34 -2
- package/dist/migration-source.d.ts +2 -0
- package/dist/native-account-runtime.d.ts +9 -4
- package/dist/native-account-view.d.ts +19 -3
- package/dist/native-custody-activation.d.ts +1 -1
- package/dist/native-custody.d.ts +130 -0
- package/dist/native-enrollment-authority.d.ts +5 -1
- package/dist/native-host-auth-read.d.ts +2 -1
- package/dist/native-host-auth-write.d.ts +2 -0
- package/dist/native-menu-executor.d.ts +1 -1
- package/dist/native-migration.d.ts +25 -4
- package/dist/native-quota-observations.d.ts +26 -0
- package/dist/native-runtime.d.ts +5 -1
- package/dist/native-transient-account.d.ts +3 -4
- package/dist/native-ui.d.ts +2 -0
- package/dist/native-usage-publication.d.ts +5 -0
- package/dist/native-vault-invalidation.d.ts +9 -0
- package/dist/native-vault-runtime.d.ts +36 -3
- package/dist/network-errors.d.ts +1 -0
- package/dist/owner-only.d.ts +19 -0
- package/dist/pool-authority.d.ts +27 -8
- package/dist/pool-paths.d.ts +2 -0
- package/dist/provider-error.d.ts +34 -0
- package/dist/publication-rename.d.ts +11 -0
- package/dist/quota-manager.d.ts +21 -4
- package/dist/quotas.d.ts +31 -2
- package/dist/relay.d.ts +20 -0
- package/dist/request-gzip.d.ts +99 -0
- package/package.json +4 -4
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { InsulaQuotaContext, InsulaUsageEntry } from "./internal-types/claustrum/index.js";
|
|
2
|
+
import { type AccountQuotaWindow, type AccountScopedQuotaWindow, type OAuthQuotaSnapshot } from './accounts.ts';
|
|
3
|
+
/** An insula reading in the shape our usage poll produces, before the account identity is attached. */
|
|
4
|
+
export type InsulaClaudeQuota = OAuthQuotaSnapshot & {
|
|
5
|
+
five_hour: AccountQuotaWindow;
|
|
6
|
+
seven_day: AccountQuotaWindow;
|
|
7
|
+
scoped: AccountScopedQuotaWindow[];
|
|
8
|
+
checkedAt: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Convert insula's Claude entry into our usage-poll snapshot. Every window's
|
|
12
|
+
* checkedAt is when insula fetched the reading, never when we read its cache,
|
|
13
|
+
* so a newer header or poll reading still wins. Both standard windows must be
|
|
14
|
+
* present; otherwise this returns undefined and our own poll stays in charge.
|
|
15
|
+
* The tertiary slot (legacy seven_day_opus/sonnet) is not a window we model.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeInsulaClaudeQuota(entry: InsulaUsageEntry, context: InsulaQuotaContext): InsulaClaudeQuota | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Insula fetches on its own schedule, so the account's stored quota can hold
|
|
20
|
+
* a window read after insula's fetch (a response header, or our own poll).
|
|
21
|
+
* Stored quota is replaced as one snapshot, newest wins, so publishing the
|
|
22
|
+
* older insula reading as is would be refused and lose its other windows.
|
|
23
|
+
* Keep the newer reading of each window instead.
|
|
24
|
+
*/
|
|
25
|
+
export declare function mergeInsulaQuota(reading: InsulaClaudeQuota, stored: OAuthQuotaSnapshot | undefined): OAuthQuotaSnapshot;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal the menu draws on and reads keys from. The process's own
|
|
3
|
+
* stdin and stdout satisfy it; tests pass a scripted stand-in, so the full
|
|
4
|
+
* key-in, frame-out path runs without a real TTY.
|
|
5
|
+
*/
|
|
6
|
+
export interface MenuTerminal {
|
|
7
|
+
input: MenuInput;
|
|
8
|
+
output: MenuOutput;
|
|
9
|
+
/**
|
|
10
|
+
* Where SIGINT and SIGTERM are heard while a selector holds raw mode, so a
|
|
11
|
+
* kill restores the terminal. Defaults to nothing: raw mode turns Ctrl-C
|
|
12
|
+
* into a key the selector already handles.
|
|
13
|
+
*/
|
|
14
|
+
signals?: MenuSignals;
|
|
15
|
+
}
|
|
16
|
+
export interface MenuInput {
|
|
17
|
+
/** True only on an interactive terminal; anything else gets a plain list. */
|
|
18
|
+
readonly isTTY?: boolean;
|
|
19
|
+
readonly isRaw?: boolean;
|
|
20
|
+
setRawMode(mode: boolean): unknown;
|
|
21
|
+
resume(): unknown;
|
|
22
|
+
pause(): unknown;
|
|
23
|
+
on(event: 'data', listener: (data: Buffer | string) => void): unknown;
|
|
24
|
+
removeListener(event: 'data', listener: (data: Buffer | string) => void): unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface MenuOutput {
|
|
27
|
+
write(text: string): unknown;
|
|
28
|
+
readonly columns?: number;
|
|
29
|
+
readonly rows?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface MenuSignals {
|
|
32
|
+
once(signal: 'SIGINT' | 'SIGTERM', listener: () => void): unknown;
|
|
33
|
+
removeListener(signal: 'SIGINT' | 'SIGTERM', listener: () => void): unknown;
|
|
34
|
+
}
|
|
35
|
+
/** The current process's terminal, with its signals. */
|
|
36
|
+
export declare function processTerminal(): MenuTerminal;
|
|
37
|
+
/**
|
|
38
|
+
* Whether keys can be read one at a time. Only the input is checked, as the
|
|
39
|
+
* plugins' menus always did: output piped to a file still gets the menu.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isInteractive(terminal: MenuTerminal): boolean;
|
|
42
|
+
/** Writes one line of plain output below the menu. */
|
|
43
|
+
export declare function printLine(terminal: MenuTerminal, line?: string): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ClaustrumConsumerError } from './errors.js';
|
|
2
|
+
export type VaultFailureClass = 'unreachable' | 'answered';
|
|
3
|
+
/**
|
|
4
|
+
* Codes from claustrum-client's decoder and its subc transport. A retry action
|
|
5
|
+
* alone is not evidence of an outage: even transient vault refusals must win
|
|
6
|
+
* over a previously served credential. New codes fail closed until reviewed.
|
|
7
|
+
*/
|
|
8
|
+
export declare const VAULT_FAILURE_CLASSES: Readonly<{
|
|
9
|
+
ECONNREFUSED: "unreachable";
|
|
10
|
+
ENOENT: "unreachable";
|
|
11
|
+
ECONNRESET: "unreachable";
|
|
12
|
+
EPIPE: "unreachable";
|
|
13
|
+
ETIMEDOUT: "unreachable";
|
|
14
|
+
transport_error: "unreachable";
|
|
15
|
+
unavailable: "unreachable";
|
|
16
|
+
timeout: "unreachable";
|
|
17
|
+
route_closed: "unreachable";
|
|
18
|
+
daemon_draining: "unreachable";
|
|
19
|
+
connection_closed: "unreachable";
|
|
20
|
+
connection_dropped: "unreachable";
|
|
21
|
+
request_deadline: "unreachable";
|
|
22
|
+
deadline_exceeded_no_drop_observed: "unreachable";
|
|
23
|
+
module_reloading: "unreachable";
|
|
24
|
+
module_warming: "unreachable";
|
|
25
|
+
target_unavailable: "unreachable";
|
|
26
|
+
module_timeout: "unreachable";
|
|
27
|
+
invalid_response: "answered";
|
|
28
|
+
invalid_record_version: "answered";
|
|
29
|
+
invalid_expiry: "answered";
|
|
30
|
+
invalid_status: "answered";
|
|
31
|
+
invalid_params: "answered";
|
|
32
|
+
ttl_unsatisfiable: "answered";
|
|
33
|
+
not_found: "answered";
|
|
34
|
+
enrollment_revoked: "answered";
|
|
35
|
+
enrollment_invalid: "answered";
|
|
36
|
+
grant_denied: "answered";
|
|
37
|
+
credential_unavailable: "answered";
|
|
38
|
+
needs_reauth: "answered";
|
|
39
|
+
declined: "answered";
|
|
40
|
+
disabled: "answered";
|
|
41
|
+
identity_changed: "answered";
|
|
42
|
+
closed: "answered";
|
|
43
|
+
client_closed: "answered";
|
|
44
|
+
pending_exists: "answered";
|
|
45
|
+
pending_queue_full: "answered";
|
|
46
|
+
already_consumed: "answered";
|
|
47
|
+
superseded: "answered";
|
|
48
|
+
stale_generation: "answered";
|
|
49
|
+
missing_identity: "answered";
|
|
50
|
+
invalid_control_body: "answered";
|
|
51
|
+
unknown_module: "answered";
|
|
52
|
+
module_removed: "answered";
|
|
53
|
+
capability_denied: "answered";
|
|
54
|
+
unknown_channel: "unreachable";
|
|
55
|
+
stale_route_epoch: "unreachable";
|
|
56
|
+
capability_forbidden: "answered";
|
|
57
|
+
capability_unprovided: "answered";
|
|
58
|
+
capability_ambiguous: "answered";
|
|
59
|
+
invalid_capability_identifier: "answered";
|
|
60
|
+
binary_body_required: "answered";
|
|
61
|
+
binary_call_requires_call_binary: "answered";
|
|
62
|
+
corr_exhausted: "answered";
|
|
63
|
+
late_route_cleanup_failed: "answered";
|
|
64
|
+
reverse_request_capability_not_declared: "answered";
|
|
65
|
+
stale_route_handle: "answered";
|
|
66
|
+
reverse_request_unhandled: "answered";
|
|
67
|
+
handler_error: "answered";
|
|
68
|
+
health_error: "answered";
|
|
69
|
+
duplicate_module_id: "answered";
|
|
70
|
+
route_rejected: "answered";
|
|
71
|
+
lock_busy: "answered";
|
|
72
|
+
owner_invalid: "answered";
|
|
73
|
+
renewal_failed: "answered";
|
|
74
|
+
unsupported_platform: "answered";
|
|
75
|
+
EACCES: "answered";
|
|
76
|
+
EPERM: "answered";
|
|
77
|
+
ERR_PRIVATE_FILES_SECURITY: "answered";
|
|
78
|
+
}>;
|
|
79
|
+
/**
|
|
80
|
+
* An injected adapter may throw this ONLY when its connection/transport is
|
|
81
|
+
* unreachable, including a failed connect before getScoped. Do not wrap an
|
|
82
|
+
* adapter shutdown, cancellation, validation error or vault refusal in it.
|
|
83
|
+
* Its fixed message cannot echo enrollment or credential material, and its
|
|
84
|
+
* kind stays `unavailable` for callers handling ClaustrumConsumerError.
|
|
85
|
+
*/
|
|
86
|
+
export declare class ClaustrumUnreachableError extends ClaustrumConsumerError {
|
|
87
|
+
constructor();
|
|
88
|
+
}
|
|
89
|
+
export declare function classifyVaultFailure(error: unknown): VaultFailureClass;
|
|
@@ -0,0 +1,172 @@
|
|
|
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, type KeptReceiptState, type VaultGrantReach } from './custody.js';
|
|
5
|
+
import { type ClaustrumLogger } from './errors.js';
|
|
6
|
+
import { type InsulaQuotaOptions } from './insula.js';
|
|
7
|
+
import { type AccountMapper, type QuotaReceipt, type VaultRosterFile } from './roster.js';
|
|
8
|
+
import { type WatchMode } from './watch.js';
|
|
9
|
+
export interface ClaustrumConsumerOptions<Q = QuotaObservation> {
|
|
10
|
+
/**
|
|
11
|
+
* The roster file: one row per vault account (route id, credential id,
|
|
12
|
+
* account identity, state, quota), plus the accounts the user declined. It
|
|
13
|
+
* never holds a token.
|
|
14
|
+
*/
|
|
15
|
+
rosterPath: string;
|
|
16
|
+
/** This host's enrollment token, written by setup. */
|
|
17
|
+
tokenPath: string;
|
|
18
|
+
family: ClaustrumFamily;
|
|
19
|
+
connect: () => Promise<ClaustrumScopedClient>;
|
|
20
|
+
/**
|
|
21
|
+
* Whether the plugin currently takes its accounts from the vault (custody
|
|
22
|
+
* mode) rather than from local logins. Checked before every discovery,
|
|
23
|
+
* commit and dispatch; false closes the vault connection and refuses sends.
|
|
24
|
+
*/
|
|
25
|
+
isCustodyActive?: () => boolean | Promise<boolean>;
|
|
26
|
+
/** Ids of the plugin's local pool rows; no vault route id may equal one. */
|
|
27
|
+
reservedRouteIds?: () => Iterable<string>;
|
|
28
|
+
routePrefix?: string;
|
|
29
|
+
mapAccount?: AccountMapper;
|
|
30
|
+
parseIdentity?: IdentityParser;
|
|
31
|
+
/**
|
|
32
|
+
* Issue a receipt only when the vault's served reply itself names the
|
|
33
|
+
* credential id and the roster's account identity (see
|
|
34
|
+
* `ClaustrumScopedCustody`). Set it for providers whose tokens do not reveal
|
|
35
|
+
* their account; leave it unset to accept a reply that asserts no identity.
|
|
36
|
+
*/
|
|
37
|
+
requireAssertion?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Fired once per change of the vault's view cursor, including the first
|
|
40
|
+
* roster. A poll that sees the same view does not fire, and neither does a
|
|
41
|
+
* routine token refresh, which never moves the view.
|
|
42
|
+
*/
|
|
43
|
+
onRoster?: (roster: VaultRosterFile) => void;
|
|
44
|
+
onError?: (error: unknown) => void;
|
|
45
|
+
/**
|
|
46
|
+
* How often `start()` ticks. A tick lists the vault when no vault watch is
|
|
47
|
+
* open (before it is accepted, or when the vault or the client cannot
|
|
48
|
+
* watch), and otherwise only every five minutes, as a safety net. A tick
|
|
49
|
+
* polls insula (at most once a minute) only while its watch is not open.
|
|
50
|
+
*/
|
|
51
|
+
pollIntervalMs?: number;
|
|
52
|
+
setTimeoutImpl?: typeof setTimeout;
|
|
53
|
+
clearTimeoutImpl?: typeof clearTimeout;
|
|
54
|
+
now?: () => number;
|
|
55
|
+
logger?: ClaustrumLogger;
|
|
56
|
+
/** Optional background quota source. In-band readings still use recordQuota. */
|
|
57
|
+
insula?: InsulaQuotaOptions<Q>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* How a started consumer learns about changes, for logs and status displays.
|
|
61
|
+
* `insula` is present when the consumer was given an insula source. Never
|
|
62
|
+
* carries secrets.
|
|
63
|
+
*/
|
|
64
|
+
export interface ClaustrumWatchState {
|
|
65
|
+
readonly vault: WatchMode;
|
|
66
|
+
readonly insula?: WatchMode;
|
|
67
|
+
}
|
|
68
|
+
export interface SendOptions {
|
|
69
|
+
/**
|
|
70
|
+
* What kind of request this is (for example `model`, `quota`, `profile`),
|
|
71
|
+
* recorded in the 401 retry log line so a retry can be traced to its caller.
|
|
72
|
+
*/
|
|
73
|
+
site: string;
|
|
74
|
+
signal?: AbortSignal;
|
|
75
|
+
reporterSource?: ClaustrumReporterSource;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* One plugin host's view of its vault accounts. It turns vault credentials
|
|
79
|
+
* into rows for `/routing`, authorizes each physical send with the vault, and
|
|
80
|
+
* reports a 401 against the exact record version that send used. Metadata
|
|
81
|
+
* (the connection and discovery) is shared and coalesced; credential reads
|
|
82
|
+
* never are. It never enrolls: a missing token is an error here, and setup
|
|
83
|
+
* is where enrollment happens.
|
|
84
|
+
*/
|
|
85
|
+
export declare class ClaustrumConsumer<Q = QuotaObservation> {
|
|
86
|
+
#private;
|
|
87
|
+
constructor(options: ClaustrumConsumerOptions<Q>);
|
|
88
|
+
/** The last committed roster this instance saw, without any I/O. */
|
|
89
|
+
snapshot(): VaultRosterFile | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* What this consumer is granted, from its latest discovery, without any I/O.
|
|
92
|
+
* It explains an empty roster: `grants === 0` means no grant at all, while
|
|
93
|
+
* `grants > 0` means a grant that reaches no credential (`tuples` names its
|
|
94
|
+
* selector). Undefined means unknown, never zero: the vault or client is too
|
|
95
|
+
* old to send it, nothing was discovered yet, or the roster now in memory is
|
|
96
|
+
* a different view than the one this instance discovered (a peer process
|
|
97
|
+
* held the discovery lease and its newer roster was served instead).
|
|
98
|
+
*/
|
|
99
|
+
grantReach(): VaultGrantReach | undefined;
|
|
100
|
+
/** How this consumer currently learns about changes, without I/O. */
|
|
101
|
+
watchState(): ClaustrumWatchState;
|
|
102
|
+
/** Whether any account is using an in-memory outage receipt, without I/O. */
|
|
103
|
+
keptReceiptState(): KeptReceiptState;
|
|
104
|
+
/** Rows for `/routing`, from the last committed roster. */
|
|
105
|
+
routingRows(): RoutingRow[];
|
|
106
|
+
refresh(): Promise<VaultRosterFile | undefined>;
|
|
107
|
+
/**
|
|
108
|
+
* Poll in the background and open the vault and insula watches. Until the
|
|
109
|
+
* vault watch is accepted, and whenever it is unavailable, every tick lists
|
|
110
|
+
* the vault exactly as before watches existed. While it is open a tick
|
|
111
|
+
* lists only when five minutes have passed since the last discovery, since
|
|
112
|
+
* the watch keeps no history; it still checks `isCustodyActive`, so a switch
|
|
113
|
+
* to local mode closes the connection as quickly as before. Insula is
|
|
114
|
+
* polled on the tick only while its own watch is not open. A host not in
|
|
115
|
+
* vault mode makes no request.
|
|
116
|
+
*/
|
|
117
|
+
start(): void;
|
|
118
|
+
/**
|
|
119
|
+
* Fetch on a background tick, at most once per minute across projects with
|
|
120
|
+
* the same daemon and enrollment token path. Admission must use the cache
|
|
121
|
+
* instead, so an optional quota daemon never delays a provider request.
|
|
122
|
+
*/
|
|
123
|
+
refreshInsulaQuota(): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Cache-only, for unknown-quota admission with a served receipt. Undefined
|
|
126
|
+
* means poll the provider normally. Only get_scoped's asserted account_id
|
|
127
|
+
* joins insula; parsed identities and display metadata cannot substitute.
|
|
128
|
+
* Background polling should use cachedInsulaQuotaForRoute instead.
|
|
129
|
+
*/
|
|
130
|
+
cachedInsulaQuota(receipt: Pick<ClaustrumScopedAttempt, 'assertedAccountIdentity'>): Promise<Q | undefined>;
|
|
131
|
+
/**
|
|
132
|
+
* Background cache lookup without fetching a credential. Re-read the roster
|
|
133
|
+
* so a peer's account replacement invalidates the old route immediately.
|
|
134
|
+
* A route without a known provider account cannot join an insula entry.
|
|
135
|
+
*/
|
|
136
|
+
cachedInsulaQuotaForRoute(routeId: string): Promise<Q | undefined>;
|
|
137
|
+
/**
|
|
138
|
+
* Authorize one physical send on a vault route. The roster file is re-read
|
|
139
|
+
* first, so a decline committed by another process applies before the next
|
|
140
|
+
* poll, and a route whose credential or account changed is refused.
|
|
141
|
+
*/
|
|
142
|
+
authorize(routeId: string, signal?: AbortSignal): Promise<ClaustrumScopedAttempt>;
|
|
143
|
+
reportFailure(attempt: ClaustrumScopedAttempt, status: number, source: ClaustrumReporterSource): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Send on a vault route. `dispatch` builds and sends the request with the
|
|
146
|
+
* receipt's token and may be called twice, so it must be able to rebuild
|
|
147
|
+
* its body. A 401 is retried once, and only when the vault now serves a new
|
|
148
|
+
* record version of the same credential and account; the final 401 is
|
|
149
|
+
* reported against the version that send actually used. Each call to
|
|
150
|
+
* `dispatch` gets its own receipt.
|
|
151
|
+
*/
|
|
152
|
+
send(routeId: string, dispatch: (attempt: ClaustrumScopedAttempt, signal?: AbortSignal) => Promise<Response>, options: SendOptions): Promise<Response>;
|
|
153
|
+
/** Decline a vault account: it stays listed and never routes until accepted. */
|
|
154
|
+
decline(routeId: string): Promise<void>;
|
|
155
|
+
accept(routeId: string): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Store a quota or profile observation for a vault route. The receipt the
|
|
158
|
+
* reading was taken with is required: the observation lands only while the
|
|
159
|
+
* route still holds the credential and account that receipt was served for
|
|
160
|
+
* (see `recordVaultQuota`), so a reading for a replaced account is dropped.
|
|
161
|
+
*/
|
|
162
|
+
recordQuota(routeId: string, observation: QuotaObservation, attempt: QuotaReceipt): Promise<boolean>;
|
|
163
|
+
/**
|
|
164
|
+
* Store a background observation that belongs to an account rather than to
|
|
165
|
+
* a served credential (an insula reading from `cachedInsulaQuotaForRoute`).
|
|
166
|
+
* It lands only while the route is still bound to that known account, so no
|
|
167
|
+
* credential fetch is needed and a replaced account's reading is dropped.
|
|
168
|
+
* The in-memory roster is reloaded from the file, without a vault call.
|
|
169
|
+
*/
|
|
170
|
+
recordAccountQuota(routeId: string, accountIdentity: string, observation: QuotaObservation): Promise<boolean>;
|
|
171
|
+
close(): void;
|
|
172
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { type ClaustrumClient, type ClaustrumReporterSource, type EnrollmentTokenFile } from '@cortexkit/claustrum-client';
|
|
2
|
+
import { type ClaustrumLogger } from './errors.js';
|
|
3
|
+
import type { InsulaUsageTransport } from './insula.js';
|
|
4
|
+
import { type ScopedWatchSource, type WatchMode } from './watch.js';
|
|
5
|
+
/**
|
|
6
|
+
* `watchScoped` is present from claustrum-client 0.12.0. Hosts detect it at
|
|
7
|
+
* run time and keep polling without it.
|
|
8
|
+
*/
|
|
9
|
+
export type ClaustrumScopedClient = Pick<ClaustrumClient, 'listScoped' | 'getScoped' | 'reportAuthFailureScoped' | 'close'> & {
|
|
10
|
+
insula?: InsulaUsageTransport;
|
|
11
|
+
} & ScopedWatchSource;
|
|
12
|
+
/** The two credential types a plugin's family can hold; routing differs by type. */
|
|
13
|
+
export type VaultCredentialType = 'oauth' | 'api_key';
|
|
14
|
+
/**
|
|
15
|
+
* Which vault rows belong to this plugin. `refreshAdapter` classifies an OAuth
|
|
16
|
+
* row by the protocol it speaks; `category` is the grant that authorizes this
|
|
17
|
+
* consumer to read it. Static API keys carry no refresh adapter, so they are
|
|
18
|
+
* admitted only when `apiKeys` is set and only by category.
|
|
19
|
+
*/
|
|
20
|
+
export interface ClaustrumFamily {
|
|
21
|
+
refreshAdapter: string;
|
|
22
|
+
category: string;
|
|
23
|
+
apiKeys?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** One vault credential this consumer may serve. Never carries bearer material. */
|
|
26
|
+
export interface VaultCredential {
|
|
27
|
+
readonly credentialId: string;
|
|
28
|
+
readonly credentialType: VaultCredentialType;
|
|
29
|
+
/**
|
|
30
|
+
* The provider account the credential logs into, when the vault's adapter
|
|
31
|
+
* claims one. Absent means the adapter makes no claim, not a mismatch.
|
|
32
|
+
*/
|
|
33
|
+
readonly accountIdentity?: string;
|
|
34
|
+
readonly state: string;
|
|
35
|
+
readonly email?: string;
|
|
36
|
+
readonly orgName?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Why a listed vault record could not be used. A closed set of fixed codes, so
|
|
40
|
+
* a log line or a roster file that carries one never echoes vault data.
|
|
41
|
+
*/
|
|
42
|
+
export type SkippedVaultReason = 'empty credential id' | 'duplicate credential id' | 'blank account identity' | 'empty state';
|
|
43
|
+
/**
|
|
44
|
+
* A vault record this consumer could not use. `credentialId` is absent when
|
|
45
|
+
* the record's id itself was unusable, so nothing can say which account the
|
|
46
|
+
* record was.
|
|
47
|
+
*/
|
|
48
|
+
export interface SkippedVaultRecord {
|
|
49
|
+
readonly credentialId?: string;
|
|
50
|
+
readonly reason: SkippedVaultReason;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* One grant this consumer's enrollment holds, as the vault's list reply names
|
|
54
|
+
* it: for example `{ selectorKind: 'category', selector: 'openai-native',
|
|
55
|
+
* operation: 'read' }`. `selectorKind` is `exact` (the selector is a credential
|
|
56
|
+
* id) or `category` (a bare category name); both open strings.
|
|
57
|
+
*/
|
|
58
|
+
export interface VaultGrantTuple {
|
|
59
|
+
readonly selectorKind: string;
|
|
60
|
+
readonly selector: string;
|
|
61
|
+
readonly operation: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* What this consumer is granted, independent of what those grants currently
|
|
65
|
+
* reach. It is what tells two empty listings apart: no rows with `grants === 0`
|
|
66
|
+
* means no grant at all (the fix is `ck auth grant`), while no rows with
|
|
67
|
+
* `grants > 0` means a grant that reaches no credential, usually logins that
|
|
68
|
+
* lack the category (`ck auth reclassify --from-registry`); `tuples` names the
|
|
69
|
+
* selector that matched nothing. Never written to the roster file.
|
|
70
|
+
*/
|
|
71
|
+
export interface VaultGrantReach {
|
|
72
|
+
/** How many grants the consumer holds, deposit grants excluded. */
|
|
73
|
+
readonly grants: number;
|
|
74
|
+
readonly tuples: readonly VaultGrantTuple[];
|
|
75
|
+
}
|
|
76
|
+
export interface VaultInventory {
|
|
77
|
+
/**
|
|
78
|
+
* The vault's change cursor: a digest over exactly what this consumer can see
|
|
79
|
+
* (ids, grants, state, identity), never over record versions, so a routine
|
|
80
|
+
* token refresh does not move it. Only equality is meaningful.
|
|
81
|
+
*/
|
|
82
|
+
readonly view: string;
|
|
83
|
+
readonly credentials: readonly VaultCredential[];
|
|
84
|
+
readonly skipped: readonly SkippedVaultRecord[];
|
|
85
|
+
/**
|
|
86
|
+
* The consumer's grants, as the vault's list reply named them. Undefined means unknown
|
|
87
|
+
* (the vault or client predates the fields), never zero grants.
|
|
88
|
+
*/
|
|
89
|
+
readonly grantReach?: VaultGrantReach;
|
|
90
|
+
}
|
|
91
|
+
export interface ClaustrumScopedIdentity {
|
|
92
|
+
readonly credentialId: string;
|
|
93
|
+
readonly credentialType: VaultCredentialType;
|
|
94
|
+
readonly accountIdentity?: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Where a receipt's `accountIdentity` came from: the vault asserted it in the
|
|
98
|
+
* served reply, the plugin's `parseIdentity` read it from the served token, or
|
|
99
|
+
* neither did and it is only the roster's expectation (`none`: no identity at
|
|
100
|
+
* all).
|
|
101
|
+
*/
|
|
102
|
+
export type AccountIdentitySource = 'asserted' | 'parsed' | 'expected' | 'none';
|
|
103
|
+
/**
|
|
104
|
+
* A receipt: what the vault served for one physical send. Each attempt gets
|
|
105
|
+
* its own. It records the exact record version served, because a 401 for
|
|
106
|
+
* this send is reported to the vault against that version.
|
|
107
|
+
*/
|
|
108
|
+
export interface ClaustrumScopedAttempt {
|
|
109
|
+
readonly credentialId: string;
|
|
110
|
+
readonly credentialType: VaultCredentialType;
|
|
111
|
+
/**
|
|
112
|
+
* The account this receipt is bound to: the vault's assertion, else the
|
|
113
|
+
* plugin's parse of the token, else the roster's expectation. Check
|
|
114
|
+
* `accountIdentitySource` before treating it as proof.
|
|
115
|
+
*/
|
|
116
|
+
readonly accountIdentity?: string;
|
|
117
|
+
readonly accountIdentitySource: AccountIdentitySource;
|
|
118
|
+
/** The account the roster row named when this receipt was requested. */
|
|
119
|
+
readonly expectedAccountIdentity?: string;
|
|
120
|
+
/** The credential id the vault itself put in the served reply, if any. */
|
|
121
|
+
readonly assertedCredentialId?: string;
|
|
122
|
+
/**
|
|
123
|
+
* The account the vault itself put in the served reply, if any. Never filled
|
|
124
|
+
* in from the roster or from a token parse.
|
|
125
|
+
*/
|
|
126
|
+
readonly assertedAccountIdentity?: string;
|
|
127
|
+
/**
|
|
128
|
+
* The non-secret Google Cloud project id the vault served in get_scoped for
|
|
129
|
+
* this attempt, if any. Never filled in from list_scoped, the roster or a
|
|
130
|
+
* token parse. A project change in the vault takes effect on the next send.
|
|
131
|
+
*/
|
|
132
|
+
readonly projectId?: string;
|
|
133
|
+
/**
|
|
134
|
+
* Kept in memory only and hidden from JSON.stringify and object spreads, so
|
|
135
|
+
* logging a receipt never leaks it. Authorize again for every dispatch and retry.
|
|
136
|
+
*/
|
|
137
|
+
readonly accessToken: string;
|
|
138
|
+
readonly recordVersion: number;
|
|
139
|
+
readonly expiresAtMs: number | null;
|
|
140
|
+
/**
|
|
141
|
+
* True when this receipt was not freshly served but is the last token kept
|
|
142
|
+
* from an earlier read, used because the vault could not be reached. A 401
|
|
143
|
+
* with such a receipt must not be retried and is not reported to the vault,
|
|
144
|
+
* which is unreachable.
|
|
145
|
+
*/
|
|
146
|
+
readonly fromKeptReceipt?: true;
|
|
147
|
+
}
|
|
148
|
+
export interface KeptReceiptState {
|
|
149
|
+
readonly active: boolean;
|
|
150
|
+
readonly since: number | null;
|
|
151
|
+
}
|
|
152
|
+
export interface ClaustrumRosterWatchOptions {
|
|
153
|
+
/**
|
|
154
|
+
* Run the host's own discovery (`custody.discover()`, then its roster
|
|
155
|
+
* commit). Called with the newest notice's view when that view differs from
|
|
156
|
+
* the last discovered one, when a notice shows a sequence gap or a new
|
|
157
|
+
* epoch, and on the first notice of a watch re-opened after a close (the
|
|
158
|
+
* time between two watches was not observed). One call runs at a time;
|
|
159
|
+
* notices that arrive meanwhile earn at most one more call, which runs only
|
|
160
|
+
* if the view still differs. A rejection goes to `onError`.
|
|
161
|
+
*/
|
|
162
|
+
onView: (view: string) => unknown;
|
|
163
|
+
/**
|
|
164
|
+
* The view of the roster the host last committed. Defaults to the view of
|
|
165
|
+
* the last successful `custody.discover()` (outage probes excluded). Pass it
|
|
166
|
+
* when the host can commit a roster it did not list itself (a peer process
|
|
167
|
+
* held the discovery lease) or can fail after listing.
|
|
168
|
+
*/
|
|
169
|
+
view?: () => string | undefined;
|
|
170
|
+
/**
|
|
171
|
+
* Called whenever the mode changes. The watch starts in `polling`. Keep the
|
|
172
|
+
* host's own list timer at its usual pace unless the mode is `watching`.
|
|
173
|
+
*/
|
|
174
|
+
onMode?: (mode: WatchMode) => void;
|
|
175
|
+
/**
|
|
176
|
+
* The vault refused the watch, and its confirming `list_scoped` read it with
|
|
177
|
+
* this error (a revoked enrollment answers `not_found`). Custody has already
|
|
178
|
+
* applied it as it applies a refused discovery (`listFailed`: every kept
|
|
179
|
+
* receipt is dropped); handle it as the host handles that refusal.
|
|
180
|
+
*/
|
|
181
|
+
onRefused?: (error: unknown) => void;
|
|
182
|
+
/** A failed `onView`, or a watch that could not be opened. */
|
|
183
|
+
onError?: (error: unknown) => void;
|
|
184
|
+
}
|
|
185
|
+
/** A vault watch held by a custody; see `ClaustrumScopedCustody.watchRoster`. */
|
|
186
|
+
export interface ClaustrumRosterWatch {
|
|
187
|
+
/** How the host currently learns about changes. */
|
|
188
|
+
readonly mode: WatchMode;
|
|
189
|
+
/** Stop the watch and its timers. Safe to call more than once. */
|
|
190
|
+
close(): void;
|
|
191
|
+
}
|
|
192
|
+
/** Reads the provider identity a served token executes under, when the plugin can tell. */
|
|
193
|
+
export type IdentityParser = (accessToken: string) => string | undefined;
|
|
194
|
+
/** Only a new version of the same account may replace an in-flight 401. */
|
|
195
|
+
export declare function isScopedCredentialRotation(served: ClaustrumScopedAttempt, current: ClaustrumScopedAttempt | undefined): current is ClaustrumScopedAttempt;
|
|
196
|
+
/** Why a scoped 401 did or did not retry. Never carries credential material. */
|
|
197
|
+
export type ScopedRetryReason = 'rotated' | 'reauthorize-failed' | 'credential-changed' | 'account-changed' | 'version-unchanged';
|
|
198
|
+
/**
|
|
199
|
+
* Decide whether a request that got a 401 should retry with the freshly
|
|
200
|
+
* re-authorized receipt, and log the decision. The vault can refresh a
|
|
201
|
+
* credential between the send and the 401; the log line lets that refresh be
|
|
202
|
+
* matched against the consumer's retry. `site` names the kind of request.
|
|
203
|
+
*/
|
|
204
|
+
export declare function decideScopedRetryAfter401(site: string, served: ClaustrumScopedAttempt, current: ClaustrumScopedAttempt | undefined, logger?: ClaustrumLogger): current is ClaustrumScopedAttempt;
|
|
205
|
+
/**
|
|
206
|
+
* How long a served OAuth token must stay valid after the vault hands it out,
|
|
207
|
+
* so it cannot expire while a long request is still using it.
|
|
208
|
+
*/
|
|
209
|
+
export declare const SERVING_MARGIN_MS = 300000;
|
|
210
|
+
/** A kept receipt needs a minute of headroom, not the vault's refresh demand. */
|
|
211
|
+
export declare const KEPT_RECEIPT_MARGIN_MS = 60000;
|
|
212
|
+
/**
|
|
213
|
+
* Reads this consumer's vault credentials (list, fetch, 401 report), with the
|
|
214
|
+
* enrollment token as authorization. Used by every host of a plugin. Every
|
|
215
|
+
* physical send asks the vault first while reachable. Only an unreachable vault permits
|
|
216
|
+
* reuse of the last validated receipt, in memory and bounded by its expiry.
|
|
217
|
+
* The enrollment token is re-read per operation so revocation and disconnect
|
|
218
|
+
* still take effect without a process restart.
|
|
219
|
+
*/
|
|
220
|
+
export declare class ClaustrumScopedCustody {
|
|
221
|
+
#private;
|
|
222
|
+
constructor(options: {
|
|
223
|
+
client: ClaustrumScopedClient;
|
|
224
|
+
family: ClaustrumFamily;
|
|
225
|
+
tokenPath?: string;
|
|
226
|
+
readToken?: () => Promise<EnrollmentTokenFile>;
|
|
227
|
+
parseIdentity?: IdentityParser;
|
|
228
|
+
/**
|
|
229
|
+
* Issue a receipt only when the vault's served reply itself names the
|
|
230
|
+
* requested credential id and the roster's (known) account identity. For
|
|
231
|
+
* providers whose tokens are opaque, where nothing else can prove which
|
|
232
|
+
* account a token belongs to. Off by default: then an absent assertion is
|
|
233
|
+
* no claim, and the receipt says where its identity came from.
|
|
234
|
+
*/
|
|
235
|
+
requireAssertion?: boolean;
|
|
236
|
+
now?: () => number;
|
|
237
|
+
logger?: ClaustrumLogger;
|
|
238
|
+
setTimeoutImpl?: typeof setTimeout;
|
|
239
|
+
clearTimeoutImpl?: typeof clearTimeout;
|
|
240
|
+
});
|
|
241
|
+
/** A watch losing its transport is the same outage as an unreachable read. */
|
|
242
|
+
transportLost(error?: unknown): void;
|
|
243
|
+
/** Any vault answer ends transport fallback, even when it refuses access. */
|
|
244
|
+
reachable(): void;
|
|
245
|
+
/**
|
|
246
|
+
* List this consumer's credentials. A record that cannot be used is skipped
|
|
247
|
+
* and warned about rather than failing the whole list, so one bad record
|
|
248
|
+
* never hides every other account. Records outside the family are not
|
|
249
|
+
* skipped records: they simply are not this consumer's.
|
|
250
|
+
*/
|
|
251
|
+
discover(signal?: AbortSignal): Promise<VaultInventory>;
|
|
252
|
+
/**
|
|
253
|
+
* Apply a failed account listing: any answer from the vault (a refusal such
|
|
254
|
+
* as a revoked enrollment's `not_found`) drops every kept receipt, while an
|
|
255
|
+
* unreachable vault keeps them. A vault watch that ends with the same
|
|
256
|
+
* refusal calls this too, so both paths treat it alike.
|
|
257
|
+
*/
|
|
258
|
+
listFailed(error: unknown): void;
|
|
259
|
+
/** Discard bearer material when a route is removed, declined or rebound. */
|
|
260
|
+
forget(credentialId: string): void;
|
|
261
|
+
/** Apply an already-held roster; this never fetches credentials or writes it. */
|
|
262
|
+
retain(identities: readonly (ClaustrumScopedIdentity & {
|
|
263
|
+
enabled: boolean;
|
|
264
|
+
state: string;
|
|
265
|
+
})[]): void;
|
|
266
|
+
/** A frozen, non-secret summary for a plugin's status display. */
|
|
267
|
+
keptReceiptState(): KeptReceiptState;
|
|
268
|
+
/** Ask the vault while reachable; retain one validated receipt in memory only. */
|
|
269
|
+
authorize(identity: ClaustrumScopedIdentity, signal?: AbortSignal, routeId?: string): Promise<ClaustrumScopedAttempt>;
|
|
270
|
+
/**
|
|
271
|
+
* Report that a send this consumer actually made was rejected. Only a 401
|
|
272
|
+
* is reported (the vault refuses 429, 402 and 5xx reports), addressed by
|
|
273
|
+
* credential id with the exact record version that send was served, and
|
|
274
|
+
* signed with the enrollment token that authorized it. A receipt this
|
|
275
|
+
* custody did not issue is refused rather than reported.
|
|
276
|
+
*/
|
|
277
|
+
reportFailure(attempt: ClaustrumScopedAttempt, status: number, reporterSource: ClaustrumReporterSource): Promise<void>;
|
|
278
|
+
/**
|
|
279
|
+
* Watch this consumer's visible vault accounts instead of listing them on a
|
|
280
|
+
* short timer. Opens one `watchScoped` on this custody's client and calls
|
|
281
|
+
* `onView` when the host must run its discovery (see
|
|
282
|
+
* `ClaustrumRosterWatchOptions`). A claustrum-client without `watchScoped`
|
|
283
|
+
* (before 0.12.0) or a vault that answers `unknown_method` reports
|
|
284
|
+
* `fallback` and opens nothing. A watch that closes before any notice was
|
|
285
|
+
* never accepted: the client reads once to learn why, and the watch reports
|
|
286
|
+
* `fallback` and tries one new watch after `WATCH_RETRY_MS`, never in a
|
|
287
|
+
* loop. A watch that closes after notices is re-opened after
|
|
288
|
+
* `RESUBSCRIBE_BACKOFF_MS`, doubling per close in a row. A lost transport
|
|
289
|
+
* starts this custody's outage, exactly as an unreachable call does, and
|
|
290
|
+
* the first notice of a watch ends it. Timers are unreferenced; `close()`
|
|
291
|
+
* on the returned watch or on this custody stops it.
|
|
292
|
+
*
|
|
293
|
+
* The watch keeps no history, so the host still lists on a slow timer while
|
|
294
|
+
* the mode is `watching`: every `SAFETY_POLL_MS` (five minutes). In any
|
|
295
|
+
* other mode the host keeps its usual list timer.
|
|
296
|
+
*/
|
|
297
|
+
watchRoster(options: ClaustrumRosterWatchOptions): ClaustrumRosterWatch;
|
|
298
|
+
close(): void;
|
|
299
|
+
}
|