@cortexkit/claustrum-client 0.2.0 → 0.3.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,18 @@ 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
+ }
170
201
  /** What a poll found. `pending` is not an error: the operator has not decided yet. */
171
202
  export type EnrollmentPollOutcome = {
172
203
  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;
@@ -313,7 +325,14 @@ function decodeScopedInventory(response, logUnknownClass) {
313
325
  const rows = result?.credentials;
314
326
  if (!Array.isArray(rows))
315
327
  throw asCredentialError(response, 'invalid_response', logUnknownClass);
316
- return rows.map((row) => {
328
+ // THE CURSOR IS NOT OPTIONAL. Dropping it silently, as this decoder did in 0.2.0, leaves
329
+ // a consumer unable to tell "nothing changed" from "I did not ask", so it reconciles
330
+ // every poll or not at all.
331
+ const view = result?.view;
332
+ if (typeof view !== 'string' || view.length === 0) {
333
+ throw asCredentialError(response, 'invalid_response', logUnknownClass);
334
+ }
335
+ const decoded = rows.map((row) => {
317
336
  if (!isRecord(row))
318
337
  throw asCredentialError(response, 'invalid_response', logUnknownClass);
319
338
  const recordVersion = asRecordVersion(row.record_version);
@@ -370,6 +389,7 @@ function decodeScopedInventory(response, logUnknownClass) {
370
389
  orgName: row.org_name,
371
390
  };
372
391
  });
392
+ return { rows: decoded, view };
373
393
  }
374
394
  function decodeEnrollmentResult(response, logUnknownClass) {
375
395
  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.3.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",