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.
- package/LICENSE +201 -0
- package/README.md +12 -0
- package/dist/cjs/envelope.d.ts +91 -0
- package/dist/cjs/envelope.js +54 -0
- package/dist/cjs/envelope.js.map +1 -0
- package/dist/cjs/index.d.ts +20 -0
- package/dist/cjs/index.js +29 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/postgres.d.ts +152 -0
- package/dist/cjs/postgres.js +373 -0
- package/dist/cjs/postgres.js.map +1 -0
- package/dist/cjs/signing.d.ts +66 -0
- package/dist/cjs/signing.js +108 -0
- package/dist/cjs/signing.js.map +1 -0
- package/dist/cjs/subjects.d.ts +53 -0
- package/dist/cjs/subjects.js +88 -0
- package/dist/cjs/subjects.js.map +1 -0
- package/dist/cjs/webhooks.d.ts +100 -0
- package/dist/cjs/webhooks.js +214 -0
- package/dist/cjs/webhooks.js.map +1 -0
- package/dist/esm/envelope.d.ts +91 -0
- package/dist/esm/envelope.js +50 -0
- package/dist/esm/envelope.js.map +1 -0
- package/dist/esm/index.d.ts +20 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/postgres.d.ts +152 -0
- package/dist/esm/postgres.js +366 -0
- package/dist/esm/postgres.js.map +1 -0
- package/dist/esm/signing.d.ts +66 -0
- package/dist/esm/signing.js +101 -0
- package/dist/esm/signing.js.map +1 -0
- package/dist/esm/subjects.d.ts +53 -0
- package/dist/esm/subjects.js +83 -0
- package/dist/esm/subjects.js.map +1 -0
- package/dist/esm/webhooks.d.ts +100 -0
- package/dist/esm/webhooks.js +209 -0
- package/dist/esm/webhooks.js.map +1 -0
- 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":"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"}
|