@nebutra/tenant 0.1.0 → 0.1.1
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/context.d.ts +87 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +125 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/isolation.d.ts +157 -0
- package/dist/isolation.d.ts.map +1 -0
- package/dist/isolation.js +319 -0
- package/dist/middleware.d.ts +106 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +247 -0
- package/dist/react.d.ts +211 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +252 -0
- package/dist/resolvers/from-auth-session.d.ts +48 -0
- package/dist/resolvers/from-auth-session.d.ts.map +1 -0
- package/dist/resolvers/from-auth-session.js +41 -0
- package/dist/resolvers.d.ts +110 -0
- package/dist/resolvers.d.ts.map +1 -0
- package/dist/resolvers.js +253 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +78 -0
- package/package.json +2 -2
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { TenantContext } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Provider component that wraps your app with tenant context.
|
|
5
|
+
*
|
|
6
|
+
* @param children Child components to wrap
|
|
7
|
+
* @param value The tenant context value (usually from server)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* // layout.tsx
|
|
12
|
+
* import { TenantProvider } from "@nebutra/tenant/react";
|
|
13
|
+
* import { getTenantContext } from "@/lib/tenant-server";
|
|
14
|
+
*
|
|
15
|
+
* export default async function RootLayout() {
|
|
16
|
+
* const tenant = await getTenantContext();
|
|
17
|
+
*
|
|
18
|
+
* return (
|
|
19
|
+
* <TenantProvider value={tenant}>
|
|
20
|
+
* <MainApp />
|
|
21
|
+
* </TenantProvider>
|
|
22
|
+
* );
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function TenantProvider({ children, value, }: {
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
value: TenantContext;
|
|
29
|
+
}): React.FunctionComponentElement<React.ProviderProps<{
|
|
30
|
+
id: string;
|
|
31
|
+
slug?: string | undefined;
|
|
32
|
+
plan?: "free" | "pro" | "enterprise" | undefined;
|
|
33
|
+
features?: string[] | undefined;
|
|
34
|
+
limits?: Record<string, number> | undefined;
|
|
35
|
+
metadata?: Record<string, unknown> | undefined;
|
|
36
|
+
} | null>>;
|
|
37
|
+
/**
|
|
38
|
+
* Hook to access the current tenant context from client-side components.
|
|
39
|
+
*
|
|
40
|
+
* Throws an error if no tenant context is available.
|
|
41
|
+
* Use `useTenantOrNull()` for optional tenant contexts.
|
|
42
|
+
*
|
|
43
|
+
* @returns The current tenant context
|
|
44
|
+
* @throws TenantRequiredError if tenant context is not available
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* function MyComponent() {
|
|
49
|
+
* const tenant = useTenant();
|
|
50
|
+
*
|
|
51
|
+
* return <div>Current tenant: {tenant.id}</div>;
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function useTenant(): TenantContext;
|
|
56
|
+
/**
|
|
57
|
+
* Hook to access the current tenant context or null if not available.
|
|
58
|
+
*
|
|
59
|
+
* Useful for components that work in both tenant and non-tenant contexts
|
|
60
|
+
* (e.g., public pages, webhooks, etc).
|
|
61
|
+
*
|
|
62
|
+
* @returns The current tenant context, or null
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* function MyComponent() {
|
|
67
|
+
* const tenant = useTenantOrNull();
|
|
68
|
+
*
|
|
69
|
+
* if (!tenant) {
|
|
70
|
+
* return <div>Public content</div>;
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* return <div>Tenant-specific content for {tenant.id}</div>;
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare function useTenantOrNull(): TenantContext | null;
|
|
78
|
+
/**
|
|
79
|
+
* Hook to get just the tenant ID from context.
|
|
80
|
+
*
|
|
81
|
+
* Shorthand for `useTenant().id`.
|
|
82
|
+
*
|
|
83
|
+
* @returns The current tenant's ID
|
|
84
|
+
* @throws TenantRequiredError if tenant context is not available
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* function MyComponent() {
|
|
89
|
+
* const tenantId = useTenantId();
|
|
90
|
+
*
|
|
91
|
+
* return <div>Tenant: {tenantId}</div>;
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare function useTenantId(): string;
|
|
96
|
+
/**
|
|
97
|
+
* Hook to get just the tenant ID, or null if not available.
|
|
98
|
+
*
|
|
99
|
+
* @returns The current tenant's ID, or null
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```tsx
|
|
103
|
+
* function MyComponent() {
|
|
104
|
+
* const tenantId = useTenantIdOrNull();
|
|
105
|
+
* return <div>{tenantId ? `Tenant: ${tenantId}` : "Public"}</div>;
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export declare function useTenantIdOrNull(): string | null;
|
|
110
|
+
/**
|
|
111
|
+
* Hook to get the tenant's plan tier.
|
|
112
|
+
*
|
|
113
|
+
* @returns The current tenant's plan ("free", "pro", "enterprise"), or undefined
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```tsx
|
|
117
|
+
* function PremiumFeature() {
|
|
118
|
+
* const plan = useTenantPlan();
|
|
119
|
+
*
|
|
120
|
+
* if (plan === "free") {
|
|
121
|
+
* return <UpgradePrompt />;
|
|
122
|
+
* }
|
|
123
|
+
*
|
|
124
|
+
* return <Feature />;
|
|
125
|
+
* }
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export declare function useTenantPlan(): "free" | "pro" | "enterprise" | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* Hook to check if a specific feature is enabled for the tenant.
|
|
131
|
+
*
|
|
132
|
+
* @param feature The feature flag name
|
|
133
|
+
* @returns Whether the feature is enabled
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```tsx
|
|
137
|
+
* function DashboardPage() {
|
|
138
|
+
* const hasAdvancedAnalytics = useTenantFeature("advanced_analytics");
|
|
139
|
+
*
|
|
140
|
+
* return (
|
|
141
|
+
* <>
|
|
142
|
+
* {hasAdvancedAnalytics && <AdvancedAnalytics />}
|
|
143
|
+
* </>
|
|
144
|
+
* );
|
|
145
|
+
* }
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export declare function useTenantFeature(feature: string): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Hook to get a rate limit or quota for the tenant.
|
|
151
|
+
*
|
|
152
|
+
* @param limitName The limit name (e.g., "requests_per_minute", "storage_gb")
|
|
153
|
+
* @param defaultValue Default value if limit is not set
|
|
154
|
+
* @returns The limit value, or default if not set
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* function ApiDashboard() {
|
|
159
|
+
* const rateLimit = useTenantLimit("requests_per_minute", 100);
|
|
160
|
+
*
|
|
161
|
+
* return <div>Rate limit: {rateLimit} req/min</div>;
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
export declare function useTenantLimit(limitName: string, defaultValue?: number): number | undefined;
|
|
166
|
+
/**
|
|
167
|
+
* Higher-order component that requires tenant context.
|
|
168
|
+
*
|
|
169
|
+
* Wraps a component and throws an error if no tenant context is available.
|
|
170
|
+
* Useful as a safety check for tenant-specific features.
|
|
171
|
+
*
|
|
172
|
+
* @param Component The component to wrap
|
|
173
|
+
* @param errorFallback Optional fallback component if tenant is missing
|
|
174
|
+
* @returns A wrapped component
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```tsx
|
|
178
|
+
* function DashboardContent() {
|
|
179
|
+
* const tenantId = useTenantId();
|
|
180
|
+
* return <div>Dashboard for {tenantId}</div>;
|
|
181
|
+
* }
|
|
182
|
+
*
|
|
183
|
+
* export default withTenantGuard(DashboardContent);
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export declare function withTenantGuard<P extends object>(Component: React.ComponentType<P>, errorFallback?: React.ComponentType<{
|
|
187
|
+
error: Error;
|
|
188
|
+
}>): {
|
|
189
|
+
(props: P): React.ReactElement<P, string | React.JSXElementConstructor<any>> | React.ReactElement<{
|
|
190
|
+
error: Error;
|
|
191
|
+
}, string | React.JSXElementConstructor<any>>;
|
|
192
|
+
displayName: string;
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Component that renders children only if tenant context is available.
|
|
196
|
+
*
|
|
197
|
+
* @param children Children to render if tenant is available
|
|
198
|
+
* @param fallback Optional fallback if tenant is missing
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```tsx
|
|
202
|
+
* <TenantBoundary fallback={<PublicPage />}>
|
|
203
|
+
* <DashboardPage />
|
|
204
|
+
* </TenantBoundary>
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export declare function TenantBoundary({ children, fallback, }: {
|
|
208
|
+
children: React.ReactNode;
|
|
209
|
+
fallback?: React.ReactNode;
|
|
210
|
+
}): React.FunctionComponentElement<React.FragmentProps>;
|
|
211
|
+
//# sourceMappingURL=react.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAEA,OAAO,KAAoC,MAAM,OAAO,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAe7C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,EAAE,aAAa,CAAC;CACtB;;;;;;;WAMA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,IAAI,aAAa,CAUzC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,IAAI,aAAa,GAAG,IAAI,CAEtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAGpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAEjD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,8CAG5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG3F;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAC9C,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,EACjC,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;YAEpB,CAAC;eAFW,KAAK;;;EAqBnD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,uDAYA"}
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { createContext, useContext } from "react";
|
|
3
|
+
import { TenantRequiredError } from "./types";
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// React Context for Tenant — Client-side multi-tenancy
|
|
6
|
+
// =============================================================================
|
|
7
|
+
/**
|
|
8
|
+
* React context that provides tenant information to client-side components.
|
|
9
|
+
*
|
|
10
|
+
* Usually populated by middleware that reads the tenant from request headers
|
|
11
|
+
* or URL and passes it as a prop to the Next.js layout or page component.
|
|
12
|
+
*/
|
|
13
|
+
const TenantContextValue = createContext(null);
|
|
14
|
+
/**
|
|
15
|
+
* Provider component that wraps your app with tenant context.
|
|
16
|
+
*
|
|
17
|
+
* @param children Child components to wrap
|
|
18
|
+
* @param value The tenant context value (usually from server)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* // layout.tsx
|
|
23
|
+
* import { TenantProvider } from "@nebutra/tenant/react";
|
|
24
|
+
* import { getTenantContext } from "@/lib/tenant-server";
|
|
25
|
+
*
|
|
26
|
+
* export default async function RootLayout() {
|
|
27
|
+
* const tenant = await getTenantContext();
|
|
28
|
+
*
|
|
29
|
+
* return (
|
|
30
|
+
* <TenantProvider value={tenant}>
|
|
31
|
+
* <MainApp />
|
|
32
|
+
* </TenantProvider>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function TenantProvider({ children, value, }) {
|
|
38
|
+
if (!value || !value.id) {
|
|
39
|
+
console.warn("TenantProvider: No tenant value provided");
|
|
40
|
+
}
|
|
41
|
+
return React.createElement(TenantContextValue.Provider, { value: value ?? null }, children);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Hook to access the current tenant context from client-side components.
|
|
45
|
+
*
|
|
46
|
+
* Throws an error if no tenant context is available.
|
|
47
|
+
* Use `useTenantOrNull()` for optional tenant contexts.
|
|
48
|
+
*
|
|
49
|
+
* @returns The current tenant context
|
|
50
|
+
* @throws TenantRequiredError if tenant context is not available
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* function MyComponent() {
|
|
55
|
+
* const tenant = useTenant();
|
|
56
|
+
*
|
|
57
|
+
* return <div>Current tenant: {tenant.id}</div>;
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function useTenant() {
|
|
62
|
+
const context = useContext(TenantContextValue);
|
|
63
|
+
if (!context) {
|
|
64
|
+
throw new TenantRequiredError("useTenant must be used within a TenantProvider that has a tenant value");
|
|
65
|
+
}
|
|
66
|
+
return context;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Hook to access the current tenant context or null if not available.
|
|
70
|
+
*
|
|
71
|
+
* Useful for components that work in both tenant and non-tenant contexts
|
|
72
|
+
* (e.g., public pages, webhooks, etc).
|
|
73
|
+
*
|
|
74
|
+
* @returns The current tenant context, or null
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```tsx
|
|
78
|
+
* function MyComponent() {
|
|
79
|
+
* const tenant = useTenantOrNull();
|
|
80
|
+
*
|
|
81
|
+
* if (!tenant) {
|
|
82
|
+
* return <div>Public content</div>;
|
|
83
|
+
* }
|
|
84
|
+
*
|
|
85
|
+
* return <div>Tenant-specific content for {tenant.id}</div>;
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function useTenantOrNull() {
|
|
90
|
+
return useContext(TenantContextValue);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Hook to get just the tenant ID from context.
|
|
94
|
+
*
|
|
95
|
+
* Shorthand for `useTenant().id`.
|
|
96
|
+
*
|
|
97
|
+
* @returns The current tenant's ID
|
|
98
|
+
* @throws TenantRequiredError if tenant context is not available
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```tsx
|
|
102
|
+
* function MyComponent() {
|
|
103
|
+
* const tenantId = useTenantId();
|
|
104
|
+
*
|
|
105
|
+
* return <div>Tenant: {tenantId}</div>;
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function useTenantId() {
|
|
110
|
+
const tenant = useTenant();
|
|
111
|
+
return tenant.id;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Hook to get just the tenant ID, or null if not available.
|
|
115
|
+
*
|
|
116
|
+
* @returns The current tenant's ID, or null
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```tsx
|
|
120
|
+
* function MyComponent() {
|
|
121
|
+
* const tenantId = useTenantIdOrNull();
|
|
122
|
+
* return <div>{tenantId ? `Tenant: ${tenantId}` : "Public"}</div>;
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function useTenantIdOrNull() {
|
|
127
|
+
return useTenantOrNull()?.id ?? null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Hook to get the tenant's plan tier.
|
|
131
|
+
*
|
|
132
|
+
* @returns The current tenant's plan ("free", "pro", "enterprise"), or undefined
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```tsx
|
|
136
|
+
* function PremiumFeature() {
|
|
137
|
+
* const plan = useTenantPlan();
|
|
138
|
+
*
|
|
139
|
+
* if (plan === "free") {
|
|
140
|
+
* return <UpgradePrompt />;
|
|
141
|
+
* }
|
|
142
|
+
*
|
|
143
|
+
* return <Feature />;
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function useTenantPlan() {
|
|
148
|
+
const tenant = useTenant();
|
|
149
|
+
return tenant.plan;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Hook to check if a specific feature is enabled for the tenant.
|
|
153
|
+
*
|
|
154
|
+
* @param feature The feature flag name
|
|
155
|
+
* @returns Whether the feature is enabled
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```tsx
|
|
159
|
+
* function DashboardPage() {
|
|
160
|
+
* const hasAdvancedAnalytics = useTenantFeature("advanced_analytics");
|
|
161
|
+
*
|
|
162
|
+
* return (
|
|
163
|
+
* <>
|
|
164
|
+
* {hasAdvancedAnalytics && <AdvancedAnalytics />}
|
|
165
|
+
* </>
|
|
166
|
+
* );
|
|
167
|
+
* }
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
export function useTenantFeature(feature) {
|
|
171
|
+
const tenant = useTenant();
|
|
172
|
+
return tenant.features?.includes(feature) ?? false;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Hook to get a rate limit or quota for the tenant.
|
|
176
|
+
*
|
|
177
|
+
* @param limitName The limit name (e.g., "requests_per_minute", "storage_gb")
|
|
178
|
+
* @param defaultValue Default value if limit is not set
|
|
179
|
+
* @returns The limit value, or default if not set
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```tsx
|
|
183
|
+
* function ApiDashboard() {
|
|
184
|
+
* const rateLimit = useTenantLimit("requests_per_minute", 100);
|
|
185
|
+
*
|
|
186
|
+
* return <div>Rate limit: {rateLimit} req/min</div>;
|
|
187
|
+
* }
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export function useTenantLimit(limitName, defaultValue) {
|
|
191
|
+
const tenant = useTenant();
|
|
192
|
+
return tenant.limits?.[limitName] ?? defaultValue;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Higher-order component that requires tenant context.
|
|
196
|
+
*
|
|
197
|
+
* Wraps a component and throws an error if no tenant context is available.
|
|
198
|
+
* Useful as a safety check for tenant-specific features.
|
|
199
|
+
*
|
|
200
|
+
* @param Component The component to wrap
|
|
201
|
+
* @param errorFallback Optional fallback component if tenant is missing
|
|
202
|
+
* @returns A wrapped component
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```tsx
|
|
206
|
+
* function DashboardContent() {
|
|
207
|
+
* const tenantId = useTenantId();
|
|
208
|
+
* return <div>Dashboard for {tenantId}</div>;
|
|
209
|
+
* }
|
|
210
|
+
*
|
|
211
|
+
* export default withTenantGuard(DashboardContent);
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export function withTenantGuard(Component, errorFallback) {
|
|
215
|
+
const WrappedComponent = (props) => {
|
|
216
|
+
try {
|
|
217
|
+
// Verify tenant context is available by calling hook
|
|
218
|
+
useTenant();
|
|
219
|
+
return React.createElement(Component, props);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
if (errorFallback) {
|
|
223
|
+
const ErrorComponent = errorFallback;
|
|
224
|
+
return React.createElement(ErrorComponent, { error: err });
|
|
225
|
+
}
|
|
226
|
+
// Re-throw if no fallback provided
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
WrappedComponent.displayName = `withTenantGuard(${Component.displayName || Component.name})`;
|
|
231
|
+
return WrappedComponent;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Component that renders children only if tenant context is available.
|
|
235
|
+
*
|
|
236
|
+
* @param children Children to render if tenant is available
|
|
237
|
+
* @param fallback Optional fallback if tenant is missing
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```tsx
|
|
241
|
+
* <TenantBoundary fallback={<PublicPage />}>
|
|
242
|
+
* <DashboardPage />
|
|
243
|
+
* </TenantBoundary>
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
export function TenantBoundary({ children, fallback, }) {
|
|
247
|
+
const tenant = useTenantOrNull();
|
|
248
|
+
if (!tenant) {
|
|
249
|
+
return React.createElement(React.Fragment, null, fallback ?? React.createElement("div", null, "No tenant context available"));
|
|
250
|
+
}
|
|
251
|
+
return React.createElement(React.Fragment, null, children);
|
|
252
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { TenantResolver } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal shape this resolver needs from a session object.
|
|
4
|
+
*
|
|
5
|
+
* Structurally compatible with `Session` from `@nebutra/auth` but defined
|
|
6
|
+
* locally so `@nebutra/tenant` stays IAM-neutral — there is no hard dep on
|
|
7
|
+
* `@nebutra/auth`. Callers pass in their own session getter (typically
|
|
8
|
+
* `(req) => auth.getSession(req)`), keeping the dependency direction one-way:
|
|
9
|
+
* apps depend on both packages; neither package depends on the other.
|
|
10
|
+
*/
|
|
11
|
+
export interface AuthSessionLike {
|
|
12
|
+
organizationId?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Function that retrieves an auth session from a resolver-input request.
|
|
16
|
+
*
|
|
17
|
+
* Receives the same request envelope as any other {@link TenantResolver}
|
|
18
|
+
* (`{ headers?, url?, token?, apiKey? }`). Returns either an `AuthSessionLike`
|
|
19
|
+
* or `null` when no session exists. May be sync or async.
|
|
20
|
+
*/
|
|
21
|
+
export type SessionGetter = (req: Parameters<TenantResolver>[0]) => Promise<AuthSessionLike | null> | AuthSessionLike | null;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve `tenantId` from an auth session's `organizationId`.
|
|
24
|
+
*
|
|
25
|
+
* This is the canonical bridge between `Session.organizationId` (from
|
|
26
|
+
* `@nebutra/auth`) and the tenant context. It is the standard second link in
|
|
27
|
+
* the resolver chain — after `fromHeader` (service-to-service) and before any
|
|
28
|
+
* fallback strategy.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { createAuth } from "@nebutra/auth/server";
|
|
33
|
+
* import { tenantMiddleware, compose, fromHeader, fromAuthSession } from "@nebutra/tenant";
|
|
34
|
+
*
|
|
35
|
+
* const auth = await createAuth({ provider: "better-auth" });
|
|
36
|
+
*
|
|
37
|
+
* app.use(
|
|
38
|
+
* tenantMiddleware({
|
|
39
|
+
* resolver: compose(
|
|
40
|
+
* fromHeader("x-tenant-id"), // service-to-service wins
|
|
41
|
+
* fromAuthSession(() => auth.getSession()), // then user-session
|
|
42
|
+
* ),
|
|
43
|
+
* }),
|
|
44
|
+
* );
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function fromAuthSession(getSession: SessionGetter): TenantResolver;
|
|
48
|
+
//# sourceMappingURL=from-auth-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"from-auth-session.d.ts","sourceRoot":"","sources":["../../src/resolvers/from-auth-session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAM/C;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,GAAG,EAAE,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,KAC/B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,eAAe,GAAG,IAAI,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,aAAa,GAAG,cAAc,CAgBzE"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { logger } from "@nebutra/logger";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve `tenantId` from an auth session's `organizationId`.
|
|
4
|
+
*
|
|
5
|
+
* This is the canonical bridge between `Session.organizationId` (from
|
|
6
|
+
* `@nebutra/auth`) and the tenant context. It is the standard second link in
|
|
7
|
+
* the resolver chain — after `fromHeader` (service-to-service) and before any
|
|
8
|
+
* fallback strategy.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { createAuth } from "@nebutra/auth/server";
|
|
13
|
+
* import { tenantMiddleware, compose, fromHeader, fromAuthSession } from "@nebutra/tenant";
|
|
14
|
+
*
|
|
15
|
+
* const auth = await createAuth({ provider: "better-auth" });
|
|
16
|
+
*
|
|
17
|
+
* app.use(
|
|
18
|
+
* tenantMiddleware({
|
|
19
|
+
* resolver: compose(
|
|
20
|
+
* fromHeader("x-tenant-id"), // service-to-service wins
|
|
21
|
+
* fromAuthSession(() => auth.getSession()), // then user-session
|
|
22
|
+
* ),
|
|
23
|
+
* }),
|
|
24
|
+
* );
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function fromAuthSession(getSession) {
|
|
28
|
+
return async (req) => {
|
|
29
|
+
// Intentionally NOT wrapped in try/catch — errors propagate to the
|
|
30
|
+
// caller (typically the `compose` chain, which logs + continues to the
|
|
31
|
+
// next resolver). Silently swallowing here would mask real auth bugs.
|
|
32
|
+
const session = await Promise.resolve(getSession(req));
|
|
33
|
+
if (session?.organizationId) {
|
|
34
|
+
logger.debug("Tenant resolved from auth session", {
|
|
35
|
+
tenantId: session.organizationId,
|
|
36
|
+
});
|
|
37
|
+
return session.organizationId;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { TenantResolver } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Extract tenant ID from an HTTP header.
|
|
4
|
+
*
|
|
5
|
+
* Default strategy for API gateways. Header name is configurable.
|
|
6
|
+
*
|
|
7
|
+
* @param headerName The header name to look for (default: "x-tenant-id")
|
|
8
|
+
* @returns A resolver function
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const resolver = fromHeader("x-tenant-id");
|
|
13
|
+
* const tenantId = await resolver({ headers: { "x-tenant-id": "org_123" } });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function fromHeader(headerName?: string): TenantResolver;
|
|
17
|
+
/**
|
|
18
|
+
* Extract tenant ID from a subdomain using a regex pattern.
|
|
19
|
+
*
|
|
20
|
+
* Useful for SaaS products with tenant subdomains (e.g., `acme.app.nebutra.com`).
|
|
21
|
+
*
|
|
22
|
+
* @param pattern A regex pattern with a capture group for tenant ID
|
|
23
|
+
* @returns A resolver function
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* // Extract "acme" from "acme.app.nebutra.com"
|
|
28
|
+
* const resolver = fromSubdomain("^([a-z0-9-]+)\\.app\\.nebutra\\.com$");
|
|
29
|
+
* const tenantId = await resolver({ url: "https://acme.app.nebutra.com/api" });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function fromSubdomain(pattern: string): TenantResolver;
|
|
33
|
+
/**
|
|
34
|
+
* Extract tenant ID from a URL path prefix.
|
|
35
|
+
*
|
|
36
|
+
* Useful for multi-tenant apps with path-based routing (e.g., `/org/acme/...`).
|
|
37
|
+
*
|
|
38
|
+
* @param prefix The path prefix pattern (e.g., "/org/:tenantId" or "/org/([^/]+)")
|
|
39
|
+
* @returns A resolver function
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* // Extract tenant ID from "/org/acme/..."
|
|
44
|
+
* const resolver = fromPath("/org/:tenantId");
|
|
45
|
+
* const tenantId = await resolver({ url: "https://app.nebutra.com/org/acme/dashboard" });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function fromPath(prefix: string): TenantResolver;
|
|
49
|
+
/**
|
|
50
|
+
* Extract tenant ID from a JWT token claim.
|
|
51
|
+
*
|
|
52
|
+
* Useful for authentication services that embed tenant info in the token.
|
|
53
|
+
*
|
|
54
|
+
* @param claimName The JWT claim name (e.g., "tenant_id", "org_id")
|
|
55
|
+
* @returns A resolver function
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* // Extract from decoded JWT { sub: "user_123", tenant_id: "org_456" }
|
|
60
|
+
* const resolver = fromJwtClaim("tenant_id");
|
|
61
|
+
* const tenantId = await resolver({ token: "eyJhbGc..." });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function fromJwtClaim(claimName: string): TenantResolver;
|
|
65
|
+
/**
|
|
66
|
+
* Resolve tenant ID from an API key by looking it up in a function (e.g., database).
|
|
67
|
+
*
|
|
68
|
+
* Useful for service-to-service authentication or API clients.
|
|
69
|
+
*
|
|
70
|
+
* @param lookupFn Async function that takes an API key and returns the tenant ID
|
|
71
|
+
* @returns A resolver function
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* // Look up API key in database
|
|
76
|
+
* const resolver = fromApiKey(async (apiKey) => {
|
|
77
|
+
* const key = await db.apiKey.findUnique({ where: { key: apiKey } });
|
|
78
|
+
* return key?.tenantId ?? null;
|
|
79
|
+
* });
|
|
80
|
+
* const tenantId = await resolver({ apiKey: "sk_123" });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function fromApiKey(lookupFn: (apiKey: string) => Promise<string | null>): TenantResolver;
|
|
84
|
+
/**
|
|
85
|
+
* Compose multiple tenant resolvers with fallback behavior.
|
|
86
|
+
*
|
|
87
|
+
* Tries each resolver in order; returns the first successful match.
|
|
88
|
+
* If all resolvers return null, returns null.
|
|
89
|
+
*
|
|
90
|
+
* @param resolvers Array of resolver functions
|
|
91
|
+
* @returns A composite resolver function
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* // Try header first, then subdomain, then path
|
|
96
|
+
* const resolver = compose(
|
|
97
|
+
* fromHeader("x-tenant-id"),
|
|
98
|
+
* fromSubdomain("^([a-z0-9-]+)\\.app\\.nebutra\\.com$"),
|
|
99
|
+
* fromPath("/org/:tenantId")
|
|
100
|
+
* );
|
|
101
|
+
*
|
|
102
|
+
* const tenantId = await resolver({
|
|
103
|
+
* headers: { "x-tenant-id": "org_123" },
|
|
104
|
+
* url: "https://acme.app.nebutra.com/org/xyz/dashboard"
|
|
105
|
+
* });
|
|
106
|
+
* // Returns "org_123" (first match)
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export declare function compose(...resolvers: TenantResolver[]): TenantResolver;
|
|
110
|
+
//# sourceMappingURL=resolvers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../src/resolvers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAsD9C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,UAAU,GAAE,MAAsB,GAAG,cAAc,CAW7E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAsB7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAwBvD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAsB9D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,cAAc,CAiB/F;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,OAAO,CAAC,GAAG,SAAS,EAAE,cAAc,EAAE,GAAG,cAAc,CAiBtE"}
|