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.
Files changed (68) hide show
  1. package/LICENSE +661 -21
  2. package/dist/assets/u/js/client.js +2 -2
  3. package/dist/assets/u/widget/authhero-widget.esm.js +1 -1
  4. package/dist/assets/u/widget/index.esm.js +1 -1
  5. package/dist/assets/u/widget/{p-52a70476.entry.js → p-cdfc4555.entry.js} +1 -1
  6. package/dist/assets/u/widget/{p-f6babd26.entry.js → p-eca60302.entry.js} +1 -1
  7. package/dist/authhero.cjs +268 -251
  8. package/dist/authhero.d.ts +186 -95
  9. package/dist/authhero.mjs +26150 -18257
  10. package/dist/client.js +2 -2
  11. package/dist/tsconfig.types.tsbuildinfo +1 -1
  12. package/dist/types/authentication-flows/passwordless.d.ts +4 -3
  13. package/dist/types/client/client-bundle.d.ts +1 -1
  14. package/dist/types/constants.d.ts +2 -0
  15. package/dist/types/generated/locale-types.d.ts +2 -0
  16. package/dist/types/helpers/client.d.ts +2 -2
  17. package/dist/types/helpers/dcr/metadata-mapping.d.ts +1 -1
  18. package/dist/types/helpers/link-candidates.d.ts +28 -0
  19. package/dist/types/helpers/logging.d.ts +8 -0
  20. package/dist/types/helpers/revoke-user-sessions.d.ts +15 -0
  21. package/dist/types/helpers/scim/default-mapping.d.ts +10 -0
  22. package/dist/types/helpers/scim/discovery.d.ts +49 -0
  23. package/dist/types/helpers/scim/filter.d.ts +58 -0
  24. package/dist/types/helpers/scim/mint-token.d.ts +11 -0
  25. package/dist/types/helpers/scim/patch.d.ts +23 -0
  26. package/dist/types/helpers/scim/responses.d.ts +36 -0
  27. package/dist/types/helpers/scim/user-mapping.d.ts +76 -0
  28. package/dist/types/helpers/scopes-permissions.d.ts +18 -0
  29. package/dist/types/helpers/signing-keys.d.ts +36 -0
  30. package/dist/types/helpers/users.d.ts +116 -1
  31. package/dist/types/hooks/codehooks.d.ts +9 -1
  32. package/dist/types/hooks/helpers/post-login-account-linking.d.ts +52 -0
  33. package/dist/types/hooks/pre-defined/account-linking.d.ts +20 -0
  34. package/dist/types/hooks/user-registration.d.ts +1 -0
  35. package/dist/types/index.d.ts +121 -91
  36. package/dist/types/middlewares/scim-auth.d.ts +19 -0
  37. package/dist/types/routes/auth-api/index.d.ts +34 -34
  38. package/dist/types/routes/auth-api/passwordless.d.ts +16 -16
  39. package/dist/types/routes/auth-api/register/index.d.ts +2 -2
  40. package/dist/types/routes/auth-api/revoke.d.ts +6 -6
  41. package/dist/types/routes/auth-api/token.d.ts +10 -10
  42. package/dist/types/routes/management-api/action-executions.d.ts +1 -1
  43. package/dist/types/routes/management-api/actions.d.ts +1 -1
  44. package/dist/types/routes/management-api/authentication-methods.d.ts +1 -1
  45. package/dist/types/routes/management-api/branding.d.ts +1 -1
  46. package/dist/types/routes/management-api/clients.d.ts +8 -8
  47. package/dist/types/routes/management-api/email-templates.d.ts +18 -18
  48. package/dist/types/routes/management-api/failed-events.d.ts +2 -1
  49. package/dist/types/routes/management-api/forms.d.ts +14 -0
  50. package/dist/types/routes/management-api/guardian.d.ts +5 -5
  51. package/dist/types/routes/management-api/index.d.ts +76 -50
  52. package/dist/types/routes/management-api/logs.d.ts +4 -4
  53. package/dist/types/routes/management-api/organizations.d.ts +1 -1
  54. package/dist/types/routes/management-api/prompts.d.ts +6 -4
  55. package/dist/types/routes/management-api/scim.d.ts +187 -0
  56. package/dist/types/routes/management-api/tenants.d.ts +1 -1
  57. package/dist/types/routes/management-api/users-by-email.d.ts +1 -0
  58. package/dist/types/routes/management-api/users.d.ts +13 -5
  59. package/dist/types/routes/scim/index.d.ts +6 -0
  60. package/dist/types/routes/universal-login/common.d.ts +3 -2
  61. package/dist/types/routes/universal-login/flow-api.d.ts +4 -4
  62. package/dist/types/routes/universal-login/screens/types.d.ts +9 -0
  63. package/dist/types/routes/universal-login/u2-index.d.ts +5 -5
  64. package/dist/types/routes/universal-login/u2-routes.d.ts +5 -5
  65. package/dist/types/types/AuthError.d.ts +1 -1
  66. package/dist/types/types/Hooks.d.ts +8 -1
  67. package/dist/types/utils/cookies.d.ts +11 -0
  68. 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
- export declare function getPrimaryUserByEmail({ userAdapter, tenant_id, email, }: GetPrimaryUserByEmailParams): Promise<User | undefined>;
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?: {