@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.
- package/dist/browser-session.d.mts +3 -3
- package/dist/browser-session.d.ts +3 -3
- package/dist/browser-session.js +31 -5
- package/dist/browser-session.mjs +1 -1
- package/dist/browser.d.mts +3 -3
- package/dist/browser.d.ts +3 -3
- package/dist/browser.js +23 -3
- package/dist/browser.mjs +1 -1
- package/dist/{chunk-YVALAG3B.mjs → chunk-25SSYDIP.mjs} +1 -1
- package/dist/{chunk-RTJAIBXY.mjs → chunk-4V7FKOTG.mjs} +23 -3
- package/dist/{chunk-SL3KRS4W.mjs → chunk-CIJORODR.mjs} +23 -1
- package/dist/chunk-JRDVUWAL.mjs +46 -0
- package/dist/{chunk-5T7GHBX6.mjs → chunk-TLET552H.mjs} +36 -0
- package/dist/{chunk-PMAFENVI.mjs → chunk-VYQ3ETCK.mjs} +27 -12
- package/dist/{chunk-RR2MGPTK.mjs → chunk-WHT6WKTY.mjs} +539 -83
- package/dist/{chunk-RUJXRTEW.mjs → chunk-WSH4SW7F.mjs} +122 -8
- package/dist/{chunk-JXQI62A7.mjs → chunk-ZLJPABB7.mjs} +31 -5
- package/dist/{client-BGFnBpfc.d.mts → client-D8L-PaWr.d.mts} +14 -4
- package/dist/{client-CDQ21LvW.d.ts → client-DkPL0EPZ.d.ts} +14 -4
- package/dist/{express-Piv2WhWM.d.ts → express-Budysq4h.d.ts} +2 -2
- package/dist/{express-CVNQEkOr.d.mts → express-DDTA3qV1.d.mts} +2 -2
- package/dist/express.d.mts +5 -5
- package/dist/express.d.ts +5 -5
- package/dist/express.js +217 -36
- package/dist/express.mjs +38 -26
- package/dist/fastify.d.mts +10 -2
- package/dist/fastify.d.ts +10 -2
- package/dist/fastify.js +260 -16
- package/dist/fastify.mjs +80 -5
- package/dist/hono.d.mts +10 -2
- package/dist/hono.d.ts +10 -2
- package/dist/hono.js +240 -16
- package/dist/hono.mjs +60 -5
- package/dist/{index-5KSZEnDe.d.ts → index-Cko-d5po.d.mts} +227 -5
- package/dist/{index-CKoZHAoc.d.mts → index-RNqwEcmY.d.ts} +227 -5
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +149 -26
- package/dist/index.mjs +5 -5
- package/dist/locales.d.mts +1 -1
- package/dist/locales.d.ts +1 -1
- package/dist/locales.js +36 -0
- package/dist/locales.mjs +1 -1
- package/dist/mobile.d.mts +3 -3
- package/dist/mobile.d.ts +3 -3
- package/dist/mobile.js +31 -5
- package/dist/mobile.mjs +1 -1
- package/dist/next.d.mts +10 -2
- package/dist/next.d.ts +10 -2
- package/dist/next.js +212 -11
- package/dist/next.mjs +62 -4
- package/dist/{provisioningBridge-M5G47LWO.d.mts → provisioningBridge-BXPMZCLe.d.ts} +30 -2
- package/dist/{provisioningBridge-CGpMRie4.d.ts → provisioningBridge-IEycmsgb.d.mts} +30 -2
- package/dist/react-permissions.d.mts +4 -4
- package/dist/react-permissions.d.ts +4 -4
- package/dist/react-permissions.mjs +4 -3
- package/dist/react.d.mts +4 -4
- package/dist/react.d.ts +4 -4
- package/dist/react.js +570 -41
- package/dist/react.mjs +19 -5
- package/dist/server/handlers.d.mts +56 -5
- package/dist/server/handlers.d.ts +56 -5
- package/dist/server/handlers.js +123 -8
- package/dist/server/handlers.mjs +3 -1
- package/dist/server.d.mts +28 -8
- package/dist/server.d.ts +28 -8
- package/dist/server.js +176 -14
- package/dist/server.mjs +9 -4
- package/dist/service.d.mts +3 -3
- package/dist/service.d.ts +3 -3
- package/dist/service.js +31 -5
- package/dist/service.mjs +1 -1
- package/dist/{signIn-T-CZ6t6r.d.mts → signIn-CReqfXsh.d.mts} +18 -1
- package/dist/{signIn-BLFnz8SV.d.ts → signIn-Cfa1GTpO.d.ts} +18 -1
- package/dist/{tokens-Bqhmqq_R.d.ts → tokens-9F6ETrzk.d.ts} +1 -1
- package/dist/{tokens-CITeoG6P.d.mts → tokens-B06VtvUi.d.mts} +1 -1
- package/dist/{types-XOV9XPVi.d.mts → types-Bn8O-OEd.d.mts} +66 -2
- package/dist/{types-XOV9XPVi.d.ts → types-Bn8O-OEd.d.ts} +66 -2
- package/dist/{types-BdQ2lqfT.d.mts → types-DnU2LhXR.d.mts} +6 -0
- package/dist/{types-BdQ2lqfT.d.ts → types-DnU2LhXR.d.ts} +6 -0
- package/dist/webhooks.d.mts +22 -9
- package/dist/webhooks.d.ts +22 -9
- package/dist/webhooks.js +27 -12
- package/dist/webhooks.mjs +1 -1
- package/dist/ws.d.mts +2 -2
- package/dist/ws.d.ts +2 -2
- package/docs/guides/invitations.md +65 -0
- 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
|
-
|
|
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,
|
|
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;
|
package/dist/webhooks.d.mts
CHANGED
|
@@ -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}`).
|
|
97
|
-
*
|
|
98
|
-
* `
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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.d.ts
CHANGED
|
@@ -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}`).
|
|
97
|
-
*
|
|
98
|
-
* `
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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
|
|
117
|
-
|
|
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
|
|
201
|
-
|
|
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
package/dist/ws.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { c as TokenVerifyOptions } from './tokens-
|
|
2
|
-
import { J as JwtClaims } from './types-
|
|
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-
|
|
2
|
-
import { J as JwtClaims } from './types-
|
|
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.
|
|
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",
|