@authhero/cloudflare-adapter 2.33.3 → 2.34.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,83 @@
1
+ /**
2
+ * Thin Cloudflare REST API client for the WFP+D1 provisioner.
3
+ *
4
+ * Each method maps 1:1 to a documented CF endpoint and returns the parsed
5
+ * response body. Errors surface as `CloudflareApiError` carrying the HTTP
6
+ * status, endpoint, and (when JSON) the CF error array — making them easy
7
+ * to log without re-fetching the response.
8
+ *
9
+ * Idempotency is the caller's responsibility — the provisioner sequences
10
+ * calls and tolerates "already exists" / "not found" depending on the
11
+ * operation (see `provisioner.ts`).
12
+ */
13
+ export declare class CloudflareApiError extends Error {
14
+ readonly status: number;
15
+ readonly endpoint: string;
16
+ readonly errors: unknown[];
17
+ readonly body: string;
18
+ constructor(status: number, endpoint: string, body: string, errors?: unknown[]);
19
+ }
20
+ export interface CfApiClientOptions {
21
+ accountId: string;
22
+ apiToken: string;
23
+ fetch?: typeof fetch;
24
+ timeoutMs?: number;
25
+ baseUrl?: string;
26
+ }
27
+ export interface D1Database {
28
+ uuid: string;
29
+ name: string;
30
+ }
31
+ export interface D1QueryResult {
32
+ success: boolean;
33
+ meta?: Record<string, unknown>;
34
+ results?: unknown[];
35
+ }
36
+ export interface ScriptBinding {
37
+ type: "d1" | "plain_text" | "secret_text";
38
+ name: string;
39
+ id?: string;
40
+ text?: string;
41
+ }
42
+ export interface ScriptUploadOptions {
43
+ /** Script source (JavaScript ES module). */
44
+ script: string;
45
+ /** Main module filename (must match part name in form data). */
46
+ mainModule: string;
47
+ /** Compatibility date, ISO yyyy-mm-dd. */
48
+ compatibilityDate: string;
49
+ /** Compatibility flags (e.g. `["nodejs_compat"]`). */
50
+ compatibilityFlags?: string[];
51
+ /** Bindings to attach (D1, plain_text, etc.). Secrets go via setSecret(). */
52
+ bindings?: ScriptBinding[];
53
+ /** Optional tags, stored on the script for operator-side lookup. */
54
+ tags?: string[];
55
+ }
56
+ export declare class CloudflareApiClient {
57
+ private readonly accountId;
58
+ private readonly apiToken;
59
+ private readonly fetchImpl;
60
+ private readonly timeoutMs;
61
+ private readonly baseUrl;
62
+ constructor(options: CfApiClientOptions);
63
+ createD1Database(name: string): Promise<D1Database>;
64
+ listD1Databases(name?: string): Promise<D1Database[]>;
65
+ deleteD1Database(databaseId: string): Promise<void>;
66
+ /**
67
+ * Execute a single SQL statement (or batch of `;`-separated statements
68
+ * permitted by D1) against the given database. Use for applying
69
+ * migrations one file at a time — the per-call response size cap means
70
+ * very large single calls can fail; splitting per file keeps each call
71
+ * bounded by the migration author.
72
+ */
73
+ execD1(databaseId: string, sql: string): Promise<D1QueryResult[]>;
74
+ uploadNamespacedScript(namespace: string, scriptName: string, options: ScriptUploadOptions): Promise<void>;
75
+ deleteNamespacedScript(namespace: string, scriptName: string): Promise<void>;
76
+ /**
77
+ * Set a single secret on a namespaced script. The CF API replaces the
78
+ * value if a secret with that name already exists, so this is safely
79
+ * re-runnable.
80
+ */
81
+ setNamespacedScriptSecret(namespace: string, scriptName: string, secretName: string, secretValue: string): Promise<void>;
82
+ private request;
83
+ }
@@ -0,0 +1,6 @@
1
+ export { createCloudflareWfpD1Provisioner } from "./provisioner";
2
+ export type { CloudflareWfpD1Provisioner, CloudflareWfpD1ProvisionerOptions, ProvisionResult, ProvisionerMigration, TenantSecretsResolver, } from "./types";
3
+ export { createWfpTenantProvisioningHook } from "./tenant-hook";
4
+ export type { WfpTenantProvisioningHook, WfpTenantProvisioningHookOptions, } from "./tenant-hook";
5
+ export { CloudflareApiClient, CloudflareApiError } from "./cf-api";
6
+ export type { CfApiClientOptions, D1Database, D1QueryResult, ScriptBinding, ScriptUploadOptions, } from "./cf-api";
@@ -0,0 +1,47 @@
1
+ import type { CloudflareWfpD1Provisioner, CloudflareWfpD1ProvisionerOptions } from "./types";
2
+ /**
3
+ * Construct the lifecycle hooks for provisioning + deprovisioning a tenant
4
+ * on Cloudflare Workers-for-Platforms backed by a per-tenant D1.
5
+ *
6
+ * Wiring on the control-plane authhero:
7
+ *
8
+ * ```ts
9
+ * import createAdapters from "@authhero/cloudflare-adapter";
10
+ * import { createCloudflareWfpD1Provisioner } from "@authhero/cloudflare-adapter";
11
+ * import { initMultiTenant } from "@authhero/multi-tenancy";
12
+ * import tenantWorkerScript from "./tenant-worker.dist.js?raw";
13
+ * import migration0001 from "@authhero/drizzle/drizzle/sqlite/0000_initial.sql?raw";
14
+ *
15
+ * const provisioner = createCloudflareWfpD1Provisioner({
16
+ * accountId: env.CLOUDFLARE_ACCOUNT_ID,
17
+ * apiToken: env.CLOUDFLARE_API_TOKEN,
18
+ * dispatchNamespace: "authhero-tenants",
19
+ * controlPlaneBaseUrl: env.PUBLIC_BASE_URL,
20
+ * tenantWorkerScript,
21
+ * migrations: [{ name: "0000_initial.sql", sql: migration0001 }],
22
+ * secrets: async (tenantId) => ({
23
+ * ENCRYPTION_KEY: env.SHARED_ENCRYPTION_KEY,
24
+ * ISSUER: `https://${tenantId}.tokens.example.com`,
25
+ * }),
26
+ * });
27
+ *
28
+ * const { app } = initMultiTenant({
29
+ * dataAdapter,
30
+ * controlPlane: { tenantId: "main", clientId: "platform" },
31
+ * databaseIsolation: {
32
+ * getAdapters: async (tenantId) => { ... }, // resolve per-tenant adapter
33
+ * onProvision: provisioner.onProvision,
34
+ * onDeprovision: provisioner.onDeprovision,
35
+ * },
36
+ * });
37
+ * ```
38
+ *
39
+ * On tenant create, the management API row write fires
40
+ * `databaseIsolation.onProvision(tenantId)` which runs the full sequence
41
+ * below. If any step throws, the upstream `createProvisioningHooks` rolls
42
+ * back the tenant row — though side effects already taken (D1 created,
43
+ * partial migrations applied) are NOT rolled back. The operator should
44
+ * treat re-running `onProvision(tenantId)` as safe; each step is idempotent
45
+ * on "already exists".
46
+ */
47
+ export declare function createCloudflareWfpD1Provisioner(options: CloudflareWfpD1ProvisionerOptions): CloudflareWfpD1Provisioner;
@@ -0,0 +1,68 @@
1
+ import type { TenantsDataAdapter } from "@authhero/adapter-interfaces";
2
+ import type { CloudflareWfpD1Provisioner } from "./types";
3
+ /**
4
+ * Adapt the provisioner to `@authhero/multi-tenancy`'s
5
+ * `databaseIsolation.onProvision` / `onDeprovision` contract by:
6
+ *
7
+ * 1. Looking up the tenant row first and gating on
8
+ * `tenant.deployment_type === "wfp"` — shared tenants short-circuit so
9
+ * the same control plane can host both kinds without code branches.
10
+ * 2. Running the provisioner sequence (D1 + script + secrets).
11
+ * 3. Writing the resulting `d1_database_id` + `worker_script_name` +
12
+ * `provisioning_state` back onto the tenant row so the admin UI can
13
+ * show real status, and so a redeploy / re-provision knows which
14
+ * resource ids to operate on.
15
+ * 4. On failure, marking `provisioning_state = "failed"` with the error
16
+ * message, then re-throwing — the multi-tenancy hook treats the throw
17
+ * as a signal to roll back the tenant row.
18
+ *
19
+ * Typical wiring on the control-plane authhero:
20
+ *
21
+ * ```ts
22
+ * import { initMultiTenant } from "@authhero/multi-tenancy";
23
+ * import {
24
+ * createCloudflareWfpD1Provisioner,
25
+ * createWfpTenantProvisioningHook,
26
+ * } from "@authhero/cloudflare-adapter";
27
+ *
28
+ * const provisioner = createCloudflareWfpD1Provisioner({ ... });
29
+ * const hook = createWfpTenantProvisioningHook({
30
+ * provisioner,
31
+ * tenants: dataAdapter.tenants,
32
+ * });
33
+ *
34
+ * const { app } = initMultiTenant({
35
+ * dataAdapter,
36
+ * databaseIsolation: {
37
+ * getAdapters: async (tenantId) => { ... },
38
+ * onProvision: hook.onProvision,
39
+ * onDeprovision: hook.onDeprovision,
40
+ * },
41
+ * });
42
+ * ```
43
+ */
44
+ export interface WfpTenantProvisioningHookOptions {
45
+ provisioner: CloudflareWfpD1Provisioner;
46
+ tenants: TenantsDataAdapter;
47
+ /**
48
+ * Optional override of "should this tenant be WFP-provisioned?". Defaults
49
+ * to `tenant.deployment_type === "wfp"`. Provide a custom predicate when
50
+ * the gating signal lives elsewhere (a feature flag, a config table, etc.).
51
+ */
52
+ shouldProvision?: (tenant: {
53
+ id: string;
54
+ deployment_type?: string;
55
+ storage_kind?: string;
56
+ }) => boolean;
57
+ /**
58
+ * Optional `console`-compatible logger for warnings emitted when the
59
+ * tenant row write-back fails after a successful provision. Defaults to
60
+ * a silent no-op so this module stays test-quiet.
61
+ */
62
+ logger?: Pick<Console, "warn">;
63
+ }
64
+ export interface WfpTenantProvisioningHook {
65
+ onProvision(tenantId: string): Promise<void>;
66
+ onDeprovision(tenantId: string): Promise<void>;
67
+ }
68
+ export declare function createWfpTenantProvisioningHook(options: WfpTenantProvisioningHookOptions): WfpTenantProvisioningHook;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Canonical tenant-worker entrypoint for a Workers-for-Platforms + D1
3
+ * authhero deployment. Reference only — copy into your own tenant-worker
4
+ * source tree and bundle from there (this file is not bundled into
5
+ * `@authhero/cloudflare-adapter`'s `dist`).
6
+ *
7
+ * The bundle the provisioner uploads must be a single self-contained ESM
8
+ * `.js`. Operators build it themselves so they can layer in custom hooks,
9
+ * code-executors, secrets, etc. Below is the minimal shape every operator
10
+ * needs.
11
+ *
12
+ * Env contract set by `createCloudflareWfpD1Provisioner.onProvision`:
13
+ *
14
+ * | Name | Source | Purpose |
15
+ * | ------------------------ | ------------ | ---------------------------------------------------- |
16
+ * | AUTH_DB | D1 binding | Per-tenant database |
17
+ * | CONTROL_PLANE_BASE_URL | plain_text | Target of `controlPlaneSync` outbox destination |
18
+ * | ENCRYPTION_KEY | secret_text | Encryption at rest (must be byte-stable across plane)|
19
+ * | ISSUER | secret_text | `iss` claim in JWTs |
20
+ * | …additional secrets… | secret_text | JWKS, mail creds, OAuth client secrets, etc. |
21
+ *
22
+ * Recommended bundler call:
23
+ *
24
+ * ```sh
25
+ * esbuild src/tenant-worker.ts \
26
+ * --bundle --format=esm --platform=neutral --target=es2022 \
27
+ * --conditions=workerd,worker,browser \
28
+ * --outfile=dist/tenant-worker.js
29
+ * ```
30
+ *
31
+ * Wiring into the provisioner (control-plane authhero side):
32
+ *
33
+ * ```ts
34
+ * import tenantWorkerScript from "./tenant-worker.dist.js?raw";
35
+ * import { createCloudflareWfpD1Provisioner } from "@authhero/cloudflare-adapter";
36
+ *
37
+ * const provisioner = createCloudflareWfpD1Provisioner({
38
+ * accountId,
39
+ * apiToken,
40
+ * dispatchNamespace: "authhero-tenants",
41
+ * controlPlaneBaseUrl: env.PUBLIC_BASE_URL,
42
+ * tenantWorkerScript,
43
+ * migrations: [
44
+ * { name: "0000_initial.sql", sql: initialMigrationSql },
45
+ * // ...one entry per @authhero/drizzle migration file, in order
46
+ * ],
47
+ * secrets: async (tenantId) => ({
48
+ * ENCRYPTION_KEY: env.SHARED_ENCRYPTION_KEY, // byte-stable across all tenants
49
+ * ISSUER: `https://${tenantId}.tokens.example.com`,
50
+ * // …whichever else authhero reads from env
51
+ * }),
52
+ * });
53
+ * ```
54
+ *
55
+ * Minimal tenant-worker source (copy and adapt):
56
+ *
57
+ * ```ts
58
+ * import { drizzle } from "drizzle-orm/d1";
59
+ * import createDataAdapters from "@authhero/drizzle";
60
+ * import { init } from "authhero";
61
+ *
62
+ * interface TenantWorkerEnv {
63
+ * AUTH_DB: D1Database;
64
+ * CONTROL_PLANE_BASE_URL: string;
65
+ * ENCRYPTION_KEY: string;
66
+ * ISSUER: string;
67
+ * // …your additional secrets
68
+ * }
69
+ *
70
+ * export default {
71
+ * async fetch(
72
+ * request: Request,
73
+ * env: TenantWorkerEnv,
74
+ * ctx: ExecutionContext,
75
+ * ): Promise<Response> {
76
+ * const db = drizzle(env.AUTH_DB);
77
+ * const dataAdapter = createDataAdapters(db);
78
+ *
79
+ * const { app } = init({
80
+ * dataAdapter,
81
+ * controlPlaneSync: {
82
+ * baseUrl: env.CONTROL_PLANE_BASE_URL,
83
+ * timeoutMs: 10_000,
84
+ * },
85
+ * });
86
+ *
87
+ * return app.fetch(request, env, ctx);
88
+ * },
89
+ * };
90
+ * ```
91
+ */
92
+ export {};
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Public types for the Workers-for-Platforms + D1 tenant provisioner.
3
+ *
4
+ * Wire-up shape — the operator constructs a provisioner once at control-plane
5
+ * boot and passes its `onProvision` / `onDeprovision` to
6
+ * `@authhero/multi-tenancy`'s `databaseIsolation` config. Every tenant
7
+ * create/delete in the management API then drives a CF API call sequence
8
+ * that:
9
+ * 1. (create) provisions a per-tenant D1, applies migrations, deploys a
10
+ * namespaced worker bound to that D1, and uploads secrets to it.
11
+ * 2. (delete) removes the namespaced worker and its D1.
12
+ *
13
+ * Failures throw — the multi-tenancy hook wraps `onProvision` such that a
14
+ * thrown error rolls the tenant row back. Idempotency on retry is best-effort:
15
+ * each step checks for "already exists" and continues, but the operator
16
+ * should treat the provisioning sequence as restartable rather than
17
+ * transactional.
18
+ */
19
+ /**
20
+ * SQL migration to run on every new tenant D1, in order.
21
+ *
22
+ * Operators bundle these from `@authhero/drizzle`'s `drizzle/sqlite/` files
23
+ * via their build tool of choice (vite's `?raw`, esbuild loader, webpack's
24
+ * raw-loader, etc.) — the provisioner is agnostic about how the SQL gets
25
+ * loaded.
26
+ */
27
+ export interface ProvisionerMigration {
28
+ /** Filename, used only for error messages and audit logging. */
29
+ name: string;
30
+ /** Full SQL text of the migration. May contain multiple statements. */
31
+ sql: string;
32
+ }
33
+ /**
34
+ * Resolver that returns the secret values to upload onto a newly-provisioned
35
+ * tenant worker. Called once per tenant during `onProvision`.
36
+ *
37
+ * Implementations typically pull from a secret store (Vault, GCP Secret
38
+ * Manager, etc.) — the values must match the control-plane authhero's
39
+ * expectations for that tenant (notably `ENCRYPTION_KEY` and JWT signing
40
+ * material, which must be byte-stable to keep encrypted-at-rest data and
41
+ * issued JWTs valid).
42
+ */
43
+ export type TenantSecretsResolver = (tenantId: string) => Promise<Record<string, string>>;
44
+ export interface CloudflareWfpD1ProvisionerOptions {
45
+ /** Cloudflare account id that owns the namespace, D1s, and tenant workers. */
46
+ accountId: string;
47
+ /**
48
+ * API token with at least these permissions on `accountId`:
49
+ * - Workers Scripts:Edit
50
+ * - D1:Edit
51
+ * - Workers for Platforms:Edit (for namespace ops)
52
+ */
53
+ apiToken: string;
54
+ /** Name of the dispatch namespace tenant workers are deployed into. */
55
+ dispatchNamespace: string;
56
+ /**
57
+ * Base URL of the control-plane authhero. Passed to the tenant worker via
58
+ * the `CONTROL_PLANE_BASE_URL` env var so its `controlPlaneSync` destination
59
+ * knows where to POST `controlplane.sync.*` events.
60
+ */
61
+ controlPlaneBaseUrl: string;
62
+ /**
63
+ * Full JavaScript bundle of the tenant worker. The operator builds this
64
+ * (typically via esbuild/vite of a thin wrapper that calls
65
+ * `authhero.init({ ... })`), and passes the resulting JS string here.
66
+ *
67
+ * The bundle MUST be self-contained — Cloudflare's script upload doesn't
68
+ * resolve npm dependencies. Use your bundler's `format: 'esm'` + `external`
69
+ * lists to inline `authhero`, `@authhero/drizzle`, and friends.
70
+ */
71
+ tenantWorkerScript: string;
72
+ /**
73
+ * Optional script metadata override. Defaults to
74
+ * `{ main_module: "index.js", compatibility_date, compatibility_flags: ["nodejs_compat"] }`.
75
+ * Set `compatibility_date` to the same date the rest of your workers use.
76
+ */
77
+ scriptMetadata?: {
78
+ main_module?: string;
79
+ compatibility_date?: string;
80
+ compatibility_flags?: string[];
81
+ };
82
+ /**
83
+ * SQL migrations applied in array order to every new tenant D1. Typically
84
+ * loaded from `@authhero/drizzle`'s shipped migrations via your build tool.
85
+ */
86
+ migrations: ProvisionerMigration[];
87
+ /**
88
+ * Resolver that returns the secret values to set on the tenant worker.
89
+ * Called once per `onProvision`. The provisioner uploads each entry via
90
+ * the per-script secrets API.
91
+ */
92
+ secrets: TenantSecretsResolver;
93
+ /**
94
+ * Naming convention for the namespaced script. Supports `{tenant_id}`
95
+ * placeholder. Defaults to `"{tenant_id}"`.
96
+ *
97
+ * Must match whatever the dispatcher synthesizes as `script_name` in its
98
+ * `dispatch_namespace` handler — otherwise the dispatcher can't reach the
99
+ * worker after provisioning.
100
+ */
101
+ scriptNameTemplate?: string;
102
+ /**
103
+ * Naming convention for the per-tenant D1. Supports `{tenant_id}`.
104
+ * Defaults to `"tenant-{tenant_id}"`. CF accepts most ASCII names; keep
105
+ * it stable so a re-provision finds the existing D1.
106
+ */
107
+ d1NameTemplate?: string;
108
+ /**
109
+ * Fetch override (tests only). Defaults to global `fetch`.
110
+ */
111
+ fetch?: typeof fetch;
112
+ /**
113
+ * Per-request timeout (ms) on the CF API. Defaults to 30s. Individual
114
+ * D1 migrations can take a few seconds each; the upload of a multi-MB
115
+ * tenant bundle also takes a noticeable chunk of that.
116
+ */
117
+ timeoutMs?: number;
118
+ }
119
+ /**
120
+ * Outcome of a successful `onProvision` — returned so the caller can persist
121
+ * the resource IDs back onto the tenant row (`tenants.d1_database_id`,
122
+ * `tenants.worker_script_name`). The control-plane authhero ships these
123
+ * fields in its schema; `createWfpTenantProvisioningHook` writes them
124
+ * automatically.
125
+ */
126
+ export interface ProvisionResult {
127
+ d1DatabaseId: string;
128
+ scriptName: string;
129
+ d1Name: string;
130
+ }
131
+ /**
132
+ * What `createCloudflareWfpD1Provisioner` returns — the two lifecycle
133
+ * callbacks that plug into `databaseIsolation` from `@authhero/multi-tenancy`
134
+ * via the `createWfpTenantProvisioningHook` wrapper (which handles
135
+ * deployment-type guarding and tenant-row writebacks).
136
+ */
137
+ export interface CloudflareWfpD1Provisioner {
138
+ onProvision(tenantId: string): Promise<ProvisionResult>;
139
+ onDeprovision(tenantId: string): Promise<void>;
140
+ }
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "type": "git",
12
12
  "url": "https://github.com/markusahlstrand/authhero"
13
13
  },
14
- "version": "2.33.3",
14
+ "version": "2.34.0",
15
15
  "files": [
16
16
  "dist"
17
17
  ],
@@ -44,8 +44,8 @@
44
44
  "dependencies": {
45
45
  "nanoid": "^5.1.11",
46
46
  "wretch": "^3.0.8",
47
- "@authhero/adapter-interfaces": "3.1.0",
48
- "@authhero/kysely-adapter": "11.8.7"
47
+ "@authhero/adapter-interfaces": "3.1.1",
48
+ "@authhero/kysely-adapter": "11.8.8"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "vite build && tsc -p tsconfig.types.json && rollup -c rollup.dts.config.mjs",