@atlasauth/backend 0.1.1 → 0.1.2

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.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './jwks-cache';
2
2
  export * from './verify';
3
3
  export * from './handshake';
4
+ export * from './webhooks';
4
5
  export { createAtlasClient } from './client';
5
6
  export type { AtlasClient } from './client';
6
7
  export * from './client';
package/dist/index.js CHANGED
@@ -19,6 +19,8 @@ exports.createAtlasClient = void 0;
19
19
  __exportStar(require("./jwks-cache"), exports);
20
20
  __exportStar(require("./verify"), exports);
21
21
  __exportStar(require("./handshake"), exports);
22
+ // §12.2 inbound webhook verification for consumers.
23
+ __exportStar(require("./webhooks"), exports);
22
24
  // The typed management client for the secret-key Backend API.
23
25
  var client_1 = require("./client");
24
26
  Object.defineProperty(exports, "createAtlasClient", { enumerable: true, get: function () { return client_1.createAtlasClient; } });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,6DAA6D;AAC7D,+CAA6B;AAC7B,2CAAyB;AACzB,8CAA4B;AAE5B,8DAA8D;AAC9D,mCAA6C;AAApC,2GAAA,iBAAiB,OAAA;AAE1B,2CAAyB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,6DAA6D;AAC7D,+CAA6B;AAC7B,2CAAyB;AACzB,8CAA4B;AAE5B,oDAAoD;AACpD,6CAA2B;AAE3B,8DAA8D;AAC9D,mCAA6C;AAApC,2GAAA,iBAAiB,OAAA;AAE1B,2CAAyB"}
@@ -0,0 +1,45 @@
1
+ /** Header names Atlas sends on every delivery (lower-case; match case-insensitively). */
2
+ export declare const ATLAS_WEBHOOK_HEADERS: {
3
+ readonly id: "atlas-id";
4
+ readonly timestamp: "atlas-timestamp";
5
+ readonly signature: "atlas-signature";
6
+ };
7
+ export type WebhookVerifyFailure = 'missing_headers' | 'malformed_signature' | 'stale_timestamp' | 'signature_mismatch';
8
+ export interface WebhookVerifyResult<T = unknown> {
9
+ /** True only when the signature and timestamp both check out. */
10
+ verified: boolean;
11
+ /** Why verification failed, when it did. */
12
+ reason?: WebhookVerifyFailure;
13
+ /** The parsed event envelope `{ id, type, timestamp, instance_id, data }` — present only when `verified`. */
14
+ event?: T;
15
+ }
16
+ type HeadersLike = Headers | Record<string, string | string[] | undefined>;
17
+ export interface VerifyWebhookInput {
18
+ /** The RAW request body as received — a string or Buffer, NOT a parsed object. */
19
+ body: string | Buffer;
20
+ /** The request headers (a plain object, Node `req.headers`, or a Fetch `Headers`). */
21
+ headers: HeadersLike;
22
+ /** The endpoint's signing secret (shown once when the endpoint was created/rotated). */
23
+ secret: string;
24
+ /** Override "now" (tests); defaults to the current time. */
25
+ now?: Date;
26
+ /** Override the ±replay tolerance (defaults to 5 minutes). */
27
+ toleranceMs?: number;
28
+ }
29
+ /**
30
+ * Verify and parse an Atlas webhook. Returns `{ verified, reason?, event? }` —
31
+ * it never throws on a bad signature (a thrown verifier is easy to accidentally
32
+ * swallow). On success, `event` is the parsed envelope.
33
+ *
34
+ * ```ts
35
+ * // Express (raw body):
36
+ * app.post('/webhooks/atlas', express.raw({ type: 'application/json' }), (req, res) => {
37
+ * const { verified, event } = verifyWebhook({ body: req.body, headers: req.headers, secret });
38
+ * if (!verified) return res.sendStatus(400);
39
+ * // handle event.type ...
40
+ * res.sendStatus(200);
41
+ * });
42
+ * ```
43
+ */
44
+ export declare function verifyWebhook<T = unknown>(input: VerifyWebhookInput): WebhookVerifyResult<T>;
45
+ export {};
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ATLAS_WEBHOOK_HEADERS = void 0;
4
+ exports.verifyWebhook = verifyWebhook;
5
+ const node_crypto_1 = require("node:crypto");
6
+ /**
7
+ * §12.2 — verify an inbound Atlas webhook, for the CUSTOMER consuming events.
8
+ *
9
+ * Atlas signs every delivery: `atlas-signature: v1,<base64>` is HMAC-SHA256 over
10
+ * `<id>.<timestamp>.<rawBody>` keyed with the endpoint secret, with `atlas-id`
11
+ * and `atlas-timestamp` (epoch ms) headers. The timestamp is INSIDE the signed
12
+ * material — a header-only timestamp would let an attacker replay a captured
13
+ * body under a fresh timestamp — and a delivery older than 5 minutes is rejected
14
+ * so a captured request cannot be replayed later.
15
+ *
16
+ * The signing logic is identical to the server's (`@atlas/core` `verifyWebhook`),
17
+ * reimplemented here with zero dependencies so an integrator gets a first-party
18
+ * verifier without pulling in Atlas internals. Verify over the RAW request body,
19
+ * BEFORE any JSON parse — re-serializing reorders keys and breaks the MAC.
20
+ */
21
+ const SIGNATURE_VERSION = 'v1';
22
+ const REPLAY_TOLERANCE_MS = 5 * 60_000;
23
+ /** Header names Atlas sends on every delivery (lower-case; match case-insensitively). */
24
+ exports.ATLAS_WEBHOOK_HEADERS = {
25
+ id: 'atlas-id',
26
+ timestamp: 'atlas-timestamp',
27
+ signature: 'atlas-signature',
28
+ };
29
+ /** Read a header case-insensitively from a plain object or a Fetch `Headers`. */
30
+ function readHeader(headers, name) {
31
+ if (typeof headers.get === 'function') {
32
+ return headers.get(name) ?? undefined;
33
+ }
34
+ const bag = headers;
35
+ const direct = bag[name] ?? bag[name.toLowerCase()];
36
+ const v = Array.isArray(direct) ? direct[0] : direct;
37
+ if (v !== undefined)
38
+ return v;
39
+ // Fall back to a case-insensitive scan (Node lower-cases, but be safe).
40
+ const hit = Object.keys(bag).find((k) => k.toLowerCase() === name);
41
+ const found = hit ? bag[hit] : undefined;
42
+ return Array.isArray(found) ? found[0] : found;
43
+ }
44
+ /**
45
+ * Verify and parse an Atlas webhook. Returns `{ verified, reason?, event? }` —
46
+ * it never throws on a bad signature (a thrown verifier is easy to accidentally
47
+ * swallow). On success, `event` is the parsed envelope.
48
+ *
49
+ * ```ts
50
+ * // Express (raw body):
51
+ * app.post('/webhooks/atlas', express.raw({ type: 'application/json' }), (req, res) => {
52
+ * const { verified, event } = verifyWebhook({ body: req.body, headers: req.headers, secret });
53
+ * if (!verified) return res.sendStatus(400);
54
+ * // handle event.type ...
55
+ * res.sendStatus(200);
56
+ * });
57
+ * ```
58
+ */
59
+ function verifyWebhook(input) {
60
+ const raw = typeof input.body === 'string' ? input.body : input.body.toString('utf8');
61
+ const id = readHeader(input.headers, exports.ATLAS_WEBHOOK_HEADERS.id);
62
+ const timestampRaw = readHeader(input.headers, exports.ATLAS_WEBHOOK_HEADERS.timestamp);
63
+ const signature = readHeader(input.headers, exports.ATLAS_WEBHOOK_HEADERS.signature);
64
+ if (!id || !timestampRaw || !signature)
65
+ return { verified: false, reason: 'missing_headers' };
66
+ const [version, provided] = signature.split(',');
67
+ if (version !== SIGNATURE_VERSION || !provided)
68
+ return { verified: false, reason: 'malformed_signature' };
69
+ const timestamp = Number(timestampRaw);
70
+ const now = (input.now ?? new Date()).getTime();
71
+ const tolerance = input.toleranceMs ?? REPLAY_TOLERANCE_MS;
72
+ // Rejected in both directions: a far-future timestamp is as suspicious as a
73
+ // stale one — accepting it would let an attacker mint a long-lived replay.
74
+ if (!Number.isFinite(timestamp) || Math.abs(now - timestamp) > tolerance) {
75
+ return { verified: false, reason: 'stale_timestamp' };
76
+ }
77
+ const expected = (0, node_crypto_1.createHmac)('sha256', input.secret).update(`${id}.${timestamp}.${raw}`).digest('base64');
78
+ const a = Buffer.from(provided);
79
+ const b = Buffer.from(expected);
80
+ if (a.length !== b.length || !(0, node_crypto_1.timingSafeEqual)(a, b))
81
+ return { verified: false, reason: 'signature_mismatch' };
82
+ try {
83
+ return { verified: true, event: JSON.parse(raw) };
84
+ }
85
+ catch {
86
+ // Signature is valid but the body isn't JSON — surface it as verified with no event.
87
+ return { verified: true };
88
+ }
89
+ }
90
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlasauth/backend",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "dependencies": {