@cortexkit/claustrum-client 0.4.0 → 0.6.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/README.md +5 -0
- package/dist/detect.d.ts +1 -0
- package/dist/detect.js +21 -26
- package/dist/manifest-lock.js +64 -5
- package/dist/wire.d.ts +54 -8
- package/dist/wire.js +38 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,11 @@ credential caching, refresh, account scheduling, and retry policy.
|
|
|
16
16
|
by design (the vault cannot enumerate which). Treat it as gone either way: re-run
|
|
17
17
|
`ck auth migrate-opencode` to mint a fresh handle; the prior record, if any, is kept.
|
|
18
18
|
• `auth_required` means the record is latched and needs reauthentication.
|
|
19
|
+
• `statusCredential` on an unknown or revoked handle RESOLVES, it does not throw: it
|
|
20
|
+
returns `ready: false, lastErrorCode: 'not_found'` with `recordVersion`, `credentialId`
|
|
21
|
+
and `stalePending` absent. Since 0.5.0 `CredentialStatus.recordVersion` is optional for
|
|
22
|
+
that reason; handle `undefined` rather than defaulting it to a number, which would read
|
|
23
|
+
as a version older than every real one.
|
|
19
24
|
• Unknown server error classes are bounded to `transient`; callers must retry them
|
|
20
25
|
rather than treating a forward-compatible class as permanent.
|
|
21
26
|
|
package/dist/detect.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type ClaustrumDetection = {
|
|
|
13
13
|
path: string;
|
|
14
14
|
reason: string;
|
|
15
15
|
};
|
|
16
|
+
export declare function userConnectionToken(getuid?: (() => number) | null, env?: NodeJS.ProcessEnv): string;
|
|
16
17
|
export declare function getDefaultClaustrumConnectionPath(): string;
|
|
17
18
|
export declare function resolveClaustrumConnectionPath(explicit?: string): string;
|
|
18
19
|
export declare function detectClaustrumConnection(explicitPath?: string): Promise<ClaustrumDetection>;
|
package/dist/detect.js
CHANGED
|
@@ -6,36 +6,15 @@ import { PROTOCOL_VERSION, readConnectionFile, } from '@cortexkit/subc-client';
|
|
|
6
6
|
const PRODUCTION_FILE_NAME = 'subc-connection.json';
|
|
7
7
|
const TEMP_PREFIX = 'subc-';
|
|
8
8
|
const TEMP_SUFFIX = '.connection.json';
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// crates/credentials-module/src/bin/credentials_cli.rs :: discover_subc_connection_file
|
|
14
|
-
// -> XDG_RUNTIME_DIR/subc-connection.json, else
|
|
15
|
-
// ~/.local/share/cortexkit/run/subc-connection.json, else
|
|
16
|
-
// <tempdir>/subc-<token>.connection.json (glob; ambiguity REFUSES)
|
|
17
|
-
//
|
|
18
|
-
// crates/subc-core/src/bootstrap.rs :: connection_file_path_with_source
|
|
19
|
-
// -> XDG_RUNTIME_DIR/subc-connection.json, else
|
|
20
|
-
// <tempdir>/subc-<user_connection_token()>.connection.json
|
|
21
|
-
// where user_connection_token() is the UID on unix (via a probe-file UID read),
|
|
22
|
-
// else a sanitized USER/USERNAME/HOME/USERPROFILE value, else "unknown".
|
|
23
|
-
//
|
|
24
|
-
// The OLD client derived `${uid}` itself via `process.getuid()`, but the daemon may
|
|
25
|
-
// produce a different token (sanitized user on macOS, the literal "unknown" if all of
|
|
26
|
-
// the lookups miss). The glob is the only way to mirror the daemon without re-deriving
|
|
27
|
-
// the token (which is filesystem-side-effecting per the Rust side comment).
|
|
28
|
-
// The home tier resolves from `$HOME` byte-for-byte, mirroring `non_empty_env("HOME")`
|
|
29
|
-
// in `discover_subc_connection_file`: only the empty string is absent, and whitespace
|
|
30
|
-
// is not trimmed. The Rust code does NOT fall back to a `getpwuid`-style lookup; the
|
|
31
|
-
// client must not either, or an operator who points HOME at a per-test fixture loses
|
|
32
|
-
// the daemon they started there.
|
|
9
|
+
// Keep discovery in the CLI's order: exclusive SUBC_CONNECTION_FILE, runtime,
|
|
10
|
+
// HOME, exact per-user temp name, then an unambiguous temp glob. The glob supports
|
|
11
|
+
// daemons whose token derivation differs without guessing between other users.
|
|
33
12
|
function homeTierPath() {
|
|
34
13
|
const homeEnv = process.env.HOME;
|
|
35
14
|
return homeEnv ? join(homeEnv, '.local', 'share', 'cortexkit', 'run', PRODUCTION_FILE_NAME) : undefined;
|
|
36
15
|
}
|
|
37
16
|
function highestPriorityAbsentMarker() {
|
|
38
|
-
const runtime = process.env.XDG_RUNTIME_DIR
|
|
17
|
+
const runtime = process.env.XDG_RUNTIME_DIR;
|
|
39
18
|
if (runtime)
|
|
40
19
|
return join(runtime, PRODUCTION_FILE_NAME);
|
|
41
20
|
const home = homeTierPath();
|
|
@@ -47,7 +26,9 @@ function highestPriorityAbsentMarker() {
|
|
|
47
26
|
return join(userInfo().homedir, '.local', 'share', 'cortexkit', 'run', PRODUCTION_FILE_NAME);
|
|
48
27
|
}
|
|
49
28
|
function findExistingConnectionPath() {
|
|
50
|
-
|
|
29
|
+
if (process.env.SUBC_CONNECTION_FILE)
|
|
30
|
+
return process.env.SUBC_CONNECTION_FILE;
|
|
31
|
+
const runtime = process.env.XDG_RUNTIME_DIR;
|
|
51
32
|
if (runtime) {
|
|
52
33
|
const p = join(runtime, PRODUCTION_FILE_NAME);
|
|
53
34
|
if (safeIsFile(p))
|
|
@@ -56,17 +37,31 @@ function findExistingConnectionPath() {
|
|
|
56
37
|
const home = homeTierPath();
|
|
57
38
|
if (home && safeIsFile(home))
|
|
58
39
|
return home;
|
|
40
|
+
const exact = join(tmpdir(), `${TEMP_PREFIX}${userConnectionToken()}${TEMP_SUFFIX}`);
|
|
41
|
+
if (safeIsFile(exact))
|
|
42
|
+
return exact;
|
|
59
43
|
const matches = listSubcConnectionFiles(tmpdir());
|
|
60
44
|
// A single matching file IS the daemon; multiple matches mean different OS users
|
|
61
45
|
// happened to share the temp dir. Picking one would route credential-bearing
|
|
62
46
|
// requests at another user's daemon, so REFUSE both picks and the absent path.
|
|
63
47
|
return matches.length === 1 ? matches[0] : undefined;
|
|
64
48
|
}
|
|
49
|
+
export function userConnectionToken(getuid = process.getuid ?? null, env = process.env) {
|
|
50
|
+
if (getuid)
|
|
51
|
+
return String(getuid());
|
|
52
|
+
for (const key of ['USER', 'USERNAME', 'HOME', 'USERPROFILE']) {
|
|
53
|
+
const value = env[key];
|
|
54
|
+
if (value)
|
|
55
|
+
return Array.from(value, (character) => /^[a-zA-Z0-9_-]$/.test(character) ? character : '_').join('');
|
|
56
|
+
}
|
|
57
|
+
return 'unknown';
|
|
58
|
+
}
|
|
65
59
|
export function getDefaultClaustrumConnectionPath() {
|
|
66
60
|
return findExistingConnectionPath() ?? highestPriorityAbsentMarker();
|
|
67
61
|
}
|
|
68
62
|
export function resolveClaustrumConnectionPath(explicit) {
|
|
69
63
|
return (explicit?.trim() ||
|
|
64
|
+
process.env.SUBC_CONNECTION_FILE ||
|
|
70
65
|
process.env.CLAUSTRUM_SUBC_CONNECTION?.trim() ||
|
|
71
66
|
getDefaultClaustrumConnectionPath());
|
|
72
67
|
}
|
package/dist/manifest-lock.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { constants as fsConstants } from 'node:fs';
|
|
2
|
-
import { chmod, lstat, mkdir, open, readFile, readdir, rename, rm, stat, unlink } from 'node:fs/promises';
|
|
2
|
+
import { chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, unlink } from 'node:fs/promises';
|
|
3
3
|
import { randomBytes, randomInt } from 'node:crypto';
|
|
4
4
|
import { basename as pathBasename, dirname, join } from 'node:path';
|
|
5
5
|
import { HANDLE_FILE_CONTRACT, parseHandleFile } from './handles.js';
|
|
@@ -128,6 +128,28 @@ async function withLockCommit(path, tenant, fn) {
|
|
|
128
128
|
if (code(error) !== 'ENOENT' && Date.now() >= deadline)
|
|
129
129
|
throw code(error) === 'owner_invalid' ? error : lockError('lock_busy', 'manifest lock busy');
|
|
130
130
|
}
|
|
131
|
+
if (!observed && code(ownerReadError) === 'ENOENT') {
|
|
132
|
+
const metadata = await lstat(lock).catch(() => undefined);
|
|
133
|
+
if (metadata?.isDirectory() && Date.now() - metadata.mtimeMs >= ttl) {
|
|
134
|
+
await testOptions?.beforeEvict?.();
|
|
135
|
+
const stale = `${lock}.stale-${Math.floor(metadata.mtimeMs)}-${token()}`;
|
|
136
|
+
try {
|
|
137
|
+
await rename(lock, stale);
|
|
138
|
+
// A claimant may have published its owner while the eviction was pending.
|
|
139
|
+
const moved = await lstat(stale);
|
|
140
|
+
const ownerExists = await lstat(join(stale, 'owner')).then(() => true, (error) => code(error) !== 'ENOENT');
|
|
141
|
+
if (!ownerExists && moved.mtimeMs === metadata.mtimeMs) {
|
|
142
|
+
testOptions?.afterEvict?.();
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
await rename(stale, lock).catch(() => { });
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (!['ENOENT', 'EEXIST', 'ENOTEMPTY'].includes(code(error) ?? ''))
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
131
153
|
if (observed && Date.now() - observed.claimed_at_ms >= ttl) {
|
|
132
154
|
await testOptions?.beforeEvict?.();
|
|
133
155
|
// The owner record remains because the quarantine is the ABA guard, not an audit log;
|
|
@@ -226,10 +248,47 @@ async function readManifest(path) {
|
|
|
226
248
|
return parseHandleFile(JSON.parse(source.toString('utf8')));
|
|
227
249
|
}
|
|
228
250
|
const foreign = (file, tenant) => file.providers.filter((provider) => provider.serve !== tenant).map((provider) => JSON.stringify(provider));
|
|
229
|
-
async function
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
async function refuseWritableAncestors(parent) {
|
|
252
|
+
let component;
|
|
253
|
+
try {
|
|
254
|
+
component = await realpath(parent);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (code(error) !== 'ENOENT')
|
|
258
|
+
throw error;
|
|
259
|
+
const next = dirname(parent);
|
|
260
|
+
if (next !== parent)
|
|
261
|
+
await refuseWritableAncestors(next);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
for (;;) {
|
|
265
|
+
const metadata = await stat(component);
|
|
266
|
+
if ((metadata.mode & 0o022) !== 0 && (metadata.mode & 0o1000) === 0)
|
|
267
|
+
throw new Error('handle file ancestor is group- or world-writable without sticky bit');
|
|
268
|
+
const next = dirname(component);
|
|
269
|
+
if (next === component)
|
|
270
|
+
return;
|
|
271
|
+
component = next;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function prepareParent(path) {
|
|
275
|
+
const parent = dirname(path);
|
|
276
|
+
// Check existing ancestors before creating directories under a replaceable tree.
|
|
277
|
+
if (process.platform !== 'win32')
|
|
278
|
+
await refuseWritableAncestors(parent);
|
|
279
|
+
const existing = await stat(parent).catch((error) => { if (code(error) !== 'ENOENT')
|
|
280
|
+
throw error; return undefined; });
|
|
281
|
+
if (existing && typeof process.getuid === 'function' && existing.uid !== process.getuid())
|
|
282
|
+
throw new Error('handle file parent is not owned by the current uid');
|
|
283
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
284
|
+
const metadata = await stat(parent);
|
|
285
|
+
if (!metadata.isDirectory())
|
|
286
|
+
throw new Error('handle file parent must be a directory');
|
|
287
|
+
if (typeof process.getuid === 'function' && metadata.uid !== process.getuid())
|
|
288
|
+
throw new Error('handle file parent is not owned by the current uid');
|
|
289
|
+
if (process.platform !== 'win32')
|
|
290
|
+
await refuseWritableAncestors(parent);
|
|
291
|
+
}
|
|
233
292
|
async function writeAtomic(path, file, commit) {
|
|
234
293
|
const bytes = Buffer.from(JSON.stringify(file));
|
|
235
294
|
if (bytes.byteLength > HANDLE_FILE_CONTRACT.maxBytes)
|
package/dist/wire.d.ts
CHANGED
|
@@ -15,6 +15,14 @@ export type ClaustrumClientOptions = {
|
|
|
15
15
|
/**
|
|
16
16
|
* Credential material plus optional non-secret identity metadata. Missing wire fields remain
|
|
17
17
|
* `undefined`; a present non-string identity value rejects the response as invalid.
|
|
18
|
+
*
|
|
19
|
+
* An absent identity field means the vault had nothing to assert — NOT that identity failed.
|
|
20
|
+
* Consumers must not collapse absence into mismatch: assert on presence, refuse on a present
|
|
21
|
+
* difference, and serve (log, if it matters) when the field is missing. Absence proves neither
|
|
22
|
+
* the right identity nor the wrong one, so treating it as a failure converts a vault that could
|
|
23
|
+
* not answer into a consumer that cannot serve. The vault records no per-read success (at most
|
|
24
|
+
* the first scoped read per principal), and nothing at all when a consumer declines to serve, so
|
|
25
|
+
* an outage caused that way is visible only to the consumer that caused it.
|
|
18
26
|
*/
|
|
19
27
|
export type ServedCredential = {
|
|
20
28
|
material: string;
|
|
@@ -30,6 +38,12 @@ export type ServedCredential = {
|
|
|
30
38
|
/**
|
|
31
39
|
* Provider account identity the served token executes under. Account-scoped routing joins this
|
|
32
40
|
* value with `recordVersion`; it is neither the operator's credential label nor the bearer handle.
|
|
41
|
+
*
|
|
42
|
+
* BEST-EFFORT, NEVER GUARANTEED, including for JWT providers. The vault parses the claim live
|
|
43
|
+
* from the served access token and falls back to stored login-time identity; both legs can be
|
|
44
|
+
* empty, and the serve path yields nothing rather than fabricating an id. It is absent when the
|
|
45
|
+
* provider has no known claim, the token does not carry one, or no identity was captured at
|
|
46
|
+
* login. Do not gate serving on its presence.
|
|
33
47
|
*/
|
|
34
48
|
accountId?: string;
|
|
35
49
|
/** Non-secret account display metadata captured at login. */
|
|
@@ -37,14 +51,43 @@ export type ServedCredential = {
|
|
|
37
51
|
/** Non-secret organization or workspace display metadata captured at login. */
|
|
38
52
|
orgName?: string;
|
|
39
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Non-secret health for one handle, read without minting anything.
|
|
56
|
+
*
|
|
57
|
+
* An unresolved handle (unknown or revoked) is an ordinary answer, not an error: it decodes
|
|
58
|
+
* as `ready: false, lastErrorCode: 'not_found'` with `credentialId`, `recordVersion` and
|
|
59
|
+
* `stalePending` all absent. The vault omits them on purpose so a probe learns nothing about
|
|
60
|
+
* what exists and no placeholder value is asserted about a record it never saw.
|
|
61
|
+
*/
|
|
40
62
|
export type CredentialStatus = {
|
|
41
63
|
ready: boolean;
|
|
42
64
|
lastErrorCode: string | null;
|
|
43
65
|
leaseHeld: boolean;
|
|
44
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Operator-chosen record label the handle resolved to, for verifying a binding the caller
|
|
68
|
+
* already holds. Not a routing key. Absent when the handle did not resolve.
|
|
69
|
+
*/
|
|
70
|
+
credentialId?: string;
|
|
71
|
+
/**
|
|
72
|
+
* The record's change cursor, present only when the handle resolved to a record.
|
|
73
|
+
*
|
|
74
|
+
* Optional, and OMITTED from the object rather than defaulted, because there is no honest
|
|
75
|
+
* default: 0 or -1 would compare as older than every real version, so a poller would read a
|
|
76
|
+
* revoked handle as a change it had yet to see, forever. With the key absent, TypeScript
|
|
77
|
+
* refuses arithmetic or ordering on it until the caller has handled `undefined`, and
|
|
78
|
+
* `'recordVersion' in status` is false. Never replace absence with a number (`?? 0`).
|
|
79
|
+
*/
|
|
80
|
+
recordVersion?: number;
|
|
45
81
|
stalePending?: boolean;
|
|
46
82
|
};
|
|
47
83
|
export type ClaustrumReporterSource = 'direct' | 'relay_status_field' | 'relay_message_parse';
|
|
84
|
+
/**
|
|
85
|
+
* Exported for the golden-reply test only; `index.ts` re-exports by name, so this does not
|
|
86
|
+
* widen the published surface.
|
|
87
|
+
*/
|
|
88
|
+
export declare function decodeCredential(response: unknown, logUnknownClass: (errorClass: string) => void): ServedCredential;
|
|
89
|
+
/** Exported for the golden-reply test only, like `decodeCredential`. */
|
|
90
|
+
export declare function decodeStatus(response: unknown, logUnknownClass: (errorClass: string) => void): CredentialStatus;
|
|
48
91
|
export declare class ClaustrumClient {
|
|
49
92
|
#private;
|
|
50
93
|
private constructor();
|
|
@@ -143,26 +186,29 @@ export declare class ClaustrumClient {
|
|
|
143
186
|
/**
|
|
144
187
|
* A row from the caller's own grant-covered inventory.
|
|
145
188
|
*
|
|
146
|
-
*
|
|
189
|
+
* Selection fields answer different questions. Pick by what you are about
|
|
147
190
|
* to do with the token:
|
|
148
191
|
*
|
|
192
|
+
* - `providerIds` — operator-assigned catalog provider ids; match exactly, with no fallback.
|
|
193
|
+
* - `authMethod` — the closed authentication method derived from the unsealed record,
|
|
194
|
+
* not the id spelling. Absent on sign/open-only rows or when no method is defined.
|
|
195
|
+
*
|
|
149
196
|
* - `refreshAdapter` — whose PROTOCOL the credential speaks. Use it when the token
|
|
150
197
|
* goes to a provider's own endpoints. Anything else is invalid there.
|
|
151
198
|
* - `serves` — which model VENDORS are reachable through it. Use it when choosing
|
|
152
199
|
* where a model can be served from, and the protocol is not yours to care about.
|
|
153
200
|
*
|
|
154
|
-
* Never
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* An earlier version of this comment called `serves` "the routing axis" full stop. That
|
|
158
|
-
* was written before `refreshAdapter` existed and is wrong for the protocol-native case:
|
|
159
|
-
* it would send a Claude OAuth request to `apikey:openrouter`.
|
|
201
|
+
* Never infer these fields from the id spelling. A credential reaching Anthropic models
|
|
202
|
+
* through OpenRouter still speaks OpenRouter's protocol, not Anthropic's.
|
|
160
203
|
*/
|
|
161
204
|
export interface ScopedInventoryRow {
|
|
162
205
|
readonly id: string;
|
|
163
206
|
readonly categories: readonly string[];
|
|
164
207
|
readonly credentialType: string;
|
|
165
208
|
readonly serves: readonly string[];
|
|
209
|
+
/** Operator-assigned catalog ids, always present, including an empty set. */
|
|
210
|
+
readonly providerIds: readonly string[];
|
|
211
|
+
readonly authMethod?: 'apikey' | 'chatgpt' | 'antigravity' | 'oauth';
|
|
166
212
|
/**
|
|
167
213
|
* Which provider protocol this credential speaks. Absent for static keys.
|
|
168
214
|
*
|
package/dist/wire.js
CHANGED
|
@@ -15,7 +15,11 @@ function asRecordVersion(value) {
|
|
|
15
15
|
function isOptionalString(value) {
|
|
16
16
|
return value === undefined || typeof value === 'string';
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Exported for the golden-reply test only; `index.ts` re-exports by name, so this does not
|
|
20
|
+
* widen the published surface.
|
|
21
|
+
*/
|
|
22
|
+
export function decodeCredential(response, logUnknownClass) {
|
|
19
23
|
if (hasCredentialError(response))
|
|
20
24
|
throw asCredentialError(response, 'invalid_response', logUnknownClass);
|
|
21
25
|
const result = isRecord(response) && isRecord(response.result) ? response.result : undefined;
|
|
@@ -50,8 +54,15 @@ function decodeCredential(response, logUnknownClass) {
|
|
|
50
54
|
!isOptionalString(orgName)) {
|
|
51
55
|
throw asCredentialError(response, 'invalid_response', logUnknownClass);
|
|
52
56
|
}
|
|
57
|
+
let material;
|
|
58
|
+
try {
|
|
59
|
+
material = new TextDecoder('utf-8', { fatal: true }).decode(Uint8Array.from(payload));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw asCredentialError(response, 'invalid_response', logUnknownClass);
|
|
63
|
+
}
|
|
53
64
|
return {
|
|
54
|
-
material
|
|
65
|
+
material,
|
|
55
66
|
recordVersion,
|
|
56
67
|
expiresAtMs,
|
|
57
68
|
credentialId,
|
|
@@ -61,15 +72,22 @@ function decodeCredential(response, logUnknownClass) {
|
|
|
61
72
|
orgName,
|
|
62
73
|
};
|
|
63
74
|
}
|
|
64
|
-
|
|
75
|
+
/** Exported for the golden-reply test only, like `decodeCredential`. */
|
|
76
|
+
export function decodeStatus(response, logUnknownClass) {
|
|
65
77
|
if (hasCredentialError(response))
|
|
66
78
|
throw asCredentialError(response, 'invalid_response', logUnknownClass);
|
|
67
79
|
const result = isRecord(response) && isRecord(response.result) ? response.result : undefined;
|
|
68
|
-
|
|
80
|
+
// Absent is a valid reply (the handle did not resolve); a PRESENT value that is not a
|
|
81
|
+
// non-negative integer is malformed. The producer omits the key rather than nulling it,
|
|
82
|
+
// so an explicit null is refused too.
|
|
83
|
+
const rawRecordVersion = result?.record_version;
|
|
84
|
+
const recordVersion = rawRecordVersion === undefined ? undefined : asRecordVersion(rawRecordVersion);
|
|
85
|
+
const credentialId = result?.credential_id;
|
|
69
86
|
if (typeof result?.ready !== 'boolean' ||
|
|
70
87
|
(result?.last_error_code !== undefined && result?.last_error_code !== null && typeof result?.last_error_code !== 'string') ||
|
|
71
88
|
typeof result?.lease_held !== 'boolean' ||
|
|
72
|
-
recordVersion === undefined ||
|
|
89
|
+
(rawRecordVersion !== undefined && recordVersion === undefined) ||
|
|
90
|
+
!isOptionalString(credentialId) ||
|
|
73
91
|
(result?.stale_pending !== undefined && typeof result.stale_pending !== 'boolean')) {
|
|
74
92
|
throw asCredentialError(response, 'invalid_status', logUnknownClass);
|
|
75
93
|
}
|
|
@@ -77,7 +95,8 @@ function decodeStatus(response, logUnknownClass) {
|
|
|
77
95
|
ready: result.ready,
|
|
78
96
|
lastErrorCode: result.last_error_code ?? null,
|
|
79
97
|
leaseHeld: result.lease_held,
|
|
80
|
-
|
|
98
|
+
...(credentialId === undefined ? {} : { credentialId }),
|
|
99
|
+
...(recordVersion === undefined ? {} : { recordVersion }),
|
|
81
100
|
...(result.stale_pending === undefined ? {} : { stalePending: result.stale_pending }),
|
|
82
101
|
};
|
|
83
102
|
}
|
|
@@ -238,7 +257,7 @@ export class ClaustrumClient {
|
|
|
238
257
|
const result = decodeEnrollmentResult(response, this.#logger);
|
|
239
258
|
const token = result.token;
|
|
240
259
|
const generation = result.token_generation;
|
|
241
|
-
if (typeof token !== 'string' || token.length === 0 || typeof generation !== 'number') {
|
|
260
|
+
if (typeof token !== 'string' || token.length === 0 || typeof generation !== 'number' || !Number.isInteger(generation) || generation < 0) {
|
|
242
261
|
throw asCredentialError(response, 'invalid_response', this.#logger);
|
|
243
262
|
}
|
|
244
263
|
return { token, tokenGeneration: generation };
|
|
@@ -248,8 +267,9 @@ export class ClaustrumClient {
|
|
|
248
267
|
this.#client.close();
|
|
249
268
|
}
|
|
250
269
|
async #call(method, params) {
|
|
270
|
+
const sentClient = this.#client;
|
|
251
271
|
try {
|
|
252
|
-
return await
|
|
272
|
+
return await sentClient.call(CLAUSTRUM_MODULE_ID, method, params, {
|
|
253
273
|
identity: this.#identity,
|
|
254
274
|
consumerIdentity: null,
|
|
255
275
|
});
|
|
@@ -257,7 +277,8 @@ export class ClaustrumClient {
|
|
|
257
277
|
catch (error) {
|
|
258
278
|
if (this.#shouldReconnect(error)) {
|
|
259
279
|
try {
|
|
260
|
-
|
|
280
|
+
if (this.#client === sentClient)
|
|
281
|
+
await this.#reconnect();
|
|
261
282
|
}
|
|
262
283
|
catch (reconnectError) {
|
|
263
284
|
throw this.#asTransportError(reconnectError);
|
|
@@ -380,6 +401,12 @@ export function decodeScopedInventory(response, logUnknownClass) {
|
|
|
380
401
|
: undefined;
|
|
381
402
|
if (createdAtMs === undefined)
|
|
382
403
|
throw asCredentialError(response, 'invalid_response', logUnknownClass);
|
|
404
|
+
const authMethod = row.auth_method;
|
|
405
|
+
if (authMethod !== undefined &&
|
|
406
|
+
authMethod !== 'apikey' && authMethod !== 'chatgpt' &&
|
|
407
|
+
authMethod !== 'antigravity' && authMethod !== 'oauth') {
|
|
408
|
+
throw asCredentialError(response, 'invalid_response', logUnknownClass);
|
|
409
|
+
}
|
|
383
410
|
if (!isOptionalString(row.refresh_adapter)) {
|
|
384
411
|
throw asCredentialError(response, 'invalid_response', logUnknownClass);
|
|
385
412
|
}
|
|
@@ -391,6 +418,8 @@ export function decodeScopedInventory(response, logUnknownClass) {
|
|
|
391
418
|
categories: strings(row.categories),
|
|
392
419
|
credentialType: row.type,
|
|
393
420
|
serves: strings(row.serves),
|
|
421
|
+
providerIds: strings(row.provider_ids),
|
|
422
|
+
authMethod,
|
|
394
423
|
state: row.state,
|
|
395
424
|
refreshAdapter: row.refresh_adapter,
|
|
396
425
|
recordVersion,
|