@heyatom/bastion-client 0.2.0 → 0.3.1

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 (56) hide show
  1. package/README.md +32 -1
  2. package/dist/cjs/core/api-client.d.ts +98 -0
  3. package/dist/cjs/core/api-client.js +226 -0
  4. package/dist/cjs/core/api-types.d.ts +138 -0
  5. package/dist/cjs/core/api-types.js +10 -0
  6. package/dist/cjs/core/bastion-http.d.ts +13 -8
  7. package/dist/cjs/core/bastion-http.js +29 -17
  8. package/dist/cjs/core/errors.d.ts +23 -2
  9. package/dist/cjs/core/errors.js +45 -3
  10. package/dist/cjs/core/index.d.ts +4 -2
  11. package/dist/cjs/core/index.js +4 -1
  12. package/dist/cjs/core/jwks-verifier.js +2 -1
  13. package/dist/cjs/core/service-token-provider.d.ts +20 -12
  14. package/dist/cjs/core/service-token-provider.js +55 -27
  15. package/dist/cjs/nest/bastion-api-client.provider.d.ts +13 -0
  16. package/dist/cjs/nest/bastion-api-client.provider.js +43 -0
  17. package/dist/cjs/nest/bastion-audit.service.d.ts +13 -2
  18. package/dist/cjs/nest/bastion-audit.service.js +7 -1
  19. package/dist/cjs/nest/bastion-http-exception.d.ts +19 -0
  20. package/dist/cjs/nest/bastion-http-exception.js +39 -0
  21. package/dist/cjs/nest/bastion.module.js +2 -0
  22. package/dist/cjs/nest/bastion.service.d.ts +5 -7
  23. package/dist/cjs/nest/bastion.service.js +5 -13
  24. package/dist/cjs/nest/index.d.ts +3 -1
  25. package/dist/cjs/nest/index.js +6 -1
  26. package/dist/cjs/nest/options.d.ts +2 -0
  27. package/dist/cjs/webhooks/index.d.ts +54 -0
  28. package/dist/cjs/webhooks/index.js +72 -0
  29. package/dist/esm/core/api-client.d.ts +98 -0
  30. package/dist/esm/core/api-client.js +222 -0
  31. package/dist/esm/core/api-types.d.ts +138 -0
  32. package/dist/esm/core/api-types.js +7 -0
  33. package/dist/esm/core/bastion-http.d.ts +13 -8
  34. package/dist/esm/core/bastion-http.js +30 -18
  35. package/dist/esm/core/errors.d.ts +23 -2
  36. package/dist/esm/core/errors.js +43 -2
  37. package/dist/esm/core/index.d.ts +4 -2
  38. package/dist/esm/core/index.js +2 -0
  39. package/dist/esm/core/jwks-verifier.js +2 -1
  40. package/dist/esm/core/service-token-provider.d.ts +20 -12
  41. package/dist/esm/core/service-token-provider.js +55 -27
  42. package/dist/esm/nest/bastion-api-client.provider.d.ts +13 -0
  43. package/dist/esm/nest/bastion-api-client.provider.js +40 -0
  44. package/dist/esm/nest/bastion-audit.service.d.ts +13 -2
  45. package/dist/esm/nest/bastion-audit.service.js +7 -1
  46. package/dist/esm/nest/bastion-http-exception.d.ts +19 -0
  47. package/dist/esm/nest/bastion-http-exception.js +34 -0
  48. package/dist/esm/nest/bastion.module.js +2 -0
  49. package/dist/esm/nest/bastion.service.d.ts +5 -7
  50. package/dist/esm/nest/bastion.service.js +6 -14
  51. package/dist/esm/nest/index.d.ts +3 -1
  52. package/dist/esm/nest/index.js +2 -0
  53. package/dist/esm/nest/options.d.ts +2 -0
  54. package/dist/esm/webhooks/index.d.ts +54 -0
  55. package/dist/esm/webhooks/index.js +68 -0
  56. package/package.json +14 -1
@@ -0,0 +1,68 @@
1
+ import { createHmac, timingSafeEqual } from 'node:crypto';
2
+ /** Headers Bastion sends with every delivery. Lower-case, as Node exposes them. */
3
+ export const BASTION_SIGNATURE_HEADER = 'x-bastion-signature';
4
+ export const BASTION_SIGNATURE_V2_HEADER = 'x-bastion-signature-v2';
5
+ export const BASTION_EVENT_HEADER = 'x-bastion-event';
6
+ export const BASTION_DELIVERY_ID_HEADER = 'x-bastion-delivery-id';
7
+ /** Sent to every app with a webhook URL, whether or not the user has a role in it. */
8
+ export const BASTION_LIFECYCLE_EVENTS = ['user.deleted', 'user.expired_unverified', 'user.deactivated'];
9
+ function header(headers, name) {
10
+ const direct = headers[name] ?? headers[name.toLowerCase()];
11
+ if (direct !== undefined)
12
+ return Array.isArray(direct) ? direct[0] : direct;
13
+ const key = Object.keys(headers).find((k) => k.toLowerCase() === name);
14
+ const v = key ? headers[key] : undefined;
15
+ return Array.isArray(v) ? v[0] : v;
16
+ }
17
+ function macEquals(hexA, hexB) {
18
+ const a = Buffer.from(hexA, 'hex');
19
+ const b = Buffer.from(hexB, 'hex');
20
+ return a.length === b.length && a.length > 0 && timingSafeEqual(a, b);
21
+ }
22
+ /**
23
+ * Verifies a Bastion delivery. Prefers `X-Bastion-Signature-V2`
24
+ * (`t=<unix>,id=<deliveryId>,v1=<hex>[,v1=<hex>]`, MAC over `"<t>.<id>.<body>"`),
25
+ * which binds the timestamp and the delivery id so a stale or replayed
26
+ * delivery is rejected without trusting any unsigned header; falls back to
27
+ * the legacy `X-Bastion-Signature: sha256=<hex>` over the body alone only when
28
+ * v2 is absent. Any of the caller's `secrets` matching any `v1=` accepts:
29
+ * that is how a secret rotation stays zero-downtime on both sides.
30
+ */
31
+ export function verifyWebhookSignature(options) {
32
+ const body = typeof options.rawBody === 'string' ? Buffer.from(options.rawBody, 'utf8') : options.rawBody;
33
+ const secrets = (Array.isArray(options.secrets) ? options.secrets : [options.secrets]).filter(Boolean);
34
+ if (secrets.length === 0)
35
+ return { ok: false, reason: 'missing' };
36
+ const v2 = header(options.headers, BASTION_SIGNATURE_V2_HEADER);
37
+ if (v2) {
38
+ const parts = v2.split(',').map((p) => p.trim());
39
+ const t = parts.find((p) => p.startsWith('t='))?.slice(2);
40
+ const id = parts.find((p) => p.startsWith('id='))?.slice(3);
41
+ const macs = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
42
+ const timestamp = Number(t);
43
+ if (!t || !id || macs.length === 0 || !Number.isFinite(timestamp))
44
+ return { ok: false, reason: 'malformed' };
45
+ const now = Math.floor((options.now ?? Date.now)() / 1000);
46
+ if (Math.abs(now - timestamp) > (options.toleranceSeconds ?? 300))
47
+ return { ok: false, reason: 'stale' };
48
+ const signed = Buffer.concat([Buffer.from(`${timestamp}.${id}.`, 'utf8'), body]);
49
+ for (const secret of secrets) {
50
+ const expected = createHmac('sha256', secret).update(signed).digest('hex');
51
+ if (macs.some((mac) => macEquals(mac, expected)))
52
+ return { ok: true, version: 'v2', timestamp, deliveryId: id };
53
+ }
54
+ return { ok: false, reason: 'mismatch' };
55
+ }
56
+ const legacy = header(options.headers, BASTION_SIGNATURE_HEADER);
57
+ if (!legacy)
58
+ return { ok: false, reason: 'missing' };
59
+ if (!legacy.startsWith('sha256='))
60
+ return { ok: false, reason: 'malformed' };
61
+ const provided = legacy.slice('sha256='.length).trim();
62
+ for (const secret of secrets) {
63
+ const expected = createHmac('sha256', secret).update(body).digest('hex');
64
+ if (macEquals(provided, expected))
65
+ return { ok: true, version: 'legacy' };
66
+ }
67
+ return { ok: false, reason: 'mismatch' };
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyatom/bastion-client",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Consumer SDK for the Bastion identity provider: JWKS verification, service-client auth, audit writes, NestJS guards",
5
5
  "license": "MIT",
6
6
  "author": "Andrea Tombolato <hey@heyatom.dev> (https://heyatom.dev)",
@@ -35,6 +35,16 @@
35
35
  "types": "./dist/cjs/nest/index.d.ts",
36
36
  "default": "./dist/cjs/nest/index.js"
37
37
  }
38
+ },
39
+ "./webhooks": {
40
+ "import": {
41
+ "types": "./dist/esm/webhooks/index.d.ts",
42
+ "default": "./dist/esm/webhooks/index.js"
43
+ },
44
+ "require": {
45
+ "types": "./dist/cjs/webhooks/index.d.ts",
46
+ "default": "./dist/cjs/webhooks/index.js"
47
+ }
38
48
  }
39
49
  },
40
50
  "scripts": {
@@ -85,6 +95,9 @@
85
95
  ],
86
96
  "*": [
87
97
  "./dist/cjs/core/index.d.ts"
98
+ ],
99
+ "webhooks": [
100
+ "./dist/cjs/webhooks/index.d.ts"
88
101
  ]
89
102
  }
90
103
  }