commitrail 0.1.0-alpha.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.
Files changed (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +12 -0
  3. package/dist/cjs/envelope.d.ts +91 -0
  4. package/dist/cjs/envelope.js +54 -0
  5. package/dist/cjs/envelope.js.map +1 -0
  6. package/dist/cjs/index.d.ts +20 -0
  7. package/dist/cjs/index.js +29 -0
  8. package/dist/cjs/index.js.map +1 -0
  9. package/dist/cjs/package.json +3 -0
  10. package/dist/cjs/postgres.d.ts +152 -0
  11. package/dist/cjs/postgres.js +373 -0
  12. package/dist/cjs/postgres.js.map +1 -0
  13. package/dist/cjs/signing.d.ts +66 -0
  14. package/dist/cjs/signing.js +108 -0
  15. package/dist/cjs/signing.js.map +1 -0
  16. package/dist/cjs/subjects.d.ts +53 -0
  17. package/dist/cjs/subjects.js +88 -0
  18. package/dist/cjs/subjects.js.map +1 -0
  19. package/dist/cjs/webhooks.d.ts +100 -0
  20. package/dist/cjs/webhooks.js +214 -0
  21. package/dist/cjs/webhooks.js.map +1 -0
  22. package/dist/esm/envelope.d.ts +91 -0
  23. package/dist/esm/envelope.js +50 -0
  24. package/dist/esm/envelope.js.map +1 -0
  25. package/dist/esm/index.d.ts +20 -0
  26. package/dist/esm/index.js +21 -0
  27. package/dist/esm/index.js.map +1 -0
  28. package/dist/esm/package.json +3 -0
  29. package/dist/esm/postgres.d.ts +152 -0
  30. package/dist/esm/postgres.js +366 -0
  31. package/dist/esm/postgres.js.map +1 -0
  32. package/dist/esm/signing.d.ts +66 -0
  33. package/dist/esm/signing.js +101 -0
  34. package/dist/esm/signing.js.map +1 -0
  35. package/dist/esm/subjects.d.ts +53 -0
  36. package/dist/esm/subjects.js +83 -0
  37. package/dist/esm/subjects.js.map +1 -0
  38. package/dist/esm/webhooks.d.ts +100 -0
  39. package/dist/esm/webhooks.js +209 -0
  40. package/dist/esm/webhooks.js.map +1 -0
  41. package/package.json +108 -0
@@ -0,0 +1,101 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ /**
3
+ * The canonical string a signature covers.
4
+ *
5
+ * Consumers reimplement this, so its exact shape matters far less than documenting it
6
+ * precisely — but it must include the delivery id, so that a captured body cannot be
7
+ * replayed as a different obligation.
8
+ */
9
+ export function canonicalPayload(input) {
10
+ return `${input.timestamp}.${input.deliveryId}.${input.body}`;
11
+ }
12
+ /**
13
+ * The same thing, as bytes, for a caller who has the raw body and has not decoded it.
14
+ *
15
+ * A signature is over bytes. Decoding to a string and re-encoding round-trips exactly for the
16
+ * valid UTF-8 that JSON must be, so the two agree — but making the caller decode first asks them
17
+ * to do something the contract does not need, and `.toString()` with a forgotten or wrong
18
+ * encoding is a real way to break verification for reasons nobody can see.
19
+ *
20
+ * Returns `Uint8Array` rather than `Buffer` deliberately: a `Buffer` in a published `.d.ts` makes
21
+ * `@types/node` a requirement to compile against this package, which a library has no business
22
+ * imposing. The packaging gate caught that, which is what it is for.
23
+ */
24
+ export function canonicalPayloadBytes(input) {
25
+ return Buffer.concat([
26
+ Buffer.from(`${input.timestamp}.${input.deliveryId}.`, 'utf8'),
27
+ Buffer.from(input.body.buffer, input.body.byteOffset, input.body.byteLength),
28
+ ]);
29
+ }
30
+ export function sign(secret, canonical) {
31
+ return createHmac('sha256', secret).update(canonical).digest('hex');
32
+ }
33
+ /**
34
+ * Build the `CommitRail-Signature` header.
35
+ *
36
+ * During a rotation this carries a signature under each secret, so a consumer's
37
+ * verification is "does one of these match mine". That code never has to know about
38
+ * versions and never changes shape when a secret rotates — which matters more than
39
+ * elegance for something that gets pasted into middleware once and left there.
40
+ */
41
+ export function signatureHeader(input) {
42
+ const canonical = canonicalPayload(input);
43
+ const signatures = [input.material.current, input.material.previous]
44
+ .filter((s) => s !== undefined)
45
+ .map((s) => `v1=${sign(s.secret, canonical)}`);
46
+ return [`t=${input.timestamp}`, ...signatures].join(',');
47
+ }
48
+ /**
49
+ * Verify a header the way a consumer would. Exists so the contract we publish is the one
50
+ * we test against, rather than a description of it.
51
+ */
52
+ export function verifySignatureHeader(input) {
53
+ const tolerance = input.toleranceSeconds ?? 300;
54
+ const now = input.now ?? Math.floor(Date.now() / 1000);
55
+ const parts = input.header.split(',').map((p) => p.trim());
56
+ const timestampPart = parts.find((p) => p.startsWith('t='));
57
+ if (timestampPart === undefined) {
58
+ return false;
59
+ }
60
+ const timestamp = parseTimestamp(timestampPart.slice(2));
61
+ // Bounds replay. A signature stays valid forever otherwise, and a captured request could
62
+ // be resent indefinitely. The bound is inclusive and symmetric: a delivery exactly `tolerance`
63
+ // seconds either side of now is accepted.
64
+ if (timestamp === undefined || Math.abs(now - timestamp) > tolerance) {
65
+ return false;
66
+ }
67
+ const expected = sign(input.secret, typeof input.body === 'string'
68
+ ? canonicalPayload({ timestamp, deliveryId: input.deliveryId, body: input.body })
69
+ : canonicalPayloadBytes({ timestamp, deliveryId: input.deliveryId, body: input.body }));
70
+ return parts
71
+ .filter((p) => p.startsWith('v1='))
72
+ .some((p) => equalsConstantTime(p.slice(3), expected));
73
+ }
74
+ function equalsConstantTime(a, b) {
75
+ const left = Buffer.from(a, 'utf8');
76
+ const right = Buffer.from(b, 'utf8');
77
+ // Length must be compared first — timingSafeEqual throws on a mismatch — and leaking the
78
+ // length of a hex digest tells an attacker nothing.
79
+ return left.length === right.length && timingSafeEqual(left, right);
80
+ }
81
+ /**
82
+ * The `t=` value, parsed strictly.
83
+ *
84
+ * `Number.parseInt` is lenient: it reads `1787000000.5` and `1787000000junk` as 1787000000, so
85
+ * three different header strings would verify against one signature. Harmless on its own — the
86
+ * signature is computed over the parsed integer, so nothing about the body or the window changes
87
+ * — but it is a divergence, and divergence is the thing this protocol cannot afford. A verifier
88
+ * written from PROTOCOL.md with a strict integer parse would reject what this one accepts, and
89
+ * the customer would meet the difference in production.
90
+ *
91
+ * Digits only. A negative timestamp is refused here rather than by the tolerance check, which
92
+ * makes the rule one sentence instead of two.
93
+ */
94
+ function parseTimestamp(value) {
95
+ if (!/^\d+$/.test(value)) {
96
+ return undefined;
97
+ }
98
+ const parsed = Number.parseInt(value, 10);
99
+ return Number.isSafeInteger(parsed) ? parsed : undefined;
100
+ }
101
+ //# sourceMappingURL=signing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing.js","sourceRoot":"","sources":["../../src/signing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Subjects: the business identities an event concerns.
3
+ *
4
+ * A subject is not what happened (the event type), not the operation it belongs to (the
5
+ * correlation id), and not what caused it (the causation id) — it is *who or what it was
6
+ * about*: an order, a payment, a customer, a document. Declaring them makes those
7
+ * identifiers first-class lookup keys in CommitRail, so an investigation can start from
8
+ * "what happened to order_1264" rather than from an event id nobody has.
9
+ *
10
+ * Subjects are an unordered set of `(type, id)` pairs. Plural on purpose: a
11
+ * `payment.captured` legitimately concerns the payment, the order and the customer, and
12
+ * CommitRail has no business deciding which of them is *the* subject.
13
+ */
14
+ export interface EventSubject {
15
+ /** The kind of thing, in the application's own vocabulary: `order`, `payment`, `customer`. */
16
+ type: string;
17
+ /** The application's identifier for it: `order_1264`, `pay_991`. */
18
+ id: string;
19
+ }
20
+ /**
21
+ * Sized so normal business modelling never meets them; they exist to stop pathological
22
+ * usage, not to ration subjects. The evidence behind each number — measured throughput,
23
+ * storage and lookup curves, and what still needs re-measuring at production scale —
24
+ * is docs/benchmarks/event-subjects-limits.md; change them there first.
25
+ */
26
+ export declare const SUBJECT_LIMITS: {
27
+ readonly maxPerEvent: 100;
28
+ readonly maxTypeLength: 200;
29
+ readonly maxIdLength: 500;
30
+ };
31
+ /** Registered globally by description, so every copy of this package agrees on it. */
32
+ declare const BRAND: unique symbol;
33
+ /** Branded like `InvalidDeliveryError`, and for the same dual-package reason. */
34
+ export declare class InvalidSubjectsError extends Error {
35
+ static readonly brand: symbol;
36
+ readonly [BRAND] = true;
37
+ constructor(message: string);
38
+ static is(error: unknown): error is InvalidSubjectsError;
39
+ }
40
+ /**
41
+ * Validate and canonicalise a subjects value.
42
+ *
43
+ * Returns `null` for "no subjects" (undefined, null, or an empty array) so callers store
44
+ * nothing rather than an empty list. Exact duplicate `(type, id)` pairs are dropped —
45
+ * duplicates carry no meaning, and producers often assemble subjects from more than one
46
+ * code path — with first-occurrence order preserved. Anything malformed throws: a subject
47
+ * that cannot be represented is an error at the boundary, never a silent truncation.
48
+ *
49
+ * Takes `unknown` because the acceptance side re-validates whatever a producer actually
50
+ * wrote to the outbox, which no type annotation can vouch for.
51
+ */
52
+ export declare function normalizeSubjects(subjects: unknown): EventSubject[] | null;
53
+ export {};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Sized so normal business modelling never meets them; they exist to stop pathological
3
+ * usage, not to ration subjects. The evidence behind each number — measured throughput,
4
+ * storage and lookup curves, and what still needs re-measuring at production scale —
5
+ * is docs/benchmarks/event-subjects-limits.md; change them there first.
6
+ */
7
+ export const SUBJECT_LIMITS = {
8
+ maxPerEvent: 100,
9
+ maxTypeLength: 200,
10
+ maxIdLength: 500,
11
+ };
12
+ /** Registered globally by description, so every copy of this package agrees on it. */
13
+ const BRAND = Symbol.for('commitrail.InvalidSubjectsError');
14
+ /** Branded like `InvalidDeliveryError`, and for the same dual-package reason. */
15
+ export class InvalidSubjectsError extends Error {
16
+ static brand = BRAND;
17
+ [BRAND] = true;
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = 'InvalidSubjectsError';
21
+ }
22
+ static is(error) {
23
+ return (typeof error === 'object' &&
24
+ error !== null &&
25
+ error[BRAND] === true);
26
+ }
27
+ }
28
+ /**
29
+ * Validate and canonicalise a subjects value.
30
+ *
31
+ * Returns `null` for "no subjects" (undefined, null, or an empty array) so callers store
32
+ * nothing rather than an empty list. Exact duplicate `(type, id)` pairs are dropped —
33
+ * duplicates carry no meaning, and producers often assemble subjects from more than one
34
+ * code path — with first-occurrence order preserved. Anything malformed throws: a subject
35
+ * that cannot be represented is an error at the boundary, never a silent truncation.
36
+ *
37
+ * Takes `unknown` because the acceptance side re-validates whatever a producer actually
38
+ * wrote to the outbox, which no type annotation can vouch for.
39
+ */
40
+ export function normalizeSubjects(subjects) {
41
+ if (subjects === undefined || subjects === null) {
42
+ return null;
43
+ }
44
+ if (!Array.isArray(subjects)) {
45
+ throw new InvalidSubjectsError('subjects must be an array of { type, id } pairs');
46
+ }
47
+ const seen = new Set();
48
+ const normalized = [];
49
+ for (const subject of subjects) {
50
+ if (typeof subject !== 'object' || subject === null || Array.isArray(subject)) {
51
+ throw new InvalidSubjectsError('each subject must be an object with string type and id');
52
+ }
53
+ const { type, id } = subject;
54
+ if (typeof type !== 'string' || type.length === 0) {
55
+ throw new InvalidSubjectsError('a subject type must be a non-empty string');
56
+ }
57
+ if (typeof id !== 'string' || id.length === 0) {
58
+ throw new InvalidSubjectsError('a subject id must be a non-empty string');
59
+ }
60
+ if (type.length > SUBJECT_LIMITS.maxTypeLength) {
61
+ throw new InvalidSubjectsError(`a subject type may be at most ${SUBJECT_LIMITS.maxTypeLength} characters`);
62
+ }
63
+ if (id.length > SUBJECT_LIMITS.maxIdLength) {
64
+ throw new InvalidSubjectsError(`a subject id may be at most ${SUBJECT_LIMITS.maxIdLength} characters`);
65
+ }
66
+ // A joining delimiter cannot be trusted when it may appear in the values themselves;
67
+ // length-prefixing makes the key unambiguous whatever the strings contain.
68
+ const key = `${type.length}:${type}${id}`;
69
+ if (seen.has(key)) {
70
+ continue;
71
+ }
72
+ seen.add(key);
73
+ normalized.push({ type, id });
74
+ }
75
+ if (normalized.length === 0) {
76
+ return null;
77
+ }
78
+ if (normalized.length > SUBJECT_LIMITS.maxPerEvent) {
79
+ throw new InvalidSubjectsError(`an event may declare at most ${SUBJECT_LIMITS.maxPerEvent} subjects`);
80
+ }
81
+ return normalized;
82
+ }
83
+ //# sourceMappingURL=subjects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subjects.js","sourceRoot":"","sources":["../../src/subjects.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ import { type CommitRailEvent } from './envelope.js';
2
+ /** Registered globally by description, so every copy of this package agrees on it. */
3
+ declare const BRAND: unique symbol;
4
+ /**
5
+ * How much clock skew is tolerated by default, in seconds.
6
+ *
7
+ * Five minutes. It bounds replay: without it a captured request stays valid forever. Too tight
8
+ * and ordinary clock drift between two correct machines starts rejecting real deliveries.
9
+ */
10
+ export declare const DEFAULT_TOLERANCE_SECONDS = 300;
11
+ /** Why a delivery was rejected. Stable values — new ones may be added. */
12
+ export type InvalidDeliveryCode = 'missing_signature' | 'missing_delivery_id'
13
+ /** The signature header could not be parsed at all. */
14
+ | 'malformed_signature'
15
+ /** The signature was well formed but the timestamp is outside your tolerance. */
16
+ | 'timestamp_out_of_range'
17
+ /** The signature did not match: the wrong secret, or a body altered after signing. */
18
+ | 'signature_mismatch' | 'malformed_body'
19
+ /** A header CommitRail sent disagrees with the signed envelope. */
20
+ | 'header_mismatch';
21
+ /**
22
+ * A delivery that could not be trusted.
23
+ *
24
+ * `error instanceof InvalidDeliveryError` is the ordinary way to catch this and works in
25
+ * every normal application. It can fail in one specific case, and the case is created by
26
+ * this package being dual-published: an application that both `import`s and `require()`s
27
+ * `commitrail` loads two copies, with two distinct classes, and an error thrown by one
28
+ * is not an `instanceof` the other. `InvalidDeliveryError.is(error)` checks a shared
29
+ * symbol instead and is true across copies.
30
+ */
31
+ export declare class InvalidDeliveryError extends Error {
32
+ static readonly brand: symbol;
33
+ readonly [BRAND] = true;
34
+ /**
35
+ * Which check failed, for your logs.
36
+ *
37
+ * The message stays deliberately vague about *why* a signature did not verify, because the
38
+ * most common mistake is echoing it to the caller — and a wrong secret, a stale timestamp and
39
+ * a tampered body are three very different hints to hand an attacker. The code exists so your
40
+ * own logs do not have to be as careful as your responses.
41
+ *
42
+ * The distinction that matters in practice: `signature_invalid` after a deploy is almost
43
+ * always the wrong secret, and `malformed_body` is almost always a framework that parsed and
44
+ * re-serialised the body instead of giving you the bytes that were signed.
45
+ *
46
+ * Return a bare 400 or 401 to the caller. Log the code.
47
+ */
48
+ readonly code: InvalidDeliveryCode;
49
+ constructor(code: InvalidDeliveryCode, reason: string);
50
+ static is(error: unknown): error is InvalidDeliveryError;
51
+ }
52
+ export interface VerifyRequestInput {
53
+ /** Request headers, however your framework exposes them. Case-insensitive. */
54
+ headers: Record<string, string | string[] | undefined>;
55
+ /**
56
+ * The RAW request body, exactly as received.
57
+ *
58
+ * Not a parsed object re-serialised: `JSON.stringify` of a parsed body can differ from
59
+ * the bytes that were signed — key order, number formatting, whitespace — and the
60
+ * signature would fail for no reason a reader could see. Most frameworks need to be told
61
+ * to keep the raw body.
62
+ *
63
+ * Bytes are preferred, because a signature is over bytes: hand Express its `Buffer` or a
64
+ * `Uint8Array` straight through rather than calling `.toString()`, which is one more place to
65
+ * pick the wrong encoding. A string is accepted and equivalent — JSON is UTF-8, which
66
+ * round-trips exactly.
67
+ */
68
+ body: string | Uint8Array;
69
+ secret: string;
70
+ /** How much clock skew to tolerate, in seconds. Defaults to five minutes. */
71
+ toleranceSeconds?: number;
72
+ }
73
+ /**
74
+ * Verify a delivery and return its event.
75
+ *
76
+ * Throws rather than returning false, so a handler that forgets to check the result still
77
+ * fails closed.
78
+ *
79
+ * ```ts
80
+ * app.post('/webhooks/commitrail', async (request, reply) => {
81
+ * const event = verifyRequest({
82
+ * headers: request.headers,
83
+ * body: request.rawBody,
84
+ * secret: process.env.COMMITRAIL_SIGNING_SECRET!,
85
+ * });
86
+ *
87
+ * if (await alreadyProcessed(event.delivery.id)) return reply.code(200).send();
88
+ *
89
+ * await handle(event);
90
+ * await recordProcessed(event.delivery.id);
91
+ * return reply.code(200).send();
92
+ * });
93
+ * ```
94
+ *
95
+ * The deduplication is not decoration. Delivery is at-least-once: a timeout or a dropped
96
+ * connection means CommitRail never learned whether you processed the event, and it will
97
+ * try again.
98
+ */
99
+ export declare function verifyRequest<TData = unknown>(input: VerifyRequestInput): CommitRailEvent<TData>;
100
+ export {};
@@ -0,0 +1,209 @@
1
+ import { HEADERS } from './envelope.js';
2
+ import { verifySignatureHeader } from './signing.js';
3
+ /** Registered globally by description, so every copy of this package agrees on it. */
4
+ const BRAND = Symbol.for('commitrail.InvalidDeliveryError');
5
+ /**
6
+ * How much clock skew is tolerated by default, in seconds.
7
+ *
8
+ * Five minutes. It bounds replay: without it a captured request stays valid forever. Too tight
9
+ * and ordinary clock drift between two correct machines starts rejecting real deliveries.
10
+ */
11
+ export const DEFAULT_TOLERANCE_SECONDS = 300;
12
+ /**
13
+ * A delivery that could not be trusted.
14
+ *
15
+ * `error instanceof InvalidDeliveryError` is the ordinary way to catch this and works in
16
+ * every normal application. It can fail in one specific case, and the case is created by
17
+ * this package being dual-published: an application that both `import`s and `require()`s
18
+ * `commitrail` loads two copies, with two distinct classes, and an error thrown by one
19
+ * is not an `instanceof` the other. `InvalidDeliveryError.is(error)` checks a shared
20
+ * symbol instead and is true across copies.
21
+ */
22
+ export class InvalidDeliveryError extends Error {
23
+ static brand = BRAND;
24
+ [BRAND] = true;
25
+ /**
26
+ * Which check failed, for your logs.
27
+ *
28
+ * The message stays deliberately vague about *why* a signature did not verify, because the
29
+ * most common mistake is echoing it to the caller — and a wrong secret, a stale timestamp and
30
+ * a tampered body are three very different hints to hand an attacker. The code exists so your
31
+ * own logs do not have to be as careful as your responses.
32
+ *
33
+ * The distinction that matters in practice: `signature_invalid` after a deploy is almost
34
+ * always the wrong secret, and `malformed_body` is almost always a framework that parsed and
35
+ * re-serialised the body instead of giving you the bytes that were signed.
36
+ *
37
+ * Return a bare 400 or 401 to the caller. Log the code.
38
+ */
39
+ code;
40
+ constructor(code, reason) {
41
+ super(`CommitRail delivery rejected: ${reason}`);
42
+ this.name = 'InvalidDeliveryError';
43
+ this.code = code;
44
+ }
45
+ static is(error) {
46
+ return (typeof error === 'object' &&
47
+ error !== null &&
48
+ error[BRAND] === true);
49
+ }
50
+ }
51
+ /**
52
+ * Verify a delivery and return its event.
53
+ *
54
+ * Throws rather than returning false, so a handler that forgets to check the result still
55
+ * fails closed.
56
+ *
57
+ * ```ts
58
+ * app.post('/webhooks/commitrail', async (request, reply) => {
59
+ * const event = verifyRequest({
60
+ * headers: request.headers,
61
+ * body: request.rawBody,
62
+ * secret: process.env.COMMITRAIL_SIGNING_SECRET!,
63
+ * });
64
+ *
65
+ * if (await alreadyProcessed(event.delivery.id)) return reply.code(200).send();
66
+ *
67
+ * await handle(event);
68
+ * await recordProcessed(event.delivery.id);
69
+ * return reply.code(200).send();
70
+ * });
71
+ * ```
72
+ *
73
+ * The deduplication is not decoration. Delivery is at-least-once: a timeout or a dropped
74
+ * connection means CommitRail never learned whether you processed the event, and it will
75
+ * try again.
76
+ */
77
+ export function verifyRequest(input) {
78
+ const signature = header(input.headers, HEADERS.signature);
79
+ const deliveryId = header(input.headers, HEADERS.deliveryId);
80
+ if (signature === undefined) {
81
+ throw new InvalidDeliveryError('missing_signature', 'no signature header');
82
+ }
83
+ if (deliveryId === undefined) {
84
+ throw new InvalidDeliveryError('missing_delivery_id', 'no delivery id header');
85
+ }
86
+ /**
87
+ * The timestamp first, and separately, so the code can say which it was.
88
+ *
89
+ * One *message* for every cryptographic failure is right: the common mistake is echoing it to
90
+ * the caller, and "your clock is wrong" versus "your secret is wrong" are different hints to
91
+ * hand an attacker. That argument does not extend to a field you have to read deliberately —
92
+ * and these are the two an integrator most needs to tell apart, because a stale timestamp
93
+ * during setup is a clock problem and a mismatch is a secret or a raw-body problem, and they
94
+ * are fixed in completely different places.
95
+ */
96
+ const tolerance = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
97
+ const timestamp = signedTimestamp(signature);
98
+ if (timestamp === undefined) {
99
+ throw new InvalidDeliveryError('malformed_signature', 'signature did not verify');
100
+ }
101
+ if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > tolerance) {
102
+ throw new InvalidDeliveryError('timestamp_out_of_range', 'signature did not verify');
103
+ }
104
+ const valid = verifySignatureHeader({
105
+ header: signature,
106
+ secret: input.secret,
107
+ deliveryId,
108
+ body: input.body,
109
+ toleranceSeconds: tolerance,
110
+ });
111
+ if (!valid) {
112
+ throw new InvalidDeliveryError('signature_mismatch', 'signature did not verify');
113
+ }
114
+ let event;
115
+ try {
116
+ event = JSON.parse(typeof input.body === 'string' ? input.body : Buffer.from(input.body).toString('utf8'));
117
+ }
118
+ catch {
119
+ throw new InvalidDeliveryError('malformed_body', 'body was not valid JSON');
120
+ }
121
+ // Before touching a member of it. `JSON.parse('null')` is null and `null.delivery` is a
122
+ // TypeError, not an InvalidDeliveryError — so a handler doing `if (InvalidDeliveryError.is(e))`
123
+ // would miss it and rethrow, turning a rejected delivery into a 500. A verifier must never
124
+ // throw a type its caller has not been told about.
125
+ if (typeof event !== 'object' || event === null || Array.isArray(event)) {
126
+ throw new InvalidDeliveryError('malformed_body', 'the body is not a CommitRail envelope');
127
+ }
128
+ if (event.delivery?.id !== deliveryId) {
129
+ throw new InvalidDeliveryError('header_mismatch', 'delivery id in the body does not match the header');
130
+ }
131
+ assertHeadersAgree(input.headers, event, signature);
132
+ return event;
133
+ }
134
+ function header(headers, name) {
135
+ const direct = headers[name] ?? headers[name.toLowerCase()];
136
+ const value = Array.isArray(direct) ? direct[0] : direct;
137
+ if (value !== undefined) {
138
+ return value;
139
+ }
140
+ // Fall back to a case-insensitive scan for frameworks that preserve the original casing.
141
+ const match = Object.entries(headers).find(([key]) => key.toLowerCase() === name);
142
+ const found = match?.[1];
143
+ return Array.isArray(found) ? found[0] : found;
144
+ }
145
+ /**
146
+ * The envelope is signed. Most of the headers are not.
147
+ *
148
+ * `canonicalPayload` covers the timestamp, the delivery id and the body — so the body cannot be
149
+ * altered and the delivery id cannot be swapped. Everything else CommitRail sends is a
150
+ * convenience copy of something already inside the envelope, carried in a header so a consumer
151
+ * can route or log without parsing. Those copies are **not** covered by the signature.
152
+ *
153
+ * That matters because the obvious thing to do with `commitrail-idempotency-key` is deduplicate
154
+ * on it, and a value an attacker can edit is a poor thing to deduplicate on: replay a captured
155
+ * delivery with a fresh key and a consumer trusting the header processes it twice; send two
156
+ * genuine deliveries with one key and it drops one. Neither needs the signing secret.
157
+ *
158
+ * So every header that duplicates a signed field is checked against the envelope here, and the
159
+ * advice becomes true rather than merely stated: after `verifyRequest` returns, the headers
160
+ * agree with the envelope, and the envelope is what was signed.
161
+ *
162
+ * **Only a header that is present and disagrees is rejected.** An absent one is fine, and that is
163
+ * deliberate: CommitRail must stay free to stop sending a convenience header without every
164
+ * deployed verifier refusing the delivery. This is the same reasoning as `verifyRequest` never
165
+ * reading `specVersion` to decide anything — comparing two things CommitRail sent is not the same
166
+ * as requiring a particular version, and it must not quietly become that.
167
+ *
168
+ * `commitrail-attempt-id` is the one header with nothing in the envelope to check it against. It
169
+ * stays unauthenticated; do not make a decision on it.
170
+ */
171
+ function assertHeadersAgree(headers, event, signature) {
172
+ const timestamp = signedTimestamp(signature);
173
+ const expected = [
174
+ [HEADERS.specVersion, event.specVersion],
175
+ [HEADERS.idempotencyKey, event.delivery?.id],
176
+ [HEADERS.eventId, event.id],
177
+ [HEADERS.eventType, event.type],
178
+ [HEADERS.attemptNumber, event.delivery?.attempt?.toString()],
179
+ [HEADERS.timestamp, timestamp?.toString()],
180
+ ];
181
+ for (const [name, value] of expected) {
182
+ const sent = header(headers, name);
183
+ if (sent !== undefined && sent !== value) {
184
+ // Named, because unlike a signature failure this one tells an attacker nothing they did
185
+ // not already choose — and it is the sort of thing a proxy causes by rewriting headers.
186
+ throw new InvalidDeliveryError('header_mismatch', `${name} does not match the signed envelope`);
187
+ }
188
+ }
189
+ }
190
+ /** The `t=` a signature header carries, which is authenticated: it is inside the signed string. */
191
+ function signedTimestamp(header) {
192
+ const part = header
193
+ .split(',')
194
+ .map((p) => p.trim())
195
+ .find((p) => p.startsWith('t='));
196
+ if (part === undefined) {
197
+ return undefined;
198
+ }
199
+ const value = part.slice(2);
200
+ // Digits only, matching `verifySignatureHeader`. See the note on `parseTimestamp` there: a
201
+ // lenient parse means several header strings verify against one signature, and a verifier
202
+ // written from the specification would not agree with us about which.
203
+ if (!/^\d+$/.test(value)) {
204
+ return undefined;
205
+ }
206
+ const parsed = Number.parseInt(value, 10);
207
+ return Number.isSafeInteger(parsed) ? parsed : undefined;
208
+ }
209
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../src/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAwB,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAErD,sFAAsF;AACtF,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,GAAG,CAAC;AAgB7C;;;;;;;;;GASG;AACH,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,MAAM,CAAU,KAAK,GAAG,KAAK,CAAC;IAErB,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAExB;;;;;;;;;;;;;OAaG;IACM,IAAI,CAAsB;IAEnC,YAAY,IAAyB,EAAE,MAAc;QACnD,KAAK,CAAC,iCAAiC,MAAM,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,MAAM,CAAC,EAAE,CAAC,KAAc;QACtB,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;YACzB,KAAK,KAAK,IAAI;YACb,KAAiC,CAAC,KAAK,CAAC,KAAK,IAAI,CACnD,CAAC;IACJ,CAAC;;AA4BH;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,aAAa,CAAkB,KAAyB;IACtE,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC3D,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAE7D,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,IAAI,oBAAoB,CAAC,mBAAmB,EAAE,qBAAqB,CAAC,CAAC;IAC7E,CAAC;IAED,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,oBAAoB,CAAC,qBAAqB,EAAE,uBAAuB,CAAC,CAAC;IACjF,CAAC;IAED;;;;;;;;;OASG;IACH,MAAM,SAAS,GAAG,KAAK,CAAC,gBAAgB,IAAI,yBAAyB,CAAC;IACtE,MAAM,SAAS,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;IAE7C,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,IAAI,oBAAoB,CAAC,qBAAqB,EAAE,0BAA0B,CAAC,CAAC;IACpF,CAAC;IAED,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC,GAAG,SAAS,EAAE,CAAC;QACpE,MAAM,IAAI,oBAAoB,CAAC,wBAAwB,EAAE,0BAA0B,CAAC,CAAC;IACvF,CAAC;IAED,MAAM,KAAK,GAAG,qBAAqB,CAAC;QAClC,MAAM,EAAE,SAAS;QACjB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,UAAU;QACV,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,gBAAgB,EAAE,SAAS;KAC5B,CAAC,CAAC;IAEH,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,oBAAoB,CAAC,oBAAoB,EAAE,0BAA0B,CAAC,CAAC;IACnF,CAAC;IAED,IAAI,KAA6B,CAAC;IAElC,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAChB,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAC7D,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,oBAAoB,CAAC,gBAAgB,EAAE,yBAAyB,CAAC,CAAC;IAC9E,CAAC;IAED,wFAAwF;IACxF,gGAAgG;IAChG,2FAA2F;IAC3F,mDAAmD;IACnD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,oBAAoB,CAAC,gBAAgB,EAAE,uCAAuC,CAAC,CAAC;IAC5F,CAAC;IAED,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,KAAK,UAAU,EAAE,CAAC;QACtC,MAAM,IAAI,oBAAoB,CAC5B,iBAAiB,EACjB,mDAAmD,CACpD,CAAC;IACJ,CAAC;IAED,kBAAkB,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;IAEpD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,MAAM,CACb,OAAsD,EACtD,IAAY;IAEZ,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAEzD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,yFAAyF;IACzF,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,CAAC;IAClF,MAAM,KAAK,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;IAEzB,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AACjD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,SAAS,kBAAkB,CACzB,OAAsC,EACtC,KAA+B,EAC/B,SAAiB;IAEjB,MAAM,SAAS,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;IAE7C,MAAM,QAAQ,GAAgD;QAC5D,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC;QACxC,CAAC,OAAO,CAAC,cAAc,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC;QAC5C,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;QAC3B,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC;QAC/B,CAAC,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;QAC5D,CAAC,OAAO,CAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;KAC3C,CAAC;IAEF,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,QAAQ,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAEnC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;YACzC,wFAAwF;YACxF,wFAAwF;YACxF,MAAM,IAAI,oBAAoB,CAC5B,iBAAiB,EACjB,GAAG,IAAI,qCAAqC,CAC7C,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,mGAAmG;AACnG,SAAS,eAAe,CAAC,MAAc;IACrC,MAAM,IAAI,GAAG,MAAM;SAChB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IAEnC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE5B,2FAA2F;IAC3F,0FAA0F;IAC1F,sEAAsE;IACtE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAE1C,OAAO,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3D,CAAC"}