@tenantscale/sdk 0.2.0 → 0.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/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.d.ts +6 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +8 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/query-guard.d.ts +31 -0
- package/dist/middleware/query-guard.d.ts.map +1 -0
- package/dist/middleware/query-guard.js +137 -0
- package/dist/middleware/query-guard.js.map +1 -0
- package/dist/middleware/user-auth.d.ts +53 -0
- package/dist/middleware/user-auth.d.ts.map +1 -0
- package/dist/middleware/user-auth.js +154 -0
- package/dist/middleware/user-auth.js.map +1 -0
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/tenant-scale.d.ts +46 -0
- package/dist/tenant-scale.d.ts.map +1 -1
- package/dist/tenant-scale.js +111 -6
- package/dist/tenant-scale.js.map +1 -1
- package/dist/testing/index.d.ts +111 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +198 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types.d.ts +29 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +1 -1
- package/package.json +72 -63
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type Tenant, type TenantUser, type TenantScaleOptions, type TenantResolver, type AuditEvent, type FeatureCheck, type ImpersonationSession, type PlanTier, type TenantSettings, TenantScaleError, PlanLimitError, UnauthorizedError, } from './types.js';
|
|
1
|
+
export { type Tenant, type TenantUser, type TenantScaleOptions, type TenantScaleContext, type TenantResolver, type AuditEvent, type FeatureCheck, type ImpersonationSession, type PlanTier, type TenantSettings, type UserTenantContext, TenantScaleError, PlanLimitError, UnauthorizedError, } from './types.js';
|
|
2
2
|
export { TenantScale } from './tenant-scale.js';
|
|
3
3
|
export { TenantClient } from './tenant.js';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,GAClB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,GAClB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,oCAAoC;AACpC,yDAAyD;AAEzD,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,oCAAoC;AACpC,yDAAyD;AAEzD,OAAO,EAYL,gBAAgB,EAChB,cAAc,EACd,iBAAiB,GAClB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { tenantMiddleware, TenantClient } from './hono.js';
|
|
2
|
+
export { expressMiddleware } from './express.js';
|
|
3
|
+
export { createTenantSafeClient } from './query-guard.js';
|
|
4
|
+
export { createUserTenantMiddleware, requireUserRole } from './user-auth.js';
|
|
5
|
+
export type { UserTenantMiddlewareOptions } from './user-auth.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AACzD,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAC5E,YAAY,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// ──────────────────────────────────────────────────────
|
|
2
|
+
// Middleware barrel — import via '@tenantscale/sdk/middleware'
|
|
3
|
+
// ──────────────────────────────────────────────────────
|
|
4
|
+
export { tenantMiddleware, TenantClient } from './hono.js';
|
|
5
|
+
export { expressMiddleware } from './express.js';
|
|
6
|
+
export { createTenantSafeClient } from './query-guard.js';
|
|
7
|
+
export { createUserTenantMiddleware, requireUserRole } from './user-auth.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,+DAA+D;AAC/D,yDAAyD;AAEzD,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AACzD,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type GuardMode = 'strict' | 'warn';
|
|
2
|
+
export interface GuardOptions {
|
|
3
|
+
/** The tenant ID to enforce on all queries */
|
|
4
|
+
tenantId: string;
|
|
5
|
+
/**
|
|
6
|
+
* strict — throw an error (default, prevents data leaks)
|
|
7
|
+
* warn — log a warning and proceed (use during migration)
|
|
8
|
+
*/
|
|
9
|
+
mode?: GuardMode;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Wrap a Supabase client so every query on every table must include
|
|
13
|
+
* `.eq('tenant_id', <tenantId>)` or explicitly call `.bypassIsolation()`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const supabase = createClient(url, key)
|
|
18
|
+
* const db = createTenantSafeClient(supabase, { tenantId: 'tenant-123' })
|
|
19
|
+
*
|
|
20
|
+
* // ✅ Works:
|
|
21
|
+
* await db.from('projects').select('*').eq('tenant_id', 'tenant-123')
|
|
22
|
+
*
|
|
23
|
+
* // ❌ Throws:
|
|
24
|
+
* await db.from('projects').select('*')
|
|
25
|
+
*
|
|
26
|
+
* // ✅ Admin bypass:
|
|
27
|
+
* await db.from('plans').select('*').bypassIsolation()
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function createTenantSafeClient(supabase: any, options: GuardOptions): any;
|
|
31
|
+
//# sourceMappingURL=query-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-guard.d.ts","sourceRoot":"","sources":["../../src/middleware/query-guard.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEzC,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,GAAG,EACb,OAAO,EAAE,YAAY,GACpB,GAAG,CAiBL"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// ──────────────────────────────────────────────────────
|
|
2
|
+
// Query Guard — runtime tenant isolation enforcement
|
|
3
|
+
// Wraps a Supabase client and rejects queries that don't
|
|
4
|
+
// filter by tenant_id. Catches the #1 multi-tenant bug
|
|
5
|
+
// at dev time instead of during a security audit.
|
|
6
|
+
// ──────────────────────────────────────────────────────
|
|
7
|
+
/**
|
|
8
|
+
* Wrap a Supabase client so every query on every table must include
|
|
9
|
+
* `.eq('tenant_id', <tenantId>)` or explicitly call `.bypassIsolation()`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const supabase = createClient(url, key)
|
|
14
|
+
* const db = createTenantSafeClient(supabase, { tenantId: 'tenant-123' })
|
|
15
|
+
*
|
|
16
|
+
* // ✅ Works:
|
|
17
|
+
* await db.from('projects').select('*').eq('tenant_id', 'tenant-123')
|
|
18
|
+
*
|
|
19
|
+
* // ❌ Throws:
|
|
20
|
+
* await db.from('projects').select('*')
|
|
21
|
+
*
|
|
22
|
+
* // ✅ Admin bypass:
|
|
23
|
+
* await db.from('plans').select('*').bypassIsolation()
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function createTenantSafeClient(supabase, options) {
|
|
27
|
+
const { tenantId, mode = 'strict' } = options;
|
|
28
|
+
const strict = mode === 'strict';
|
|
29
|
+
return new Proxy(supabase, {
|
|
30
|
+
get(target, prop, receiver) {
|
|
31
|
+
if (prop === 'from') {
|
|
32
|
+
return (table) => {
|
|
33
|
+
const rawQb = target.from(table);
|
|
34
|
+
return wrapBuilder(rawQb, tenantId, strict, table);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const value = Reflect.get(target, prop, receiver);
|
|
38
|
+
if (typeof value === 'function')
|
|
39
|
+
return value.bind(target);
|
|
40
|
+
return value;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// ── Methods that return a new query builder (must wrap results) ──
|
|
45
|
+
const CHAIN_METHODS = new Set([
|
|
46
|
+
'select', 'insert', 'update', 'delete', 'upsert',
|
|
47
|
+
'eq', 'neq', 'gt', 'gte', 'lt', 'lte',
|
|
48
|
+
'like', 'ilike', 'is', 'isNot', 'in', 'not',
|
|
49
|
+
'or', 'and', 'filter', 'match',
|
|
50
|
+
'contains', 'containedBy', 'overlaps',
|
|
51
|
+
'textSearch', 'csv',
|
|
52
|
+
'order', 'limit', 'range',
|
|
53
|
+
]);
|
|
54
|
+
function wrapBuilder(rootQb, tenantId, strict, table) {
|
|
55
|
+
let hasTenantFilter = false;
|
|
56
|
+
let bypassed = false;
|
|
57
|
+
function guardCheck() {
|
|
58
|
+
if (bypassed)
|
|
59
|
+
return;
|
|
60
|
+
if (hasTenantFilter)
|
|
61
|
+
return;
|
|
62
|
+
const msg = `[TenantScale] Query on "${table}" is missing a tenant_id filter. ` +
|
|
63
|
+
`Add .eq('tenant_id', '${tenantId}') to scope this query to the current tenant, ` +
|
|
64
|
+
`or use .bypassIsolation() for admin-only cross-tenant queries.`;
|
|
65
|
+
if (strict)
|
|
66
|
+
throw new TypeError(msg);
|
|
67
|
+
console.warn(msg);
|
|
68
|
+
}
|
|
69
|
+
// Build the proxy once, then reuse it for wrapped results.
|
|
70
|
+
// Chain methods return a new proxy on the same state.
|
|
71
|
+
function makeProxy(target) {
|
|
72
|
+
return new Proxy(target, proxyHandler);
|
|
73
|
+
}
|
|
74
|
+
// `.single()` and `.maybeSingle()` modify internal state and return `this`.
|
|
75
|
+
// We intercept them to return our proxy-wrapped `this`.
|
|
76
|
+
const TERMINAL_THIS = new Set(['single', 'maybeSingle']);
|
|
77
|
+
const proxyHandler = {
|
|
78
|
+
get(target, prop) {
|
|
79
|
+
// ── Track tenant_id filter ──
|
|
80
|
+
if (prop === 'eq') {
|
|
81
|
+
return (column, value) => {
|
|
82
|
+
if (column === 'tenant_id')
|
|
83
|
+
hasTenantFilter = true;
|
|
84
|
+
const nextQb = target.eq(column, value);
|
|
85
|
+
return makeProxy(nextQb);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// ── Bypass for admin routes ──
|
|
89
|
+
if (prop === 'bypassIsolation') {
|
|
90
|
+
return () => {
|
|
91
|
+
bypassed = true;
|
|
92
|
+
// Return a transparent pass-through proxy
|
|
93
|
+
return new Proxy(target, {
|
|
94
|
+
get(t, p) {
|
|
95
|
+
if (p === 'bypassIsolation')
|
|
96
|
+
return () => t;
|
|
97
|
+
const v = Reflect.get(t, p);
|
|
98
|
+
if (typeof v === 'function')
|
|
99
|
+
return v.bind(t);
|
|
100
|
+
return v;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// ── Intercept .then() — the HTTP request ──
|
|
106
|
+
if (prop === 'then') {
|
|
107
|
+
return (resolve, reject) => {
|
|
108
|
+
guardCheck();
|
|
109
|
+
return target.then(resolve, reject);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// ── Chain methods — wrap their return values ──
|
|
113
|
+
if (typeof prop === 'string' && CHAIN_METHODS.has(prop)) {
|
|
114
|
+
return (...args) => {
|
|
115
|
+
const nextQb = target[prop](...args);
|
|
116
|
+
return makeProxy(nextQb);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// ── Terminal-this methods (.single(), .maybeSingle()) ──
|
|
120
|
+
if (typeof prop === 'string' && TERMINAL_THIS.has(prop)) {
|
|
121
|
+
return (...args) => {
|
|
122
|
+
const result = target[prop](...args);
|
|
123
|
+
// single() returns `this` (the PostgrestFilterBuilder)
|
|
124
|
+
// Wrap it so further chaining still works
|
|
125
|
+
return makeProxy(result);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// ── Everything else passes through ──
|
|
129
|
+
const value = Reflect.get(target, prop, target);
|
|
130
|
+
if (typeof value === 'function')
|
|
131
|
+
return value.bind(target);
|
|
132
|
+
return value;
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
return makeProxy(rootQb);
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=query-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-guard.js","sourceRoot":"","sources":["../../src/middleware/query-guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Context, type MiddlewareHandler } from 'hono';
|
|
2
|
+
import { type Tenant, type TenantScaleOptions, type UserTenantContext } from '../types.js';
|
|
3
|
+
declare module 'hono' {
|
|
4
|
+
interface ContextVariableMap {
|
|
5
|
+
userTenant: UserTenantContext;
|
|
6
|
+
tenant: Tenant;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Options for the user tenant middleware */
|
|
10
|
+
export interface UserTenantMiddlewareOptions extends TenantScaleOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Function that extracts the authenticated user's ID from the request.
|
|
13
|
+
* Return null/undefined if the user is not authenticated.
|
|
14
|
+
*/
|
|
15
|
+
getUser: (c: Context) => string | null | undefined | Promise<string | null | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* What to do when no authenticated user is found.
|
|
18
|
+
* - 'reject': Return 401 Unauthorized (default)
|
|
19
|
+
* - 'skip': Call next() without setting userTenant — useful when some routes are public
|
|
20
|
+
*/
|
|
21
|
+
onUnauthenticated?: 'reject' | 'skip';
|
|
22
|
+
/**
|
|
23
|
+
* Optional: How to determine which tenant to use when a user belongs to multiple.
|
|
24
|
+
* - 'first': Use the first tenant (default)
|
|
25
|
+
* - 'header': Read tenant from a custom header (specify via tenantHeader)
|
|
26
|
+
* - 'domain': Resolve from the request's host/domain
|
|
27
|
+
*/
|
|
28
|
+
multiTenantStrategy?: 'first' | 'header' | 'domain';
|
|
29
|
+
/** Header name to use when multiTenantStrategy is 'header' (default: 'X-Tenant-ID') */
|
|
30
|
+
tenantHeader?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Role values that count as "admin" for the `isAdmin` flag.
|
|
33
|
+
* Default: ['owner', 'admin']
|
|
34
|
+
* Set this to match your own role model.
|
|
35
|
+
*/
|
|
36
|
+
adminRoles?: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Creates Hono middleware that resolves a user's tenant context
|
|
40
|
+
* from your existing auth system. BYOA — Bring Your Own Auth.
|
|
41
|
+
*
|
|
42
|
+
* The developer provides `getUser(c)` which extracts the user ID
|
|
43
|
+
* from their auth provider. TenantScale looks up the tenant membership
|
|
44
|
+
* and attaches the full context to the request.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createUserTenantMiddleware(options: UserTenantMiddlewareOptions): MiddlewareHandler;
|
|
47
|
+
/**
|
|
48
|
+
* Role guard middleware — restricts routes based on user role.
|
|
49
|
+
* Must be used after `createUserTenantMiddleware()`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function requireUserRole(...roles: string[]): MiddlewareHandler;
|
|
52
|
+
export { type UserTenantContext };
|
|
53
|
+
//# sourceMappingURL=user-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-auth.d.ts","sourceRoot":"","sources":["../../src/middleware/user-auth.ts"],"names":[],"mappings":"AAoCA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAE3D,OAAO,EACL,KAAK,MAAM,EACX,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EAGvB,MAAM,aAAa,CAAA;AAGpB,OAAO,QAAQ,MAAM,CAAC;IACpB,UAAU,kBAAkB;QAC1B,UAAU,EAAE,iBAAiB,CAAA;QAC7B,MAAM,EAAE,MAAM,CAAA;KACf;CACF;AAED,6CAA6C;AAC7C,MAAM,WAAW,2BAA4B,SAAQ,kBAAkB;IACrE;;;OAGG;IACH,OAAO,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;IAEvF;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;IAErC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAA;IAEnD,uFAAuF;IACvF,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,2BAA2B,GACnC,iBAAiB,CA0HnB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,iBAAiB,CAarE;AAED,OAAO,EAAE,KAAK,iBAAiB,EAAE,CAAA"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// ──────────────────────────────────────────────────────
|
|
2
|
+
// User Auth Middleware — Bring Your Own Auth (BYOA)
|
|
3
|
+
// ──────────────────────────────────────────────────────
|
|
4
|
+
// Developers keep their auth provider (Clerk, Auth0, Supabase Auth,
|
|
5
|
+
// Firebase, raw JWT, session cookies). TenantScale resolves the
|
|
6
|
+
// tenant context from the authenticated user.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// ```ts
|
|
10
|
+
// import { Hono } from 'hono'
|
|
11
|
+
// import { createUserTenantMiddleware } from '@tenantscale/sdk/user'
|
|
12
|
+
//
|
|
13
|
+
// const app = new Hono()
|
|
14
|
+
//
|
|
15
|
+
// // Developer's auth middleware (their own provider)
|
|
16
|
+
// app.use('*', async (c, next) => {
|
|
17
|
+
// const session = await getMyAuthSession(c) // Clerk, Auth0, etc.
|
|
18
|
+
// c.set('userId', session?.userId)
|
|
19
|
+
// await next()
|
|
20
|
+
// })
|
|
21
|
+
//
|
|
22
|
+
// // TenantScale resolves tenant from the user
|
|
23
|
+
// app.use('/api/*', createUserTenantMiddleware({
|
|
24
|
+
// apiKey: process.env.TENANTSCALE_API_KEY,
|
|
25
|
+
// baseUrl: 'https://api.tenantscale.com',
|
|
26
|
+
// getUser: (c) => c.get('userId'),
|
|
27
|
+
// onUnauthenticated: 'reject', // or 'skip'
|
|
28
|
+
// }))
|
|
29
|
+
//
|
|
30
|
+
// app.get('/api/projects', (c) => {
|
|
31
|
+
// const ctx = c.var.userTenant
|
|
32
|
+
// // ctx.userId, ctx.tenantId, ctx.role, ctx.tenant
|
|
33
|
+
// })
|
|
34
|
+
// ```
|
|
35
|
+
// ──────────────────────────────────────────────────────
|
|
36
|
+
import { TenantClient } from '../tenant.js';
|
|
37
|
+
import { UnauthorizedError, TenantScaleError, } from '../types.js';
|
|
38
|
+
/**
|
|
39
|
+
* Creates Hono middleware that resolves a user's tenant context
|
|
40
|
+
* from your existing auth system. BYOA — Bring Your Own Auth.
|
|
41
|
+
*
|
|
42
|
+
* The developer provides `getUser(c)` which extracts the user ID
|
|
43
|
+
* from their auth provider. TenantScale looks up the tenant membership
|
|
44
|
+
* and attaches the full context to the request.
|
|
45
|
+
*/
|
|
46
|
+
export function createUserTenantMiddleware(options) {
|
|
47
|
+
const { getUser, onUnauthenticated = 'reject', multiTenantStrategy = 'first', tenantHeader = 'X-Tenant-ID', adminRoles = ['owner', 'admin'], ...tsOptions } = options;
|
|
48
|
+
const client = new TenantClient(tsOptions);
|
|
49
|
+
return async (c, next) => {
|
|
50
|
+
// 1. Get the authenticated user from the developer's auth
|
|
51
|
+
const userId = await getUser(c);
|
|
52
|
+
if (!userId) {
|
|
53
|
+
if (onUnauthenticated === 'skip') {
|
|
54
|
+
// Let the request pass through — public routes handled downstream
|
|
55
|
+
await next();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
return c.json({ error: 'Authentication required' }, 401);
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
// 2. Resolve tenant membership from TenantScale API
|
|
62
|
+
const baseUrl = tsOptions.baseUrl ?? 'https://api.tenantscale.com';
|
|
63
|
+
const apiKey = tsOptions.apiKey;
|
|
64
|
+
// Fetch user's tenant memberships
|
|
65
|
+
const res = await fetch(`${baseUrl}/v1/users/${userId}/tenants`, {
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: `Bearer ${apiKey}`,
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
throw new TenantScaleError(`Failed to resolve user tenants: ${res.statusText}`, 'USER_TENANT_RESOLVE_FAILED', res.status);
|
|
73
|
+
}
|
|
74
|
+
const body = await res.json();
|
|
75
|
+
if (!body.tenants || body.tenants.length === 0) {
|
|
76
|
+
return c.json({ error: 'User is not associated with any tenant' }, 403);
|
|
77
|
+
}
|
|
78
|
+
// 3. Determine which tenant to use
|
|
79
|
+
let selectedTenant;
|
|
80
|
+
if (body.tenants.length === 1 || multiTenantStrategy === 'first') {
|
|
81
|
+
selectedTenant = body.tenants[0];
|
|
82
|
+
}
|
|
83
|
+
else if (multiTenantStrategy === 'header') {
|
|
84
|
+
const requestedTenantId = c.req.header(tenantHeader);
|
|
85
|
+
const match = body.tenants.find(t => t.tenantId === requestedTenantId || t.tenantSlug === requestedTenantId);
|
|
86
|
+
if (!match) {
|
|
87
|
+
return c.json({
|
|
88
|
+
error: `Tenant not found or access denied`,
|
|
89
|
+
available_tenants: body.tenants.map(t => ({
|
|
90
|
+
id: t.tenantId,
|
|
91
|
+
name: t.tenantName,
|
|
92
|
+
slug: t.tenantSlug,
|
|
93
|
+
role: t.role,
|
|
94
|
+
})),
|
|
95
|
+
}, 404);
|
|
96
|
+
}
|
|
97
|
+
selectedTenant = match;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// 'domain' strategy — resolve from host
|
|
101
|
+
const host = c.req.header('host') ?? '';
|
|
102
|
+
const match = body.tenants.find(t => t.tenantSlug && host.startsWith(t.tenantSlug));
|
|
103
|
+
selectedTenant = match ?? body.tenants[0];
|
|
104
|
+
}
|
|
105
|
+
// 4. Build the user tenant context
|
|
106
|
+
const ctx = {
|
|
107
|
+
userId,
|
|
108
|
+
tenantId: selectedTenant.tenantId,
|
|
109
|
+
role: selectedTenant.role,
|
|
110
|
+
tenant: selectedTenant.tenant,
|
|
111
|
+
isAdmin: adminRoles.includes(selectedTenant.role),
|
|
112
|
+
tenants: body.tenants.map(t => ({
|
|
113
|
+
tenantId: t.tenantId,
|
|
114
|
+
tenantName: t.tenantName,
|
|
115
|
+
tenantSlug: t.tenantSlug,
|
|
116
|
+
role: t.role,
|
|
117
|
+
})),
|
|
118
|
+
};
|
|
119
|
+
// 5. Attach to Hono context
|
|
120
|
+
c.set('userTenant', ctx);
|
|
121
|
+
c.set('tenant', selectedTenant.tenant);
|
|
122
|
+
await next();
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (err instanceof UnauthorizedError) {
|
|
126
|
+
return c.json({ error: err.message }, 401);
|
|
127
|
+
}
|
|
128
|
+
if (err instanceof TenantScaleError) {
|
|
129
|
+
return c.json({ error: err.message, code: err.code }, err.status);
|
|
130
|
+
}
|
|
131
|
+
console.error('[TenantScale] User auth error:', err);
|
|
132
|
+
return c.json({ error: 'Failed to resolve user tenant context' }, 500);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Role guard middleware — restricts routes based on user role.
|
|
138
|
+
* Must be used after `createUserTenantMiddleware()`.
|
|
139
|
+
*/
|
|
140
|
+
export function requireUserRole(...roles) {
|
|
141
|
+
return async (c, next) => {
|
|
142
|
+
const ctx = c.get('userTenant');
|
|
143
|
+
if (!ctx) {
|
|
144
|
+
return c.json({ error: 'Authentication required' }, 401);
|
|
145
|
+
}
|
|
146
|
+
if (!roles.includes(ctx.role)) {
|
|
147
|
+
return c.json({
|
|
148
|
+
error: `This endpoint requires one of these roles: ${roles.join(', ')}`,
|
|
149
|
+
}, 403);
|
|
150
|
+
}
|
|
151
|
+
await next();
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=user-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-auth.js","sourceRoot":"","sources":["../../src/middleware/user-auth.ts"],"names":[],"mappings":"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"}
|
package/dist/react/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export const TenantContext = createContext({
|
|
|
13
13
|
});
|
|
14
14
|
export function useTenant() {
|
|
15
15
|
const ctx = useContext(TenantContext);
|
|
16
|
-
if (!ctx) {
|
|
16
|
+
if (!ctx.tenant && !ctx.loading) {
|
|
17
17
|
throw new Error('useTenant must be used within a TenantProvider');
|
|
18
18
|
}
|
|
19
19
|
return ctx;
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,4DAA4D;AAC5D,yDAAyD;AAEzD,YAAY,CAAA;AAEZ,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAejD,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAqB;IAC7D,MAAM,EAAE,IAAI;IACZ,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,IAAI;IACb,YAAY,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;IAC5B,UAAU,EAAE,GAAG,EAAE,CAAC,KAAK;IACvB,MAAM,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;CACvB,CAAC,CAAA;AAEF,MAAM,UAAU,SAAS;IACvB,MAAM,GAAG,GAAG,UAAU,CAAC,aAAa,CAAC,CAAA;IACrC,IAAI,CAAC,GAAG,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,4DAA4D;AAC5D,yDAAyD;AAEzD,YAAY,CAAA;AAEZ,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAejD,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAqB;IAC7D,MAAM,EAAE,IAAI;IACZ,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,IAAI;IACb,YAAY,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;IAC5B,UAAU,EAAE,GAAG,EAAE,CAAC,KAAK;IACvB,MAAM,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;CACvB,CAAC,CAAA;AAEF,MAAM,UAAU,SAAS;IACvB,MAAM,GAAG,GAAG,UAAU,CAAC,aAAa,CAAC,CAAA;IACrC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAA;IACnE,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
package/dist/tenant-scale.d.ts
CHANGED
|
@@ -79,5 +79,51 @@ export declare class TenantScale {
|
|
|
79
79
|
tenantId?: string;
|
|
80
80
|
limit?: number;
|
|
81
81
|
}): Promise<AuditEvent[]>;
|
|
82
|
+
/**
|
|
83
|
+
* Get a specific plan limit value for the current tenant.
|
|
84
|
+
* Returns null if the limit is not defined.
|
|
85
|
+
*
|
|
86
|
+
* Usage:
|
|
87
|
+
* const maxUsers = ts.getPlanLimit(c, 'max_users')
|
|
88
|
+
* // → 100 (or null if unlimited / not set)
|
|
89
|
+
*/
|
|
90
|
+
getPlanLimit(c: Context, key: string): boolean | number | string | null;
|
|
91
|
+
/**
|
|
92
|
+
* Get all plan limits for the current tenant.
|
|
93
|
+
* Returns the merged feature map (plan defaults + tenant overrides).
|
|
94
|
+
*
|
|
95
|
+
* Usage:
|
|
96
|
+
* const limits = ts.getPlanLimits(c)
|
|
97
|
+
* // → { max_users: 100, audit_log_retention_days: 30, sso: true, ... }
|
|
98
|
+
*/
|
|
99
|
+
getPlanLimits(c: Context): Record<string, boolean | number | string>;
|
|
100
|
+
/**
|
|
101
|
+
* Check if a usage value exceeds the plan limit.
|
|
102
|
+
* Returns true if the limit is exceeded (or is 0).
|
|
103
|
+
* Returns false if the limit is null (unlimited).
|
|
104
|
+
*
|
|
105
|
+
* Usage:
|
|
106
|
+
* if (ts.planLimitExceeded(c, 'max_users', currentUserCount)) {
|
|
107
|
+
* return c.json({ error: 'User limit reached' }, 403)
|
|
108
|
+
* }
|
|
109
|
+
*/
|
|
110
|
+
planLimitExceeded(c: Context, key: string, currentValue: number): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Fetch the full plan definition (metadata + limits) from the API.
|
|
113
|
+
* Use this when you need plan pricing, name, description, etc.
|
|
114
|
+
*
|
|
115
|
+
* Usage:
|
|
116
|
+
* const plan = await ts.fetchPlan(c)
|
|
117
|
+
* // → { id: 'pro', name: 'Pro', price_monthly: 39900, features: {...}, ... }
|
|
118
|
+
*/
|
|
119
|
+
fetchPlan(c: Context): Promise<Record<string, unknown> | null>;
|
|
120
|
+
/**
|
|
121
|
+
* Fetch all available plans from the API.
|
|
122
|
+
*
|
|
123
|
+
* Usage:
|
|
124
|
+
* const plans = await ts.fetchPlans()
|
|
125
|
+
* // → [{ id: 'free', name: 'Free', ... }, { id: 'pro', ... }]
|
|
126
|
+
*/
|
|
127
|
+
fetchPlans(): Promise<Record<string, unknown>[]>;
|
|
82
128
|
}
|
|
83
129
|
//# sourceMappingURL=tenant-scale.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant-scale.d.ts","sourceRoot":"","sources":["../src/tenant-scale.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAE,KAAK,UAAU,EAAkB,MAAM,YAAY,CAAA;AAClG,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"tenant-scale.d.ts","sourceRoot":"","sources":["../src/tenant-scale.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAE,KAAK,UAAU,EAAkB,MAAM,YAAY,CAAA;AAClG,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAGtD,OAAO,QAAQ,MAAM,CAAC;IACpB,UAAU,kBAAkB;QAC1B,MAAM,EAAE,MAAM,CAAA;QACd,YAAY,EAAE,YAAY,CAAA;KAC3B;CACF;AAKD,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,MAAM,CAAQ;gBAEV,OAAO,EAAE,kBAAkB,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IAK7D;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW;IAMvC;;;;;;OAMG;IACH,OAAO,IAAI,iBAAiB;IAwB5B;;;;;;OAMG;IACH,KAAK,IAAI,iBAAiB;IAsB1B;;;;;OAKG;IACH,YAAY,IAAI,iBAAiB;IAuBjC;;;OAGG;IACH,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS;IAIzC;;;OAGG;IACH,OAAO,CAAC,CAAC,EAAE,OAAO,GAAG;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS;IAO9E;;;;;;;OAOG;IACG,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAYzH;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAKtC;;OAEG;IACG,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAMjG;;;;;;;OAOG;IACH,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI;IAMvE;;;;;;;OAOG;IACH,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IAKpE;;;;;;;;;OASG;IACH,iBAAiB,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO;IAYzE;;;;;;;OAOG;IACG,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAqBpE;;;;;;OAMG;IACG,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;CAiBvD"}
|
package/dist/tenant-scale.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// ──────────────────────────────────────────────────────
|
|
11
11
|
import { TenantClient } from './tenant.js';
|
|
12
12
|
import { PlanLimitError } from './types.js';
|
|
13
|
+
// Internal storage for the API key on the Hono context
|
|
14
|
+
const API_KEY_SYMBOL = Symbol('__tenantscale_apiKey');
|
|
13
15
|
export class TenantScale {
|
|
14
16
|
client;
|
|
15
17
|
apiKey;
|
|
@@ -43,7 +45,7 @@ export class TenantScale {
|
|
|
43
45
|
const tenant = await this.client.resolveTenant(apiKey);
|
|
44
46
|
c.set('tenant', tenant);
|
|
45
47
|
c.set('tenantClient', this.client);
|
|
46
|
-
c
|
|
48
|
+
c[API_KEY_SYMBOL] = apiKey;
|
|
47
49
|
await next();
|
|
48
50
|
}
|
|
49
51
|
catch (err) {
|
|
@@ -67,7 +69,7 @@ export class TenantScale {
|
|
|
67
69
|
const tenant = c.get('tenant');
|
|
68
70
|
if (!tenant)
|
|
69
71
|
return;
|
|
70
|
-
const apiKey = c
|
|
72
|
+
const apiKey = c[API_KEY_SYMBOL] || '';
|
|
71
73
|
const method = c.req.method;
|
|
72
74
|
const path = c.req.path;
|
|
73
75
|
const status = c.res.status;
|
|
@@ -94,13 +96,11 @@ export class TenantScale {
|
|
|
94
96
|
return c.json({ error: 'Missing Authorization header' }, 401);
|
|
95
97
|
}
|
|
96
98
|
const apiKey = authHeader.slice(7);
|
|
97
|
-
// Admin keys start with a special prefix — check via the API
|
|
98
99
|
try {
|
|
99
100
|
const tenant = await this.client.resolveTenant(apiKey);
|
|
100
|
-
// The resolveTenant call succeeds for admin keys too
|
|
101
|
-
// but we flag it differently here
|
|
102
101
|
c.set('tenant', tenant);
|
|
103
102
|
c.set('tenantClient', this.client);
|
|
103
|
+
c[API_KEY_SYMBOL] = apiKey;
|
|
104
104
|
await next();
|
|
105
105
|
}
|
|
106
106
|
catch {
|
|
@@ -125,7 +125,7 @@ export class TenantScale {
|
|
|
125
125
|
if (!tenant)
|
|
126
126
|
return undefined;
|
|
127
127
|
// For now, return the tenant's admin user as the current user
|
|
128
|
-
return { id: tenant.id, email:
|
|
128
|
+
return { id: tenant.id, email: undefined, role: 'admin' };
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
131
|
* Log a custom audit event from within a route handler.
|
|
@@ -158,5 +158,110 @@ export class TenantScale {
|
|
|
158
158
|
async getAuditLog(c, opts) {
|
|
159
159
|
throw new Error('getAuditLog() requires the hosted API — coming soon');
|
|
160
160
|
}
|
|
161
|
+
// ── Plan Limit Helpers ──
|
|
162
|
+
/**
|
|
163
|
+
* Get a specific plan limit value for the current tenant.
|
|
164
|
+
* Returns null if the limit is not defined.
|
|
165
|
+
*
|
|
166
|
+
* Usage:
|
|
167
|
+
* const maxUsers = ts.getPlanLimit(c, 'max_users')
|
|
168
|
+
* // → 100 (or null if unlimited / not set)
|
|
169
|
+
*/
|
|
170
|
+
getPlanLimit(c, key) {
|
|
171
|
+
const tenant = c.get('tenant');
|
|
172
|
+
if (!tenant)
|
|
173
|
+
return null;
|
|
174
|
+
return tenant.features[key] ?? null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get all plan limits for the current tenant.
|
|
178
|
+
* Returns the merged feature map (plan defaults + tenant overrides).
|
|
179
|
+
*
|
|
180
|
+
* Usage:
|
|
181
|
+
* const limits = ts.getPlanLimits(c)
|
|
182
|
+
* // → { max_users: 100, audit_log_retention_days: 30, sso: true, ... }
|
|
183
|
+
*/
|
|
184
|
+
getPlanLimits(c) {
|
|
185
|
+
const tenant = c.get('tenant');
|
|
186
|
+
return (tenant?.features ?? {});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Check if a usage value exceeds the plan limit.
|
|
190
|
+
* Returns true if the limit is exceeded (or is 0).
|
|
191
|
+
* Returns false if the limit is null (unlimited).
|
|
192
|
+
*
|
|
193
|
+
* Usage:
|
|
194
|
+
* if (ts.planLimitExceeded(c, 'max_users', currentUserCount)) {
|
|
195
|
+
* return c.json({ error: 'User limit reached' }, 403)
|
|
196
|
+
* }
|
|
197
|
+
*/
|
|
198
|
+
planLimitExceeded(c, key, currentValue) {
|
|
199
|
+
const limit = this.getPlanLimit(c, key);
|
|
200
|
+
// null means unlimited
|
|
201
|
+
if (limit === null)
|
|
202
|
+
return false;
|
|
203
|
+
// Boolean limits: true means allowed, false means blocked
|
|
204
|
+
if (typeof limit === 'boolean')
|
|
205
|
+
return !limit;
|
|
206
|
+
// Numeric limits
|
|
207
|
+
if (typeof limit === 'number')
|
|
208
|
+
return currentValue >= limit;
|
|
209
|
+
// String limits — can't compare numerically
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Fetch the full plan definition (metadata + limits) from the API.
|
|
214
|
+
* Use this when you need plan pricing, name, description, etc.
|
|
215
|
+
*
|
|
216
|
+
* Usage:
|
|
217
|
+
* const plan = await ts.fetchPlan(c)
|
|
218
|
+
* // → { id: 'pro', name: 'Pro', price_monthly: 39900, features: {...}, ... }
|
|
219
|
+
*/
|
|
220
|
+
async fetchPlan(c) {
|
|
221
|
+
const tenant = c.get('tenant');
|
|
222
|
+
if (!tenant)
|
|
223
|
+
return null;
|
|
224
|
+
const baseUrl = this.client.getOptions().baseUrl ?? 'https://api.tenantscale.com';
|
|
225
|
+
const apiKey = c[API_KEY_SYMBOL] || this.apiKey;
|
|
226
|
+
try {
|
|
227
|
+
const res = await fetch(`${baseUrl}/v1/plans/${tenant.plan}`, {
|
|
228
|
+
headers: {
|
|
229
|
+
Authorization: `Bearer ${apiKey}`,
|
|
230
|
+
'Content-Type': 'application/json',
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
if (!res.ok)
|
|
234
|
+
return null;
|
|
235
|
+
return await res.json();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Fetch all available plans from the API.
|
|
243
|
+
*
|
|
244
|
+
* Usage:
|
|
245
|
+
* const plans = await ts.fetchPlans()
|
|
246
|
+
* // → [{ id: 'free', name: 'Free', ... }, { id: 'pro', ... }]
|
|
247
|
+
*/
|
|
248
|
+
async fetchPlans() {
|
|
249
|
+
const baseUrl = this.client.getOptions().baseUrl ?? 'https://api.tenantscale.com';
|
|
250
|
+
try {
|
|
251
|
+
const res = await fetch(`${baseUrl}/v1/plans`, {
|
|
252
|
+
headers: {
|
|
253
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
254
|
+
'Content-Type': 'application/json',
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
if (!res.ok)
|
|
258
|
+
return [];
|
|
259
|
+
const data = await res.json();
|
|
260
|
+
return data.plans ?? [];
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
161
266
|
}
|
|
162
267
|
//# sourceMappingURL=tenant-scale.js.map
|
package/dist/tenant-scale.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant-scale.js","sourceRoot":"","sources":["../src/tenant-scale.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,kCAAkC;AAClC,wDAAwD;AACxD,yDAAyD;AACzD,EAAE;AACF,SAAS;AACT,mDAAmD;AACnD,kDAAkD;AAClD,+BAA+B;AAC/B,yDAAyD;AAEzD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAyD,cAAc,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"tenant-scale.js","sourceRoot":"","sources":["../src/tenant-scale.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export interface ApiKeyPair {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
}
|
|
4
|
+
/** Result of a single API call */
|
|
5
|
+
interface ApiResponse {
|
|
6
|
+
status: number;
|
|
7
|
+
data: unknown;
|
|
8
|
+
headers: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
/** A generic API client used internally */
|
|
11
|
+
declare class ApiClient {
|
|
12
|
+
private baseUrl;
|
|
13
|
+
private apiKey;
|
|
14
|
+
constructor(baseUrl: string, apiKey: string);
|
|
15
|
+
request(method: string, path: string, body?: unknown): Promise<ApiResponse>;
|
|
16
|
+
get(path: string): Promise<ApiResponse>;
|
|
17
|
+
post(path: string, body?: unknown): Promise<ApiResponse>;
|
|
18
|
+
patch(path: string, body?: unknown): Promise<ApiResponse>;
|
|
19
|
+
del(path: string): Promise<ApiResponse>;
|
|
20
|
+
}
|
|
21
|
+
export interface EndpointPattern {
|
|
22
|
+
/** HTTP method */
|
|
23
|
+
method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
24
|
+
/**
|
|
25
|
+
* Path template. If it's a function, it receives the resource ID
|
|
26
|
+
* from the setup step (for read/update/delete isolation tests).
|
|
27
|
+
* If it's a string, it's used as-is.
|
|
28
|
+
*/
|
|
29
|
+
path: string | ((resourceId: string) => string);
|
|
30
|
+
}
|
|
31
|
+
export interface TestScenarios {
|
|
32
|
+
/**
|
|
33
|
+
* Test that tenant B cannot READ resources created by tenant A.
|
|
34
|
+
* For each pattern: setup creates a resource, then tenant B tries to GET it.
|
|
35
|
+
* Expected: 401/403/404 (not 200 with data from another tenant)
|
|
36
|
+
*/
|
|
37
|
+
read?: EndpointPattern[];
|
|
38
|
+
/**
|
|
39
|
+
* Test that tenant B cannot WRITE (update) resources created by tenant A.
|
|
40
|
+
* For each pattern: setup creates a resource, then tenant B tries to PATCH it.
|
|
41
|
+
* Expected: 401/403/404
|
|
42
|
+
*/
|
|
43
|
+
write?: EndpointPattern[];
|
|
44
|
+
/**
|
|
45
|
+
* Test that tenant B cannot DELETE resources created by tenant A.
|
|
46
|
+
* Expected: 401/403/404
|
|
47
|
+
*/
|
|
48
|
+
delete?: EndpointPattern[];
|
|
49
|
+
/**
|
|
50
|
+
* Test that tenant B cannot CREATE resources in tenant A's context.
|
|
51
|
+
* For each pattern: tenant B tries to POST to the path.
|
|
52
|
+
* Expected: 401/403
|
|
53
|
+
*/
|
|
54
|
+
create?: EndpointPattern[];
|
|
55
|
+
/**
|
|
56
|
+
* Custom test functions for more complex scenarios.
|
|
57
|
+
* Each receives helpers for making requests as both tenants.
|
|
58
|
+
*/
|
|
59
|
+
custom?: CustomTest[];
|
|
60
|
+
}
|
|
61
|
+
export interface CustomTest {
|
|
62
|
+
name: string;
|
|
63
|
+
run: (helpers: {
|
|
64
|
+
asTenantA: ApiClient;
|
|
65
|
+
asTenantB: ApiClient;
|
|
66
|
+
}) => Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
export interface IsolationTestConfig {
|
|
69
|
+
/** Base URL of the API under test */
|
|
70
|
+
baseUrl: string;
|
|
71
|
+
/** API key for tenant A (the "owner" of resources) */
|
|
72
|
+
tenantA: ApiKeyPair;
|
|
73
|
+
/** API key for tenant B (the "attacker" — should NOT have access) */
|
|
74
|
+
tenantB: ApiKeyPair;
|
|
75
|
+
/** Test scenarios to run */
|
|
76
|
+
tests: TestScenarios;
|
|
77
|
+
}
|
|
78
|
+
export interface IsolationTestResult {
|
|
79
|
+
passed: boolean;
|
|
80
|
+
totalTests: number;
|
|
81
|
+
passedTests: number;
|
|
82
|
+
failedTests: number;
|
|
83
|
+
failures: Array<{
|
|
84
|
+
name: string;
|
|
85
|
+
error: string;
|
|
86
|
+
}>;
|
|
87
|
+
details: Array<{
|
|
88
|
+
name: string;
|
|
89
|
+
status: 'pass' | 'fail' | 'skip';
|
|
90
|
+
error?: string;
|
|
91
|
+
}>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Run a battery of tenant isolation tests.
|
|
95
|
+
* Creates resources as tenant A, then verifies tenant B cannot access them.
|
|
96
|
+
*
|
|
97
|
+
* @returns Detailed results including pass/fail counts and error messages.
|
|
98
|
+
*/
|
|
99
|
+
export declare function runIsolationTests(config: IsolationTestConfig): Promise<IsolationTestResult>;
|
|
100
|
+
/**
|
|
101
|
+
* Quick assertion helper — use inside custom test functions.
|
|
102
|
+
* Throws with a descriptive message if the condition fails.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* await expectIsolated(res.status, 'Tenant B should not see this', 404)
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export declare function expectIsolated(actualStatus: number, description: string, expectedStatus?: number): void;
|
|
110
|
+
export {};
|
|
111
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,kCAAkC;AAClC,UAAU,WAAW;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,2CAA2C;AAC3C,cAAM,SAAS;IACb,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,MAAM,CAAQ;gBAEV,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAKrC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC;IAwBjF,GAAG,CAAC,IAAI,EAAE,MAAM;IAChB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO;IACjC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO;IAClC,GAAG,CAAC,IAAI,EAAE,MAAM;CACjB;AAID,MAAM,WAAW,eAAe;IAC9B,kBAAkB;IAClB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;IAC3C;;;;OAIG;IACH,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC,CAAA;CAChD;AAED,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,IAAI,CAAC,EAAE,eAAe,EAAE,CAAA;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,eAAe,EAAE,CAAA;IACzB;;;OAGG;IACH,MAAM,CAAC,EAAE,eAAe,EAAE,CAAA;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,eAAe,EAAE,CAAA;IAC1B;;;OAGG;IACH,MAAM,CAAC,EAAE,UAAU,EAAE,CAAA;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,CAAC,OAAO,EAAE;QACb,SAAS,EAAE,SAAS,CAAA;QACpB,SAAS,EAAE,SAAS,CAAA;KACrB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,OAAO,EAAE,UAAU,CAAA;IACnB,qEAAqE;IACrE,OAAO,EAAE,UAAU,CAAA;IACnB,4BAA4B;IAC5B,KAAK,EAAE,aAAa,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,OAAO,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,KAAK,CAAC;QACd,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;KACd,CAAC,CAAA;IACF,OAAO,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;QAChC,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAC,CAAA;CACH;AAYD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA6GjG;AAsBD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI,CAaN"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// ──────────────────────────────────────────────────────
|
|
2
|
+
// TenantScale Isolation Test Fixture
|
|
3
|
+
// Verifies that one tenant cannot access another tenant's data.
|
|
4
|
+
// Designed to run inside Vitest, Jest, or any test runner.
|
|
5
|
+
// ──────────────────────────────────────────────────────
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// import { runIsolationTests, expectIsolated } from '@tenantscale/sdk/testing'
|
|
9
|
+
//
|
|
10
|
+
// it('enforces tenant isolation', async () => {
|
|
11
|
+
// const result = await runIsolationTests({
|
|
12
|
+
// baseUrl: 'http://localhost:3001',
|
|
13
|
+
// tenantA: { apiKey: 'tk_tenant_a_key' },
|
|
14
|
+
// tenantB: { apiKey: 'tk_tenant_b_key' },
|
|
15
|
+
// tests: {
|
|
16
|
+
// read: [
|
|
17
|
+
// { method: 'GET', path: (id) => `/v1/tenants/${id}` },
|
|
18
|
+
// ],
|
|
19
|
+
// },
|
|
20
|
+
// })
|
|
21
|
+
//
|
|
22
|
+
// expect(result.passed).toBe(true)
|
|
23
|
+
// expect(result.failures).toHaveLength(0)
|
|
24
|
+
// })
|
|
25
|
+
// ──────────────────────────────────────────────────────
|
|
26
|
+
/** A generic API client used internally */
|
|
27
|
+
class ApiClient {
|
|
28
|
+
baseUrl;
|
|
29
|
+
apiKey;
|
|
30
|
+
constructor(baseUrl, apiKey) {
|
|
31
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
32
|
+
this.apiKey = apiKey;
|
|
33
|
+
}
|
|
34
|
+
async request(method, path, body) {
|
|
35
|
+
const url = `${this.baseUrl}${path}`;
|
|
36
|
+
const headers = {
|
|
37
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
};
|
|
40
|
+
const res = await fetch(url, {
|
|
41
|
+
method,
|
|
42
|
+
headers,
|
|
43
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
44
|
+
});
|
|
45
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
46
|
+
const data = contentType.includes('application/json')
|
|
47
|
+
? await res.json()
|
|
48
|
+
: await res.text();
|
|
49
|
+
const responseHeaders = {};
|
|
50
|
+
res.headers.forEach((value, key) => { responseHeaders[key] = value; });
|
|
51
|
+
return { status: res.status, data, headers: responseHeaders };
|
|
52
|
+
}
|
|
53
|
+
get(path) { return this.request('GET', path); }
|
|
54
|
+
post(path, body) { return this.request('POST', path, body); }
|
|
55
|
+
patch(path, body) { return this.request('PATCH', path, body); }
|
|
56
|
+
del(path) { return this.request('DELETE', path); }
|
|
57
|
+
}
|
|
58
|
+
// ── Default expectations ──
|
|
59
|
+
const DEFAULT_FORBIDDEN_STATUSES = [401, 403, 404];
|
|
60
|
+
function isForbidden(status) {
|
|
61
|
+
return DEFAULT_FORBIDDEN_STATUSES.includes(status);
|
|
62
|
+
}
|
|
63
|
+
// ── Main runner ──
|
|
64
|
+
/**
|
|
65
|
+
* Run a battery of tenant isolation tests.
|
|
66
|
+
* Creates resources as tenant A, then verifies tenant B cannot access them.
|
|
67
|
+
*
|
|
68
|
+
* @returns Detailed results including pass/fail counts and error messages.
|
|
69
|
+
*/
|
|
70
|
+
export async function runIsolationTests(config) {
|
|
71
|
+
const { baseUrl, tenantA, tenantB, tests } = config;
|
|
72
|
+
const apiA = new ApiClient(baseUrl, tenantA.apiKey);
|
|
73
|
+
const apiB = new ApiClient(baseUrl, tenantB.apiKey);
|
|
74
|
+
const details = [];
|
|
75
|
+
const failures = [];
|
|
76
|
+
let total = 0;
|
|
77
|
+
// ── Helper to run a single test ──
|
|
78
|
+
async function runTest(name, fn) {
|
|
79
|
+
total++;
|
|
80
|
+
try {
|
|
81
|
+
await fn();
|
|
82
|
+
details.push({ name, status: 'pass' });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
86
|
+
failures.push({ name, error: msg });
|
|
87
|
+
details.push({ name, status: 'fail', error: msg });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// ── Read isolation tests ──
|
|
91
|
+
if (tests.read) {
|
|
92
|
+
for (const pattern of tests.read) {
|
|
93
|
+
await runTest(`read: ${pattern.method} ${typeof pattern.path === 'string' ? pattern.path : '(dynamic)'}`, async () => {
|
|
94
|
+
const resourceId = await createTempResource(apiA);
|
|
95
|
+
const path = typeof pattern.path === 'function' ? pattern.path(resourceId) : pattern.path;
|
|
96
|
+
const res = await apiB.request(pattern.method, path);
|
|
97
|
+
if (!isForbidden(res.status)) {
|
|
98
|
+
throw new Error(`Expected 401/403/404 when tenant B reads tenant A's resource, got ${res.status}`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ── Write isolation tests ──
|
|
104
|
+
if (tests.write) {
|
|
105
|
+
for (const pattern of tests.write) {
|
|
106
|
+
await runTest(`write: ${pattern.method} ${typeof pattern.path === 'string' ? pattern.path : '(dynamic)'}`, async () => {
|
|
107
|
+
const resourceId = await createTempResource(apiA);
|
|
108
|
+
const path = typeof pattern.path === 'function' ? pattern.path(resourceId) : pattern.path;
|
|
109
|
+
const res = await apiB.request(pattern.method, path, { name: 'should-not-work' });
|
|
110
|
+
if (!isForbidden(res.status)) {
|
|
111
|
+
throw new Error(`Expected 401/403/404 when tenant B updates tenant A's resource, got ${res.status}`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ── Delete isolation tests ──
|
|
117
|
+
if (tests.delete) {
|
|
118
|
+
for (const pattern of tests.delete) {
|
|
119
|
+
await runTest(`delete: ${pattern.method} ${typeof pattern.path === 'string' ? pattern.path : '(dynamic)'}`, async () => {
|
|
120
|
+
const resourceId = await createTempResource(apiA);
|
|
121
|
+
const path = typeof pattern.path === 'function' ? pattern.path(resourceId) : pattern.path;
|
|
122
|
+
const res = await apiB.request(pattern.method, path);
|
|
123
|
+
if (!isForbidden(res.status)) {
|
|
124
|
+
throw new Error(`Expected 401/403/404 when tenant B deletes tenant A's resource, got ${res.status}`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ── Create isolation tests ──
|
|
130
|
+
if (tests.create) {
|
|
131
|
+
for (const pattern of tests.create) {
|
|
132
|
+
await runTest(`create: ${pattern.method} ${typeof pattern.path === 'string' ? pattern.path : '(dynamic)'}`, async () => {
|
|
133
|
+
const path = typeof pattern.path === 'function' ? pattern.path('') : pattern.path;
|
|
134
|
+
const res = await apiB.request(pattern.method, path, { name: 'should-be-blocked' });
|
|
135
|
+
if (!isForbidden(res.status)) {
|
|
136
|
+
throw new Error(`Expected 401/403/404 when tenant B creates resources, got ${res.status}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ── Custom tests ──
|
|
142
|
+
if (tests.custom) {
|
|
143
|
+
for (const test of tests.custom) {
|
|
144
|
+
await runTest(`custom: ${test.name}`, () => test.run({
|
|
145
|
+
asTenantA: apiA,
|
|
146
|
+
asTenantB: apiB,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const passed = failures.length === 0;
|
|
151
|
+
return {
|
|
152
|
+
passed,
|
|
153
|
+
totalTests: total,
|
|
154
|
+
passedTests: total - failures.length,
|
|
155
|
+
failedTests: failures.length,
|
|
156
|
+
failures,
|
|
157
|
+
details,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// ── Helpers ──
|
|
161
|
+
/**
|
|
162
|
+
* Create a temporary resource for isolation testing.
|
|
163
|
+
* This creates a tenant via the public API endpoint.
|
|
164
|
+
* Override this if your setup flow is different.
|
|
165
|
+
*/
|
|
166
|
+
async function createTempResource(api) {
|
|
167
|
+
const slug = `test-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
168
|
+
const res = await api.post('/v1/tenants', {
|
|
169
|
+
name: `Test Tenant ${slug}`,
|
|
170
|
+
slug,
|
|
171
|
+
});
|
|
172
|
+
if (res.status >= 400) {
|
|
173
|
+
throw new Error(`Setup: Failed to create resource (${res.status}): ${JSON.stringify(res.data)}`);
|
|
174
|
+
}
|
|
175
|
+
const data = res.data;
|
|
176
|
+
return String(data.id ?? '');
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Quick assertion helper — use inside custom test functions.
|
|
180
|
+
* Throws with a descriptive message if the condition fails.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* await expectIsolated(res.status, 'Tenant B should not see this', 404)
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export function expectIsolated(actualStatus, description, expectedStatus) {
|
|
188
|
+
const acceptable = DEFAULT_FORBIDDEN_STATUSES;
|
|
189
|
+
if (expectedStatus !== undefined) {
|
|
190
|
+
if (actualStatus !== expectedStatus) {
|
|
191
|
+
throw new Error(`${description}: expected ${expectedStatus}, got ${actualStatus}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (!acceptable.includes(actualStatus)) {
|
|
195
|
+
throw new Error(`${description}: expected one of [${acceptable.join(', ')}], got ${actualStatus}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -20,9 +20,33 @@ export interface TenantUser {
|
|
|
20
20
|
id: string;
|
|
21
21
|
email: string;
|
|
22
22
|
name: string;
|
|
23
|
-
role
|
|
23
|
+
/** Developer-defined role string. TenantScale only reserves 'owner' internally. */
|
|
24
|
+
role: string;
|
|
24
25
|
tenant_id: string;
|
|
25
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolved user+tenant context for BYOA auth.
|
|
29
|
+
* Set on the request after `resolveUserTenant()` middleware runs.
|
|
30
|
+
*/
|
|
31
|
+
export interface UserTenantContext {
|
|
32
|
+
/** The authenticated user's ID */
|
|
33
|
+
userId: string;
|
|
34
|
+
/** The tenant this user belongs to */
|
|
35
|
+
tenantId: string;
|
|
36
|
+
/** Developer-defined role string (e.g. 'owner', 'admin', 'editor', 'manager'). */
|
|
37
|
+
role: string;
|
|
38
|
+
/** The resolved tenant object (cached) */
|
|
39
|
+
tenant: Tenant;
|
|
40
|
+
/** Whether this user can perform admin actions */
|
|
41
|
+
isAdmin: boolean;
|
|
42
|
+
/** All tenants this user belongs to (for multi-tenant switching) */
|
|
43
|
+
tenants: Array<{
|
|
44
|
+
tenantId: string;
|
|
45
|
+
tenantName: string;
|
|
46
|
+
tenantSlug: string;
|
|
47
|
+
role: string;
|
|
48
|
+
}>;
|
|
49
|
+
}
|
|
26
50
|
/** Options passed to the TenantScale constructor */
|
|
27
51
|
export interface TenantScaleOptions {
|
|
28
52
|
/** Your TenantScale API key (tk_xxx) */
|
|
@@ -70,6 +94,10 @@ export interface ImpersonationSession {
|
|
|
70
94
|
expiresAt: string;
|
|
71
95
|
redirectUrl: string;
|
|
72
96
|
}
|
|
97
|
+
/** Internal context passed between TenantScale middleware methods */
|
|
98
|
+
export interface TenantScaleContext {
|
|
99
|
+
apiKey: string;
|
|
100
|
+
}
|
|
73
101
|
export declare class TenantScaleError extends Error {
|
|
74
102
|
code: string;
|
|
75
103
|
status: number;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAIA,0DAA0D;AAC1D,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,QAAQ,CAAA;IACd,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,CAAA;IACnD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,QAAQ,EAAE,cAAc,CAAA;CACzB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,UAAU,CAAA;AAE5D,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,uCAAuC;AACvC,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// ──────────────────────────────────────────────────────
|
|
2
2
|
// Tenant Types — the domain model for the SDK
|
|
3
3
|
// ──────────────────────────────────────────────────────
|
|
4
|
-
// ──────────────────────────────────────────────────────
|
|
5
|
-
// Error types
|
|
6
|
-
// ──────────────────────────────────────────────────────
|
|
7
4
|
export class TenantScaleError extends Error {
|
|
8
5
|
code;
|
|
9
6
|
status;
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,8CAA8C;AAC9C,yDAAyD;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,8CAA8C;AAC9C,yDAAyD;AAwGzD,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAGhC;IACA;IAHT,YACE,OAAe,EACR,IAAY,EACZ,SAAiB,GAAG;QAE3B,KAAK,CAAC,OAAO,CAAC,CAAA;QAHP,SAAI,GAAJ,IAAI,CAAQ;QACZ,WAAM,GAAN,MAAM,CAAc;QAG3B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;IAChC,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,gBAAgB;IAClD,YAAY,OAAe;QACzB,KAAK,CACH,uBAAuB,OAAO,6CAA6C,EAC3E,oBAAoB,EACpB,GAAG,CACJ,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAA;IAC9B,CAAC;CACF;AAED,MAAM,OAAO,iBAAkB,SAAQ,gBAAgB;IACrD,YAAY,OAAO,GAAG,4BAA4B;QAChD,KAAK,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,CAAC,CAAA;QACnC,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAA;IACjC,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,63 +1,72 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@tenantscale/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Multi-tenant middleware SDK for B2B SaaS — tenant isolation, audit logging, admin tools",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"import": "./dist/index.js",
|
|
11
|
-
"types": "./dist/index.d.ts"
|
|
12
|
-
},
|
|
13
|
-
"./middleware": {
|
|
14
|
-
"import": "./dist/middleware/index.js",
|
|
15
|
-
"types": "./dist/middleware/index.d.ts"
|
|
16
|
-
},
|
|
17
|
-
"./react": {
|
|
18
|
-
"import": "./dist/react/index.js",
|
|
19
|
-
"types": "./dist/react/index.d.ts"
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"@
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
}
|
|
63
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@tenantscale/sdk",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Multi-tenant middleware SDK for B2B SaaS — tenant isolation, audit logging, admin tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./middleware": {
|
|
14
|
+
"import": "./dist/middleware/index.js",
|
|
15
|
+
"types": "./dist/middleware/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./react": {
|
|
18
|
+
"import": "./dist/react/index.js",
|
|
19
|
+
"types": "./dist/react/index.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./testing": {
|
|
22
|
+
"import": "./dist/testing/index.js",
|
|
23
|
+
"types": "./dist/testing/index.d.ts"
|
|
24
|
+
},
|
|
25
|
+
"./user": {
|
|
26
|
+
"import": "./dist/middleware/user-auth.js",
|
|
27
|
+
"types": "./dist/middleware/user-auth.d.ts"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"superjson": "^2.2.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"express": "^5.0.0",
|
|
38
|
+
"hono": "^4.6.0",
|
|
39
|
+
"next": "^15.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"hono": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"next": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"express": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@supabase/supabase-js": "^2.49.0",
|
|
54
|
+
"@types/express": "^5.0.0",
|
|
55
|
+
"@types/react": "^19.0.0",
|
|
56
|
+
"@vitest/coverage-v8": "^3.2.6",
|
|
57
|
+
"hono": "^4.6.0",
|
|
58
|
+
"next": "^15.0.0",
|
|
59
|
+
"react": "^19.0.0",
|
|
60
|
+
"typescript": "^5.7.0",
|
|
61
|
+
"vitest": "^3.0.0"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsc",
|
|
68
|
+
"dev": "tsc --watch",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"lint": "eslint src/"
|
|
71
|
+
}
|
|
72
|
+
}
|