@atlasauth/backend 0.28.0 → 0.30.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/CHANGELOG.md +19 -0
- package/README.md +30 -0
- package/dist/backchannel-handlers.js +2 -2
- package/dist/backchannel-handlers.js.map +1 -1
- package/dist/client/domains.d.ts +33 -3
- package/dist/client/domains.js.map +1 -1
- package/dist/client/index.d.ts +53 -11
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/jwt-connections.d.ts +46 -0
- package/dist/client/jwt-connections.js +24 -0
- package/dist/client/jwt-connections.js.map +1 -0
- package/dist/client/org-ldap-connections.d.ts +10 -0
- package/dist/client/org-ldap-connections.js.map +1 -1
- package/dist/client/organizations.d.ts +106 -8
- package/dist/client/organizations.js +35 -3
- package/dist/client/organizations.js.map +1 -1
- package/dist/client/referrals.d.ts +35 -6
- package/dist/client/referrals.js +2 -2
- package/dist/client/referrals.js.map +1 -1
- package/dist/client/scim-tokens.d.ts +7 -0
- package/dist/client/scim-tokens.js.map +1 -1
- package/dist/client/sso-connections.d.ts +14 -0
- package/dist/client/sso-connections.js.map +1 -1
- package/dist/client/sso-onboarding.d.ts +14 -1
- package/dist/client/sso-onboarding.js +3 -2
- package/dist/client/sso-onboarding.js.map +1 -1
- package/dist/client/users.d.ts +42 -2
- package/dist/client/users.js +18 -0
- package/dist/client/users.js.map +1 -1
- package/dist/jwks-cache.d.ts +27 -0
- package/dist/jwks-cache.js +45 -4
- package/dist/jwks-cache.js.map +1 -1
- package/dist/oidc.d.ts +35 -1
- package/dist/oidc.js +39 -0
- package/dist/oidc.js.map +1 -1
- package/dist/rp.d.ts +189 -0
- package/dist/rp.js +224 -0
- package/dist/rp.js.map +1 -0
- package/dist/verify.d.ts +8 -0
- package/dist/verify.js +3 -1
- package/dist/verify.js.map +1 -1
- package/dist/webhook-common.d.ts +53 -0
- package/dist/webhook-common.js +48 -0
- package/dist/webhook-common.js.map +1 -0
- package/dist/webhooks-web.d.ts +24 -0
- package/dist/webhooks-web.js +82 -0
- package/dist/webhooks-web.js.map +1 -0
- package/dist/webhooks.d.ts +23 -41
- package/dist/webhooks.js +19 -70
- package/dist/webhooks.js.map +1 -1
- package/package.json +26 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type WebhookVerifyOptions, type WebhookVerifyResult } from './webhook-common';
|
|
2
|
+
export interface VerifyAtlasWebhookInput extends WebhookVerifyOptions {
|
|
3
|
+
/** The RAW request body: the string or bytes exactly as received, never a parsed object. */
|
|
4
|
+
body: string | Uint8Array | ArrayBuffer;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The Node `verifyWebhook` on Web Crypto only (no `Buffer`, no `node:crypto`), so
|
|
8
|
+
* it runs unchanged on Node 18+, Deno, Bun and Workers. Same signed material,
|
|
9
|
+
* same 5-minute window in both directions, same `v1` / `v2` handling, same
|
|
10
|
+
* result shape; never throws on a bad delivery.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const { verified, event, deliveryId } = await verifyAtlasWebhook({
|
|
14
|
+
* body: await request.text(), headers: request.headers, secret: env.ATLAS_WEBHOOK_SECRET,
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function verifyAtlasWebhook<T = unknown>(input: VerifyAtlasWebhookInput): Promise<WebhookVerifyResult<T>>;
|
|
19
|
+
/**
|
|
20
|
+
* {@link verifyAtlasWebhook} straight from a Fetch `Request` (Hono, Workers,
|
|
21
|
+
* Deno, Bun, Next.js route handlers). Reads the body once, as text; do not
|
|
22
|
+
* read it before calling this.
|
|
23
|
+
*/
|
|
24
|
+
export declare function verifyAtlasWebhookRequest<T = unknown>(request: Request, options: Omit<VerifyAtlasWebhookInput, 'body' | 'headers'>): Promise<WebhookVerifyResult<T>>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyAtlasWebhook = verifyAtlasWebhook;
|
|
4
|
+
exports.verifyAtlasWebhookRequest = verifyAtlasWebhookRequest;
|
|
5
|
+
const webhook_common_1 = require("./webhook-common");
|
|
6
|
+
/**
|
|
7
|
+
* The Node `verifyWebhook` on Web Crypto only (no `Buffer`, no `node:crypto`), so
|
|
8
|
+
* it runs unchanged on Node 18+, Deno, Bun and Workers. Same signed material,
|
|
9
|
+
* same 5-minute window in both directions, same `v1` / `v2` handling, same
|
|
10
|
+
* result shape; never throws on a bad delivery.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const { verified, event, deliveryId } = await verifyAtlasWebhook({
|
|
14
|
+
* body: await request.text(), headers: request.headers, secret: env.ATLAS_WEBHOOK_SECRET,
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
async function verifyAtlasWebhook(input) {
|
|
19
|
+
const raw = typeof input.body === 'string' ? input.body : new TextDecoder().decode(input.body);
|
|
20
|
+
const id = (0, webhook_common_1.readHeader)(input.headers, webhook_common_1.ATLAS_WEBHOOK_HEADERS.id);
|
|
21
|
+
const timestampRaw = (0, webhook_common_1.readHeader)(input.headers, webhook_common_1.ATLAS_WEBHOOK_HEADERS.timestamp);
|
|
22
|
+
const signature = (0, webhook_common_1.readHeader)(input.headers, webhook_common_1.ATLAS_WEBHOOK_HEADERS.signature);
|
|
23
|
+
if (!id || !timestampRaw || !signature)
|
|
24
|
+
return { verified: false, reason: 'missing_headers' };
|
|
25
|
+
if (typeof input.secret !== 'string' || input.secret === '')
|
|
26
|
+
return { verified: false, reason: 'signature_mismatch' };
|
|
27
|
+
const provided = input.requireV2 ? [] : (0, webhook_common_1.parseSignatures)(signature);
|
|
28
|
+
const providedV2 = input.url ? (0, webhook_common_1.parseSignatures)(signature, 'v2') : [];
|
|
29
|
+
if (provided.length === 0 && providedV2.length === 0)
|
|
30
|
+
return { verified: false, reason: 'malformed_signature' };
|
|
31
|
+
const timestamp = Number(timestampRaw);
|
|
32
|
+
const now = (input.now ?? new Date()).getTime();
|
|
33
|
+
if (!Number.isFinite(timestamp) || Math.abs(now - timestamp) > (input.toleranceMs ?? webhook_common_1.REPLAY_TOLERANCE_MS)) {
|
|
34
|
+
return { verified: false, reason: 'stale_timestamp' };
|
|
35
|
+
}
|
|
36
|
+
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(input.secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
37
|
+
const mac = async (material) => toBase64(new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(material))));
|
|
38
|
+
let matched = false;
|
|
39
|
+
if (provided.length > 0) {
|
|
40
|
+
const expected = await mac(`${id}.${timestamp}.${raw}`);
|
|
41
|
+
for (const candidate of provided)
|
|
42
|
+
if (constantTimeEqual(candidate, expected))
|
|
43
|
+
matched = true;
|
|
44
|
+
}
|
|
45
|
+
if (providedV2.length > 0) {
|
|
46
|
+
const expected = await mac(`${id}.${timestamp}.${input.url}.${raw}`);
|
|
47
|
+
for (const candidate of providedV2)
|
|
48
|
+
if (constantTimeEqual(candidate, expected))
|
|
49
|
+
matched = true;
|
|
50
|
+
}
|
|
51
|
+
if (!matched)
|
|
52
|
+
return { verified: false, reason: 'signature_mismatch' };
|
|
53
|
+
try {
|
|
54
|
+
return { verified: true, event: JSON.parse(raw), deliveryId: id, timestamp };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { verified: true, deliveryId: id, timestamp };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* {@link verifyAtlasWebhook} straight from a Fetch `Request` (Hono, Workers,
|
|
62
|
+
* Deno, Bun, Next.js route handlers). Reads the body once, as text; do not
|
|
63
|
+
* read it before calling this.
|
|
64
|
+
*/
|
|
65
|
+
async function verifyAtlasWebhookRequest(request, options) {
|
|
66
|
+
return verifyAtlasWebhook({ ...options, body: await request.text(), headers: request.headers });
|
|
67
|
+
}
|
|
68
|
+
function toBase64(bytes) {
|
|
69
|
+
let bin = '';
|
|
70
|
+
for (const b of bytes)
|
|
71
|
+
bin += String.fromCharCode(b);
|
|
72
|
+
return btoa(bin);
|
|
73
|
+
}
|
|
74
|
+
/** Compares every byte whatever the lengths, so timing reveals neither where nor whether they differ early. */
|
|
75
|
+
function constantTimeEqual(a, b) {
|
|
76
|
+
let diff = a.length ^ b.length;
|
|
77
|
+
const n = Math.max(a.length, b.length);
|
|
78
|
+
for (let i = 0; i < n; i++)
|
|
79
|
+
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
80
|
+
return diff === 0;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=webhooks-web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks-web.js","sourceRoot":"","sources":["../src/webhooks-web.ts"],"names":[],"mappings":"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"}
|
package/dist/webhooks.d.ts
CHANGED
|
@@ -1,45 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export interface VerifyWebhookInput {
|
|
1
|
+
import { type HeadersLike, type WebhookVerifyOptions, type WebhookVerifyResult } from './webhook-common';
|
|
2
|
+
export { ATLAS_WEBHOOK_HEADERS } from './webhook-common';
|
|
3
|
+
export type { WebhookVerifyFailure, WebhookVerifyOptions, WebhookVerifyResult } from './webhook-common';
|
|
4
|
+
export { verifyAtlasWebhook, verifyAtlasWebhookRequest } from './webhooks-web';
|
|
5
|
+
export type { VerifyAtlasWebhookInput } from './webhooks-web';
|
|
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. The header may carry several
|
|
15
|
+
* space-separated `v1,<base64>` entries (one per secret while a secret rotates
|
|
16
|
+
* with an overlap); the delivery verifies when any one of them matches.
|
|
17
|
+
*
|
|
18
|
+
* The signing logic is identical to the server's (`@atlas/core` `verifyWebhook`),
|
|
19
|
+
* reimplemented here with zero dependencies so an integrator gets a first-party
|
|
20
|
+
* verifier without pulling in Atlas internals. Verify over the RAW request body,
|
|
21
|
+
* BEFORE any JSON parse — re-serializing reorders keys and breaks the MAC.
|
|
22
|
+
*/
|
|
23
|
+
export interface VerifyWebhookInput extends WebhookVerifyOptions {
|
|
25
24
|
/** The RAW request body as received — a string or Buffer, NOT a parsed object. */
|
|
26
25
|
body: string | Buffer;
|
|
27
|
-
/** The request headers (a plain object, Node `req.headers`, or a Fetch `Headers`). */
|
|
28
|
-
headers: HeadersLike;
|
|
29
|
-
/** The endpoint's signing secret (shown once when the endpoint was created/rotated). */
|
|
30
|
-
secret: string;
|
|
31
|
-
/** Override "now" (tests); defaults to the current time. */
|
|
32
|
-
now?: Date;
|
|
33
|
-
/** Override the ±replay tolerance (defaults to 5 minutes). */
|
|
34
|
-
toleranceMs?: number;
|
|
35
|
-
/**
|
|
36
|
-
* The exact URL Atlas POSTed to (your endpoint's `url`). When given, `v2`
|
|
37
|
-
* signature entries — which also cover the URL, for endpoints with
|
|
38
|
-
* `signature_versions` including `v2` — are checked too.
|
|
39
|
-
*/
|
|
40
|
-
url?: string;
|
|
41
|
-
/** Accept only a `v2` entry (needs `url`): a delivery replayed to another URL then fails. */
|
|
42
|
-
requireV2?: boolean;
|
|
43
26
|
}
|
|
44
27
|
/**
|
|
45
28
|
* Verify and parse an Atlas webhook. Returns `{ verified, reason?, event?,
|
|
@@ -92,4 +75,3 @@ export interface VerifyStandardWebhookInput {
|
|
|
92
75
|
* ```
|
|
93
76
|
*/
|
|
94
77
|
export declare function verifyStandardWebhook<T = unknown>(input: VerifyStandardWebhookInput): WebhookVerifyResult<T>;
|
|
95
|
-
export {};
|
package/dist/webhooks.js
CHANGED
|
@@ -1,67 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.STANDARD_WEBHOOK_HEADERS = exports.ATLAS_WEBHOOK_HEADERS = void 0;
|
|
3
|
+
exports.STANDARD_WEBHOOK_HEADERS = exports.verifyAtlasWebhookRequest = exports.verifyAtlasWebhook = exports.ATLAS_WEBHOOK_HEADERS = void 0;
|
|
4
4
|
exports.verifyWebhook = verifyWebhook;
|
|
5
5
|
exports.verifyStandardWebhook = verifyStandardWebhook;
|
|
6
6
|
const edge_crypto_1 = require("./edge-crypto");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* body under a fresh timestamp — and a delivery older than 5 minutes is rejected
|
|
15
|
-
* so a captured request cannot be replayed later. The header may carry several
|
|
16
|
-
* space-separated `v1,<base64>` entries (one per secret while a secret rotates
|
|
17
|
-
* with an overlap); the delivery verifies when any one of them matches.
|
|
18
|
-
*
|
|
19
|
-
* The signing logic is identical to the server's (`@atlas/core` `verifyWebhook`),
|
|
20
|
-
* reimplemented here with zero dependencies so an integrator gets a first-party
|
|
21
|
-
* verifier without pulling in Atlas internals. Verify over the RAW request body,
|
|
22
|
-
* BEFORE any JSON parse — re-serializing reorders keys and breaks the MAC.
|
|
23
|
-
*/
|
|
24
|
-
const SIGNATURE_VERSION = 'v1';
|
|
25
|
-
const REPLAY_TOLERANCE_MS = 5 * 60_000;
|
|
26
|
-
/** Header names Atlas sends on every delivery (lower-case; match case-insensitively). */
|
|
27
|
-
exports.ATLAS_WEBHOOK_HEADERS = {
|
|
28
|
-
id: 'atlas-id',
|
|
29
|
-
timestamp: 'atlas-timestamp',
|
|
30
|
-
signature: 'atlas-signature',
|
|
31
|
-
};
|
|
32
|
-
/** Read a header case-insensitively from a plain object or a Fetch `Headers`. */
|
|
33
|
-
function readHeader(headers, name) {
|
|
34
|
-
if (typeof headers.get === 'function') {
|
|
35
|
-
return headers.get(name) ?? undefined;
|
|
36
|
-
}
|
|
37
|
-
const bag = headers;
|
|
38
|
-
const direct = bag[name] ?? bag[name.toLowerCase()];
|
|
39
|
-
const v = Array.isArray(direct) ? direct[0] : direct;
|
|
40
|
-
if (v !== undefined)
|
|
41
|
-
return v;
|
|
42
|
-
// Fall back to a case-insensitive scan (Node lower-cases, but be safe).
|
|
43
|
-
const hit = Object.keys(bag).find((k) => k.toLowerCase() === name);
|
|
44
|
-
const found = hit ? bag[hit] : undefined;
|
|
45
|
-
return Array.isArray(found) ? found[0] : found;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* The `v1` signatures in an `atlas-signature` header. The header carries one
|
|
49
|
-
* `v1,<base64>` entry, or several separated by spaces (while an endpoint
|
|
50
|
-
* secret rotates, Atlas signs with the old and the new secret). Entries of
|
|
51
|
-
* another version are skipped, so a future scheme never breaks this verifier.
|
|
52
|
-
*/
|
|
53
|
-
function parseSignatures(header, version = SIGNATURE_VERSION) {
|
|
54
|
-
const out = [];
|
|
55
|
-
for (const entry of header.trim().split(/\s+/)) {
|
|
56
|
-
const comma = entry.indexOf(',');
|
|
57
|
-
if (comma <= 0)
|
|
58
|
-
continue;
|
|
59
|
-
const value = entry.slice(comma + 1);
|
|
60
|
-
if (entry.slice(0, comma) === version && value)
|
|
61
|
-
out.push(value);
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
7
|
+
const webhook_common_1 = require("./webhook-common");
|
|
8
|
+
var webhook_common_2 = require("./webhook-common");
|
|
9
|
+
Object.defineProperty(exports, "ATLAS_WEBHOOK_HEADERS", { enumerable: true, get: function () { return webhook_common_2.ATLAS_WEBHOOK_HEADERS; } });
|
|
10
|
+
// The Web-standards verifier lives in its own module (the rp entry loads only that one).
|
|
11
|
+
var webhooks_web_1 = require("./webhooks-web");
|
|
12
|
+
Object.defineProperty(exports, "verifyAtlasWebhook", { enumerable: true, get: function () { return webhooks_web_1.verifyAtlasWebhook; } });
|
|
13
|
+
Object.defineProperty(exports, "verifyAtlasWebhookRequest", { enumerable: true, get: function () { return webhooks_web_1.verifyAtlasWebhookRequest; } });
|
|
65
14
|
/**
|
|
66
15
|
* Verify and parse an Atlas webhook. Returns `{ verified, reason?, event?,
|
|
67
16
|
* deliveryId?, timestamp? }` — it never throws on a bad signature (a thrown
|
|
@@ -83,18 +32,18 @@ function parseSignatures(header, version = SIGNATURE_VERSION) {
|
|
|
83
32
|
*/
|
|
84
33
|
function verifyWebhook(input) {
|
|
85
34
|
const raw = typeof input.body === 'string' ? input.body : input.body.toString('utf8');
|
|
86
|
-
const id = readHeader(input.headers,
|
|
87
|
-
const timestampRaw = readHeader(input.headers,
|
|
88
|
-
const signature = readHeader(input.headers,
|
|
35
|
+
const id = (0, webhook_common_1.readHeader)(input.headers, webhook_common_1.ATLAS_WEBHOOK_HEADERS.id);
|
|
36
|
+
const timestampRaw = (0, webhook_common_1.readHeader)(input.headers, webhook_common_1.ATLAS_WEBHOOK_HEADERS.timestamp);
|
|
37
|
+
const signature = (0, webhook_common_1.readHeader)(input.headers, webhook_common_1.ATLAS_WEBHOOK_HEADERS.signature);
|
|
89
38
|
if (!id || !timestampRaw || !signature)
|
|
90
39
|
return { verified: false, reason: 'missing_headers' };
|
|
91
|
-
const provided = input.requireV2 ? [] : parseSignatures(signature);
|
|
92
|
-
const providedV2 = input.url ? parseSignatures(signature, 'v2') : [];
|
|
40
|
+
const provided = input.requireV2 ? [] : (0, webhook_common_1.parseSignatures)(signature);
|
|
41
|
+
const providedV2 = input.url ? (0, webhook_common_1.parseSignatures)(signature, 'v2') : [];
|
|
93
42
|
if (provided.length === 0 && providedV2.length === 0)
|
|
94
43
|
return { verified: false, reason: 'malformed_signature' };
|
|
95
44
|
const timestamp = Number(timestampRaw);
|
|
96
45
|
const now = (input.now ?? new Date()).getTime();
|
|
97
|
-
const tolerance = input.toleranceMs ?? REPLAY_TOLERANCE_MS;
|
|
46
|
+
const tolerance = input.toleranceMs ?? webhook_common_1.REPLAY_TOLERANCE_MS;
|
|
98
47
|
// Rejected in both directions: a far-future timestamp is as suspicious as a
|
|
99
48
|
// stale one — accepting it would let an attacker mint a long-lived replay.
|
|
100
49
|
if (!Number.isFinite(timestamp) || Math.abs(now - timestamp) > tolerance) {
|
|
@@ -148,17 +97,17 @@ exports.STANDARD_WEBHOOK_HEADERS = {
|
|
|
148
97
|
*/
|
|
149
98
|
function verifyStandardWebhook(input) {
|
|
150
99
|
const raw = typeof input.body === 'string' ? input.body : input.body.toString('utf8');
|
|
151
|
-
const id = readHeader(input.headers, exports.STANDARD_WEBHOOK_HEADERS.id);
|
|
152
|
-
const timestampRaw = readHeader(input.headers, exports.STANDARD_WEBHOOK_HEADERS.timestamp);
|
|
153
|
-
const signature = readHeader(input.headers, exports.STANDARD_WEBHOOK_HEADERS.signature);
|
|
100
|
+
const id = (0, webhook_common_1.readHeader)(input.headers, exports.STANDARD_WEBHOOK_HEADERS.id);
|
|
101
|
+
const timestampRaw = (0, webhook_common_1.readHeader)(input.headers, exports.STANDARD_WEBHOOK_HEADERS.timestamp);
|
|
102
|
+
const signature = (0, webhook_common_1.readHeader)(input.headers, exports.STANDARD_WEBHOOK_HEADERS.signature);
|
|
154
103
|
if (!id || !timestampRaw || !signature)
|
|
155
104
|
return { verified: false, reason: 'missing_headers' };
|
|
156
|
-
const provided = parseSignatures(signature);
|
|
105
|
+
const provided = (0, webhook_common_1.parseSignatures)(signature);
|
|
157
106
|
if (provided.length === 0)
|
|
158
107
|
return { verified: false, reason: 'malformed_signature' };
|
|
159
108
|
const timestamp = /^\d+$/.test(timestampRaw) ? Number(timestampRaw) : NaN;
|
|
160
109
|
const nowSec = Math.floor((input.now ?? new Date()).getTime() / 1000);
|
|
161
|
-
if (!Number.isFinite(timestamp) || Math.abs(nowSec - timestamp) > (input.toleranceSec ?? REPLAY_TOLERANCE_MS / 1000)) {
|
|
110
|
+
if (!Number.isFinite(timestamp) || Math.abs(nowSec - timestamp) > (input.toleranceSec ?? webhook_common_1.REPLAY_TOLERANCE_MS / 1000)) {
|
|
162
111
|
return { verified: false, reason: 'stale_timestamp' };
|
|
163
112
|
}
|
|
164
113
|
const key = Buffer.from(input.secret.startsWith('whsec_') ? input.secret.slice('whsec_'.length) : input.secret, 'base64');
|
package/dist/webhooks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlasauth/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./rp": {
|
|
12
|
+
"types": "./dist/rp.d.ts",
|
|
13
|
+
"default": "./dist/rp.js"
|
|
14
|
+
},
|
|
15
|
+
"./testing": {
|
|
16
|
+
"types": "./dist/testing.d.ts",
|
|
17
|
+
"default": "./dist/testing.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"typesVersions": {
|
|
22
|
+
"*": {
|
|
23
|
+
"rp": [
|
|
24
|
+
"./dist/rp.d.ts"
|
|
25
|
+
],
|
|
26
|
+
"testing": [
|
|
27
|
+
"./dist/testing.d.ts"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
6
31
|
"dependencies": {
|
|
7
32
|
"jose": "^5.9.6",
|
|
8
33
|
"@noble/ciphers": "^1.3.0",
|