@cortexkit/claustrum-client 0.15.0 → 0.16.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 CHANGED
@@ -76,6 +76,11 @@ copy. One behaviour reaches callers: while the vault is restarting or refusing r
76
76
  opens as retryable, a call now keeps retrying for up to 90 s (paced, with jitter) instead
77
77
  of 30 s. Give every vault call on a latency-sensitive path its own timeout.
78
78
 
79
+ **Changed in 0.16.0:** the dependency is now `@cortexkit/subc-client` ^0.24.0.
80
+ The SDK negotiates FIFO request/watch windows and retries only daemon-origin
81
+ `route_busy` refusals within the request deadline. Claustrum adds no busy retry;
82
+ older daemons keep the SDK's previous behavior.
83
+
79
84
  ## Reading credential material
80
85
 
81
86
  The wire payload is opaque bytes. `getCredential(handle, minTtlMs?)` and
@@ -161,10 +166,37 @@ for (;;) {
161
166
  }
162
167
  ```
163
168
 
169
+ ## Enrollment
170
+
171
+ Use `newEnrollmentSecret()` to prepare an enrollment proposal. It generates 32
172
+ random bytes as exactly 64 lowercase hex characters, plus `requestSecretHash`:
173
+ the lowercase hex SHA-256 of the **32 decoded bytes**, not of the UTF-8 hex text.
174
+ `enrollmentSecretHash(requestSecret)` recomputes the hash of a persisted secret
175
+ and refuses anything other than exactly 64 lowercase hex characters.
176
+
177
+ ```ts
178
+ import { ClaustrumClient, newEnrollmentSecret } from '@cortexkit/claustrum-client'
179
+
180
+ const client = await ClaustrumClient.connect()
181
+ const { requestSecret, requestSecretHash } = newEnrollmentSecret()
182
+ // Persist the secret privately before proposing so a crash can recover the poll.
183
+ await persistRequestSecret(requestSecret)
184
+ const { requestId } = await client.enrollPropose({ name: 'consumer', requestSecretHash })
185
+ await persistRequestId(requestId)
186
+ // After operator approval, poll with the original secret, not its hash.
187
+ const outcome = await client.enrollPoll({ requestId, requestSecret })
188
+ ```
189
+
190
+ The persistence functions above are caller-provided. Re-proposing the same name
191
+ with the same persisted secret/hash recovers the request id after a lost reply.
192
+ Only `pending` means poll again; an approved outcome returns the token once, so
193
+ persist it with `writeEnrollmentTokenFile` before using it. Never log the request
194
+ secret or token.
195
+
164
196
  ## Private files and Windows support
165
197
 
166
198
  Enrollment tokens support Windows as well as Unix. The client uses
167
- `@cortexkit/store` 0.2.1 or newer: exclusive private creation
199
+ `@cortexkit/store` 0.2.3 or newer: exclusive private creation
168
200
  protects files before any secret bytes are written, and atomic replacement keeps
169
201
  that protection. Unix files use mode 0600 and new directories use 0700. On Windows,
170
202
  the store's `@cortexkit/private-files` native module creates owner-only ACLs and the
@@ -3,5 +3,12 @@ export type EnrollmentTokenFile = {
3
3
  token: string;
4
4
  token_generation: number;
5
5
  };
6
+ /** Hash the 32 decoded bytes, not the UTF-8 text of the hex request secret. */
7
+ export declare function enrollmentSecretHash(requestSecret: string): string;
8
+ /** Mint an enrollment request secret and the hash to send with the proposal. */
9
+ export declare function newEnrollmentSecret(): {
10
+ requestSecret: string;
11
+ requestSecretHash: string;
12
+ };
6
13
  export declare function createEnrollmentWriter(checkPath?: typeof checkPrivatePath): (path: string, value: EnrollmentTokenFile) => Promise<void>;
7
14
  export declare const writeEnrollmentTokenFile: (path: string, value: EnrollmentTokenFile) => Promise<void>;
@@ -1,9 +1,21 @@
1
1
  import { createPrivateFile } from '@cortexkit/store';
2
2
  import { checkPrivatePath, createPrivateParents, notPrivate, privateCreationCode } from './private-path.js';
3
- import { randomBytes } from 'node:crypto';
3
+ import { createHash, randomBytes } from 'node:crypto';
4
4
  import { open, rename, unlink } from 'node:fs/promises';
5
5
  import { basename, dirname, join, resolve } from 'node:path';
6
6
  const TOKEN_RE = /^[0-9a-f]{64}$/;
7
+ /** Hash the 32 decoded bytes, not the UTF-8 text of the hex request secret. */
8
+ export function enrollmentSecretHash(requestSecret) {
9
+ if (typeof requestSecret !== 'string' || requestSecret.length !== 64 || !TOKEN_RE.test(requestSecret)) {
10
+ throw new Error('enrollment request secret must be exactly 64 lowercase hex characters');
11
+ }
12
+ return createHash('sha256').update(Buffer.from(requestSecret, 'hex')).digest('hex');
13
+ }
14
+ /** Mint an enrollment request secret and the hash to send with the proposal. */
15
+ export function newEnrollmentSecret() {
16
+ const requestSecret = randomBytes(32).toString('hex');
17
+ return { requestSecret, requestSecretHash: enrollmentSecretHash(requestSecret) };
18
+ }
7
19
  function validateTokenFile(value) {
8
20
  if (!TOKEN_RE.test(value.token))
9
21
  throw new Error('enrollment token must be exactly 64 lowercase hex characters');
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { detectClaustrumConnection, getDefaultClaustrumConnectionPath, resolveClaustrumConnectionPath, type ClaustrumDetection, type ClaustrumEndpoint, } from './detect.js';
2
2
  export { storeIdentity, storageFingerprint } from './identity.js';
3
- export { writeEnrollmentTokenFile, type EnrollmentTokenFile, } from './enrollment.js';
3
+ export { enrollmentSecretHash, newEnrollmentSecret, writeEnrollmentTokenFile, type EnrollmentTokenFile, } from './enrollment.js';
4
4
  export { ClaustrumCredentialError, credentialErrorAction, ERROR_CLASS_WIRE_SET, type ClaustrumCredentialErrorAction, type ClaustrumCredentialErrorClass, } from './errors.js';
5
5
  export { ClaustrumClient, type ClaustrumClientOptions, type ClaustrumConnector, type ClaustrumReporterSource, type CredentialStatus, type EnrollmentPollOutcome, type ScopedGrantTuple, type ScopedInventory, type ScopedInventoryRow, type ServedCredential, type ServedCredentialBytes, } from './wire.js';
6
6
  export { ScopedWatchError, type ScopedWatch, type ScopedWatchNotice, type ScopedWatchOptions, type ScopedWatchOutcome, } from './watch.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { detectClaustrumConnection, getDefaultClaustrumConnectionPath, resolveClaustrumConnectionPath, } from './detect.js';
2
2
  export { storeIdentity, storageFingerprint } from './identity.js';
3
- export { writeEnrollmentTokenFile, } from './enrollment.js';
3
+ export { enrollmentSecretHash, newEnrollmentSecret, writeEnrollmentTokenFile, } from './enrollment.js';
4
4
  export { ClaustrumCredentialError, credentialErrorAction, ERROR_CLASS_WIRE_SET, } from './errors.js';
5
5
  export { ClaustrumClient, } from './wire.js';
6
6
  export { ScopedWatchError, } from './watch.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/claustrum-client",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,7 +27,7 @@
27
27
  "typecheck": "tsc -p tsconfig.json --noEmit"
28
28
  },
29
29
  "dependencies": {
30
- "@cortexkit/store": "^0.2.1",
31
- "@cortexkit/subc-client": "^0.22.0"
30
+ "@cortexkit/store": "^0.2.3",
31
+ "@cortexkit/subc-client": "^0.24.0"
32
32
  }
33
33
  }