@t-0/provider-sdk 1.1.28 → 1.1.29

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.
Files changed (46) hide show
  1. package/README.md +33 -0
  2. package/lib/cjs/crypto/hash.d.ts +2 -0
  3. package/lib/cjs/crypto/hash.js +17 -0
  4. package/lib/cjs/crypto/index.d.ts +5 -0
  5. package/lib/cjs/crypto/index.js +14 -0
  6. package/lib/cjs/crypto/keys.d.ts +2 -0
  7. package/lib/cjs/crypto/keys.js +19 -0
  8. package/lib/cjs/crypto/request.d.ts +20 -0
  9. package/lib/cjs/crypto/request.js +49 -0
  10. package/lib/cjs/crypto/verify.d.ts +1 -0
  11. package/lib/cjs/crypto/verify.js +19 -0
  12. package/lib/cjs/index.d.ts +3 -0
  13. package/lib/cjs/index.js +8 -1
  14. package/lib/cjs/service/health_pb.js +5 -2
  15. package/lib/cjs/service/service.d.ts +12 -1
  16. package/lib/cjs/service/service.js +6 -16
  17. package/lib/cjs/service/validate.d.ts +25 -0
  18. package/lib/cjs/service/validate.js +42 -0
  19. package/lib/cjs/service/validate_response.d.ts +15 -1
  20. package/lib/cjs/service/validate_response.js +29 -2
  21. package/lib/cjs/version.d.ts +1 -1
  22. package/lib/cjs/version.js +1 -1
  23. package/lib/esm/crypto/hash.d.ts +2 -0
  24. package/lib/esm/crypto/hash.js +13 -0
  25. package/lib/esm/crypto/index.d.ts +5 -0
  26. package/lib/esm/crypto/index.js +4 -0
  27. package/lib/esm/crypto/keys.d.ts +2 -0
  28. package/lib/esm/crypto/keys.js +15 -0
  29. package/lib/esm/crypto/request.d.ts +20 -0
  30. package/lib/esm/crypto/request.js +45 -0
  31. package/lib/esm/crypto/verify.d.ts +1 -0
  32. package/lib/esm/crypto/verify.js +16 -0
  33. package/lib/esm/index.d.ts +3 -0
  34. package/lib/esm/index.js +3 -0
  35. package/lib/esm/service/health_pb.js +5 -2
  36. package/lib/esm/service/service.d.ts +12 -1
  37. package/lib/esm/service/service.js +6 -16
  38. package/lib/esm/service/validate.d.ts +25 -0
  39. package/lib/esm/service/validate.js +38 -0
  40. package/lib/esm/service/validate_response.d.ts +15 -1
  41. package/lib/esm/service/validate_response.js +29 -2
  42. package/lib/esm/version.d.ts +1 -1
  43. package/lib/esm/version.js +1 -1
  44. package/lib/tsconfig.cjs.tsbuildinfo +1 -1
  45. package/lib/tsconfig.esm.tsbuildinfo +1 -1
  46. package/package.json +15 -5
package/README.md CHANGED
@@ -64,6 +64,39 @@ server.listen(3000);
64
64
 
65
65
  The middleware chain: `signatureValidation` captures raw request bytes for hashing, `nodeAdapter` bridges ConnectRPC to Node.js HTTP, and `createService` registers your handlers with signature verification.
66
66
 
67
+ ### Standalone Signature Verification
68
+
69
+ For frameworks that don't use Node's `http.createServer` (Effect, Koa, Fastify, etc.), use `createRequestVerifier` to verify inbound requests with just the raw body bytes and headers:
70
+
71
+ ```ts
72
+ import { createRequestVerifier, parsePublicKey } from "@t-0/provider-sdk";
73
+
74
+ const verify = createRequestVerifier({
75
+ networkPublicKey: process.env.NETWORK_PUBLIC_KEY!,
76
+ });
77
+
78
+ // In your framework's request handler:
79
+ function handleRequest(rawBody: Uint8Array, headers: Record<string, string>) {
80
+ const result = verify({
81
+ body: rawBody,
82
+ signatureHeader: headers["x-signature"],
83
+ publicKeyHeader: headers["x-public-key"],
84
+ timestampHeader: headers["x-signature-timestamp"],
85
+ });
86
+
87
+ if (!result.valid) {
88
+ // result.reason is one of: 'invalid_timestamp', 'timestamp_out_of_range',
89
+ // 'invalid_public_key', 'unknown_public_key', 'invalid_signature_format',
90
+ // 'signature_failed'
91
+ return errorResponse(result.reason);
92
+ }
93
+
94
+ // Request is authenticated — parse the protobuf body and handle it
95
+ }
96
+ ```
97
+
98
+ The lower-level primitives are also exported individually: `verifySignature`, `computeDigest`, `keccak256`, `parsePublicKey`, `publicKeysEqual`.
99
+
67
100
  ### Network Client
68
101
 
69
102
  Use `createClient` to call T-0 Network APIs. The client handles request signing automatically:
@@ -0,0 +1,2 @@
1
+ export declare function keccak256(...inputs: Uint8Array[]): Buffer;
2
+ export declare function computeDigest(body: Uint8Array, timestampMs: number): Buffer;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.keccak256 = keccak256;
4
+ exports.computeDigest = computeDigest;
5
+ const sha3_js_1 = require("@noble/hashes/sha3.js");
6
+ function keccak256(...inputs) {
7
+ const h = sha3_js_1.keccak_256.create();
8
+ for (const input of inputs) {
9
+ h.update(input);
10
+ }
11
+ return Buffer.from(h.digest());
12
+ }
13
+ function computeDigest(body, timestampMs) {
14
+ const tsBuf = Buffer.alloc(8);
15
+ tsBuf.writeBigUInt64LE(BigInt(timestampMs));
16
+ return keccak256(body, tsBuf);
17
+ }
@@ -0,0 +1,5 @@
1
+ export { verifySignature } from './verify.js';
2
+ export { keccak256, computeDigest } from './hash.js';
3
+ export { parsePublicKey, publicKeysEqual } from './keys.js';
4
+ export { createRequestVerifier, DEFAULT_TOLERANCE_MS } from './request.js';
5
+ export type { CreateVerifierOptions, VerifyRequest, VerifyRequestResult, VerifyRequestFailure, RequestVerifier } from './request.js';
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_TOLERANCE_MS = exports.createRequestVerifier = exports.publicKeysEqual = exports.parsePublicKey = exports.computeDigest = exports.keccak256 = exports.verifySignature = void 0;
4
+ var verify_js_1 = require("./verify.js");
5
+ Object.defineProperty(exports, "verifySignature", { enumerable: true, get: function () { return verify_js_1.verifySignature; } });
6
+ var hash_js_1 = require("./hash.js");
7
+ Object.defineProperty(exports, "keccak256", { enumerable: true, get: function () { return hash_js_1.keccak256; } });
8
+ Object.defineProperty(exports, "computeDigest", { enumerable: true, get: function () { return hash_js_1.computeDigest; } });
9
+ var keys_js_1 = require("./keys.js");
10
+ Object.defineProperty(exports, "parsePublicKey", { enumerable: true, get: function () { return keys_js_1.parsePublicKey; } });
11
+ Object.defineProperty(exports, "publicKeysEqual", { enumerable: true, get: function () { return keys_js_1.publicKeysEqual; } });
12
+ var request_js_1 = require("./request.js");
13
+ Object.defineProperty(exports, "createRequestVerifier", { enumerable: true, get: function () { return request_js_1.createRequestVerifier; } });
14
+ Object.defineProperty(exports, "DEFAULT_TOLERANCE_MS", { enumerable: true, get: function () { return request_js_1.DEFAULT_TOLERANCE_MS; } });
@@ -0,0 +1,2 @@
1
+ export declare function parsePublicKey(key: string | Buffer): Buffer;
2
+ export declare function publicKeysEqual(a: Uint8Array, b: Uint8Array): boolean;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parsePublicKey = parsePublicKey;
4
+ exports.publicKeysEqual = publicKeysEqual;
5
+ function parsePublicKey(key) {
6
+ if (typeof key === 'string') {
7
+ key = Buffer.from(key.startsWith('0x') ? key.slice(2) : key, 'hex');
8
+ }
9
+ if (key.length !== 65 || key[0] !== 0x04) {
10
+ throw new Error('Public key must be 65 bytes in uncompressed format (0x04 prefix)');
11
+ }
12
+ return Buffer.from(key);
13
+ }
14
+ function publicKeysEqual(a, b) {
15
+ if (a.length !== b.length) {
16
+ return false;
17
+ }
18
+ return Buffer.from(a).compare(Buffer.from(b)) === 0;
19
+ }
@@ -0,0 +1,20 @@
1
+ export declare const DEFAULT_TOLERANCE_MS = 60000;
2
+ export interface CreateVerifierOptions {
3
+ networkPublicKey: string | Buffer;
4
+ toleranceMs?: number;
5
+ }
6
+ export interface VerifyRequest {
7
+ body: Uint8Array;
8
+ signatureHeader: string;
9
+ publicKeyHeader: string;
10
+ timestampHeader: string;
11
+ }
12
+ export type VerifyRequestFailure = 'invalid_timestamp' | 'timestamp_out_of_range' | 'invalid_public_key' | 'unknown_public_key' | 'invalid_signature_format' | 'signature_failed';
13
+ export type VerifyRequestResult = {
14
+ valid: true;
15
+ } | {
16
+ valid: false;
17
+ reason: VerifyRequestFailure;
18
+ };
19
+ export type RequestVerifier = (req: VerifyRequest) => VerifyRequestResult;
20
+ export declare function createRequestVerifier(opts: CreateVerifierOptions): RequestVerifier;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_TOLERANCE_MS = void 0;
4
+ exports.createRequestVerifier = createRequestVerifier;
5
+ const verify_js_1 = require("./verify.js");
6
+ const hash_js_1 = require("./hash.js");
7
+ const keys_js_1 = require("./keys.js");
8
+ exports.DEFAULT_TOLERANCE_MS = 60000;
9
+ function createRequestVerifier(opts) {
10
+ const networkKey = (0, keys_js_1.parsePublicKey)(opts.networkPublicKey);
11
+ const tolerance = opts.toleranceMs ?? exports.DEFAULT_TOLERANCE_MS;
12
+ return (req) => {
13
+ const ts = parseInt(req.timestampHeader, 10);
14
+ if (!Number.isFinite(ts) || ts < 0) {
15
+ return { valid: false, reason: 'invalid_timestamp' };
16
+ }
17
+ if (Math.abs(Date.now() - ts) > tolerance) {
18
+ return { valid: false, reason: 'timestamp_out_of_range' };
19
+ }
20
+ let publicKey;
21
+ try {
22
+ publicKey = (0, keys_js_1.parsePublicKey)(req.publicKeyHeader);
23
+ }
24
+ catch {
25
+ return { valid: false, reason: 'invalid_public_key' };
26
+ }
27
+ if (!(0, keys_js_1.publicKeysEqual)(publicKey, networkKey)) {
28
+ return { valid: false, reason: 'unknown_public_key' };
29
+ }
30
+ let signature;
31
+ try {
32
+ const hex = req.signatureHeader.startsWith('0x')
33
+ ? req.signatureHeader.slice(2)
34
+ : req.signatureHeader;
35
+ signature = Buffer.from(hex, 'hex');
36
+ }
37
+ catch {
38
+ return { valid: false, reason: 'invalid_signature_format' };
39
+ }
40
+ if (signature.length !== 64 && signature.length !== 65) {
41
+ return { valid: false, reason: 'invalid_signature_format' };
42
+ }
43
+ const digest = (0, hash_js_1.computeDigest)(req.body, ts);
44
+ if (!(0, verify_js_1.verifySignature)(publicKey, digest, signature)) {
45
+ return { valid: false, reason: 'signature_failed' };
46
+ }
47
+ return { valid: true };
48
+ };
49
+ }
@@ -0,0 +1 @@
1
+ export declare function verifySignature(publicKey: Uint8Array, digest: Uint8Array, signature: Uint8Array): boolean;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifySignature = verifySignature;
4
+ const secp256k1_js_1 = require("@noble/curves/secp256k1.js");
5
+ function verifySignature(publicKey, digest, signature) {
6
+ if (digest.length !== 32) {
7
+ return false;
8
+ }
9
+ if (signature.length !== 64 && signature.length !== 65) {
10
+ return false;
11
+ }
12
+ const sig64 = signature.length === 65 ? signature.subarray(0, 64) : signature;
13
+ try {
14
+ return secp256k1_js_1.secp256k1.verify(sig64, digest, publicKey, { prehash: false });
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
@@ -1,7 +1,10 @@
1
+ export * from "./crypto/index.js";
1
2
  export * from "./client/client.js";
2
3
  export * from "./service/service.js";
3
4
  export * from "./service/validate_response.js";
5
+ export * from "./service/validate.js";
4
6
  export * from "./service/node.js";
7
+ export { default as NetworkHeaders } from "./common/headers.js";
5
8
  export { connectNodeAdapter as nodeAdapter } from "@connectrpc/connect-node";
6
9
  export type { Client, HandlerContext } from "@connectrpc/connect";
7
10
  export * from './common/gen/tzero/v1/common/common_pb.js';
package/lib/cjs/index.js CHANGED
@@ -35,12 +35,19 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  return result;
36
36
  };
37
37
  })();
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
38
41
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.PaymentIntentRecipient = exports.PaymentIntentProvider = exports.PaymentIntentBeneficiary = exports.PaymentIntentPayInProvider = exports.PaymentIntentNetwork = exports.nodeAdapter = void 0;
42
+ exports.PaymentIntentRecipient = exports.PaymentIntentProvider = exports.PaymentIntentBeneficiary = exports.PaymentIntentPayInProvider = exports.PaymentIntentNetwork = exports.nodeAdapter = exports.NetworkHeaders = void 0;
43
+ __exportStar(require("./crypto/index.js"), exports);
40
44
  __exportStar(require("./client/client.js"), exports);
41
45
  __exportStar(require("./service/service.js"), exports);
42
46
  __exportStar(require("./service/validate_response.js"), exports);
47
+ __exportStar(require("./service/validate.js"), exports);
43
48
  __exportStar(require("./service/node.js"), exports);
49
+ var headers_js_1 = require("./common/headers.js");
50
+ Object.defineProperty(exports, "NetworkHeaders", { enumerable: true, get: function () { return __importDefault(headers_js_1).default; } });
44
51
  var connect_node_1 = require("@connectrpc/connect-node");
45
52
  Object.defineProperty(exports, "nodeAdapter", { enumerable: true, get: function () { return connect_node_1.connectNodeAdapter; } });
46
53
  __exportStar(require("./common/gen/tzero/v1/common/common_pb.js"), exports);
@@ -6,12 +6,15 @@
6
6
  // buf.gen.yaml pin -- plugin and @bufbuild/protobuf runtime must
7
7
  // agree on major).
8
8
  //
9
- // Regenerate by overwriting this file:
9
+ // Regenerate with the following, which keeps this header and replaces
10
+ // everything from the gRPC copyright line down. Do not just `mv` the generated
11
+ // file over this one -- that would delete these instructions along with it.
10
12
  //
11
13
  // cd node/sdk
12
14
  // buf generate buf.build/grpc/grpc --path grpc/health/v1/health.proto \
13
15
  // --template '{"version":"v2","plugins":[{"remote":"buf.build/bufbuild/es:v2.12.0","out":"gen-health","opt":["target=ts","import_extension=js"]}]}'
14
- // mv gen-health/grpc/health/v1/health_pb.ts src/service/health_pb.ts
16
+ // sed '/^\/\/ Copyright 2015 The gRPC Authors/,$d' src/service/health_pb.ts > gen-health/header
17
+ // cat gen-health/header gen-health/grpc/health/v1/health_pb.ts > src/service/health_pb.ts
15
18
  // rm -rf gen-health
16
19
  //
17
20
  // Vendored rather than depended on: the equivalent package
@@ -2,11 +2,22 @@ import { ConnectRouter } from "@connectrpc/connect";
2
2
  import type { Interceptor } from "@connectrpc/connect";
3
3
  import type { DescService } from "@bufbuild/protobuf";
4
4
  import type { ServiceImpl } from "@connectrpc/connect";
5
+ import { type Logger } from "./validate_response.js";
6
+ export interface CreateServiceOptions {
7
+ /**
8
+ * Logger used by the SDK for error-level events (currently:
9
+ * response-validation failures from the interceptor safety net). The same
10
+ * logger will be used for any future server-wide SDK log sites.
11
+ *
12
+ * If omitted, the SDK logs to `console.error` with a JSON-encoded payload.
13
+ */
14
+ logger?: Logger;
15
+ }
5
16
  export declare const REQUEST_VALIDITY_MILLIS = 60000;
6
17
  interface Router {
7
18
  service: <T extends DescService, I extends ServiceImpl<T>>(service: T, implementation: I) => void;
8
19
  }
9
- export declare const createService: (networkPublicKey: string | Buffer, registerRoutes: (router: Router) => void) => {
20
+ export declare const createService: (networkPublicKey: string | Buffer, registerRoutes: (router: Router) => void, options?: CreateServiceOptions) => {
10
21
  routes: (router: ConnectRouter) => void;
11
22
  interceptors: Interceptor[];
12
23
  grpcWeb: boolean;
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createService = exports.REQUEST_VALIDITY_MILLIS = void 0;
7
7
  const connect_1 = require("@connectrpc/connect");
8
8
  const headers_js_1 = __importDefault(require("../common/headers.js"));
9
- const secp256k1_js_1 = require("@noble/curves/secp256k1.js");
9
+ const verify_js_1 = require("../crypto/verify.js");
10
10
  const validate_response_js_1 = require("./validate_response.js");
11
11
  const health_pb_js_1 = require("./health_pb.js");
12
12
  const health_js_1 = require("./health.js");
@@ -20,29 +20,19 @@ const createSignatureVerification = (networkPublicKey) => (next) => async (req)
20
20
  if (networkPublicKey.compare(publicKey) !== 0) {
21
21
  throw new connect_1.ConnectError(`${headers_js_1.default.PublicKey} value is not network public key`, connect_1.Code.Unauthenticated);
22
22
  }
23
- let signature = decodeHex(getHeader(req, headers_js_1.default.Signature));
24
- if (signature.length === 65) {
25
- signature = signature.subarray(0, 64);
26
- }
23
+ const signature = decodeHex(getHeader(req, headers_js_1.default.Signature));
27
24
  const hasher = req.contextValues.get(kHash);
28
25
  const tsBuf = Buffer.alloc(8);
29
26
  tsBuf.writeBigUInt64LE(BigInt(ts)); // 64‑bit little‑endian timestamp
30
- const hash = hasher
27
+ const digest = hasher
31
28
  .update(tsBuf)
32
29
  .digest();
33
- let signatureValid = false;
34
- try {
35
- signatureValid = secp256k1_js_1.secp256k1.verify(signature, hash, publicKey, { prehash: false });
36
- }
37
- catch (e) {
38
- throw new connect_1.ConnectError(`${headers_js_1.default.Signature} has invalid signature or public key format: ${e}`, connect_1.Code.Unauthenticated);
39
- }
40
- if (!signatureValid) {
30
+ if (!(0, verify_js_1.verifySignature)(publicKey, digest, signature)) {
41
31
  throw new connect_1.ConnectError(`${headers_js_1.default.Signature} has invalid signature`, connect_1.Code.Unauthenticated);
42
32
  }
43
33
  return await next(req);
44
34
  };
45
- const createService = (networkPublicKey, registerRoutes) => {
35
+ const createService = (networkPublicKey, registerRoutes, options) => {
46
36
  if (typeof networkPublicKey == "string") {
47
37
  networkPublicKey = decodeHex(networkPublicKey);
48
38
  }
@@ -63,7 +53,7 @@ const createService = (networkPublicKey, registerRoutes) => {
63
53
  collected.push(health_pb_js_1.Health.typeName);
64
54
  origService(health_pb_js_1.Health, (0, health_js_1.createHealthServiceImpl)(collected));
65
55
  },
66
- interceptors: [createSignatureVerification(networkPublicKey), (0, validate_response_js_1.createValidationInterceptor)()],
56
+ interceptors: [createSignatureVerification(networkPublicKey), (0, validate_response_js_1.createValidationInterceptor)(options?.logger)],
67
57
  grpcWeb: false,
68
58
  contextValues: (req) => {
69
59
  return (0, connect_1.createContextValues)().set(kHash, req.hasher);
@@ -0,0 +1,25 @@
1
+ import type { DescMessage, MessageShape } from "@bufbuild/protobuf";
2
+ /**
3
+ * Shared protovalidate validator instance for the public {@link validate} helper.
4
+ * Construction is cheap, but reusing one instance avoids repeated compilation
5
+ * of the same rules.
6
+ */
7
+ export declare const validator: import("@bufbuild/protovalidate").Validator;
8
+ /**
9
+ * Validates a response message against its buf.validate proto annotations.
10
+ *
11
+ * On success, returns the message unchanged (typed). On failure, throws a
12
+ * {@link ConnectError} with {@link Code.Internal} and a message matching the
13
+ * shape emitted by the SDK's response-validation interceptor — so propagating
14
+ * the error from a handler produces the same wire response as not calling
15
+ * `validate` at all.
16
+ *
17
+ * Intended use:
18
+ * ```ts
19
+ * return validate(PayOutResponseSchema, { result: { case: "accepted", value: {} } });
20
+ * ```
21
+ *
22
+ * Catch the error to convert it into a domain-level failure (e.g. the `Failed`
23
+ * arm of a `oneof result`) instead of an opaque `Code.Internal`.
24
+ */
25
+ export declare function validate<Desc extends DescMessage>(schema: Desc, msg: MessageShape<Desc>): MessageShape<Desc>;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validator = void 0;
4
+ exports.validate = validate;
5
+ const connect_1 = require("@connectrpc/connect");
6
+ const protovalidate_1 = require("@bufbuild/protovalidate");
7
+ /**
8
+ * Shared protovalidate validator instance for the public {@link validate} helper.
9
+ * Construction is cheap, but reusing one instance avoids repeated compilation
10
+ * of the same rules.
11
+ */
12
+ exports.validator = (0, protovalidate_1.createValidator)();
13
+ /**
14
+ * Validates a response message against its buf.validate proto annotations.
15
+ *
16
+ * On success, returns the message unchanged (typed). On failure, throws a
17
+ * {@link ConnectError} with {@link Code.Internal} and a message matching the
18
+ * shape emitted by the SDK's response-validation interceptor — so propagating
19
+ * the error from a handler produces the same wire response as not calling
20
+ * `validate` at all.
21
+ *
22
+ * Intended use:
23
+ * ```ts
24
+ * return validate(PayOutResponseSchema, { result: { case: "accepted", value: {} } });
25
+ * ```
26
+ *
27
+ * Catch the error to convert it into a domain-level failure (e.g. the `Failed`
28
+ * arm of a `oneof result`) instead of an opaque `Code.Internal`.
29
+ */
30
+ function validate(schema, msg) {
31
+ const result = exports.validator.validate(schema, msg);
32
+ if (result.kind === "invalid") {
33
+ const details = result.violations
34
+ .map((v) => `${v.field?.toString() ?? ""}: ${v.message}`)
35
+ .join("; ");
36
+ throw new connect_1.ConnectError(`response validation failed: ${details}`, connect_1.Code.Internal);
37
+ }
38
+ if (result.kind === "error") {
39
+ throw new connect_1.ConnectError(`response validation error: ${result.error.message}`, connect_1.Code.Internal);
40
+ }
41
+ return msg;
42
+ }
@@ -1,12 +1,26 @@
1
1
  import type { Interceptor } from "@connectrpc/connect";
2
+ /**
3
+ * Minimal logger contract accepted by the SDK. Providers may pass `console`
4
+ * directly, or adapt their preferred logger (e.g. pino) with:
5
+ *
6
+ * { error: (msg, fields) => pinoInstance.error(fields, msg) }
7
+ */
8
+ export interface Logger {
9
+ error(msg: string, fields?: Record<string, unknown>): void;
10
+ }
2
11
  /**
3
12
  * Creates a ConnectRPC interceptor that validates provider responses against
4
13
  * buf.validate proto annotations before they are serialized and sent.
5
14
  * Also validates incoming requests using the official @connectrpc/validate interceptor.
6
15
  *
7
16
  * Invalid requests return Code.InvalidArgument; invalid responses return Code.Internal.
17
+ *
18
+ * On invalid responses, a single structured `error`-level line is emitted to
19
+ * the supplied {@link Logger} (default: `console.error` with JSON-encoded
20
+ * fields) before the `Code.Internal` error is thrown. This is the safety net
21
+ * for handler code paths that skipped the public `validate()` helper.
8
22
  */
9
- export declare function createValidationInterceptor(): Interceptor;
23
+ export declare function createValidationInterceptor(logger?: Logger): Interceptor;
10
24
  /**
11
25
  * @deprecated Use createValidationInterceptor instead.
12
26
  */
@@ -5,15 +5,26 @@ exports.createValidationInterceptor = createValidationInterceptor;
5
5
  const connect_1 = require("@connectrpc/connect");
6
6
  const protovalidate_1 = require("@bufbuild/protovalidate");
7
7
  const validate_1 = require("@connectrpc/validate");
8
+ const version_js_1 = require("../version.js");
8
9
  const validator = (0, protovalidate_1.createValidator)();
10
+ const defaultLogger = {
11
+ error: (msg, fields) =>
12
+ // eslint-disable-next-line no-console
13
+ console.error(JSON.stringify({ msg, ...(fields ?? {}) })),
14
+ };
9
15
  /**
10
16
  * Creates a ConnectRPC interceptor that validates provider responses against
11
17
  * buf.validate proto annotations before they are serialized and sent.
12
18
  * Also validates incoming requests using the official @connectrpc/validate interceptor.
13
19
  *
14
20
  * Invalid requests return Code.InvalidArgument; invalid responses return Code.Internal.
21
+ *
22
+ * On invalid responses, a single structured `error`-level line is emitted to
23
+ * the supplied {@link Logger} (default: `console.error` with JSON-encoded
24
+ * fields) before the `Code.Internal` error is thrown. This is the safety net
25
+ * for handler code paths that skipped the public `validate()` helper.
15
26
  */
16
- function createValidationInterceptor() {
27
+ function createValidationInterceptor(logger = defaultLogger) {
17
28
  const requestInterceptor = (0, validate_1.createValidateInterceptor)();
18
29
  return (next) => async (req) => {
19
30
  // Validate request (delegates to official interceptor which throws on invalid)
@@ -23,10 +34,26 @@ function createValidationInterceptor() {
23
34
  const msg = resp.message;
24
35
  const result = validator.validate(schema, msg);
25
36
  if (result.kind === "invalid") {
26
- const details = result.violations.map(v => `${v.field?.toString() ?? ""}: ${v.message}`).join("; ");
37
+ const violations = result.violations.map((v) => ({
38
+ field: v.field?.toString() ?? "",
39
+ message: v.message,
40
+ }));
41
+ const details = violations.map((v) => `${v.field}: ${v.message}`).join("; ");
42
+ logger.error("response validation failed", {
43
+ rpc_method: `${req.service.typeName}/${req.method.name}`,
44
+ response_type: schema.typeName,
45
+ violations,
46
+ sdk_version: version_js_1.SDK_VERSION,
47
+ });
27
48
  throw new connect_1.ConnectError(`response validation failed: ${details}`, connect_1.Code.Internal);
28
49
  }
29
50
  if (result.kind === "error") {
51
+ logger.error("response validation error", {
52
+ rpc_method: `${req.service.typeName}/${req.method.name}`,
53
+ response_type: schema.typeName,
54
+ error: result.error.message,
55
+ sdk_version: version_js_1.SDK_VERSION,
56
+ });
30
57
  throw new connect_1.ConnectError(`response validation error: ${result.error.message}`, connect_1.Code.Internal);
31
58
  }
32
59
  return resp;
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "1.1.28";
1
+ export declare const SDK_VERSION = "1.1.29";
@@ -3,4 +3,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SDK_VERSION = void 0;
4
4
  // SDK semantic version. Bumped in lockstep with all other SDKs by the
5
5
  // release.yaml workflow.
6
- exports.SDK_VERSION = "1.1.28";
6
+ exports.SDK_VERSION = "1.1.29";
@@ -0,0 +1,2 @@
1
+ export declare function keccak256(...inputs: Uint8Array[]): Buffer;
2
+ export declare function computeDigest(body: Uint8Array, timestampMs: number): Buffer;
@@ -0,0 +1,13 @@
1
+ import { keccak_256 } from '@noble/hashes/sha3.js';
2
+ export function keccak256(...inputs) {
3
+ const h = keccak_256.create();
4
+ for (const input of inputs) {
5
+ h.update(input);
6
+ }
7
+ return Buffer.from(h.digest());
8
+ }
9
+ export function computeDigest(body, timestampMs) {
10
+ const tsBuf = Buffer.alloc(8);
11
+ tsBuf.writeBigUInt64LE(BigInt(timestampMs));
12
+ return keccak256(body, tsBuf);
13
+ }
@@ -0,0 +1,5 @@
1
+ export { verifySignature } from './verify.js';
2
+ export { keccak256, computeDigest } from './hash.js';
3
+ export { parsePublicKey, publicKeysEqual } from './keys.js';
4
+ export { createRequestVerifier, DEFAULT_TOLERANCE_MS } from './request.js';
5
+ export type { CreateVerifierOptions, VerifyRequest, VerifyRequestResult, VerifyRequestFailure, RequestVerifier } from './request.js';
@@ -0,0 +1,4 @@
1
+ export { verifySignature } from './verify.js';
2
+ export { keccak256, computeDigest } from './hash.js';
3
+ export { parsePublicKey, publicKeysEqual } from './keys.js';
4
+ export { createRequestVerifier, DEFAULT_TOLERANCE_MS } from './request.js';
@@ -0,0 +1,2 @@
1
+ export declare function parsePublicKey(key: string | Buffer): Buffer;
2
+ export declare function publicKeysEqual(a: Uint8Array, b: Uint8Array): boolean;
@@ -0,0 +1,15 @@
1
+ export function parsePublicKey(key) {
2
+ if (typeof key === 'string') {
3
+ key = Buffer.from(key.startsWith('0x') ? key.slice(2) : key, 'hex');
4
+ }
5
+ if (key.length !== 65 || key[0] !== 0x04) {
6
+ throw new Error('Public key must be 65 bytes in uncompressed format (0x04 prefix)');
7
+ }
8
+ return Buffer.from(key);
9
+ }
10
+ export function publicKeysEqual(a, b) {
11
+ if (a.length !== b.length) {
12
+ return false;
13
+ }
14
+ return Buffer.from(a).compare(Buffer.from(b)) === 0;
15
+ }
@@ -0,0 +1,20 @@
1
+ export declare const DEFAULT_TOLERANCE_MS = 60000;
2
+ export interface CreateVerifierOptions {
3
+ networkPublicKey: string | Buffer;
4
+ toleranceMs?: number;
5
+ }
6
+ export interface VerifyRequest {
7
+ body: Uint8Array;
8
+ signatureHeader: string;
9
+ publicKeyHeader: string;
10
+ timestampHeader: string;
11
+ }
12
+ export type VerifyRequestFailure = 'invalid_timestamp' | 'timestamp_out_of_range' | 'invalid_public_key' | 'unknown_public_key' | 'invalid_signature_format' | 'signature_failed';
13
+ export type VerifyRequestResult = {
14
+ valid: true;
15
+ } | {
16
+ valid: false;
17
+ reason: VerifyRequestFailure;
18
+ };
19
+ export type RequestVerifier = (req: VerifyRequest) => VerifyRequestResult;
20
+ export declare function createRequestVerifier(opts: CreateVerifierOptions): RequestVerifier;
@@ -0,0 +1,45 @@
1
+ import { verifySignature } from './verify.js';
2
+ import { computeDigest } from './hash.js';
3
+ import { parsePublicKey, publicKeysEqual } from './keys.js';
4
+ export const DEFAULT_TOLERANCE_MS = 60000;
5
+ export function createRequestVerifier(opts) {
6
+ const networkKey = parsePublicKey(opts.networkPublicKey);
7
+ const tolerance = opts.toleranceMs ?? DEFAULT_TOLERANCE_MS;
8
+ return (req) => {
9
+ const ts = parseInt(req.timestampHeader, 10);
10
+ if (!Number.isFinite(ts) || ts < 0) {
11
+ return { valid: false, reason: 'invalid_timestamp' };
12
+ }
13
+ if (Math.abs(Date.now() - ts) > tolerance) {
14
+ return { valid: false, reason: 'timestamp_out_of_range' };
15
+ }
16
+ let publicKey;
17
+ try {
18
+ publicKey = parsePublicKey(req.publicKeyHeader);
19
+ }
20
+ catch {
21
+ return { valid: false, reason: 'invalid_public_key' };
22
+ }
23
+ if (!publicKeysEqual(publicKey, networkKey)) {
24
+ return { valid: false, reason: 'unknown_public_key' };
25
+ }
26
+ let signature;
27
+ try {
28
+ const hex = req.signatureHeader.startsWith('0x')
29
+ ? req.signatureHeader.slice(2)
30
+ : req.signatureHeader;
31
+ signature = Buffer.from(hex, 'hex');
32
+ }
33
+ catch {
34
+ return { valid: false, reason: 'invalid_signature_format' };
35
+ }
36
+ if (signature.length !== 64 && signature.length !== 65) {
37
+ return { valid: false, reason: 'invalid_signature_format' };
38
+ }
39
+ const digest = computeDigest(req.body, ts);
40
+ if (!verifySignature(publicKey, digest, signature)) {
41
+ return { valid: false, reason: 'signature_failed' };
42
+ }
43
+ return { valid: true };
44
+ };
45
+ }
@@ -0,0 +1 @@
1
+ export declare function verifySignature(publicKey: Uint8Array, digest: Uint8Array, signature: Uint8Array): boolean;
@@ -0,0 +1,16 @@
1
+ import { secp256k1 } from '@noble/curves/secp256k1.js';
2
+ export function verifySignature(publicKey, digest, signature) {
3
+ if (digest.length !== 32) {
4
+ return false;
5
+ }
6
+ if (signature.length !== 64 && signature.length !== 65) {
7
+ return false;
8
+ }
9
+ const sig64 = signature.length === 65 ? signature.subarray(0, 64) : signature;
10
+ try {
11
+ return secp256k1.verify(sig64, digest, publicKey, { prehash: false });
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }