@authhero/cloudflare-adapter 2.37.5 → 2.38.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,392 @@
1
+ import { TenantsDataAdapter, TenantOperation } from '@authhero/adapter-interfaces';
2
+ import { TenantOperationStores, StepConfig, StepRunner, TenantOperationExecutor } from '@authhero/multi-tenancy';
3
+
4
+ interface CfApiClientOptions {
5
+ accountId: string;
6
+ apiToken: string;
7
+ fetch?: typeof fetch;
8
+ timeoutMs?: number;
9
+ baseUrl?: string;
10
+ }
11
+ interface D1Database {
12
+ uuid: string;
13
+ name: string;
14
+ }
15
+ interface D1QueryResult {
16
+ success: boolean;
17
+ meta?: Record<string, unknown>;
18
+ results?: unknown[];
19
+ }
20
+ type ScriptBinding = {
21
+ type: "d1";
22
+ name: string;
23
+ id: string;
24
+ } | {
25
+ type: "plain_text";
26
+ name: string;
27
+ text: string;
28
+ } | {
29
+ type: "secret_text";
30
+ name: string;
31
+ text: string;
32
+ } | {
33
+ type: "service";
34
+ name: string;
35
+ service: string;
36
+ environment?: string;
37
+ };
38
+ interface ScriptUploadOptions {
39
+ /** Script source (JavaScript ES module). */
40
+ script: string;
41
+ /** Main module filename (must match part name in form data). */
42
+ mainModule: string;
43
+ /** Compatibility date, ISO yyyy-mm-dd. */
44
+ compatibilityDate: string;
45
+ /** Compatibility flags (e.g. `["nodejs_compat"]`). */
46
+ compatibilityFlags?: string[];
47
+ /** Bindings to attach (D1, plain_text, etc.). Secrets go via setSecret(). */
48
+ bindings?: ScriptBinding[];
49
+ /** Optional tags, stored on the script for operator-side lookup. */
50
+ tags?: string[];
51
+ }
52
+ declare class CloudflareApiClient {
53
+ private readonly accountId;
54
+ private readonly apiToken;
55
+ private readonly fetchImpl;
56
+ private readonly timeoutMs;
57
+ private readonly baseUrl;
58
+ constructor(options: CfApiClientOptions);
59
+ createD1Database(name: string): Promise<D1Database>;
60
+ listD1Databases(name?: string): Promise<D1Database[]>;
61
+ deleteD1Database(databaseId: string): Promise<void>;
62
+ /**
63
+ * Execute a single SQL statement (or batch of `;`-separated statements
64
+ * permitted by D1) against the given database. Use for applying
65
+ * migrations one file at a time — the per-call response size cap means
66
+ * very large single calls can fail; splitting per file keeps each call
67
+ * bounded by the migration author.
68
+ */
69
+ execD1(databaseId: string, sql: string): Promise<D1QueryResult[]>;
70
+ uploadNamespacedScript(namespace: string, scriptName: string, options: ScriptUploadOptions): Promise<void>;
71
+ deleteNamespacedScript(namespace: string, scriptName: string): Promise<void>;
72
+ /**
73
+ * Set a single secret on a namespaced script. The CF API replaces the
74
+ * value if a secret with that name already exists, so this is safely
75
+ * re-runnable.
76
+ */
77
+ setNamespacedScriptSecret(namespace: string, scriptName: string, secretName: string, secretValue: string): Promise<void>;
78
+ private request;
79
+ }
80
+
81
+ interface TenantProvisionNames {
82
+ scriptName: string;
83
+ databaseName: string;
84
+ }
85
+ /**
86
+ * Provider-agnostic contract for the individual, idempotent units of
87
+ * tenant provisioning. Deliberately free of Cloudflare/D1 terminology so
88
+ * the durable operation orchestration (issue #1026 phase 2) — and any
89
+ * future provider (e.g. Bunny with its SQLite databases) — can run against
90
+ * it. `createWfpProvisionerSteps` is the Cloudflare Workers-for-Platforms
91
+ * + D1 implementation.
92
+ */
93
+ interface TenantProvisionerSteps {
94
+ names(tenantId: string): TenantProvisionNames;
95
+ /**
96
+ * Validate the version token persisted into `tenants.database_version`
97
+ * BEFORE any provider side effects. Returns the recorded versions.
98
+ */
99
+ validate(): {
100
+ databaseVersion?: string;
101
+ bundleConfiguration?: string;
102
+ workerVersion?: string;
103
+ };
104
+ findOrCreateDatabase(name: string): Promise<{
105
+ id: string;
106
+ created: boolean;
107
+ }>;
108
+ /**
109
+ * Reconcile migrations against the provisioner-owned tracking table
110
+ * (`_authhero_provisioner_migrations`), including the legacy backfill
111
+ * branch for pre-tracking databases.
112
+ */
113
+ applyMigrations(databaseId: string, created: boolean): Promise<void>;
114
+ uploadScript(scriptName: string, databaseId: string): Promise<void>;
115
+ uploadSecrets(scriptName: string, tenantId: string): Promise<void>;
116
+ /** Best-effort teardown of both resources; throws a combined error. */
117
+ deprovision(tenantId: string): Promise<void>;
118
+ }
119
+
120
+ /**
121
+ * Structural types for the Cloudflare Workflows engine. Deliberately NOT
122
+ * imported from `cloudflare:workers` / `@cloudflare/workers-types` — this
123
+ * package ships platform-agnostic bundles and the real runtime objects
124
+ * satisfy these shapes structurally (precedent: `@authhero/proxy`'s local
125
+ * binding types). Only the downstream worker's `WorkflowEntrypoint` shell
126
+ * touches `cloudflare:workers` (see `entrypoint.example.ts`).
127
+ */
128
+ /**
129
+ * The ONLY data that crosses the enqueue boundary. Workflows persists
130
+ * params (and shows them in the dashboard), so never put secrets here —
131
+ * the workflow re-resolves everything else from its worker env.
132
+ */
133
+ interface TenantOperationWorkflowParams {
134
+ operation_id: string;
135
+ tenant_id: string;
136
+ /** Widened beyond "provision" in later phases (upgrade, backup, …). */
137
+ kind: "provision";
138
+ }
139
+ /**
140
+ * Instance status as reported by the engine. The exact value set has
141
+ * drifted across Cloudflare releases; the reconciler only branches on the
142
+ * terminal values and treats anything unrecognized as still-running.
143
+ */
144
+ interface WorkflowInstanceStatus {
145
+ status: "queued" | "running" | "paused" | "errored" | "terminated" | "complete" | "waiting" | "waitingForPause" | "unknown" | (string & {});
146
+ error?: {
147
+ name?: string;
148
+ message?: string;
149
+ } | string | null;
150
+ output?: unknown;
151
+ }
152
+ /** Structurally satisfied by the handles a `workflows` binding returns. */
153
+ interface WorkflowInstanceHandle {
154
+ id: string;
155
+ status(): Promise<WorkflowInstanceStatus>;
156
+ }
157
+ /**
158
+ * Structurally satisfied by a Workflows binding
159
+ * (e.g. `env.TENANT_OPERATIONS_WORKFLOW`).
160
+ */
161
+ interface WorkflowsBinding {
162
+ create(options: {
163
+ id: string;
164
+ params: TenantOperationWorkflowParams;
165
+ }): Promise<WorkflowInstanceHandle>;
166
+ get(id: string): Promise<WorkflowInstanceHandle>;
167
+ }
168
+ declare const TENANT_OPERATION_ENGINE = "cloudflare-workflows";
169
+
170
+ type ProvisionStepName = "mark-running" | "create-database" | "apply-migrations" | "upload-script" | "upload-secrets" | "seed-defaults" | "verify" | "mark-ready" | "mark-failed";
171
+ interface ProvisionOperationDeps {
172
+ /** Provider-agnostic provisioner steps (all idempotent) — e.g.
173
+ * `createWfpProvisionerSteps` for Cloudflare WFP + D1. */
174
+ steps: TenantProvisionerSteps;
175
+ /** Control-plane tenants adapter (snapshot writes). */
176
+ tenants: TenantsDataAdapter;
177
+ /** Control-plane operation log stores. */
178
+ stores: TenantOperationStores;
179
+ /**
180
+ * Defaults seed (`createDispatchSyncDefaults(...)`). Runs as a retried
181
+ * step BEFORE `ready`; per-entity errors in the resolved result fail the
182
+ * step. Optional only for parity with the inline hook — WFP control
183
+ * planes should always set it.
184
+ */
185
+ syncDefaults?: (tenantId: string) => Promise<unknown>;
186
+ /**
187
+ * Post-seed verification (`createProvisionVerifier(...)`). Throws until
188
+ * the tenant database actually holds keys + the tenant row; retried with backoff so a
189
+ * propagation race becomes a few retries instead of a bad `ready`.
190
+ */
191
+ verify?: (databaseId: string, tenantId: string) => Promise<void>;
192
+ /** Same gate + default as the inline hook: `deployment_type === "wfp"`. */
193
+ shouldProvision?: (tenant: {
194
+ id: string;
195
+ deployment_type?: string;
196
+ storage_kind?: string;
197
+ }) => boolean;
198
+ logger?: Pick<Console, "warn">;
199
+ /** Per-step overrides of the retry/timeout defaults. */
200
+ stepConfig?: Partial<Record<ProvisionStepName, StepConfig>>;
201
+ }
202
+ /**
203
+ * Durable provision-as-workflow (issue #1026 phase 2): the full provision
204
+ * sequence — resources, migrations, script, secrets, seed, verify — as one
205
+ * engine step per unit, writing back to the control-plane operation log at
206
+ * every step boundary. The DB write happens inside the same `step.do` as
207
+ * the side effect, so it is durable and retried with the step; all side
208
+ * effects are idempotent, which makes replay after a retry or eviction
209
+ * safe.
210
+ *
211
+ * Terminal writes: `mark-ready` flips the tenant snapshot to `ready` and
212
+ * the operation to `succeeded`; any failure runs `mark-failed` (persisting
213
+ * whatever resource ids exist, mirroring the inline hook's failed branch)
214
+ * and rethrows so the engine instance ends `errored`. If even `mark-failed`
215
+ * dies, the operation stays `running` and the reconciler sweep copies the
216
+ * engine's terminal state into the DB.
217
+ *
218
+ * Runs against any `StepRunner` — Cloudflare's `WorkflowStep` satisfies it
219
+ * structurally, and tests use fakes.
220
+ */
221
+ declare function runProvisionOperation(deps: ProvisionOperationDeps, params: TenantOperationWorkflowParams, step: StepRunner): Promise<void>;
222
+
223
+ interface ProvisionVerifierOptions {
224
+ client: CloudflareApiClient;
225
+ /** Minimum number of signing keys the tenant D1 must hold. Default 1. */
226
+ minKeys?: number;
227
+ }
228
+ /**
229
+ * Thrown when the freshly provisioned D1 fails the post-seed checks. The
230
+ * message is self-contained (it survives workflow step serialization) and
231
+ * names exactly which check failed — it ends up in
232
+ * `tenant_operation_events.detail` and, when retries exhaust, in
233
+ * `tenants.provisioning_error`.
234
+ */
235
+ declare class TenantProvisionVerificationError extends Error {
236
+ readonly checks: {
237
+ keyCount: number;
238
+ tenantRowCount: number;
239
+ };
240
+ constructor(message: string, checks: {
241
+ keyCount: number;
242
+ tenantRowCount: number;
243
+ });
244
+ }
245
+ /**
246
+ * Post-provision verification (issue #1026): assert the tenant D1 actually
247
+ * contains signing keys and its own tenant row before the tenant is marked
248
+ * `ready`. Queries D1 over the same REST path the provisioner's migrations
249
+ * use — this is what turns the 2026-07-02 "ready but empty D1" incident
250
+ * class into a retried workflow step instead of a silent success.
251
+ *
252
+ * Limitation (accepted for v1): this proves the rows landed in D1 via the
253
+ * REST API, not that the tenant worker's binding observes them; a worker
254
+ * HTTP probe can be layered on later.
255
+ */
256
+ declare function createProvisionVerifier(options: ProvisionVerifierOptions): (databaseId: string, tenantId: string) => Promise<void>;
257
+
258
+ interface CloudflareWorkflowsExecutorOptions {
259
+ binding: WorkflowsBinding;
260
+ }
261
+ /**
262
+ * `TenantOperationExecutor` backed by a Cloudflare Workflows binding
263
+ * (issue #1026 phase 2). Row-first contract: `enqueueTenantOperation` has
264
+ * already persisted the operation (with its deterministic
265
+ * `engine_instance_id`) before `start` is called, so an instance can never
266
+ * exist without a tracking row.
267
+ *
268
+ * `start` resolves as soon as the instance is created — the workflow owns
269
+ * every subsequent write. Creating an instance whose id already exists is
270
+ * treated as success (idempotent re-enqueue after a crashed caller).
271
+ */
272
+ declare function createCloudflareWorkflowsExecutor(options: CloudflareWorkflowsExecutorOptions): TenantOperationExecutor;
273
+
274
+ interface ReconcileTenantOperationsOptions {
275
+ stores: TenantOperationStores;
276
+ tenants: TenantsDataAdapter;
277
+ binding: WorkflowsBinding;
278
+ /**
279
+ * Only touch operations whose `updated_at` is older than this — fresh
280
+ * runs write at every step boundary, so a recently-updated operation is
281
+ * alive. Default 10 minutes.
282
+ */
283
+ minAgeMs?: number;
284
+ /** Max stuck operations per sweep. Default 100. */
285
+ limit?: number;
286
+ logger?: Pick<Console, "warn">;
287
+ }
288
+ interface ReconcileTenantOperationsResult {
289
+ scanned: number;
290
+ markedFailed: number;
291
+ markedSucceeded: number;
292
+ stillRunning: number;
293
+ instanceMissing: number;
294
+ errors: number;
295
+ }
296
+ /**
297
+ * Sweep for operations stuck in `pending`/`running` whose engine instance
298
+ * died before reaching its own terminal write (issue #1026): resolve the
299
+ * instance (via the stored — or re-derived, it's deterministic — id), and
300
+ * copy terminal engine states into the control-plane DB.
301
+ *
302
+ * The engine retains completed-instance state for at most 30 days, so
303
+ * hosts must run this at least daily; every 15–60 minutes is recommended
304
+ * (wire it to the worker's `scheduled` handler). One operation's engine
305
+ * error never aborts the sweep.
306
+ *
307
+ * Decision table per stuck operation:
308
+ * - handle lookup throws (expired / never created) → `failed`
309
+ * - engine `errored` / `terminated` → `failed` (engine error copied)
310
+ * - engine `complete` but operation non-terminal (the final write raced or
311
+ * was lost) → decided from the tenant snapshot, which is the source of
312
+ * truth: `provisioning_state === "ready"` → `succeeded`, else `failed`
313
+ * - anything else → still running, left untouched
314
+ */
315
+ declare function reconcileTenantOperations(options: ReconcileTenantOperationsOptions): Promise<ReconcileTenantOperationsResult>;
316
+
317
+ /**
318
+ * Structural mirror of `@authhero/multi-tenancy`'s `StepReporter` — kept
319
+ * local so this module carries no multi-tenancy dependency. When the
320
+ * control plane records provisions as tenant operations (issue #1026),
321
+ * the hook receives this callback and surfaces coarse step boundaries
322
+ * (`provision-resources`, `seed-defaults`) in the operation history.
323
+ */
324
+ type WfpProvisioningStepReporter = (step: string, outcome: "started" | "succeeded" | "failed", detail?: Record<string, unknown>) => Promise<void>;
325
+ interface WfpTenantProvisioningHook {
326
+ onProvision(tenantId: string, report?: WfpProvisioningStepReporter): Promise<void>;
327
+ onDeprovision(tenantId: string): Promise<void>;
328
+ /**
329
+ * Re-run provisioning for an already-existing WFP tenant to pull it onto the
330
+ * current bundle + migrations — i.e. an upgrade. Re-uploads the worker
331
+ * script (an upload overwrites), reconciles any migrations not yet applied to
332
+ * the tenant D1, re-runs `syncDefaults`, then rewrites `worker_version`,
333
+ * `bundle_configuration`, and `database_version` so the recorded versions
334
+ * reflect what now runs. Marks `provisioning_state = "pending"` while the
335
+ * upgrade is in flight and `ready` on success (`failed` on error).
336
+ *
337
+ * Throws if the tenant doesn't exist or isn't WFP-provisioned — callers
338
+ * (e.g. a management-API redeploy endpoint) surface that as a 4xx.
339
+ */
340
+ onUpgrade(tenantId: string, report?: WfpProvisioningStepReporter): Promise<void>;
341
+ }
342
+
343
+ interface WfpWorkflowProvisioningHookOptions {
344
+ tenants: TenantsDataAdapter;
345
+ /**
346
+ * Enqueues a provision operation on the durable engine — typically
347
+ * `(input) => enqueueTenantOperation(stores, createCloudflareWorkflowsExecutor({ binding }), input)`.
348
+ * Resolves as soon as the engine instance is created.
349
+ */
350
+ enqueueOperation: (input: {
351
+ kind: "provision";
352
+ tenant_id: string;
353
+ initiated_by?: string;
354
+ }) => Promise<TenantOperation>;
355
+ /** Same gate + default as the inline hook: `deployment_type === "wfp"`. */
356
+ shouldProvision?: (tenant: {
357
+ id: string;
358
+ deployment_type?: string;
359
+ storage_kind?: string;
360
+ }) => boolean;
361
+ /**
362
+ * The existing inline hook (`createWfpTenantProvisioningHook(...)`).
363
+ * Upgrade and deprovision keep running inline until later phases make
364
+ * them durable.
365
+ */
366
+ inline: WfpTenantProvisioningHook;
367
+ }
368
+ /**
369
+ * Drop-in replacement for `createWfpTenantProvisioningHook` on control
370
+ * planes that run provisioning through Cloudflare Workflows (issue #1026
371
+ * phase 2): `onProvision` enqueues a durable provision operation and
372
+ * returns immediately, leaving the tenant `pending` — the workflow's
373
+ * `mark-ready` / `mark-failed` steps own the terminal snapshot writes, and
374
+ * the reconciler covers instances that die mid-run.
375
+ *
376
+ * Semantic change to plan for downstream: tenant-create now returns with
377
+ * `provisioning_state: "pending"`; clients poll the tenant row or the
378
+ * operations API. An enqueue failure still throws, so
379
+ * `createProvisioningHooks.afterCreate` rolls the tenant row back exactly
380
+ * like an inline provision failure does today. This also replaces any
381
+ * best-effort post-create seed — the seed is a durable step inside the
382
+ * workflow.
383
+ *
384
+ * Wire it with `databaseIsolation.recordProvisionOperations: false` — this
385
+ * hook's `enqueueOperation` creates the operation row itself, and the
386
+ * multi-tenancy recording wrapper would otherwise write a second row that
387
+ * gets marked succeeded while the engine run is still in flight.
388
+ */
389
+ declare function createWfpWorkflowProvisioningHook(options: WfpWorkflowProvisioningHookOptions): WfpTenantProvisioningHook;
390
+
391
+ export { TENANT_OPERATION_ENGINE, TenantProvisionVerificationError, createCloudflareWorkflowsExecutor, createProvisionVerifier, createWfpWorkflowProvisioningHook, reconcileTenantOperations, runProvisionOperation };
392
+ export type { CloudflareWorkflowsExecutorOptions, ProvisionOperationDeps, ProvisionStepName, ProvisionVerifierOptions, ReconcileTenantOperationsOptions, ReconcileTenantOperationsResult, TenantOperationWorkflowParams, WfpWorkflowProvisioningHookOptions, WorkflowInstanceHandle, WorkflowInstanceStatus, WorkflowsBinding };
@@ -0,0 +1,279 @@
1
+ import { r as e, t } from "./sync-defaults-errors-BlLZh6Gt.mjs";
2
+ import { buildEngineInstanceId as n, createOperationRecorder as r, isTerminalStatus as i } from "@authhero/multi-tenancy";
3
+ //#region src/workflows/provision-operation.ts
4
+ var a = {
5
+ retries: {
6
+ limit: 5,
7
+ delay: "5 seconds",
8
+ backoff: "exponential"
9
+ },
10
+ timeout: "2 minutes"
11
+ }, o = {
12
+ retries: {
13
+ limit: 8,
14
+ delay: "10 seconds",
15
+ backoff: "exponential"
16
+ },
17
+ timeout: "1 minute"
18
+ };
19
+ function s(e) {
20
+ return e instanceof Error ? e.message : String(e);
21
+ }
22
+ async function c(e, n, i) {
23
+ let c = r(e.stores), l = e.shouldProvision ?? ((e) => e.deployment_type === "wfp"), u = (t) => e.stepConfig?.[t] ?? (t === "verify" ? o : a), d = n.operation_id, f = n.tenant_id, p = await i.do("mark-running", u("mark-running"), async () => {
24
+ let t = await e.tenants.get(f);
25
+ if (!t || !l(t)) return await c.appendEvent(d, {
26
+ step: "mark-running",
27
+ outcome: "skipped",
28
+ detail: { reason: t ? "tenant is not WFP-provisioned" : "tenant row missing (rolled back?)" }
29
+ }), await c.markSucceeded(d), {
30
+ skipped: !0,
31
+ scriptName: "",
32
+ databaseName: ""
33
+ };
34
+ let n = e.steps.validate(), r = e.steps.names(f);
35
+ return await c.markRunning(d, "mark-running"), await c.appendEvent(d, {
36
+ step: "mark-running",
37
+ outcome: "succeeded"
38
+ }), {
39
+ skipped: !1,
40
+ ...r,
41
+ ...n
42
+ };
43
+ });
44
+ if (p.skipped) return;
45
+ let m = {
46
+ worker_script_name: p.scriptName,
47
+ bundle_configuration: p.bundleConfiguration,
48
+ worker_version: p.workerVersion,
49
+ database_version: p.databaseVersion
50
+ };
51
+ try {
52
+ let n = await i.do("create-database", u("create-database"), async () => {
53
+ await c.setCurrentStep(d, "create-database");
54
+ let { id: t, created: n } = await e.steps.findOrCreateDatabase(p.databaseName);
55
+ return await c.appendEvent(d, {
56
+ step: "create-database",
57
+ outcome: "succeeded",
58
+ detail: {
59
+ database_id: t,
60
+ created: n
61
+ }
62
+ }), {
63
+ databaseId: t,
64
+ created: n
65
+ };
66
+ });
67
+ if (m.d1_database_id = n.databaseId, await i.do("apply-migrations", u("apply-migrations"), async () => {
68
+ await c.setCurrentStep(d, "apply-migrations"), await e.steps.applyMigrations(n.databaseId, n.created), await c.appendEvent(d, {
69
+ step: "apply-migrations",
70
+ outcome: "succeeded",
71
+ detail: { database_version: p.databaseVersion }
72
+ });
73
+ }), await i.do("upload-script", u("upload-script"), async () => {
74
+ await c.setCurrentStep(d, "upload-script"), await e.steps.uploadScript(p.scriptName, n.databaseId), await c.appendEvent(d, {
75
+ step: "upload-script",
76
+ outcome: "succeeded",
77
+ detail: { worker_version: p.workerVersion }
78
+ });
79
+ }), await i.do("upload-secrets", u("upload-secrets"), async () => {
80
+ await c.setCurrentStep(d, "upload-secrets"), await e.steps.uploadSecrets(p.scriptName, f), await c.appendEvent(d, {
81
+ step: "upload-secrets",
82
+ outcome: "succeeded"
83
+ });
84
+ }), e.syncDefaults) {
85
+ let n = e.syncDefaults;
86
+ await i.do("seed-defaults", u("seed-defaults"), async () => {
87
+ await c.setCurrentStep(d, "seed-defaults");
88
+ let e = t(await n(f));
89
+ if (e.length > 0) throw Error(`sync-defaults seed reported ${e.length} error(s): ${e.join("; ")}`);
90
+ await c.appendEvent(d, {
91
+ step: "seed-defaults",
92
+ outcome: "succeeded"
93
+ });
94
+ });
95
+ }
96
+ if (e.verify) {
97
+ let t = e.verify;
98
+ await i.do("verify", u("verify"), async () => {
99
+ await c.setCurrentStep(d, "verify"), await t(n.databaseId, f), await c.appendEvent(d, {
100
+ step: "verify",
101
+ outcome: "succeeded"
102
+ });
103
+ });
104
+ }
105
+ await i.do("mark-ready", u("mark-ready"), async () => {
106
+ await c.setCurrentStep(d, "mark-ready"), await e.tenants.update(f, {
107
+ ...m,
108
+ provisioning_state: "ready",
109
+ provisioning_error: void 0,
110
+ provisioning_state_changed_at: (/* @__PURE__ */ new Date()).toISOString()
111
+ }), await c.appendEvent(d, {
112
+ step: "mark-ready",
113
+ outcome: "succeeded"
114
+ }), await c.markSucceeded(d);
115
+ });
116
+ } catch (t) {
117
+ throw await i.do("mark-failed", u("mark-failed"), async () => {
118
+ let n = s(t);
119
+ try {
120
+ await e.tenants.update(f, {
121
+ ...m,
122
+ provisioning_state: "failed",
123
+ provisioning_error: n.slice(0, 2048),
124
+ provisioning_state_changed_at: (/* @__PURE__ */ new Date()).toISOString()
125
+ });
126
+ } catch (t) {
127
+ e.logger?.warn(`Failed to write provisioning_state="failed" for tenant ${f}:`, t);
128
+ }
129
+ await c.appendEvent(d, {
130
+ step: "mark-failed",
131
+ outcome: "failed",
132
+ detail: { message: n }
133
+ }), await c.markFailed(d, t);
134
+ }), t;
135
+ }
136
+ }
137
+ //#endregion
138
+ //#region src/workflows/verify.ts
139
+ var l = class extends Error {
140
+ checks;
141
+ constructor(e, t) {
142
+ super(e), this.name = "TenantProvisionVerificationError", this.checks = t;
143
+ }
144
+ };
145
+ function u(e, t) {
146
+ for (let n of e) for (let e of n.results ?? []) if (!(!e || typeof e != "object")) {
147
+ for (let [n, r] of Object.entries(e)) if (n === t) {
148
+ if (typeof r == "number") return r;
149
+ if (typeof r == "string" && r !== "" && !isNaN(Number(r))) return Number(r);
150
+ }
151
+ }
152
+ return 0;
153
+ }
154
+ function d(t) {
155
+ let n = t.minKeys ?? 1;
156
+ return async (r, i) => {
157
+ let a = await t.client.execD1(r, `SELECT (SELECT COUNT(*) FROM keys) AS key_count, (SELECT COUNT(*) FROM tenants WHERE id = '${e(i)}') AS tenant_count;`), o = u(a, "key_count"), s = u(a, "tenant_count"), c = [];
158
+ if (o < n && c.push(`expected at least ${n} signing key(s) in "keys", found ${o}`), s < 1 && c.push(`tenant row "${i}" missing from "tenants"`), c.length > 0) throw new l(`Tenant D1 verification failed for "${i}": ${c.join("; ")}`, {
159
+ keyCount: o,
160
+ tenantRowCount: s
161
+ });
162
+ };
163
+ }
164
+ //#endregion
165
+ //#region src/workflows/executor.ts
166
+ function f(e) {
167
+ let t = e instanceof Error ? e.message : String(e ?? "");
168
+ return /already exists|already been created|duplicate/i.test(t);
169
+ }
170
+ function p(e) {
171
+ return {
172
+ engine: "cloudflare-workflows",
173
+ async start(t) {
174
+ if (t.kind !== "provision") throw Error(`The Cloudflare Workflows executor does not support "${t.kind}" operations yet (phase 2 covers provision only)`);
175
+ if (!t.tenant_id) throw Error("provision operations require a tenant_id");
176
+ let r = t.engine_instance_id ?? n(t);
177
+ try {
178
+ await e.binding.create({
179
+ id: r,
180
+ params: {
181
+ operation_id: t.id,
182
+ tenant_id: t.tenant_id,
183
+ kind: t.kind
184
+ }
185
+ });
186
+ } catch (e) {
187
+ if (!f(e)) throw e;
188
+ }
189
+ }
190
+ };
191
+ }
192
+ //#endregion
193
+ //#region src/workflows/types.ts
194
+ var m = "cloudflare-workflows";
195
+ //#endregion
196
+ //#region src/workflows/reconcile.ts
197
+ async function h(e) {
198
+ let { stores: t, tenants: a, binding: o } = e, s = r(t), c = e.minAgeMs ?? 600 * 1e3, l = e.limit ?? 100, u = new Date(Date.now() - c).toISOString(), { operations: d } = await t.tenantOperations.list({
199
+ status: ["pending", "running"],
200
+ engine: m,
201
+ updated_before: u,
202
+ per_page: l
203
+ }), f = {
204
+ scanned: d.length,
205
+ markedFailed: 0,
206
+ markedSucceeded: 0,
207
+ stillRunning: 0,
208
+ instanceMissing: 0,
209
+ errors: 0
210
+ };
211
+ for (let t of d) try {
212
+ await h(t);
213
+ } catch (n) {
214
+ f.errors++, e.logger?.warn(`Failed to reconcile tenant operation ${t.id}:`, n);
215
+ }
216
+ return f;
217
+ async function p(e, t) {
218
+ await s.appendEvent(e.id, {
219
+ step: e.current_step ?? "reconcile",
220
+ outcome: "reconciled",
221
+ detail: {
222
+ resolution: "failed",
223
+ reason: t
224
+ }
225
+ }), await s.markFailed(e.id, t), e.kind === "provision" && e.tenant_id && (await a.get(e.tenant_id))?.provisioning_state === "pending" && await a.update(e.tenant_id, {
226
+ provisioning_state: "failed",
227
+ provisioning_error: t.slice(0, 2048),
228
+ provisioning_state_changed_at: (/* @__PURE__ */ new Date()).toISOString()
229
+ });
230
+ }
231
+ async function h(e) {
232
+ let r = await t.tenantOperations.get(e.id);
233
+ if (!r || i(r.status)) return;
234
+ let c = r.engine_instance_id ?? n(r), l;
235
+ try {
236
+ l = await o.get(c);
237
+ } catch {
238
+ f.instanceMissing++, f.markedFailed++, await p(r, `engine instance "${c}" not found (expired past retention or never started)`);
239
+ return;
240
+ }
241
+ let u = await l.status();
242
+ if (u.status === "errored" || u.status === "terminated") {
243
+ let e = typeof u.error == "string" ? u.error : u.error?.message ?? `engine reported ${u.status}`;
244
+ f.markedFailed++, await p(r, e);
245
+ return;
246
+ }
247
+ if (u.status === "complete") {
248
+ (r.tenant_id ? await a.get(r.tenant_id) : null)?.provisioning_state === "ready" ? (f.markedSucceeded++, await s.appendEvent(r.id, {
249
+ step: r.current_step ?? "reconcile",
250
+ outcome: "reconciled",
251
+ detail: { resolution: "succeeded" }
252
+ }), await s.markSucceeded(r.id)) : (f.markedFailed++, await p(r, "engine instance completed but the operation was never finalized and the tenant is not ready"));
253
+ return;
254
+ }
255
+ f.stillRunning++;
256
+ }
257
+ }
258
+ //#endregion
259
+ //#region src/workflows/enqueue-hook.ts
260
+ function g(e) {
261
+ let t = e.shouldProvision ?? ((e) => e.deployment_type === "wfp");
262
+ return {
263
+ async onProvision(n, r) {
264
+ let i = await e.tenants.get(n);
265
+ i && t(i) && await e.enqueueOperation({
266
+ kind: "provision",
267
+ tenant_id: n
268
+ });
269
+ },
270
+ async onUpgrade(t, n) {
271
+ await e.inline.onUpgrade(t, n);
272
+ },
273
+ async onDeprovision(t) {
274
+ await e.inline.onDeprovision(t);
275
+ }
276
+ };
277
+ }
278
+ //#endregion
279
+ export { m as TENANT_OPERATION_ENGINE, l as TenantProvisionVerificationError, p as createCloudflareWorkflowsExecutor, d as createProvisionVerifier, g as createWfpWorkflowProvisioningHook, h as reconcileTenantOperations, c as runProvisionOperation };