@authhero/adapter-interfaces 4.12.0 → 4.14.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.
@@ -0,0 +1,57 @@
1
+ import { TenantOperationRow, TenantOperationRowCounts, TenantOperationRowInsert, TenantOperationRowOutcome, TenantOperationRowStatus } from "../types/TenantOperationRow";
2
+ import { ListParams } from "../types/ListParams";
3
+ export interface ListTenantOperationRowsParams extends ListParams {
4
+ /** Single status or a set (the errors endpoint queries `failed`). */
5
+ status?: TenantOperationRowStatus | TenantOperationRowStatus[];
6
+ }
7
+ export interface ListTenantOperationRowsResult {
8
+ rows: TenantOperationRow[];
9
+ start: number;
10
+ limit: number;
11
+ length: number;
12
+ }
13
+ /**
14
+ * Per-item checkpoints for batch tenant operations (issue #1325).
15
+ *
16
+ * Like `tenantOperations` this is a control-plane adapter, so it is
17
+ * unscoped — rows are addressed by `operation_id`, and the operation row
18
+ * carries the `tenant_id`.
19
+ *
20
+ * The contract that makes batch operations resumable: `createMany` stages
21
+ * every item as `pending`, `claimPending` hands a worker the next slice of
22
+ * unprocessed items, and `recordOutcomes` commits their terminal statuses.
23
+ * An interrupted run leaves its items `pending` and is retried verbatim,
24
+ * so implementations MUST make `recordOutcomes` atomic per call and MUST
25
+ * NOT mutate rows that already hold a terminal status.
26
+ */
27
+ export interface TenantOperationRowsAdapter {
28
+ /**
29
+ * Stage items in bulk. Implementations should insert in batches rather
30
+ * than one statement per row — callers stage thousands at a time.
31
+ * Returns the number of rows written.
32
+ */
33
+ createMany(rows: TenantOperationRowInsert[]): Promise<number>;
34
+ /**
35
+ * Fetch the next `limit` items still awaiting processing, ordered by
36
+ * `seq` ascending. Does not itself lock — exclusivity between drivers
37
+ * comes from the lease on the parent operation
38
+ * (`TenantOperationsAdapter.claim`).
39
+ */
40
+ claimPending(operation_id: string, limit: number): Promise<TenantOperationRow[]>;
41
+ /**
42
+ * Commit terminal outcomes for processed items. Idempotent: applying an
43
+ * outcome to a row that is already terminal is a no-op, so a retried
44
+ * chunk cannot corrupt earlier results. Returns rows actually updated.
45
+ */
46
+ recordOutcomes(operation_id: string, outcomes: TenantOperationRowOutcome[]): Promise<number>;
47
+ /** Row counts per status, used to build the operation's summary. */
48
+ countByStatus(operation_id: string): Promise<TenantOperationRowCounts>;
49
+ /** Paginated read, ordered by `seq` ascending. Backs the errors endpoint. */
50
+ list(operation_id: string, params?: ListTenantOperationRowsParams): Promise<ListTenantOperationRowsResult>;
51
+ /**
52
+ * Retention cleanup. Also runs via the cascade when the parent operation
53
+ * is removed; exposed separately so a sweep can drop staged payloads
54
+ * without deleting the operation's audit record.
55
+ */
56
+ removeByOperation(operation_id: string): Promise<number>;
57
+ }
@@ -29,6 +29,36 @@ export interface TenantOperationsAdapter {
29
29
  list(params?: ListTenantOperationsParams): Promise<ListTenantOperationsResult>;
30
30
  /** Always bumps `updated_at`. */
31
31
  update(id: string, operation: TenantOperationUpdate): Promise<boolean>;
32
+ /**
33
+ * Atomically take the lease on an operation so only one driver advances
34
+ * it at a time. Succeeds when the operation is unclaimed or its existing
35
+ * lease has expired (or is already held by `worker_id`, making re-claim
36
+ * by the same worker a no-op); returns false when another live worker
37
+ * holds it.
38
+ *
39
+ * The lease is an efficiency guard, not a correctness guard — per-item
40
+ * checkpoints in `tenant_operation_rows` are what keep a batch operation
41
+ * correct if two drivers ever overlap. Implementations MUST perform the
42
+ * check and the write in a single conditional statement.
43
+ */
44
+ claim(id: string, worker_id: string, leaseMs: number): Promise<boolean>;
45
+ /**
46
+ * Release a lease held by `worker_id`. Returns false when the lease has
47
+ * since been taken by someone else, in which case the caller must not
48
+ * assume its own writes were the last ones.
49
+ */
50
+ release(id: string, worker_id: string): Promise<boolean>;
51
+ /**
52
+ * Operations that still have work to do and are free to be picked up:
53
+ * status `pending` or `running`, matching `kind`, whose lease is absent
54
+ * or expired. Ordered `created_at` ascending so the oldest unfinished
55
+ * work drains first. Backs the resume sweep that guarantees a batch
56
+ * completes even when every driver that touched it died.
57
+ */
58
+ listResumable(params: {
59
+ kind: TenantOperationKind;
60
+ limit: number;
61
+ }): Promise<TenantOperation[]>;
32
62
  /** Retention cleanup only — not exposed via routes; events cascade. */
33
63
  remove(id: string): Promise<boolean>;
34
64
  }
@@ -36,6 +36,25 @@ export interface UserDataAdapter {
36
36
  * the same atomic unit as the user row.
37
37
  */
38
38
  rawCreate(tenantId: string, user: UserInsert, options?: WriteOptions): Promise<User>;
39
+ /**
40
+ * Insert many users in as few round-trips as the backend allows.
41
+ *
42
+ * Optional. Callers MUST fall back to looping `create` when it is absent —
43
+ * this exists to make bulk paths (the users-import job) fast, not to add a
44
+ * second way of writing a user that every backend has to implement.
45
+ *
46
+ * Unlike `create` this is deliberately narrow: it writes the users row and,
47
+ * where `user.password` is set, the companion password row. It does NOT
48
+ * handle identities, activity counters or outbox events, because no bulk
49
+ * caller needs them and supporting them would make the batch no cheaper than
50
+ * the loop it replaces. Pass a user carrying those and the implementation is
51
+ * free to reject the batch.
52
+ *
53
+ * Returns the created users in input order. On a uniqueness violation the
54
+ * whole batch is rejected — the caller is expected to have de-duplicated and
55
+ * probed first, and to retry row-by-row to attribute the failure.
56
+ */
57
+ createMany?(tenantId: string, users: UserInsert[], options?: WriteOptions): Promise<User[]>;
39
58
  remove(tenantId: string, id: string, options?: WriteOptions): Promise<boolean>;
40
59
  list(tenantId: string, params?: ListParams): Promise<ListUsersResponse>;
41
60
  update(tenantId: string, id: string, user: Partial<User>, options?: WriteOptions): Promise<boolean>;
@@ -55,6 +55,7 @@ import { OutboxAdapter } from "./Outbox";
55
55
  import { RateLimitAdapter } from "./RateLimit";
56
56
  import { TenantOperationsAdapter } from "./TenantOperations";
57
57
  import { TenantOperationEventsAdapter } from "./TenantOperationEvents";
58
+ import { TenantOperationRowsAdapter } from "./TenantOperationRows";
58
59
  import { RolloutsAdapter } from "./Rollouts";
59
60
  /**
60
61
  * Parameters for cleaning up expired sessions
@@ -162,6 +163,12 @@ export interface DataAdapters {
162
163
  tenantOperations?: TenantOperationsAdapter;
163
164
  /** Per-step history rows for `tenantOperations`; append-only. */
164
165
  tenantOperationEvents?: TenantOperationEventsAdapter;
166
+ /**
167
+ * Per-item checkpoints for batch operations (issue #1325). Required for
168
+ * the Auth0-compatible `POST /api/v2/jobs/users-imports` endpoints —
169
+ * without it, batch imports cannot be staged or resumed.
170
+ */
171
+ tenantOperationRows?: TenantOperationRowsAdapter;
165
172
  /**
166
173
  * Optional fleet rollout records (issue #1026). Phase 1 ships the table
167
174
  * and CRUD only; the wave/canary coordinator and routes come later.
@@ -287,4 +294,5 @@ export * from "./UserActivity";
287
294
  export * from "./Users";
288
295
  export * from "./TenantOperations";
289
296
  export * from "./TenantOperationEvents";
297
+ export * from "./TenantOperationRows";
290
298
  export * from "./Rollouts";
@@ -13,6 +13,7 @@ export declare const tenantOperationKindSchema: z.ZodEnum<{
13
13
  upgrade: "upgrade";
14
14
  backup: "backup";
15
15
  deprovision: "deprovision";
16
+ users_import: "users_import";
16
17
  }>;
17
18
  export type TenantOperationKind = z.infer<typeof tenantOperationKindSchema>;
18
19
  export declare const tenantOperationStatusSchema: z.ZodEnum<{
@@ -23,11 +24,22 @@ export declare const tenantOperationStatusSchema: z.ZodEnum<{
23
24
  cancelled: "cancelled";
24
25
  }>;
25
26
  export type TenantOperationStatus = z.infer<typeof tenantOperationStatusSchema>;
26
- export declare const tenantOperationEngineSchema: z.ZodEnum<{
27
- inline: "inline";
28
- "cloudflare-workflows": "cloudflare-workflows";
29
- }>;
27
+ /**
28
+ * Which driver advances an operation. Deliberately an open string, not an
29
+ * enum: the engine name is recorded in a core database column, so closing
30
+ * it here would bake vendor names into the schema and lock out
31
+ * self-hosters running their own driver (pg-boss, BullMQ, SQS, a plain
32
+ * cron). The values AuthHero ships are {@link KNOWN_TENANT_OPERATION_ENGINES}.
33
+ *
34
+ * Durability never depends on this value. Operation progress is checkpointed
35
+ * in the database (for `users_import`, one `tenant_operation_rows` row per
36
+ * user), so a crashed driver loses nothing and any other driver can resume
37
+ * the work.
38
+ */
39
+ export declare const tenantOperationEngineSchema: z.ZodString;
30
40
  export type TenantOperationEngine = z.infer<typeof tenantOperationEngineSchema>;
41
+ /** Engines AuthHero ships. Not exhaustive — the column accepts any name. */
42
+ export declare const KNOWN_TENANT_OPERATION_ENGINES: readonly ["inline", "cloudflare-workflows"];
31
43
  export declare const tenantOperationInsertSchema: z.ZodObject<{
32
44
  tenant_id: z.ZodDefault<z.ZodNullable<z.ZodString>>;
33
45
  rollout_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -37,15 +49,14 @@ export declare const tenantOperationInsertSchema: z.ZodObject<{
37
49
  upgrade: "upgrade";
38
50
  backup: "backup";
39
51
  deprovision: "deprovision";
52
+ users_import: "users_import";
40
53
  }>;
41
- engine: z.ZodEnum<{
42
- inline: "inline";
43
- "cloudflare-workflows": "cloudflare-workflows";
44
- }>;
54
+ engine: z.ZodString;
45
55
  engine_instance_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
46
56
  target_worker_version: z.ZodOptional<z.ZodNullable<z.ZodString>>;
47
57
  target_database_version: z.ZodOptional<z.ZodNullable<z.ZodString>>;
48
58
  initiated_by: z.ZodOptional<z.ZodNullable<z.ZodString>>;
59
+ input: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
49
60
  }, z.core.$strip>;
50
61
  export type TenantOperationInsert = z.input<typeof tenantOperationInsertSchema>;
51
62
  export declare const tenantOperationSchema: z.ZodObject<{
@@ -57,15 +68,14 @@ export declare const tenantOperationSchema: z.ZodObject<{
57
68
  upgrade: "upgrade";
58
69
  backup: "backup";
59
70
  deprovision: "deprovision";
71
+ users_import: "users_import";
60
72
  }>;
61
- engine: z.ZodEnum<{
62
- inline: "inline";
63
- "cloudflare-workflows": "cloudflare-workflows";
64
- }>;
73
+ engine: z.ZodString;
65
74
  engine_instance_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
66
75
  target_worker_version: z.ZodOptional<z.ZodNullable<z.ZodString>>;
67
76
  target_database_version: z.ZodOptional<z.ZodNullable<z.ZodString>>;
68
77
  initiated_by: z.ZodOptional<z.ZodNullable<z.ZodString>>;
78
+ input: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
69
79
  id: z.ZodString;
70
80
  status: z.ZodEnum<{
71
81
  pending: "pending";
@@ -76,6 +86,9 @@ export declare const tenantOperationSchema: z.ZodObject<{
76
86
  }>;
77
87
  current_step: z.ZodOptional<z.ZodNullable<z.ZodString>>;
78
88
  error: z.ZodOptional<z.ZodNullable<z.ZodString>>;
89
+ result: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
90
+ claimed_by: z.ZodOptional<z.ZodNullable<z.ZodString>>;
91
+ claim_expires_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
79
92
  created_at: z.ZodString;
80
93
  updated_at: z.ZodString;
81
94
  finished_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -94,6 +107,9 @@ export declare const tenantOperationUpdateSchema: z.ZodObject<{
94
107
  target_worker_version: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
95
108
  target_database_version: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
96
109
  current_step: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
110
+ result: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>>;
111
+ claimed_by: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
112
+ claim_expires_at: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
97
113
  finished_at: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
98
114
  }, z.core.$strip>;
99
115
  export type TenantOperationUpdate = z.infer<typeof tenantOperationUpdateSchema>;
@@ -0,0 +1,86 @@
1
+ import { z } from "@hono/zod-openapi";
2
+ /**
3
+ * Per-item checkpoint for a batch tenant operation (issue #1325).
4
+ *
5
+ * One row per item in the operation's input — for `users_import`, one per
6
+ * user in the uploaded file. This table is what makes a batch operation
7
+ * durable and resumable: an item is staged as `pending`, and its terminal
8
+ * status is committed in the same chunk that performs the write. A driver
9
+ * that dies mid-run leaves its unprocessed items `pending`, so any other
10
+ * driver (a cron sweep, a workflow step, a later request) resumes exactly
11
+ * where it stopped, without re-doing committed work.
12
+ *
13
+ * Durability therefore does not depend on the execution engine — engines
14
+ * only decide *who* advances the operation and *how fast*.
15
+ */
16
+ export declare const tenantOperationRowStatusSchema: z.ZodEnum<{
17
+ pending: "pending";
18
+ failed: "failed";
19
+ skipped: "skipped";
20
+ inserted: "inserted";
21
+ updated: "updated";
22
+ }>;
23
+ export type TenantOperationRowStatus = z.infer<typeof tenantOperationRowStatusSchema>;
24
+ /** Terminal statuses — a row in any of these is never reprocessed. */
25
+ export declare const TERMINAL_TENANT_OPERATION_ROW_STATUSES: readonly TenantOperationRowStatus[];
26
+ export declare const tenantOperationRowInsertSchema: z.ZodObject<{
27
+ operation_id: z.ZodString;
28
+ seq: z.ZodNumber;
29
+ payload: z.ZodRecord<z.ZodString, z.ZodUnknown>;
30
+ status: z.ZodDefault<z.ZodEnum<{
31
+ pending: "pending";
32
+ failed: "failed";
33
+ skipped: "skipped";
34
+ inserted: "inserted";
35
+ updated: "updated";
36
+ }>>;
37
+ error_code: z.ZodOptional<z.ZodNullable<z.ZodString>>;
38
+ error_message: z.ZodOptional<z.ZodNullable<z.ZodString>>;
39
+ error_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
40
+ entity_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
41
+ }, z.core.$strip>;
42
+ export type TenantOperationRowInsert = z.input<typeof tenantOperationRowInsertSchema>;
43
+ export declare const tenantOperationRowSchema: z.ZodObject<{
44
+ operation_id: z.ZodString;
45
+ seq: z.ZodNumber;
46
+ payload: z.ZodRecord<z.ZodString, z.ZodUnknown>;
47
+ error_code: z.ZodOptional<z.ZodNullable<z.ZodString>>;
48
+ error_message: z.ZodOptional<z.ZodNullable<z.ZodString>>;
49
+ error_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
50
+ entity_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
51
+ status: z.ZodEnum<{
52
+ pending: "pending";
53
+ failed: "failed";
54
+ skipped: "skipped";
55
+ inserted: "inserted";
56
+ updated: "updated";
57
+ }>;
58
+ created_at: z.ZodString;
59
+ updated_at: z.ZodString;
60
+ }, z.core.$strip>;
61
+ export type TenantOperationRow = z.infer<typeof tenantOperationRowSchema>;
62
+ /** The per-item outcome a processor reports back for one staged row. */
63
+ export declare const tenantOperationRowOutcomeSchema: z.ZodObject<{
64
+ seq: z.ZodNumber;
65
+ status: z.ZodEnum<{
66
+ pending: "pending";
67
+ failed: "failed";
68
+ skipped: "skipped";
69
+ inserted: "inserted";
70
+ updated: "updated";
71
+ }>;
72
+ error_code: z.ZodOptional<z.ZodNullable<z.ZodString>>;
73
+ error_message: z.ZodOptional<z.ZodNullable<z.ZodString>>;
74
+ error_path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
75
+ entity_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
76
+ }, z.core.$strip>;
77
+ export type TenantOperationRowOutcome = z.infer<typeof tenantOperationRowOutcomeSchema>;
78
+ /** Count of rows in each status, used to build a job summary. */
79
+ export interface TenantOperationRowCounts {
80
+ total: number;
81
+ pending: number;
82
+ inserted: number;
83
+ updated: number;
84
+ failed: number;
85
+ skipped: number;
86
+ }
@@ -31,6 +31,7 @@ export * from "./SigningKey";
31
31
  export * from "./Tenant";
32
32
  export * from "./TenantOperation";
33
33
  export * from "./TenantOperationEvent";
34
+ export * from "./TenantOperationRow";
34
35
  export * from "./Rollout";
35
36
  export * from "./Token";
36
37
  export * from "./User";
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "type": "git",
12
12
  "url": "https://github.com/markusahlstrand/authhero"
13
13
  },
14
- "version": "4.12.0",
14
+ "version": "4.14.0",
15
15
  "files": [
16
16
  "dist"
17
17
  ],