@iqauth/sdk 2.7.0 → 2.8.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 (88) hide show
  1. package/dist/browser-session.d.mts +3 -3
  2. package/dist/browser-session.d.ts +3 -3
  3. package/dist/browser-session.js +31 -5
  4. package/dist/browser-session.mjs +1 -1
  5. package/dist/browser.d.mts +3 -3
  6. package/dist/browser.d.ts +3 -3
  7. package/dist/browser.js +23 -3
  8. package/dist/browser.mjs +1 -1
  9. package/dist/{chunk-YVALAG3B.mjs → chunk-25SSYDIP.mjs} +1 -1
  10. package/dist/{chunk-RTJAIBXY.mjs → chunk-4V7FKOTG.mjs} +23 -3
  11. package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
  12. package/dist/chunk-JRDVUWAL.mjs +46 -0
  13. package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
  14. package/dist/{chunk-PMAFENVI.mjs → chunk-VYQ3ETCK.mjs} +27 -12
  15. package/dist/{chunk-RR2MGPTK.mjs → chunk-WHT6WKTY.mjs} +539 -83
  16. package/dist/{chunk-RUJXRTEW.mjs → chunk-WSH4SW7F.mjs} +122 -8
  17. package/dist/{chunk-JXQI62A7.mjs → chunk-ZLJPABB7.mjs} +31 -5
  18. package/dist/{client-BGFnBpfc.d.mts → client-D8L-PaWr.d.mts} +14 -4
  19. package/dist/{client-CDQ21LvW.d.ts → client-DkPL0EPZ.d.ts} +14 -4
  20. package/dist/{express-Piv2WhWM.d.ts → express-Budysq4h.d.ts} +2 -2
  21. package/dist/{express-CVNQEkOr.d.mts → express-DDTA3qV1.d.mts} +2 -2
  22. package/dist/express.d.mts +5 -5
  23. package/dist/express.d.ts +5 -5
  24. package/dist/express.js +217 -36
  25. package/dist/express.mjs +38 -26
  26. package/dist/fastify.d.mts +10 -2
  27. package/dist/fastify.d.ts +10 -2
  28. package/dist/fastify.js +260 -16
  29. package/dist/fastify.mjs +80 -5
  30. package/dist/hono.d.mts +10 -2
  31. package/dist/hono.d.ts +10 -2
  32. package/dist/hono.js +240 -16
  33. package/dist/hono.mjs +60 -5
  34. package/dist/{index-5KSZEnDe.d.ts → index-Cko-d5po.d.mts} +227 -5
  35. package/dist/{index-CKoZHAoc.d.mts → index-RNqwEcmY.d.ts} +227 -5
  36. package/dist/index.d.mts +5 -5
  37. package/dist/index.d.ts +5 -5
  38. package/dist/index.js +149 -26
  39. package/dist/index.mjs +5 -5
  40. package/dist/locales.d.mts +1 -1
  41. package/dist/locales.d.ts +1 -1
  42. package/dist/locales.js +36 -0
  43. package/dist/locales.mjs +1 -1
  44. package/dist/mobile.d.mts +3 -3
  45. package/dist/mobile.d.ts +3 -3
  46. package/dist/mobile.js +31 -5
  47. package/dist/mobile.mjs +1 -1
  48. package/dist/next.d.mts +10 -2
  49. package/dist/next.d.ts +10 -2
  50. package/dist/next.js +212 -11
  51. package/dist/next.mjs +62 -4
  52. package/dist/{provisioningBridge-M5G47LWO.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
  53. package/dist/{provisioningBridge-CGpMRie4.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
  54. package/dist/react-permissions.d.mts +4 -4
  55. package/dist/react-permissions.d.ts +4 -4
  56. package/dist/react-permissions.mjs +4 -3
  57. package/dist/react.d.mts +4 -4
  58. package/dist/react.d.ts +4 -4
  59. package/dist/react.js +570 -41
  60. package/dist/react.mjs +19 -5
  61. package/dist/server/handlers.d.mts +56 -5
  62. package/dist/server/handlers.d.ts +56 -5
  63. package/dist/server/handlers.js +123 -8
  64. package/dist/server/handlers.mjs +3 -1
  65. package/dist/server.d.mts +28 -8
  66. package/dist/server.d.ts +28 -8
  67. package/dist/server.js +176 -14
  68. package/dist/server.mjs +9 -4
  69. package/dist/service.d.mts +3 -3
  70. package/dist/service.d.ts +3 -3
  71. package/dist/service.js +31 -5
  72. package/dist/service.mjs +1 -1
  73. package/dist/{signIn-T-CZ6t6r.d.mts → signIn-CReqfXsh.d.mts} +18 -1
  74. package/dist/{signIn-BLFnz8SV.d.ts → signIn-Cfa1GTpO.d.ts} +18 -1
  75. package/dist/{tokens-Bqhmqq_R.d.ts → tokens-9F6ETrzk.d.ts} +1 -1
  76. package/dist/{tokens-CITeoG6P.d.mts → tokens-B06VtvUi.d.mts} +1 -1
  77. package/dist/{types-XOV9XPVi.d.mts → types-Bn8O-OEd.d.mts} +66 -2
  78. package/dist/{types-XOV9XPVi.d.ts → types-Bn8O-OEd.d.ts} +66 -2
  79. package/dist/{types-BdQ2lqfT.d.mts → types-DnU2LhXR.d.mts} +6 -0
  80. package/dist/{types-BdQ2lqfT.d.ts → types-DnU2LhXR.d.ts} +6 -0
  81. package/dist/webhooks.d.mts +22 -9
  82. package/dist/webhooks.d.ts +22 -9
  83. package/dist/webhooks.js +27 -12
  84. package/dist/webhooks.mjs +1 -1
  85. package/dist/ws.d.mts +2 -2
  86. package/dist/ws.d.ts +2 -2
  87. package/docs/guides/invitations.md +65 -0
  88. package/package.json +7 -2
@@ -176,6 +176,14 @@ interface SessionUser {
176
176
  givenName?: string;
177
177
  familyName?: string;
178
178
  locale?: string;
179
+ /**
180
+ * Task #171 — When the active session was minted under a source/client
181
+ * scope (either via a scope_hint, single-resolved scope, or post-pick),
182
+ * the access token carries a `scopeContext` claim and we project it here
183
+ * so SDK consumers (`useUser()`, framework adapters) can read the active
184
+ * scope without re-parsing the JWT. Absent for tenant-wide sessions.
185
+ */
186
+ scopeContext?: ScopeContext;
179
187
  }
180
188
  interface Tenant {
181
189
  tenantId: string;
@@ -201,6 +209,24 @@ interface SessionAuthenticatedLoginResult {
201
209
  authMode: "session";
202
210
  user: SessionUser;
203
211
  }
212
+ /**
213
+ * Task #171 — A user can have multiple source/client scoped memberships in
214
+ * the same tenant with no tenant-wide role. When login resolves to that
215
+ * state the backend returns a short-lived `scopeSelectionToken` plus the
216
+ * list of choices; the caller redeems it via `AuthModule.selectScope`.
217
+ */
218
+ interface ScopeChoice {
219
+ membershipId: string;
220
+ scopeType: "vendor" | "source" | "client";
221
+ scopeId: string;
222
+ scopeName: string;
223
+ roleName: string;
224
+ }
225
+ /** Task #171 — Optional hint forwarded with login / select-tenant / OIDC. */
226
+ interface ScopeHint {
227
+ type: "vendor" | "source" | "client";
228
+ id: string;
229
+ }
204
230
  type LoginResult = TokenAuthenticatedLoginResult | SessionAuthenticatedLoginResult | {
205
231
  status: "mfa_required";
206
232
  mfaChallengeToken: string;
@@ -209,6 +235,11 @@ type LoginResult = TokenAuthenticatedLoginResult | SessionAuthenticatedLoginResu
209
235
  status: "tenant_selection";
210
236
  tenantSelectionToken: string;
211
237
  tenants: Tenant[];
238
+ } | {
239
+ status: "scope_selection";
240
+ scopeSelectionToken: string;
241
+ tenantId: string;
242
+ scopes: ScopeChoice[];
212
243
  };
213
244
  interface Session {
214
245
  id: string;
@@ -718,13 +749,46 @@ interface Invitation {
718
749
  invitedBy: string;
719
750
  expiresAt?: string;
720
751
  createdAt?: string;
752
+ /** Scope the invite grants into ("tenant" | "vendor" | "source" | "client"). */
753
+ scopeType?: string | null;
754
+ /** Scope target id (paired with `scopeType`). */
755
+ scopeId?: string | null;
756
+ /** OIDC client bound for post-accept auto-redirect (paired with `redirectUri`). */
757
+ clientId?: string | null;
758
+ /** Registered redirect URI the new invitee is sent to after account creation. */
759
+ redirectUri?: string | null;
760
+ /** Display name pre-filled on the hosted accept page. */
761
+ inviteeName?: string | null;
721
762
  }
722
763
  interface CreateInviteRequest {
723
764
  email: string;
724
- tenantId: string;
765
+ /**
766
+ * Target tenant. Optional for service (API-key) callers — the backend
767
+ * derives the tenant from the key and rejects a mismatching value. Platform
768
+ * admins may target any tenant.
769
+ */
770
+ tenantId?: string;
725
771
  vendorId?: string;
726
772
  role: string;
727
773
  products?: string[];
774
+ /** Scope to grant into. Must match the backend's accepted values. */
775
+ scopeType?: "tenant" | "vendor" | "source" | "client";
776
+ /** Scope target id (paired with `scopeType`). */
777
+ scopeId?: string;
778
+ /**
779
+ * Opt-in auto-redirect after the invitee creates their account. `clientId`
780
+ * and `redirectUri` are all-or-nothing — pass both or neither. The backend
781
+ * validates that `clientId` is an active OIDC client in the invite's tenant
782
+ * and that `redirectUri` is in that client's registered allowlist, then mints
783
+ * an OIDC authorization code and 302s the brand-new invitee to
784
+ * `${redirectUri}?code=…&state=…`. Point this at your app's
785
+ * `/api/iqauth/callback` so the framework adapter signs the user in on first
786
+ * paint. Existing-user accepts do NOT auto-redirect (see the integration guide).
787
+ */
788
+ clientId?: string;
789
+ redirectUri?: string;
790
+ /** Optional display name to pre-fill on the hosted accept page. Never used for auth. */
791
+ inviteeName?: string;
728
792
  }
729
793
  interface InviteValidation {
730
794
  valid: boolean;
@@ -992,4 +1056,4 @@ interface BackupCodeCountResult {
992
1056
  remainingBackupCodes: number;
993
1057
  }
994
1058
 
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 };
1059
+ 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, ScopeHint as b1, SignupRequest as b2, HostedClientContext as b3, 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 };
@@ -45,6 +45,7 @@ interface IQAuthLocaleBundle {
45
45
  "signIn.useDifferentAccount": string;
46
46
  "signIn.selectTenant": string;
47
47
  "signIn.selectTenantSubtitle": string;
48
+ "signIn.selectScope": string;
48
49
  "signIn.dividerOr": string;
49
50
  "signIn.preparingExperience": string;
50
51
  "signIn.applicationUnavailable": string;
@@ -133,6 +134,11 @@ interface IQAuthLocaleBundle {
133
134
  "orgSwitcher.createNew": string;
134
135
  "orgSwitcher.manage": string;
135
136
  "orgSwitcher.noOrgs": string;
137
+ "orgSwitcher.mfaRequiredTitle": string;
138
+ "orgSwitcher.mfaRequiredBody": string;
139
+ "orgSwitcher.scopeSelectionRequiredTitle": string;
140
+ "orgSwitcher.scopeSelectionRequiredBody": string;
141
+ "orgSwitcher.continueInHostedSignIn": string;
136
142
  "orgProfile.title": string;
137
143
  "orgProfile.generalTab": string;
138
144
  "orgProfile.membersTab": string;
@@ -45,6 +45,7 @@ interface IQAuthLocaleBundle {
45
45
  "signIn.useDifferentAccount": string;
46
46
  "signIn.selectTenant": string;
47
47
  "signIn.selectTenantSubtitle": string;
48
+ "signIn.selectScope": string;
48
49
  "signIn.dividerOr": string;
49
50
  "signIn.preparingExperience": string;
50
51
  "signIn.applicationUnavailable": string;
@@ -133,6 +134,11 @@ interface IQAuthLocaleBundle {
133
134
  "orgSwitcher.createNew": string;
134
135
  "orgSwitcher.manage": string;
135
136
  "orgSwitcher.noOrgs": string;
137
+ "orgSwitcher.mfaRequiredTitle": string;
138
+ "orgSwitcher.mfaRequiredBody": string;
139
+ "orgSwitcher.scopeSelectionRequiredTitle": string;
140
+ "orgSwitcher.scopeSelectionRequiredBody": string;
141
+ "orgSwitcher.continueInHostedSignIn": string;
136
142
  "orgProfile.title": string;
137
143
  "orgProfile.generalTab": string;
138
144
  "orgProfile.membersTab": string;
@@ -43,6 +43,17 @@ interface VerifyWebhookOptions {
43
43
  toleranceSeconds?: number;
44
44
  /** Override the current time (seconds since epoch) — for tests. */
45
45
  nowSeconds?: number;
46
+ /**
47
+ * Security escape hatch (H-3): modern `v1=<hex>`-only deliveries carry no
48
+ * header `t=`, so freshness is derived from the CloudEvents envelope `time`
49
+ * field instead. By default a modern delivery that carries NO enforceable
50
+ * timestamp (no header `t=` and no parseable envelope `time`) is **rejected**
51
+ * so a captured body cannot be replayed indefinitely.
52
+ *
53
+ * Set `true` ONLY for legacy integrations that sign non-envelope bodies with
54
+ * the modern scheme and accept the replay risk. Defaults to `false` (secure).
55
+ */
56
+ allowMissingTimestamp?: boolean;
46
57
  }
47
58
  interface IQAuthWebhookEvent {
48
59
  id?: string;
@@ -93,15 +104,17 @@ declare const LEGACY_SIGNATURE_HEADERS: readonly ["x-webhook-signature", "x-iq-a
93
104
  * Throws `WebhookSignatureError` on any verification failure.
94
105
  *
95
106
  * 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.
107
+ * legacy `t=<unix>,v1=<hex>` header (HMAC over `${t}.${body}`). Replay-window
108
+ * tolerance is enforced for BOTH forms (H-3):
109
+ * - legacy `t=` deliveries: from the header timestamp (checked pre-verify);
110
+ * - modern `v1=`-only deliveries: from the CloudEvents envelope `time` field
111
+ * (checked post-verify). A modern delivery that carries no enforceable
112
+ * timestamp is rejected unless `allowMissingTimestamp` is set.
113
+ *
114
+ * **For canonical Task #129 deliveries, prefer `parseWebhookEvent`** — it
115
+ * validates the full CloudEvents envelope, supports multi-secret rotation, and
116
+ * returns a typed event with a stable `idempotencyKey`. `verifyWebhookSignature`
117
+ * is retained for back-compat with single-secret integrations.
105
118
  */
106
119
  declare function verifyWebhookSignature(opts: VerifyWebhookOptions): IQAuthWebhookEvent;
107
120
  /**
@@ -43,6 +43,17 @@ interface VerifyWebhookOptions {
43
43
  toleranceSeconds?: number;
44
44
  /** Override the current time (seconds since epoch) — for tests. */
45
45
  nowSeconds?: number;
46
+ /**
47
+ * Security escape hatch (H-3): modern `v1=<hex>`-only deliveries carry no
48
+ * header `t=`, so freshness is derived from the CloudEvents envelope `time`
49
+ * field instead. By default a modern delivery that carries NO enforceable
50
+ * timestamp (no header `t=` and no parseable envelope `time`) is **rejected**
51
+ * so a captured body cannot be replayed indefinitely.
52
+ *
53
+ * Set `true` ONLY for legacy integrations that sign non-envelope bodies with
54
+ * the modern scheme and accept the replay risk. Defaults to `false` (secure).
55
+ */
56
+ allowMissingTimestamp?: boolean;
46
57
  }
47
58
  interface IQAuthWebhookEvent {
48
59
  id?: string;
@@ -93,15 +104,17 @@ declare const LEGACY_SIGNATURE_HEADERS: readonly ["x-webhook-signature", "x-iq-a
93
104
  * Throws `WebhookSignatureError` on any verification failure.
94
105
  *
95
106
  * 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.
107
+ * legacy `t=<unix>,v1=<hex>` header (HMAC over `${t}.${body}`). Replay-window
108
+ * tolerance is enforced for BOTH forms (H-3):
109
+ * - legacy `t=` deliveries: from the header timestamp (checked pre-verify);
110
+ * - modern `v1=`-only deliveries: from the CloudEvents envelope `time` field
111
+ * (checked post-verify). A modern delivery that carries no enforceable
112
+ * timestamp is rejected unless `allowMissingTimestamp` is set.
113
+ *
114
+ * **For canonical Task #129 deliveries, prefer `parseWebhookEvent`** — it
115
+ * validates the full CloudEvents envelope, supports multi-secret rotation, and
116
+ * returns a typed event with a stable `idempotencyKey`. `verifyWebhookSignature`
117
+ * is retained for back-compat with single-secret integrations.
105
118
  */
106
119
  declare function verifyWebhookSignature(opts: VerifyWebhookOptions): IQAuthWebhookEvent;
107
120
  /**
package/dist/webhooks.js CHANGED
@@ -113,12 +113,8 @@ function verifyWebhookSignature(opts) {
113
113
  }
114
114
  const body = toBuffer(opts.payload);
115
115
  const { modern, legacy } = computeSignatures(opts.secret, body, t);
116
- const matched = v1.some((sig) => {
117
- const lower = sig.toLowerCase();
118
- if (timingSafeEqualHex(lower, modern)) return true;
119
- if (legacy && timingSafeEqualHex(lower, legacy)) return true;
120
- return false;
121
- });
116
+ const expected = Number.isFinite(t) ? legacy : modern;
117
+ const matched = expected !== null && v1.some((sig) => timingSafeEqualHex(sig.toLowerCase(), expected));
122
118
  if (!matched) {
123
119
  throw new WebhookSignatureError("SIGNATURE_MISMATCH", "Webhook signature does not match expected value");
124
120
  }
@@ -128,6 +124,29 @@ function verifyWebhookSignature(opts) {
128
124
  } catch {
129
125
  throw new WebhookSignatureError("MALFORMED_BODY", "Webhook body is not valid JSON");
130
126
  }
127
+ if (!Number.isFinite(t) && !opts.allowMissingTimestamp) {
128
+ const rawTime = parsed.time;
129
+ if (typeof rawTime !== "string" || !rawTime) {
130
+ throw new WebhookSignatureError(
131
+ "MISSING_TIMESTAMP",
132
+ "Modern webhook delivery has no header `t=` and no envelope `time`; cannot enforce replay protection. Use parseWebhookEvent, or set allowMissingTimestamp:true (insecure)."
133
+ );
134
+ }
135
+ const eventMs = Date.parse(rawTime);
136
+ if (!Number.isFinite(eventMs)) {
137
+ throw new WebhookSignatureError(
138
+ "MALFORMED_TIMESTAMP",
139
+ `Envelope \`time\` is not a valid ISO timestamp: ${rawTime}`
140
+ );
141
+ }
142
+ const nowMs = (opts.nowSeconds ?? Math.floor(Date.now() / 1e3)) * 1e3;
143
+ if (Math.abs(nowMs - eventMs) > tolerance * 1e3) {
144
+ throw new WebhookSignatureError(
145
+ "TIMESTAMP_OUT_OF_TOLERANCE",
146
+ `Envelope time ${rawTime} is outside the ${tolerance}s tolerance window`
147
+ );
148
+ }
149
+ }
131
150
  return parsed;
132
151
  }
133
152
  function isValidWebhookSignature(opts) {
@@ -197,12 +216,8 @@ function parseWebhookEvent(rawBody, headers, secrets, opts = {}) {
197
216
  const secret = secrets[i];
198
217
  if (!secret) continue;
199
218
  const { modern, legacy } = computeSignatures(secret, body, t);
200
- const ok = v1.some((sig) => {
201
- const lower = sig.toLowerCase();
202
- if (timingSafeEqualHex(lower, modern)) return true;
203
- if (legacy && timingSafeEqualHex(lower, legacy)) return true;
204
- return false;
205
- });
219
+ const expected = Number.isFinite(t) ? legacy : modern;
220
+ const ok = expected !== null && v1.some((sig) => timingSafeEqualHex(sig.toLowerCase(), expected));
206
221
  if (ok) {
207
222
  verifiedIdx = i;
208
223
  break;
package/dist/webhooks.mjs CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  isValidWebhookSignature,
6
6
  parseWebhookEvent,
7
7
  verifyWebhookSignature
8
- } from "./chunk-PMAFENVI.mjs";
8
+ } from "./chunk-VYQ3ETCK.mjs";
9
9
  import "./chunk-Y6FXYEAI.mjs";
10
10
  export {
11
11
  IQAUTH_SIGNATURE_HEADER,
package/dist/ws.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { c as TokenVerifyOptions } from './tokens-CITeoG6P.mjs';
2
- import { J as JwtClaims } from './types-XOV9XPVi.mjs';
1
+ import { c as TokenVerifyOptions } from './tokens-B06VtvUi.mjs';
2
+ import { J as JwtClaims } from './types-Bn8O-OEd.mjs';
3
3
 
4
4
  /**
5
5
  * @iqauth/sdk/ws — WebSocket upgrade auth helper.
package/dist/ws.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { c as TokenVerifyOptions } from './tokens-Bqhmqq_R.js';
2
- import { J as JwtClaims } from './types-XOV9XPVi.js';
1
+ import { c as TokenVerifyOptions } from './tokens-9F6ETrzk.js';
2
+ import { J as JwtClaims } from './types-Bn8O-OEd.js';
3
3
 
4
4
  /**
5
5
  * @iqauth/sdk/ws — WebSocket upgrade auth helper.
@@ -33,11 +33,76 @@ const result = await client.invites.create({
33
33
  role: "user",
34
34
  vendorId: "vendor-uuid", // optional
35
35
  products: ["iqcapture"], // optional product access
36
+
37
+ // Optional: auto-land the invitee in your app after they accept
38
+ // (new users only — see "Auto-redirect after accept" below).
39
+ redirectUri: "https://app.example.com/iqauth/callback",
40
+ clientId: "oidc-client-id-for-your-app",
36
41
  });
37
42
  // result.inviteToken — for programmatic flows
38
43
  // In production, user receives email with invite link
39
44
  ```
40
45
 
46
+ ### Auto-redirect after accept (opt-in)
47
+
48
+ Pass `redirectUri` + `clientId` together when creating the invite to have
49
+ the hosted accept page mint an OIDC authorization code and `302` straight
50
+ into your app after acceptance. The inviter app's existing
51
+ `/api/iqauth/callback` adapter (Express / Fastify / Hono / Next) handles
52
+ the code exchange — no SDK code changes required on the consumer side.
53
+
54
+ **Constraints (validated at create time):**
55
+
56
+ - Both fields must be present, or both omitted (half-pairs return `400`).
57
+ - `clientId` must reference an **active OIDC client bound to the same tenant**
58
+ as the invite. Platform-wide clients (`tenant_id IS NULL`) are rejected.
59
+ - `redirectUri` must match one of the client's registered redirect URIs,
60
+ using the same normalizing comparator as `/oidc/authorize` (trailing-slash
61
+ and case-insensitive host).
62
+ - Both checks re-run at accept time; if the client/redirect config drifted
63
+ after the invite was sent, the accept silently falls back to the default
64
+ post-accept landing.
65
+
66
+ **Response shape on `accept()`:**
67
+
68
+ The `redirectTo` key is **omitted from the response entirely** (not `null`) when:
69
+
70
+ - the invite was created without `redirectUri` + `clientId` (byte-for-byte back-compat),
71
+ - the accepting user already had an IQAuth account (`existedUser === true`) — see
72
+ *Known limitations* below,
73
+ - or the client/redirect config changed between create and accept.
74
+
75
+ When present, `redirectTo` is a fully-qualified URL of the form
76
+ `<redirectUri>?code=<oidc_code>&state=<random>`. Treat it as optional and
77
+ only act on it when present.
78
+
79
+ **Known limitations (v1):**
80
+
81
+ - **Existing users.** When the invitee already has an IQAuth account, `redirectTo`
82
+ is suppressed. The accept endpoint is anonymous (the invite token is the only
83
+ credential) — minting an OIDC code for a pre-existing account would be an
84
+ auth-assurance downgrade for any account that has MFA configured.
85
+ - **Multi-membership / scope picker.** Deferred for the same reason.
86
+ - **PKCE / public clients.** The minted code is a confidential-client OIDC code.
87
+ Public-client / SPA flows that require PKCE are out of scope in v1.
88
+
89
+ ### Bulk-invite redirect
90
+
91
+ `/api/v1/invites/bulk` accepts a single bulk-level `redirectUri` + `clientId`
92
+ pair applied to every row, validated once before any rows are created:
93
+
94
+ ```typescript
95
+ await client.invites.createBulk({
96
+ tenantId: "tenant-uuid",
97
+ invitations: [
98
+ { email: "alice@example.com", role: "user" },
99
+ { email: "bob@example.com", role: "user" },
100
+ ],
101
+ redirectUri: "https://app.example.com/iqauth/callback",
102
+ clientId: "oidc-client-id-for-your-app",
103
+ });
104
+ ```
105
+
41
106
  ### 2. Validate Token
42
107
 
43
108
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iqauth/sdk",
3
- "version": "2.7.0",
3
+ "version": "2.8.1",
4
4
  "description": "TypeScript SDK for IQAuth — the canonical way for all IQ projects to integrate with IQAuthService",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -102,7 +102,9 @@
102
102
  ],
103
103
  "scripts": {
104
104
  "build": "tsup src/index.ts src/browser-session.ts src/browser.ts src/react.ts src/react-permissions.ts src/server.ts src/server/handlers.ts src/express.ts src/fastify.ts src/hono.ts src/next.ts src/mobile.ts src/service.ts src/ws.ts src/test.ts src/webhooks.ts src/locales.ts src/cli/index.ts --format cjs,esm --dts --clean --external react --external react-dom --external next/headers --external @tanstack/react-query",
105
- "test": "vitest run",
105
+ "test": "vitest run && vitest run --config vitest.dom.config.mts",
106
+ "test:node": "vitest run",
107
+ "test:dom": "vitest run --config vitest.dom.config.mts",
106
108
  "test:watch": "vitest",
107
109
  "test:coverage": "vitest run --coverage",
108
110
  "typecheck": "tsc --noEmit"
@@ -129,10 +131,13 @@
129
131
  },
130
132
  "devDependencies": {
131
133
  "@tanstack/react-query": "^5.60.5",
134
+ "@testing-library/dom": "^10.4.1",
135
+ "@testing-library/react": "^16.3.2",
132
136
  "@types/jsonwebtoken": "^9.0.7",
133
137
  "@types/node": "^20.0.0",
134
138
  "@types/react": "^18.0.0",
135
139
  "@types/react-dom": "^18.0.0",
140
+ "jsdom": "^29.1.1",
136
141
  "react": "^18.0.0",
137
142
  "react-dom": "^18.0.0",
138
143
  "tsup": "^8.0.0",