kxco-post-quantum 1.3.0 → 1.4.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/src/slh-dsa.d.ts CHANGED
@@ -1,72 +1,72 @@
1
- /// <reference types="node" />
2
-
3
- export interface SlhDsaKeypair {
4
- /** 48-byte public key */
5
- publicKey: Buffer
6
- /** 96-byte secret key */
7
- secretKey: Buffer
8
- }
9
-
10
- /**
11
- * Generate an SLH-DSA-SHA2-192s (NIST FIPS 205) keypair deterministically
12
- * from a master + domain-separation info string.
13
- *
14
- * Same inputs always produce the same keypair — no state, no DB row.
15
- * Security Category 3, matching ML-DSA-65.
16
- */
17
- export function keypairFromMaster(
18
- master: Buffer | Uint8Array,
19
- info?: string,
20
- ): SlhDsaKeypair
21
-
22
- /** Maximum context length in bytes (FIPS 205, matching FIPS 204). */
23
- export const MAX_CONTEXT_BYTES: 255
24
-
25
- export interface SignatureOptions {
26
- /**
27
- * Optional context string, at most 255 bytes. Strings are encoded as UTF-8.
28
- * A signature made under a context does not verify without it. An empty
29
- * context is identical to omitting it.
30
- */
31
- context?: Buffer | Uint8Array | string
32
- }
33
-
34
- /**
35
- * Sign a message under an SLH-DSA-SHA2-192s secret key. Returns the signature
36
- * as a hex string (16224 bytes = 32448 hex characters).
37
- *
38
- * @throws {TypeError} if `opts` is not an options object, or `context` is
39
- * neither a string nor a Uint8Array
40
- * @throws {RangeError} if `context` exceeds 255 bytes
41
- */
42
- export function sign(
43
- secretKey: Buffer | Uint8Array,
44
- message: Buffer | Uint8Array | string,
45
- opts?: SignatureOptions,
46
- ): string
47
-
48
- /**
49
- * Verify a hex-encoded SLH-DSA-SHA2-192s signature against a public key +
50
- * message.
51
- *
52
- * Returns `false` on any cryptographic failure (invalid hex, wrong length,
53
- * mismatch, or a missing/incorrect context). Throws only on caller misuse of
54
- * `opts`.
55
- *
56
- * @throws {TypeError} if `opts` is not an options object, or `context` is
57
- * neither a string nor a Uint8Array
58
- * @throws {RangeError} if `context` exceeds 255 bytes
59
- */
60
- export function verify(
61
- publicKey: Buffer | Uint8Array,
62
- message: Buffer | Uint8Array | string,
63
- sigHex: string,
64
- opts?: SignatureOptions,
65
- ): boolean
66
-
67
- /**
68
- * Raw `@noble/post-quantum` SLH-DSA-SHA2-192s primitive, re-exported for
69
- * callers who want the lower-level API. The wrapper functions above are
70
- * recommended for production use.
71
- */
72
- export const slh_dsa_sha2_192s: typeof import('@noble/post-quantum/slh-dsa.js').slh_dsa_sha2_192s
1
+ /// <reference types="node" />
2
+
3
+ export interface SlhDsaKeypair {
4
+ /** 48-byte public key */
5
+ publicKey: Buffer
6
+ /** 96-byte secret key */
7
+ secretKey: Buffer
8
+ }
9
+
10
+ /**
11
+ * Generate an SLH-DSA-SHA2-192s (NIST FIPS 205) keypair deterministically
12
+ * from a master + domain-separation info string.
13
+ *
14
+ * Same inputs always produce the same keypair — no state, no DB row.
15
+ * Security Category 3, matching ML-DSA-65.
16
+ */
17
+ export function keypairFromMaster(
18
+ master: Buffer | Uint8Array,
19
+ info?: string,
20
+ ): SlhDsaKeypair
21
+
22
+ /** Maximum context length in bytes (FIPS 205, matching FIPS 204). */
23
+ export const MAX_CONTEXT_BYTES: 255
24
+
25
+ export interface SignatureOptions {
26
+ /**
27
+ * Optional context string, at most 255 bytes. Strings are encoded as UTF-8.
28
+ * A signature made under a context does not verify without it. An empty
29
+ * context is identical to omitting it.
30
+ */
31
+ context?: Buffer | Uint8Array | string
32
+ }
33
+
34
+ /**
35
+ * Sign a message under an SLH-DSA-SHA2-192s secret key. Returns the signature
36
+ * as a hex string (16224 bytes = 32448 hex characters).
37
+ *
38
+ * @throws {TypeError} if `opts` is not an options object, or `context` is
39
+ * neither a string nor a Uint8Array
40
+ * @throws {RangeError} if `context` exceeds 255 bytes
41
+ */
42
+ export function sign(
43
+ secretKey: Buffer | Uint8Array,
44
+ message: Buffer | Uint8Array | string,
45
+ opts?: SignatureOptions,
46
+ ): string
47
+
48
+ /**
49
+ * Verify a hex-encoded SLH-DSA-SHA2-192s signature against a public key +
50
+ * message.
51
+ *
52
+ * Returns `false` on any cryptographic failure (invalid hex, wrong length,
53
+ * mismatch, or a missing/incorrect context). Throws only on caller misuse of
54
+ * `opts`.
55
+ *
56
+ * @throws {TypeError} if `opts` is not an options object, or `context` is
57
+ * neither a string nor a Uint8Array
58
+ * @throws {RangeError} if `context` exceeds 255 bytes
59
+ */
60
+ export function verify(
61
+ publicKey: Buffer | Uint8Array,
62
+ message: Buffer | Uint8Array | string,
63
+ sigHex: string,
64
+ opts?: SignatureOptions,
65
+ ): boolean
66
+
67
+ /**
68
+ * Raw `@noble/post-quantum` SLH-DSA-SHA2-192s primitive, re-exported for
69
+ * callers who want the lower-level API. The wrapper functions above are
70
+ * recommended for production use.
71
+ */
72
+ export const slh_dsa_sha2_192s: typeof import('@noble/post-quantum/slh-dsa.js').slh_dsa_sha2_192s
package/src/slh-dsa.js CHANGED
@@ -1,106 +1,106 @@
1
- // SLH-DSA-SHA2-192s helpers (NIST FIPS 205, SPHINCS+).
2
- //
3
- // Stateless hash-based signatures. Security Category 3 (≈ AES-192), matching
4
- // ML-DSA-65's security level. Public key 48 bytes, secret key 96 bytes,
5
- // signature 16224 bytes. Security rests only on the SHA-2 hash function — no
6
- // lattice or number-theoretic assumptions — which makes it the conservative
7
- // hedge alongside ML-DSA-65.
8
- //
9
- // Tradeoff: signatures are ~5x larger than ML-DSA-65 (16224 vs 3309 bytes) and
10
- // signing is slower. Use ML-DSA-65 as the default; reach for SLH-DSA when you
11
- // want a signature whose security does not depend on lattice hardness.
12
- //
13
- // Isomorphic: works in Node and modern browsers. Returns Buffer on Node
14
- // (consistent with ml-dsa.js), Uint8Array in browsers.
15
-
16
- import { slh_dsa_sha2_192s } from '@noble/post-quantum/slh-dsa.js'
17
- import { deriveSeed } from './derive.js'
18
- import { normalizeContext, MAX_CONTEXT_BYTES } from './_context.js'
19
-
20
- export { MAX_CONTEXT_BYTES }
21
-
22
- const HAS_BUFFER = typeof Buffer !== 'undefined'
23
- const enc = new TextEncoder()
24
-
25
- function toBytes(input) {
26
- if (input instanceof Uint8Array) return input
27
- if (typeof input === 'string') return enc.encode(input)
28
- throw new Error('expected Uint8Array or string')
29
- }
30
- function hexToBytes(hex) {
31
- if (typeof hex !== 'string' || hex.length % 2) throw new Error('invalid hex')
32
- const b = new Uint8Array(hex.length / 2)
33
- for (let i = 0; i < b.length; i++) b[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
34
- return b
35
- }
36
- function bytesToHex(bytes) {
37
- let s = ''
38
- for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, '0')
39
- return s
40
- }
41
- function wrap(bytes) {
42
- return HAS_BUFFER ? Buffer.from(bytes) : bytes
43
- }
44
-
45
- // Seed length for SLH-DSA-SHA2-192s keygen (FIPS 205: SK.seed || SK.prf || PK.seed).
46
- const SEED_BYTES = slh_dsa_sha2_192s.lengths.seed
47
-
48
- /**
49
- * Generate an SLH-DSA-SHA2-192s keypair from a master + domain-separation info.
50
- *
51
- * @returns {{ publicKey: Buffer|Uint8Array, secretKey: Buffer|Uint8Array }}
52
- */
53
- export function keypairFromMaster(master, info = 'slh-dsa-sha2-192s-v1') {
54
- const seed = deriveSeed(master, info, SEED_BYTES)
55
- const seedU8 = seed instanceof Uint8Array ? seed : new Uint8Array(seed)
56
- const k = slh_dsa_sha2_192s.keygen(seedU8)
57
- return {
58
- publicKey: wrap(k.publicKey),
59
- secretKey: wrap(k.secretKey),
60
- }
61
- }
62
-
63
- /**
64
- * Sign a message. Returns the signature as a hex string.
65
- *
66
- * Accepts the same optional context string as ml-dsa.js (FIPS 205 allows it on
67
- * the same terms as FIPS 204, at most 255 bytes). Omit it and the behaviour is
68
- * exactly as before this parameter existed.
69
- *
70
- * @param {Buffer|Uint8Array} secretKey
71
- * @param {Buffer|Uint8Array|string} message
72
- * @param {{ context?: Uint8Array|Buffer|string }} [opts] at most 255 context bytes
73
- * @returns {string} hex-encoded signature (32448 chars)
74
- */
75
- export function sign(secretKey, message, opts) {
76
- const context = normalizeContext(opts)
77
- const sig = context === undefined
78
- ? slh_dsa_sha2_192s.sign(toBytes(message), secretKey)
79
- : slh_dsa_sha2_192s.sign(toBytes(message), secretKey, { context })
80
- return bytesToHex(sig)
81
- }
82
-
83
- /**
84
- * Verify a hex-encoded signature.
85
- *
86
- * Pass the same context the signer used. Returns false for any cryptographic
87
- * failure; throws only on caller misuse of `opts`.
88
- *
89
- * @param {Buffer|Uint8Array} publicKey
90
- * @param {Buffer|Uint8Array|string} message
91
- * @param {string} sigHex
92
- * @param {{ context?: Uint8Array|Buffer|string }} [opts]
93
- * @returns {boolean}
94
- */
95
- export function verify(publicKey, message, sigHex, opts) {
96
- const context = normalizeContext(opts)
97
- try {
98
- return context === undefined
99
- ? slh_dsa_sha2_192s.verify(hexToBytes(sigHex), toBytes(message), publicKey)
100
- : slh_dsa_sha2_192s.verify(hexToBytes(sigHex), toBytes(message), publicKey, { context })
101
- } catch {
102
- return false
103
- }
104
- }
105
-
106
- export { slh_dsa_sha2_192s }
1
+ // SLH-DSA-SHA2-192s helpers (NIST FIPS 205, SPHINCS+).
2
+ //
3
+ // Stateless hash-based signatures. Security Category 3 (≈ AES-192), matching
4
+ // ML-DSA-65's security level. Public key 48 bytes, secret key 96 bytes,
5
+ // signature 16224 bytes. Security rests only on the SHA-2 hash function — no
6
+ // lattice or number-theoretic assumptions — which makes it the conservative
7
+ // hedge alongside ML-DSA-65.
8
+ //
9
+ // Tradeoff: signatures are ~5x larger than ML-DSA-65 (16224 vs 3309 bytes) and
10
+ // signing is slower. Use ML-DSA-65 as the default; reach for SLH-DSA when you
11
+ // want a signature whose security does not depend on lattice hardness.
12
+ //
13
+ // Isomorphic: works in Node and modern browsers. Returns Buffer on Node
14
+ // (consistent with ml-dsa.js), Uint8Array in browsers.
15
+
16
+ import { slh_dsa_sha2_192s } from '@noble/post-quantum/slh-dsa.js'
17
+ import { deriveSeed } from './derive.js'
18
+ import { normalizeContext, MAX_CONTEXT_BYTES } from './_context.js'
19
+
20
+ export { MAX_CONTEXT_BYTES }
21
+
22
+ const HAS_BUFFER = typeof Buffer !== 'undefined'
23
+ const enc = new TextEncoder()
24
+
25
+ function toBytes(input) {
26
+ if (input instanceof Uint8Array) return input
27
+ if (typeof input === 'string') return enc.encode(input)
28
+ throw new Error('expected Uint8Array or string')
29
+ }
30
+ function hexToBytes(hex) {
31
+ if (typeof hex !== 'string' || hex.length % 2) throw new Error('invalid hex')
32
+ const b = new Uint8Array(hex.length / 2)
33
+ for (let i = 0; i < b.length; i++) b[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
34
+ return b
35
+ }
36
+ function bytesToHex(bytes) {
37
+ let s = ''
38
+ for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, '0')
39
+ return s
40
+ }
41
+ function wrap(bytes) {
42
+ return HAS_BUFFER ? Buffer.from(bytes) : bytes
43
+ }
44
+
45
+ // Seed length for SLH-DSA-SHA2-192s keygen (FIPS 205: SK.seed || SK.prf || PK.seed).
46
+ const SEED_BYTES = slh_dsa_sha2_192s.lengths.seed
47
+
48
+ /**
49
+ * Generate an SLH-DSA-SHA2-192s keypair from a master + domain-separation info.
50
+ *
51
+ * @returns {{ publicKey: Buffer|Uint8Array, secretKey: Buffer|Uint8Array }}
52
+ */
53
+ export function keypairFromMaster(master, info = 'slh-dsa-sha2-192s-v1') {
54
+ const seed = deriveSeed(master, info, SEED_BYTES)
55
+ const seedU8 = seed instanceof Uint8Array ? seed : new Uint8Array(seed)
56
+ const k = slh_dsa_sha2_192s.keygen(seedU8)
57
+ return {
58
+ publicKey: wrap(k.publicKey),
59
+ secretKey: wrap(k.secretKey),
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Sign a message. Returns the signature as a hex string.
65
+ *
66
+ * Accepts the same optional context string as ml-dsa.js (FIPS 205 allows it on
67
+ * the same terms as FIPS 204, at most 255 bytes). Omit it and the behaviour is
68
+ * exactly as before this parameter existed.
69
+ *
70
+ * @param {Buffer|Uint8Array} secretKey
71
+ * @param {Buffer|Uint8Array|string} message
72
+ * @param {{ context?: Uint8Array|Buffer|string }} [opts] at most 255 context bytes
73
+ * @returns {string} hex-encoded signature (32448 chars)
74
+ */
75
+ export function sign(secretKey, message, opts) {
76
+ const context = normalizeContext(opts)
77
+ const sig = context === undefined
78
+ ? slh_dsa_sha2_192s.sign(toBytes(message), secretKey)
79
+ : slh_dsa_sha2_192s.sign(toBytes(message), secretKey, { context })
80
+ return bytesToHex(sig)
81
+ }
82
+
83
+ /**
84
+ * Verify a hex-encoded signature.
85
+ *
86
+ * Pass the same context the signer used. Returns false for any cryptographic
87
+ * failure; throws only on caller misuse of `opts`.
88
+ *
89
+ * @param {Buffer|Uint8Array} publicKey
90
+ * @param {Buffer|Uint8Array|string} message
91
+ * @param {string} sigHex
92
+ * @param {{ context?: Uint8Array|Buffer|string }} [opts]
93
+ * @returns {boolean}
94
+ */
95
+ export function verify(publicKey, message, sigHex, opts) {
96
+ const context = normalizeContext(opts)
97
+ try {
98
+ return context === undefined
99
+ ? slh_dsa_sha2_192s.verify(hexToBytes(sigHex), toBytes(message), publicKey)
100
+ : slh_dsa_sha2_192s.verify(hexToBytes(sigHex), toBytes(message), publicKey, { context })
101
+ } catch {
102
+ return false
103
+ }
104
+ }
105
+
106
+ export { slh_dsa_sha2_192s }
package/src/webhook.d.ts CHANGED
@@ -1,119 +1,119 @@
1
- /// <reference types="node" />
2
-
3
- /**
4
- * Build the canonical signed envelope: `timestamp + "." + raw_body`.
5
- *
6
- * Receivers MUST construct the envelope from the timestamp header and the
7
- * RAW request body bytes as received. Re-serialising a parsed JSON object
8
- * will not produce the same bytes and signature verification will fail.
9
- */
10
- export function envelope(
11
- timestamp: string | number,
12
- rawBody: string | Buffer,
13
- ): Buffer
14
-
15
- /**
16
- * Compute the hex HMAC-SHA-256 of the envelope using the shared secret.
17
- * Returns the hex value WITHOUT the `sha256=` prefix.
18
- */
19
- export function hmacHex(
20
- secret: string | Buffer,
21
- timestamp: string | number,
22
- rawBody: string | Buffer,
23
- ): string
24
-
25
- /**
26
- * Constant-time verify of the X-KXCO-Signature header.
27
- * Accepts the header value with or without the `sha256=` prefix.
28
- */
29
- export function verifyHmac(
30
- secret: string | Buffer,
31
- timestamp: string | number,
32
- rawBody: string | Buffer,
33
- sigHeader: string,
34
- ): boolean
35
-
36
- /**
37
- * Produce the X-KXCO-PQ-Signature header value: the hex ML-DSA-65
38
- * signature over the envelope, prefixed with `ml-dsa-65=`.
39
- */
40
- export function pqSign(
41
- secretKey: Buffer | Uint8Array,
42
- timestamp: string | number,
43
- rawBody: string | Buffer,
44
- ): string
45
-
46
- /**
47
- * Verify a hex ML-DSA-65 signature header.
48
- * Accepts the value with or without the `ml-dsa-65=` prefix.
49
- */
50
- export function verifyPq(
51
- publicKey: Buffer | Uint8Array,
52
- timestamp: string | number,
53
- rawBody: string | Buffer,
54
- sigHeader: string,
55
- ): boolean
56
-
57
- export interface SignDeliveryArgs {
58
- /** The exact body bytes that will be transmitted */
59
- rawBody: string | Buffer
60
- /** Per-endpoint shared secret for HMAC */
61
- hmacSecret: string | Buffer
62
- /** Raw ML-DSA-65 secret key */
63
- pqSecretKey: Buffer | Uint8Array
64
- /** 16-hex kid fingerprint of the matching public key */
65
- pqKid: string
66
- /** Optional event name (eg. "payment.settled") */
67
- event?: string
68
- /** Optional delivery / idempotency identifier */
69
- deliveryId?: string
70
- }
71
-
72
- export interface SignDeliveryHeaders {
73
- 'Content-Type': 'application/json'
74
- 'X-KXCO-Timestamp': string
75
- 'X-KXCO-Signature': string // sha256=<hex>
76
- 'X-KXCO-PQ-Signature': string // ml-dsa-65=<hex>
77
- 'X-KXCO-PQ-Kid': string
78
- 'X-KXCO-Event'?: string
79
- 'X-KXCO-Delivery'?: string
80
- }
81
-
82
- /**
83
- * Sign a webhook delivery. Returns the full set of headers a sender
84
- * should attach to the HTTP request.
85
- *
86
- * The caller is responsible for sending `rawBody` byte-for-byte
87
- * unchanged. The receiver verifies against the bytes as transmitted.
88
- */
89
- export function signDelivery(args: SignDeliveryArgs): SignDeliveryHeaders
90
-
91
- export interface VerifyDeliveryArgs {
92
- /** HTTP headers with LOWERCASE keys */
93
- headers: Record<string, string | undefined>
94
- /** The EXACT request body bytes as received */
95
- rawBody: string | Buffer
96
- /** Optional: enable HMAC verification by providing the shared secret */
97
- hmacSecret?: string | Buffer
98
- /** Optional: enable PQ verification by providing the platform public key */
99
- pqPublicKey?: Buffer | Uint8Array
100
- /** Required when `pqPublicKey` is provided */
101
- pinnedKid?: string
102
- /** Replay-window in seconds. Default 300 (5 minutes). */
103
- windowSeconds?: number
104
- }
105
-
106
- export interface VerifyDeliveryResult {
107
- hmacOk: boolean
108
- pqOk: boolean
109
- timestampOk: boolean
110
- kidOk: boolean
111
- }
112
-
113
- /**
114
- * Verify a webhook delivery on the receiving side.
115
- *
116
- * Returns a breakdown of which predicates passed. A delivery is
117
- * acceptable when `(hmacOk || pqOk) && timestampOk && kidOk`.
118
- */
119
- export function verifyDelivery(args: VerifyDeliveryArgs): VerifyDeliveryResult
1
+ /// <reference types="node" />
2
+
3
+ /**
4
+ * Build the canonical signed envelope: `timestamp + "." + raw_body`.
5
+ *
6
+ * Receivers MUST construct the envelope from the timestamp header and the
7
+ * RAW request body bytes as received. Re-serialising a parsed JSON object
8
+ * will not produce the same bytes and signature verification will fail.
9
+ */
10
+ export function envelope(
11
+ timestamp: string | number,
12
+ rawBody: string | Buffer,
13
+ ): Buffer
14
+
15
+ /**
16
+ * Compute the hex HMAC-SHA-256 of the envelope using the shared secret.
17
+ * Returns the hex value WITHOUT the `sha256=` prefix.
18
+ */
19
+ export function hmacHex(
20
+ secret: string | Buffer,
21
+ timestamp: string | number,
22
+ rawBody: string | Buffer,
23
+ ): string
24
+
25
+ /**
26
+ * Constant-time verify of the X-KXCO-Signature header.
27
+ * Accepts the header value with or without the `sha256=` prefix.
28
+ */
29
+ export function verifyHmac(
30
+ secret: string | Buffer,
31
+ timestamp: string | number,
32
+ rawBody: string | Buffer,
33
+ sigHeader: string,
34
+ ): boolean
35
+
36
+ /**
37
+ * Produce the X-KXCO-PQ-Signature header value: the hex ML-DSA-65
38
+ * signature over the envelope, prefixed with `ml-dsa-65=`.
39
+ */
40
+ export function pqSign(
41
+ secretKey: Buffer | Uint8Array,
42
+ timestamp: string | number,
43
+ rawBody: string | Buffer,
44
+ ): string
45
+
46
+ /**
47
+ * Verify a hex ML-DSA-65 signature header.
48
+ * Accepts the value with or without the `ml-dsa-65=` prefix.
49
+ */
50
+ export function verifyPq(
51
+ publicKey: Buffer | Uint8Array,
52
+ timestamp: string | number,
53
+ rawBody: string | Buffer,
54
+ sigHeader: string,
55
+ ): boolean
56
+
57
+ export interface SignDeliveryArgs {
58
+ /** The exact body bytes that will be transmitted */
59
+ rawBody: string | Buffer
60
+ /** Per-endpoint shared secret for HMAC */
61
+ hmacSecret: string | Buffer
62
+ /** Raw ML-DSA-65 secret key */
63
+ pqSecretKey: Buffer | Uint8Array
64
+ /** 16-hex kid fingerprint of the matching public key */
65
+ pqKid: string
66
+ /** Optional event name (eg. "payment.settled") */
67
+ event?: string
68
+ /** Optional delivery / idempotency identifier */
69
+ deliveryId?: string
70
+ }
71
+
72
+ export interface SignDeliveryHeaders {
73
+ 'Content-Type': 'application/json'
74
+ 'X-KXCO-Timestamp': string
75
+ 'X-KXCO-Signature': string // sha256=<hex>
76
+ 'X-KXCO-PQ-Signature': string // ml-dsa-65=<hex>
77
+ 'X-KXCO-PQ-Kid': string
78
+ 'X-KXCO-Event'?: string
79
+ 'X-KXCO-Delivery'?: string
80
+ }
81
+
82
+ /**
83
+ * Sign a webhook delivery. Returns the full set of headers a sender
84
+ * should attach to the HTTP request.
85
+ *
86
+ * The caller is responsible for sending `rawBody` byte-for-byte
87
+ * unchanged. The receiver verifies against the bytes as transmitted.
88
+ */
89
+ export function signDelivery(args: SignDeliveryArgs): SignDeliveryHeaders
90
+
91
+ export interface VerifyDeliveryArgs {
92
+ /** HTTP headers with LOWERCASE keys */
93
+ headers: Record<string, string | undefined>
94
+ /** The EXACT request body bytes as received */
95
+ rawBody: string | Buffer
96
+ /** Optional: enable HMAC verification by providing the shared secret */
97
+ hmacSecret?: string | Buffer
98
+ /** Optional: enable PQ verification by providing the platform public key */
99
+ pqPublicKey?: Buffer | Uint8Array
100
+ /** Required when `pqPublicKey` is provided */
101
+ pinnedKid?: string
102
+ /** Replay-window in seconds. Default 300 (5 minutes). */
103
+ windowSeconds?: number
104
+ }
105
+
106
+ export interface VerifyDeliveryResult {
107
+ hmacOk: boolean
108
+ pqOk: boolean
109
+ timestampOk: boolean
110
+ kidOk: boolean
111
+ }
112
+
113
+ /**
114
+ * Verify a webhook delivery on the receiving side.
115
+ *
116
+ * Returns a breakdown of which predicates passed. A delivery is
117
+ * acceptable when `(hmacOk || pqOk) && timestampOk && kidOk`.
118
+ */
119
+ export function verifyDelivery(args: VerifyDeliveryArgs): VerifyDeliveryResult