@authhero/multi-tenancy 13.1.3 → 13.3.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/README.md +35 -0
- package/dist/multi-tenancy.cjs +1 -0
- package/dist/multi-tenancy.d.ts +9966 -0
- package/dist/multi-tenancy.mjs +552 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Multi-tenancy support for AuthHero with organization-based access control, per-t
|
|
|
10
10
|
- 🌐 **Subdomain Routing** - Automatic subdomain-to-tenant resolution
|
|
11
11
|
- 🔄 **Tenant Lifecycle** - Automated provisioning and deprovisioning
|
|
12
12
|
- 🪝 **Hooks Integration** - Seamless integration with AuthHero hooks system
|
|
13
|
+
- 📡 **Resource Server Sync** - Automatically sync resource servers from main tenant to all child tenants
|
|
13
14
|
|
|
14
15
|
## Installation
|
|
15
16
|
|
|
@@ -82,6 +83,40 @@ const token = await getAccessTokenSilently({
|
|
|
82
83
|
});
|
|
83
84
|
```
|
|
84
85
|
|
|
86
|
+
## Resource Server Synchronization
|
|
87
|
+
|
|
88
|
+
Automatically sync resource servers (APIs) from the main tenant to all child tenants. When you create, update, or delete a resource server on the main tenant, it's automatically propagated to all other tenants.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { createResourceServerSyncHooks } from "@authhero/multi-tenancy";
|
|
92
|
+
|
|
93
|
+
const resourceServerHooks = createResourceServerSyncHooks({
|
|
94
|
+
mainTenantId: "main",
|
|
95
|
+
getChildTenantIds: async () => {
|
|
96
|
+
// Return all tenant IDs except the main tenant
|
|
97
|
+
const { tenants } = await adapters.tenants.list();
|
|
98
|
+
return tenants.filter((t) => t.id !== "main").map((t) => t.id);
|
|
99
|
+
},
|
|
100
|
+
getAdapters: async (tenantId) => {
|
|
101
|
+
// Return adapters for the target tenant
|
|
102
|
+
return createAdaptersForTenant(tenantId);
|
|
103
|
+
},
|
|
104
|
+
// Optional: filter which resource servers to sync
|
|
105
|
+
shouldSync: (resourceServer) => {
|
|
106
|
+
// Only sync resource servers that start with "api:"
|
|
107
|
+
return resourceServer.identifier.startsWith("api:");
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Use with AuthHero config
|
|
112
|
+
const config: AuthHeroConfig = {
|
|
113
|
+
dataAdapter,
|
|
114
|
+
entityHooks: {
|
|
115
|
+
resourceServers: resourceServerHooks,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
85
120
|
## License
|
|
86
121
|
|
|
87
122
|
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var q=Object.defineProperty;var z=(a,e,s)=>e in a?q(a,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):a[e]=s;var g=(a,e,s)=>z(a,typeof e!="symbol"?e+"":e,s);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const C=require("hono"),F=require("zod"),T=require("@authhero/adapter-interfaces");function h(a){const{mainTenantId:e,requireOrganizationMatch:s=!0}=a;return{async onTenantAccessValidation(t,n){if(n===e)return!0;if(s){const r=t.var.organization_id;return r?r===n:!1}return!0}}}function v(a,e,s){return e===s?!0:a?a===e:!1}function M(a){return{async resolveDataAdapters(e){try{return await a.getAdapters(e)}catch(s){console.error(`Failed to resolve data adapters for tenant ${e}:`,s);return}}}}function R(a){return{async afterCreate(e,s){const{accessControl:t,databaseIsolation:n,settingsInheritance:r}=a;t&&e.ctx&&await P(e,s,t),n!=null&&n.onProvision&&await n.onProvision(s.id),(r==null?void 0:r.inheritFromMain)!==!1&&e.ctx&&await j(e,s,a)},async beforeDelete(e,s){const{accessControl:t,databaseIsolation:n}=a;if(t)try{const l=(await e.adapters.organizations.list(t.mainTenantId)).organizations.find(c=>c.name===s);l&&await e.adapters.organizations.remove(t.mainTenantId,l.id)}catch(r){console.warn(`Failed to remove organization for tenant ${s}:`,r)}if(n!=null&&n.onDeprovision)try{await n.onDeprovision(s)}catch(r){console.warn(`Failed to deprovision database for tenant ${s}:`,r)}}}}async function P(a,e,s){const{mainTenantId:t,defaultPermissions:n,defaultRoles:r}=s;await a.adapters.organizations.create(t,{id:e.id,name:e.id,display_name:e.friendly_name||e.id}),r&&r.length>0&&console.log(`Would assign roles ${r.join(", ")} to organization ${e.id}`),n&&n.length>0&&console.log(`Would grant permissions ${n.join(", ")} to organization ${e.id}`)}async function j(a,e,s){const{accessControl:t,settingsInheritance:n}=s;if(!t)return;const r=await a.adapters.tenants.get(t.mainTenantId);if(!r)return;let l={...r};const c=["id","created_at","updated_at","friendly_name","audience","sender_email","sender_name"];for(const o of c)delete l[o];if(n!=null&&n.inheritedKeys){const o={};for(const i of n.inheritedKeys)i in r&&!c.includes(i)&&(o[i]=r[i]);l=o}if(n!=null&&n.excludedKeys)for(const o of n.excludedKeys)delete l[o];n!=null&&n.transformSettings&&(l=n.transformSettings(l,e.id)),Object.keys(l).length>0&&await a.adapters.tenants.update(e.id,l)}function K(a){const{mainTenantId:e,getChildTenantIds:s,getAdapters:t,shouldSync:n=()=>!0,transformForSync:r}=a;async function l(i,f,d){return(await i.resourceServers.list(f,{q:`identifier:${d}`,per_page:1})).resource_servers[0]??null}async function c(i,f){const d=await s();await Promise.all(d.map(async u=>{try{const m=await t(u),p=r?r(i,u):{name:i.name,identifier:i.identifier,scopes:i.scopes,signing_alg:i.signing_alg,signing_secret:i.signing_secret,token_lifetime:i.token_lifetime,token_lifetime_for_web:i.token_lifetime_for_web,skip_consent_for_verifiable_first_party_clients:i.skip_consent_for_verifiable_first_party_clients,allow_offline_access:i.allow_offline_access,verificationKey:i.verificationKey,options:i.options};if(f==="create"){const y=await l(m,u,i.identifier);y&&y.id?await m.resourceServers.update(u,y.id,p):await m.resourceServers.create(u,p)}else{const y=await l(m,u,i.identifier);y&&y.id&&await m.resourceServers.update(u,y.id,p)}}catch(m){console.error(`Failed to sync resource server "${i.identifier}" to tenant "${u}":`,m)}}))}async function o(i){const f=await s();await Promise.all(f.map(async d=>{try{const u=await t(d),m=await l(u,d,i);m&&m.id&&await u.resourceServers.remove(d,m.id)}catch(u){console.error(`Failed to delete resource server "${i}" from tenant "${d}":`,u)}}))}return{afterCreate:async(i,f)=>{i.tenantId===e&&n(f)&&await c(f,"create")},afterUpdate:async(i,f,d)=>{i.tenantId===e&&n(d)&&await c(d,"update")},afterDelete:async(i,f)=>{i.tenantId===e&&await o(f)}}}function S(a){const{mainTenantId:e,getMainTenantAdapters:s,getAdapters:t,shouldSync:n=()=>!0,transformForSync:r}=a;return{async afterCreate(l,c){if(c.id!==e)try{const o=await s(),i=await t(c.id),f=await o.resourceServers.list(e,{per_page:100});await Promise.all(f.resource_servers.filter(d=>n(d)).map(async d=>{try{const u=r?r(d,c.id):{name:d.name,identifier:d.identifier,scopes:d.scopes,signing_alg:d.signing_alg,signing_secret:d.signing_secret,token_lifetime:d.token_lifetime,token_lifetime_for_web:d.token_lifetime_for_web,skip_consent_for_verifiable_first_party_clients:d.skip_consent_for_verifiable_first_party_clients,allow_offline_access:d.allow_offline_access,verificationKey:d.verificationKey,options:d.options};await i.resourceServers.create(c.id,u)}catch(u){console.error(`Failed to sync resource server "${d.identifier}" to new tenant "${c.id}":`,u)}}))}catch(o){console.error(`Failed to sync resource servers to new tenant "${c.id}":`,o)}}}}var w=class extends Error{constructor(e=500,s){super(s==null?void 0:s.message,{cause:s==null?void 0:s.cause});g(this,"res");g(this,"status");this.res=s==null?void 0:s.res,this.status=e}getResponse(){return this.res?new Response(this.res.body,{status:this.status,headers:this.res.headers}):new Response(this.message,{status:this.status})}};function b(a,e){const s=new C.Hono;return s.get("/",async t=>{var f;if(a.accessControl&&await((f=e.onTenantAccessValidation)==null?void 0:f.call(e,t,a.accessControl.mainTenantId))===!1)throw new w(403,{message:"Access denied to tenant management"});const n=T.auth0QuerySchema.parse(t.req.query()),{page:r,per_page:l,include_totals:c,q:o}=n,i=await t.env.data.tenants.list({page:r,per_page:l,include_totals:c,q:o});return c?t.json(i):t.json(i.tenants)}),s.get("/:id",async t=>{var c;const n=t.req.param("id");if(await((c=e.onTenantAccessValidation)==null?void 0:c.call(e,t,n))===!1)throw new w(403,{message:"Access denied to this tenant"});const l=await t.env.data.tenants.get(n);if(!l)throw new w(404,{message:"Tenant not found"});return t.json(l)}),s.post("/",async t=>{var n,r,l,c;try{if(a.accessControl&&await((n=e.onTenantAccessValidation)==null?void 0:n.call(e,t,a.accessControl.mainTenantId))===!1)throw new w(403,{message:"Access denied to create tenants"});let o=T.tenantInsertSchema.parse(await t.req.json());const i={adapters:t.env.data,ctx:t};(r=e.tenants)!=null&&r.beforeCreate&&(o=await e.tenants.beforeCreate(i,o));const f=await t.env.data.tenants.create(o);return(l=e.tenants)!=null&&l.afterCreate&&await e.tenants.afterCreate(i,f),t.json(f,201)}catch(o){throw o instanceof F.z.ZodError?new w(400,{message:"Validation error",cause:o}):o instanceof Error&&("code"in o&&o.code==="SQLITE_CONSTRAINT_PRIMARYKEY"||(c=o.message)!=null&&c.includes("UNIQUE constraint failed"))?new w(409,{message:"Tenant with this ID already exists"}):o}}),s.patch("/:id",async t=>{var m,p,y;const n=t.req.param("id");if(await((m=e.onTenantAccessValidation)==null?void 0:m.call(e,t,n))===!1)throw new w(403,{message:"Access denied to update this tenant"});const l=T.tenantInsertSchema.partial().parse(await t.req.json()),{id:c,...o}=l;if(!await t.env.data.tenants.get(n))throw new w(404,{message:"Tenant not found"});const f={adapters:t.env.data,ctx:t};let d=o;(p=e.tenants)!=null&&p.beforeUpdate&&(d=await e.tenants.beforeUpdate(f,n,o)),await t.env.data.tenants.update(n,d);const u=await t.env.data.tenants.get(n);if(!u)throw new w(404,{message:"Tenant not found after update"});return(y=e.tenants)!=null&&y.afterUpdate&&await e.tenants.afterUpdate(f,u),t.json(u)}),s.delete("/:id",async t=>{var c,o,i;const n=t.req.param("id");if(a.accessControl&&n===a.accessControl.mainTenantId)throw new w(400,{message:"Cannot delete the main tenant"});if(a.accessControl&&await((c=e.onTenantAccessValidation)==null?void 0:c.call(e,t,a.accessControl.mainTenantId))===!1)throw new w(403,{message:"Access denied to delete tenants"});if(!await t.env.data.tenants.get(n))throw new w(404,{message:"Tenant not found"});const l={adapters:t.env.data,ctx:t};return(o=e.tenants)!=null&&o.beforeDelete&&await e.tenants.beforeDelete(l,n),await t.env.data.tenants.remove(n),(i=e.tenants)!=null&&i.afterDelete&&await e.tenants.afterDelete(l,n),t.body(null,204)}),s}function $(a){return async(e,s)=>{if(!a.accessControl)return s();const t=e.var.tenant_id,n=e.var.organization_id;if(!t)throw new w(400,{message:"Tenant ID not found in request"});if(!v(n,t,a.accessControl.mainTenantId))throw new w(403,{message:`Access denied to tenant ${t}`});return s()}}function D(a){return async(e,s)=>{if(!a.subdomainRouting)return s();const{baseDomain:t,reservedSubdomains:n=[],resolveSubdomain:r}=a.subdomainRouting,l=e.req.header("host")||"";let c=null;if(l.endsWith(t)){const i=l.slice(0,-(t.length+1));i&&!i.includes(".")&&(c=i)}if(c&&n.includes(c)&&(c=null),!c)return a.accessControl&&e.set("tenant_id",a.accessControl.mainTenantId),s();let o=null;if(r)o=await r(c);else if(a.subdomainRouting.useOrganizations!==!1&&a.accessControl)try{const i=await e.env.data.organizations.get(a.accessControl.mainTenantId,c);i&&(o=i.id)}catch{}if(!o)throw new w(404,{message:`Tenant not found for subdomain: ${c}`});return e.set("tenant_id",o),s()}}function H(a){return async(e,s)=>{if(!a.databaseIsolation)return s();const t=e.var.tenant_id;if(!t)throw new w(400,{message:"Tenant ID not found in request"});try{const n=await a.databaseIsolation.getAdapters(t);e.env.data=n}catch(n){throw console.error(`Failed to resolve database for tenant ${t}:`,n),new w(500,{message:"Failed to resolve tenant database"})}return s()}}function A(a){const e=D(a),s=$(a),t=H(a);return async(n,r)=>(await e(n,async()=>{}),await s(n,async()=>{}),await t(n,async()=>{}),r())}function U(a){const e=_(a);return{name:"multi-tenancy",middleware:A(a),hooks:e,routes:[{path:"/management",handler:b(a,e)}],onRegister:async()=>{console.log("Multi-tenancy plugin registered"),a.accessControl&&console.log(` - Access control enabled (main tenant: ${a.accessControl.mainTenantId})`),a.subdomainRouting&&console.log(` - Subdomain routing enabled (base domain: ${a.subdomainRouting.baseDomain})`),a.databaseIsolation&&console.log(" - Database isolation enabled")}}}function _(a){const e=a.accessControl?h(a.accessControl):{},s=a.databaseIsolation?M(a.databaseIsolation):{},t=R(a);return{...e,...s,tenants:t}}function I(a){const e=new C.Hono,s=_(a);return e.route("/tenants",b(a,s)),e}function E(a){return{hooks:_(a),middleware:A(a),app:I(a),config:a}}exports.createAccessControlHooks=h;exports.createAccessControlMiddleware=$;exports.createDatabaseHooks=M;exports.createDatabaseMiddleware=H;exports.createMultiTenancy=I;exports.createMultiTenancyHooks=_;exports.createMultiTenancyMiddleware=A;exports.createMultiTenancyPlugin=U;exports.createProvisioningHooks=R;exports.createResourceServerSyncHooks=K;exports.createSubdomainMiddleware=D;exports.createTenantResourceServerSyncHooks=S;exports.createTenantsRouter=b;exports.setupMultiTenancy=E;exports.validateTenantAccess=v;
|