@authhero/cloudflare-adapter 2.33.2 → 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.
- package/dist/cloudflare-adapter.cjs +1 -1
- package/dist/cloudflare-adapter.d.ts +342 -3
- package/dist/cloudflare-adapter.mjs +1683 -1470
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/wfp-provisioner/cf-api.d.ts +83 -0
- package/dist/types/wfp-provisioner/index.d.ts +6 -0
- package/dist/types/wfp-provisioner/provisioner.d.ts +47 -0
- package/dist/types/wfp-provisioner/tenant-hook.d.ts +68 -0
- package/dist/types/wfp-provisioner/tenant-worker.example.d.ts +92 -0
- package/dist/types/wfp-provisioner/types.d.ts +140 -0
- package/package.json +3 -3
|
@@ -330,4 +330,4 @@ export default {
|
|
|
330
330
|
}
|
|
331
331
|
},
|
|
332
332
|
};
|
|
333
|
-
`}var Lh=class{loader;compatibilityDate;constructor(e){this.loader=e.loader,this.compatibilityDate=e.compatibilityDate||`2025-01-01`}async execute(e){let t=Date.now();try{let t=Ih(e.code),n={compatibilityDate:this.compatibilityDate,mainModule:`hook.js`,modules:{"hook.js":t}},r=e.hookCodeId?`${e.hookCodeId}-${await Fh(e.code)}`:null;return await(await(r?this.loader.get(r,async()=>n):this.loader.load(n)).getEntrypoint().fetch(new Request(`https://hook/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({event:e.event,triggerId:e.triggerId})}))).json()}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e),durationMs:Date.now()-t,apiCalls:[]}}}};function Rh(e){let t={customDomains:Dm(e),cache:km({...e.cacheName&&{cacheName:e.cacheName},...e.defaultTtlSeconds!==void 0&&{defaultTtlSeconds:e.defaultTtlSeconds},...e.keyPrefix&&{keyPrefix:e.keyPrefix}}),geo:Dh()};e.r2SqlLogs?t.logs=Um(e.r2SqlLogs):e.analyticsEngineLogs&&(t.logs=mh(e.analyticsEngineLogs)),e.analyticsEngineLogs&&(t.analytics=ph(e.analyticsEngineLogs)),e.analyticsEngineActionExecutions&&(t.actionExecutions=Eh(e.analyticsEngineActionExecutions));let n=kh(e.rateLimitBindings);return n&&(t.rateLimit=n),t}exports.CloudflareCodeExecutor=Mh,exports.DispatchNamespaceCodeExecutor=jh,exports.WorkerLoaderCodeExecutor=Lh,exports.createAnalyticsEngineActionExecutionsAdapter=Eh,exports.createAnalyticsEngineAnalyticsAdapter=ph,exports.createAnalyticsEngineLogsAdapter=mh,exports.createAnalyticsEngineStatsAdapter=ih,exports.createCloudflareRateLimitAdapter=kh,exports.createR2SQLLogsAdapter=Um,exports.createR2SQLStatsAdapter=Hm,exports.default=
|
|
333
|
+
`}var Lh=class{loader;compatibilityDate;constructor(e){this.loader=e.loader,this.compatibilityDate=e.compatibilityDate||`2025-01-01`}async execute(e){let t=Date.now();try{let t=Ih(e.code),n={compatibilityDate:this.compatibilityDate,mainModule:`hook.js`,modules:{"hook.js":t}},r=e.hookCodeId?`${e.hookCodeId}-${await Fh(e.code)}`:null;return await(await(r?this.loader.get(r,async()=>n):this.loader.load(n)).getEntrypoint().fetch(new Request(`https://hook/execute`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({event:e.event,triggerId:e.triggerId})}))).json()}catch(e){return{success:!1,error:e instanceof Error?e.message:String(e),durationMs:Date.now()-t,apiCalls:[]}}}},Rh=class extends Error{status;endpoint;errors;body;constructor(e,t,n,r=[]){super(`Cloudflare API ${e} ${t}: ${zh(n,256)}`),this.name=`CloudflareApiError`,this.status=e,this.endpoint=t,this.body=n,this.errors=r}};function zh(e,t){return e.length<=t?e:`${e.slice(0,t)}…`}var Bh=class{accountId;apiToken;fetchImpl;timeoutMs;baseUrl;constructor(e){this.accountId=e.accountId,this.apiToken=e.apiToken,this.fetchImpl=e.fetch??fetch,this.timeoutMs=e.timeoutMs??3e4,this.baseUrl=(e.baseUrl??`https://api.cloudflare.com/client/v4`).replace(/\/+$/,``)}async createD1Database(e){let t=`/accounts/${encodeURIComponent(this.accountId)}/d1/database`;return(await this.request(`POST`,t,{body:JSON.stringify({name:e})})).result}async listD1Databases(e){let t=e?`?name=${encodeURIComponent(e)}`:``,n=`/accounts/${encodeURIComponent(this.accountId)}/d1/database${t}`;return(await this.request(`GET`,n)).result??[]}async deleteD1Database(e){let t=`/accounts/${encodeURIComponent(this.accountId)}/d1/database/${encodeURIComponent(e)}`;await this.request(`DELETE`,t)}async execD1(e,t){let n=`/accounts/${encodeURIComponent(this.accountId)}/d1/database/${encodeURIComponent(e)}/query`;return(await this.request(`POST`,n,{body:JSON.stringify({sql:t})})).result??[]}async uploadNamespacedScript(e,t,n){let r=`/accounts/${this.accountId}/workers/dispatch/namespaces/${encodeURIComponent(e)}/scripts/${encodeURIComponent(t)}`,i={main_module:n.mainModule,compatibility_date:n.compatibilityDate,compatibility_flags:n.compatibilityFlags??[],bindings:n.bindings??[],tags:n.tags},a=new FormData;a.append(`metadata`,new Blob([JSON.stringify(i)],{type:`application/json`})),a.append(n.mainModule,new Blob([n.script],{type:`application/javascript+module`}),n.mainModule),await this.request(`PUT`,r,{body:a})}async deleteNamespacedScript(e,t){let n=`/accounts/${this.accountId}/workers/dispatch/namespaces/${encodeURIComponent(e)}/scripts/${encodeURIComponent(t)}`;await this.request(`DELETE`,n)}async setNamespacedScriptSecret(e,t,n,r){let i=`/accounts/${this.accountId}/workers/dispatch/namespaces/${encodeURIComponent(e)}/scripts/${encodeURIComponent(t)}/secrets`;await this.request(`PUT`,i,{body:JSON.stringify({name:n,text:r,type:`secret_text`})})}async request(e,t,n){let r=`${this.baseUrl}${t}`,i=new AbortController,a=setTimeout(()=>i.abort(),this.timeoutMs),o={Authorization:`Bearer ${this.apiToken}`,...n?.headers??{}};n?.body&&!(n.body instanceof FormData)&&!o[`Content-Type`]&&(o[`Content-Type`]=`application/json`);let s;try{s=await this.fetchImpl(r,{method:e,headers:o,body:n?.body,signal:i.signal})}finally{clearTimeout(a)}let c=await s.text();if(!s.ok){let e=[];try{let t=JSON.parse(c);Array.isArray(t.errors)&&(e=t.errors)}catch{}throw new Rh(s.status,t,c,e)}if(!(c===``||s.status===204))return JSON.parse(c)}},Vh=`{tenant_id}`,Hh=`tenant-{tenant_id}`,Uh=`index.js`,Wh=`2026-05-01`;function Gh(e,t){return e.replace(/\{tenant_id\}/g,t)}function Kh(e){if(!(e instanceof Rh))return!1;let t=e.body.toLowerCase();return e.status===409||t.includes(`already exists`)||t.includes(`name is already taken`)||t.includes(`already in use`)}function qh(e){return e instanceof Rh?e.status===404:!1}function Jh(e){let t=new Bh({accountId:e.accountId,apiToken:e.apiToken,fetch:e.fetch,timeoutMs:e.timeoutMs}),n=e.scriptNameTemplate??Vh,r=e.d1NameTemplate??Hh,i=e.scriptMetadata?.main_module??Uh,a=e.scriptMetadata?.compatibility_date??Wh,o=e.scriptMetadata?.compatibility_flags??[`nodejs_compat`],s=e.dispatchNamespace;async function c(e){let n=(await t.listD1Databases(e)).find(t=>t.name===e);if(n)return n.uuid;try{return(await t.createD1Database(e)).uuid}catch(n){if(!Kh(n))throw n;let r=(await t.listD1Databases(e)).find(t=>t.name===e);if(r)return r.uuid;throw n}}async function l(n){for(let r of e.migrations)try{await t.execD1(n,r.sql)}catch(e){throw e instanceof Error?Error(`Failed to apply migration "${r.name}" to D1 ${n}: ${e.message}`,{cause:e}):e}}async function u(n,r){let c=[{type:`d1`,name:`AUTH_DB`,id:r},{type:`plain_text`,name:`CONTROL_PLANE_BASE_URL`,text:e.controlPlaneBaseUrl}];await t.uploadNamespacedScript(s,n,{script:e.tenantWorkerScript,mainModule:i,compatibilityDate:a,compatibilityFlags:o,bindings:c,tags:[`authhero-tenant`,`tenant:${n}`]})}async function d(n,r){let i=await e.secrets(r);for(let[e,r]of Object.entries(i))await t.setNamespacedScriptSecret(s,n,e,r)}return{async onProvision(e){let t=Gh(n,e),i=Gh(r,e),a=await c(i);return await l(a),await u(t,a),await d(t,e),{d1DatabaseId:a,scriptName:t,d1Name:i}},async onDeprovision(e){let i=Gh(n,e),a=Gh(r,e);try{await t.deleteNamespacedScript(s,i)}catch(e){if(!qh(e))throw e}let o=(await t.listD1Databases(a)).find(e=>e.name===a);if(o)try{await t.deleteD1Database(o.uuid)}catch(e){if(!qh(e))throw e}}}}function Yh(e){return e.deployment_type===`wfp`}function Xh(e){let{provisioner:t,tenants:n}=e,r=e.shouldProvision??Yh,i=e.logger;async function a(e,t){let r=t instanceof Error?t.message:String(t);try{await n.update(e,{provisioning_state:`failed`,provisioning_error:r.slice(0,2048),provisioning_state_changed_at:new Date().toISOString()})}catch(t){i?.warn(`Failed to write provisioning_state="failed" for tenant ${e}:`,t)}}return{async onProvision(e){let i=await n.get(e);if(i&&r(i))try{let r=await t.onProvision(e);await n.update(e,{d1_database_id:r.d1DatabaseId,worker_script_name:r.scriptName,provisioning_state:`ready`,provisioning_error:void 0,provisioning_state_changed_at:new Date().toISOString()})}catch(t){throw await a(e,t),t}},async onDeprovision(e){let i=await n.get(e);i&&!r(i)||await t.onDeprovision(e)}}}function Zh(e){let t={customDomains:Dm(e),cache:km({...e.cacheName&&{cacheName:e.cacheName},...e.defaultTtlSeconds!==void 0&&{defaultTtlSeconds:e.defaultTtlSeconds},...e.keyPrefix&&{keyPrefix:e.keyPrefix}}),geo:Dh()};e.r2SqlLogs?t.logs=Um(e.r2SqlLogs):e.analyticsEngineLogs&&(t.logs=mh(e.analyticsEngineLogs)),e.analyticsEngineLogs&&(t.analytics=ph(e.analyticsEngineLogs)),e.analyticsEngineActionExecutions&&(t.actionExecutions=Eh(e.analyticsEngineActionExecutions));let n=kh(e.rateLimitBindings);return n&&(t.rateLimit=n),t}exports.CloudflareApiClient=Bh,exports.CloudflareApiError=Rh,exports.CloudflareCodeExecutor=Mh,exports.DispatchNamespaceCodeExecutor=jh,exports.WorkerLoaderCodeExecutor=Lh,exports.createAnalyticsEngineActionExecutionsAdapter=Eh,exports.createAnalyticsEngineAnalyticsAdapter=ph,exports.createAnalyticsEngineLogsAdapter=mh,exports.createAnalyticsEngineStatsAdapter=ih,exports.createCloudflareRateLimitAdapter=kh,exports.createCloudflareWfpD1Provisioner=Jh,exports.createR2SQLLogsAdapter=Um,exports.createR2SQLStatsAdapter=Hm,exports.createWfpTenantProvisioningHook=Xh,exports.default=Zh,exports.generateWorkerScript=Ah;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StatsAdapter, LogsDataAdapter, AnalyticsAdapter, ActionExecutionsAdapter, RateLimitScope, RateLimitAdapter, CustomDomainsAdapter, CodeExecutor, CodeExecutionResult, CacheAdapter, GeoAdapter } from '@authhero/adapter-interfaces';
|
|
1
|
+
import { StatsAdapter, LogsDataAdapter, AnalyticsAdapter, ActionExecutionsAdapter, RateLimitScope, RateLimitAdapter, CustomDomainsAdapter, CodeExecutor, CodeExecutionResult, TenantsDataAdapter, CacheAdapter, GeoAdapter } from '@authhero/adapter-interfaces';
|
|
2
2
|
|
|
3
3
|
interface R2SQLLogsAdapterConfig {
|
|
4
4
|
/**
|
|
@@ -508,6 +508,345 @@ declare class WorkerLoaderCodeExecutor implements CodeExecutor {
|
|
|
508
508
|
}): Promise<CodeExecutionResult>;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Public types for the Workers-for-Platforms + D1 tenant provisioner.
|
|
513
|
+
*
|
|
514
|
+
* Wire-up shape — the operator constructs a provisioner once at control-plane
|
|
515
|
+
* boot and passes its `onProvision` / `onDeprovision` to
|
|
516
|
+
* `@authhero/multi-tenancy`'s `databaseIsolation` config. Every tenant
|
|
517
|
+
* create/delete in the management API then drives a CF API call sequence
|
|
518
|
+
* that:
|
|
519
|
+
* 1. (create) provisions a per-tenant D1, applies migrations, deploys a
|
|
520
|
+
* namespaced worker bound to that D1, and uploads secrets to it.
|
|
521
|
+
* 2. (delete) removes the namespaced worker and its D1.
|
|
522
|
+
*
|
|
523
|
+
* Failures throw — the multi-tenancy hook wraps `onProvision` such that a
|
|
524
|
+
* thrown error rolls the tenant row back. Idempotency on retry is best-effort:
|
|
525
|
+
* each step checks for "already exists" and continues, but the operator
|
|
526
|
+
* should treat the provisioning sequence as restartable rather than
|
|
527
|
+
* transactional.
|
|
528
|
+
*/
|
|
529
|
+
/**
|
|
530
|
+
* SQL migration to run on every new tenant D1, in order.
|
|
531
|
+
*
|
|
532
|
+
* Operators bundle these from `@authhero/drizzle`'s `drizzle/sqlite/` files
|
|
533
|
+
* via their build tool of choice (vite's `?raw`, esbuild loader, webpack's
|
|
534
|
+
* raw-loader, etc.) — the provisioner is agnostic about how the SQL gets
|
|
535
|
+
* loaded.
|
|
536
|
+
*/
|
|
537
|
+
interface ProvisionerMigration {
|
|
538
|
+
/** Filename, used only for error messages and audit logging. */
|
|
539
|
+
name: string;
|
|
540
|
+
/** Full SQL text of the migration. May contain multiple statements. */
|
|
541
|
+
sql: string;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Resolver that returns the secret values to upload onto a newly-provisioned
|
|
545
|
+
* tenant worker. Called once per tenant during `onProvision`.
|
|
546
|
+
*
|
|
547
|
+
* Implementations typically pull from a secret store (Vault, GCP Secret
|
|
548
|
+
* Manager, etc.) — the values must match the control-plane authhero's
|
|
549
|
+
* expectations for that tenant (notably `ENCRYPTION_KEY` and JWT signing
|
|
550
|
+
* material, which must be byte-stable to keep encrypted-at-rest data and
|
|
551
|
+
* issued JWTs valid).
|
|
552
|
+
*/
|
|
553
|
+
type TenantSecretsResolver = (tenantId: string) => Promise<Record<string, string>>;
|
|
554
|
+
interface CloudflareWfpD1ProvisionerOptions {
|
|
555
|
+
/** Cloudflare account id that owns the namespace, D1s, and tenant workers. */
|
|
556
|
+
accountId: string;
|
|
557
|
+
/**
|
|
558
|
+
* API token with at least these permissions on `accountId`:
|
|
559
|
+
* - Workers Scripts:Edit
|
|
560
|
+
* - D1:Edit
|
|
561
|
+
* - Workers for Platforms:Edit (for namespace ops)
|
|
562
|
+
*/
|
|
563
|
+
apiToken: string;
|
|
564
|
+
/** Name of the dispatch namespace tenant workers are deployed into. */
|
|
565
|
+
dispatchNamespace: string;
|
|
566
|
+
/**
|
|
567
|
+
* Base URL of the control-plane authhero. Passed to the tenant worker via
|
|
568
|
+
* the `CONTROL_PLANE_BASE_URL` env var so its `controlPlaneSync` destination
|
|
569
|
+
* knows where to POST `controlplane.sync.*` events.
|
|
570
|
+
*/
|
|
571
|
+
controlPlaneBaseUrl: string;
|
|
572
|
+
/**
|
|
573
|
+
* Full JavaScript bundle of the tenant worker. The operator builds this
|
|
574
|
+
* (typically via esbuild/vite of a thin wrapper that calls
|
|
575
|
+
* `authhero.init({ ... })`), and passes the resulting JS string here.
|
|
576
|
+
*
|
|
577
|
+
* The bundle MUST be self-contained — Cloudflare's script upload doesn't
|
|
578
|
+
* resolve npm dependencies. Use your bundler's `format: 'esm'` + `external`
|
|
579
|
+
* lists to inline `authhero`, `@authhero/drizzle`, and friends.
|
|
580
|
+
*/
|
|
581
|
+
tenantWorkerScript: string;
|
|
582
|
+
/**
|
|
583
|
+
* Optional script metadata override. Defaults to
|
|
584
|
+
* `{ main_module: "index.js", compatibility_date, compatibility_flags: ["nodejs_compat"] }`.
|
|
585
|
+
* Set `compatibility_date` to the same date the rest of your workers use.
|
|
586
|
+
*/
|
|
587
|
+
scriptMetadata?: {
|
|
588
|
+
main_module?: string;
|
|
589
|
+
compatibility_date?: string;
|
|
590
|
+
compatibility_flags?: string[];
|
|
591
|
+
};
|
|
592
|
+
/**
|
|
593
|
+
* SQL migrations applied in array order to every new tenant D1. Typically
|
|
594
|
+
* loaded from `@authhero/drizzle`'s shipped migrations via your build tool.
|
|
595
|
+
*/
|
|
596
|
+
migrations: ProvisionerMigration[];
|
|
597
|
+
/**
|
|
598
|
+
* Resolver that returns the secret values to set on the tenant worker.
|
|
599
|
+
* Called once per `onProvision`. The provisioner uploads each entry via
|
|
600
|
+
* the per-script secrets API.
|
|
601
|
+
*/
|
|
602
|
+
secrets: TenantSecretsResolver;
|
|
603
|
+
/**
|
|
604
|
+
* Naming convention for the namespaced script. Supports `{tenant_id}`
|
|
605
|
+
* placeholder. Defaults to `"{tenant_id}"`.
|
|
606
|
+
*
|
|
607
|
+
* Must match whatever the dispatcher synthesizes as `script_name` in its
|
|
608
|
+
* `dispatch_namespace` handler — otherwise the dispatcher can't reach the
|
|
609
|
+
* worker after provisioning.
|
|
610
|
+
*/
|
|
611
|
+
scriptNameTemplate?: string;
|
|
612
|
+
/**
|
|
613
|
+
* Naming convention for the per-tenant D1. Supports `{tenant_id}`.
|
|
614
|
+
* Defaults to `"tenant-{tenant_id}"`. CF accepts most ASCII names; keep
|
|
615
|
+
* it stable so a re-provision finds the existing D1.
|
|
616
|
+
*/
|
|
617
|
+
d1NameTemplate?: string;
|
|
618
|
+
/**
|
|
619
|
+
* Fetch override (tests only). Defaults to global `fetch`.
|
|
620
|
+
*/
|
|
621
|
+
fetch?: typeof fetch;
|
|
622
|
+
/**
|
|
623
|
+
* Per-request timeout (ms) on the CF API. Defaults to 30s. Individual
|
|
624
|
+
* D1 migrations can take a few seconds each; the upload of a multi-MB
|
|
625
|
+
* tenant bundle also takes a noticeable chunk of that.
|
|
626
|
+
*/
|
|
627
|
+
timeoutMs?: number;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Outcome of a successful `onProvision` — returned so the caller can persist
|
|
631
|
+
* the resource IDs back onto the tenant row (`tenants.d1_database_id`,
|
|
632
|
+
* `tenants.worker_script_name`). The control-plane authhero ships these
|
|
633
|
+
* fields in its schema; `createWfpTenantProvisioningHook` writes them
|
|
634
|
+
* automatically.
|
|
635
|
+
*/
|
|
636
|
+
interface ProvisionResult {
|
|
637
|
+
d1DatabaseId: string;
|
|
638
|
+
scriptName: string;
|
|
639
|
+
d1Name: string;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* What `createCloudflareWfpD1Provisioner` returns — the two lifecycle
|
|
643
|
+
* callbacks that plug into `databaseIsolation` from `@authhero/multi-tenancy`
|
|
644
|
+
* via the `createWfpTenantProvisioningHook` wrapper (which handles
|
|
645
|
+
* deployment-type guarding and tenant-row writebacks).
|
|
646
|
+
*/
|
|
647
|
+
interface CloudflareWfpD1Provisioner {
|
|
648
|
+
onProvision(tenantId: string): Promise<ProvisionResult>;
|
|
649
|
+
onDeprovision(tenantId: string): Promise<void>;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Construct the lifecycle hooks for provisioning + deprovisioning a tenant
|
|
654
|
+
* on Cloudflare Workers-for-Platforms backed by a per-tenant D1.
|
|
655
|
+
*
|
|
656
|
+
* Wiring on the control-plane authhero:
|
|
657
|
+
*
|
|
658
|
+
* ```ts
|
|
659
|
+
* import createAdapters from "@authhero/cloudflare-adapter";
|
|
660
|
+
* import { createCloudflareWfpD1Provisioner } from "@authhero/cloudflare-adapter";
|
|
661
|
+
* import { initMultiTenant } from "@authhero/multi-tenancy";
|
|
662
|
+
* import tenantWorkerScript from "./tenant-worker.dist.js?raw";
|
|
663
|
+
* import migration0001 from "@authhero/drizzle/drizzle/sqlite/0000_initial.sql?raw";
|
|
664
|
+
*
|
|
665
|
+
* const provisioner = createCloudflareWfpD1Provisioner({
|
|
666
|
+
* accountId: env.CLOUDFLARE_ACCOUNT_ID,
|
|
667
|
+
* apiToken: env.CLOUDFLARE_API_TOKEN,
|
|
668
|
+
* dispatchNamespace: "authhero-tenants",
|
|
669
|
+
* controlPlaneBaseUrl: env.PUBLIC_BASE_URL,
|
|
670
|
+
* tenantWorkerScript,
|
|
671
|
+
* migrations: [{ name: "0000_initial.sql", sql: migration0001 }],
|
|
672
|
+
* secrets: async (tenantId) => ({
|
|
673
|
+
* ENCRYPTION_KEY: env.SHARED_ENCRYPTION_KEY,
|
|
674
|
+
* ISSUER: `https://${tenantId}.tokens.example.com`,
|
|
675
|
+
* }),
|
|
676
|
+
* });
|
|
677
|
+
*
|
|
678
|
+
* const { app } = initMultiTenant({
|
|
679
|
+
* dataAdapter,
|
|
680
|
+
* controlPlane: { tenantId: "main", clientId: "platform" },
|
|
681
|
+
* databaseIsolation: {
|
|
682
|
+
* getAdapters: async (tenantId) => { ... }, // resolve per-tenant adapter
|
|
683
|
+
* onProvision: provisioner.onProvision,
|
|
684
|
+
* onDeprovision: provisioner.onDeprovision,
|
|
685
|
+
* },
|
|
686
|
+
* });
|
|
687
|
+
* ```
|
|
688
|
+
*
|
|
689
|
+
* On tenant create, the management API row write fires
|
|
690
|
+
* `databaseIsolation.onProvision(tenantId)` which runs the full sequence
|
|
691
|
+
* below. If any step throws, the upstream `createProvisioningHooks` rolls
|
|
692
|
+
* back the tenant row — though side effects already taken (D1 created,
|
|
693
|
+
* partial migrations applied) are NOT rolled back. The operator should
|
|
694
|
+
* treat re-running `onProvision(tenantId)` as safe; each step is idempotent
|
|
695
|
+
* on "already exists".
|
|
696
|
+
*/
|
|
697
|
+
declare function createCloudflareWfpD1Provisioner(options: CloudflareWfpD1ProvisionerOptions): CloudflareWfpD1Provisioner;
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Adapt the provisioner to `@authhero/multi-tenancy`'s
|
|
701
|
+
* `databaseIsolation.onProvision` / `onDeprovision` contract by:
|
|
702
|
+
*
|
|
703
|
+
* 1. Looking up the tenant row first and gating on
|
|
704
|
+
* `tenant.deployment_type === "wfp"` — shared tenants short-circuit so
|
|
705
|
+
* the same control plane can host both kinds without code branches.
|
|
706
|
+
* 2. Running the provisioner sequence (D1 + script + secrets).
|
|
707
|
+
* 3. Writing the resulting `d1_database_id` + `worker_script_name` +
|
|
708
|
+
* `provisioning_state` back onto the tenant row so the admin UI can
|
|
709
|
+
* show real status, and so a redeploy / re-provision knows which
|
|
710
|
+
* resource ids to operate on.
|
|
711
|
+
* 4. On failure, marking `provisioning_state = "failed"` with the error
|
|
712
|
+
* message, then re-throwing — the multi-tenancy hook treats the throw
|
|
713
|
+
* as a signal to roll back the tenant row.
|
|
714
|
+
*
|
|
715
|
+
* Typical wiring on the control-plane authhero:
|
|
716
|
+
*
|
|
717
|
+
* ```ts
|
|
718
|
+
* import { initMultiTenant } from "@authhero/multi-tenancy";
|
|
719
|
+
* import {
|
|
720
|
+
* createCloudflareWfpD1Provisioner,
|
|
721
|
+
* createWfpTenantProvisioningHook,
|
|
722
|
+
* } from "@authhero/cloudflare-adapter";
|
|
723
|
+
*
|
|
724
|
+
* const provisioner = createCloudflareWfpD1Provisioner({ ... });
|
|
725
|
+
* const hook = createWfpTenantProvisioningHook({
|
|
726
|
+
* provisioner,
|
|
727
|
+
* tenants: dataAdapter.tenants,
|
|
728
|
+
* });
|
|
729
|
+
*
|
|
730
|
+
* const { app } = initMultiTenant({
|
|
731
|
+
* dataAdapter,
|
|
732
|
+
* databaseIsolation: {
|
|
733
|
+
* getAdapters: async (tenantId) => { ... },
|
|
734
|
+
* onProvision: hook.onProvision,
|
|
735
|
+
* onDeprovision: hook.onDeprovision,
|
|
736
|
+
* },
|
|
737
|
+
* });
|
|
738
|
+
* ```
|
|
739
|
+
*/
|
|
740
|
+
interface WfpTenantProvisioningHookOptions {
|
|
741
|
+
provisioner: CloudflareWfpD1Provisioner;
|
|
742
|
+
tenants: TenantsDataAdapter;
|
|
743
|
+
/**
|
|
744
|
+
* Optional override of "should this tenant be WFP-provisioned?". Defaults
|
|
745
|
+
* to `tenant.deployment_type === "wfp"`. Provide a custom predicate when
|
|
746
|
+
* the gating signal lives elsewhere (a feature flag, a config table, etc.).
|
|
747
|
+
*/
|
|
748
|
+
shouldProvision?: (tenant: {
|
|
749
|
+
id: string;
|
|
750
|
+
deployment_type?: string;
|
|
751
|
+
storage_kind?: string;
|
|
752
|
+
}) => boolean;
|
|
753
|
+
/**
|
|
754
|
+
* Optional `console`-compatible logger for warnings emitted when the
|
|
755
|
+
* tenant row write-back fails after a successful provision. Defaults to
|
|
756
|
+
* a silent no-op so this module stays test-quiet.
|
|
757
|
+
*/
|
|
758
|
+
logger?: Pick<Console, "warn">;
|
|
759
|
+
}
|
|
760
|
+
interface WfpTenantProvisioningHook {
|
|
761
|
+
onProvision(tenantId: string): Promise<void>;
|
|
762
|
+
onDeprovision(tenantId: string): Promise<void>;
|
|
763
|
+
}
|
|
764
|
+
declare function createWfpTenantProvisioningHook(options: WfpTenantProvisioningHookOptions): WfpTenantProvisioningHook;
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Thin Cloudflare REST API client for the WFP+D1 provisioner.
|
|
768
|
+
*
|
|
769
|
+
* Each method maps 1:1 to a documented CF endpoint and returns the parsed
|
|
770
|
+
* response body. Errors surface as `CloudflareApiError` carrying the HTTP
|
|
771
|
+
* status, endpoint, and (when JSON) the CF error array — making them easy
|
|
772
|
+
* to log without re-fetching the response.
|
|
773
|
+
*
|
|
774
|
+
* Idempotency is the caller's responsibility — the provisioner sequences
|
|
775
|
+
* calls and tolerates "already exists" / "not found" depending on the
|
|
776
|
+
* operation (see `provisioner.ts`).
|
|
777
|
+
*/
|
|
778
|
+
declare class CloudflareApiError extends Error {
|
|
779
|
+
readonly status: number;
|
|
780
|
+
readonly endpoint: string;
|
|
781
|
+
readonly errors: unknown[];
|
|
782
|
+
readonly body: string;
|
|
783
|
+
constructor(status: number, endpoint: string, body: string, errors?: unknown[]);
|
|
784
|
+
}
|
|
785
|
+
interface CfApiClientOptions {
|
|
786
|
+
accountId: string;
|
|
787
|
+
apiToken: string;
|
|
788
|
+
fetch?: typeof fetch;
|
|
789
|
+
timeoutMs?: number;
|
|
790
|
+
baseUrl?: string;
|
|
791
|
+
}
|
|
792
|
+
interface D1Database {
|
|
793
|
+
uuid: string;
|
|
794
|
+
name: string;
|
|
795
|
+
}
|
|
796
|
+
interface D1QueryResult {
|
|
797
|
+
success: boolean;
|
|
798
|
+
meta?: Record<string, unknown>;
|
|
799
|
+
results?: unknown[];
|
|
800
|
+
}
|
|
801
|
+
interface ScriptBinding {
|
|
802
|
+
type: "d1" | "plain_text" | "secret_text";
|
|
803
|
+
name: string;
|
|
804
|
+
id?: string;
|
|
805
|
+
text?: string;
|
|
806
|
+
}
|
|
807
|
+
interface ScriptUploadOptions {
|
|
808
|
+
/** Script source (JavaScript ES module). */
|
|
809
|
+
script: string;
|
|
810
|
+
/** Main module filename (must match part name in form data). */
|
|
811
|
+
mainModule: string;
|
|
812
|
+
/** Compatibility date, ISO yyyy-mm-dd. */
|
|
813
|
+
compatibilityDate: string;
|
|
814
|
+
/** Compatibility flags (e.g. `["nodejs_compat"]`). */
|
|
815
|
+
compatibilityFlags?: string[];
|
|
816
|
+
/** Bindings to attach (D1, plain_text, etc.). Secrets go via setSecret(). */
|
|
817
|
+
bindings?: ScriptBinding[];
|
|
818
|
+
/** Optional tags, stored on the script for operator-side lookup. */
|
|
819
|
+
tags?: string[];
|
|
820
|
+
}
|
|
821
|
+
declare class CloudflareApiClient {
|
|
822
|
+
private readonly accountId;
|
|
823
|
+
private readonly apiToken;
|
|
824
|
+
private readonly fetchImpl;
|
|
825
|
+
private readonly timeoutMs;
|
|
826
|
+
private readonly baseUrl;
|
|
827
|
+
constructor(options: CfApiClientOptions);
|
|
828
|
+
createD1Database(name: string): Promise<D1Database>;
|
|
829
|
+
listD1Databases(name?: string): Promise<D1Database[]>;
|
|
830
|
+
deleteD1Database(databaseId: string): Promise<void>;
|
|
831
|
+
/**
|
|
832
|
+
* Execute a single SQL statement (or batch of `;`-separated statements
|
|
833
|
+
* permitted by D1) against the given database. Use for applying
|
|
834
|
+
* migrations one file at a time — the per-call response size cap means
|
|
835
|
+
* very large single calls can fail; splitting per file keeps each call
|
|
836
|
+
* bounded by the migration author.
|
|
837
|
+
*/
|
|
838
|
+
execD1(databaseId: string, sql: string): Promise<D1QueryResult[]>;
|
|
839
|
+
uploadNamespacedScript(namespace: string, scriptName: string, options: ScriptUploadOptions): Promise<void>;
|
|
840
|
+
deleteNamespacedScript(namespace: string, scriptName: string): Promise<void>;
|
|
841
|
+
/**
|
|
842
|
+
* Set a single secret on a namespaced script. The CF API replaces the
|
|
843
|
+
* value if a secret with that name already exists, so this is safely
|
|
844
|
+
* re-runnable.
|
|
845
|
+
*/
|
|
846
|
+
setNamespacedScriptSecret(namespace: string, scriptName: string, secretName: string, secretValue: string): Promise<void>;
|
|
847
|
+
private request;
|
|
848
|
+
}
|
|
849
|
+
|
|
511
850
|
interface CloudflareAdapters {
|
|
512
851
|
customDomains: CustomDomainsAdapter;
|
|
513
852
|
cache: CacheAdapter;
|
|
@@ -519,5 +858,5 @@ interface CloudflareAdapters {
|
|
|
519
858
|
}
|
|
520
859
|
declare function createAdapters(config: CloudflareConfig): CloudflareAdapters;
|
|
521
860
|
|
|
522
|
-
export { CloudflareCodeExecutor, DispatchNamespaceCodeExecutor, WorkerLoaderCodeExecutor, createAnalyticsEngineActionExecutionsAdapter, createAnalyticsEngineAnalyticsAdapter, createAnalyticsEngineLogsAdapter, createAnalyticsEngineStatsAdapter, createCloudflareRateLimitAdapter, createR2SQLLogsAdapter, createR2SQLStatsAdapter, createAdapters as default, generateWorkerScript };
|
|
523
|
-
export type { AnalyticsEngineActionExecutionsAdapterConfig, AnalyticsEngineDataset, AnalyticsEngineLogsAdapterConfig, CloudflareAdapters, CloudflareCodeExecutorConfig, CloudflareConfig, CloudflareRateLimitBinding, CloudflareRateLimitBindings, DispatchNamespace, DispatchNamespaceCodeExecutorConfig, R2SQLLogsAdapterConfig, WorkerCode, WorkerLoader, WorkerLoaderCodeExecutorOptions, WorkerStub };
|
|
861
|
+
export { CloudflareApiClient, CloudflareApiError, CloudflareCodeExecutor, DispatchNamespaceCodeExecutor, WorkerLoaderCodeExecutor, createAnalyticsEngineActionExecutionsAdapter, createAnalyticsEngineAnalyticsAdapter, createAnalyticsEngineLogsAdapter, createAnalyticsEngineStatsAdapter, createCloudflareRateLimitAdapter, createCloudflareWfpD1Provisioner, createR2SQLLogsAdapter, createR2SQLStatsAdapter, createWfpTenantProvisioningHook, createAdapters as default, generateWorkerScript };
|
|
862
|
+
export type { AnalyticsEngineActionExecutionsAdapterConfig, AnalyticsEngineDataset, AnalyticsEngineLogsAdapterConfig, CfApiClientOptions, CloudflareAdapters, CloudflareCodeExecutorConfig, CloudflareConfig, CloudflareRateLimitBinding, CloudflareRateLimitBindings, CloudflareWfpD1Provisioner, CloudflareWfpD1ProvisionerOptions, D1Database, D1QueryResult, DispatchNamespace, DispatchNamespaceCodeExecutorConfig, ProvisionResult, ProvisionerMigration, R2SQLLogsAdapterConfig, ScriptBinding, ScriptUploadOptions, TenantSecretsResolver, WfpTenantProvisioningHook, WfpTenantProvisioningHookOptions, WorkerCode, WorkerLoader, WorkerLoaderCodeExecutorOptions, WorkerStub };
|