@t-0/provider-sdk 1.1.36 → 1.1.37

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
@@ -74,55 +74,82 @@ const server = http.createServer(
74
74
  `signatureValidation` captures raw request bytes for hashing, `nodeAdapter` bridges the RPC transport to Node.js HTTP, and `createService` registers your handlers with signature verification.
75
75
  </details>
76
76
 
77
- ### Standalone Signature Verification
77
+ ### Standalone Request Decoding
78
78
 
79
- 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:
79
+ For frameworks that don't use Node's `http.createServer` (Hono, Effect, Koa, Fastify, etc.), use `createRequestDecoder` for one-call signature verification + Content-Type-aware decoding + protovalidation. It returns an either-type result: success with the decoded message and a response encoder, or failure with a ready-to-send HTTP error.
80
80
 
81
81
  ```ts
82
- import { fromBinary, toBinary } from "@bufbuild/protobuf";
83
- import { createRequestVerifier, rejectRequest, PayoutRequestSchema, PayoutResponseSchema } from "@t-0/provider-sdk";
82
+ import { createRequestDecoder, PayoutRequestSchema, PayoutResponseSchema } from "@t-0/provider-sdk";
84
83
 
85
- const verify = createRequestVerifier({
84
+ const decode = createRequestDecoder({
86
85
  networkPublicKey: process.env.NETWORK_PUBLIC_KEY!,
87
86
  });
88
87
 
89
- // In your framework's request handler:
90
- function handleRequest(rawBody: Uint8Array, headers: Record<string, string>) {
91
- const result = verify({
92
- body: rawBody,
93
- signatureHeader: headers["x-signature"],
94
- publicKeyHeader: headers["x-public-key"],
95
- timestampHeader: headers["x-signature-timestamp"],
96
- });
88
+ // Hono / fetch-shaped framework route by Connect procedure path:
89
+ app.post("/tzero.v1.payment.ProviderService/PayOut", async (c) => {
90
+ const body = new Uint8Array(await c.req.arrayBuffer());
91
+ const result = decode(PayoutRequestSchema, { body, headers: c.req.raw.headers });
97
92
 
98
- if (!result.valid) {
99
- // rejectRequest maps the failure reason to a well-formed HTTP error
100
- // with status (400 or 401), Content-Type header, and JSON body.
101
- const rejected = rejectRequest(result.reason);
102
- return errorResponse(rejected.status, rejected.headers, rejected.body);
93
+ if (!result.ok) {
94
+ return new Response(result.error.body, {
95
+ status: result.error.status,
96
+ headers: result.error.headers,
97
+ });
103
98
  }
104
99
 
105
- // Deserialize the Protobuf request (same raw bytes you verified)
106
- const request = fromBinary(PayoutRequestSchema, rawBody);
107
- const response = handlePayout(request);
100
+ const response = await handlePayout(result.request);
101
+
102
+ // encodeResponse validates + encodes in the matching wire format (JSON or proto)
103
+ const wire = result.encodeResponse(PayoutResponseSchema, response);
104
+ return new Response(wire.body, { status: wire.status, headers: wire.headers });
105
+ });
106
+ ```
107
+
108
+ ```ts
109
+ // Raw Node http example:
110
+ import http from "node:http";
111
+ import { createRequestDecoder, PayoutRequestSchema, PayoutResponseSchema } from "@t-0/provider-sdk";
112
+
113
+ const decode = createRequestDecoder({
114
+ networkPublicKey: process.env.NETWORK_PUBLIC_KEY!,
115
+ });
108
116
 
109
- // Serialize the Protobuf response
110
- return successResponse(toBinary(PayoutResponseSchema, response), {
111
- "content-type": "application/proto",
117
+ http.createServer((req, res) => {
118
+ const chunks: Buffer[] = [];
119
+ req.on("data", (c) => chunks.push(c));
120
+ req.on("end", async () => {
121
+ const body = Buffer.concat(chunks);
122
+ const result = decode(PayoutRequestSchema, { body, headers: req.headers });
123
+
124
+ if (!result.ok) {
125
+ res.writeHead(result.error.status, result.error.headers);
126
+ res.end(result.error.body);
127
+ return;
128
+ }
129
+
130
+ const response = await handlePayout(result.request);
131
+ const wire = result.encodeResponse(PayoutResponseSchema, response);
132
+ res.writeHead(wire.status, wire.headers);
133
+ res.end(wire.body);
112
134
  });
113
- }
135
+ }).listen(3000);
114
136
  ```
115
137
 
116
- The lower-level primitives are also exported individually: `verifySignature`, `computeDigest`, `keccak256`, `parsePublicKey`, `publicKeysEqual`. You can also import just the crypto module via the `./crypto` subpath: `import { createRequestVerifier } from "@t-0/provider-sdk/crypto"`.
138
+ The decoder accepts both fetch `Headers` and Node's `Record<string, string | string[] | undefined>`. It normalizes header case internally, detects Content-Type (`application/json` or `application/proto` / `application/protobuf` / `application/x-protobuf`), and the returned `encodeResponse` closure responds in the matching format.
139
+
140
+ For custom proto registries (e.g. non-network schemas with custom predefined rules), use the generic `createRequestDecoder` from `@t-0/provider-sdk/crypto` and pass your own `registry`.
117
141
 
118
142
  **Important constraints for standalone integrations:**
119
143
 
120
- - **Raw body bytes only.** Pass the exact wire bytes to the verifier — no body parsers, no auto-decompression, never re-serialized protobuf. Protobuf encoding is not canonical; re-encoding produces different bytes and breaks verification.
121
- - **Pass `Uint8Array`, not `ArrayBuffer`.** If your framework gives you an `ArrayBuffer` (e.g. `request.arrayBuffer()`), wrap it: `new Uint8Array(buf)`.
122
- - **Header case.** `NetworkHeaders` enum values are title-case (`X-Signature`), but Node lowercases incoming headers. Look up headers by lowercase name: `headers["x-signature"]`.
123
- - **Wire format is binary protobuf.** Requests and successful responses use `Content-Type: application/proto`. Deserialize with `fromBinary()`, serialize with `toBinary()` from `@bufbuild/protobuf`. For verification errors, use `rejectRequest(result.reason)` to get the correct HTTP status, headers, and JSON body. Success responses **must** include `Content-Type: application/proto`.
144
+ - **Raw body bytes only.** Pass the exact wire bytes — no body parsers, no auto-decompression, never re-serialized protobuf. Protobuf encoding is not canonical; re-encoding produces different bytes and breaks verification.
124
145
  - **Health endpoint.** The T-0 Network probes `/grpc.health.v1.Health/Check` on every endpoint. The probe is signed. Standalone integrations must route this path and return a valid health response. See [`docs/HEALTH_SERVICE.md`](../../docs/HEALTH_SERVICE.md) for the wire contract.
125
- - **`VerifyRequestFailure` is an open union.** New reason values may be added without a major version bump. Handle unknown reasons as generic failures.
146
+ - **`DecodeRequestFailure` is an open union.** New error shapes may be added without a major version bump. Handle unknown failures as generic errors.
147
+
148
+ <details>
149
+ <summary>Lower-level primitives</summary>
150
+
151
+ The individual building blocks are also exported: `createRequestVerifier`, `rejectRequest`, `verifySignature`, `computeDigest`, `keccak256`, `parsePublicKey`, `publicKeysEqual`. You can import just the crypto module via the `./crypto` subpath: `import { createRequestVerifier } from "@t-0/provider-sdk/crypto"`.
152
+ </details>
126
153
 
127
154
  ### Network Client
128
155
 
@@ -0,0 +1,41 @@
1
+ import type { DescMessage, MessageShape, Registry } from '@bufbuild/protobuf';
2
+ import type { CreateVerifierOptions, RejectedRequest } from './request.js';
3
+ export interface CreateDecoderOptions extends CreateVerifierOptions {
4
+ registry?: Registry;
5
+ }
6
+ export type IncomingHeaders = {
7
+ get(name: string): string | null;
8
+ } | Record<string, string | string[] | undefined>;
9
+ export interface IncomingRequest {
10
+ body: Uint8Array | ArrayBufferView | ArrayBufferLike;
11
+ headers: IncomingHeaders;
12
+ }
13
+ export type WireFormat = 'json' | 'proto';
14
+ export interface Violation {
15
+ field: string;
16
+ message: string;
17
+ }
18
+ export type DecodeError = 'unsupported_content_type' | 'malformed_body' | 'invalid_request' | 'validation_error';
19
+ export interface WireResponse {
20
+ status: number;
21
+ headers: Record<string, string>;
22
+ body: string | Uint8Array<ArrayBuffer>;
23
+ }
24
+ export type DecodeRequestFailure = RejectedRequest | {
25
+ status: number;
26
+ headers: Record<string, string>;
27
+ body: string;
28
+ error: DecodeError;
29
+ violations?: Violation[];
30
+ };
31
+ export type DecodeRequestResult<Desc extends DescMessage> = {
32
+ ok: true;
33
+ request: MessageShape<Desc>;
34
+ format: WireFormat;
35
+ encodeResponse: <R extends DescMessage>(schema: R, message: MessageShape<R>) => WireResponse;
36
+ } | {
37
+ ok: false;
38
+ error: DecodeRequestFailure;
39
+ };
40
+ export type RequestDecoder = <Desc extends DescMessage>(schema: Desc, req: IncomingRequest) => DecodeRequestResult<Desc>;
41
+ export declare function createRequestDecoder(opts: CreateDecoderOptions): RequestDecoder;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createRequestDecoder = createRequestDecoder;
7
+ const protobuf_1 = require("@bufbuild/protobuf");
8
+ const protovalidate_1 = require("@bufbuild/protovalidate");
9
+ const request_js_1 = require("./request.js");
10
+ const headers_js_1 = __importDefault(require("../headers.js"));
11
+ function getHeader(headers, name) {
12
+ if (typeof headers.get === 'function') {
13
+ return headers.get(name) ?? '';
14
+ }
15
+ const rec = headers;
16
+ const lc = name.toLowerCase();
17
+ for (const key of Object.keys(rec)) {
18
+ if (key.toLowerCase() === lc) {
19
+ const v = rec[key];
20
+ if (Array.isArray(v))
21
+ return v[0] ?? '';
22
+ return v ?? '';
23
+ }
24
+ }
25
+ return '';
26
+ }
27
+ function normalizeBody(body) {
28
+ if (body instanceof Uint8Array)
29
+ return body;
30
+ if (ArrayBuffer.isView(body))
31
+ return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
32
+ return new Uint8Array(body);
33
+ }
34
+ function detectFormat(contentType) {
35
+ const base = contentType.split(';')[0].trim().toLowerCase();
36
+ if (base === 'application/json')
37
+ return 'json';
38
+ if (base === 'application/proto' || base === 'application/protobuf' || base === 'application/x-protobuf')
39
+ return 'proto';
40
+ return null;
41
+ }
42
+ function failResponse(status, code, message, error, violations) {
43
+ return {
44
+ status,
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify(violations ? { code, message, violations } : { code, message }),
47
+ error,
48
+ violations,
49
+ };
50
+ }
51
+ function createRequestDecoder(opts) {
52
+ const verify = (0, request_js_1.createRequestVerifier)(opts);
53
+ const validator = (0, protovalidate_1.createValidator)(opts.registry ? { registry: opts.registry } : undefined);
54
+ const textDecoder = new TextDecoder('utf-8', { fatal: true });
55
+ return (schema, req) => {
56
+ const body = normalizeBody(req.body);
57
+ const sigResult = verify({
58
+ body,
59
+ signatureHeader: getHeader(req.headers, headers_js_1.default.Signature),
60
+ publicKeyHeader: getHeader(req.headers, headers_js_1.default.PublicKey),
61
+ timestampHeader: getHeader(req.headers, headers_js_1.default.SignatureTimestamp),
62
+ });
63
+ if (!sigResult.valid) {
64
+ return { ok: false, error: (0, request_js_1.rejectRequest)(sigResult.reason) };
65
+ }
66
+ const format = detectFormat(getHeader(req.headers, 'content-type'));
67
+ if (!format) {
68
+ return { ok: false, error: failResponse(415, 'unsupported_content_type', 'Unsupported Content-Type', 'unsupported_content_type') };
69
+ }
70
+ let message;
71
+ try {
72
+ if (format === 'json') {
73
+ message = (0, protobuf_1.fromJsonString)(schema, textDecoder.decode(body), { ignoreUnknownFields: true, registry: opts.registry });
74
+ }
75
+ else {
76
+ message = (0, protobuf_1.fromBinary)(schema, body);
77
+ }
78
+ }
79
+ catch {
80
+ return { ok: false, error: failResponse(400, 'invalid_argument', 'Malformed request body', 'malformed_body') };
81
+ }
82
+ const valResult = validator.validate(schema, message);
83
+ if (valResult.kind === 'invalid') {
84
+ const violations = valResult.violations.map(v => ({
85
+ field: v.field?.toString() ?? '',
86
+ message: v.message,
87
+ }));
88
+ return {
89
+ ok: false,
90
+ error: failResponse(400, 'invalid_argument', 'Request validation failed', 'invalid_request', violations),
91
+ };
92
+ }
93
+ if (valResult.kind === 'error') {
94
+ return { ok: false, error: failResponse(500, 'internal', `Validation error: ${valResult.error.message}`, 'validation_error') };
95
+ }
96
+ const encodeResponse = (respSchema, resp) => {
97
+ const respVal = validator.validate(respSchema, resp);
98
+ if (respVal.kind === 'invalid' || respVal.kind === 'error') {
99
+ return {
100
+ status: 500,
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ code: 'internal', message: 'Response validation failed' }),
103
+ };
104
+ }
105
+ if (format === 'json') {
106
+ return {
107
+ status: 200,
108
+ headers: { 'Content-Type': 'application/json' },
109
+ body: (0, protobuf_1.toJsonString)(respSchema, resp, { registry: opts.registry }),
110
+ };
111
+ }
112
+ return {
113
+ status: 200,
114
+ headers: { 'Content-Type': 'application/proto' },
115
+ body: (0, protobuf_1.toBinary)(respSchema, resp),
116
+ };
117
+ };
118
+ return { ok: true, request: message, format, encodeResponse };
119
+ };
120
+ }
@@ -3,3 +3,4 @@ export { keccak256, computeDigest } from './hash.js';
3
3
  export { parsePublicKey, publicKeysEqual } from './keys.js';
4
4
  export { createRequestVerifier, DEFAULT_TOLERANCE_MS, rejectRequest } from './request.js';
5
5
  export type { CreateVerifierOptions, VerifyRequest, VerifyRequestResult, VerifyRequestFailure, RequestVerifier, RejectedRequest } from './request.js';
6
+ export type { CreateDecoderOptions, IncomingHeaders, IncomingRequest, WireFormat, DecodeRequestFailure, Violation, WireResponse, DecodeError, DecodeRequestResult, RequestDecoder } from './decode.js';
@@ -1 +1,2 @@
1
1
  export * from "../common/crypto/index.js";
2
+ export { createRequestDecoder } from "../common/crypto/decode.js";
@@ -14,4 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.createRequestDecoder = void 0;
17
18
  __exportStar(require("../common/crypto/index.js"), exports);
19
+ var decode_js_1 = require("../common/crypto/decode.js");
20
+ Object.defineProperty(exports, "createRequestDecoder", { enumerable: true, get: function () { return decode_js_1.createRequestDecoder; } });
@@ -1,4 +1,5 @@
1
- export * from "./crypto/index.js";
1
+ export { verifySignature, keccak256, computeDigest, parsePublicKey, publicKeysEqual, createRequestVerifier, DEFAULT_TOLERANCE_MS, rejectRequest } from "./crypto/index.js";
2
+ export type { CreateVerifierOptions, VerifyRequest, VerifyRequestResult, VerifyRequestFailure, RequestVerifier, RejectedRequest, CreateDecoderOptions, IncomingHeaders, IncomingRequest, WireFormat, DecodeRequestFailure, Violation, WireResponse, DecodeError, DecodeRequestResult, RequestDecoder } from "./crypto/index.js";
2
3
  export * from "./client/client.js";
3
4
  export * from "./service/service.js";
4
5
  export * from "./common/validation.js";
package/lib/cjs/index.js CHANGED
@@ -39,8 +39,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  return (mod && mod.__esModule) ? mod : { "default": mod };
40
40
  };
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.PaymentIntentRecipient = exports.PaymentIntentProvider = exports.PaymentIntentBeneficiary = exports.PaymentIntentPayInProvider = exports.PaymentIntentNetwork = exports.nodeAdapter = exports.NetworkHeaders = void 0;
43
- __exportStar(require("./crypto/index.js"), exports);
42
+ exports.PaymentIntentRecipient = exports.PaymentIntentProvider = exports.PaymentIntentBeneficiary = exports.PaymentIntentPayInProvider = exports.PaymentIntentNetwork = exports.nodeAdapter = exports.NetworkHeaders = exports.rejectRequest = exports.DEFAULT_TOLERANCE_MS = exports.createRequestVerifier = exports.publicKeysEqual = exports.parsePublicKey = exports.computeDigest = exports.keccak256 = exports.verifySignature = void 0;
43
+ var index_js_1 = require("./crypto/index.js");
44
+ Object.defineProperty(exports, "verifySignature", { enumerable: true, get: function () { return index_js_1.verifySignature; } });
45
+ Object.defineProperty(exports, "keccak256", { enumerable: true, get: function () { return index_js_1.keccak256; } });
46
+ Object.defineProperty(exports, "computeDigest", { enumerable: true, get: function () { return index_js_1.computeDigest; } });
47
+ Object.defineProperty(exports, "parsePublicKey", { enumerable: true, get: function () { return index_js_1.parsePublicKey; } });
48
+ Object.defineProperty(exports, "publicKeysEqual", { enumerable: true, get: function () { return index_js_1.publicKeysEqual; } });
49
+ Object.defineProperty(exports, "createRequestVerifier", { enumerable: true, get: function () { return index_js_1.createRequestVerifier; } });
50
+ Object.defineProperty(exports, "DEFAULT_TOLERANCE_MS", { enumerable: true, get: function () { return index_js_1.DEFAULT_TOLERANCE_MS; } });
51
+ Object.defineProperty(exports, "rejectRequest", { enumerable: true, get: function () { return index_js_1.rejectRequest; } });
44
52
  __exportStar(require("./client/client.js"), exports);
45
53
  __exportStar(require("./service/service.js"), exports);
46
54
  __exportStar(require("./common/validation.js"), exports);
@@ -1,6 +1,8 @@
1
1
  import type { Interceptor } from "@connectrpc/connect";
2
2
  import type { Logger } from "../common/validation.js";
3
3
  import { createValidationInterceptor } from "../common/validation.js";
4
+ import type { RequestDecoder } from "../common/crypto/decode.js";
5
+ import type { CreateVerifierOptions } from "../common/crypto/request.js";
4
6
  export { createValidationInterceptor, type Logger, type ValidationInterceptorOptions } from "../common/validation.js";
5
7
  /**
6
8
  * Registry covering the t-0 network provider contract protos. Leaf file
@@ -12,6 +14,10 @@ export declare const networkRegistry: import("@bufbuild/protobuf").Registry;
12
14
  * Validation interceptor pre-configured for the t-0 network provider contract.
13
15
  */
14
16
  export declare function createNetworkValidationInterceptor(logger?: Logger): Interceptor;
17
+ /**
18
+ * Request decoder pre-configured for the t-0 network provider contract.
19
+ */
20
+ export declare function createRequestDecoder(opts: CreateVerifierOptions): RequestDecoder;
15
21
  /**
16
22
  * @deprecated Use createValidationInterceptor instead.
17
23
  */
@@ -2,10 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createResponseValidation = exports.networkRegistry = exports.createValidationInterceptor = void 0;
4
4
  exports.createNetworkValidationInterceptor = createNetworkValidationInterceptor;
5
+ exports.createRequestDecoder = createRequestDecoder;
5
6
  const protobuf_1 = require("@bufbuild/protobuf");
6
7
  const provider_pb_js_1 = require("../common/gen/tzero/v1/payment/provider_pb.js");
7
8
  const network_pb_js_1 = require("../common/gen/tzero/v1/payment/network_pb.js");
8
9
  const validation_js_1 = require("../common/validation.js");
10
+ const decode_js_1 = require("../common/crypto/decode.js");
9
11
  // Re-export everything from the common layer for backward compatibility.
10
12
  var validation_js_2 = require("../common/validation.js");
11
13
  Object.defineProperty(exports, "createValidationInterceptor", { enumerable: true, get: function () { return validation_js_2.createValidationInterceptor; } });
@@ -21,6 +23,12 @@ exports.networkRegistry = (0, protobuf_1.createRegistry)(provider_pb_js_1.file_t
21
23
  function createNetworkValidationInterceptor(logger) {
22
24
  return (0, validation_js_1.createValidationInterceptor)({ logger, registry: exports.networkRegistry });
23
25
  }
26
+ /**
27
+ * Request decoder pre-configured for the t-0 network provider contract.
28
+ */
29
+ function createRequestDecoder(opts) {
30
+ return (0, decode_js_1.createRequestDecoder)({ ...opts, registry: exports.networkRegistry });
31
+ }
24
32
  /**
25
33
  * @deprecated Use createValidationInterceptor instead.
26
34
  */
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "1.1.36";
1
+ export declare const SDK_VERSION = "1.1.37";
@@ -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.36";
6
+ exports.SDK_VERSION = "1.1.37";
@@ -0,0 +1,41 @@
1
+ import type { DescMessage, MessageShape, Registry } from '@bufbuild/protobuf';
2
+ import type { CreateVerifierOptions, RejectedRequest } from './request.js';
3
+ export interface CreateDecoderOptions extends CreateVerifierOptions {
4
+ registry?: Registry;
5
+ }
6
+ export type IncomingHeaders = {
7
+ get(name: string): string | null;
8
+ } | Record<string, string | string[] | undefined>;
9
+ export interface IncomingRequest {
10
+ body: Uint8Array | ArrayBufferView | ArrayBufferLike;
11
+ headers: IncomingHeaders;
12
+ }
13
+ export type WireFormat = 'json' | 'proto';
14
+ export interface Violation {
15
+ field: string;
16
+ message: string;
17
+ }
18
+ export type DecodeError = 'unsupported_content_type' | 'malformed_body' | 'invalid_request' | 'validation_error';
19
+ export interface WireResponse {
20
+ status: number;
21
+ headers: Record<string, string>;
22
+ body: string | Uint8Array<ArrayBuffer>;
23
+ }
24
+ export type DecodeRequestFailure = RejectedRequest | {
25
+ status: number;
26
+ headers: Record<string, string>;
27
+ body: string;
28
+ error: DecodeError;
29
+ violations?: Violation[];
30
+ };
31
+ export type DecodeRequestResult<Desc extends DescMessage> = {
32
+ ok: true;
33
+ request: MessageShape<Desc>;
34
+ format: WireFormat;
35
+ encodeResponse: <R extends DescMessage>(schema: R, message: MessageShape<R>) => WireResponse;
36
+ } | {
37
+ ok: false;
38
+ error: DecodeRequestFailure;
39
+ };
40
+ export type RequestDecoder = <Desc extends DescMessage>(schema: Desc, req: IncomingRequest) => DecodeRequestResult<Desc>;
41
+ export declare function createRequestDecoder(opts: CreateDecoderOptions): RequestDecoder;
@@ -0,0 +1,114 @@
1
+ import { fromJsonString, fromBinary, toJsonString, toBinary } from '@bufbuild/protobuf';
2
+ import { createValidator } from '@bufbuild/protovalidate';
3
+ import { createRequestVerifier, rejectRequest } from './request.js';
4
+ import NetworkHeaders from '../headers.js';
5
+ function getHeader(headers, name) {
6
+ if (typeof headers.get === 'function') {
7
+ return headers.get(name) ?? '';
8
+ }
9
+ const rec = headers;
10
+ const lc = name.toLowerCase();
11
+ for (const key of Object.keys(rec)) {
12
+ if (key.toLowerCase() === lc) {
13
+ const v = rec[key];
14
+ if (Array.isArray(v))
15
+ return v[0] ?? '';
16
+ return v ?? '';
17
+ }
18
+ }
19
+ return '';
20
+ }
21
+ function normalizeBody(body) {
22
+ if (body instanceof Uint8Array)
23
+ return body;
24
+ if (ArrayBuffer.isView(body))
25
+ return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
26
+ return new Uint8Array(body);
27
+ }
28
+ function detectFormat(contentType) {
29
+ const base = contentType.split(';')[0].trim().toLowerCase();
30
+ if (base === 'application/json')
31
+ return 'json';
32
+ if (base === 'application/proto' || base === 'application/protobuf' || base === 'application/x-protobuf')
33
+ return 'proto';
34
+ return null;
35
+ }
36
+ function failResponse(status, code, message, error, violations) {
37
+ return {
38
+ status,
39
+ headers: { 'Content-Type': 'application/json' },
40
+ body: JSON.stringify(violations ? { code, message, violations } : { code, message }),
41
+ error,
42
+ violations,
43
+ };
44
+ }
45
+ export function createRequestDecoder(opts) {
46
+ const verify = createRequestVerifier(opts);
47
+ const validator = createValidator(opts.registry ? { registry: opts.registry } : undefined);
48
+ const textDecoder = new TextDecoder('utf-8', { fatal: true });
49
+ return (schema, req) => {
50
+ const body = normalizeBody(req.body);
51
+ const sigResult = verify({
52
+ body,
53
+ signatureHeader: getHeader(req.headers, NetworkHeaders.Signature),
54
+ publicKeyHeader: getHeader(req.headers, NetworkHeaders.PublicKey),
55
+ timestampHeader: getHeader(req.headers, NetworkHeaders.SignatureTimestamp),
56
+ });
57
+ if (!sigResult.valid) {
58
+ return { ok: false, error: rejectRequest(sigResult.reason) };
59
+ }
60
+ const format = detectFormat(getHeader(req.headers, 'content-type'));
61
+ if (!format) {
62
+ return { ok: false, error: failResponse(415, 'unsupported_content_type', 'Unsupported Content-Type', 'unsupported_content_type') };
63
+ }
64
+ let message;
65
+ try {
66
+ if (format === 'json') {
67
+ message = fromJsonString(schema, textDecoder.decode(body), { ignoreUnknownFields: true, registry: opts.registry });
68
+ }
69
+ else {
70
+ message = fromBinary(schema, body);
71
+ }
72
+ }
73
+ catch {
74
+ return { ok: false, error: failResponse(400, 'invalid_argument', 'Malformed request body', 'malformed_body') };
75
+ }
76
+ const valResult = validator.validate(schema, message);
77
+ if (valResult.kind === 'invalid') {
78
+ const violations = valResult.violations.map(v => ({
79
+ field: v.field?.toString() ?? '',
80
+ message: v.message,
81
+ }));
82
+ return {
83
+ ok: false,
84
+ error: failResponse(400, 'invalid_argument', 'Request validation failed', 'invalid_request', violations),
85
+ };
86
+ }
87
+ if (valResult.kind === 'error') {
88
+ return { ok: false, error: failResponse(500, 'internal', `Validation error: ${valResult.error.message}`, 'validation_error') };
89
+ }
90
+ const encodeResponse = (respSchema, resp) => {
91
+ const respVal = validator.validate(respSchema, resp);
92
+ if (respVal.kind === 'invalid' || respVal.kind === 'error') {
93
+ return {
94
+ status: 500,
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: JSON.stringify({ code: 'internal', message: 'Response validation failed' }),
97
+ };
98
+ }
99
+ if (format === 'json') {
100
+ return {
101
+ status: 200,
102
+ headers: { 'Content-Type': 'application/json' },
103
+ body: toJsonString(respSchema, resp, { registry: opts.registry }),
104
+ };
105
+ }
106
+ return {
107
+ status: 200,
108
+ headers: { 'Content-Type': 'application/proto' },
109
+ body: toBinary(respSchema, resp),
110
+ };
111
+ };
112
+ return { ok: true, request: message, format, encodeResponse };
113
+ };
114
+ }
@@ -3,3 +3,4 @@ export { keccak256, computeDigest } from './hash.js';
3
3
  export { parsePublicKey, publicKeysEqual } from './keys.js';
4
4
  export { createRequestVerifier, DEFAULT_TOLERANCE_MS, rejectRequest } from './request.js';
5
5
  export type { CreateVerifierOptions, VerifyRequest, VerifyRequestResult, VerifyRequestFailure, RequestVerifier, RejectedRequest } from './request.js';
6
+ export type { CreateDecoderOptions, IncomingHeaders, IncomingRequest, WireFormat, DecodeRequestFailure, Violation, WireResponse, DecodeError, DecodeRequestResult, RequestDecoder } from './decode.js';
@@ -1 +1,2 @@
1
1
  export * from "../common/crypto/index.js";
2
+ export { createRequestDecoder } from "../common/crypto/decode.js";
@@ -1 +1,2 @@
1
1
  export * from "../common/crypto/index.js";
2
+ export { createRequestDecoder } from "../common/crypto/decode.js";
@@ -1,4 +1,5 @@
1
- export * from "./crypto/index.js";
1
+ export { verifySignature, keccak256, computeDigest, parsePublicKey, publicKeysEqual, createRequestVerifier, DEFAULT_TOLERANCE_MS, rejectRequest } from "./crypto/index.js";
2
+ export type { CreateVerifierOptions, VerifyRequest, VerifyRequestResult, VerifyRequestFailure, RequestVerifier, RejectedRequest, CreateDecoderOptions, IncomingHeaders, IncomingRequest, WireFormat, DecodeRequestFailure, Violation, WireResponse, DecodeError, DecodeRequestResult, RequestDecoder } from "./crypto/index.js";
2
3
  export * from "./client/client.js";
3
4
  export * from "./service/service.js";
4
5
  export * from "./common/validation.js";
package/lib/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export * from "./crypto/index.js";
1
+ export { verifySignature, keccak256, computeDigest, parsePublicKey, publicKeysEqual, createRequestVerifier, DEFAULT_TOLERANCE_MS, rejectRequest } from "./crypto/index.js";
2
2
  export * from "./client/client.js";
3
3
  export * from "./service/service.js";
4
4
  export * from "./common/validation.js";
@@ -1,6 +1,8 @@
1
1
  import type { Interceptor } from "@connectrpc/connect";
2
2
  import type { Logger } from "../common/validation.js";
3
3
  import { createValidationInterceptor } from "../common/validation.js";
4
+ import type { RequestDecoder } from "../common/crypto/decode.js";
5
+ import type { CreateVerifierOptions } from "../common/crypto/request.js";
4
6
  export { createValidationInterceptor, type Logger, type ValidationInterceptorOptions } from "../common/validation.js";
5
7
  /**
6
8
  * Registry covering the t-0 network provider contract protos. Leaf file
@@ -12,6 +14,10 @@ export declare const networkRegistry: import("@bufbuild/protobuf").Registry;
12
14
  * Validation interceptor pre-configured for the t-0 network provider contract.
13
15
  */
14
16
  export declare function createNetworkValidationInterceptor(logger?: Logger): Interceptor;
17
+ /**
18
+ * Request decoder pre-configured for the t-0 network provider contract.
19
+ */
20
+ export declare function createRequestDecoder(opts: CreateVerifierOptions): RequestDecoder;
15
21
  /**
16
22
  * @deprecated Use createValidationInterceptor instead.
17
23
  */
@@ -2,6 +2,7 @@ import { createRegistry } from "@bufbuild/protobuf";
2
2
  import { file_tzero_v1_payment_provider } from "../common/gen/tzero/v1/payment/provider_pb.js";
3
3
  import { file_tzero_v1_payment_network } from "../common/gen/tzero/v1/payment/network_pb.js";
4
4
  import { createValidationInterceptor } from "../common/validation.js";
5
+ import { createRequestDecoder as createBaseRequestDecoder } from "../common/crypto/decode.js";
5
6
  // Re-export everything from the common layer for backward compatibility.
6
7
  export { createValidationInterceptor } from "../common/validation.js";
7
8
  /**
@@ -16,6 +17,12 @@ export const networkRegistry = createRegistry(file_tzero_v1_payment_provider, fi
16
17
  export function createNetworkValidationInterceptor(logger) {
17
18
  return createValidationInterceptor({ logger, registry: networkRegistry });
18
19
  }
20
+ /**
21
+ * Request decoder pre-configured for the t-0 network provider contract.
22
+ */
23
+ export function createRequestDecoder(opts) {
24
+ return createBaseRequestDecoder({ ...opts, registry: networkRegistry });
25
+ }
19
26
  /**
20
27
  * @deprecated Use createValidationInterceptor instead.
21
28
  */
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "1.1.36";
1
+ export declare const SDK_VERSION = "1.1.37";
@@ -1,3 +1,3 @@
1
1
  // SDK semantic version. Bumped in lockstep with all other SDKs by the
2
2
  // release.yaml workflow.
3
- export const SDK_VERSION = "1.1.36";
3
+ export const SDK_VERSION = "1.1.37";