@authhero/cloudflare-adapter 2.35.2 → 2.36.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,5 +1,5 @@
1
1
  import type { DataAdapters } from "@authhero/adapter-interfaces";
2
- import { type DefaultsPayloadEntities } from "@authhero/multi-tenancy";
2
+ import { type ControlPlaneDefaultsApplyResult, type DefaultsPayloadEntities } from "@authhero/multi-tenancy";
3
3
  import type { DispatchNamespace } from "../code-executor";
4
4
  export interface DispatchSyncDefaultsOptions {
5
5
  /** The dispatch namespace binding the tenant workers live in. */
@@ -29,5 +29,10 @@ export interface DispatchSyncDefaultsOptions {
29
29
  * `/internal/sync-defaults` route (which applies it). The tenant worker never
30
30
  * calls back to the control plane. Use the returned function as the
31
31
  * provision-time seed and for on-change / rotation re-syncs.
32
+ *
33
+ * Resolves with the tenant worker's {@link ControlPlaneDefaultsApplyResult} so
34
+ * the caller can act on what actually landed — warn when an entity's `received`
35
+ * is `0`, surface per-entity `errors`, or assert the signing keys projected.
36
+ * Rejects (does not resolve) when the push fails or the worker returns non-2xx.
32
37
  */
33
- export declare function createDispatchSyncDefaults(options: DispatchSyncDefaultsOptions): (tenantId: string) => Promise<void>;
38
+ export declare function createDispatchSyncDefaults(options: DispatchSyncDefaultsOptions): (tenantId: string) => Promise<ControlPlaneDefaultsApplyResult>;
@@ -1,5 +1,6 @@
1
1
  import { type ExecutionContext } from "hono";
2
- import { type AuthHeroConfig, type DataAdapters } from "authhero";
2
+ import { type AuthHeroConfig, type DataAdapters, type IssuerResolver } from "authhero";
3
+ import { type ControlPlaneDefaultsApplyResult } from "@authhero/multi-tenancy";
3
4
  /**
4
5
  * Env contract for a WFP tenant worker — the bindings + secrets
5
6
  * `createCloudflareWfpD1Provisioner` (and the host's `secrets` resolver) set on
@@ -45,6 +46,31 @@ export interface WfpTenantAppOptions<Env extends WfpTenantEnv = WfpTenantEnv> {
45
46
  * scaffold's base config and the env.
46
47
  */
47
48
  configure?: (base: AuthHeroConfig, env: Env) => AuthHeroConfig;
49
+ /**
50
+ * Resolver for the issuers accepted *in addition to* this tenant's own
51
+ * `ISSUER`. Defaults to a **gated** resolver that accepts
52
+ * `env.CONTROL_PLANE_ISSUER` **only** for control-plane-minted tokens
53
+ * (`tenant_id === env.CONTROL_PLANE_TENANT_ID`) and `[]` otherwise — so a
54
+ * tenant token can never assert the control-plane issuer. Provide your own
55
+ * resolver to widen or narrow this; it fully replaces the default.
56
+ */
57
+ additionalIssuers?: (env: Env) => IssuerResolver;
58
+ /**
59
+ * When applying a `/internal/sync-defaults` push, keep going past a failing
60
+ * row and collect per-entity errors in the result rather than aborting the
61
+ * whole projection on the first failure. Defaults to `true` — a single bad
62
+ * inherited row shouldn't sink the entire sync into an opaque 500. Set
63
+ * `false` to restore fail-fast.
64
+ */
65
+ continueOnError?: boolean;
66
+ /**
67
+ * Called after a `/internal/sync-defaults` push is applied, with the result
68
+ * that is also returned to the control plane. Use it to log what landed
69
+ * (per-entity `received` / `upserted` / `errors`) without re-implementing the
70
+ * route. A throw here is caught and turns into a structured 500 — the apply
71
+ * has already committed, so treat it as best-effort logging.
72
+ */
73
+ onSyncResult?: (result: ControlPlaneDefaultsApplyResult, env: Env) => void | Promise<void>;
48
74
  }
49
75
  /**
50
76
  * Builds a WFP tenant worker `{ fetch }` handler: key-ring encryption over the
@@ -60,6 +60,23 @@ export interface WfpTenantProvisioningHookOptions {
60
60
  * a silent no-op so this module stays test-quiet.
61
61
  */
62
62
  logger?: Pick<Console, "warn">;
63
+ /**
64
+ * Optional post-provision seed run *after* the CF resources exist but
65
+ * *before* the tenant is marked `ready` — typically
66
+ * `createDispatchSyncDefaults(...)`, which projects the control plane's
67
+ * defaults (and the FK-target tenant rows) into the fresh D1.
68
+ *
69
+ * Folding the seed into provisioning closes the "ready but empty D1" gap: a
70
+ * best-effort seed fired separately after this hook can be lost, leaving a
71
+ * tenant marked `ready` whose D1 has no tenant row and no projected defaults.
72
+ * Here, if the seed throws — or resolves with collected per-entity errors
73
+ * (the seed applies with `continueOnError`, so failures surface in the
74
+ * result rather than as a rejection) — the tenant is marked `failed`
75
+ * (resource ids are still persisted so a re-provision can find them) and the
76
+ * error is re-thrown. Leave unset to mark `ready` as soon as the resources
77
+ * exist.
78
+ */
79
+ syncDefaults?: (tenantId: string) => Promise<unknown>;
63
80
  }
64
81
  export interface WfpTenantProvisioningHook {
65
82
  onProvision(tenantId: string): Promise<void>;
@@ -127,6 +127,12 @@ export interface CloudflareWfpD1ProvisionerOptions {
127
127
  * tenant bundle also takes a noticeable chunk of that.
128
128
  */
129
129
  timeoutMs?: number;
130
+ /**
131
+ * Optional `console`-compatible logger. Used to note non-fatal recovery
132
+ * decisions — e.g. skipping migrations while re-provisioning to heal an
133
+ * orphaned worker over an existing D1. Defaults to silent.
134
+ */
135
+ logger?: Pick<Console, "warn">;
130
136
  }
131
137
  /**
132
138
  * Outcome of a successful `onProvision` — returned so the caller can persist
package/dist/wfp.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@authhero/multi-tenancy"),t=require("hono"),n=require("authhero");var r=`tenant-{tenant_id}-auth`,i=3e4,a=`/internal/sync-defaults`;function o(e,t){return e.replace(/\{tenant_id\}/g,t)}function s(t){let{dispatcher:n,scriptNameTemplate:s=r,internalSecret:c,controlPlaneTenantId:l,controlPlaneAdapters:u,entities:d,timeoutMs:f=i}=t;return async t=>{let r=await(0,e.buildControlPlaneDefaultsPayload)(u,l,d,t),i=o(s,t),p=new AbortController,m=setTimeout(()=>p.abort(),f),h;try{h=await n.get(i).fetch(`https://tenant.internal${a}`,{method:`POST`,headers:{"content-type":`application/json`,authorization:`Bearer ${c}`},body:JSON.stringify(r),signal:p.signal})}finally{clearTimeout(m)}if(!h.ok){let e=await h.text().catch(()=>``);throw Error(`sync-defaults push to "${i}" failed: ${h.status} ${e.slice(0,256)}`)}}}var c=`cp`,l=`/internal/sync-defaults`;async function u(r,i){let a=r.CONTROL_PLANE_TENANT_ID,o=await(0,n.loadEncryptionKey)(r.ENCRYPTION_KEY),s=r.CONTROL_PLANE_ENCRYPTION_KEY?{default:o,keys:{[c]:await(0,n.loadEncryptionKey)(r.CONTROL_PLANE_ENCRYPTION_KEY)}}:{default:o},u=(0,n.createEncryptedDataAdapterWithKeyRing)(i.createDataAdapter(r),s,{resolveEncryptKeyId:e=>e===a?c:void 0}),d={dataAdapter:(0,e.withRuntimeFallback)(u,{controlPlaneTenantId:a}),additionalIssuers:()=>[r.CONTROL_PLANE_ISSUER]},{app:f}=(0,n.init)(i.configure?i.configure(d,r):d),p=new t.Hono;return p.post(l,async t=>{let n=r.WFP_INTERNAL_SYNC_SECRET,i=t.req.header(`authorization`);if(!n||i!==`Bearer ${n}`)return t.json({error:`unauthorized`},401);let o;try{o=await t.req.json()}catch{return t.json({error:`invalid JSON`},400)}let s=await(0,e.applyControlPlaneDefaultsPayload)(o,u,a);return t.json(s)}),p.route(`/`,f),p}function d(e){let t=new WeakMap;return{fetch(n,r,i){let a=t.get(r);return a||(a=u(r,e),t.set(r,a)),a.then(e=>e.fetch(n,r,i))}}}exports.createDispatchSyncDefaults=s,exports.createWfpTenantApp=d;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@authhero/multi-tenancy"),t=require("hono"),n=require("authhero");var r=`tenant-{tenant_id}-auth`,i=3e4,a=`/internal/sync-defaults`;function o(e,t){return e.replace(/\{tenant_id\}/g,t)}function s(t){let{dispatcher:n,scriptNameTemplate:s=r,internalSecret:c,controlPlaneTenantId:l,controlPlaneAdapters:u,entities:d,timeoutMs:f=i}=t;return async t=>{let r=await(0,e.buildControlPlaneDefaultsPayload)(u,l,d,t),i=o(s,t),p=new AbortController,m=setTimeout(()=>p.abort(),f),h;try{h=await n.get(i).fetch(`https://tenant.internal${a}`,{method:`POST`,headers:{"content-type":`application/json`,authorization:`Bearer ${c}`},body:JSON.stringify(r),signal:p.signal})}finally{clearTimeout(m)}if(!h.ok){let e=await h.text().catch(()=>``),t=h.headers.get(`x-authhero-error`);throw Error(`sync-defaults push to "${i}" failed: ${h.status}${t?` (${t})`:``} ${e.slice(0,256)}`)}return await h.json()}}var c=`cp`,l=`/internal/sync-defaults`;function u(e,t,n,r){console.error(`[wfp-tenant] ${t}:`,n);let i=n instanceof Error?n.message:String(n);return e.header(`X-Authhero-Error`,t),e.json({error:t,detail:i,result:r},500)}function d(e){return({tenant_id:t})=>t===e.CONTROL_PLANE_TENANT_ID?[e.CONTROL_PLANE_ISSUER]:[]}async function f(r,i){let a=r.CONTROL_PLANE_TENANT_ID,o=await(0,n.loadEncryptionKey)(r.ENCRYPTION_KEY),s=r.CONTROL_PLANE_ENCRYPTION_KEY?{default:o,keys:{[c]:await(0,n.loadEncryptionKey)(r.CONTROL_PLANE_ENCRYPTION_KEY)}}:{default:o},f=(0,n.createEncryptedDataAdapterWithKeyRing)(i.createDataAdapter(r),s,{resolveEncryptKeyId:e=>e===a?c:void 0}),p={dataAdapter:(0,e.withRuntimeFallback)(f,{controlPlaneTenantId:a}),additionalIssuers:(i.additionalIssuers??d)(r)},{app:m}=(0,n.init)(i.configure?i.configure(p,r):p),h=new t.Hono;return h.post(l,async t=>{let n=r.WFP_INTERNAL_SYNC_SECRET,o=t.req.header(`authorization`);if(!n||o!==`Bearer ${n}`)return t.json({error:`unauthorized`},401);let s;try{s=await t.req.json()}catch{return t.json({error:`invalid JSON`},400)}let c;try{c=await(0,e.applyControlPlaneDefaultsPayload)(s,f,a,{continueOnError:i.continueOnError??!0})}catch(e){return u(t,`sync_defaults_apply_failed`,e)}if(i.onSyncResult)try{await i.onSyncResult(c,r)}catch(e){return u(t,`sync_defaults_on_result_failed`,e,c)}return t.json(c)}),h.route(`/`,m),h}function p(e){let t=new WeakMap;return{fetch(n,r,i){let a=t.get(r);return a||(a=f(r,e),t.set(r,a)),a.then(e=>e.fetch(n,r,i))}}}exports.createDispatchSyncDefaults=s,exports.createWfpTenantApp=p;
package/dist/wfp.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DataAdapters } from '@authhero/adapter-interfaces';
2
- import { DefaultsPayloadEntities } from '@authhero/multi-tenancy';
2
+ import { DefaultsPayloadEntities, ControlPlaneDefaultsApplyResult } from '@authhero/multi-tenancy';
3
3
  import { ExecutionContext } from 'hono';
4
- import { DataAdapters as DataAdapters$1, AuthHeroConfig } from 'authhero';
4
+ import { DataAdapters as DataAdapters$1, AuthHeroConfig, IssuerResolver } from 'authhero';
5
5
 
6
6
  /**
7
7
  * Cloudflare Workers for Platforms dispatch namespace binding type.
@@ -52,8 +52,13 @@ interface DispatchSyncDefaultsOptions {
52
52
  * `/internal/sync-defaults` route (which applies it). The tenant worker never
53
53
  * calls back to the control plane. Use the returned function as the
54
54
  * provision-time seed and for on-change / rotation re-syncs.
55
+ *
56
+ * Resolves with the tenant worker's {@link ControlPlaneDefaultsApplyResult} so
57
+ * the caller can act on what actually landed — warn when an entity's `received`
58
+ * is `0`, surface per-entity `errors`, or assert the signing keys projected.
59
+ * Rejects (does not resolve) when the push fails or the worker returns non-2xx.
55
60
  */
56
- declare function createDispatchSyncDefaults(options: DispatchSyncDefaultsOptions): (tenantId: string) => Promise<void>;
61
+ declare function createDispatchSyncDefaults(options: DispatchSyncDefaultsOptions): (tenantId: string) => Promise<ControlPlaneDefaultsApplyResult>;
57
62
 
58
63
  /**
59
64
  * Env contract for a WFP tenant worker — the bindings + secrets
@@ -100,6 +105,31 @@ interface WfpTenantAppOptions<Env extends WfpTenantEnv = WfpTenantEnv> {
100
105
  * scaffold's base config and the env.
101
106
  */
102
107
  configure?: (base: AuthHeroConfig, env: Env) => AuthHeroConfig;
108
+ /**
109
+ * Resolver for the issuers accepted *in addition to* this tenant's own
110
+ * `ISSUER`. Defaults to a **gated** resolver that accepts
111
+ * `env.CONTROL_PLANE_ISSUER` **only** for control-plane-minted tokens
112
+ * (`tenant_id === env.CONTROL_PLANE_TENANT_ID`) and `[]` otherwise — so a
113
+ * tenant token can never assert the control-plane issuer. Provide your own
114
+ * resolver to widen or narrow this; it fully replaces the default.
115
+ */
116
+ additionalIssuers?: (env: Env) => IssuerResolver;
117
+ /**
118
+ * When applying a `/internal/sync-defaults` push, keep going past a failing
119
+ * row and collect per-entity errors in the result rather than aborting the
120
+ * whole projection on the first failure. Defaults to `true` — a single bad
121
+ * inherited row shouldn't sink the entire sync into an opaque 500. Set
122
+ * `false` to restore fail-fast.
123
+ */
124
+ continueOnError?: boolean;
125
+ /**
126
+ * Called after a `/internal/sync-defaults` push is applied, with the result
127
+ * that is also returned to the control plane. Use it to log what landed
128
+ * (per-entity `received` / `upserted` / `errors`) without re-implementing the
129
+ * route. A throw here is caught and turns into a structured 500 — the apply
130
+ * has already committed, so treat it as best-effort logging.
131
+ */
132
+ onSyncResult?: (result: ControlPlaneDefaultsApplyResult, env: Env) => void | Promise<void>;
103
133
  }
104
134
  /**
105
135
  * Builds a WFP tenant worker `{ fetch }` handler: key-ring encryption over the
package/dist/wfp.mjs CHANGED
@@ -24,23 +24,36 @@ function d(e) {
24
24
  clearTimeout(m);
25
25
  }
26
26
  if (!h.ok) {
27
- let e = await h.text().catch(() => "");
28
- throw Error(`sync-defaults push to "${c}" failed: ${h.status} ${e.slice(0, 256)}`);
27
+ let e = await h.text().catch(() => ""), t = h.headers.get("x-authhero-error");
28
+ throw Error(`sync-defaults push to "${c}" failed: ${h.status}${t ? ` (${t})` : ""} ${e.slice(0, 256)}`);
29
29
  }
30
+ return await h.json();
30
31
  };
31
32
  }
32
33
  //#endregion
33
34
  //#region src/wfp/tenant-app.ts
34
35
  var f = "cp", p = "/internal/sync-defaults";
35
- async function m(t, s) {
36
+ function m(e, t, n, r) {
37
+ console.error(`[wfp-tenant] ${t}:`, n);
38
+ let i = n instanceof Error ? n.message : String(n);
39
+ return e.header("X-Authhero-Error", t), e.json({
40
+ error: t,
41
+ detail: i,
42
+ result: r
43
+ }, 500);
44
+ }
45
+ function h(e) {
46
+ return ({ tenant_id: t }) => t === e.CONTROL_PLANE_TENANT_ID ? [e.CONTROL_PLANE_ISSUER] : [];
47
+ }
48
+ async function g(t, s) {
36
49
  let c = t.CONTROL_PLANE_TENANT_ID, l = await o(t.ENCRYPTION_KEY), u = t.CONTROL_PLANE_ENCRYPTION_KEY ? {
37
50
  default: l,
38
51
  keys: { [f]: await o(t.CONTROL_PLANE_ENCRYPTION_KEY) }
39
- } : { default: l }, d = i(s.createDataAdapter(t), u, { resolveEncryptKeyId: (e) => e === c ? f : void 0 }), m = {
52
+ } : { default: l }, d = i(s.createDataAdapter(t), u, { resolveEncryptKeyId: (e) => e === c ? f : void 0 }), g = {
40
53
  dataAdapter: n(d, { controlPlaneTenantId: c }),
41
- additionalIssuers: () => [t.CONTROL_PLANE_ISSUER]
42
- }, { app: h } = a(s.configure ? s.configure(m, t) : m), g = new r();
43
- return g.post(p, async (n) => {
54
+ additionalIssuers: (s.additionalIssuers ?? h)(t)
55
+ }, { app: _ } = a(s.configure ? s.configure(g, t) : g), v = new r();
56
+ return v.post(p, async (n) => {
44
57
  let r = t.WFP_INTERNAL_SYNC_SECRET, i = n.req.header("authorization");
45
58
  if (!r || i !== `Bearer ${r}`) return n.json({ error: "unauthorized" }, 401);
46
59
  let a;
@@ -49,16 +62,26 @@ async function m(t, s) {
49
62
  } catch {
50
63
  return n.json({ error: "invalid JSON" }, 400);
51
64
  }
52
- let o = await e(a, d, c);
65
+ let o;
66
+ try {
67
+ o = await e(a, d, c, { continueOnError: s.continueOnError ?? !0 });
68
+ } catch (e) {
69
+ return m(n, "sync_defaults_apply_failed", e);
70
+ }
71
+ if (s.onSyncResult) try {
72
+ await s.onSyncResult(o, t);
73
+ } catch (e) {
74
+ return m(n, "sync_defaults_on_result_failed", e, o);
75
+ }
53
76
  return n.json(o);
54
- }), g.route("/", h), g;
77
+ }), v.route("/", _), v;
55
78
  }
56
- function h(e) {
79
+ function _(e) {
57
80
  let t = /* @__PURE__ */ new WeakMap();
58
81
  return { fetch(n, r, i) {
59
82
  let a = t.get(r);
60
- return a || (a = m(r, e), t.set(r, a)), a.then((e) => e.fetch(n, r, i));
83
+ return a || (a = g(r, e), t.set(r, a)), a.then((e) => e.fetch(n, r, i));
61
84
  } };
62
85
  }
63
86
  //#endregion
64
- export { d as createDispatchSyncDefaults, h as createWfpTenantApp };
87
+ export { d as createDispatchSyncDefaults, _ as createWfpTenantApp };
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.35.2",
14
+ "version": "2.36.0",
15
15
  "files": [
16
16
  "dist"
17
17
  ],
@@ -43,7 +43,7 @@
43
43
  "vite": "^8.0.14",
44
44
  "vitest": "^4.1.7",
45
45
  "@authhero/multi-tenancy": "14.25.1",
46
- "authhero": "8.7.0"
46
+ "authhero": "8.7.1"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@authhero/multi-tenancy": "^14.0.0",
@@ -62,8 +62,8 @@
62
62
  "dependencies": {
63
63
  "nanoid": "^5.1.11",
64
64
  "wretch": "^3.0.8",
65
- "@authhero/adapter-interfaces": "3.1.1",
66
- "@authhero/kysely-adapter": "11.8.9"
65
+ "@authhero/kysely-adapter": "11.8.10",
66
+ "@authhero/adapter-interfaces": "3.2.0"
67
67
  },
68
68
  "scripts": {
69
69
  "build": "vite build && tsc -p tsconfig.types.json && rollup -c rollup.dts.config.mjs",