@iqauth/sdk 2.6.3 → 2.7.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 (112) hide show
  1. package/README.md +173 -1
  2. package/dist/browser-session.d.mts +4 -4
  3. package/dist/browser-session.d.ts +4 -4
  4. package/dist/browser-session.js +181 -41
  5. package/dist/browser-session.mjs +3 -3
  6. package/dist/browser.d.mts +5 -5
  7. package/dist/browser.d.ts +5 -5
  8. package/dist/browser.js +271 -32
  9. package/dist/browser.mjs +10 -8
  10. package/dist/{chunk-6I6RM4MN.mjs → chunk-6PJRLRB4.mjs} +33 -3
  11. package/dist/chunk-C2ZTBOAC.mjs +36 -0
  12. package/dist/{chunk-LIZYFXH7.mjs → chunk-DFWHSDYQ.mjs} +1 -1
  13. package/dist/chunk-GLXSIGVS.mjs +66 -0
  14. package/dist/{chunk-TKZTCPEK.mjs → chunk-GN37E64I.mjs} +32 -40
  15. package/dist/{chunk-WQWBJSSS.mjs → chunk-HVHNYPDC.mjs} +6 -6
  16. package/dist/{chunk-W3F4JYGP.mjs → chunk-JXQI62A7.mjs} +108 -18
  17. package/dist/{chunk-UNYDG2L4.mjs → chunk-NUO2I65G.mjs} +56 -23
  18. package/dist/chunk-PMAFENVI.mjs +229 -0
  19. package/dist/chunk-RR2MGPTK.mjs +2724 -0
  20. package/dist/{chunk-76W5TLQQ.mjs → chunk-RTJAIBXY.mjs} +220 -20
  21. package/dist/{chunk-6TDJJER7.mjs → chunk-RUJXRTEW.mjs} +164 -5
  22. package/dist/{chunk-3JULWS6F.mjs → chunk-WCELYTJ3.mjs} +3 -3
  23. package/dist/{chunk-MKKZULZR.mjs → chunk-WIFG74IK.mjs} +1 -1
  24. package/dist/{chunk-BVV54LPI.mjs → chunk-YVALAG3B.mjs} +10 -4
  25. package/dist/cli/index.js +2 -2
  26. package/dist/cli/index.mjs +2 -2
  27. package/dist/{client-kYlJFgPv.d.mts → client-BGFnBpfc.d.mts} +47 -4
  28. package/dist/{client-BNQe3AgF.d.ts → client-CDQ21LvW.d.ts} +47 -4
  29. package/dist/{doctor-YYNHNMLD.mjs → doctor-JAFXWU3X.mjs} +2 -2
  30. package/dist/errors-Jl1Jtm-6.d.mts +107 -0
  31. package/dist/errors-Jl1Jtm-6.d.ts +107 -0
  32. package/dist/{express-B6_1vBYZ.d.mts → express-CVNQEkOr.d.mts} +2 -2
  33. package/dist/{express-CHpfa7D_.d.ts → express-Piv2WhWM.d.ts} +2 -2
  34. package/dist/express.d.mts +7 -6
  35. package/dist/express.d.ts +7 -6
  36. package/dist/express.js +349 -52
  37. package/dist/express.mjs +39 -12
  38. package/dist/fastify.d.mts +2 -0
  39. package/dist/fastify.d.ts +2 -0
  40. package/dist/fastify.js +332 -52
  41. package/dist/fastify.mjs +23 -8
  42. package/dist/hono.d.mts +2 -0
  43. package/dist/hono.d.ts +2 -0
  44. package/dist/hono.js +329 -52
  45. package/dist/hono.mjs +20 -8
  46. package/dist/index-5KSZEnDe.d.ts +1626 -0
  47. package/dist/index-CKoZHAoc.d.mts +1626 -0
  48. package/dist/index.d.mts +56 -8
  49. package/dist/index.d.ts +56 -8
  50. package/dist/index.js +565 -69
  51. package/dist/index.mjs +29 -9
  52. package/dist/{keys-NLWFAOEM.mjs → keys-6Y776TG2.mjs} +2 -2
  53. package/dist/locales.d.mts +1 -1
  54. package/dist/locales.d.ts +1 -1
  55. package/dist/mobile.d.mts +77 -7
  56. package/dist/mobile.d.ts +77 -7
  57. package/dist/mobile.js +276 -41
  58. package/dist/mobile.mjs +98 -3
  59. package/dist/next.d.mts +2 -1
  60. package/dist/next.d.ts +2 -1
  61. package/dist/next.js +391 -201
  62. package/dist/next.mjs +22 -7
  63. package/dist/pkce-7WKV4OIN.mjs +11 -0
  64. package/dist/{provisioningBridge-DnTfzdZK.d.ts → provisioningBridge-CGpMRie4.d.ts} +1 -1
  65. package/dist/{provisioningBridge-88xjOS2n.d.mts → provisioningBridge-M5G47LWO.d.mts} +1 -1
  66. package/dist/{publishableKey-BaR0HoAH.d.ts → publishableKey-f2kq-rKw.d.mts} +1 -1
  67. package/dist/{publishableKey-BaR0HoAH.d.mts → publishableKey-f2kq-rKw.d.ts} +1 -1
  68. package/dist/react-permissions.d.mts +52 -0
  69. package/dist/react-permissions.d.ts +52 -0
  70. package/dist/react-permissions.js +239 -0
  71. package/dist/react-permissions.mjs +97 -0
  72. package/dist/react.d.mts +9 -1624
  73. package/dist/react.d.ts +9 -1624
  74. package/dist/react.js +343 -36
  75. package/dist/react.mjs +59 -2611
  76. package/dist/{reverify-4UEJXUS6.mjs → reverify-C64QXKJO.mjs} +2 -2
  77. package/dist/server/handlers.d.mts +148 -3
  78. package/dist/server/handlers.d.ts +148 -3
  79. package/dist/server/handlers.js +410 -11
  80. package/dist/server/handlers.mjs +12 -3
  81. package/dist/server.d.mts +151 -8
  82. package/dist/server.d.ts +151 -8
  83. package/dist/server.js +406 -50
  84. package/dist/server.mjs +93 -11
  85. package/dist/service.d.mts +4 -4
  86. package/dist/service.d.ts +4 -4
  87. package/dist/service.js +181 -41
  88. package/dist/service.mjs +3 -3
  89. package/dist/{signIn-CiIBTJIh.d.mts → signIn-BLFnz8SV.d.ts} +78 -3
  90. package/dist/{signIn-CCY4JE5G.mjs → signIn-SHBW6Z4T.mjs} +2 -1
  91. package/dist/{signIn-OCr88Zf8.d.ts → signIn-T-CZ6t6r.d.mts} +78 -3
  92. package/dist/test.mjs +3 -3
  93. package/dist/{tokens-DCyzzn8L.d.mts → tokens-Bqhmqq_R.d.ts} +9 -2
  94. package/dist/{tokens-aHiGFr_E.d.ts → tokens-CITeoG6P.d.mts} +9 -2
  95. package/dist/{types-6bNdxesb.d.ts → types-BdQ2lqfT.d.mts} +1 -1
  96. package/dist/{types-6bNdxesb.d.mts → types-BdQ2lqfT.d.ts} +1 -1
  97. package/dist/{types-DZAflmmq.d.mts → types-XOV9XPVi.d.mts} +99 -10
  98. package/dist/{types-DZAflmmq.d.ts → types-XOV9XPVi.d.ts} +99 -10
  99. package/dist/webhooks.d.mts +100 -17
  100. package/dist/webhooks.d.ts +100 -17
  101. package/dist/webhooks.js +164 -15
  102. package/dist/webhooks.mjs +7 -1
  103. package/dist/ws.d.mts +2 -2
  104. package/dist/ws.d.ts +2 -2
  105. package/dist/ws.js +80 -30
  106. package/dist/ws.mjs +4 -4
  107. package/docs/error-handling.md +101 -0
  108. package/docs/guides/effective-permissions.md +171 -0
  109. package/package.json +13 -3
  110. package/dist/chunk-UKZLOHZG.mjs +0 -83
  111. package/dist/errors-CDdl24MP.d.mts +0 -52
  112. package/dist/errors-CDdl24MP.d.ts +0 -52
@@ -36,7 +36,17 @@ interface IQAuthTokenClientConfig extends IQAuthClientConfigBase {
36
36
  apiKey?: string;
37
37
  accessToken?: string;
38
38
  refreshToken?: string;
39
- autoRefresh?: boolean;
39
+ /**
40
+ * Token auto-refresh strategy.
41
+ * - `true` (default): proactively refresh when the access token is within 60s of expiry,
42
+ * AND retry once on a TOKEN_EXPIRED 401 response.
43
+ * - `false`: never auto-refresh — caller drives `tokens.refresh()` manually.
44
+ * - `'app-state'` (mobile only): skip the per-request expiring-soon proactive refresh
45
+ * (which fights with React Native's app-suspension lifecycle) and instead refresh on
46
+ * AppState `active` transitions. Reactive 401 retry stays enabled. Recognized only by
47
+ * `createMobileClient`; passing it to other constructors falls back to `true`.
48
+ */
49
+ autoRefresh?: boolean | "app-state";
40
50
  onTokenRefresh?: (tokens: TokenPair) => void;
41
51
  }
42
52
  interface IQAuthBrowserSessionClientConfig extends IQAuthClientConfigBase {
@@ -75,7 +85,66 @@ interface JwtClaims {
75
85
  email?: string;
76
86
  name?: string;
77
87
  };
88
+ picture?: string;
89
+ email_verified?: boolean;
90
+ given_name?: string;
91
+ family_name?: string;
92
+ locale?: string;
78
93
  }
94
+ /**
95
+ * Task #127 — Base claims shape (OIDC standard + IQAuth tenant/role).
96
+ *
97
+ * Required fields mirror what the IQAuth issuer always emits today; if a
98
+ * token is missing one of them it would fail `tokens.verify()` against the
99
+ * expected issuer/audience first, so this type trades runtime checks for
100
+ * compile-time ergonomics.
101
+ */
102
+ interface IQAuthBaseClaims {
103
+ /** Subject — opaque IQAuth user id. */
104
+ sub: string;
105
+ /** OIDC issuer URL (e.g. `https://auth.dispositioniq.com`). */
106
+ iss: string;
107
+ /** Audience(s) the token was minted for. */
108
+ aud: string | string[];
109
+ /** Expiry in seconds since epoch. */
110
+ exp: number;
111
+ /** Issued-at in seconds since epoch. */
112
+ iat: number;
113
+ email?: string;
114
+ email_verified?: boolean;
115
+ name?: string;
116
+ picture?: string;
117
+ locale?: string;
118
+ tenantId?: string;
119
+ tenantName?: string;
120
+ tenantSlug?: string;
121
+ vendorId?: string | null;
122
+ roles?: string[];
123
+ entitlements?: string[];
124
+ sessionId?: string;
125
+ jti?: string;
126
+ scopeContext?: ScopeContext;
127
+ loginMethod?: string;
128
+ /** RFC 8693 §4.1 actor — present on impersonation tokens. */
129
+ purpose?: string;
130
+ act?: {
131
+ sub: string;
132
+ email?: string;
133
+ name?: string;
134
+ };
135
+ }
136
+ /**
137
+ * Generic typed claims envelope. The type parameter `T` is structurally
138
+ * intersected with the base claims so app-specific fields minted via JWT
139
+ * templates surface with full IntelliSense:
140
+ *
141
+ * ```ts
142
+ * type MyClaims = { plan: "free" | "pro"; orgId: string };
143
+ * const claims = await client.tokens.verify<MyClaims>(token);
144
+ * if (claims.plan === "pro" && claims.orgId) { … } // both fields typed
145
+ * ```
146
+ */
147
+ type IQAuthClaims<T extends object = {}> = IQAuthBaseClaims & T;
79
148
  interface UserProfile {
80
149
  id: string;
81
150
  email: string;
@@ -102,6 +171,11 @@ interface SessionUser {
102
171
  vendorId?: string | null;
103
172
  roles: string[];
104
173
  entitlements: string[];
174
+ picture?: string;
175
+ emailVerified?: boolean;
176
+ givenName?: string;
177
+ familyName?: string;
178
+ locale?: string;
105
179
  }
106
180
  interface Tenant {
107
181
  tenantId: string;
@@ -407,6 +481,13 @@ interface PermissionNodeManifest {
407
481
  metadata?: Record<string, unknown>;
408
482
  children?: PermissionNodeManifest[];
409
483
  }
484
+ /**
485
+ * Task #130 — every manifest write must declare its origin environment so a
486
+ * dev workstation can never silently overwrite a production app's permission
487
+ * tree. The Admin API rejects writes whose `environment` is missing or not
488
+ * one of these three values with `{code: "ENVIRONMENT_REQUIRED"}`.
489
+ */
490
+ type AppManifestEnvironment = "production" | "staging" | "development";
410
491
  interface AppManifest {
411
492
  key: string;
412
493
  name: string;
@@ -415,6 +496,8 @@ interface AppManifest {
415
496
  tenantId?: string | null;
416
497
  metadata?: Record<string, unknown>;
417
498
  permissions: PermissionNodeManifest[];
499
+ /** Required by `POST /api/v1/apps/sync`. See {@link AppManifestEnvironment}. */
500
+ environment: AppManifestEnvironment;
418
501
  }
419
502
  interface AppInfo {
420
503
  id: string;
@@ -545,13 +628,17 @@ interface GroupPermission {
545
628
  nodeKey?: string | null;
546
629
  createdAt?: string;
547
630
  }
631
+ /**
632
+ * Task #130 — `appKey` and `nodeKey` are REQUIRED on this app-scoped admin
633
+ * call. The legacy `product` / `scope` shape is rejected at the SDK boundary
634
+ * to prevent the silent-fallback failure mode where a misconfigured value
635
+ * led to an empty/wrong permission set without any error.
636
+ */
548
637
  interface AddGroupPermissionRequest {
549
- product?: string;
550
- scope?: string;
638
+ appKey: string;
639
+ nodeKey: string;
551
640
  effect: string;
552
641
  weight?: number;
553
- appKey?: string;
554
- nodeKey?: string;
555
642
  }
556
643
  interface InheritanceRelation {
557
644
  id: string;
@@ -569,14 +656,16 @@ interface UserPermissionOverride {
569
656
  expiresAt?: string | null;
570
657
  createdAt?: string;
571
658
  }
659
+ /**
660
+ * Task #130 — `appKey` and `nodeKey` are REQUIRED. See `AddGroupPermissionRequest`
661
+ * for rationale.
662
+ */
572
663
  interface AddUserOverrideRequest {
573
- product?: string;
574
- scope?: string;
664
+ appKey: string;
665
+ nodeKey: string;
575
666
  effect: string;
576
667
  weight?: number;
577
668
  expiresAt?: string;
578
- appKey?: string;
579
- nodeKey?: string;
580
669
  }
581
670
  interface EffectivePermission {
582
671
  scope: string;
@@ -903,4 +992,4 @@ interface BackupCodeCountResult {
903
992
  remainingBackupCodes: number;
904
993
  }
905
994
 
906
- export type { PermissionNodeInfo as $, ApiSuccessResponse as A, BrandingConfig as B, CreateTenantRequest as C, MfaVerifyResult as D, PasswordPolicy as E, MfaPolicy as F, UserPermissions as G, ProvisionUserRequest as H, IQAuthEnvironment as I, JwtClaims as J, ProvisionUserResponse as K, LoginResult as L, MigrateUserRequest as M, ExpressMiddlewareOptions as N, OidcDiscovery as O, PromoteToVendorRequest as P, IQAuthRequestLike as Q, IQAuthResponseLike as R, ScopeContext as S, TokenPair as T, UserProfile as U, IQAuthNextFunction as V, IQAuthRetryConfig as W, IQAuthVerifyConfig as X, PermissionNodeManifest as Y, AppManifest as Z, AppInfo as _, IQAuthClientConfig as a, SignupRequest as a$, AppSyncResult as a0, Role as a1, CreateRoleRequest as a2, UpdateRoleRequest as a3, AssignRoleRequest as a4, UserRoleAssignment as a5, UserGroupAssignment as a6, TenantUser as a7, PermissionGroup as a8, GroupPermission as a9, UpdateSourceRequest as aA, Client as aB, CreateClientRequest as aC, UpdateClientRequest as aD, HierarchyVendor as aE, HierarchySource as aF, HierarchyClient as aG, HierarchyLink as aH, Membership as aI, CreateMembershipRequest as aJ, UpdateMembershipRequest as aK, MembershipWithDetails as aL, AvailableScopesTree as aM, ScopeTreeClient as aN, ScopeTreeSource as aO, ScopeTreeVendor as aP, ScopeSwitchResult as aQ, GdprExportData as aR, PinStatus as aS, PinLoginResult as aT, MfaAvailableMethods as aU, TotpEnrollResult as aV, TotpVerifyResult as aW, SmsEnrollResult as aX, EmailEnrollResult as aY, BackupCodesResult as aZ, BackupCodeCountResult as a_, AddGroupPermissionRequest as aa, InheritanceRelation as ab, UserPermissionOverride as ac, AddUserOverrideRequest as ad, EffectivePermission as ae, PermissionCheckResult as af, ApiKeyInfo as ag, CreateApiKeyRequest as ah, CreateApiKeyResult as ai, ApiKeyIntrospection as aj, Invitation as ak, CreateInviteRequest as al, InviteValidation as am, AcceptInviteRequest as an, WebhookEndpoint as ao, CreateWebhookRequest as ap, CreateWebhookResult as aq, WebhookDelivery as ar, WebhookTestResult as as, Entitlement as at, GrantEntitlementRequest as au, Vendor as av, CreateVendorRequest as aw, UpdateVendorRequest as ax, Source as ay, CreateSourceRequest as az, IQAuthTokenClientConfig as b, HostedClientContext as b0, IQAuthBrowserSessionClientConfig as c, SessionUser as d, Tenant as e, TokenAuthenticatedLoginResult as f, SessionAuthenticatedLoginResult as g, Session as h, TenantInfo as i, UpdateTenantRequest as j, PromoteToVendorResult as k, InviteTenantUserRequest as l, InviteTenantUserResult as m, TenantUserRoleUpdate as n, UpdateBrandingRequest as o, BrandingAsset as p, UploadAssetRequest as q, BrandingDomainMapping as r, JwksKey as s, JwksResponse as t, OidcTokenResponse as u, ApiErrorResponse as v, ApiResponse as w, MfaMethod as x, MfaEnrollment as y, TotpEnrollmentResult as z };
995
+ export type { AppManifest as $, ApiSuccessResponse as A, BrandingConfig as B, CreateTenantRequest as C, ApiErrorResponse as D, ApiResponse as E, MfaMethod as F, MfaEnrollment as G, TotpEnrollmentResult as H, IQAuthBrowserSessionClientConfig as I, JwtClaims as J, MfaVerifyResult as K, LoginResult as L, MigrateUserRequest as M, PasswordPolicy as N, OidcDiscovery as O, PromoteToVendorRequest as P, MfaPolicy as Q, UserPermissions as R, SessionUser as S, TokenPair as T, UserProfile as U, ProvisionUserRequest as V, ProvisionUserResponse as W, ExpressMiddlewareOptions as X, IQAuthRetryConfig as Y, IQAuthVerifyConfig as Z, PermissionNodeManifest as _, IQAuthRequestLike as a, BackupCodesResult as a$, AppInfo as a0, PermissionNodeInfo as a1, AppSyncResult as a2, Role as a3, CreateRoleRequest as a4, UpdateRoleRequest as a5, AssignRoleRequest as a6, UserRoleAssignment as a7, UserGroupAssignment as a8, TenantUser as a9, Source as aA, CreateSourceRequest as aB, UpdateSourceRequest as aC, Client as aD, CreateClientRequest as aE, UpdateClientRequest as aF, HierarchyVendor as aG, HierarchySource as aH, HierarchyClient as aI, HierarchyLink as aJ, Membership as aK, CreateMembershipRequest as aL, UpdateMembershipRequest as aM, MembershipWithDetails as aN, AvailableScopesTree as aO, ScopeTreeClient as aP, ScopeTreeSource as aQ, ScopeTreeVendor as aR, ScopeSwitchResult as aS, GdprExportData as aT, PinStatus as aU, PinLoginResult as aV, MfaAvailableMethods as aW, TotpEnrollResult as aX, TotpVerifyResult as aY, SmsEnrollResult as aZ, EmailEnrollResult as a_, PermissionGroup as aa, GroupPermission as ab, AddGroupPermissionRequest as ac, InheritanceRelation as ad, UserPermissionOverride as ae, AddUserOverrideRequest as af, EffectivePermission as ag, PermissionCheckResult as ah, ApiKeyInfo as ai, CreateApiKeyRequest as aj, CreateApiKeyResult as ak, ApiKeyIntrospection as al, Invitation as am, CreateInviteRequest as an, InviteValidation as ao, AcceptInviteRequest as ap, WebhookEndpoint as aq, CreateWebhookRequest as ar, CreateWebhookResult as as, WebhookDelivery as at, WebhookTestResult as au, Entitlement as av, GrantEntitlementRequest as aw, Vendor as ax, CreateVendorRequest as ay, UpdateVendorRequest as az, IQAuthResponseLike as b, BackupCodeCountResult as b0, SignupRequest as b1, HostedClientContext as b2, IQAuthNextFunction as c, IQAuthEnvironment as d, IQAuthClientConfig as e, IQAuthTokenClientConfig as f, ScopeContext as g, IQAuthClaims as h, IQAuthBaseClaims as i, Tenant as j, TokenAuthenticatedLoginResult as k, SessionAuthenticatedLoginResult as l, Session as m, TenantInfo as n, UpdateTenantRequest as o, PromoteToVendorResult as p, InviteTenantUserRequest as q, InviteTenantUserResult as r, TenantUserRoleUpdate as s, UpdateBrandingRequest as t, BrandingAsset as u, UploadAssetRequest as v, BrandingDomainMapping as w, JwksKey as x, JwksResponse as y, OidcTokenResponse as z };
@@ -1,24 +1,34 @@
1
1
  /**
2
- * @iqauth/sdk/webhooks — webhook signature verification.
2
+ * @iqauth/sdk/webhooks — webhook signature verification + event parsing.
3
3
  *
4
- * Mirrors Stripe's `constructEvent` shape. Verifies the `X-IQAuth-Signature`
5
- * header (format `t=<unix>,v1=<hex hmac sha256>`) emitted by IQAuth's
6
- * webhook fan-out (src/services/webhook.service.ts). Constant-time compare;
7
- * tolerance window in seconds (default 300).
4
+ * IQAuth's webhook fan-out emits a CloudEvents-style envelope:
5
+ * { specversion: "1.0", id, type, subject, time, data, tenantId }
8
6
  *
9
- * Usage:
10
- * import { verifyWebhookSignature } from "@iqauth/sdk/webhooks";
7
+ * and signs the raw request body with HMAC-SHA256, exposed via the canonical
8
+ * header `X-IQAuth-Signature: v1=<hex>`. Legacy header names
9
+ * (`X-Webhook-Signature`, `X-IQ-Auth-Signature`, `X-Signature`) are also
10
+ * accepted for one minor with a deprecation log.
11
+ *
12
+ * Two entry points:
13
+ *
14
+ * - `verifyWebhookSignature(opts)` — single-secret verifier kept for
15
+ * back-compat. Accepts both the modern `v1=<hex>` form (sig over body)
16
+ * AND the legacy `t=<unix>,v1=<hex>` form (sig over `${t}.${body}`).
17
+ *
18
+ * - `parseWebhookEvent(rawBody, headers, secrets, opts?)` — modern entry
19
+ * point. Verifies the signature against ANY secret in the array
20
+ * (rotation-safe, subsumes #28), enforces the CloudEvents envelope, and
21
+ * returns a typed `IQAuthEvent` with a stable `idempotencyKey`.
22
+ *
23
+ * Usage (Express, 5 lines):
24
+ *
25
+ * import { parseWebhookEvent } from "@iqauth/sdk/webhooks";
11
26
  * app.post("/webhooks/iqauth", express.raw({ type: "application/json" }), (req, res) => {
12
27
  * try {
13
- * const evt = verifyWebhookSignature({
14
- * payload: req.body, // Buffer or string of the RAW request body
15
- * header: req.header("X-IQAuth-Signature"),
16
- * secret: process.env.IQAUTH_WEBHOOK_SECRET!,
17
- * });
18
- * // evt is the parsed JSON event
19
- * } catch (err) {
20
- * return res.status(400).send(err.message);
21
- * }
28
+ * const evt = parseWebhookEvent(req.body, req.headers, [process.env.IQAUTH_WEBHOOK_SECRET!]);
29
+ * handle(evt); // evt.idempotencyKey is stable across retries
30
+ * res.status(200).end();
31
+ * } catch (err) { res.status(400).send((err as Error).message); }
22
32
  * });
23
33
  */
24
34
  interface VerifyWebhookOptions {
@@ -42,13 +52,56 @@ interface IQAuthWebhookEvent {
42
52
  createdAt?: number | string;
43
53
  [key: string]: unknown;
44
54
  }
55
+ /**
56
+ * Canonical CloudEvents-style event surfaced by `parseWebhookEvent`. Includes
57
+ * a stable `idempotencyKey` (= `id`) so receivers can dedupe retries without
58
+ * inventing their own subject-extraction logic.
59
+ */
60
+ interface IQAuthEvent<TData = Record<string, unknown>> {
61
+ /** CloudEvents `specversion`. Always `"1.0"` in the canonical envelope. */
62
+ specversion: "1.0";
63
+ /** Unique event id. Same value as `idempotencyKey`. */
64
+ id: string;
65
+ /** Reverse-DNS-like event type (e.g. `user.deactivated`). */
66
+ type: string;
67
+ /** Stable per-event subject (e.g. the user id, invitation id, …). */
68
+ subject: string;
69
+ /** ISO-8601 UTC timestamp the producer assigned. */
70
+ time: string;
71
+ /** Tenant the event was fired for. */
72
+ tenantId: string;
73
+ /** Event-type-specific payload. */
74
+ data: TData;
75
+ /** Stable dedupe key — receivers MUST treat repeats as no-ops. */
76
+ idempotencyKey: string;
77
+ /** Which secret index in the `secrets` array verified the signature.
78
+ * Useful while rotating secrets — log this to confirm callers have moved
79
+ * onto the new key before retiring the old one. */
80
+ verifiedWithSecretIndex: number;
81
+ }
45
82
  declare class WebhookSignatureError extends Error {
46
83
  code: string;
47
84
  constructor(code: string, message: string);
48
85
  }
86
+ /** Canonical signature header name. */
87
+ declare const IQAUTH_SIGNATURE_HEADER = "x-iqauth-signature";
88
+ /** Legacy header names accepted for one minor. Receivers SHOULD migrate to
89
+ * `X-IQAuth-Signature`; verifying via these emits a deprecation log. */
90
+ declare const LEGACY_SIGNATURE_HEADERS: readonly ["x-webhook-signature", "x-iq-auth-signature", "x-signature"];
49
91
  /**
50
92
  * Verify the signature on a webhook payload and return the parsed event.
51
93
  * Throws `WebhookSignatureError` on any verification failure.
94
+ *
95
+ * Accepts both the modern `v1=<hex>` header (HMAC over body bytes) and the
96
+ * legacy `t=<unix>,v1=<hex>` header (HMAC over `${t}.${body}`). Tolerance
97
+ * window is enforced ONLY when a `t=` is present in the header — modern
98
+ * `v1=<hex>`-only deliveries do NOT carry a header timestamp, so this
99
+ * function cannot enforce replay-window protection on them. **For canonical
100
+ * Task #129 deliveries, use `parseWebhookEvent`** — it enforces tolerance
101
+ * via the envelope's `time` field, validates the CloudEvents envelope,
102
+ * supports multi-secret rotation, and returns a typed event with a stable
103
+ * `idempotencyKey`. `verifyWebhookSignature` is retained for back-compat
104
+ * with single-secret integrations that pre-date the standardized envelope.
52
105
  */
53
106
  declare function verifyWebhookSignature(opts: VerifyWebhookOptions): IQAuthWebhookEvent;
54
107
  /**
@@ -57,5 +110,35 @@ declare function verifyWebhookSignature(opts: VerifyWebhookOptions): IQAuthWebho
57
110
  * branch on validity rather than catch.
58
111
  */
59
112
  declare function isValidWebhookSignature(opts: VerifyWebhookOptions): boolean;
113
+ interface ParseWebhookEventOptions {
114
+ /** How many seconds old the envelope `time` (or legacy `t=`) may be. Default 300. */
115
+ toleranceSeconds?: number;
116
+ /** Override current time (ms since epoch) — for tests. */
117
+ nowMs?: number;
118
+ /** Sink for deprecation logs. Defaults to `console.warn`; pass `() => {}` to silence. */
119
+ onDeprecation?: (message: string) => void;
120
+ }
121
+ type HeadersLike = Record<string, string | string[] | undefined> | {
122
+ get(name: string): string | null;
123
+ } | Headers;
124
+ /**
125
+ * Verify and parse a webhook delivery into the canonical `IQAuthEvent`.
126
+ *
127
+ * Signature is verified against EVERY secret in `secrets` (rotation-safe).
128
+ * Pass `[currentSecret, previousSecret]` during a rotation window so deliveries
129
+ * signed with either key are accepted.
130
+ *
131
+ * Header lookup order:
132
+ * 1. `X-IQAuth-Signature` (canonical)
133
+ * 2. `X-Webhook-Signature` (legacy — emits deprecation log)
134
+ * 3. `X-IQ-Auth-Signature` (legacy — emits deprecation log)
135
+ * 4. `X-Signature` (legacy — emits deprecation log)
136
+ *
137
+ * Throws `WebhookSignatureError` (with a `.code` field) on any failure:
138
+ * `MISSING_HEADER` | `MISSING_SECRET` | `MALFORMED_HEADER`
139
+ * `TIMESTAMP_OUT_OF_TOLERANCE` | `SIGNATURE_MISMATCH`
140
+ * `MALFORMED_BODY` | `MALFORMED_ENVELOPE`
141
+ */
142
+ declare function parseWebhookEvent<TData = Record<string, unknown>>(rawBody: Buffer | Uint8Array | string, headers: HeadersLike, secrets: string[], opts?: ParseWebhookEventOptions): IQAuthEvent<TData>;
60
143
 
61
- export { type IQAuthWebhookEvent, type VerifyWebhookOptions, WebhookSignatureError, isValidWebhookSignature, verifyWebhookSignature };
144
+ export { IQAUTH_SIGNATURE_HEADER, type IQAuthEvent, type IQAuthWebhookEvent, LEGACY_SIGNATURE_HEADERS, type ParseWebhookEventOptions, type VerifyWebhookOptions, WebhookSignatureError, isValidWebhookSignature, parseWebhookEvent, verifyWebhookSignature };
@@ -1,24 +1,34 @@
1
1
  /**
2
- * @iqauth/sdk/webhooks — webhook signature verification.
2
+ * @iqauth/sdk/webhooks — webhook signature verification + event parsing.
3
3
  *
4
- * Mirrors Stripe's `constructEvent` shape. Verifies the `X-IQAuth-Signature`
5
- * header (format `t=<unix>,v1=<hex hmac sha256>`) emitted by IQAuth's
6
- * webhook fan-out (src/services/webhook.service.ts). Constant-time compare;
7
- * tolerance window in seconds (default 300).
4
+ * IQAuth's webhook fan-out emits a CloudEvents-style envelope:
5
+ * { specversion: "1.0", id, type, subject, time, data, tenantId }
8
6
  *
9
- * Usage:
10
- * import { verifyWebhookSignature } from "@iqauth/sdk/webhooks";
7
+ * and signs the raw request body with HMAC-SHA256, exposed via the canonical
8
+ * header `X-IQAuth-Signature: v1=<hex>`. Legacy header names
9
+ * (`X-Webhook-Signature`, `X-IQ-Auth-Signature`, `X-Signature`) are also
10
+ * accepted for one minor with a deprecation log.
11
+ *
12
+ * Two entry points:
13
+ *
14
+ * - `verifyWebhookSignature(opts)` — single-secret verifier kept for
15
+ * back-compat. Accepts both the modern `v1=<hex>` form (sig over body)
16
+ * AND the legacy `t=<unix>,v1=<hex>` form (sig over `${t}.${body}`).
17
+ *
18
+ * - `parseWebhookEvent(rawBody, headers, secrets, opts?)` — modern entry
19
+ * point. Verifies the signature against ANY secret in the array
20
+ * (rotation-safe, subsumes #28), enforces the CloudEvents envelope, and
21
+ * returns a typed `IQAuthEvent` with a stable `idempotencyKey`.
22
+ *
23
+ * Usage (Express, 5 lines):
24
+ *
25
+ * import { parseWebhookEvent } from "@iqauth/sdk/webhooks";
11
26
  * app.post("/webhooks/iqauth", express.raw({ type: "application/json" }), (req, res) => {
12
27
  * try {
13
- * const evt = verifyWebhookSignature({
14
- * payload: req.body, // Buffer or string of the RAW request body
15
- * header: req.header("X-IQAuth-Signature"),
16
- * secret: process.env.IQAUTH_WEBHOOK_SECRET!,
17
- * });
18
- * // evt is the parsed JSON event
19
- * } catch (err) {
20
- * return res.status(400).send(err.message);
21
- * }
28
+ * const evt = parseWebhookEvent(req.body, req.headers, [process.env.IQAUTH_WEBHOOK_SECRET!]);
29
+ * handle(evt); // evt.idempotencyKey is stable across retries
30
+ * res.status(200).end();
31
+ * } catch (err) { res.status(400).send((err as Error).message); }
22
32
  * });
23
33
  */
24
34
  interface VerifyWebhookOptions {
@@ -42,13 +52,56 @@ interface IQAuthWebhookEvent {
42
52
  createdAt?: number | string;
43
53
  [key: string]: unknown;
44
54
  }
55
+ /**
56
+ * Canonical CloudEvents-style event surfaced by `parseWebhookEvent`. Includes
57
+ * a stable `idempotencyKey` (= `id`) so receivers can dedupe retries without
58
+ * inventing their own subject-extraction logic.
59
+ */
60
+ interface IQAuthEvent<TData = Record<string, unknown>> {
61
+ /** CloudEvents `specversion`. Always `"1.0"` in the canonical envelope. */
62
+ specversion: "1.0";
63
+ /** Unique event id. Same value as `idempotencyKey`. */
64
+ id: string;
65
+ /** Reverse-DNS-like event type (e.g. `user.deactivated`). */
66
+ type: string;
67
+ /** Stable per-event subject (e.g. the user id, invitation id, …). */
68
+ subject: string;
69
+ /** ISO-8601 UTC timestamp the producer assigned. */
70
+ time: string;
71
+ /** Tenant the event was fired for. */
72
+ tenantId: string;
73
+ /** Event-type-specific payload. */
74
+ data: TData;
75
+ /** Stable dedupe key — receivers MUST treat repeats as no-ops. */
76
+ idempotencyKey: string;
77
+ /** Which secret index in the `secrets` array verified the signature.
78
+ * Useful while rotating secrets — log this to confirm callers have moved
79
+ * onto the new key before retiring the old one. */
80
+ verifiedWithSecretIndex: number;
81
+ }
45
82
  declare class WebhookSignatureError extends Error {
46
83
  code: string;
47
84
  constructor(code: string, message: string);
48
85
  }
86
+ /** Canonical signature header name. */
87
+ declare const IQAUTH_SIGNATURE_HEADER = "x-iqauth-signature";
88
+ /** Legacy header names accepted for one minor. Receivers SHOULD migrate to
89
+ * `X-IQAuth-Signature`; verifying via these emits a deprecation log. */
90
+ declare const LEGACY_SIGNATURE_HEADERS: readonly ["x-webhook-signature", "x-iq-auth-signature", "x-signature"];
49
91
  /**
50
92
  * Verify the signature on a webhook payload and return the parsed event.
51
93
  * Throws `WebhookSignatureError` on any verification failure.
94
+ *
95
+ * Accepts both the modern `v1=<hex>` header (HMAC over body bytes) and the
96
+ * legacy `t=<unix>,v1=<hex>` header (HMAC over `${t}.${body}`). Tolerance
97
+ * window is enforced ONLY when a `t=` is present in the header — modern
98
+ * `v1=<hex>`-only deliveries do NOT carry a header timestamp, so this
99
+ * function cannot enforce replay-window protection on them. **For canonical
100
+ * Task #129 deliveries, use `parseWebhookEvent`** — it enforces tolerance
101
+ * via the envelope's `time` field, validates the CloudEvents envelope,
102
+ * supports multi-secret rotation, and returns a typed event with a stable
103
+ * `idempotencyKey`. `verifyWebhookSignature` is retained for back-compat
104
+ * with single-secret integrations that pre-date the standardized envelope.
52
105
  */
53
106
  declare function verifyWebhookSignature(opts: VerifyWebhookOptions): IQAuthWebhookEvent;
54
107
  /**
@@ -57,5 +110,35 @@ declare function verifyWebhookSignature(opts: VerifyWebhookOptions): IQAuthWebho
57
110
  * branch on validity rather than catch.
58
111
  */
59
112
  declare function isValidWebhookSignature(opts: VerifyWebhookOptions): boolean;
113
+ interface ParseWebhookEventOptions {
114
+ /** How many seconds old the envelope `time` (or legacy `t=`) may be. Default 300. */
115
+ toleranceSeconds?: number;
116
+ /** Override current time (ms since epoch) — for tests. */
117
+ nowMs?: number;
118
+ /** Sink for deprecation logs. Defaults to `console.warn`; pass `() => {}` to silence. */
119
+ onDeprecation?: (message: string) => void;
120
+ }
121
+ type HeadersLike = Record<string, string | string[] | undefined> | {
122
+ get(name: string): string | null;
123
+ } | Headers;
124
+ /**
125
+ * Verify and parse a webhook delivery into the canonical `IQAuthEvent`.
126
+ *
127
+ * Signature is verified against EVERY secret in `secrets` (rotation-safe).
128
+ * Pass `[currentSecret, previousSecret]` during a rotation window so deliveries
129
+ * signed with either key are accepted.
130
+ *
131
+ * Header lookup order:
132
+ * 1. `X-IQAuth-Signature` (canonical)
133
+ * 2. `X-Webhook-Signature` (legacy — emits deprecation log)
134
+ * 3. `X-IQ-Auth-Signature` (legacy — emits deprecation log)
135
+ * 4. `X-Signature` (legacy — emits deprecation log)
136
+ *
137
+ * Throws `WebhookSignatureError` (with a `.code` field) on any failure:
138
+ * `MISSING_HEADER` | `MISSING_SECRET` | `MALFORMED_HEADER`
139
+ * `TIMESTAMP_OUT_OF_TOLERANCE` | `SIGNATURE_MISMATCH`
140
+ * `MALFORMED_BODY` | `MALFORMED_ENVELOPE`
141
+ */
142
+ declare function parseWebhookEvent<TData = Record<string, unknown>>(rawBody: Buffer | Uint8Array | string, headers: HeadersLike, secrets: string[], opts?: ParseWebhookEventOptions): IQAuthEvent<TData>;
60
143
 
61
- export { type IQAuthWebhookEvent, type VerifyWebhookOptions, WebhookSignatureError, isValidWebhookSignature, verifyWebhookSignature };
144
+ export { IQAUTH_SIGNATURE_HEADER, type IQAuthEvent, type IQAuthWebhookEvent, LEGACY_SIGNATURE_HEADERS, type ParseWebhookEventOptions, type VerifyWebhookOptions, WebhookSignatureError, isValidWebhookSignature, parseWebhookEvent, verifyWebhookSignature };