@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.
@@ -1,4 +1,7 @@
1
1
  export { createCloudflareWfpD1Provisioner } from "./provisioner";
2
+ export { createWfpProvisionerSteps, escapeSqlLiteral, } from "./provisioner-steps";
3
+ export type { TenantProvisionerSteps, TenantProvisionNames, WfpProvisionerSteps, } from "./provisioner-steps";
4
+ export { collectSyncDefaultsErrors } from "./sync-defaults-errors";
2
5
  export type { CloudflareWfpD1Provisioner, CloudflareWfpD1ProvisionerOptions, ProvisionResult, ProvisionerMigration, TenantSecretsResolver, } from "./types";
3
6
  export { createWfpTenantProvisioningHook } from "./tenant-hook";
4
7
  export type { WfpTenantProvisioningHook, WfpTenantProvisioningHookOptions, } from "./tenant-hook";
@@ -0,0 +1,54 @@
1
+ import { CloudflareApiClient } from "./cf-api";
2
+ import type { CloudflareWfpD1ProvisionerOptions } from "./types";
3
+ export declare function isNotFoundError(err: unknown): boolean;
4
+ export declare function escapeSqlLiteral(value: string): string;
5
+ export interface TenantProvisionNames {
6
+ scriptName: string;
7
+ databaseName: string;
8
+ }
9
+ /**
10
+ * Provider-agnostic contract for the individual, idempotent units of
11
+ * tenant provisioning. Deliberately free of Cloudflare/D1 terminology so
12
+ * the durable operation orchestration (issue #1026 phase 2) — and any
13
+ * future provider (e.g. Bunny with its SQLite databases) — can run against
14
+ * it. `createWfpProvisionerSteps` is the Cloudflare Workers-for-Platforms
15
+ * + D1 implementation.
16
+ */
17
+ export interface TenantProvisionerSteps {
18
+ names(tenantId: string): TenantProvisionNames;
19
+ /**
20
+ * Validate the version token persisted into `tenants.database_version`
21
+ * BEFORE any provider side effects. Returns the recorded versions.
22
+ */
23
+ validate(): {
24
+ databaseVersion?: string;
25
+ bundleConfiguration?: string;
26
+ workerVersion?: string;
27
+ };
28
+ findOrCreateDatabase(name: string): Promise<{
29
+ id: string;
30
+ created: boolean;
31
+ }>;
32
+ /**
33
+ * Reconcile migrations against the provisioner-owned tracking table
34
+ * (`_authhero_provisioner_migrations`), including the legacy backfill
35
+ * branch for pre-tracking databases.
36
+ */
37
+ applyMigrations(databaseId: string, created: boolean): Promise<void>;
38
+ uploadScript(scriptName: string, databaseId: string): Promise<void>;
39
+ uploadSecrets(scriptName: string, tenantId: string): Promise<void>;
40
+ /** Best-effort teardown of both resources; throws a combined error. */
41
+ deprovision(tenantId: string): Promise<void>;
42
+ }
43
+ /**
44
+ * The Cloudflare WFP + D1 implementation of `TenantProvisionerSteps`,
45
+ * extracted from `createCloudflareWfpD1Provisioner` so each unit can run
46
+ * as its own durable workflow step while the inline provisioner keeps
47
+ * running them as one sequence. No behavior change from the original
48
+ * closures — the provisioner is a thin sequence over this object.
49
+ */
50
+ export interface WfpProvisionerSteps extends TenantProvisionerSteps {
51
+ /** Underlying REST client — shared with deprovision and the verify step. */
52
+ client: CloudflareApiClient;
53
+ }
54
+ export declare function createWfpProvisionerSteps(options: CloudflareWfpD1ProvisionerOptions): WfpProvisionerSteps;
@@ -43,5 +43,9 @@ import type { CloudflareWfpD1Provisioner, CloudflareWfpD1ProvisionerOptions } fr
43
43
  * partial migrations applied) are NOT rolled back. The operator should
44
44
  * treat re-running `onProvision(tenantId)` as safe; each step is idempotent
45
45
  * on "already exists".
46
+ *
47
+ * The individual steps live in `createWfpProvisionerSteps` so the durable
48
+ * workflow executor (issue #1026 phase 2) can run the same units with
49
+ * per-step retries; this factory is a thin inline sequence over them.
46
50
  */
47
51
  export declare function createCloudflareWfpD1Provisioner(options: CloudflareWfpD1ProvisionerOptions): CloudflareWfpD1Provisioner;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Collects per-entity `errors` from a sync-defaults apply result. The seed runs
3
+ * with `continueOnError`, so the tenant worker returns a 2xx (no rejection)
4
+ * even when individual entities fail — a clean resolve is therefore not proof
5
+ * the seed landed. Walk the result's entity outcomes and surface any collected
6
+ * errors so a partially-seeded tenant isn't marked `ready`.
7
+ *
8
+ * Shared between the inline tenant hook and the durable workflow's
9
+ * seed-defaults step (issue #1026 phase 2).
10
+ */
11
+ export declare function collectSyncDefaultsErrors(result: unknown): string[];
@@ -78,8 +78,16 @@ export interface WfpTenantProvisioningHookOptions {
78
78
  */
79
79
  syncDefaults?: (tenantId: string) => Promise<unknown>;
80
80
  }
81
+ /**
82
+ * Structural mirror of `@authhero/multi-tenancy`'s `StepReporter` — kept
83
+ * local so this module carries no multi-tenancy dependency. When the
84
+ * control plane records provisions as tenant operations (issue #1026),
85
+ * the hook receives this callback and surfaces coarse step boundaries
86
+ * (`provision-resources`, `seed-defaults`) in the operation history.
87
+ */
88
+ export type WfpProvisioningStepReporter = (step: string, outcome: "started" | "succeeded" | "failed", detail?: Record<string, unknown>) => Promise<void>;
81
89
  export interface WfpTenantProvisioningHook {
82
- onProvision(tenantId: string): Promise<void>;
90
+ onProvision(tenantId: string, report?: WfpProvisioningStepReporter): Promise<void>;
83
91
  onDeprovision(tenantId: string): Promise<void>;
84
92
  /**
85
93
  * Re-run provisioning for an already-existing WFP tenant to pull it onto the
@@ -93,6 +101,6 @@ export interface WfpTenantProvisioningHook {
93
101
  * Throws if the tenant doesn't exist or isn't WFP-provisioned — callers
94
102
  * (e.g. a management-API redeploy endpoint) surface that as a 4xx.
95
103
  */
96
- onUpgrade(tenantId: string): Promise<void>;
104
+ onUpgrade(tenantId: string, report?: WfpProvisioningStepReporter): Promise<void>;
97
105
  }
98
106
  export declare function createWfpTenantProvisioningHook(options: WfpTenantProvisioningHookOptions): WfpTenantProvisioningHook;
@@ -0,0 +1,49 @@
1
+ import type { TenantOperation, TenantsDataAdapter } from "@authhero/adapter-interfaces";
2
+ import type { WfpTenantProvisioningHook } from "../wfp-provisioner/tenant-hook";
3
+ export interface WfpWorkflowProvisioningHookOptions {
4
+ tenants: TenantsDataAdapter;
5
+ /**
6
+ * Enqueues a provision operation on the durable engine — typically
7
+ * `(input) => enqueueTenantOperation(stores, createCloudflareWorkflowsExecutor({ binding }), input)`.
8
+ * Resolves as soon as the engine instance is created.
9
+ */
10
+ enqueueOperation: (input: {
11
+ kind: "provision";
12
+ tenant_id: string;
13
+ initiated_by?: string;
14
+ }) => Promise<TenantOperation>;
15
+ /** Same gate + default as the inline hook: `deployment_type === "wfp"`. */
16
+ shouldProvision?: (tenant: {
17
+ id: string;
18
+ deployment_type?: string;
19
+ storage_kind?: string;
20
+ }) => boolean;
21
+ /**
22
+ * The existing inline hook (`createWfpTenantProvisioningHook(...)`).
23
+ * Upgrade and deprovision keep running inline until later phases make
24
+ * them durable.
25
+ */
26
+ inline: WfpTenantProvisioningHook;
27
+ }
28
+ /**
29
+ * Drop-in replacement for `createWfpTenantProvisioningHook` on control
30
+ * planes that run provisioning through Cloudflare Workflows (issue #1026
31
+ * phase 2): `onProvision` enqueues a durable provision operation and
32
+ * returns immediately, leaving the tenant `pending` — the workflow's
33
+ * `mark-ready` / `mark-failed` steps own the terminal snapshot writes, and
34
+ * the reconciler covers instances that die mid-run.
35
+ *
36
+ * Semantic change to plan for downstream: tenant-create now returns with
37
+ * `provisioning_state: "pending"`; clients poll the tenant row or the
38
+ * operations API. An enqueue failure still throws, so
39
+ * `createProvisioningHooks.afterCreate` rolls the tenant row back exactly
40
+ * like an inline provision failure does today. This also replaces any
41
+ * best-effort post-create seed — the seed is a durable step inside the
42
+ * workflow.
43
+ *
44
+ * Wire it with `databaseIsolation.recordProvisionOperations: false` — this
45
+ * hook's `enqueueOperation` creates the operation row itself, and the
46
+ * multi-tenancy recording wrapper would otherwise write a second row that
47
+ * gets marked succeeded while the engine run is still in flight.
48
+ */
49
+ export declare function createWfpWorkflowProvisioningHook(options: WfpWorkflowProvisioningHookOptions): WfpTenantProvisioningHook;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * REFERENCE ONLY — not part of the published bundle (nothing imports it).
3
+ *
4
+ * The downstream control-plane worker (the deploy repo) owns the
5
+ * `WorkflowEntrypoint` shell, because `cloudflare:workers` only exists in
6
+ * the Workers runtime. Everything else — orchestration, verify, write-back
7
+ * — comes from `@authhero/cloudflare-adapter/workflows`.
8
+ *
9
+ * wrangler.jsonc:
10
+ * ```jsonc
11
+ * {
12
+ * "workflows": [
13
+ * {
14
+ * "name": "tenant-operations",
15
+ * "binding": "TENANT_OPERATIONS_WORKFLOW",
16
+ * "class_name": "TenantOperationWorkflow"
17
+ * }
18
+ * ],
19
+ * // Reconciler cadence: at least daily (engine retention is 30 days);
20
+ * // every 15–60 minutes recommended.
21
+ * "triggers": { "crons": ["0,30 * * * *"] }
22
+ * }
23
+ * ```
24
+ *
25
+ * Worker module:
26
+ * ```ts
27
+ * import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from "cloudflare:workers";
28
+ * import {
29
+ * createProvisionVerifier,
30
+ * createCloudflareWorkflowsExecutor,
31
+ * createWfpWorkflowProvisioningHook,
32
+ * reconcileTenantOperations,
33
+ * runProvisionOperation,
34
+ * type ProvisionOperationDeps,
35
+ * type TenantOperationWorkflowParams,
36
+ * } from "@authhero/cloudflare-adapter/workflows";
37
+ * import { enqueueTenantOperation } from "@authhero/multi-tenancy";
38
+ * import {
39
+ * createWfpProvisionerSteps,
40
+ * createWfpTenantProvisioningHook,
41
+ * CloudflareApiClient,
42
+ * } from "@authhero/cloudflare-adapter";
43
+ * import { createDispatchSyncDefaults } from "@authhero/cloudflare-adapter/wfp";
44
+ *
45
+ * // Host-owned: build every workflow dependency from the worker env. Params
46
+ * // carry only { operation_id, tenant_id, kind } — never secrets.
47
+ * function buildProvisionDeps(env: Env): ProvisionOperationDeps {
48
+ * const adapters = createControlPlaneAdapters(env); // host's DB wiring
49
+ * const steps = createWfpProvisionerSteps({
50
+ * accountId: env.CLOUDFLARE_ACCOUNT_ID,
51
+ * apiToken: env.CLOUDFLARE_API_TOKEN,
52
+ * dispatchNamespace: "authhero-tenants",
53
+ * controlPlaneBaseUrl: env.PUBLIC_BASE_URL,
54
+ * tenantWorkerScript,
55
+ * migrations,
56
+ * secrets: async (tenantId) => ({ ... }),
57
+ * });
58
+ * return {
59
+ * steps,
60
+ * tenants: adapters.tenants,
61
+ * stores: {
62
+ * tenantOperations: adapters.tenantOperations!,
63
+ * tenantOperationEvents: adapters.tenantOperationEvents!,
64
+ * },
65
+ * syncDefaults: createDispatchSyncDefaults({ ... }),
66
+ * verify: createProvisionVerifier({ client: steps.client }),
67
+ * };
68
+ * }
69
+ *
70
+ * export class TenantOperationWorkflow extends WorkflowEntrypoint<Env, TenantOperationWorkflowParams> {
71
+ * async run(event: WorkflowEvent<TenantOperationWorkflowParams>, step: WorkflowStep) {
72
+ * // CF's WorkflowStep satisfies StepRunner structurally.
73
+ * await runProvisionOperation(buildProvisionDeps(this.env), event.payload, step);
74
+ * }
75
+ * }
76
+ *
77
+ * export default {
78
+ * fetch: app.fetch,
79
+ * async scheduled(_controller: ScheduledController, env: Env) {
80
+ * const adapters = createControlPlaneAdapters(env);
81
+ * const result = await reconcileTenantOperations({
82
+ * stores: {
83
+ * tenantOperations: adapters.tenantOperations!,
84
+ * tenantOperationEvents: adapters.tenantOperationEvents!,
85
+ * },
86
+ * tenants: adapters.tenants,
87
+ * binding: env.TENANT_OPERATIONS_WORKFLOW,
88
+ * });
89
+ * console.log("reconcileTenantOperations", result);
90
+ * },
91
+ * };
92
+ *
93
+ * // Tenant create wiring — replaces the inline provision (and any
94
+ * // best-effort post-create seed) with a durable enqueue:
95
+ * const inlineHook = createWfpTenantProvisioningHook({ provisioner, tenants, syncDefaults });
96
+ * const workflowHook = createWfpWorkflowProvisioningHook({
97
+ * tenants: adapters.tenants,
98
+ * inline: inlineHook, // upgrade/deprovision stay inline until phase 3/4
99
+ * enqueueOperation: (input) =>
100
+ * enqueueTenantOperation(
101
+ * stores,
102
+ * createCloudflareWorkflowsExecutor({ binding: env.TENANT_OPERATIONS_WORKFLOW }),
103
+ * input,
104
+ * ),
105
+ * });
106
+ * // databaseIsolation: {
107
+ * // getAdapters,
108
+ * // onProvision: workflowHook.onProvision,
109
+ * // onDeprovision: workflowHook.onDeprovision,
110
+ * // recordProvisionOperations: false, // the workflow owns the operation row
111
+ * // }
112
+ * ```
113
+ *
114
+ * Rollout order for existing deployments: apply the control-plane
115
+ * migrations first, deploy the worker with both paths available, then flip
116
+ * tenant-create to the workflow hook.
117
+ */
118
+ export {};
@@ -0,0 +1,17 @@
1
+ import { type TenantOperationExecutor } from "@authhero/multi-tenancy";
2
+ import type { WorkflowsBinding } from "./types";
3
+ export interface CloudflareWorkflowsExecutorOptions {
4
+ binding: WorkflowsBinding;
5
+ }
6
+ /**
7
+ * `TenantOperationExecutor` backed by a Cloudflare Workflows binding
8
+ * (issue #1026 phase 2). Row-first contract: `enqueueTenantOperation` has
9
+ * already persisted the operation (with its deterministic
10
+ * `engine_instance_id`) before `start` is called, so an instance can never
11
+ * exist without a tracking row.
12
+ *
13
+ * `start` resolves as soon as the instance is created — the workflow owns
14
+ * every subsequent write. Creating an instance whose id already exists is
15
+ * treated as success (idempotent re-enqueue after a crashed caller).
16
+ */
17
+ export declare function createCloudflareWorkflowsExecutor(options: CloudflareWorkflowsExecutorOptions): TenantOperationExecutor;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Durable tenant lifecycle operations on Cloudflare Workflows
3
+ * (issue #1026 phase 2). The control-plane database is the source of
4
+ * truth; the engine is the executor — the workflow writes back to the
5
+ * `tenant_operations` / `tenant_operation_events` log at every step
6
+ * boundary, and `reconcileTenantOperations` sweeps up instances that died
7
+ * before their terminal write.
8
+ *
9
+ * Requires the optional `@authhero/multi-tenancy` peer (like `./wfp`).
10
+ * Nothing here imports `cloudflare:workers`: the downstream worker
11
+ * provides the ~10-line `WorkflowEntrypoint` shell (see
12
+ * `entrypoint.example.ts`) whose `WorkflowStep` satisfies `StepRunner`
13
+ * structurally.
14
+ */
15
+ export { runProvisionOperation, type ProvisionOperationDeps, type ProvisionStepName, } from "./provision-operation";
16
+ export { createProvisionVerifier, TenantProvisionVerificationError, type ProvisionVerifierOptions, } from "./verify";
17
+ export { createCloudflareWorkflowsExecutor, type CloudflareWorkflowsExecutorOptions, } from "./executor";
18
+ export { reconcileTenantOperations, type ReconcileTenantOperationsOptions, type ReconcileTenantOperationsResult, } from "./reconcile";
19
+ export { createWfpWorkflowProvisioningHook, type WfpWorkflowProvisioningHookOptions, } from "./enqueue-hook";
20
+ export { TENANT_OPERATION_ENGINE, type TenantOperationWorkflowParams, type WorkflowInstanceHandle, type WorkflowInstanceStatus, type WorkflowsBinding, } from "./types";
@@ -0,0 +1,56 @@
1
+ import type { TenantsDataAdapter } from "@authhero/adapter-interfaces";
2
+ import { type StepConfig, type StepRunner, type TenantOperationStores } from "@authhero/multi-tenancy";
3
+ import type { TenantProvisionerSteps } from "../wfp-provisioner/provisioner-steps";
4
+ import type { TenantOperationWorkflowParams } from "./types";
5
+ export type ProvisionStepName = "mark-running" | "create-database" | "apply-migrations" | "upload-script" | "upload-secrets" | "seed-defaults" | "verify" | "mark-ready" | "mark-failed";
6
+ export interface ProvisionOperationDeps {
7
+ /** Provider-agnostic provisioner steps (all idempotent) — e.g.
8
+ * `createWfpProvisionerSteps` for Cloudflare WFP + D1. */
9
+ steps: TenantProvisionerSteps;
10
+ /** Control-plane tenants adapter (snapshot writes). */
11
+ tenants: TenantsDataAdapter;
12
+ /** Control-plane operation log stores. */
13
+ stores: TenantOperationStores;
14
+ /**
15
+ * Defaults seed (`createDispatchSyncDefaults(...)`). Runs as a retried
16
+ * step BEFORE `ready`; per-entity errors in the resolved result fail the
17
+ * step. Optional only for parity with the inline hook — WFP control
18
+ * planes should always set it.
19
+ */
20
+ syncDefaults?: (tenantId: string) => Promise<unknown>;
21
+ /**
22
+ * Post-seed verification (`createProvisionVerifier(...)`). Throws until
23
+ * the tenant database actually holds keys + the tenant row; retried with backoff so a
24
+ * propagation race becomes a few retries instead of a bad `ready`.
25
+ */
26
+ verify?: (databaseId: string, tenantId: string) => Promise<void>;
27
+ /** Same gate + default as the inline hook: `deployment_type === "wfp"`. */
28
+ shouldProvision?: (tenant: {
29
+ id: string;
30
+ deployment_type?: string;
31
+ storage_kind?: string;
32
+ }) => boolean;
33
+ logger?: Pick<Console, "warn">;
34
+ /** Per-step overrides of the retry/timeout defaults. */
35
+ stepConfig?: Partial<Record<ProvisionStepName, StepConfig>>;
36
+ }
37
+ /**
38
+ * Durable provision-as-workflow (issue #1026 phase 2): the full provision
39
+ * sequence — resources, migrations, script, secrets, seed, verify — as one
40
+ * engine step per unit, writing back to the control-plane operation log at
41
+ * every step boundary. The DB write happens inside the same `step.do` as
42
+ * the side effect, so it is durable and retried with the step; all side
43
+ * effects are idempotent, which makes replay after a retry or eviction
44
+ * safe.
45
+ *
46
+ * Terminal writes: `mark-ready` flips the tenant snapshot to `ready` and
47
+ * the operation to `succeeded`; any failure runs `mark-failed` (persisting
48
+ * whatever resource ids exist, mirroring the inline hook's failed branch)
49
+ * and rethrows so the engine instance ends `errored`. If even `mark-failed`
50
+ * dies, the operation stays `running` and the reconciler sweep copies the
51
+ * engine's terminal state into the DB.
52
+ *
53
+ * Runs against any `StepRunner` — Cloudflare's `WorkflowStep` satisfies it
54
+ * structurally, and tests use fakes.
55
+ */
56
+ export declare function runProvisionOperation(deps: ProvisionOperationDeps, params: TenantOperationWorkflowParams, step: StepRunner): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import type { TenantsDataAdapter } from "@authhero/adapter-interfaces";
2
+ import { type TenantOperationStores } from "@authhero/multi-tenancy";
3
+ import { type WorkflowsBinding } from "./types";
4
+ export interface ReconcileTenantOperationsOptions {
5
+ stores: TenantOperationStores;
6
+ tenants: TenantsDataAdapter;
7
+ binding: WorkflowsBinding;
8
+ /**
9
+ * Only touch operations whose `updated_at` is older than this — fresh
10
+ * runs write at every step boundary, so a recently-updated operation is
11
+ * alive. Default 10 minutes.
12
+ */
13
+ minAgeMs?: number;
14
+ /** Max stuck operations per sweep. Default 100. */
15
+ limit?: number;
16
+ logger?: Pick<Console, "warn">;
17
+ }
18
+ export interface ReconcileTenantOperationsResult {
19
+ scanned: number;
20
+ markedFailed: number;
21
+ markedSucceeded: number;
22
+ stillRunning: number;
23
+ instanceMissing: number;
24
+ errors: number;
25
+ }
26
+ /**
27
+ * Sweep for operations stuck in `pending`/`running` whose engine instance
28
+ * died before reaching its own terminal write (issue #1026): resolve the
29
+ * instance (via the stored — or re-derived, it's deterministic — id), and
30
+ * copy terminal engine states into the control-plane DB.
31
+ *
32
+ * The engine retains completed-instance state for at most 30 days, so
33
+ * hosts must run this at least daily; every 15–60 minutes is recommended
34
+ * (wire it to the worker's `scheduled` handler). One operation's engine
35
+ * error never aborts the sweep.
36
+ *
37
+ * Decision table per stuck operation:
38
+ * - handle lookup throws (expired / never created) → `failed`
39
+ * - engine `errored` / `terminated` → `failed` (engine error copied)
40
+ * - engine `complete` but operation non-terminal (the final write raced or
41
+ * was lost) → decided from the tenant snapshot, which is the source of
42
+ * truth: `provisioning_state === "ready"` → `succeeded`, else `failed`
43
+ * - anything else → still running, left untouched
44
+ */
45
+ export declare function reconcileTenantOperations(options: ReconcileTenantOperationsOptions): Promise<ReconcileTenantOperationsResult>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Structural types for the Cloudflare Workflows engine. Deliberately NOT
3
+ * imported from `cloudflare:workers` / `@cloudflare/workers-types` — this
4
+ * package ships platform-agnostic bundles and the real runtime objects
5
+ * satisfy these shapes structurally (precedent: `@authhero/proxy`'s local
6
+ * binding types). Only the downstream worker's `WorkflowEntrypoint` shell
7
+ * touches `cloudflare:workers` (see `entrypoint.example.ts`).
8
+ */
9
+ /**
10
+ * The ONLY data that crosses the enqueue boundary. Workflows persists
11
+ * params (and shows them in the dashboard), so never put secrets here —
12
+ * the workflow re-resolves everything else from its worker env.
13
+ */
14
+ export interface TenantOperationWorkflowParams {
15
+ operation_id: string;
16
+ tenant_id: string;
17
+ /** Widened beyond "provision" in later phases (upgrade, backup, …). */
18
+ kind: "provision";
19
+ }
20
+ /**
21
+ * Instance status as reported by the engine. The exact value set has
22
+ * drifted across Cloudflare releases; the reconciler only branches on the
23
+ * terminal values and treats anything unrecognized as still-running.
24
+ */
25
+ export interface WorkflowInstanceStatus {
26
+ status: "queued" | "running" | "paused" | "errored" | "terminated" | "complete" | "waiting" | "waitingForPause" | "unknown" | (string & {});
27
+ error?: {
28
+ name?: string;
29
+ message?: string;
30
+ } | string | null;
31
+ output?: unknown;
32
+ }
33
+ /** Structurally satisfied by the handles a `workflows` binding returns. */
34
+ export interface WorkflowInstanceHandle {
35
+ id: string;
36
+ status(): Promise<WorkflowInstanceStatus>;
37
+ }
38
+ /**
39
+ * Structurally satisfied by a Workflows binding
40
+ * (e.g. `env.TENANT_OPERATIONS_WORKFLOW`).
41
+ */
42
+ export interface WorkflowsBinding {
43
+ create(options: {
44
+ id: string;
45
+ params: TenantOperationWorkflowParams;
46
+ }): Promise<WorkflowInstanceHandle>;
47
+ get(id: string): Promise<WorkflowInstanceHandle>;
48
+ }
49
+ export declare const TENANT_OPERATION_ENGINE = "cloudflare-workflows";
@@ -0,0 +1,35 @@
1
+ import { CloudflareApiClient } from "../wfp-provisioner/cf-api";
2
+ export interface ProvisionVerifierOptions {
3
+ client: CloudflareApiClient;
4
+ /** Minimum number of signing keys the tenant D1 must hold. Default 1. */
5
+ minKeys?: number;
6
+ }
7
+ /**
8
+ * Thrown when the freshly provisioned D1 fails the post-seed checks. The
9
+ * message is self-contained (it survives workflow step serialization) and
10
+ * names exactly which check failed — it ends up in
11
+ * `tenant_operation_events.detail` and, when retries exhaust, in
12
+ * `tenants.provisioning_error`.
13
+ */
14
+ export declare class TenantProvisionVerificationError extends Error {
15
+ readonly checks: {
16
+ keyCount: number;
17
+ tenantRowCount: number;
18
+ };
19
+ constructor(message: string, checks: {
20
+ keyCount: number;
21
+ tenantRowCount: number;
22
+ });
23
+ }
24
+ /**
25
+ * Post-provision verification (issue #1026): assert the tenant D1 actually
26
+ * contains signing keys and its own tenant row before the tenant is marked
27
+ * `ready`. Queries D1 over the same REST path the provisioner's migrations
28
+ * use — this is what turns the 2026-07-02 "ready but empty D1" incident
29
+ * class into a retried workflow step instead of a silent success.
30
+ *
31
+ * Limitation (accepted for v1): this proves the rows landed in D1 via the
32
+ * REST API, not that the tenant worker's binding observes them; a worker
33
+ * HTTP probe can be layered on later.
34
+ */
35
+ export declare function createProvisionVerifier(options: ProvisionVerifierOptions): (databaseId: string, tenantId: string) => Promise<void>;
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./sync-defaults-errors-Htp1NWTq.js");let t=require("@authhero/multi-tenancy");var n={retries:{limit:5,delay:`5 seconds`,backoff:`exponential`},timeout:`2 minutes`},r={retries:{limit:8,delay:`10 seconds`,backoff:`exponential`},timeout:`1 minute`};function i(e){return e instanceof Error?e.message:String(e)}async function a(a,o,s){let c=(0,t.createOperationRecorder)(a.stores),l=a.shouldProvision??(e=>e.deployment_type===`wfp`),u=e=>a.stepConfig?.[e]??(e===`verify`?r:n),d=o.operation_id,f=o.tenant_id,p=await s.do(`mark-running`,u(`mark-running`),async()=>{let e=await a.tenants.get(f);if(!e||!l(e))return await c.appendEvent(d,{step:`mark-running`,outcome:`skipped`,detail:{reason:e?`tenant is not WFP-provisioned`:`tenant row missing (rolled back?)`}}),await c.markSucceeded(d),{skipped:!0,scriptName:``,databaseName:``};let t=a.steps.validate(),n=a.steps.names(f);return await c.markRunning(d,`mark-running`),await c.appendEvent(d,{step:`mark-running`,outcome:`succeeded`}),{skipped:!1,...n,...t}});if(p.skipped)return;let m={worker_script_name:p.scriptName,bundle_configuration:p.bundleConfiguration,worker_version:p.workerVersion,database_version:p.databaseVersion};try{let t=await s.do(`create-database`,u(`create-database`),async()=>{await c.setCurrentStep(d,`create-database`);let{id:e,created:t}=await a.steps.findOrCreateDatabase(p.databaseName);return await c.appendEvent(d,{step:`create-database`,outcome:`succeeded`,detail:{database_id:e,created:t}}),{databaseId:e,created:t}});if(m.d1_database_id=t.databaseId,await s.do(`apply-migrations`,u(`apply-migrations`),async()=>{await c.setCurrentStep(d,`apply-migrations`),await a.steps.applyMigrations(t.databaseId,t.created),await c.appendEvent(d,{step:`apply-migrations`,outcome:`succeeded`,detail:{database_version:p.databaseVersion}})}),await s.do(`upload-script`,u(`upload-script`),async()=>{await c.setCurrentStep(d,`upload-script`),await a.steps.uploadScript(p.scriptName,t.databaseId),await c.appendEvent(d,{step:`upload-script`,outcome:`succeeded`,detail:{worker_version:p.workerVersion}})}),await s.do(`upload-secrets`,u(`upload-secrets`),async()=>{await c.setCurrentStep(d,`upload-secrets`),await a.steps.uploadSecrets(p.scriptName,f),await c.appendEvent(d,{step:`upload-secrets`,outcome:`succeeded`})}),a.syncDefaults){let t=a.syncDefaults;await s.do(`seed-defaults`,u(`seed-defaults`),async()=>{await c.setCurrentStep(d,`seed-defaults`);let n=e.t(await t(f));if(n.length>0)throw Error(`sync-defaults seed reported ${n.length} error(s): ${n.join(`; `)}`);await c.appendEvent(d,{step:`seed-defaults`,outcome:`succeeded`})})}if(a.verify){let e=a.verify;await s.do(`verify`,u(`verify`),async()=>{await c.setCurrentStep(d,`verify`),await e(t.databaseId,f),await c.appendEvent(d,{step:`verify`,outcome:`succeeded`})})}await s.do(`mark-ready`,u(`mark-ready`),async()=>{await c.setCurrentStep(d,`mark-ready`),await a.tenants.update(f,{...m,provisioning_state:`ready`,provisioning_error:void 0,provisioning_state_changed_at:new Date().toISOString()}),await c.appendEvent(d,{step:`mark-ready`,outcome:`succeeded`}),await c.markSucceeded(d)})}catch(e){throw await s.do(`mark-failed`,u(`mark-failed`),async()=>{let t=i(e);try{await a.tenants.update(f,{...m,provisioning_state:`failed`,provisioning_error:t.slice(0,2048),provisioning_state_changed_at:new Date().toISOString()})}catch(e){a.logger?.warn(`Failed to write provisioning_state="failed" for tenant ${f}:`,e)}await c.appendEvent(d,{step:`mark-failed`,outcome:`failed`,detail:{message:t}}),await c.markFailed(d,e)}),e}}var o=class extends Error{checks;constructor(e,t){super(e),this.name=`TenantProvisionVerificationError`,this.checks=t}};function s(e,t){for(let n of e)for(let e of n.results??[])if(!(!e||typeof e!=`object`)){for(let[n,r]of Object.entries(e))if(n===t){if(typeof r==`number`)return r;if(typeof r==`string`&&r!==``&&!isNaN(Number(r)))return Number(r)}}return 0}function c(t){let n=t.minKeys??1;return async(r,i)=>{let a=await t.client.execD1(r,`SELECT (SELECT COUNT(*) FROM keys) AS key_count, (SELECT COUNT(*) FROM tenants WHERE id = '${e.r(i)}') AS tenant_count;`),c=s(a,`key_count`),l=s(a,`tenant_count`),u=[];if(c<n&&u.push(`expected at least ${n} signing key(s) in "keys", found ${c}`),l<1&&u.push(`tenant row "${i}" missing from "tenants"`),u.length>0)throw new o(`Tenant D1 verification failed for "${i}": ${u.join(`; `)}`,{keyCount:c,tenantRowCount:l})}}function l(e){let t=e instanceof Error?e.message:String(e??``);return/already exists|already been created|duplicate/i.test(t)}function u(e){return{engine:`cloudflare-workflows`,async start(n){if(n.kind!==`provision`)throw Error(`The Cloudflare Workflows executor does not support "${n.kind}" operations yet (phase 2 covers provision only)`);if(!n.tenant_id)throw Error(`provision operations require a tenant_id`);let r=n.engine_instance_id??(0,t.buildEngineInstanceId)(n);try{await e.binding.create({id:r,params:{operation_id:n.id,tenant_id:n.tenant_id,kind:n.kind}})}catch(e){if(!l(e))throw e}}}}var d=`cloudflare-workflows`;async function f(e){let{stores:n,tenants:r,binding:i}=e,a=(0,t.createOperationRecorder)(n),o=e.minAgeMs??600*1e3,s=e.limit??100,c=new Date(Date.now()-o).toISOString(),{operations:l}=await n.tenantOperations.list({status:[`pending`,`running`],engine:d,updated_before:c,per_page:s}),u={scanned:l.length,markedFailed:0,markedSucceeded:0,stillRunning:0,instanceMissing:0,errors:0};for(let t of l)try{await p(t)}catch(n){u.errors++,e.logger?.warn(`Failed to reconcile tenant operation ${t.id}:`,n)}return u;async function f(e,t){await a.appendEvent(e.id,{step:e.current_step??`reconcile`,outcome:`reconciled`,detail:{resolution:`failed`,reason:t}}),await a.markFailed(e.id,t),e.kind===`provision`&&e.tenant_id&&(await r.get(e.tenant_id))?.provisioning_state===`pending`&&await r.update(e.tenant_id,{provisioning_state:`failed`,provisioning_error:t.slice(0,2048),provisioning_state_changed_at:new Date().toISOString()})}async function p(e){let o=await n.tenantOperations.get(e.id);if(!o||(0,t.isTerminalStatus)(o.status))return;let s=o.engine_instance_id??(0,t.buildEngineInstanceId)(o),c;try{c=await i.get(s)}catch{u.instanceMissing++,u.markedFailed++,await f(o,`engine instance "${s}" not found (expired past retention or never started)`);return}let l=await c.status();if(l.status===`errored`||l.status===`terminated`){let e=typeof l.error==`string`?l.error:l.error?.message??`engine reported ${l.status}`;u.markedFailed++,await f(o,e);return}if(l.status===`complete`){(o.tenant_id?await r.get(o.tenant_id):null)?.provisioning_state===`ready`?(u.markedSucceeded++,await a.appendEvent(o.id,{step:o.current_step??`reconcile`,outcome:`reconciled`,detail:{resolution:`succeeded`}}),await a.markSucceeded(o.id)):(u.markedFailed++,await f(o,`engine instance completed but the operation was never finalized and the tenant is not ready`));return}u.stillRunning++}}function p(e){let t=e.shouldProvision??(e=>e.deployment_type===`wfp`);return{async onProvision(n,r){let i=await e.tenants.get(n);i&&t(i)&&await e.enqueueOperation({kind:`provision`,tenant_id:n})},async onUpgrade(t,n){await e.inline.onUpgrade(t,n)},async onDeprovision(t){await e.inline.onDeprovision(t)}}}exports.TENANT_OPERATION_ENGINE=d,exports.TenantProvisionVerificationError=o,exports.createCloudflareWorkflowsExecutor=u,exports.createProvisionVerifier=c,exports.createWfpWorkflowProvisioningHook=p,exports.reconcileTenantOperations=f,exports.runProvisionOperation=a;