@arkyc/webhooks 1.0.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/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -0
- package/dist/payload.d.mts +33 -0
- package/dist/payload.d.mts.map +1 -0
- package/dist/payload.mjs +30 -0
- package/dist/payload.mjs.map +1 -0
- package/dist/signing.d.mts +49 -0
- package/dist/signing.d.mts.map +1 -0
- package/dist/signing.mjs +47 -0
- package/dist/signing.mjs.map +1 -0
- package/package.json +28 -0
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DecisionReason, VerificationStatus, WebhookChecks, WebhookEvent, WebhookEventName } from "@arkyc/types";
|
|
2
|
+
|
|
3
|
+
//#region src/payload.d.ts
|
|
4
|
+
/** The data needed to assemble a {@link WebhookEvent} body. */
|
|
5
|
+
interface BuildWebhookPayloadInput {
|
|
6
|
+
event: WebhookEventName;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
organizationId: string;
|
|
9
|
+
projectId: string;
|
|
10
|
+
userReference: string | null;
|
|
11
|
+
status: VerificationStatus;
|
|
12
|
+
checks: WebhookChecks;
|
|
13
|
+
decisionReason: DecisionReason | null;
|
|
14
|
+
/** Signed inline-image URLs for captured assets (when any exist). */
|
|
15
|
+
assets?: Record<string, string> | null;
|
|
16
|
+
/** ISO-8601 timestamp the event occurred. */
|
|
17
|
+
createdAt: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Assemble the JSON body delivered to a webhook endpoint.
|
|
21
|
+
*/
|
|
22
|
+
declare class WebhookPayload {
|
|
23
|
+
/**
|
|
24
|
+
* Build the JSON body delivered to a webhook endpoint (matches the spec shape).
|
|
25
|
+
*
|
|
26
|
+
* @param input
|
|
27
|
+
* @returns
|
|
28
|
+
*/
|
|
29
|
+
static build(input: BuildWebhookPayloadInput): WebhookEvent;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { BuildWebhookPayloadInput, WebhookPayload };
|
|
33
|
+
//# sourceMappingURL=payload.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload.d.mts","names":[],"sources":["../src/payload.ts"],"mappings":";;;;UAGiB,wBAAA;EACf,KAAA,EAAO,gBAAA;EACP,SAAA;EACA,cAAA;EACA,SAAA;EACA,aAAA;EACA,MAAA,EAAQ,kBAAA;EACR,MAAA,EAAQ,aAAA;EACR,cAAA,EAAgB,cAAA;EAED;EAAf,MAAA,GAAS,MAAA;EATT;EAWA,SAAA;AAAA;;;;cAMW,cAAA;EAZH;;;;;;EAAA,OAmBD,KAAA,CAAM,KAAA,EAAO,wBAAA,GAA2B,YAAY;AAAA"}
|
package/dist/payload.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/payload.ts
|
|
2
|
+
/**
|
|
3
|
+
* Assemble the JSON body delivered to a webhook endpoint.
|
|
4
|
+
*/
|
|
5
|
+
var WebhookPayload = class {
|
|
6
|
+
/**
|
|
7
|
+
* Build the JSON body delivered to a webhook endpoint (matches the spec shape).
|
|
8
|
+
*
|
|
9
|
+
* @param input
|
|
10
|
+
* @returns
|
|
11
|
+
*/
|
|
12
|
+
static build(input) {
|
|
13
|
+
return {
|
|
14
|
+
event: input.event,
|
|
15
|
+
session_id: input.sessionId,
|
|
16
|
+
organization_id: input.organizationId,
|
|
17
|
+
project_id: input.projectId,
|
|
18
|
+
user_reference: input.userReference,
|
|
19
|
+
status: input.status,
|
|
20
|
+
checks: input.checks,
|
|
21
|
+
decision_reason: input.decisionReason,
|
|
22
|
+
assets: input.assets ?? null,
|
|
23
|
+
created_at: input.createdAt
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
export { WebhookPayload };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=payload.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload.mjs","names":[],"sources":["../src/payload.ts"],"sourcesContent":["import type { DecisionReason, VerificationStatus, WebhookChecks, WebhookEvent, WebhookEventName } from '@arkyc/types'\n\n/** The data needed to assemble a {@link WebhookEvent} body. */\nexport interface BuildWebhookPayloadInput {\n event: WebhookEventName\n sessionId: string\n organizationId: string\n projectId: string\n userReference: string | null\n status: VerificationStatus\n checks: WebhookChecks\n decisionReason: DecisionReason | null\n /** Signed inline-image URLs for captured assets (when any exist). */\n assets?: Record<string, string> | null\n /** ISO-8601 timestamp the event occurred. */\n createdAt: string\n}\n\n/**\n * Assemble the JSON body delivered to a webhook endpoint.\n */\nexport class WebhookPayload {\n /**\n * Build the JSON body delivered to a webhook endpoint (matches the spec shape).\n *\n * @param input\n * @returns\n */\n static build(input: BuildWebhookPayloadInput): WebhookEvent {\n return {\n event: input.event,\n session_id: input.sessionId,\n organization_id: input.organizationId,\n project_id: input.projectId,\n user_reference: input.userReference,\n status: input.status,\n checks: input.checks,\n decision_reason: input.decisionReason,\n assets: input.assets ?? null,\n created_at: input.createdAt,\n }\n }\n}\n"],"mappings":";;;;AAqBA,IAAa,iBAAb,MAA4B;;;;;;;CAO1B,OAAO,MAAM,OAA+C;EAC1D,OAAO;GACL,OAAO,MAAM;GACb,YAAY,MAAM;GAClB,iBAAiB,MAAM;GACvB,YAAY,MAAM;GAClB,gBAAgB,MAAM;GACtB,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,iBAAiB,MAAM;GACvB,QAAQ,MAAM,UAAU;GACxB,YAAY,MAAM;EACpB;CACF;AACF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/signing.d.ts
|
|
2
|
+
/** Inputs for verifying a received webhook signature. */
|
|
3
|
+
interface VerifyWebhookInput {
|
|
4
|
+
/** Raw request body as received. */
|
|
5
|
+
payload: string;
|
|
6
|
+
/** The endpoint's signing secret. */
|
|
7
|
+
secret: string;
|
|
8
|
+
/** Signature from the `X-Arkyc-Signature` header. */
|
|
9
|
+
signature: string;
|
|
10
|
+
/** Timestamp from the `X-Arkyc-Timestamp` header (unix seconds). */
|
|
11
|
+
timestamp: number;
|
|
12
|
+
/** Allowed clock skew in seconds (default 300). */
|
|
13
|
+
toleranceSec?: number;
|
|
14
|
+
/** Injected clock (epoch ms) — pure + testable. */
|
|
15
|
+
now?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sign and verify webhook deliveries (HMAC-SHA256, Stripe-style).
|
|
19
|
+
*/
|
|
20
|
+
declare class WebhookSigner {
|
|
21
|
+
/** Header carrying the HMAC signature on a delivered webhook. */
|
|
22
|
+
static readonly SIGNATURE_HEADER = "X-Arkyc-Signature";
|
|
23
|
+
/** Header carrying the unix-seconds timestamp the signature covers. */
|
|
24
|
+
static readonly TIMESTAMP_HEADER = "X-Arkyc-Timestamp";
|
|
25
|
+
/** Default tolerance (seconds) for the timestamp when verifying. */
|
|
26
|
+
static readonly DEFAULT_TOLERANCE_SEC = 300;
|
|
27
|
+
/**
|
|
28
|
+
* Compute the HMAC-SHA256 signature for a webhook body.
|
|
29
|
+
*
|
|
30
|
+
* The signed message is `${timestamp}.${payload}` (Stripe-style), so a captured
|
|
31
|
+
* body can't be replayed under a different timestamp.
|
|
32
|
+
*
|
|
33
|
+
* @param payload The exact JSON string that will be sent as the body.
|
|
34
|
+
* @param secret The endpoint's signing secret.
|
|
35
|
+
* @param timestamp Unix seconds the signature is bound to.
|
|
36
|
+
*/
|
|
37
|
+
static sign(payload: string, secret: string, timestamp: number): string;
|
|
38
|
+
/**
|
|
39
|
+
* Verify a webhook signature and reject stale/forged deliveries: the timestamp
|
|
40
|
+
* must be within tolerance and the HMAC must match (constant-time compare).
|
|
41
|
+
*
|
|
42
|
+
* @param input
|
|
43
|
+
* @returns
|
|
44
|
+
*/
|
|
45
|
+
static verify(input: VerifyWebhookInput): boolean;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { VerifyWebhookInput, WebhookSigner };
|
|
49
|
+
//# sourceMappingURL=signing.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signing.d.mts","names":[],"sources":["../src/signing.ts"],"mappings":";;UAGiB,kBAAA;EAAkB;EAEjC,OAAA;EAFiC;EAIjC,MAAA;EAAA;EAEA,SAAA;EAEA;EAAA,SAAA;EAIA;EAFA,YAAA;EAEG;EAAH,GAAA;AAAA;;;;cAMW,aAAA;EAQK;EAAA,gBANA,gBAAA;EAkBJ;EAAA,gBAfI,gBAAA;EAe6B;EAAA,gBAZ7B,qBAAA;EAuBK;;;AAAkB;;;;;;;EAAlB,OAXd,IAAA,CAAK,OAAA,UAAiB,MAAA,UAAgB,SAAA;;;;;;;;SAWtC,MAAA,CAAO,KAAA,EAAO,kBAAkB;AAAA"}
|
package/dist/signing.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
//#region src/signing.ts
|
|
3
|
+
/**
|
|
4
|
+
* Sign and verify webhook deliveries (HMAC-SHA256, Stripe-style).
|
|
5
|
+
*/
|
|
6
|
+
var WebhookSigner = class WebhookSigner {
|
|
7
|
+
/** Header carrying the HMAC signature on a delivered webhook. */
|
|
8
|
+
static SIGNATURE_HEADER = "X-Arkyc-Signature";
|
|
9
|
+
/** Header carrying the unix-seconds timestamp the signature covers. */
|
|
10
|
+
static TIMESTAMP_HEADER = "X-Arkyc-Timestamp";
|
|
11
|
+
/** Default tolerance (seconds) for the timestamp when verifying. */
|
|
12
|
+
static DEFAULT_TOLERANCE_SEC = 300;
|
|
13
|
+
/**
|
|
14
|
+
* Compute the HMAC-SHA256 signature for a webhook body.
|
|
15
|
+
*
|
|
16
|
+
* The signed message is `${timestamp}.${payload}` (Stripe-style), so a captured
|
|
17
|
+
* body can't be replayed under a different timestamp.
|
|
18
|
+
*
|
|
19
|
+
* @param payload The exact JSON string that will be sent as the body.
|
|
20
|
+
* @param secret The endpoint's signing secret.
|
|
21
|
+
* @param timestamp Unix seconds the signature is bound to.
|
|
22
|
+
*/
|
|
23
|
+
static sign(payload, secret, timestamp) {
|
|
24
|
+
return createHmac("sha256", secret).update(`${timestamp}.${payload}`).digest("hex");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Verify a webhook signature and reject stale/forged deliveries: the timestamp
|
|
28
|
+
* must be within tolerance and the HMAC must match (constant-time compare).
|
|
29
|
+
*
|
|
30
|
+
* @param input
|
|
31
|
+
* @returns
|
|
32
|
+
*/
|
|
33
|
+
static verify(input) {
|
|
34
|
+
const tolerance = input.toleranceSec ?? WebhookSigner.DEFAULT_TOLERANCE_SEC;
|
|
35
|
+
const nowSec = Math.floor((input.now ?? Date.now()) / 1e3);
|
|
36
|
+
if (!Number.isFinite(input.timestamp)) return false;
|
|
37
|
+
if (Math.abs(nowSec - input.timestamp) > tolerance) return false;
|
|
38
|
+
const expected = WebhookSigner.sign(input.payload, input.secret, input.timestamp);
|
|
39
|
+
const a = Buffer.from(input.signature);
|
|
40
|
+
const b = Buffer.from(expected);
|
|
41
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { WebhookSigner };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=signing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signing.mjs","names":[],"sources":["../src/signing.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto'\n\n/** Inputs for verifying a received webhook signature. */\nexport interface VerifyWebhookInput {\n /** Raw request body as received. */\n payload: string\n /** The endpoint's signing secret. */\n secret: string\n /** Signature from the `X-Arkyc-Signature` header. */\n signature: string\n /** Timestamp from the `X-Arkyc-Timestamp` header (unix seconds). */\n timestamp: number\n /** Allowed clock skew in seconds (default 300). */\n toleranceSec?: number\n /** Injected clock (epoch ms) — pure + testable. */\n now?: number\n}\n\n/**\n * Sign and verify webhook deliveries (HMAC-SHA256, Stripe-style).\n */\nexport class WebhookSigner {\n /** Header carrying the HMAC signature on a delivered webhook. */\n static readonly SIGNATURE_HEADER = 'X-Arkyc-Signature'\n\n /** Header carrying the unix-seconds timestamp the signature covers. */\n static readonly TIMESTAMP_HEADER = 'X-Arkyc-Timestamp'\n\n /** Default tolerance (seconds) for the timestamp when verifying. */\n static readonly DEFAULT_TOLERANCE_SEC = 300\n\n /**\n * Compute the HMAC-SHA256 signature for a webhook body.\n *\n * The signed message is `${timestamp}.${payload}` (Stripe-style), so a captured\n * body can't be replayed under a different timestamp.\n *\n * @param payload The exact JSON string that will be sent as the body.\n * @param secret The endpoint's signing secret.\n * @param timestamp Unix seconds the signature is bound to.\n */\n static sign(payload: string, secret: string, timestamp: number): string {\n return createHmac('sha256', secret).update(`${timestamp}.${payload}`).digest('hex')\n }\n\n /**\n * Verify a webhook signature and reject stale/forged deliveries: the timestamp\n * must be within tolerance and the HMAC must match (constant-time compare).\n *\n * @param input\n * @returns\n */\n static verify(input: VerifyWebhookInput): boolean {\n const tolerance = input.toleranceSec ?? WebhookSigner.DEFAULT_TOLERANCE_SEC\n const nowSec = Math.floor((input.now ?? Date.now()) / 1000)\n if (!Number.isFinite(input.timestamp)) return false\n if (Math.abs(nowSec - input.timestamp) > tolerance) return false\n\n const expected = WebhookSigner.sign(input.payload, input.secret, input.timestamp)\n const a = Buffer.from(input.signature)\n const b = Buffer.from(expected)\n\n return a.length === b.length && timingSafeEqual(a, b)\n }\n}\n"],"mappings":";;;;;AAqBA,IAAa,gBAAb,MAAa,cAAc;;CAEzB,OAAgB,mBAAmB;;CAGnC,OAAgB,mBAAmB;;CAGnC,OAAgB,wBAAwB;;;;;;;;;;;CAYxC,OAAO,KAAK,SAAiB,QAAgB,WAA2B;EACtE,OAAO,WAAW,UAAU,MAAM,CAAC,CAAC,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC,CAAC,OAAO,KAAK;CACpF;;;;;;;;CASA,OAAO,OAAO,OAAoC;EAChD,MAAM,YAAY,MAAM,gBAAgB,cAAc;EACtD,MAAM,SAAS,KAAK,OAAO,MAAM,OAAO,KAAK,IAAI,KAAK,GAAI;EAC1D,IAAI,CAAC,OAAO,SAAS,MAAM,SAAS,GAAG,OAAO;EAC9C,IAAI,KAAK,IAAI,SAAS,MAAM,SAAS,IAAI,WAAW,OAAO;EAE3D,MAAM,WAAW,cAAc,KAAK,MAAM,SAAS,MAAM,QAAQ,MAAM,SAAS;EAChF,MAAM,IAAI,OAAO,KAAK,MAAM,SAAS;EACrC,MAAM,IAAI,OAAO,KAAK,QAAQ;EAE9B,OAAO,EAAE,WAAW,EAAE,UAAU,gBAAgB,GAAG,CAAC;CACtD;AACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arkyc/webhooks",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Webhook signing, verification, and payload building",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.mjs",
|
|
8
|
+
"module": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.mts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.mts",
|
|
13
|
+
"import": "./dist/index.mjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@arkyc/types": "^1.0.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"lint": "eslint src",
|
|
26
|
+
"clean": "rm -rf dist"
|
|
27
|
+
}
|
|
28
|
+
}
|