@vocoweb/tenant 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 VocoWeb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # @vocoweb/tenant
2
+
3
+ [![npm version](https://badge.fury.io/js/%40vocoweb%2Ftenant.svg)](https://www.npmjs.com/package/@vocoweb/tenant)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Production-ready multi-tenancy and RBAC for B2B SaaS applications.
7
+
8
+ ## Overview
9
+
10
+ `@vocoweb/tenant` is the "Tenant Guard" module that solves the nightmare of building Teams, Invites, Roles, Permissions, and Data Isolation for B2B SaaS. It provides automatic tenant_id injection, pre-built components, and complete organization management.
11
+
12
+ **Key Features:**
13
+ - 🏢 **Organization Management** - Create and manage teams/workspaces
14
+ - 👥 **Role-Based Access Control** - Admin, Member, Viewer roles with custom permissions
15
+ - 📧 **Invite System** - Email-based team invitations
16
+ - 🔒 **Data Isolation** - Automatic tenant_id injection in all queries
17
+ - ⚛️ **Pre-built Components** - Team switcher, invite UI, settings
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @vocoweb/tenant
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ### Server-Side (API Routes)
28
+
29
+ ```typescript
30
+ import { tenant } from '@vocoweb/tenant';
31
+
32
+ // Create organization
33
+ export async function POST(request: Request) {
34
+ const { name } = await request.json();
35
+ const user = await auth.requireUser(request);
36
+
37
+ const org = await tenant.createOrganization({
38
+ name,
39
+ ownerId: user.id,
40
+ });
41
+
42
+ return Response.json({ organization: org });
43
+ }
44
+
45
+ // Invite member
46
+ export async function POST(request: Request) {
47
+ const { email, role, organizationId } = await request.json();
48
+
49
+ await tenant.inviteMember({
50
+ organizationId,
51
+ email,
52
+ role: 'member', // or 'admin', 'viewer'
53
+ });
54
+
55
+ return Response.json({ success: true });
56
+ }
57
+ ```
58
+
59
+ ### Client-Side (React Components)
60
+
61
+ ```tsx
62
+ import { VocoTeamSwitcher, VocoInviteMember } from '@vocoweb/tenant/react';
63
+
64
+ // Navbar with team switcher
65
+ export default function Navbar() {
66
+ return (
67
+ <nav>
68
+ <VocoTeamSwitcher />
69
+ </nav>
70
+ );
71
+ }
72
+
73
+ // Settings page with invite UI
74
+ export default function TeamSettings() {
75
+ return (
76
+ <div>
77
+ <h1>Team Settings</h1>
78
+ <VocoInviteMember role="admin" />
79
+ </div>
80
+ );
81
+ }
82
+ ```
83
+
84
+ ### Middleware (Automatic Tenant Injection)
85
+
86
+ ```typescript
87
+ // middleware.ts
88
+ import { createTenantMiddleware } from '@vocoweb/tenant';
89
+
90
+ export const middleware = createTenantMiddleware({
91
+ // All database queries automatically include tenant_id
92
+ enforceIsolation: true,
93
+ });
94
+
95
+ export const config = {
96
+ matcher: ['/api/:path*', '/dashboard/:path*'],
97
+ };
98
+ ```
99
+
100
+ ## API Reference
101
+
102
+ ### Server Methods
103
+
104
+ - `createOrganization(options)` - Create new organization
105
+ - `inviteMember(options)` - Send organization invite
106
+ - `acceptInvite(token)` - Accept organization invite
107
+ - `setMemberRole(options)` - Update member role
108
+ - `removeMember(options)` - Remove member from organization
109
+ - `getOrganizationMembers(orgId)` - List all members
110
+ - `getUserOrganizations(userId)` - Get user's organizations
111
+
112
+ ### Client Methods
113
+
114
+ - `switchOrganization(orgId)` - Switch active organization
115
+ - `getCurrentOrganization()` - Get current active organization
116
+
117
+ ### React Components
118
+
119
+ - `<VocoTeamSwitcher />` - Notion-style workspace switcher
120
+ - `<VocoInviteMember role="admin" />` - Invite member UI
121
+
122
+ ## Environment Variables
123
+
124
+ ```bash
125
+ # Supabase (Required)
126
+ NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
127
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
128
+ SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
129
+
130
+ # App Configuration
131
+ NEXT_PUBLIC_APP_URL=https://yourapp.com
132
+ SUPPORT_EMAIL=support@yourapp.com
133
+ ```
134
+
135
+ ## Database Schema
136
+
137
+ The package expects the following tables:
138
+
139
+ ```sql
140
+ -- Organizations
141
+ CREATE TABLE organizations (
142
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
143
+ name TEXT NOT NULL,
144
+ owner_id UUID NOT NULL REFERENCES auth.users(id),
145
+ created_at TIMESTAMPTZ DEFAULT NOW()
146
+ );
147
+
148
+ -- Organization Members
149
+ CREATE TABLE organization_members (
150
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
151
+ organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
152
+ user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
153
+ role TEXT NOT NULL CHECK (role IN ('admin', 'member', 'viewer')),
154
+ created_at TIMESTAMPTZ DEFAULT NOW(),
155
+ UNIQUE(organization_id, user_id)
156
+ );
157
+
158
+ -- Invites
159
+ CREATE TABLE organization_invites (
160
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
161
+ organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
162
+ email TEXT NOT NULL,
163
+ role TEXT NOT NULL,
164
+ token TEXT UNIQUE NOT NULL,
165
+ expires_at TIMESTAMPTZ NOT NULL,
166
+ created_at TIMESTAMPTZ DEFAULT NOW()
167
+ );
168
+ ```
169
+
170
+ ## License
171
+
172
+ MIT © VocoWeb
173
+
174
+ ## Support
175
+
176
+ - Email: legal@vocoweb.in
177
+ - Documentation: [GitHub Wiki](https://github.com/vocoweb/vocoweb-tenant/wiki)
178
+
179
+ ---
180
+
181
+ **Built with care by [VocoWeb](https://vocoweb.in)**
@@ -0,0 +1,86 @@
1
+ import { A as AcceptInviteOptions, O as OrganizationMember, C as CreateOrganizationOptions, a as Organization, I as InviteMemberOptions, b as OrganizationInvite, R as RemoveMemberOptions, S as SetMemberRoleOptions } from './types-BX4GZa4r.mjs';
2
+ export { T as TenantContext, c as TenantMiddlewareOptions, U as UserRole } from './types-BX4GZa4r.mjs';
3
+
4
+ /**
5
+ * Client-side functions for @vocoweb/tenant
6
+ */
7
+ /**
8
+ * Switch active organization
9
+ */
10
+ declare function switchOrganization(organizationId: string): Promise<void>;
11
+ /**
12
+ * Get current active organization
13
+ */
14
+ declare function getCurrentOrganization(): string | null;
15
+ /**
16
+ * Listen to organization changes
17
+ */
18
+ declare function onOrganizationChange(callback: (organizationId: string | null) => void): () => void;
19
+
20
+ /**
21
+ * Server-side functions for @vocoweb/tenant
22
+ */
23
+
24
+ /**
25
+ * Create a new organization
26
+ */
27
+ declare function createOrganization(options: CreateOrganizationOptions): Promise<Organization>;
28
+ /**
29
+ * Invite a member to an organization
30
+ */
31
+ declare function inviteMember(options: InviteMemberOptions): Promise<OrganizationInvite>;
32
+ /**
33
+ * Accept an organization invite
34
+ */
35
+ declare function acceptInvite(options: AcceptInviteOptions): Promise<OrganizationMember>;
36
+ /**
37
+ * Set member role
38
+ */
39
+ declare function setMemberRole(options: SetMemberRoleOptions): Promise<void>;
40
+ /**
41
+ * Remove member from organization
42
+ */
43
+ declare function removeMember(options: RemoveMemberOptions): Promise<void>;
44
+ /**
45
+ * Get organization members
46
+ */
47
+ declare function getOrganizationMembers(organizationId: string): Promise<OrganizationMember[]>;
48
+ /**
49
+ * Get user's organizations
50
+ */
51
+ declare function getUserOrganizations(userId: string): Promise<Organization[]>;
52
+
53
+ /**
54
+ * @vocoweb/tenant
55
+ * Production-ready multi-tenancy and RBAC for B2B SaaS
56
+ */
57
+
58
+ /**
59
+ * Main tenant API
60
+ *
61
+ * @example
62
+ * import { tenant } from '@vocoweb/tenant';
63
+ *
64
+ * // Server-side
65
+ * const org = await tenant.createOrganization({ name: 'Acme Inc', ownerId: userId });
66
+ * await tenant.inviteMember({ organizationId, email, role: 'member' });
67
+ *
68
+ * // Client-side
69
+ * await tenant.switchOrganization(orgId);
70
+ */
71
+ declare const tenant: {
72
+ switchOrganization: typeof switchOrganization;
73
+ getCurrentOrganization: typeof getCurrentOrganization;
74
+ onOrganizationChange: typeof onOrganizationChange;
75
+ createOrganization: typeof createOrganization;
76
+ inviteMember: typeof inviteMember;
77
+ acceptInvite: typeof acceptInvite;
78
+ setMemberRole: typeof setMemberRole;
79
+ removeMember: typeof removeMember;
80
+ getOrganizationMembers: typeof getOrganizationMembers;
81
+ getUserOrganizations: typeof getUserOrganizations;
82
+ };
83
+
84
+ declare const VERSION = "1.0.0";
85
+
86
+ export { AcceptInviteOptions, CreateOrganizationOptions, InviteMemberOptions, Organization, OrganizationInvite, OrganizationMember, RemoveMemberOptions, SetMemberRoleOptions, VERSION, acceptInvite, createOrganization, getCurrentOrganization, getOrganizationMembers, getUserOrganizations, inviteMember, onOrganizationChange, removeMember, setMemberRole, switchOrganization, tenant };
@@ -0,0 +1,86 @@
1
+ import { A as AcceptInviteOptions, O as OrganizationMember, C as CreateOrganizationOptions, a as Organization, I as InviteMemberOptions, b as OrganizationInvite, R as RemoveMemberOptions, S as SetMemberRoleOptions } from './types-BX4GZa4r.js';
2
+ export { T as TenantContext, c as TenantMiddlewareOptions, U as UserRole } from './types-BX4GZa4r.js';
3
+
4
+ /**
5
+ * Client-side functions for @vocoweb/tenant
6
+ */
7
+ /**
8
+ * Switch active organization
9
+ */
10
+ declare function switchOrganization(organizationId: string): Promise<void>;
11
+ /**
12
+ * Get current active organization
13
+ */
14
+ declare function getCurrentOrganization(): string | null;
15
+ /**
16
+ * Listen to organization changes
17
+ */
18
+ declare function onOrganizationChange(callback: (organizationId: string | null) => void): () => void;
19
+
20
+ /**
21
+ * Server-side functions for @vocoweb/tenant
22
+ */
23
+
24
+ /**
25
+ * Create a new organization
26
+ */
27
+ declare function createOrganization(options: CreateOrganizationOptions): Promise<Organization>;
28
+ /**
29
+ * Invite a member to an organization
30
+ */
31
+ declare function inviteMember(options: InviteMemberOptions): Promise<OrganizationInvite>;
32
+ /**
33
+ * Accept an organization invite
34
+ */
35
+ declare function acceptInvite(options: AcceptInviteOptions): Promise<OrganizationMember>;
36
+ /**
37
+ * Set member role
38
+ */
39
+ declare function setMemberRole(options: SetMemberRoleOptions): Promise<void>;
40
+ /**
41
+ * Remove member from organization
42
+ */
43
+ declare function removeMember(options: RemoveMemberOptions): Promise<void>;
44
+ /**
45
+ * Get organization members
46
+ */
47
+ declare function getOrganizationMembers(organizationId: string): Promise<OrganizationMember[]>;
48
+ /**
49
+ * Get user's organizations
50
+ */
51
+ declare function getUserOrganizations(userId: string): Promise<Organization[]>;
52
+
53
+ /**
54
+ * @vocoweb/tenant
55
+ * Production-ready multi-tenancy and RBAC for B2B SaaS
56
+ */
57
+
58
+ /**
59
+ * Main tenant API
60
+ *
61
+ * @example
62
+ * import { tenant } from '@vocoweb/tenant';
63
+ *
64
+ * // Server-side
65
+ * const org = await tenant.createOrganization({ name: 'Acme Inc', ownerId: userId });
66
+ * await tenant.inviteMember({ organizationId, email, role: 'member' });
67
+ *
68
+ * // Client-side
69
+ * await tenant.switchOrganization(orgId);
70
+ */
71
+ declare const tenant: {
72
+ switchOrganization: typeof switchOrganization;
73
+ getCurrentOrganization: typeof getCurrentOrganization;
74
+ onOrganizationChange: typeof onOrganizationChange;
75
+ createOrganization: typeof createOrganization;
76
+ inviteMember: typeof inviteMember;
77
+ acceptInvite: typeof acceptInvite;
78
+ setMemberRole: typeof setMemberRole;
79
+ removeMember: typeof removeMember;
80
+ getOrganizationMembers: typeof getOrganizationMembers;
81
+ getUserOrganizations: typeof getUserOrganizations;
82
+ };
83
+
84
+ declare const VERSION = "1.0.0";
85
+
86
+ export { AcceptInviteOptions, CreateOrganizationOptions, InviteMemberOptions, Organization, OrganizationInvite, OrganizationMember, RemoveMemberOptions, SetMemberRoleOptions, VERSION, acceptInvite, createOrganization, getCurrentOrganization, getOrganizationMembers, getUserOrganizations, inviteMember, onOrganizationChange, removeMember, setMemberRole, switchOrganization, tenant };
package/dist/index.js ADDED
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ var supabaseJs = require('@supabase/supabase-js');
4
+
5
+ // src/client.ts
6
+ var CURRENT_ORG_KEY = "voco_current_organization";
7
+ async function switchOrganization(organizationId) {
8
+ if (typeof window === "undefined") {
9
+ throw new Error("switchOrganization can only be called client-side");
10
+ }
11
+ localStorage.setItem(CURRENT_ORG_KEY, organizationId);
12
+ window.dispatchEvent(
13
+ new StorageEvent("storage", {
14
+ key: CURRENT_ORG_KEY,
15
+ newValue: organizationId
16
+ })
17
+ );
18
+ }
19
+ function getCurrentOrganization() {
20
+ if (typeof window === "undefined") {
21
+ return null;
22
+ }
23
+ return localStorage.getItem(CURRENT_ORG_KEY);
24
+ }
25
+ function onOrganizationChange(callback) {
26
+ if (typeof window === "undefined") {
27
+ return () => {
28
+ };
29
+ }
30
+ const handler = (e) => {
31
+ if (e.key === CURRENT_ORG_KEY) {
32
+ callback(e.newValue);
33
+ }
34
+ };
35
+ window.addEventListener("storage", handler);
36
+ return () => {
37
+ window.removeEventListener("storage", handler);
38
+ };
39
+ }
40
+ var getSupabaseClient = () => {
41
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
42
+ const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
43
+ if (!supabaseUrl || !supabaseKey) {
44
+ throw new Error("Supabase credentials not configured");
45
+ }
46
+ return supabaseJs.createClient(supabaseUrl, supabaseKey);
47
+ };
48
+ async function createOrganization(options) {
49
+ const supabase = getSupabaseClient();
50
+ const { data, error } = await supabase.from("organizations").insert({
51
+ name: options.name,
52
+ owner_id: options.ownerId
53
+ }).select().single();
54
+ if (error) {
55
+ throw new Error(`Failed to create organization: ${error.message}`);
56
+ }
57
+ await supabase.from("organization_members").insert({
58
+ organization_id: data.id,
59
+ user_id: options.ownerId,
60
+ role: "owner"
61
+ });
62
+ return {
63
+ id: data.id,
64
+ name: data.name,
65
+ ownerId: data.owner_id,
66
+ createdAt: new Date(data.created_at)
67
+ };
68
+ }
69
+ async function inviteMember(options) {
70
+ const supabase = getSupabaseClient();
71
+ const token = crypto.randomUUID();
72
+ const expiresAt = /* @__PURE__ */ new Date();
73
+ expiresAt.setDate(expiresAt.getDate() + 7);
74
+ const { data, error } = await supabase.from("organization_invites").insert({
75
+ organization_id: options.organizationId,
76
+ email: options.email,
77
+ role: options.role,
78
+ token,
79
+ expires_at: expiresAt.toISOString()
80
+ }).select().single();
81
+ if (error) {
82
+ throw new Error(`Failed to create invite: ${error.message}`);
83
+ }
84
+ return {
85
+ id: data.id,
86
+ organizationId: data.organization_id,
87
+ email: data.email,
88
+ role: data.role,
89
+ token: data.token,
90
+ expiresAt: new Date(data.expires_at),
91
+ createdAt: new Date(data.created_at)
92
+ };
93
+ }
94
+ async function acceptInvite(options) {
95
+ const supabase = getSupabaseClient();
96
+ const { data: invite, error: inviteError } = await supabase.from("organization_invites").select("*").eq("token", options.token).single();
97
+ if (inviteError || !invite) {
98
+ throw new Error("Invalid or expired invite");
99
+ }
100
+ if (new Date(invite.expires_at) < /* @__PURE__ */ new Date()) {
101
+ throw new Error("Invite has expired");
102
+ }
103
+ const { data, error } = await supabase.from("organization_members").insert({
104
+ organization_id: invite.organization_id,
105
+ user_id: options.userId,
106
+ role: invite.role
107
+ }).select().single();
108
+ if (error) {
109
+ throw new Error(`Failed to add member: ${error.message}`);
110
+ }
111
+ await supabase.from("organization_invites").delete().eq("id", invite.id);
112
+ return {
113
+ id: data.id,
114
+ organizationId: data.organization_id,
115
+ userId: data.user_id,
116
+ role: data.role,
117
+ createdAt: new Date(data.created_at)
118
+ };
119
+ }
120
+ async function setMemberRole(options) {
121
+ const supabase = getSupabaseClient();
122
+ const { error } = await supabase.from("organization_members").update({ role: options.role }).eq("organization_id", options.organizationId).eq("user_id", options.userId);
123
+ if (error) {
124
+ throw new Error(`Failed to update role: ${error.message}`);
125
+ }
126
+ }
127
+ async function removeMember(options) {
128
+ const supabase = getSupabaseClient();
129
+ const { error } = await supabase.from("organization_members").delete().eq("organization_id", options.organizationId).eq("user_id", options.userId);
130
+ if (error) {
131
+ throw new Error(`Failed to remove member: ${error.message}`);
132
+ }
133
+ }
134
+ async function getOrganizationMembers(organizationId) {
135
+ const supabase = getSupabaseClient();
136
+ const { data, error } = await supabase.from("organization_members").select("*").eq("organization_id", organizationId);
137
+ if (error) {
138
+ throw new Error(`Failed to get members: ${error.message}`);
139
+ }
140
+ return data.map((row) => ({
141
+ id: row.id,
142
+ organizationId: row.organization_id,
143
+ userId: row.user_id,
144
+ role: row.role,
145
+ createdAt: new Date(row.created_at)
146
+ }));
147
+ }
148
+ async function getUserOrganizations(userId) {
149
+ const supabase = getSupabaseClient();
150
+ const { data, error } = await supabase.from("organization_members").select("organizations(*)").eq("user_id", userId);
151
+ if (error) {
152
+ throw new Error(`Failed to get organizations: ${error.message}`);
153
+ }
154
+ return data.map((row) => ({
155
+ id: row.organizations.id,
156
+ name: row.organizations.name,
157
+ ownerId: row.organizations.owner_id,
158
+ createdAt: new Date(row.organizations.created_at)
159
+ }));
160
+ }
161
+
162
+ // src/index.ts
163
+ var tenant = {
164
+ // Client methods
165
+ switchOrganization,
166
+ getCurrentOrganization,
167
+ onOrganizationChange,
168
+ // Server methods
169
+ createOrganization,
170
+ inviteMember,
171
+ acceptInvite,
172
+ setMemberRole,
173
+ removeMember,
174
+ getOrganizationMembers,
175
+ getUserOrganizations
176
+ };
177
+ var VERSION = "1.0.0";
178
+
179
+ exports.VERSION = VERSION;
180
+ exports.acceptInvite = acceptInvite;
181
+ exports.createOrganization = createOrganization;
182
+ exports.getCurrentOrganization = getCurrentOrganization;
183
+ exports.getOrganizationMembers = getOrganizationMembers;
184
+ exports.getUserOrganizations = getUserOrganizations;
185
+ exports.inviteMember = inviteMember;
186
+ exports.onOrganizationChange = onOrganizationChange;
187
+ exports.removeMember = removeMember;
188
+ exports.setMemberRole = setMemberRole;
189
+ exports.switchOrganization = switchOrganization;
190
+ exports.tenant = tenant;
191
+ //# sourceMappingURL=index.js.map
192
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/server.ts","../src/index.ts"],"names":["createClient"],"mappings":";;;;;AAMA,IAAM,eAAA,GAAkB,2BAAA;AAKxB,eAAsB,mBAAmB,cAAA,EAAuC;AAC5E,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAC/B,IAAA,MAAM,IAAI,MAAM,mDAAmD,CAAA;AAAA,EACvE;AAEA,EAAA,YAAA,CAAa,OAAA,CAAQ,iBAAiB,cAAc,CAAA;AAGpD,EAAA,MAAA,CAAO,aAAA;AAAA,IACH,IAAI,aAAa,SAAA,EAAW;AAAA,MACxB,GAAA,EAAK,eAAA;AAAA,MACL,QAAA,EAAU;AAAA,KACb;AAAA,GACL;AACJ;AAKO,SAAS,sBAAA,GAAwC;AACpD,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAC/B,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,OAAO,YAAA,CAAa,QAAQ,eAAe,CAAA;AAC/C;AAKO,SAAS,qBACZ,QAAA,EACU;AACV,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAC/B,IAAA,OAAO,MAAM;AAAA,IAAE,CAAA;AAAA,EACnB;AAEA,EAAA,MAAM,OAAA,GAAU,CAAC,CAAA,KAAoB;AACjC,IAAA,IAAI,CAAA,CAAE,QAAQ,eAAA,EAAiB;AAC3B,MAAA,QAAA,CAAS,EAAE,QAAQ,CAAA;AAAA,IACvB;AAAA,EACJ,CAAA;AAEA,EAAA,MAAA,CAAO,gBAAA,CAAiB,WAAW,OAAO,CAAA;AAE1C,EAAA,OAAO,MAAM;AACT,IAAA,MAAA,CAAO,mBAAA,CAAoB,WAAW,OAAO,CAAA;AAAA,EACjD,CAAA;AACJ;AC3CA,IAAM,oBAAoB,MAAM;AAC5B,EAAA,MAAM,WAAA,GAAc,QAAQ,GAAA,CAAI,wBAAA;AAChC,EAAA,MAAM,WAAA,GAAc,QAAQ,GAAA,CAAI,yBAAA;AAEhC,EAAA,IAAI,CAAC,WAAA,IAAe,CAAC,WAAA,EAAa;AAC9B,IAAA,MAAM,IAAI,MAAM,qCAAqC,CAAA;AAAA,EACzD;AAEA,EAAA,OAAOA,uBAAA,CAAa,aAAa,WAAW,CAAA;AAChD,CAAA;AAKA,eAAsB,mBAClB,OAAA,EACqB;AACrB,EAAA,MAAM,WAAW,iBAAA,EAAkB;AAEnC,EAAA,MAAM,EAAE,MAAM,KAAA,EAAM,GAAI,MAAM,QAAA,CACzB,IAAA,CAAK,eAAe,CAAA,CACpB,MAAA,CAAO;AAAA,IACJ,MAAM,OAAA,CAAQ,IAAA;AAAA,IACd,UAAU,OAAA,CAAQ;AAAA,GACrB,CAAA,CACA,MAAA,EAAO,CACP,MAAA,EAAO;AAEZ,EAAA,IAAI,KAAA,EAAO;AACP,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,+BAAA,EAAkC,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,EACrE;AAGA,EAAA,MAAM,QAAA,CAAS,IAAA,CAAK,sBAAsB,CAAA,CAAE,MAAA,CAAO;AAAA,IAC/C,iBAAiB,IAAA,CAAK,EAAA;AAAA,IACtB,SAAS,OAAA,CAAQ,OAAA;AAAA,IACjB,IAAA,EAAM;AAAA,GACT,CAAA;AAED,EAAA,OAAO;AAAA,IACH,IAAI,IAAA,CAAK,EAAA;AAAA,IACT,MAAM,IAAA,CAAK,IAAA;AAAA,IACX,SAAS,IAAA,CAAK,QAAA;AAAA,IACd,SAAA,EAAW,IAAI,IAAA,CAAK,IAAA,CAAK,UAAU;AAAA,GACvC;AACJ;AAKA,eAAsB,aAClB,OAAA,EAC2B;AAC3B,EAAA,MAAM,WAAW,iBAAA,EAAkB;AAGnC,EAAA,MAAM,KAAA,GAAQ,OAAO,UAAA,EAAW;AAChC,EAAA,MAAM,SAAA,uBAAgB,IAAA,EAAK;AAC3B,EAAA,SAAA,CAAU,OAAA,CAAQ,SAAA,CAAU,OAAA,EAAQ,GAAI,CAAC,CAAA;AAEzC,EAAA,MAAM,EAAE,MAAM,KAAA,EAAM,GAAI,MAAM,QAAA,CACzB,IAAA,CAAK,sBAAsB,CAAA,CAC3B,MAAA,CAAO;AAAA,IACJ,iBAAiB,OAAA,CAAQ,cAAA;AAAA,IACzB,OAAO,OAAA,CAAQ,KAAA;AAAA,IACf,MAAM,OAAA,CAAQ,IAAA;AAAA,IACd,KAAA;AAAA,IACA,UAAA,EAAY,UAAU,WAAA;AAAY,GACrC,CAAA,CACA,MAAA,EAAO,CACP,MAAA,EAAO;AAEZ,EAAA,IAAI,KAAA,EAAO;AACP,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,yBAAA,EAA4B,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,EAC/D;AASA,EAAA,OAAO;AAAA,IACH,IAAI,IAAA,CAAK,EAAA;AAAA,IACT,gBAAgB,IAAA,CAAK,eAAA;AAAA,IACrB,OAAO,IAAA,CAAK,KAAA;AAAA,IACZ,MAAM,IAAA,CAAK,IAAA;AAAA,IACX,OAAO,IAAA,CAAK,KAAA;AAAA,IACZ,SAAA,EAAW,IAAI,IAAA,CAAK,IAAA,CAAK,UAAU,CAAA;AAAA,IACnC,SAAA,EAAW,IAAI,IAAA,CAAK,IAAA,CAAK,UAAU;AAAA,GACvC;AACJ;AAKA,eAAsB,aAClB,OAAA,EAC2B;AAC3B,EAAA,MAAM,WAAW,iBAAA,EAAkB;AAGnC,EAAA,MAAM,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAO,WAAA,EAAY,GAAI,MAAM,QAAA,CAC9C,IAAA,CAAK,sBAAsB,CAAA,CAC3B,MAAA,CAAO,GAAG,CAAA,CACV,EAAA,CAAG,SAAS,OAAA,CAAQ,KAAK,EACzB,MAAA,EAAO;AAEZ,EAAA,IAAI,WAAA,IAAe,CAAC,MAAA,EAAQ;AACxB,IAAA,MAAM,IAAI,MAAM,2BAA2B,CAAA;AAAA,EAC/C;AAGA,EAAA,IAAI,IAAI,IAAA,CAAK,MAAA,CAAO,UAAU,CAAA,mBAAI,IAAI,MAAK,EAAG;AAC1C,IAAA,MAAM,IAAI,MAAM,oBAAoB,CAAA;AAAA,EACxC;AAGA,EAAA,MAAM,EAAE,MAAM,KAAA,EAAM,GAAI,MAAM,QAAA,CACzB,IAAA,CAAK,sBAAsB,CAAA,CAC3B,MAAA,CAAO;AAAA,IACJ,iBAAiB,MAAA,CAAO,eAAA;AAAA,IACxB,SAAS,OAAA,CAAQ,MAAA;AAAA,IACjB,MAAM,MAAA,CAAO;AAAA,GAChB,CAAA,CACA,MAAA,EAAO,CACP,MAAA,EAAO;AAEZ,EAAA,IAAI,KAAA,EAAO;AACP,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyB,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,EAC5D;AAGA,EAAA,MAAM,QAAA,CAAS,KAAK,sBAAsB,CAAA,CAAE,QAAO,CAAE,EAAA,CAAG,IAAA,EAAM,MAAA,CAAO,EAAE,CAAA;AAEvE,EAAA,OAAO;AAAA,IACH,IAAI,IAAA,CAAK,EAAA;AAAA,IACT,gBAAgB,IAAA,CAAK,eAAA;AAAA,IACrB,QAAQ,IAAA,CAAK,OAAA;AAAA,IACb,MAAM,IAAA,CAAK,IAAA;AAAA,IACX,SAAA,EAAW,IAAI,IAAA,CAAK,IAAA,CAAK,UAAU;AAAA,GACvC;AACJ;AAKA,eAAsB,cAClB,OAAA,EACa;AACb,EAAA,MAAM,WAAW,iBAAA,EAAkB;AAEnC,EAAA,MAAM,EAAE,OAAM,GAAI,MAAM,SACnB,IAAA,CAAK,sBAAsB,CAAA,CAC3B,MAAA,CAAO,EAAE,IAAA,EAAM,QAAQ,IAAA,EAAM,CAAA,CAC7B,EAAA,CAAG,iBAAA,EAAmB,OAAA,CAAQ,cAAc,CAAA,CAC5C,EAAA,CAAG,SAAA,EAAW,OAAA,CAAQ,MAAM,CAAA;AAEjC,EAAA,IAAI,KAAA,EAAO;AACP,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,uBAAA,EAA0B,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,EAC7D;AACJ;AAKA,eAAsB,aAClB,OAAA,EACa;AACb,EAAA,MAAM,WAAW,iBAAA,EAAkB;AAEnC,EAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,QAAA,CACnB,IAAA,CAAK,sBAAsB,CAAA,CAC3B,MAAA,EAAO,CACP,EAAA,CAAG,mBAAmB,OAAA,CAAQ,cAAc,EAC5C,EAAA,CAAG,SAAA,EAAW,QAAQ,MAAM,CAAA;AAEjC,EAAA,IAAI,KAAA,EAAO;AACP,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,yBAAA,EAA4B,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,EAC/D;AACJ;AAKA,eAAsB,uBAClB,cAAA,EAC6B;AAC7B,EAAA,MAAM,WAAW,iBAAA,EAAkB;AAEnC,EAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAM,QAAA,CACzB,IAAA,CAAK,sBAAsB,CAAA,CAC3B,MAAA,CAAO,GAAG,CAAA,CACV,EAAA,CAAG,mBAAmB,cAAc,CAAA;AAEzC,EAAA,IAAI,KAAA,EAAO;AACP,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,uBAAA,EAA0B,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,EAC7D;AAEA,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAC,GAAA,MAAS;AAAA,IACtB,IAAI,GAAA,CAAI,EAAA;AAAA,IACR,gBAAgB,GAAA,CAAI,eAAA;AAAA,IACpB,QAAQ,GAAA,CAAI,OAAA;AAAA,IACZ,MAAM,GAAA,CAAI,IAAA;AAAA,IACV,SAAA,EAAW,IAAI,IAAA,CAAK,GAAA,CAAI,UAAU;AAAA,GACtC,CAAE,CAAA;AACN;AAKA,eAAsB,qBAClB,MAAA,EACuB;AACvB,EAAA,MAAM,WAAW,iBAAA,EAAkB;AAEnC,EAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAM,QAAA,CACzB,IAAA,CAAK,sBAAsB,CAAA,CAC3B,MAAA,CAAO,kBAAkB,CAAA,CACzB,EAAA,CAAG,WAAW,MAAM,CAAA;AAEzB,EAAA,IAAI,KAAA,EAAO;AACP,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,6BAAA,EAAgC,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,EACnE;AAEA,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAC,GAAA,MAAc;AAAA,IAC3B,EAAA,EAAI,IAAI,aAAA,CAAc,EAAA;AAAA,IACtB,IAAA,EAAM,IAAI,aAAA,CAAc,IAAA;AAAA,IACxB,OAAA,EAAS,IAAI,aAAA,CAAc,QAAA;AAAA,IAC3B,SAAA,EAAW,IAAI,IAAA,CAAK,GAAA,CAAI,cAAc,UAAU;AAAA,GACpD,CAAE,CAAA;AACN;;;AC7MO,IAAM,MAAA,GAAS;AAAA;AAAA,EAElB,kBAAA;AAAA,EACA,sBAAA;AAAA,EACA,oBAAA;AAAA;AAAA,EAGA,kBAAA;AAAA,EACA,YAAA;AAAA,EACA,YAAA;AAAA,EACA,aAAA;AAAA,EACA,YAAA;AAAA,EACA,sBAAA;AAAA,EACA;AACJ;AAgBO,IAAM,OAAA,GAAU","file":"index.js","sourcesContent":["/**\n * Client-side functions for @vocoweb/tenant\n */\n\n\n\nconst CURRENT_ORG_KEY = 'voco_current_organization';\n\n/**\n * Switch active organization\n */\nexport async function switchOrganization(organizationId: string): Promise<void> {\n if (typeof window === 'undefined') {\n throw new Error('switchOrganization can only be called client-side');\n }\n\n localStorage.setItem(CURRENT_ORG_KEY, organizationId);\n\n // Trigger storage event for cross-tab synchronization\n window.dispatchEvent(\n new StorageEvent('storage', {\n key: CURRENT_ORG_KEY,\n newValue: organizationId,\n })\n );\n}\n\n/**\n * Get current active organization\n */\nexport function getCurrentOrganization(): string | null {\n if (typeof window === 'undefined') {\n return null;\n }\n\n return localStorage.getItem(CURRENT_ORG_KEY);\n}\n\n/**\n * Listen to organization changes\n */\nexport function onOrganizationChange(\n callback: (organizationId: string | null) => void\n): () => void {\n if (typeof window === 'undefined') {\n return () => { };\n }\n\n const handler = (e: StorageEvent) => {\n if (e.key === CURRENT_ORG_KEY) {\n callback(e.newValue);\n }\n };\n\n window.addEventListener('storage', handler);\n\n return () => {\n window.removeEventListener('storage', handler);\n };\n}\n","/**\n * Server-side functions for @vocoweb/tenant\n */\n\nimport { createClient } from '@supabase/supabase-js';\nimport type {\n Organization,\n OrganizationMember,\n OrganizationInvite,\n CreateOrganizationOptions,\n InviteMemberOptions,\n AcceptInviteOptions,\n SetMemberRoleOptions,\n RemoveMemberOptions,\n} from './types';\n\nconst getSupabaseClient = () => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;\n\n if (!supabaseUrl || !supabaseKey) {\n throw new Error('Supabase credentials not configured');\n }\n\n return createClient(supabaseUrl, supabaseKey);\n};\n\n/**\n * Create a new organization\n */\nexport async function createOrganization(\n options: CreateOrganizationOptions\n): Promise<Organization> {\n const supabase = getSupabaseClient();\n\n const { data, error } = await supabase\n .from('organizations')\n .insert({\n name: options.name,\n owner_id: options.ownerId,\n })\n .select()\n .single();\n\n if (error) {\n throw new Error(`Failed to create organization: ${error.message}`);\n }\n\n // Add owner as admin member\n await supabase.from('organization_members').insert({\n organization_id: data.id,\n user_id: options.ownerId,\n role: 'owner',\n });\n\n return {\n id: data.id,\n name: data.name,\n ownerId: data.owner_id,\n createdAt: new Date(data.created_at),\n };\n}\n\n/**\n * Invite a member to an organization\n */\nexport async function inviteMember(\n options: InviteMemberOptions\n): Promise<OrganizationInvite> {\n const supabase = getSupabaseClient();\n\n // Generate unique token\n const token = crypto.randomUUID();\n const expiresAt = new Date();\n expiresAt.setDate(expiresAt.getDate() + 7); // 7 days expiry\n\n const { data, error } = await supabase\n .from('organization_invites')\n .insert({\n organization_id: options.organizationId,\n email: options.email,\n role: options.role,\n token,\n expires_at: expiresAt.toISOString(),\n })\n .select()\n .single();\n\n if (error) {\n throw new Error(`Failed to create invite: ${error.message}`);\n }\n\n // TODO: Send email notification\n // await notify.sendEmail({\n // to: options.email,\n // template: 'organization-invite',\n // data: { organizationId: options.organizationId, token }\n // });\n\n return {\n id: data.id,\n organizationId: data.organization_id,\n email: data.email,\n role: data.role,\n token: data.token,\n expiresAt: new Date(data.expires_at),\n createdAt: new Date(data.created_at),\n };\n}\n\n/**\n * Accept an organization invite\n */\nexport async function acceptInvite(\n options: AcceptInviteOptions\n): Promise<OrganizationMember> {\n const supabase = getSupabaseClient();\n\n // Get invite\n const { data: invite, error: inviteError } = await supabase\n .from('organization_invites')\n .select('*')\n .eq('token', options.token)\n .single();\n\n if (inviteError || !invite) {\n throw new Error('Invalid or expired invite');\n }\n\n // Check expiry\n if (new Date(invite.expires_at) < new Date()) {\n throw new Error('Invite has expired');\n }\n\n // Add member\n const { data, error } = await supabase\n .from('organization_members')\n .insert({\n organization_id: invite.organization_id,\n user_id: options.userId,\n role: invite.role,\n })\n .select()\n .single();\n\n if (error) {\n throw new Error(`Failed to add member: ${error.message}`);\n }\n\n // Delete invite\n await supabase.from('organization_invites').delete().eq('id', invite.id);\n\n return {\n id: data.id,\n organizationId: data.organization_id,\n userId: data.user_id,\n role: data.role,\n createdAt: new Date(data.created_at),\n };\n}\n\n/**\n * Set member role\n */\nexport async function setMemberRole(\n options: SetMemberRoleOptions\n): Promise<void> {\n const supabase = getSupabaseClient();\n\n const { error } = await supabase\n .from('organization_members')\n .update({ role: options.role })\n .eq('organization_id', options.organizationId)\n .eq('user_id', options.userId);\n\n if (error) {\n throw new Error(`Failed to update role: ${error.message}`);\n }\n}\n\n/**\n * Remove member from organization\n */\nexport async function removeMember(\n options: RemoveMemberOptions\n): Promise<void> {\n const supabase = getSupabaseClient();\n\n const { error } = await supabase\n .from('organization_members')\n .delete()\n .eq('organization_id', options.organizationId)\n .eq('user_id', options.userId);\n\n if (error) {\n throw new Error(`Failed to remove member: ${error.message}`);\n }\n}\n\n/**\n * Get organization members\n */\nexport async function getOrganizationMembers(\n organizationId: string\n): Promise<OrganizationMember[]> {\n const supabase = getSupabaseClient();\n\n const { data, error } = await supabase\n .from('organization_members')\n .select('*')\n .eq('organization_id', organizationId);\n\n if (error) {\n throw new Error(`Failed to get members: ${error.message}`);\n }\n\n return data.map((row) => ({\n id: row.id,\n organizationId: row.organization_id,\n userId: row.user_id,\n role: row.role,\n createdAt: new Date(row.created_at),\n }));\n}\n\n/**\n * Get user's organizations\n */\nexport async function getUserOrganizations(\n userId: string\n): Promise<Organization[]> {\n const supabase = getSupabaseClient();\n\n const { data, error } = await supabase\n .from('organization_members')\n .select('organizations(*)')\n .eq('user_id', userId);\n\n if (error) {\n throw new Error(`Failed to get organizations: ${error.message}`);\n }\n\n return data.map((row: any) => ({\n id: row.organizations.id,\n name: row.organizations.name,\n ownerId: row.organizations.owner_id,\n createdAt: new Date(row.organizations.created_at),\n }));\n}\n","/**\n * @vocoweb/tenant\n * Production-ready multi-tenancy and RBAC for B2B SaaS\n */\n\n// Types\nexport * from './types';\n\n// Client-side\nexport * from './client';\n\n// Server-side\nexport * from './server';\n\n// Import for unified API\nimport * as clientTenant from './client';\nimport * as serverTenant from './server';\n\nimport type {\n Organization,\n OrganizationMember,\n OrganizationInvite,\n CreateOrganizationOptions,\n InviteMemberOptions,\n AcceptInviteOptions,\n SetMemberRoleOptions,\n RemoveMemberOptions,\n UserRole,\n} from './types';\n\n/**\n * Main tenant API\n *\n * @example\n * import { tenant } from '@vocoweb/tenant';\n *\n * // Server-side\n * const org = await tenant.createOrganization({ name: 'Acme Inc', ownerId: userId });\n * await tenant.inviteMember({ organizationId, email, role: 'member' });\n *\n * // Client-side\n * await tenant.switchOrganization(orgId);\n */\nexport const tenant = {\n // Client methods\n switchOrganization: clientTenant.switchOrganization,\n getCurrentOrganization: clientTenant.getCurrentOrganization,\n onOrganizationChange: clientTenant.onOrganizationChange,\n\n // Server methods\n createOrganization: serverTenant.createOrganization,\n inviteMember: serverTenant.inviteMember,\n acceptInvite: serverTenant.acceptInvite,\n setMemberRole: serverTenant.setMemberRole,\n removeMember: serverTenant.removeMember,\n getOrganizationMembers: serverTenant.getOrganizationMembers,\n getUserOrganizations: serverTenant.getUserOrganizations,\n};\n\n// Re-export types\nexport type {\n Organization,\n OrganizationMember,\n OrganizationInvite,\n CreateOrganizationOptions,\n InviteMemberOptions,\n AcceptInviteOptions,\n SetMemberRoleOptions,\n RemoveMemberOptions,\n UserRole,\n};\n\n// Version\nexport const VERSION = '1.0.0';\n"]}