@tenantscale/sdk 0.1.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.
@@ -0,0 +1,267 @@
1
+ // ──────────────────────────────────────────────────────
2
+ // TenantScale — Unified SDK Class
3
+ // Combines tenant context, middleware, audit, and admin
4
+ // into the single `TenantScale` import the docs promise.
5
+ //
6
+ // Usage:
7
+ // import { TenantScale } from '@tenantscale/sdk'
8
+ // const ts = new TenantScale({ apiKey: '...' })
9
+ // app.use('*', ts.protect())
10
+ // ──────────────────────────────────────────────────────
11
+ import { TenantClient } from './tenant.js';
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');
15
+ export class TenantScale {
16
+ client;
17
+ apiKey;
18
+ constructor(options) {
19
+ this.client = new TenantClient(options);
20
+ this.apiKey = options.apiKey ?? '';
21
+ }
22
+ /**
23
+ * Create a new instance with a different API key.
24
+ * Useful when you need to switch tenants.
25
+ */
26
+ withApiKey(apiKey) {
27
+ return new TenantScale({ ...this.client.getOptions(), apiKey });
28
+ }
29
+ // ── Middleware ──
30
+ /**
31
+ * Enforces tenant isolation — extracts the API key from the
32
+ * Authorization header and attaches tenant context to the request.
33
+ *
34
+ * Usage:
35
+ * app.use('/api/*', ts.protect())
36
+ */
37
+ protect() {
38
+ return async (c, next) => {
39
+ const authHeader = c.req.header('Authorization');
40
+ if (!authHeader?.startsWith('Bearer ')) {
41
+ return c.json({ error: 'Missing or invalid Authorization header' }, 401);
42
+ }
43
+ const apiKey = authHeader.slice(7);
44
+ try {
45
+ const tenant = await this.client.resolveTenant(apiKey);
46
+ c.set('tenant', tenant);
47
+ c.set('tenantClient', this.client);
48
+ c[API_KEY_SYMBOL] = apiKey;
49
+ await next();
50
+ }
51
+ catch (err) {
52
+ if (err instanceof PlanLimitError) {
53
+ return c.json({ error: err.message, code: err.code }, err.status);
54
+ }
55
+ return c.json({ error: 'Failed to resolve tenant' }, 401);
56
+ }
57
+ };
58
+ }
59
+ /**
60
+ * Logs every request to the audit trail.
61
+ * Place after protect() so tenant context is available.
62
+ *
63
+ * Usage:
64
+ * app.use('/api/*', ts.audit())
65
+ */
66
+ audit() {
67
+ return async (c, next) => {
68
+ await next();
69
+ const tenant = c.get('tenant');
70
+ if (!tenant)
71
+ return;
72
+ const apiKey = c[API_KEY_SYMBOL] || '';
73
+ const method = c.req.method;
74
+ const path = c.req.path;
75
+ const status = c.res.status;
76
+ this.client.logAudit(apiKey, {
77
+ action: `${method} ${path}`,
78
+ resource: path,
79
+ actor_id: tenant.id,
80
+ details: { status_code: status },
81
+ }).catch(() => {
82
+ // Fire and forget — audit failures shouldn't crash the app
83
+ });
84
+ };
85
+ }
86
+ /**
87
+ * Requires an admin API key. Use on admin routes.
88
+ *
89
+ * Usage:
90
+ * app.use('/admin/*', ts.requireAdmin())
91
+ */
92
+ requireAdmin() {
93
+ return async (c, next) => {
94
+ const authHeader = c.req.header('Authorization');
95
+ if (!authHeader?.startsWith('Bearer ')) {
96
+ return c.json({ error: 'Missing Authorization header' }, 401);
97
+ }
98
+ const apiKey = authHeader.slice(7);
99
+ try {
100
+ const tenant = await this.client.resolveTenant(apiKey);
101
+ c.set('tenant', tenant);
102
+ c.set('tenantClient', this.client);
103
+ c[API_KEY_SYMBOL] = apiKey;
104
+ await next();
105
+ }
106
+ catch {
107
+ return c.json({ error: 'Invalid admin key' }, 401);
108
+ }
109
+ };
110
+ }
111
+ // ── Route Helpers ──
112
+ /**
113
+ * Get the current tenant context from a Hono context.
114
+ * Works after protect() middleware has run.
115
+ */
116
+ getTenant(c) {
117
+ return c.get('tenant');
118
+ }
119
+ /**
120
+ * Get the current user context.
121
+ * (Placeholder — user resolution depends on your auth setup.)
122
+ */
123
+ getUser(c) {
124
+ const tenant = c.get('tenant');
125
+ if (!tenant)
126
+ return undefined;
127
+ // For now, return the tenant's admin user as the current user
128
+ return { id: tenant.id, email: undefined, role: 'admin' };
129
+ }
130
+ /**
131
+ * Log a custom audit event from within a route handler.
132
+ *
133
+ * Usage:
134
+ * app.post('/api/orders', async (c) => {
135
+ * await ts.logAudit(c, { action: 'order.created', resource: 'order:123' })
136
+ * })
137
+ */
138
+ async logAudit(c, event) {
139
+ const tenant = c.get('tenant');
140
+ await this.client.logAudit(this.apiKey, {
141
+ action: event.action,
142
+ resource: event.resource,
143
+ actor_id: tenant?.id ?? 'unknown',
144
+ details: event.details,
145
+ });
146
+ }
147
+ // ── Admin Helpers ──
148
+ /**
149
+ * List all tenants (admin only).
150
+ */
151
+ async listTenants() {
152
+ // This would call the admin API endpoint
153
+ throw new Error('listTenants() requires the hosted API — coming soon');
154
+ }
155
+ /**
156
+ * Get paginated audit log (admin only).
157
+ */
158
+ async getAuditLog(c, opts) {
159
+ throw new Error('getAuditLog() requires the hosted API — coming soon');
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
+ }
266
+ }
267
+ //# sourceMappingURL=tenant-scale.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-scale.js","sourceRoot":"","sources":["../src/tenant-scale.ts"],"names":[],"mappings":"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"}
package/dist/tenant.d.ts CHANGED
@@ -4,6 +4,8 @@ export declare class TenantClient {
4
4
  private cache;
5
5
  private cacheTtlMs;
6
6
  constructor(options: TenantScaleOptions);
7
+ /** Access the current options */
8
+ getOptions(): TenantScaleOptions;
7
9
  /**
8
10
  * Fetch a tenant by API key.
9
11
  * Results are cached for `cacheTtlMs` to avoid hammering the API on every request.
@@ -1 +1 @@
1
- {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAoB,MAAM,YAAY,CAAA;AAOnF,qBAAa,YAAY;IAIX,OAAO,CAAC,OAAO;IAH3B,OAAO,CAAC,KAAK,CAAkC;IAC/C,OAAO,CAAC,UAAU,CAAS;gBAEP,OAAO,EAAE,kBAAkB;IAE/C;;;OAGG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAsCpD,yDAAyD;IACnD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQrE,uCAAuC;IACjC,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE;QACtC,MAAM,EAAE,MAAM,CAAA;QACd,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KACrC,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBjB,yBAAyB;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE;QACpC,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC;IAajB,wEAAwE;IACxE,eAAe,CAAC,MAAM,EAAE,MAAM;CAG/B"}
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAoB,MAAM,YAAY,CAAA;AAOnF,qBAAa,YAAY;IAIX,OAAO,CAAC,OAAO;IAH3B,OAAO,CAAC,KAAK,CAAkC;IAC/C,OAAO,CAAC,UAAU,CAAS;gBAEP,OAAO,EAAE,kBAAkB;IAE/C,iCAAiC;IACjC,UAAU,IAAI,kBAAkB;IAEhC;;;OAGG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAsCpD,yDAAyD;IACnD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQrE,uCAAuC;IACjC,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE;QACtC,MAAM,EAAE,MAAM,CAAA;QACd,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KACrC,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBjB,yBAAyB;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE;QACpC,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC;IAajB,wEAAwE;IACxE,eAAe,CAAC,MAAM,EAAE,MAAM;CAG/B"}
package/dist/tenant.js CHANGED
@@ -9,6 +9,8 @@ export class TenantClient {
9
9
  constructor(options) {
10
10
  this.options = options;
11
11
  }
12
+ /** Access the current options */
13
+ getOptions() { return this.options; }
12
14
  /**
13
15
  * Fetch a tenant by API key.
14
16
  * Results are cached for `cacheTtlMs` to avoid hammering the API on every request.
@@ -1 +1 @@
1
- {"version":3,"file":"tenant.js","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tenant.js","sourceRoot":"","sources":["../src/tenant.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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"}