authhero 9.6.1 → 9.6.4

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 (35) hide show
  1. package/dist/assets/u/widget/index.esm.js +1 -1
  2. package/dist/authhero.cjs +156 -142
  3. package/dist/authhero.d.ts +188 -188
  4. package/dist/authhero.mjs +11226 -11030
  5. package/dist/tsconfig.types.tsbuildinfo +1 -1
  6. package/dist/types/helpers/dcr/metadata-mapping.d.ts +1 -1
  7. package/dist/types/helpers/users.d.ts +111 -20
  8. package/dist/types/hooks/helpers/post-login-account-linking.d.ts +3 -2
  9. package/dist/types/index.d.ts +188 -188
  10. package/dist/types/routes/auth-api/authorize.d.ts +3 -3
  11. package/dist/types/routes/auth-api/index.d.ts +13 -13
  12. package/dist/types/routes/auth-api/oidc-logout.d.ts +2 -2
  13. package/dist/types/routes/auth-api/passwordless.d.ts +10 -10
  14. package/dist/types/routes/auth-api/register/index.d.ts +2 -2
  15. package/dist/types/routes/auth-api/revoke.d.ts +6 -6
  16. package/dist/types/routes/management-api/authentication-methods.d.ts +1 -1
  17. package/dist/types/routes/management-api/branding.d.ts +2 -2
  18. package/dist/types/routes/management-api/clients.d.ts +14 -14
  19. package/dist/types/routes/management-api/connections.d.ts +1 -1
  20. package/dist/types/routes/management-api/custom-domains.d.ts +6 -6
  21. package/dist/types/routes/management-api/forms.d.ts +126 -126
  22. package/dist/types/routes/management-api/guardian.d.ts +5 -5
  23. package/dist/types/routes/management-api/helpers.d.ts +1 -1
  24. package/dist/types/routes/management-api/index.d.ts +170 -170
  25. package/dist/types/routes/management-api/logs.d.ts +4 -4
  26. package/dist/types/routes/management-api/organizations.d.ts +5 -5
  27. package/dist/types/routes/management-api/prompts.d.ts +4 -4
  28. package/dist/types/routes/management-api/roles.d.ts +1 -1
  29. package/dist/types/routes/management-api/tenant-export-import.d.ts +5 -5
  30. package/dist/types/routes/management-api/tenants.d.ts +28 -28
  31. package/dist/types/routes/management-api/users.d.ts +2 -2
  32. package/dist/types/routes/universal-login/flow-api.d.ts +8 -8
  33. package/dist/types/routes/universal-login/u2-index.d.ts +5 -5
  34. package/dist/types/routes/universal-login/u2-routes.d.ts +5 -5
  35. package/package.json +6 -6
@@ -24,10 +24,10 @@ export declare const dcrRequestSchema: z.ZodObject<{
24
24
  response_types: z.ZodOptional<z.ZodArray<z.ZodString>>;
25
25
  token_endpoint_auth_method: z.ZodOptional<z.ZodEnum<{
26
26
  none: "none";
27
- private_key_jwt: "private_key_jwt";
28
27
  client_secret_post: "client_secret_post";
29
28
  client_secret_basic: "client_secret_basic";
30
29
  client_secret_jwt: "client_secret_jwt";
30
+ private_key_jwt: "private_key_jwt";
31
31
  }>>;
32
32
  jwks_uri: z.ZodOptional<z.ZodString>;
33
33
  jwks: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -3,6 +3,18 @@ import { EnrichedClient } from "./client";
3
3
  import { Context } from "hono";
4
4
  import { Bindings, Variables } from "../types";
5
5
  export declare function getUsersByEmail(userAdapter: UserDataAdapter, tenantId: string, email: string): Promise<User[]>;
6
+ /**
7
+ * Every user sharing `email`, across all pages.
8
+ *
9
+ * {@link getUsersByEmail} returns a single ten-row page, which is fine for the
10
+ * Auth0-shaped `/users-by-email` endpoint but not for a uniqueness check:
11
+ * {@link findEmailConflict} *filters* candidates by connection and cluster, so a
12
+ * truncated page can hide the one row that genuinely conflicts and let a
13
+ * duplicate through. An address legitimately spans many rows (one per provider
14
+ * in a linked cluster), so ten is well within reach. Mirrors the pagination loop
15
+ * in `resolveLinkCandidates`.
16
+ */
17
+ export declare function getAllUsersByEmail(userAdapter: UserDataAdapter, tenantId: string, email: string): Promise<User[]>;
6
18
  interface GetUserByProviderParams {
7
19
  userAdapter: UserDataAdapter;
8
20
  tenant_id: string;
@@ -89,33 +101,71 @@ interface GetLastUsedUserByEmailParams {
89
101
  */
90
102
  export declare function getLastUsedUserByEmail({ userAdapter, tenant_id, email, }: GetLastUsedUserByEmailParams): Promise<User | undefined>;
91
103
  /**
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.
104
+ * Walk `linked_to` from `userId` to the root of its cluster.
105
+ *
106
+ * Correct data resolves in a single hop {@link linkUserTo} is the only writer
107
+ * of `linked_to` and it keeps clusters flat. This follows the chain anyway, and
108
+ * loudly, because pre-existing rows written before that invariant was enforced
109
+ * can still be several hops deep: degrading to the right answer beats handing a
110
+ * caller a mid-chain identity that reads as a different person.
111
+ *
112
+ * Returns `userId` unchanged when it is a root, is unknown to the adapter, or
113
+ * carries a dangling link, so callers never lose the identity they started
114
+ * with. Cycles and over-deep chains stop at the last id reached.
115
+ */
116
+ export declare function resolveClusterRootId(userAdapter: UserDataAdapter, tenant_id: string, userId: string): Promise<string>;
117
+ /**
118
+ * Resolve a user to the primary of its linked cluster, falling back to the
119
+ * given user when the link is dangling so callers never lose the identity they
120
+ * started with.
121
+ *
122
+ * This is the single resolver every read path should use — token minting
123
+ * (authorization-code, refresh-token, silent, token-exchange, password), the
124
+ * forms engine, and anything else that needs the canonical identity behind a
125
+ * session. Resolving through {@link resolveClusterRootId} means a cluster that
126
+ * is already corrupt degrades to the correct answer instead of minting a token
127
+ * for a mid-chain `sub`.
100
128
  */
101
129
  export declare function resolvePrimaryUser(userAdapter: UserDataAdapter, tenant_id: string, user: User): Promise<User>;
102
- interface RepointPrimaryParams {
130
+ interface LinkUserToParams {
131
+ /**
132
+ * Must be an *undecorated* adapter. {@link linkUserTo} is what the user-update
133
+ * decorator's `linked_to` fast-path calls, so handing it a decorated adapter
134
+ * would make its own writes re-enter that fast-path and recurse. Hook code
135
+ * holding `ctx.env.data` should not call this — a plain
136
+ * `users.update(id, { linked_to })` already routes through here.
137
+ */
103
138
  userAdapter: UserDataAdapter;
104
139
  tenant_id: string;
105
- formerPrimary: User;
106
- newPrimaryId: string;
140
+ /** The user being demoted to a secondary. */
141
+ userId: string;
142
+ /** The intended primary. Resolved to its own cluster root first. */
143
+ primaryId: string;
107
144
  }
108
145
  /**
109
- * Demote `formerPrimary` to a secondary of `newPrimaryId`. Any users
110
- * currently linked to `formerPrimary` are repointed first so the resulting
111
- * graph remains a single hop deep — `getPrimaryUserByProvider` and similar
112
- * resolvers only follow one `linked_to` step.
113
- *
114
- * Each write is a single-field `linked_to` update so the user-update
115
- * decorator's fast-path bypasses the pre/post hooks and we don't re-enter
116
- * the linking logic recursively.
146
+ * Demote `userId` to a secondary of `primaryId`'s cluster — the one and only
147
+ * way `linked_to` is ever set (issue #1250).
148
+ *
149
+ * Two things have to happen together for the graph to stay a single hop deep,
150
+ * and doing either without the other silently corrupts it:
151
+ *
152
+ * - **Resolve the target.** Linking onto a row that is itself a secondary
153
+ * would build a second hop, so the real root is resolved first.
154
+ * - **Repoint the children.** Any users already linked to `userId` are moved
155
+ * onto the root *before* `userId` is demoted. Skipping this strands them
156
+ * behind a now-secondary parent, where no resolver can reach them and they
157
+ * disappear from the API entirely.
158
+ *
159
+ * Callers get this for free: the decorator routes every single-field
160
+ * `linked_to` update through here, so a bare
161
+ * `users.update(id, { linked_to })` — from the management API, a consumer's
162
+ * hook, or internal linking code — is safe by construction rather than by
163
+ * remembering to call the right helper.
164
+ *
165
+ * Returns the cluster root `userId` was actually linked to, which may differ
166
+ * from the requested `primaryId`.
117
167
  */
118
- export declare function repointPrimary({ userAdapter, tenant_id, formerPrimary, newPrimaryId, }: RepointPrimaryParams): Promise<void>;
168
+ export declare function linkUserTo({ userAdapter, tenant_id, userId, primaryId, }: LinkUserToParams): Promise<string>;
119
169
  /**
120
170
  * True for identities whose *login identifier* is the email address: the native
121
171
  * username-password providers (`auth0`/`auth2`) and the passwordless `email`
@@ -128,6 +178,47 @@ export declare function repointPrimary({ userAdapter, tenant_id, formerPrimary,
128
178
  * is email-identified and neither should have its `email` rewritten by a cascade.
129
179
  */
130
180
  export declare function isEmailIdentifiedUser(user: Pick<User, "provider" | "connection">): boolean;
181
+ interface EmailConflictParams {
182
+ /** Every user sharing the new address, as returned by {@link getUsersByEmail}. */
183
+ candidates: User[];
184
+ /** The identity whose email is being changed. */
185
+ target: Pick<User, "user_id" | "provider" | "connection">;
186
+ /**
187
+ * The cluster root of the patched user. `target` may be the root itself or one
188
+ * of its linked secondaries (when the caller passed `connection`).
189
+ */
190
+ clusterRootId: string;
191
+ }
192
+ /**
193
+ * Find the user, if any, that genuinely blocks changing `target`'s email to an
194
+ * address already present on other rows.
195
+ *
196
+ * Two rows sharing an email is only a *conflict* when it makes a login ambiguous,
197
+ * and login lookups are always provider-scoped (`getUserByProvider` filters on
198
+ * `email:x provider:y`). So the check is scoped the way Auth0 scopes it — per
199
+ * connection — with one hard floor from the storage layer:
200
+ *
201
+ * - **Same provider** → always a conflict, cluster-mate or not. The
202
+ * `(tenant_id, provider, email)` unique index makes a second row with that
203
+ * address unrepresentable, so the write cannot succeed; a 409 is the honest
204
+ * answer where allowing it would surface as a constraint-violation 500.
205
+ * - **Same connection, different provider** → a conflict *between* clusters. Two
206
+ * `Username-Password-Authentication` rows (the `auth0`/`auth2` pair) sharing an
207
+ * address are competing login rows for one credential. Unknown/custom
208
+ * connections are included, so tenant-specific database connections keep the
209
+ * protection they have today.
210
+ * - **Different connection** → not a conflict. An `sms` identity is keyed by
211
+ * `phone_number` and a social identity by its provider sub, so their `email` is
212
+ * ordinary profile data that can't shadow an email login. This mirrors the
213
+ * `phone_number` carve-out on the same route (#1162) and matches Auth0, where
214
+ * one address may exist across connections.
215
+ * - **Cluster-mates** (the root and its secondaries) → not a conflict, provider
216
+ * permitting: they are the same person, and `cascadeEmailToLinkedIdentities`
217
+ * deliberately converges the cluster onto one address right after this check.
218
+ * Because AuthHero keeps secondaries as real rows (Auth0 folds them into the
219
+ * primary's `identities[]`), they'd otherwise surface here as "another user".
220
+ */
221
+ export declare function findEmailConflict({ candidates, target, clusterRootId, }: EmailConflictParams): User | undefined;
131
222
  interface CascadeEmailParams {
132
223
  userAdapter: UserDataAdapter;
133
224
  tenant_id: string;
@@ -29,8 +29,9 @@ export type PostLoginUserApi = {
29
29
  * logging-in user must have a verified email, regardless of what the action
30
30
  * asserts.
31
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.
32
+ * user pre-dates the target, the target is demoted instead. Either way the
33
+ * write goes through the update decorator's `linkUserTo` chokepoint, which
34
+ * keeps the cluster a single hop deep.
34
35
  * - **Idempotency.** A no-op when the user is already linked to the target.
35
36
  */
36
37
  export declare function createPostLoginUserApi(params: {