@cortexkit/claustrum-client 0.2.0 → 0.4.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/index.d.ts CHANGED
@@ -3,5 +3,5 @@ export { storeIdentity, storageFingerprint } from './identity.js';
3
3
  export { writeEnrollmentTokenFile, type EnrollmentTokenFile, } from './enrollment.js';
4
4
  export { MANIFEST_LOCK, manifestLockQuarantinePrefix, withManifestLock, writeHandleFileLocked, type ManifestHandleAccount, type ManifestHandleFile, type ManifestHandleProvider, type ManifestLockError, type ManifestLockErrorCode, } from './manifest-lock.js';
5
5
  export { ClaustrumCredentialError, credentialErrorAction, ERROR_CLASS_WIRE_SET, type ClaustrumCredentialErrorAction, type ClaustrumCredentialErrorClass, } from './errors.js';
6
- export { ClaustrumClient, type ClaustrumClientOptions, type ClaustrumConnector, type ClaustrumReporterSource, type CredentialStatus, type EnrollmentPollOutcome, type ScopedInventoryRow, type ServedCredential, } from './wire.js';
6
+ export { ClaustrumClient, type ClaustrumClientOptions, type ClaustrumConnector, type ClaustrumReporterSource, type CredentialStatus, type EnrollmentPollOutcome, type ScopedInventory, type ScopedInventoryRow, type ServedCredential, } from './wire.js';
7
7
  export { HANDLE_FILE_CONTRACT, HandleFileValidationError, credentialIdMatchesProvider, defaultHandleFilePath, handleFileRevision, identifierIsValid, parseHandleFile, readHandleFile, type HandleAccount, type HandleFileIo, type HandleProvider, type OpenCodeHandleFileV1, } from './handles.js';
package/dist/wire.d.ts CHANGED
@@ -63,7 +63,7 @@ export declare class ClaustrumClient {
63
63
  * `params` is `{}` and must be PRESENT: the vault rejects an absent or null params
64
64
  * object, so a caller cannot ask a different question by omitting it.
65
65
  */
66
- listScoped(enrollmentToken?: string): Promise<readonly ScopedInventoryRow[]>;
66
+ listScoped(enrollmentToken?: string): Promise<ScopedInventory>;
67
67
  /**
68
68
  * Fetch by credential id, authorized by a grant rather than a bearer handle.
69
69
  *
@@ -121,9 +121,19 @@ export declare class ClaustrumClient {
121
121
  requestId: string;
122
122
  requestSecret: string;
123
123
  }): Promise<EnrollmentPollOutcome>;
124
- /** Exchange a live token for its successor. The old token dies when the new one is issued. */
124
+ /**
125
+ * Exchange a live token for its successor. The old token dies when the new one is issued.
126
+ *
127
+ * `expectedTokenGeneration` IS REQUIRED AND IS A FENCE, not bookkeeping: it must be the
128
+ * generation this caller currently holds. Two rotations racing — a retry after an
129
+ * uncertain reply, or two instances of the same consumer — would otherwise each mint a
130
+ * successor and each invalidate the other's, leaving a consumer holding a token the
131
+ * vault has already replaced. The vault refuses a rotation whose expectation does not
132
+ * match, so the loser learns it lost instead of discovering it on a later spend.
133
+ */
125
134
  enrollRotate(input: {
126
135
  token: string;
136
+ expectedTokenGeneration: number;
127
137
  }): Promise<{
128
138
  token: string;
129
139
  tokenGeneration: number;
@@ -133,11 +143,20 @@ export declare class ClaustrumClient {
133
143
  /**
134
144
  * A row from the caller's own grant-covered inventory.
135
145
  *
136
- * `serves` is the ROUTING AXIS, not the id spelling. `apikey:openrouter` and
137
- * `antigravity:google` both serve Anthropic models and contain no "anthropic"
138
- * anywhere in their ids, so a consumer that filters on the id segment silently
139
- * drops working accounts. Measured on a live vault: 8 Anthropic-capable rows, 3 of
140
- * them invisible to an id-substring filter.
146
+ * TWO SELECTION AXES, AND THEY ANSWER DIFFERENT QUESTIONS. Pick by what you are about
147
+ * to do with the token:
148
+ *
149
+ * - `refreshAdapter` — whose PROTOCOL the credential speaks. Use it when the token
150
+ * goes to a provider's own endpoints. Anything else is invalid there.
151
+ * - `serves` — which model VENDORS are reachable through it. Use it when choosing
152
+ * where a model can be served from, and the protocol is not yours to care about.
153
+ *
154
+ * Never the id spelling, in either case: on a live vault 8 rows serve Anthropic models
155
+ * and 3 of them contain no "anthropic" anywhere in their id.
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`.
141
160
  */
142
161
  export interface ScopedInventoryRow {
143
162
  readonly id: string;
@@ -167,6 +186,24 @@ export interface ScopedInventoryRow {
167
186
  readonly email?: string;
168
187
  readonly orgName?: string;
169
188
  }
189
+ /**
190
+ * An inventory reply: the covered rows, plus the cursor that says whether they moved.
191
+ *
192
+ * `view` is a DIGEST OVER WHAT THIS CALLER CAN SEE — ids, categories, state, operations —
193
+ * and deliberately NOT over `record_version`, so a routine token refresh does not move it.
194
+ * Compare it against the previous call's to decide whether to reconcile at all; it is an
195
+ * opaque string and nothing but equality is defined on it.
196
+ */
197
+ export interface ScopedInventory {
198
+ readonly rows: readonly ScopedInventoryRow[];
199
+ readonly view: string;
200
+ }
201
+ /**
202
+ * Exported for the golden-reply test only; `index.ts` re-exports by name, so this does not
203
+ * widen the published surface. The test drives THIS function rather than asserting keys,
204
+ * because a key check passes on a decoder that reads the wrong key.
205
+ */
206
+ export declare function decodeScopedInventory(response: unknown, logUnknownClass: (errorClass: string) => void): ScopedInventory;
170
207
  /** What a poll found. `pending` is not an error: the operator has not decided yet. */
171
208
  export type EnrollmentPollOutcome = {
172
209
  readonly status: 'pending';
package/dist/wire.js CHANGED
@@ -220,9 +220,21 @@ export class ClaustrumClient {
220
220
  });
221
221
  return decodeEnrollmentPoll(response, this.#logger);
222
222
  }
223
- /** Exchange a live token for its successor. The old token dies when the new one is issued. */
223
+ /**
224
+ * Exchange a live token for its successor. The old token dies when the new one is issued.
225
+ *
226
+ * `expectedTokenGeneration` IS REQUIRED AND IS A FENCE, not bookkeeping: it must be the
227
+ * generation this caller currently holds. Two rotations racing — a retry after an
228
+ * uncertain reply, or two instances of the same consumer — would otherwise each mint a
229
+ * successor and each invalidate the other's, leaving a consumer holding a token the
230
+ * vault has already replaced. The vault refuses a rotation whose expectation does not
231
+ * match, so the loser learns it lost instead of discovering it on a later spend.
232
+ */
224
233
  async enrollRotate(input) {
225
- const response = await this.#call('auth.enroll_rotate', { token: input.token });
234
+ const response = await this.#call('auth.enroll_rotate', {
235
+ token: input.token,
236
+ expected_token_generation: input.expectedTokenGeneration,
237
+ });
226
238
  const result = decodeEnrollmentResult(response, this.#logger);
227
239
  const token = result.token;
228
240
  const generation = result.token_generation;
@@ -274,7 +286,14 @@ export class ClaustrumClient {
274
286
  error instanceof SubcCallError &&
275
287
  error.kind === 'terminal' &&
276
288
  error.code !== 'missing_identity' &&
277
- error.code !== 'invalid_control_body');
289
+ error.code !== 'invalid_control_body' &&
290
+ // The daemon answers unknown_module only when no module with this id is
291
+ // registered or supervised at all; a vault that is restarting or warming up
292
+ // is reported as module_warming or target_unavailable, which the SDK already
293
+ // retries in place. A fresh connection to the same daemon gets the same
294
+ // answer, and the wasted reconnect would also start the reconnect backoff,
295
+ // leaving a genuinely wedged connection unable to recover for a minute.
296
+ error.code !== 'unknown_module');
278
297
  }
279
298
  async #reconnect() {
280
299
  if (this.#closed)
@@ -306,14 +325,26 @@ export class ClaustrumClient {
306
325
  await this.#reconnecting;
307
326
  }
308
327
  }
309
- function decodeScopedInventory(response, logUnknownClass) {
328
+ /**
329
+ * Exported for the golden-reply test only; `index.ts` re-exports by name, so this does not
330
+ * widen the published surface. The test drives THIS function rather than asserting keys,
331
+ * because a key check passes on a decoder that reads the wrong key.
332
+ */
333
+ export function decodeScopedInventory(response, logUnknownClass) {
310
334
  if (hasCredentialError(response))
311
335
  throw asCredentialError(response, 'invalid_response', logUnknownClass);
312
336
  const result = isRecord(response) && isRecord(response.result) ? response.result : undefined;
313
337
  const rows = result?.credentials;
314
338
  if (!Array.isArray(rows))
315
339
  throw asCredentialError(response, 'invalid_response', logUnknownClass);
316
- return rows.map((row) => {
340
+ // THE CURSOR IS NOT OPTIONAL. Dropping it silently, as this decoder did in 0.2.0, leaves
341
+ // a consumer unable to tell "nothing changed" from "I did not ask", so it reconciles
342
+ // every poll or not at all.
343
+ const view = result?.view;
344
+ if (typeof view !== 'string' || view.length === 0) {
345
+ throw asCredentialError(response, 'invalid_response', logUnknownClass);
346
+ }
347
+ const decoded = rows.map((row) => {
317
348
  if (!isRecord(row))
318
349
  throw asCredentialError(response, 'invalid_response', logUnknownClass);
319
350
  const recordVersion = asRecordVersion(row.record_version);
@@ -370,6 +401,7 @@ function decodeScopedInventory(response, logUnknownClass) {
370
401
  orgName: row.org_name,
371
402
  };
372
403
  });
404
+ return { rows: decoded, view };
373
405
  }
374
406
  function decodeEnrollmentResult(response, logUnknownClass) {
375
407
  if (hasCredentialError(response))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/claustrum-client",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,6 +27,6 @@
27
27
  "typecheck": "tsc -p tsconfig.json --noEmit"
28
28
  },
29
29
  "dependencies": {
30
- "@cortexkit/subc-client": "^0.8.1"
30
+ "@cortexkit/subc-client": "^0.16.0"
31
31
  }
32
32
  }