authhero 8.27.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -21
- package/dist/assets/u/js/client.js +2 -2
- package/dist/assets/u/widget/authhero-widget.esm.js +1 -1
- package/dist/assets/u/widget/index.esm.js +1 -1
- package/dist/assets/u/widget/{p-52a70476.entry.js → p-cdfc4555.entry.js} +1 -1
- package/dist/assets/u/widget/{p-f6babd26.entry.js → p-eca60302.entry.js} +1 -1
- package/dist/authhero.cjs +268 -251
- package/dist/authhero.d.ts +186 -95
- package/dist/authhero.mjs +26150 -18257
- package/dist/client.js +2 -2
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/dist/types/authentication-flows/passwordless.d.ts +4 -3
- package/dist/types/client/client-bundle.d.ts +1 -1
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/generated/locale-types.d.ts +2 -0
- package/dist/types/helpers/client.d.ts +2 -2
- package/dist/types/helpers/dcr/metadata-mapping.d.ts +1 -1
- package/dist/types/helpers/link-candidates.d.ts +28 -0
- package/dist/types/helpers/logging.d.ts +8 -0
- package/dist/types/helpers/revoke-user-sessions.d.ts +15 -0
- package/dist/types/helpers/scim/default-mapping.d.ts +10 -0
- package/dist/types/helpers/scim/discovery.d.ts +49 -0
- package/dist/types/helpers/scim/filter.d.ts +58 -0
- package/dist/types/helpers/scim/mint-token.d.ts +11 -0
- package/dist/types/helpers/scim/patch.d.ts +23 -0
- package/dist/types/helpers/scim/responses.d.ts +36 -0
- package/dist/types/helpers/scim/user-mapping.d.ts +76 -0
- package/dist/types/helpers/scopes-permissions.d.ts +18 -0
- package/dist/types/helpers/signing-keys.d.ts +36 -0
- package/dist/types/helpers/users.d.ts +116 -1
- package/dist/types/hooks/codehooks.d.ts +9 -1
- package/dist/types/hooks/helpers/post-login-account-linking.d.ts +52 -0
- package/dist/types/hooks/pre-defined/account-linking.d.ts +20 -0
- package/dist/types/hooks/user-registration.d.ts +1 -0
- package/dist/types/index.d.ts +121 -91
- package/dist/types/middlewares/scim-auth.d.ts +19 -0
- package/dist/types/routes/auth-api/index.d.ts +34 -34
- package/dist/types/routes/auth-api/passwordless.d.ts +16 -16
- package/dist/types/routes/auth-api/register/index.d.ts +2 -2
- package/dist/types/routes/auth-api/revoke.d.ts +6 -6
- package/dist/types/routes/auth-api/token.d.ts +10 -10
- package/dist/types/routes/management-api/action-executions.d.ts +1 -1
- package/dist/types/routes/management-api/actions.d.ts +1 -1
- package/dist/types/routes/management-api/authentication-methods.d.ts +1 -1
- package/dist/types/routes/management-api/branding.d.ts +1 -1
- package/dist/types/routes/management-api/clients.d.ts +8 -8
- package/dist/types/routes/management-api/email-templates.d.ts +18 -18
- package/dist/types/routes/management-api/failed-events.d.ts +2 -1
- package/dist/types/routes/management-api/forms.d.ts +14 -0
- package/dist/types/routes/management-api/guardian.d.ts +5 -5
- package/dist/types/routes/management-api/index.d.ts +76 -50
- package/dist/types/routes/management-api/logs.d.ts +4 -4
- package/dist/types/routes/management-api/organizations.d.ts +1 -1
- package/dist/types/routes/management-api/prompts.d.ts +6 -4
- package/dist/types/routes/management-api/scim.d.ts +187 -0
- package/dist/types/routes/management-api/tenants.d.ts +1 -1
- package/dist/types/routes/management-api/users-by-email.d.ts +1 -0
- package/dist/types/routes/management-api/users.d.ts +13 -5
- package/dist/types/routes/scim/index.d.ts +6 -0
- package/dist/types/routes/universal-login/common.d.ts +3 -2
- package/dist/types/routes/universal-login/flow-api.d.ts +4 -4
- package/dist/types/routes/universal-login/screens/types.d.ts +9 -0
- package/dist/types/routes/universal-login/u2-index.d.ts +5 -5
- package/dist/types/routes/universal-login/u2-routes.d.ts +5 -5
- package/dist/types/types/AuthError.d.ts +1 -1
- package/dist/types/types/Hooks.d.ts +8 -1
- package/dist/types/utils/cookies.d.ts +11 -0
- package/package.json +7 -6
|
@@ -15,3 +15,39 @@ export interface ResolveSigningKeysOptions {
|
|
|
15
15
|
type?: string;
|
|
16
16
|
}
|
|
17
17
|
export declare function resolveSigningKeys(keys: KeysAdapter, tenantId: string, modeOption: SigningKeyModeOption | undefined, opts: ResolveSigningKeysOptions): Promise<SigningKey[]>;
|
|
18
|
+
/** A key is signable only if it carries private material, not just a cert. */
|
|
19
|
+
export declare function isSignable(key: SigningKey): boolean;
|
|
20
|
+
export interface EnsureSigningKeyOptions {
|
|
21
|
+
/**
|
|
22
|
+
* When set, the key is created tenant-scoped (`tenant_id` stamped) and the
|
|
23
|
+
* existence check is scoped to that tenant. Omit for a control-plane key
|
|
24
|
+
* (no `tenant_id`), which `resolveSigningKeys` resolves in both modes — as
|
|
25
|
+
* the primary key in `"control-plane"` mode and as the fallback in
|
|
26
|
+
* `"tenant"` mode. Control-plane scope is the right default for a freshly
|
|
27
|
+
* provisioned WFP tenant.
|
|
28
|
+
*/
|
|
29
|
+
tenantId?: string;
|
|
30
|
+
/** Cert CN. Falls back to the tenant id, then to `"authhero"`. */
|
|
31
|
+
name?: string;
|
|
32
|
+
/** Defaults to `"jwt_signing"`. */
|
|
33
|
+
type?: SigningKey["type"];
|
|
34
|
+
}
|
|
35
|
+
export interface EnsureSigningKeyResult {
|
|
36
|
+
/** True when a new key was minted; false when a signable key already existed. */
|
|
37
|
+
created: boolean;
|
|
38
|
+
key: SigningKey;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Guarantees the target scope holds at least one *signable* key — one carrying
|
|
42
|
+
* private material (`pkcs7` + `cert`), not merely a projected public verify key.
|
|
43
|
+
*
|
|
44
|
+
* A freshly provisioned WFP tenant inherits only the control plane's public
|
|
45
|
+
* keys (private material stripped at the boundary), so it can serve JWKS and
|
|
46
|
+
* `/authorize` but 500s at `/oauth/token` with nothing to sign — issue #1181.
|
|
47
|
+
* Calling this at provision time closes that gap by minting the tenant's own
|
|
48
|
+
* RS256 key locally (private material never crosses the control-plane boundary).
|
|
49
|
+
*
|
|
50
|
+
* Create-if-missing and idempotent: a scope that already has a signable key is
|
|
51
|
+
* left untouched, so it is safe to call on every provision and re-sync.
|
|
52
|
+
*/
|
|
53
|
+
export declare function ensureSigningKey(keys: KeysAdapter, opts?: EnsureSigningKeyOptions): Promise<EnsureSigningKeyResult>;
|
|
@@ -21,12 +21,84 @@ export declare function getUserByProvider({ userAdapter, tenant_id, username, pr
|
|
|
21
21
|
* same millisecond).
|
|
22
22
|
*/
|
|
23
23
|
export declare function compareUsersByAge(a: User, b: User): number;
|
|
24
|
+
/**
|
|
25
|
+
* Order users by how recently they were used to log in (most recent first).
|
|
26
|
+
*
|
|
27
|
+
* Distinct from {@link compareUsersByAge}: that encodes the *linking* policy
|
|
28
|
+
* ("the older account is canonical"), while this answers "which of these
|
|
29
|
+
* accounts is the person actually using?" — the question behind the login
|
|
30
|
+
* screen's last-used-strategy hint.
|
|
31
|
+
*
|
|
32
|
+
* Users that have never logged in sort last. Ties fall back to
|
|
33
|
+
* `compareUsersByAge` so the ordering is fully deterministic.
|
|
34
|
+
*/
|
|
35
|
+
export declare function compareUsersByLastLogin(a: User, b: User): number;
|
|
36
|
+
interface UserExistsByEmailParams {
|
|
37
|
+
userAdapter: UserDataAdapter;
|
|
38
|
+
tenant_id: string;
|
|
39
|
+
email: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* True when any user row carries this email — primary or secondary.
|
|
43
|
+
*
|
|
44
|
+
* The login screens' signup gates only need to know whether an account exists
|
|
45
|
+
* for the address, never which row is canonical. Asking that narrower question
|
|
46
|
+
* directly (rather than via {@link getPrimaryUserByEmail}) keeps them correct
|
|
47
|
+
* on tenants running with user linking off, where several unlinked primaries
|
|
48
|
+
* legitimately share an email, and avoids throwing on a dangling `linked_to`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function userExistsByEmail({ userAdapter, tenant_id, email, }: UserExistsByEmailParams): Promise<boolean>;
|
|
24
51
|
interface GetPrimaryUserByEmailParams {
|
|
25
52
|
userAdapter: UserDataAdapter;
|
|
26
53
|
tenant_id: string;
|
|
27
54
|
email: string;
|
|
55
|
+
/**
|
|
56
|
+
* Log when more than one primary shares the email. Only meaningful on
|
|
57
|
+
* account-linking paths, where duplicate primaries mean linking failed to
|
|
58
|
+
* converge. With `userLinkingMode: "off"` several primaries per email is the
|
|
59
|
+
* expected steady state (it's Auth0's default behaviour), so read paths
|
|
60
|
+
* leave this off rather than logging an error on every login.
|
|
61
|
+
*/
|
|
62
|
+
warnOnMultiplePrimaries?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export declare function getPrimaryUserByEmail({ userAdapter, tenant_id, email, warnOnMultiplePrimaries, }: GetPrimaryUserByEmailParams): Promise<User | undefined>;
|
|
65
|
+
interface GetLastUsedUserByEmailParams {
|
|
66
|
+
userAdapter: UserDataAdapter;
|
|
67
|
+
tenant_id: string;
|
|
68
|
+
email: string;
|
|
28
69
|
}
|
|
29
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The account for `email` that was most recently logged in to.
|
|
72
|
+
*
|
|
73
|
+
* Used for UI hints that describe *this person's habits* — chiefly the
|
|
74
|
+
* last-used-strategy shortcut on the identifier screen. When linking is off, an
|
|
75
|
+
* email can map to several primaries (say a password account and a Google one);
|
|
76
|
+
* picking the oldest, as {@link getPrimaryUserByEmail} does, would hand back
|
|
77
|
+
* whichever account they happened to create first and hint at a login method
|
|
78
|
+
* they may have abandoned. Picking by `last_login` follows the account actually
|
|
79
|
+
* in use.
|
|
80
|
+
*
|
|
81
|
+
* Deliberately *not* provider-biased: preferring the password account would
|
|
82
|
+
* push habitual social users to the password screen.
|
|
83
|
+
*
|
|
84
|
+
* When only secondaries match, the cluster root is returned — login updates
|
|
85
|
+
* land on the primary, so that's where the hint lives. Unlike
|
|
86
|
+
* `getPrimaryUserByEmail` a dangling `linked_to` yields `undefined` rather than
|
|
87
|
+
* throwing; callers fall back to the tenant's default strategy, which is the
|
|
88
|
+
* right outcome for a hint.
|
|
89
|
+
*/
|
|
90
|
+
export declare function getLastUsedUserByEmail({ userAdapter, tenant_id, email, }: GetLastUsedUserByEmailParams): Promise<User | undefined>;
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a user to the primary of its linked cluster. Follows a single
|
|
93
|
+
* `linked_to` hop — the linking invariants keep clusters one level deep
|
|
94
|
+
* (see {@link repointPrimary}) — and falls back to the given user on a
|
|
95
|
+
* dangling link so callers never lose the identity they started with.
|
|
96
|
+
*
|
|
97
|
+
* Used by the forms engine so that post-login profile forms evaluate
|
|
98
|
+
* router conditions against, and stamp submitted values onto, the primary
|
|
99
|
+
* identity even when the session points at a secondary.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resolvePrimaryUser(userAdapter: UserDataAdapter, tenant_id: string, user: User): Promise<User>;
|
|
30
102
|
interface RepointPrimaryParams {
|
|
31
103
|
userAdapter: UserDataAdapter;
|
|
32
104
|
tenant_id: string;
|
|
@@ -44,6 +116,49 @@ interface RepointPrimaryParams {
|
|
|
44
116
|
* the linking logic recursively.
|
|
45
117
|
*/
|
|
46
118
|
export declare function repointPrimary({ userAdapter, tenant_id, formerPrimary, newPrimaryId, }: RepointPrimaryParams): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* True for identities whose *login identifier* is the email address: the native
|
|
121
|
+
* username-password providers (`auth0`/`auth2`) and the passwordless `email`
|
|
122
|
+
* connection. For these, `email` is a credential — it's how the login row is
|
|
123
|
+
* found (`getUserByProvider`), so it must stay consistent across a linked
|
|
124
|
+
* cluster.
|
|
125
|
+
*
|
|
126
|
+
* Social identities carry `email` as ordinary profile data (re-synced from the
|
|
127
|
+
* IdP on every login) and sms identities are keyed by `phone_number`, so neither
|
|
128
|
+
* is email-identified and neither should have its `email` rewritten by a cascade.
|
|
129
|
+
*/
|
|
130
|
+
export declare function isEmailIdentifiedUser(user: Pick<User, "provider" | "connection">): boolean;
|
|
131
|
+
interface CascadeEmailParams {
|
|
132
|
+
userAdapter: UserDataAdapter;
|
|
133
|
+
tenant_id: string;
|
|
134
|
+
/** The cluster root (primary) user_id — its secondaries are enumerated. */
|
|
135
|
+
primaryUserId: string;
|
|
136
|
+
/** The row whose email the caller already updated; skipped by the cascade. */
|
|
137
|
+
sourceUserId: string;
|
|
138
|
+
email: string;
|
|
139
|
+
email_verified: boolean;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Propagate an email change across every *email-identified* identity in a linked
|
|
143
|
+
* cluster so a merged user keeps a single login email.
|
|
144
|
+
*
|
|
145
|
+
* Because account-linking matches on a shared email, at link time every
|
|
146
|
+
* email-identified identity in a cluster carries the same address. The only way
|
|
147
|
+
* they diverge is a later email change on one of them — and when they diverge,
|
|
148
|
+
* the login row for the *other* email-identified identities still carries the
|
|
149
|
+
* old address, so the user can no longer sign in with the address now shown on
|
|
150
|
+
* their profile (the classic "changed my email, can't log in with my password"
|
|
151
|
+
* bug).
|
|
152
|
+
*
|
|
153
|
+
* This re-establishes the invariant: only email-identified rows are touched
|
|
154
|
+
* ({@link isEmailIdentifiedUser}); sms (`phone_number`) and social (provider
|
|
155
|
+
* sub) identifiers are never rewritten. `sourceUserId` is skipped (the caller
|
|
156
|
+
* already wrote it). Each cascaded write goes through the normal decorated
|
|
157
|
+
* `update`, so every affected identity emits its own `user.updated` event for
|
|
158
|
+
* downstream propagation, and `email_verified` moves in lock-step so the whole
|
|
159
|
+
* cluster shares one verification state.
|
|
160
|
+
*/
|
|
161
|
+
export declare function cascadeEmailToLinkedIdentities({ userAdapter, tenant_id, primaryUserId, sourceUserId, email, email_verified, }: CascadeEmailParams): Promise<void>;
|
|
47
162
|
interface GetPrimaryUserByProviderParams {
|
|
48
163
|
userAdapter: UserDataAdapter;
|
|
49
164
|
tenant_id: string;
|
|
@@ -60,11 +60,19 @@ export type CodeHookApi = Record<string, CodeHookApiNamespace>;
|
|
|
60
60
|
/**
|
|
61
61
|
* Replay recorded API calls from code hook execution against real API objects.
|
|
62
62
|
* Handles calls like "accessToken.setCustomClaim" by navigating the api object.
|
|
63
|
+
*
|
|
64
|
+
* Awaits each replayed call. Most registered methods are synchronous (they
|
|
65
|
+
* mutate a token or throw), but some do async work — notably
|
|
66
|
+
* `user.setLinkedTo` at `post-user-login`, which paginates and writes user rows
|
|
67
|
+
* to perform the link. Since replay runs *after* the isolate returns, the login
|
|
68
|
+
* response is built from whatever this resolves to; awaiting guarantees such
|
|
69
|
+
* work completes before the caller proceeds. Synchronous methods return
|
|
70
|
+
* non-thenables, so awaiting them is a no-op.
|
|
63
71
|
*/
|
|
64
72
|
export declare function replayApiCalls(apiCalls: Array<{
|
|
65
73
|
method: string;
|
|
66
74
|
args: unknown[];
|
|
67
|
-
}>, api: CodeHookApi): void
|
|
75
|
+
}>, api: CodeHookApi): Promise<void>;
|
|
68
76
|
export type HandleCodeHookOutcome = {
|
|
69
77
|
result: ActionExecutionResult;
|
|
70
78
|
logs: CodeExecutionLog[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Context } from "hono";
|
|
2
|
+
import { DataAdapters, User } from "@authhero/adapter-interfaces";
|
|
3
|
+
import { Bindings, Variables } from "../../types";
|
|
4
|
+
/**
|
|
5
|
+
* The `user` namespace exposed to `post-user-login` code hooks. `setLinkedTo`
|
|
6
|
+
* is the programmable account-linking verb (issue #1184): an action reads
|
|
7
|
+
* `event.link_candidates` and calls `api.user.setLinkedTo(candidate.user_id)`.
|
|
8
|
+
*/
|
|
9
|
+
export type PostLoginUserApi = {
|
|
10
|
+
user: {
|
|
11
|
+
setLinkedTo: (primaryUserId: string) => Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Builds the guarded `api.user.setLinkedTo` implementation for the
|
|
16
|
+
* `post-user-login` trigger, plus a getter for the resulting primary user id.
|
|
17
|
+
*
|
|
18
|
+
* The recording proxy captures the action's `setLinkedTo(id)` call; the host
|
|
19
|
+
* replays it against this implementation *after* the isolate returns
|
|
20
|
+
* (`replayApiCalls`, which awaits — so the DB writes complete before the login
|
|
21
|
+
* response is built).
|
|
22
|
+
*
|
|
23
|
+
* Guards, all enforced server-side so a malicious/buggy action cannot escalate:
|
|
24
|
+
* - **Candidate allowlist.** `primaryUserId` must be one of `candidates`
|
|
25
|
+
* (the same set serialized into `event.link_candidates`). Without this an
|
|
26
|
+
* action could link the current user to an arbitrary `user_id` —
|
|
27
|
+
* account-takeover. Rejections log `FAILED_HOOK` and mutate nothing.
|
|
28
|
+
* - **Verified-email gate.** When `requireVerifiedEmail` (the default), the
|
|
29
|
+
* logging-in user must have a verified email, regardless of what the action
|
|
30
|
+
* asserts.
|
|
31
|
+
* - **Direction.** Older account wins (`compareUsersByAge`): if the logging-in
|
|
32
|
+
* user pre-dates the target, the target is demoted via `repointPrimary`
|
|
33
|
+
* (avoiding 2-hop chains) instead.
|
|
34
|
+
* - **Idempotency.** A no-op when the user is already linked to the target.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createPostLoginUserApi(params: {
|
|
37
|
+
ctx: Context<{
|
|
38
|
+
Bindings: Bindings;
|
|
39
|
+
Variables: Variables;
|
|
40
|
+
}>;
|
|
41
|
+
data: DataAdapters;
|
|
42
|
+
tenantId: string;
|
|
43
|
+
user: User;
|
|
44
|
+
/** Resolved candidate primaries (same set as `event.link_candidates`). */
|
|
45
|
+
candidates: User[];
|
|
46
|
+
/** @default true */
|
|
47
|
+
requireVerifiedEmail?: boolean;
|
|
48
|
+
}): {
|
|
49
|
+
api: PostLoginUserApi;
|
|
50
|
+
/** The user id of the primary after linking, or null if nothing linked. */
|
|
51
|
+
getLinkedPrimaryId: () => string | null;
|
|
52
|
+
};
|
|
@@ -20,6 +20,26 @@ export interface AccountLinkingOptions {
|
|
|
20
20
|
* @default false
|
|
21
21
|
*/
|
|
22
22
|
copyUserMetadata?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* When the link is performed, fill in *absent* root profile fields on the
|
|
25
|
+
* primary from the secondary — e.g. a secondary that has a `birthdate` the
|
|
26
|
+
* primary lacks, or a phone number an email primary doesn't carry.
|
|
27
|
+
*
|
|
28
|
+
* Fill-if-absent only: a field already set on the primary is never
|
|
29
|
+
* overwritten (primary stays authoritative), matching `copyUserMetadata`.
|
|
30
|
+
* Identifier and verification fields are never touched — `email`,
|
|
31
|
+
* `email_verified`, `phone_verified`, `username`, `provider`, `connection`
|
|
32
|
+
* are excluded — so this can't rewrite a login identifier. In particular an
|
|
33
|
+
* sms primary already carries a `phone_number`, so its identifier is never
|
|
34
|
+
* filled over; the promotion only reaches a primary that has no phone yet.
|
|
35
|
+
*
|
|
36
|
+
* Off by default to match Auth0, which keeps a linked identity's own
|
|
37
|
+
* attributes under `identities[].profileData` rather than promoting them to
|
|
38
|
+
* the merged user's root profile.
|
|
39
|
+
*
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
copyProfileFields?: boolean;
|
|
23
43
|
}
|
|
24
44
|
/**
|
|
25
45
|
* Trigger-agnostic event handler used by all `account-linking` template
|
|
@@ -61,6 +61,7 @@ export declare function createUserHooks(ctx: Context<{
|
|
|
61
61
|
verify_email?: boolean | undefined;
|
|
62
62
|
last_ip?: string | undefined;
|
|
63
63
|
last_login?: string | undefined;
|
|
64
|
+
blocked?: boolean | undefined;
|
|
64
65
|
registration_completed_at?: string | undefined;
|
|
65
66
|
email?: string | undefined;
|
|
66
67
|
identities?: {
|