authhero 8.18.0 → 8.20.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.
@@ -414,7 +414,7 @@ export declare function passwordlessGrantUser(ctx: Context<{
414
414
  } | undefined;
415
415
  } | undefined;
416
416
  passkey_options?: {
417
- challenge_ui?: "button" | "both" | "autofill" | undefined;
417
+ challenge_ui?: "both" | "autofill" | "button" | undefined;
418
418
  local_enrollment_enabled?: boolean | undefined;
419
419
  progressive_enrollment_enabled?: boolean | undefined;
420
420
  } | undefined;
@@ -0,0 +1,6 @@
1
+ /** Target entity types whose id identifies an affected user. For these, the
2
+ * flat log's `user_id` is the target (the user the operation acted on) rather
3
+ * than the actor — matching Auth0, e.g. "Delete a User" logs the deleted user.
4
+ * Shared by `helpers/logging.ts` (write path) and
5
+ * `outbox-destinations/logs.ts` (outbox transform) so the two never drift. */
6
+ export declare const USER_TARGET_TYPES: Set<string>;
@@ -1,13 +1,15 @@
1
- import { DataAdapters } from "@authhero/adapter-interfaces";
1
+ import { CodeExecutor, DataAdapters } from "@authhero/adapter-interfaces";
2
2
  import { EventDestination } from "./outbox-relay";
3
3
  import { type GetServiceToken } from "./outbox-destinations/webhooks";
4
4
  import type { WebhookInvoker } from "../types/AuthHeroConfig";
5
5
  export interface CreateDefaultDestinationsConfig {
6
6
  /**
7
- * Data adapter — only the `logs`, `hooks`, `users`, and `logStreams`
8
- * adapters are used by the built-in destinations.
7
+ * Data adapter — the `logs`, `hooks`, `users`, and `logStreams` adapters are
8
+ * used by the built-in destinations. When `codeExecutor` is set, the
9
+ * `actions`, `hookCode`, and `actionExecutions` adapters are also required so
10
+ * `CodeHookDestination` can resolve and run tenant code hooks.
9
11
  */
10
- dataAdapter: Pick<DataAdapters, "logs" | "hooks" | "users" | "logStreams">;
12
+ dataAdapter: Pick<DataAdapters, "logs" | "hooks" | "users" | "logStreams"> & Partial<Pick<DataAdapters, "actions" | "hookCode" | "actionExecutions" | "multiTenancyConfig">>;
11
13
  /**
12
14
  * Produces a Bearer access token for the given tenant, used when POSTing
13
15
  * `hook.*` events to the configured webhook URLs.
@@ -37,6 +39,14 @@ export interface CreateDefaultDestinationsConfig {
37
39
  * deliveries don't diverge from inline per-request ones.
38
40
  */
39
41
  webhookInvoker?: WebhookInvoker;
42
+ /**
43
+ * Code executor — same instance passed to `init({ codeExecutor })`. When
44
+ * provided (and `dataAdapter` carries `actions`, `hookCode`, and
45
+ * `actionExecutions`), a `CodeHookDestination` is added so cron-drained
46
+ * `hook.*` events also run tenant code hooks. Without it, code hooks that
47
+ * failed per-request delivery would be silently skipped on retry.
48
+ */
49
+ codeExecutor?: CodeExecutor;
40
50
  }
41
51
  /**
42
52
  * Build the same array of outbox destinations that authhero's per-request
@@ -1,20 +1,40 @@
1
1
  import { Context } from "hono";
2
- import { User } from "@authhero/adapter-interfaces";
2
+ import { OutboxEventInsert, User } from "@authhero/adapter-interfaces";
3
3
  import { Bindings, Variables } from "../types";
4
+ type HookCtx = Context<{
5
+ Bindings: Bindings;
6
+ Variables: Variables;
7
+ }>;
4
8
  /**
5
- * Enqueue a `hook.{triggerId}` event to the outbox so the `WebhookDestination`
6
- * (and future `CodeHookDestination`) can dispatch the hook asynchronously with
7
- * retries instead of firing inline while the request is being served.
8
- *
9
- * Mirrors the synchronous-push pattern used by `logMessage`: the promise from
10
- * `outbox.create` is pushed onto `ctx.var.outboxEventPromises` so the outbox
11
- * middleware can await it in its finally block and then relay the resulting
12
- * event IDs.
9
+ * Whether the outbox is configured for this request. When false, hook dispatch
10
+ * falls back to inline webhook invocation (no retry / dead-letter).
11
+ */
12
+ export declare function outboxEnabled(ctx: HookCtx): boolean;
13
+ /**
14
+ * Build a `hook.{triggerId}` outbox event with a caller-assigned id, or
15
+ * `undefined` when the outbox is not configured (the caller then falls back to
16
+ * inline dispatch via {@link dispatchPostHookInline}).
13
17
  *
14
- * When the outbox is not configured, falls back to inline webhook invocation
15
- * (via `waitUntil`) so tenants without outbox still receive webhook calls.
18
+ * The returned event is meant to be handed to an event-emitting user write
19
+ * (`rawCreate` / `remove`) via its `outboxEvents` option so it commits in the
20
+ * same atomic unit as the business row (issue #1057). Because the id is fixed
21
+ * up front, the caller can relay it with {@link relayOutboxEvent} once the
22
+ * write commits without needing a return value from the adapter.
16
23
  */
17
- export declare function enqueuePostHookEvent(ctx: Context<{
18
- Bindings: Bindings;
19
- Variables: Variables;
20
- }>, tenantId: string, triggerId: string, user: User): void;
24
+ export declare function buildPostHookEvent(ctx: HookCtx, tenantId: string, triggerId: string, user: User): OutboxEventInsert | undefined;
25
+ /**
26
+ * Relay an outbox event that has already been persisted (atomically with its
27
+ * business write) so the outbox middleware picks it up for delivery. Mirrors
28
+ * the synchronous-push pattern used by `logMessage`: the id is pushed onto
29
+ * `ctx.var.outboxEventPromises`, which the outbox middleware drains and hands
30
+ * to `processOutboxEvents`.
31
+ */
32
+ export declare function relayOutboxEvent(ctx: HookCtx, id: string): void;
33
+ /**
34
+ * Fallback dispatch for when the outbox is not configured: invoke the
35
+ * post-hook webhooks inline (fire-and-forget via `waitUntil`). No retry /
36
+ * dead-letter support in this mode — configure the outbox for durable
37
+ * delivery.
38
+ */
39
+ export declare function dispatchPostHookInline(ctx: HookCtx, tenantId: string, triggerId: string, user: User): void;
40
+ export {};
@@ -0,0 +1,56 @@
1
+ import { AuditEvent, CodeExecutor } from "@authhero/adapter-interfaces";
2
+ import { EventDestination } from "../outbox-relay";
3
+ import { CodeHookData } from "../../hooks/codehooks";
4
+ interface CodeHookInvocation {
5
+ eventId: string;
6
+ tenantId: string;
7
+ triggerId: string;
8
+ /** Serialized user snapshot from the audit event (`target.after`). */
9
+ user?: unknown;
10
+ /** Serialized request context from the audit event. */
11
+ request?: unknown;
12
+ }
13
+ /**
14
+ * Delivers `hook.*` outbox events to tenant-authored **code hooks** (actions)
15
+ * for the matching `trigger_id`. Runs alongside `WebhookDestination` — both
16
+ * accept the same `hook.*` events — so a single registration/deletion event
17
+ * fans out to webhooks *and* code hooks. The fan-out is not retried
18
+ * independently: the relay retries the whole outbox event, running its
19
+ * destinations in order and stopping at the first failure, so a code-hook
20
+ * failure here causes earlier destinations (e.g. webhooks) to run again on the
21
+ * retry. Every destination on the event must therefore be idempotent.
22
+ *
23
+ * Reliability model: unlike the previous inline execution (best-effort,
24
+ * at-most-once, failures logged and dropped), code hooks delivered here are
25
+ * **at-least-once**. If any code hook for the trigger fails, `deliver` throws
26
+ * so the relay retries the whole event with backoff and, after `maxRetries`,
27
+ * dead-letters it. A retry re-runs every code hook for the trigger, so the
28
+ * event id is passed to user code as `event.idempotency_key` for dedupe.
29
+ *
30
+ * Must be listed AFTER `WebhookDestination` and BEFORE
31
+ * `RegistrationFinalizerDestination` so `registration_completed_at` is only
32
+ * flipped on a pass where every webhook *and* code hook succeeded.
33
+ *
34
+ * Constructed per-request (via `getDestinations(ctx)`) so it can close over
35
+ * `ctx.env.codeExecutor`. The same class is used by the cron `drainOutbox`
36
+ * safety net when a `codeExecutor` is supplied to `createDefaultDestinations`.
37
+ * When no executor is configured, code hooks cannot run (nor be deployed), so
38
+ * `deliver` is a no-op success — matching the inline `handleCodeHook` which
39
+ * returns `null` without an executor.
40
+ */
41
+ export declare class CodeHookDestination implements EventDestination {
42
+ name: string;
43
+ private data;
44
+ private codeExecutor?;
45
+ constructor(data: CodeHookData, codeExecutor?: CodeExecutor);
46
+ accepts(event: AuditEvent): boolean;
47
+ transform(event: AuditEvent): CodeHookInvocation;
48
+ deliver(invocations: CodeHookInvocation[]): Promise<void>;
49
+ /**
50
+ * Fetch every hook for the tenant, paging past the adapter's default page
51
+ * size. `hooks.list` returns only the first page by default, so a tenant with
52
+ * more enabled code hooks than one page would silently skip the rest.
53
+ */
54
+ private listAllHooks;
55
+ }
56
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { EnrichedClient } from "./client";
2
+ /**
3
+ * Resolve the tenant's database (username/password) connection for a client.
4
+ *
5
+ * The reset-password flows need the real connection name/id because a user's
6
+ * stored `user.connection` may be the hardcoded "Username-Password-Authentication"
7
+ * fallback rather than the tenant's actual connection. Centralised here so the
8
+ * HTML route and the screen handler resolve it identically.
9
+ *
10
+ * @param fallbackConnection connection name to use when the client has no
11
+ * database connection (typically `user.connection`).
12
+ */
13
+ export declare function resolvePasswordConnection(client: EnrichedClient, fallbackConnection: string): {
14
+ connection: string;
15
+ connectionId?: string;
16
+ };
@@ -1,4 +1,4 @@
1
- import { DataAdapters } from "@authhero/adapter-interfaces";
1
+ import { CodeExecutor, DataAdapters } from "@authhero/adapter-interfaces";
2
2
  import type { WebhookInvoker } from "../types/AuthHeroConfig";
3
3
  export interface RunOutboxRelayConfig {
4
4
  /** Same `DataAdapters` passed to `init()`. Must include `outbox` to drain. */
@@ -24,6 +24,13 @@ export interface RunOutboxRelayConfig {
24
24
  maxRetries?: number;
25
25
  /** Webhook HTTP timeout (ms), when the default invoker is used. */
26
26
  webhookTimeoutMs?: number;
27
+ /**
28
+ * Optional code executor — same instance passed to `init({ codeExecutor })`.
29
+ * When provided, cron-drained `hook.*` events also run tenant code hooks, so
30
+ * a code hook that failed per-request delivery is retried rather than
31
+ * silently skipped.
32
+ */
33
+ codeExecutor?: CodeExecutor;
27
34
  }
28
35
  /**
29
36
  * One-call outbox relay for cron / scheduled handlers.
@@ -1,22 +1,62 @@
1
1
  import { Context } from "hono";
2
- import { ActionExecutionResult, CodeExecutionLog, DataAdapters, Hook } from "@authhero/adapter-interfaces";
2
+ import { ActionExecutionResult, CodeExecutionLog, CodeExecutor, DataAdapters, Hook } from "@authhero/adapter-interfaces";
3
3
  import { Bindings, Variables } from "../types";
4
4
  import { HookEvent, OnExecuteCredentialsExchangeAPI } from "../types/Hooks";
5
+ import { EnrichedClient } from "../helpers/client";
5
6
  /**
6
7
  * Auth0 uses `post-login` for what we internally call `post-user-login`.
7
8
  * Normalize when writing execution records so the public API matches Auth0.
8
9
  */
9
10
  export declare function toAuth0TriggerId(internal: string): string;
11
+ /**
12
+ * The subset of `DataAdapters` needed to resolve and run a code hook and to
13
+ * persist its execution record. Narrowed so `CodeHookDestination` and the cron
14
+ * `createDefaultDestinations` helper can construct it without depending on the
15
+ * full adapter surface.
16
+ */
17
+ export type CodeHookData = Pick<DataAdapters, "hooks" | "actions" | "hookCode" | "actionExecutions" | "multiTenancyConfig">;
10
18
  type CodeHook = Extract<Hook, {
11
19
  code_id: string;
12
20
  }>;
13
21
  export declare function isCodeHook(hook: Hook): hook is CodeHook;
22
+ /**
23
+ * Loosened event shape accepted by `buildSerializableEvent` / `executeCodeHook`.
24
+ *
25
+ * The inline hook call sites pass a full `HookEvent`. The `CodeHookDestination`
26
+ * (which runs from the outbox relay, after the request has closed) has no live
27
+ * `ctx` and reconstructs the event from the persisted audit event, so `user`
28
+ * and `request` arrive as already-serialized `unknown` values. Both are
29
+ * assignable to this type.
30
+ */
31
+ export type CodeHookEventInput = {
32
+ ctx?: unknown;
33
+ client?: Pick<EnrichedClient, "client_id" | "name" | "client_metadata"> | null;
34
+ } & Record<string, unknown>;
14
35
  /**
15
36
  * Build a serializable event object from a HookEvent.
16
37
  * Strips the `ctx` property (Hono context) which cannot be serialized,
17
38
  * and returns a plain JSON-compatible object.
18
39
  */
19
- export declare function buildSerializableEvent(event: HookEvent, secrets?: Record<string, string>): Record<string, unknown>;
40
+ export declare function buildSerializableEvent(event: CodeHookEventInput, secrets?: Record<string, string>): Record<string, unknown>;
41
+ /**
42
+ * The namespaced API surface exposed to code hooks. User code records calls of
43
+ * the form `<namespace>.<method>(...)` (e.g. `accessToken.setCustomClaim`,
44
+ * `access.deny`), which {@link replayApiCalls} replays against the real
45
+ * namespace objects. Each top-level key is a namespace whose members are the
46
+ * callable methods for that namespace — so the shape is nested (namespace →
47
+ * method → function) rather than a flat `any` surface. Concrete per-trigger API
48
+ * types (`OnExecute*API`) structurally satisfy this.
49
+ *
50
+ * The method args stay permissive (`any[]`): this surface is a dynamic
51
+ * dispatcher — {@link replayApiCalls} looks methods up by string and applies
52
+ * the recorded `unknown[]` args, while call sites register concretely-typed
53
+ * methods (`(key: string, value: unknown) => void`, …). A stricter `unknown[]`
54
+ * args tuple would reject those concrete registrations (parameter
55
+ * contravariance). The return type is still narrowed to `unknown`.
56
+ */
57
+ export type CodeHookApiMethod = (...args: any[]) => unknown;
58
+ export type CodeHookApiNamespace = Record<string, CodeHookApiMethod>;
59
+ export type CodeHookApi = Record<string, CodeHookApiNamespace>;
20
60
  /**
21
61
  * Replay recorded API calls from code hook execution against real API objects.
22
62
  * Handles calls like "accessToken.setCustomClaim" by navigating the api object.
@@ -24,22 +64,51 @@ export declare function buildSerializableEvent(event: HookEvent, secrets?: Recor
24
64
  export declare function replayApiCalls(apiCalls: Array<{
25
65
  method: string;
26
66
  args: unknown[];
27
- }>, api: Record<string, any>): void;
67
+ }>, api: CodeHookApi): void;
28
68
  export type HandleCodeHookOutcome = {
29
69
  result: ActionExecutionResult;
30
70
  logs: CodeExecutionLog[];
31
71
  /** True if api.access.deny was recorded by the executor. */
32
72
  denied: boolean;
33
73
  };
74
+ /**
75
+ * Core code-hook execution, decoupled from the Hono request `ctx`.
76
+ *
77
+ * Given an explicit executor, data adapter, and tenant, fetches the code,
78
+ * runs it, and replays the recorded API calls against `api`. Shared by the
79
+ * inline `handleCodeHook` wrapper (request-time) and by `CodeHookDestination`
80
+ * (outbox relay, after the request has closed).
81
+ *
82
+ * Always returns an outcome — including a `code_not_found` / `execution_failed`
83
+ * outcome — so the caller can persist an `action_executions` record. It only
84
+ * throws for `api.access.deny`, which surfaces as a thrown `HTTPException` from
85
+ * the replayed API call; callers that must abort the flow catch it.
86
+ *
87
+ * `idempotencyKey`, when set, is exposed to user code as `event.idempotency_key`.
88
+ * The outbox is at-least-once, so a retried hook event re-runs the code; authors
89
+ * performing non-idempotent side effects can dedupe on this key.
90
+ */
91
+ export declare function executeCodeHook(params: {
92
+ codeExecutor: CodeExecutor;
93
+ data: CodeHookData;
94
+ tenantId: string;
95
+ hook: {
96
+ code_id: string;
97
+ hook_id?: string;
98
+ };
99
+ event: CodeHookEventInput;
100
+ triggerId: string;
101
+ api: CodeHookApi;
102
+ idempotencyKey?: string;
103
+ }): Promise<HandleCodeHookOutcome>;
34
104
  /**
35
105
  * Execute a code hook by fetching the code from the database, running it
36
106
  * through the code executor, and replaying API calls against the real api
37
107
  * object.
38
108
  *
39
- * Returns the per-action result (Auth0 shape) so the caller can aggregate
40
- * results across all actions on a trigger into a single `action_executions`
41
- * record. Returns `null` when the code cannot be located or the executor is
42
- * unavailable — the caller decides whether to surface that.
109
+ * Thin request-time wrapper over `executeCodeHook` that resolves the executor
110
+ * and tenant from `ctx`. Returns `null` when the code executor is unavailable
111
+ * or the tenant can't be resolved the caller decides whether to surface that.
43
112
  */
44
113
  export declare function handleCodeHook(ctx: Context<{
45
114
  Bindings: Bindings;
@@ -47,13 +116,13 @@ export declare function handleCodeHook(ctx: Context<{
47
116
  }>, data: DataAdapters, hook: {
48
117
  code_id: string;
49
118
  hook_id: string;
50
- }, event: HookEvent, triggerId: string, api: Record<string, any>): Promise<HandleCodeHookOutcome | null>;
119
+ }, event: HookEvent, triggerId: string, api: CodeHookApi): Promise<HandleCodeHookOutcome | null>;
51
120
  /**
52
121
  * Aggregate per-action outcomes into an Auth0-shape execution record and
53
122
  * persist it via the adapter. Returns the generated execution_id (uuid)
54
123
  * so the caller can embed it in the surrounding tenant log.
55
124
  */
56
- export declare function persistActionExecution(data: DataAdapters, tenant_id: string, triggerId: string, outcomes: HandleCodeHookOutcome[]): Promise<string | null>;
125
+ export declare function persistActionExecution(data: Pick<DataAdapters, "actionExecutions">, tenant_id: string, triggerId: string, outcomes: HandleCodeHookOutcome[]): Promise<string | null>;
57
126
  /**
58
127
  * Execute code hooks for the credentials-exchange trigger.
59
128
  * Filters enabled code hooks from the provided hooks list and executes them.
@@ -1,4 +1,4 @@
1
- import { DataAdapters, User } from "@authhero/adapter-interfaces";
1
+ import { DataAdapters, OutboxEventInsert, User } from "@authhero/adapter-interfaces";
2
2
  export interface CommitUserResult {
3
3
  user: User;
4
4
  created: boolean;
@@ -16,6 +16,14 @@ export interface CommitUserOptions {
16
16
  * goes away entirely).
17
17
  */
18
18
  resolveEmailLinkedPrimary?: boolean;
19
+ /**
20
+ * Post-registration outbox events to persist in the same atomic unit as the
21
+ * user row (issue #1057). Forwarded to `rawCreate`'s `outboxEvents` option so
22
+ * the event and the user commit together — a race-loser whose `rawCreate`
23
+ * rolls back never leaves a stranded event. The caller relays the event ids
24
+ * (via `relayOutboxEvent`) only after `created === true`.
25
+ */
26
+ outboxEvents?: OutboxEventInsert[];
19
27
  }
20
28
  /**
21
29
  * Commits a new user inside a transaction. Validates `linked_to` (if set),
@@ -8,11 +8,14 @@ import { Bindings, Variables } from "../types";
8
8
  * 1. Pre-registration blocking hooks (outside any transaction so webhook
9
9
  * latency and user-authored code don't hold a DB connection).
10
10
  * 2. `commitUserHook` — the internal transactional step that atomically
11
- * writes the user row (via `rawCreate`) and, when the built-in
12
- * email-based linking path is enabled, also resolves `linked_to` from
13
- * the existing primary inside the same transaction.
14
- * 3. Post-registration hooks webhook delivery is handed to the outbox
15
- * (`enqueuePostHookEvent`) for retryable / idempotent dispatch; code
11
+ * writes the user row (via `rawCreate`), the post-registration outbox
12
+ * event (via `rawCreate`'s `outboxEvents` option, so business row and
13
+ * event commit together issue #1057), and, when the built-in
14
+ * email-based linking path is enabled, resolves `linked_to` from the
15
+ * existing primary inside the same transaction.
16
+ * 3. Post-registration hooks — the already-persisted event id is relayed
17
+ * (`relayOutboxEvent`) for retryable / idempotent webhook dispatch, or
18
+ * `dispatchPostHookInline` runs when the outbox isn't configured; code
16
19
  * hooks currently still run inline with ctx.
17
20
  */
18
21
  export declare function createUserHooks(ctx: Context<{