verdifax 0.2.11 → 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/README.md CHANGED
@@ -52,6 +52,8 @@ if (v.verified) {
52
52
  | `client.execute(req)` | `POST /execute` | required |
53
53
  | `client.getRun(id)` | `GET /runs/{id}` | required |
54
54
  | `client.verifyRun(id)` | `GET /runs/{id}/verify` | required |
55
+ | `client.registerRequestKey()` | `POST /keys/request-key` | required |
56
+ | `client.clearRequestKey()` | `DELETE /keys/request-key` | required |
55
57
 
56
58
  The full HTTP surface (every endpoint, deletion-receipt list,
57
59
  DCAE verify, admin endpoints) is documented in the
@@ -96,6 +98,40 @@ try {
96
98
  }
97
99
  ```
98
100
 
101
+ ## Request signing
102
+
103
+ Request signing is optional. A client with no signing key sends the same requests as before.
104
+
105
+ You can register an Ed25519 public key on your API key, so that each run must also be signed with a private key that only you hold. The key pair is created on your machine, and Verdifax receives only the public key.
106
+
107
+ ```typescript
108
+ import { VerdifaxClient, generateRequestKeyPair } from "verdifax";
109
+
110
+ // Run once, then store privateKey as a secret.
111
+ const { privateKey } = generateRequestKeyPair();
112
+
113
+ const client = new VerdifaxClient({
114
+ apiKey: process.env.VERDIFAX_API_KEY!,
115
+ signingKey: privateKey, // the 32 raw bytes, as a Buffer or Uint8Array
116
+ });
117
+
118
+ await client.registerRequestKey(); // sends the public key with its proof
119
+
120
+ // execute, attest, attestClaudeResponse and attestOpenAIResponse now sign.
121
+ await client.execute({
122
+ program_id: "1111111111111111111111111111111111111111111111111111111111111111",
123
+ route_id: "demo-attestation",
124
+ registry_record_hash: "2222222222222222222222222222222222222222222222222222222222222222",
125
+ payload_text: "first signed attestation",
126
+ });
127
+
128
+ await client.clearRequestKey(); // stops requiring signatures on this API key
129
+ ```
130
+
131
+ Registering a key changes that API key for every client that uses it: the server then refuses a run without a valid signature, so every caller on the key needs the private key. Each request carries the time and a fresh random nonce. The server refuses a repeated nonce and a time more than five minutes from its own clock.
132
+
133
+ The signature shows the holder of the key requested the run. It does not show the key holder accepted the outcome.
134
+
99
135
  ## Trust model
100
136
 
101
137
  This SDK only mediates HTTP I/O, it does NOT verify sealed
package/dist/index.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ import { KeyObject } from 'node:crypto';
2
+
1
3
  /**
2
4
  * Type definitions for the Verdifax orchestrator REST API.
3
5
  *
@@ -48,6 +50,37 @@ interface ExecuteRequest {
48
50
  ai_output_text?: string;
49
51
  /** Caller-attested actor + decision metadata; open schema. */
50
52
  attested_context?: Record<string, unknown>;
53
+ /** Filled in by the client when the `signingKey` option is set. */
54
+ request_signature?: RequestSignature;
55
+ }
56
+ /** The signature an API key with a registered request key must carry.
57
+ * The public key comes from the registration, never from the request. */
58
+ interface RequestSignature {
59
+ /** Base64 Ed25519 signature. */
60
+ signature: string;
61
+ /** RFC 3339 UTC with whole seconds, for example 2026-11-05T02:18:00Z. */
62
+ signed_at: string;
63
+ /** 16 random bytes as 32 lowercase hex characters, fresh per request. */
64
+ nonce: string;
65
+ }
66
+ /** A key pair from {@link generateRequestKeyPair}, as raw bytes. */
67
+ interface RequestKeyPair {
68
+ /** The 32-byte Ed25519 public key. */
69
+ publicKey: Buffer;
70
+ /** The 32-byte Ed25519 private key. Keep it secret. */
71
+ privateKey: Buffer;
72
+ }
73
+ /** Body of POST /keys/request-key. */
74
+ interface RequestKeyRegistrationBody {
75
+ /** Base64 of the raw 32-byte public key. */
76
+ public_key: string;
77
+ /** Base64 signature proving the caller holds the private key. */
78
+ proof: string;
79
+ }
80
+ /** Returned by {@link VerdifaxClient.registerRequestKey}. */
81
+ interface RequestKeyRegistration {
82
+ ok: boolean;
83
+ public_key: string;
51
84
  }
52
85
  interface ExecuteResponse {
53
86
  ok: boolean;
@@ -132,6 +165,10 @@ interface ClientConfig {
132
165
  * it on each subsequent retry (exponential backoff with full
133
166
  * jitter). Defaults to 200ms. */
134
167
  retryBaseDelayMs?: number;
168
+ /** The 32 raw bytes of an Ed25519 private key. When set, every path to
169
+ * POST /execute signs its request. The key is never sent to the
170
+ * server. Leave unset for unsigned requests. */
171
+ signingKey?: Uint8Array;
135
172
  }
136
173
  /** Friendly shape for {@link VerdifaxClient.attest}. Mirrors the
137
174
  * Python SDK's `attest()` signature so polyglot teams have parity. */
@@ -258,6 +295,11 @@ interface AdminAPIKey {
258
295
  * | `adminRevokeKey(id)` | DELETE /admin/keys/{id} | required |
259
296
  * | `attestClaudeResponse(...)` | POST /execute (helper) | required |
260
297
  * | `attestOpenAIResponse(...)` | POST /execute (helper) | required |
298
+ * | `registerRequestKey()` | POST /keys/request-key | required |
299
+ * | `clearRequestKey()` | DELETE /keys/request-key | required |
300
+ *
301
+ * With the `signingKey` option set, every path to POST /execute carries
302
+ * a fresh request signature. Without it, requests are unsigned.
261
303
  *
262
304
  * Idempotent GETs that hit a transient failure (502/503/504/429)
263
305
  * are retried with exponential backoff + full jitter. Non-
@@ -271,6 +313,7 @@ declare class VerdifaxClient {
271
313
  private readonly userAgent;
272
314
  private readonly maxRetries;
273
315
  private readonly retryBaseDelayMs;
316
+ private readonly signingKey;
274
317
  constructor(config?: ClientConfig);
275
318
  health(): Promise<Health>;
276
319
  version(): Promise<VerdifaxVersion>;
@@ -280,6 +323,13 @@ declare class VerdifaxClient {
280
323
  * string (for plain text). Validates the hex fields and
281
324
  * routes the call through to the canonical /execute. */
282
325
  attest(req: AttestRequest): Promise<ExecuteResponse>;
326
+ /** Registers this client's public key on its API key, with the proof
327
+ * that the client holds the private key. From then on the server
328
+ * refuses a run on this API key without a request signature. */
329
+ registerRequestKey(): Promise<RequestKeyRegistration>;
330
+ /** Clears the registered public key, so the server stops requiring
331
+ * request signatures on this API key. */
332
+ clearRequestKey(): Promise<void>;
283
333
  getRun(runId: number): Promise<Run>;
284
334
  verifyRun(runId: number): Promise<VerifyResponse>;
285
335
  listRuns(params?: ListRunsParams): Promise<{
@@ -339,6 +389,43 @@ declare class VerdifaxClient {
339
389
 
340
390
  declare function fromEnv(): VerdifaxClient;
341
391
 
392
+ /**
393
+ * Optional customer request signing.
394
+ *
395
+ * A customer can register an Ed25519 public key on an API key. From then
396
+ * on the orchestrator accepts a run only with a signature by the matching
397
+ * private key. The private key stays with the customer: it is never sent
398
+ * to the server, logged, or placed in an error message.
399
+ */
400
+
401
+ /** Creates a new Ed25519 key pair on this machine, as raw bytes: a
402
+ * 32-byte public key and a 32-byte private key. Nothing is sent to
403
+ * the server. Store the private key as a secret. */
404
+ declare function generateRequestKeyPair(): RequestKeyPair;
405
+ /** The bytes a request signature signs: the fields joined with single
406
+ * zero bytes, matching the orchestrator's RequestSignaturePreimage. */
407
+ declare function requestSignaturePreimage(fields: {
408
+ payload_hash: string;
409
+ program_id: string;
410
+ route_id: string;
411
+ registry_record_hash: string;
412
+ signed_at: string;
413
+ nonce: string;
414
+ }): Buffer;
415
+ /** Signs one execute request. The server hashes payload_text when it is
416
+ * non-empty, otherwise the payload string exactly as sent (it does not
417
+ * base64-decode it), so this does the same.
418
+ *
419
+ * `signedAt` and `nonce` default to the current time and fresh random
420
+ * bytes. A nonce the server has already seen is refused. */
421
+ declare function signRequest(signingKey: Uint8Array | KeyObject, req: ExecuteRequest, options?: {
422
+ signedAt?: string;
423
+ nonce?: string;
424
+ }): RequestSignature;
425
+ /** The body of POST /keys/request-key: the base64 public key and a
426
+ * signature proving the caller holds its private key. */
427
+ declare function requestKeyRegistrationBody(signingKey: Uint8Array | KeyObject): RequestKeyRegistrationBody;
428
+
342
429
  /**
343
430
  * Input-validation helpers, mirrors the Python SDK's
344
431
  * `_validation.py` and the Go SDK's `validation.go` for
@@ -411,4 +498,4 @@ declare class APIKeyRequiredError extends VerdifaxError {
411
498
  constructor();
412
499
  }
413
500
 
414
- export { APIError, APIKeyRequiredError, type AdminAPIKey, type AdminCreateKeyResponse, type AdminEraseRequest, type AttestRequest, AuthError, type ClientConfig, type DCAEProofBundle, type DCAEVerdict, type DeletionReceipt, type ExecuteRequest, type ExecuteResponse, type ExecutionManifest, type FormulaEra, type Health, type Hex64, type ListRunsParams, NotFoundError, type PDFFormat, type Run, type RunSummary, VerdifaxClient, VerdifaxError, type VerdifaxVersion, type VerifyResponse, fromEnv, validateHex64, validateRouteId };
501
+ export { APIError, APIKeyRequiredError, type AdminAPIKey, type AdminCreateKeyResponse, type AdminEraseRequest, type AttestRequest, AuthError, type ClientConfig, type DCAEProofBundle, type DCAEVerdict, type DeletionReceipt, type ExecuteRequest, type ExecuteResponse, type ExecutionManifest, type FormulaEra, type Health, type Hex64, type ListRunsParams, NotFoundError, type PDFFormat, type RequestKeyPair, type RequestKeyRegistration, type RequestKeyRegistrationBody, type RequestSignature, type Run, type RunSummary, VerdifaxClient, VerdifaxError, type VerdifaxVersion, type VerifyResponse, fromEnv, generateRequestKeyPair, requestKeyRegistrationBody, requestSignaturePreimage, signRequest, validateHex64, validateRouteId };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { KeyObject } from 'node:crypto';
2
+
1
3
  /**
2
4
  * Type definitions for the Verdifax orchestrator REST API.
3
5
  *
@@ -48,6 +50,37 @@ interface ExecuteRequest {
48
50
  ai_output_text?: string;
49
51
  /** Caller-attested actor + decision metadata; open schema. */
50
52
  attested_context?: Record<string, unknown>;
53
+ /** Filled in by the client when the `signingKey` option is set. */
54
+ request_signature?: RequestSignature;
55
+ }
56
+ /** The signature an API key with a registered request key must carry.
57
+ * The public key comes from the registration, never from the request. */
58
+ interface RequestSignature {
59
+ /** Base64 Ed25519 signature. */
60
+ signature: string;
61
+ /** RFC 3339 UTC with whole seconds, for example 2026-11-05T02:18:00Z. */
62
+ signed_at: string;
63
+ /** 16 random bytes as 32 lowercase hex characters, fresh per request. */
64
+ nonce: string;
65
+ }
66
+ /** A key pair from {@link generateRequestKeyPair}, as raw bytes. */
67
+ interface RequestKeyPair {
68
+ /** The 32-byte Ed25519 public key. */
69
+ publicKey: Buffer;
70
+ /** The 32-byte Ed25519 private key. Keep it secret. */
71
+ privateKey: Buffer;
72
+ }
73
+ /** Body of POST /keys/request-key. */
74
+ interface RequestKeyRegistrationBody {
75
+ /** Base64 of the raw 32-byte public key. */
76
+ public_key: string;
77
+ /** Base64 signature proving the caller holds the private key. */
78
+ proof: string;
79
+ }
80
+ /** Returned by {@link VerdifaxClient.registerRequestKey}. */
81
+ interface RequestKeyRegistration {
82
+ ok: boolean;
83
+ public_key: string;
51
84
  }
52
85
  interface ExecuteResponse {
53
86
  ok: boolean;
@@ -132,6 +165,10 @@ interface ClientConfig {
132
165
  * it on each subsequent retry (exponential backoff with full
133
166
  * jitter). Defaults to 200ms. */
134
167
  retryBaseDelayMs?: number;
168
+ /** The 32 raw bytes of an Ed25519 private key. When set, every path to
169
+ * POST /execute signs its request. The key is never sent to the
170
+ * server. Leave unset for unsigned requests. */
171
+ signingKey?: Uint8Array;
135
172
  }
136
173
  /** Friendly shape for {@link VerdifaxClient.attest}. Mirrors the
137
174
  * Python SDK's `attest()` signature so polyglot teams have parity. */
@@ -258,6 +295,11 @@ interface AdminAPIKey {
258
295
  * | `adminRevokeKey(id)` | DELETE /admin/keys/{id} | required |
259
296
  * | `attestClaudeResponse(...)` | POST /execute (helper) | required |
260
297
  * | `attestOpenAIResponse(...)` | POST /execute (helper) | required |
298
+ * | `registerRequestKey()` | POST /keys/request-key | required |
299
+ * | `clearRequestKey()` | DELETE /keys/request-key | required |
300
+ *
301
+ * With the `signingKey` option set, every path to POST /execute carries
302
+ * a fresh request signature. Without it, requests are unsigned.
261
303
  *
262
304
  * Idempotent GETs that hit a transient failure (502/503/504/429)
263
305
  * are retried with exponential backoff + full jitter. Non-
@@ -271,6 +313,7 @@ declare class VerdifaxClient {
271
313
  private readonly userAgent;
272
314
  private readonly maxRetries;
273
315
  private readonly retryBaseDelayMs;
316
+ private readonly signingKey;
274
317
  constructor(config?: ClientConfig);
275
318
  health(): Promise<Health>;
276
319
  version(): Promise<VerdifaxVersion>;
@@ -280,6 +323,13 @@ declare class VerdifaxClient {
280
323
  * string (for plain text). Validates the hex fields and
281
324
  * routes the call through to the canonical /execute. */
282
325
  attest(req: AttestRequest): Promise<ExecuteResponse>;
326
+ /** Registers this client's public key on its API key, with the proof
327
+ * that the client holds the private key. From then on the server
328
+ * refuses a run on this API key without a request signature. */
329
+ registerRequestKey(): Promise<RequestKeyRegistration>;
330
+ /** Clears the registered public key, so the server stops requiring
331
+ * request signatures on this API key. */
332
+ clearRequestKey(): Promise<void>;
283
333
  getRun(runId: number): Promise<Run>;
284
334
  verifyRun(runId: number): Promise<VerifyResponse>;
285
335
  listRuns(params?: ListRunsParams): Promise<{
@@ -339,6 +389,43 @@ declare class VerdifaxClient {
339
389
 
340
390
  declare function fromEnv(): VerdifaxClient;
341
391
 
392
+ /**
393
+ * Optional customer request signing.
394
+ *
395
+ * A customer can register an Ed25519 public key on an API key. From then
396
+ * on the orchestrator accepts a run only with a signature by the matching
397
+ * private key. The private key stays with the customer: it is never sent
398
+ * to the server, logged, or placed in an error message.
399
+ */
400
+
401
+ /** Creates a new Ed25519 key pair on this machine, as raw bytes: a
402
+ * 32-byte public key and a 32-byte private key. Nothing is sent to
403
+ * the server. Store the private key as a secret. */
404
+ declare function generateRequestKeyPair(): RequestKeyPair;
405
+ /** The bytes a request signature signs: the fields joined with single
406
+ * zero bytes, matching the orchestrator's RequestSignaturePreimage. */
407
+ declare function requestSignaturePreimage(fields: {
408
+ payload_hash: string;
409
+ program_id: string;
410
+ route_id: string;
411
+ registry_record_hash: string;
412
+ signed_at: string;
413
+ nonce: string;
414
+ }): Buffer;
415
+ /** Signs one execute request. The server hashes payload_text when it is
416
+ * non-empty, otherwise the payload string exactly as sent (it does not
417
+ * base64-decode it), so this does the same.
418
+ *
419
+ * `signedAt` and `nonce` default to the current time and fresh random
420
+ * bytes. A nonce the server has already seen is refused. */
421
+ declare function signRequest(signingKey: Uint8Array | KeyObject, req: ExecuteRequest, options?: {
422
+ signedAt?: string;
423
+ nonce?: string;
424
+ }): RequestSignature;
425
+ /** The body of POST /keys/request-key: the base64 public key and a
426
+ * signature proving the caller holds its private key. */
427
+ declare function requestKeyRegistrationBody(signingKey: Uint8Array | KeyObject): RequestKeyRegistrationBody;
428
+
342
429
  /**
343
430
  * Input-validation helpers, mirrors the Python SDK's
344
431
  * `_validation.py` and the Go SDK's `validation.go` for
@@ -411,4 +498,4 @@ declare class APIKeyRequiredError extends VerdifaxError {
411
498
  constructor();
412
499
  }
413
500
 
414
- export { APIError, APIKeyRequiredError, type AdminAPIKey, type AdminCreateKeyResponse, type AdminEraseRequest, type AttestRequest, AuthError, type ClientConfig, type DCAEProofBundle, type DCAEVerdict, type DeletionReceipt, type ExecuteRequest, type ExecuteResponse, type ExecutionManifest, type FormulaEra, type Health, type Hex64, type ListRunsParams, NotFoundError, type PDFFormat, type Run, type RunSummary, VerdifaxClient, VerdifaxError, type VerdifaxVersion, type VerifyResponse, fromEnv, validateHex64, validateRouteId };
501
+ export { APIError, APIKeyRequiredError, type AdminAPIKey, type AdminCreateKeyResponse, type AdminEraseRequest, type AttestRequest, AuthError, type ClientConfig, type DCAEProofBundle, type DCAEVerdict, type DeletionReceipt, type ExecuteRequest, type ExecuteResponse, type ExecutionManifest, type FormulaEra, type Health, type Hex64, type ListRunsParams, NotFoundError, type PDFFormat, type RequestKeyPair, type RequestKeyRegistration, type RequestKeyRegistrationBody, type RequestSignature, type Run, type RunSummary, VerdifaxClient, VerdifaxError, type VerdifaxVersion, type VerifyResponse, fromEnv, generateRequestKeyPair, requestKeyRegistrationBody, requestSignaturePreimage, signRequest, validateHex64, validateRouteId };
package/dist/index.js CHANGED
@@ -27,6 +27,10 @@ __export(index_exports, {
27
27
  VerdifaxClient: () => VerdifaxClient,
28
28
  VerdifaxError: () => VerdifaxError,
29
29
  fromEnv: () => fromEnv,
30
+ generateRequestKeyPair: () => generateRequestKeyPair,
31
+ requestKeyRegistrationBody: () => requestKeyRegistrationBody,
32
+ requestSignaturePreimage: () => requestSignaturePreimage,
33
+ signRequest: () => signRequest,
30
34
  validateHex64: () => validateHex64,
31
35
  validateRouteId: () => validateRouteId
32
36
  });
@@ -75,6 +79,84 @@ var APIKeyRequiredError = class extends VerdifaxError {
75
79
  }
76
80
  };
77
81
 
82
+ // src/signing.ts
83
+ var import_node_crypto = require("crypto");
84
+ var REQUEST_SIGNATURE_FORM = "verdifax.request.v1";
85
+ var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
86
+ var SIGNING_KEY_BYTES = 32;
87
+ var PKCS8_ED25519_HEADER = Buffer.from(
88
+ "302e020100300506032b657004220420",
89
+ "hex"
90
+ );
91
+ function rawPublicKey(key) {
92
+ return Buffer.from(key.export({ format: "der", type: "spki" }).subarray(-32));
93
+ }
94
+ function generateRequestKeyPair() {
95
+ const { publicKey, privateKey } = (0, import_node_crypto.generateKeyPairSync)("ed25519");
96
+ return {
97
+ publicKey: rawPublicKey(publicKey),
98
+ privateKey: Buffer.from(
99
+ privateKey.export({ format: "der", type: "pkcs8" }).subarray(-32)
100
+ )
101
+ };
102
+ }
103
+ function loadSigningKey(key) {
104
+ if (key instanceof import_node_crypto.KeyObject) return key;
105
+ if (!(key instanceof Uint8Array) || key.length !== SIGNING_KEY_BYTES) {
106
+ throw new VerdifaxError(
107
+ "verdifax: signingKey must be the 32 raw bytes of an Ed25519 private key"
108
+ );
109
+ }
110
+ return (0, import_node_crypto.createPrivateKey)({
111
+ key: Buffer.concat([PKCS8_ED25519_HEADER, key]),
112
+ format: "der",
113
+ type: "pkcs8"
114
+ });
115
+ }
116
+ function requestSignaturePreimage(fields) {
117
+ return Buffer.from(
118
+ [
119
+ REQUEST_SIGNATURE_FORM,
120
+ fields.payload_hash,
121
+ fields.program_id,
122
+ fields.route_id,
123
+ fields.registry_record_hash,
124
+ fields.signed_at,
125
+ fields.nonce
126
+ ].join("\0"),
127
+ "utf8"
128
+ );
129
+ }
130
+ function signRequest(signingKey, req, options = {}) {
131
+ const key = loadSigningKey(signingKey);
132
+ const signed_at = options.signedAt ?? (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
133
+ const nonce = options.nonce ?? (0, import_node_crypto.randomBytes)(16).toString("hex");
134
+ const payload_hash = (0, import_node_crypto.createHash)("sha256").update(Buffer.from(req.payload_text || req.payload || "", "utf8")).digest("hex");
135
+ const preimage = requestSignaturePreimage({
136
+ payload_hash,
137
+ program_id: req.program_id,
138
+ route_id: req.route_id,
139
+ registry_record_hash: req.registry_record_hash,
140
+ signed_at,
141
+ nonce
142
+ });
143
+ return {
144
+ signature: (0, import_node_crypto.sign)(null, preimage, key).toString("base64"),
145
+ signed_at,
146
+ nonce
147
+ };
148
+ }
149
+ function requestKeyRegistrationBody(signingKey) {
150
+ const key = loadSigningKey(signingKey);
151
+ const public_key = rawPublicKey((0, import_node_crypto.createPublicKey)(key)).toString("base64");
152
+ const proof = (0, import_node_crypto.sign)(
153
+ null,
154
+ Buffer.from(`${REQUEST_KEY_PROOF_FORM}\0${public_key}`, "utf8"),
155
+ key
156
+ ).toString("base64");
157
+ return { public_key, proof };
158
+ }
159
+
78
160
  // src/validation.ts
79
161
  function validateHex64(value, fieldName) {
80
162
  if (typeof value !== "string" || value.length !== 64) {
@@ -105,7 +187,7 @@ function validateRouteId(value) {
105
187
  }
106
188
 
107
189
  // src/version.ts
108
- var SDK_VERSION = "0.2.11";
190
+ var SDK_VERSION = "0.3.0";
109
191
 
110
192
  // src/client.ts
111
193
  var DEFAULT_BASE_URL = "https://api.verdifax.com";
@@ -114,6 +196,7 @@ var DEFAULT_MAX_RETRIES = 3;
114
196
  var DEFAULT_RETRY_BASE_DELAY_MS = 200;
115
197
  var SDK_USER_AGENT = `verdifax-sdk-node/${SDK_VERSION}`;
116
198
  var HEADER_API_KEY = "X-Verdifax-Key";
199
+ var REQUEST_KEY_PATH = "/keys/request-key";
117
200
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
118
201
  var VerdifaxClient = class {
119
202
  baseURL;
@@ -122,6 +205,7 @@ var VerdifaxClient = class {
122
205
  userAgent;
123
206
  maxRetries;
124
207
  retryBaseDelayMs;
208
+ signingKey;
125
209
  constructor(config = {}) {
126
210
  this.baseURL = (config.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
127
211
  this.apiKey = config.apiKey ?? "";
@@ -129,6 +213,7 @@ var VerdifaxClient = class {
129
213
  this.userAgent = config.userAgent ?? SDK_USER_AGENT;
130
214
  this.maxRetries = config.maxRetries === void 0 ? DEFAULT_MAX_RETRIES : Math.max(0, config.maxRetries);
131
215
  this.retryBaseDelayMs = config.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;
216
+ this.signingKey = config.signingKey === void 0 ? void 0 : loadSigningKey(config.signingKey);
132
217
  }
133
218
  // ── Health + Version (anonymous) ──────────────────────────────
134
219
  async health() {
@@ -150,7 +235,8 @@ var VerdifaxClient = class {
150
235
  "verdifax: program_id, route_id, and registry_record_hash are required"
151
236
  );
152
237
  }
153
- return this.request("POST", "/execute", req);
238
+ const body = this.signingKey ? { ...req, request_signature: signRequest(this.signingKey, req) } : req;
239
+ return this.request("POST", "/execute", body);
154
240
  }
155
241
  /** High-level convenience wrapper around `execute()`. Accepts
156
242
  * either a Buffer (for binary payloads, auto-base64) or a
@@ -185,6 +271,29 @@ var VerdifaxClient = class {
185
271
  }
186
272
  return this.execute(body);
187
273
  }
274
+ // ── Request signing (auth) ───────────────────────────────────
275
+ /** Registers this client's public key on its API key, with the proof
276
+ * that the client holds the private key. From then on the server
277
+ * refuses a run on this API key without a request signature. */
278
+ async registerRequestKey() {
279
+ this.requireAPIKey();
280
+ if (!this.signingKey) {
281
+ throw new VerdifaxError(
282
+ "verdifax: registerRequestKey needs the signingKey client option"
283
+ );
284
+ }
285
+ return this.request(
286
+ "POST",
287
+ REQUEST_KEY_PATH,
288
+ requestKeyRegistrationBody(this.signingKey)
289
+ );
290
+ }
291
+ /** Clears the registered public key, so the server stops requiring
292
+ * request signatures on this API key. */
293
+ async clearRequestKey() {
294
+ this.requireAPIKey();
295
+ await this.request("DELETE", REQUEST_KEY_PATH);
296
+ }
188
297
  // ── Runs ──────────────────────────────────────────────────────
189
298
  async getRun(runId) {
190
299
  this.requireAPIKey();
@@ -459,6 +568,10 @@ function fromEnv() {
459
568
  VerdifaxClient,
460
569
  VerdifaxError,
461
570
  fromEnv,
571
+ generateRequestKeyPair,
572
+ requestKeyRegistrationBody,
573
+ requestSignaturePreimage,
574
+ signRequest,
462
575
  validateHex64,
463
576
  validateRouteId
464
577
  });
package/dist/index.mjs CHANGED
@@ -41,6 +41,92 @@ var APIKeyRequiredError = class extends VerdifaxError {
41
41
  }
42
42
  };
43
43
 
44
+ // src/signing.ts
45
+ import {
46
+ createHash,
47
+ createPrivateKey,
48
+ createPublicKey,
49
+ generateKeyPairSync,
50
+ KeyObject,
51
+ randomBytes,
52
+ sign
53
+ } from "crypto";
54
+ var REQUEST_SIGNATURE_FORM = "verdifax.request.v1";
55
+ var REQUEST_KEY_PROOF_FORM = "verdifax.request-key.v1";
56
+ var SIGNING_KEY_BYTES = 32;
57
+ var PKCS8_ED25519_HEADER = Buffer.from(
58
+ "302e020100300506032b657004220420",
59
+ "hex"
60
+ );
61
+ function rawPublicKey(key) {
62
+ return Buffer.from(key.export({ format: "der", type: "spki" }).subarray(-32));
63
+ }
64
+ function generateRequestKeyPair() {
65
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
66
+ return {
67
+ publicKey: rawPublicKey(publicKey),
68
+ privateKey: Buffer.from(
69
+ privateKey.export({ format: "der", type: "pkcs8" }).subarray(-32)
70
+ )
71
+ };
72
+ }
73
+ function loadSigningKey(key) {
74
+ if (key instanceof KeyObject) return key;
75
+ if (!(key instanceof Uint8Array) || key.length !== SIGNING_KEY_BYTES) {
76
+ throw new VerdifaxError(
77
+ "verdifax: signingKey must be the 32 raw bytes of an Ed25519 private key"
78
+ );
79
+ }
80
+ return createPrivateKey({
81
+ key: Buffer.concat([PKCS8_ED25519_HEADER, key]),
82
+ format: "der",
83
+ type: "pkcs8"
84
+ });
85
+ }
86
+ function requestSignaturePreimage(fields) {
87
+ return Buffer.from(
88
+ [
89
+ REQUEST_SIGNATURE_FORM,
90
+ fields.payload_hash,
91
+ fields.program_id,
92
+ fields.route_id,
93
+ fields.registry_record_hash,
94
+ fields.signed_at,
95
+ fields.nonce
96
+ ].join("\0"),
97
+ "utf8"
98
+ );
99
+ }
100
+ function signRequest(signingKey, req, options = {}) {
101
+ const key = loadSigningKey(signingKey);
102
+ const signed_at = options.signedAt ?? (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
103
+ const nonce = options.nonce ?? randomBytes(16).toString("hex");
104
+ const payload_hash = createHash("sha256").update(Buffer.from(req.payload_text || req.payload || "", "utf8")).digest("hex");
105
+ const preimage = requestSignaturePreimage({
106
+ payload_hash,
107
+ program_id: req.program_id,
108
+ route_id: req.route_id,
109
+ registry_record_hash: req.registry_record_hash,
110
+ signed_at,
111
+ nonce
112
+ });
113
+ return {
114
+ signature: sign(null, preimage, key).toString("base64"),
115
+ signed_at,
116
+ nonce
117
+ };
118
+ }
119
+ function requestKeyRegistrationBody(signingKey) {
120
+ const key = loadSigningKey(signingKey);
121
+ const public_key = rawPublicKey(createPublicKey(key)).toString("base64");
122
+ const proof = sign(
123
+ null,
124
+ Buffer.from(`${REQUEST_KEY_PROOF_FORM}\0${public_key}`, "utf8"),
125
+ key
126
+ ).toString("base64");
127
+ return { public_key, proof };
128
+ }
129
+
44
130
  // src/validation.ts
45
131
  function validateHex64(value, fieldName) {
46
132
  if (typeof value !== "string" || value.length !== 64) {
@@ -71,7 +157,7 @@ function validateRouteId(value) {
71
157
  }
72
158
 
73
159
  // src/version.ts
74
- var SDK_VERSION = "0.2.11";
160
+ var SDK_VERSION = "0.3.0";
75
161
 
76
162
  // src/client.ts
77
163
  var DEFAULT_BASE_URL = "https://api.verdifax.com";
@@ -80,6 +166,7 @@ var DEFAULT_MAX_RETRIES = 3;
80
166
  var DEFAULT_RETRY_BASE_DELAY_MS = 200;
81
167
  var SDK_USER_AGENT = `verdifax-sdk-node/${SDK_VERSION}`;
82
168
  var HEADER_API_KEY = "X-Verdifax-Key";
169
+ var REQUEST_KEY_PATH = "/keys/request-key";
83
170
  var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
84
171
  var VerdifaxClient = class {
85
172
  baseURL;
@@ -88,6 +175,7 @@ var VerdifaxClient = class {
88
175
  userAgent;
89
176
  maxRetries;
90
177
  retryBaseDelayMs;
178
+ signingKey;
91
179
  constructor(config = {}) {
92
180
  this.baseURL = (config.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
93
181
  this.apiKey = config.apiKey ?? "";
@@ -95,6 +183,7 @@ var VerdifaxClient = class {
95
183
  this.userAgent = config.userAgent ?? SDK_USER_AGENT;
96
184
  this.maxRetries = config.maxRetries === void 0 ? DEFAULT_MAX_RETRIES : Math.max(0, config.maxRetries);
97
185
  this.retryBaseDelayMs = config.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;
186
+ this.signingKey = config.signingKey === void 0 ? void 0 : loadSigningKey(config.signingKey);
98
187
  }
99
188
  // ── Health + Version (anonymous) ──────────────────────────────
100
189
  async health() {
@@ -116,7 +205,8 @@ var VerdifaxClient = class {
116
205
  "verdifax: program_id, route_id, and registry_record_hash are required"
117
206
  );
118
207
  }
119
- return this.request("POST", "/execute", req);
208
+ const body = this.signingKey ? { ...req, request_signature: signRequest(this.signingKey, req) } : req;
209
+ return this.request("POST", "/execute", body);
120
210
  }
121
211
  /** High-level convenience wrapper around `execute()`. Accepts
122
212
  * either a Buffer (for binary payloads, auto-base64) or a
@@ -151,6 +241,29 @@ var VerdifaxClient = class {
151
241
  }
152
242
  return this.execute(body);
153
243
  }
244
+ // ── Request signing (auth) ───────────────────────────────────
245
+ /** Registers this client's public key on its API key, with the proof
246
+ * that the client holds the private key. From then on the server
247
+ * refuses a run on this API key without a request signature. */
248
+ async registerRequestKey() {
249
+ this.requireAPIKey();
250
+ if (!this.signingKey) {
251
+ throw new VerdifaxError(
252
+ "verdifax: registerRequestKey needs the signingKey client option"
253
+ );
254
+ }
255
+ return this.request(
256
+ "POST",
257
+ REQUEST_KEY_PATH,
258
+ requestKeyRegistrationBody(this.signingKey)
259
+ );
260
+ }
261
+ /** Clears the registered public key, so the server stops requiring
262
+ * request signatures on this API key. */
263
+ async clearRequestKey() {
264
+ this.requireAPIKey();
265
+ await this.request("DELETE", REQUEST_KEY_PATH);
266
+ }
154
267
  // ── Runs ──────────────────────────────────────────────────────
155
268
  async getRun(runId) {
156
269
  this.requireAPIKey();
@@ -424,6 +537,10 @@ export {
424
537
  VerdifaxClient,
425
538
  VerdifaxError,
426
539
  fromEnv,
540
+ generateRequestKeyPair,
541
+ requestKeyRegistrationBody,
542
+ requestSignaturePreimage,
543
+ signRequest,
427
544
  validateHex64,
428
545
  validateRouteId
429
546
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verdifax",
3
- "version": "0.2.11",
3
+ "version": "0.3.0",
4
4
  "description": "Official Node.js / TypeScript SDK for the Verdifax orchestrator REST API.",
5
5
  "license": "MIT",
6
6
  "author": "Verdifax <team@verdifax.com>",