@tenantscale/sdk 0.1.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,3 @@
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';
2
+ export { TenantClient } from './tenant.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,YAAY,EAAE,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // ──────────────────────────────────────────────────────
2
+ // @tenantscale/sdk — Barrel exports
3
+ // ──────────────────────────────────────────────────────
4
+ export { TenantScaleError, PlanLimitError, UnauthorizedError, } from './types.js';
5
+ export { TenantClient } from './tenant.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,oCAAoC;AACpC,yDAAyD;AAEzD,OAAO,EAUL,gBAAgB,EAChB,cAAc,EACd,iBAAiB,GAClB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,23 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import { TenantClient } from '../tenant.js';
3
+ import { type Tenant, type TenantScaleOptions } from '../types.js';
4
+ declare global {
5
+ namespace Express {
6
+ interface Request {
7
+ tenant?: Tenant;
8
+ tenantScale?: TenantClient;
9
+ }
10
+ }
11
+ }
12
+ /**
13
+ * TenantScale Express middleware.
14
+ *
15
+ * Usage:
16
+ * ```ts
17
+ * import { expressMiddleware } from '@tenantscale/sdk/middleware'
18
+ *
19
+ * app.use('/api', expressMiddleware({ apiKey: process.env.TENANTSCALE_API_KEY }))
20
+ * ```
21
+ */
22
+ export declare function expressMiddleware(options: TenantScaleOptions): (req: Request, res: Response, next: NextFunction) => Promise<void>;
23
+ //# sourceMappingURL=express.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/middleware/express.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAkB,MAAM,aAAa,CAAA;AAGlF,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,MAAM,CAAC,EAAE,MAAM,CAAA;YACf,WAAW,CAAC,EAAE,YAAY,CAAA;SAC3B;KACF;CACF;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,IAG7C,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBAuB9D"}
@@ -0,0 +1,42 @@
1
+ // ──────────────────────────────────────────────────────
2
+ // Express Middleware — add `req.tenant` to every request
3
+ // ──────────────────────────────────────────────────────
4
+ import { TenantClient } from '../tenant.js';
5
+ import { PlanLimitError } from '../types.js';
6
+ /**
7
+ * TenantScale Express middleware.
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * import { expressMiddleware } from '@tenantscale/sdk/middleware'
12
+ *
13
+ * app.use('/api', expressMiddleware({ apiKey: process.env.TENANTSCALE_API_KEY }))
14
+ * ```
15
+ */
16
+ export function expressMiddleware(options) {
17
+ const client = new TenantClient(options);
18
+ return async (req, res, next) => {
19
+ // 1. Extract API key
20
+ const authHeader = req.headers.authorization;
21
+ if (!authHeader?.startsWith('Bearer ')) {
22
+ res.status(401).json({ error: 'Missing or invalid Authorization header' });
23
+ return;
24
+ }
25
+ const apiKey = authHeader.slice(7);
26
+ // 2. Resolve tenant
27
+ try {
28
+ const tenant = await client.resolveTenant(apiKey);
29
+ req.tenant = tenant;
30
+ req.tenantScale = client;
31
+ next();
32
+ }
33
+ catch (err) {
34
+ if (err instanceof PlanLimitError) {
35
+ res.status(err.status).json({ error: err.message, code: err.code });
36
+ return;
37
+ }
38
+ res.status(401).json({ error: 'Failed to resolve tenant' });
39
+ }
40
+ };
41
+ }
42
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/middleware/express.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,yDAAyD;AACzD,yDAAyD;AAGzD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAwC,cAAc,EAAE,MAAM,aAAa,CAAA;AAYlF;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,CAAA;IAExC,OAAO,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QAC/D,qBAAqB;QACrB,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAA;QAC5C,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACvC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,CAAC,CAAA;YAC1E,OAAM;QACR,CAAC;QACD,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAElC,oBAAoB;QACpB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAA;YACjD,GAAG,CAAC,MAAM,GAAG,MAAM,CAAA;YACnB,GAAG,CAAC,WAAW,GAAG,MAAM,CAAA;YACxB,IAAI,EAAE,CAAA;QACR,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,cAAc,EAAE,CAAC;gBAClC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAA;gBACnE,OAAM;YACR,CAAC;YACD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAA;QAC7D,CAAC;IACH,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { type MiddlewareHandler } from 'hono';
2
+ import { TenantClient } from '../tenant.js';
3
+ import { type Tenant, type TenantScaleOptions } from '../types.js';
4
+ declare module 'hono' {
5
+ interface ContextVariableMap {
6
+ tenant: Tenant;
7
+ tenantClient: TenantClient;
8
+ }
9
+ }
10
+ /**
11
+ * TenantScale Hono middleware.
12
+ *
13
+ * Usage:
14
+ * ```ts
15
+ * import { tenantMiddleware } from '@tenantscale/sdk/middleware'
16
+ *
17
+ * const app = new Hono()
18
+ * app.use('/api/*', tenantMiddleware({ apiKey: process.env.TENANTSCALE_API_KEY }))
19
+ *
20
+ * app.get('/api/projects', (c) => {
21
+ * const tenant = c.var.tenant
22
+ * // tenant.id, tenant.plan, tenant.features
23
+ * })
24
+ * ```
25
+ */
26
+ export declare function tenantMiddleware(options: TenantScaleOptions): MiddlewareHandler;
27
+ export { TenantClient };
28
+ //# sourceMappingURL=hono.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../../src/middleware/hono.ts"],"names":[],"mappings":"AAIA,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAkB,MAAM,aAAa,CAAA;AAGlF,OAAO,QAAQ,MAAM,CAAC;IACpB,UAAU,kBAAkB;QAC1B,MAAM,EAAE,MAAM,CAAA;QACd,YAAY,EAAE,YAAY,CAAA;KAC3B;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,kBAAkB,GAC1B,iBAAiB,CA8BnB;AAED,OAAO,EAAE,YAAY,EAAE,CAAA"}
@@ -0,0 +1,48 @@
1
+ // ──────────────────────────────────────────────────────
2
+ // Hono Middleware — add `c.var.tenant` to every request
3
+ // ──────────────────────────────────────────────────────
4
+ import { TenantClient } from '../tenant.js';
5
+ import { PlanLimitError } from '../types.js';
6
+ /**
7
+ * TenantScale Hono middleware.
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * import { tenantMiddleware } from '@tenantscale/sdk/middleware'
12
+ *
13
+ * const app = new Hono()
14
+ * app.use('/api/*', tenantMiddleware({ apiKey: process.env.TENANTSCALE_API_KEY }))
15
+ *
16
+ * app.get('/api/projects', (c) => {
17
+ * const tenant = c.var.tenant
18
+ * // tenant.id, tenant.plan, tenant.features
19
+ * })
20
+ * ```
21
+ */
22
+ export function tenantMiddleware(options) {
23
+ const client = new TenantClient(options);
24
+ return async (c, next) => {
25
+ // 1. Extract API key from Authorization header
26
+ const authHeader = c.req.header('Authorization');
27
+ if (!authHeader?.startsWith('Bearer ')) {
28
+ return c.json({ error: 'Missing or invalid Authorization header' }, 401);
29
+ }
30
+ const apiKey = authHeader.slice(7);
31
+ // 2. Resolve tenant
32
+ try {
33
+ const tenant = await client.resolveTenant(apiKey);
34
+ // 3. Attach to context
35
+ c.set('tenant', tenant);
36
+ c.set('tenantClient', client);
37
+ await next();
38
+ }
39
+ catch (err) {
40
+ if (err instanceof PlanLimitError) {
41
+ return c.json({ error: err.message, code: err.code }, err.status);
42
+ }
43
+ return c.json({ error: 'Failed to resolve tenant' }, 401);
44
+ }
45
+ };
46
+ }
47
+ export { TenantClient };
48
+ //# sourceMappingURL=hono.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hono.js","sourceRoot":"","sources":["../../src/middleware/hono.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,wDAAwD;AACxD,yDAAyD;AAGzD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAwC,cAAc,EAAE,MAAM,aAAa,CAAA;AAUlF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAA2B;IAE3B,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,CAAA;IAExC,OAAO,KAAK,EAAE,CAAU,EAAE,IAAI,EAAE,EAAE;QAChC,+CAA+C;QAC/C,MAAM,UAAU,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAA;QAChD,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACvC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,EAAE,GAAG,CAAC,CAAA;QAC1E,CAAC;QACD,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAElC,oBAAoB;QACpB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAA;YAEjD,uBAAuB;YACvB,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;YACvB,CAAC,CAAC,GAAG,CAAC,cAAc,EAAE,MAAM,CAAC,CAAA;YAE7B,MAAM,IAAI,EAAE,CAAA;QACd,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,cAAc,EAAE,CAAC;gBAClC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE,GAAG,CAAC,MAAa,CAAC,CAAA;YAC1E,CAAC;YACD,OAAO,CAAC,CAAC,IAAI,CACX,EAAE,KAAK,EAAE,0BAA0B,EAAE,EACrC,GAAG,CACJ,CAAA;QACH,CAAC;IACH,CAAC,CAAA;AACH,CAAC;AAED,OAAO,EAAE,YAAY,EAAE,CAAA"}
@@ -0,0 +1,15 @@
1
+ import type { Tenant, TenantUser } from '../types.js';
2
+ export interface TenantContextValue {
3
+ tenant: Tenant | null;
4
+ user: TenantUser | null;
5
+ loading: boolean;
6
+ /** Switch to a different org (for multi-tenant users) */
7
+ switchTenant: (slug: string) => Promise<void>;
8
+ /** Check if a feature is enabled */
9
+ hasFeature: (feature: string) => boolean;
10
+ /** Log out of the current tenant session */
11
+ logout: () => Promise<void>;
12
+ }
13
+ export declare const TenantContext: import("react").Context<TenantContextValue>;
14
+ export declare function useTenant(): TenantContextValue;
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAErD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,OAAO,CAAA;IAChB,yDAAyD;IACzD,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,oCAAoC;IACpC,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;IACxC,4CAA4C;IAC5C,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B;AAED,eAAO,MAAM,aAAa,6CAOxB,CAAA;AAEF,wBAAgB,SAAS,uBAMxB"}
@@ -0,0 +1,21 @@
1
+ // ──────────────────────────────────────────────────────
2
+ // Next.js React helpers — tenant context for the App Router
3
+ // ──────────────────────────────────────────────────────
4
+ 'use client';
5
+ import { createContext, useContext } from 'react';
6
+ export const TenantContext = createContext({
7
+ tenant: null,
8
+ user: null,
9
+ loading: true,
10
+ switchTenant: async () => { },
11
+ hasFeature: () => false,
12
+ logout: async () => { },
13
+ });
14
+ export function useTenant() {
15
+ const ctx = useContext(TenantContext);
16
+ if (!ctx) {
17
+ throw new Error('useTenant must be used within a TenantProvider');
18
+ }
19
+ return ctx;
20
+ }
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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;QACT,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAA;IACnE,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { type Tenant, type TenantScaleOptions } from './types.js';
2
+ export declare class TenantClient {
3
+ private options;
4
+ private cache;
5
+ private cacheTtlMs;
6
+ constructor(options: TenantScaleOptions);
7
+ /**
8
+ * Fetch a tenant by API key.
9
+ * Results are cached for `cacheTtlMs` to avoid hammering the API on every request.
10
+ */
11
+ resolveTenant(apiKey: string): Promise<Tenant>;
12
+ /** Check if a tenant has access to a specific feature */
13
+ checkFeature(apiKey: string, feature: string): Promise<boolean>;
14
+ /** Track a usage event for metering */
15
+ trackEvent(apiKey: string, event: {
16
+ metric: string;
17
+ value: number;
18
+ properties?: Record<string, unknown>;
19
+ }): Promise<void>;
20
+ /** Log an audit event */
21
+ logAudit(apiKey: string, event: {
22
+ action: string;
23
+ resource: string;
24
+ details?: Record<string, unknown>;
25
+ actor_id?: string;
26
+ }): Promise<void>;
27
+ /** Invalidate cache for a specific key (called after tenant updates) */
28
+ invalidateCache(apiKey: string): void;
29
+ }
30
+ //# sourceMappingURL=tenant.d.ts.map
@@ -0,0 +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"}
package/dist/tenant.js ADDED
@@ -0,0 +1,88 @@
1
+ // ──────────────────────────────────────────────────────
2
+ // Tenant Client — fetches tenant context from the API
3
+ // ──────────────────────────────────────────────────────
4
+ import { TenantScaleError } from './types.js';
5
+ export class TenantClient {
6
+ options;
7
+ cache = new Map();
8
+ cacheTtlMs = 60_000; // 1 minute
9
+ constructor(options) {
10
+ this.options = options;
11
+ }
12
+ /**
13
+ * Fetch a tenant by API key.
14
+ * Results are cached for `cacheTtlMs` to avoid hammering the API on every request.
15
+ */
16
+ async resolveTenant(apiKey) {
17
+ // Check cache
18
+ const cached = this.cache.get(apiKey);
19
+ if (cached && cached.expiresAt > Date.now()) {
20
+ return cached.data;
21
+ }
22
+ const baseUrl = this.options.baseUrl ?? 'https://api.tenantscale.com';
23
+ const res = await fetch(`${baseUrl}/v1/tenants/me`, {
24
+ headers: {
25
+ Authorization: `Bearer ${apiKey}`,
26
+ 'Content-Type': 'application/json',
27
+ },
28
+ });
29
+ if (!res.ok) {
30
+ if (res.status === 401) {
31
+ throw new TenantScaleError('Invalid API key', 'UNAUTHORIZED', 401);
32
+ }
33
+ throw new TenantScaleError(`Failed to resolve tenant: ${res.statusText}`, 'TENANT_RESOLVE_FAILED', res.status);
34
+ }
35
+ const tenant = (await res.json());
36
+ // Cache it
37
+ this.cache.set(apiKey, {
38
+ data: tenant,
39
+ expiresAt: Date.now() + this.cacheTtlMs,
40
+ });
41
+ return tenant;
42
+ }
43
+ /** Check if a tenant has access to a specific feature */
44
+ async checkFeature(apiKey, feature) {
45
+ const tenant = await this.resolveTenant(apiKey);
46
+ const value = tenant.features[feature];
47
+ if (typeof value === 'boolean')
48
+ return value;
49
+ if (typeof value === 'number')
50
+ return value > 0;
51
+ return false;
52
+ }
53
+ /** Track a usage event for metering */
54
+ async trackEvent(apiKey, event) {
55
+ const baseUrl = this.options.baseUrl ?? 'https://api.tenantscale.com';
56
+ const res = await fetch(`${baseUrl}/v1/events`, {
57
+ method: 'POST',
58
+ headers: {
59
+ Authorization: `Bearer ${apiKey}`,
60
+ 'Content-Type': 'application/json',
61
+ },
62
+ body: JSON.stringify(event),
63
+ });
64
+ if (!res.ok) {
65
+ // Fire and forget — don't crash the request for metering
66
+ if (this.options.logging) {
67
+ console.warn('[TenantScale] Failed to track event:', res.statusText);
68
+ }
69
+ }
70
+ }
71
+ /** Log an audit event */
72
+ async logAudit(apiKey, event) {
73
+ const baseUrl = this.options.baseUrl ?? 'https://api.tenantscale.com';
74
+ await fetch(`${baseUrl}/v1/audit`, {
75
+ method: 'POST',
76
+ headers: {
77
+ Authorization: `Bearer ${apiKey}`,
78
+ 'Content-Type': 'application/json',
79
+ },
80
+ body: JSON.stringify(event),
81
+ });
82
+ }
83
+ /** Invalidate cache for a specific key (called after tenant updates) */
84
+ invalidateCache(apiKey) {
85
+ this.cache.delete(apiKey);
86
+ }
87
+ }
88
+ //# sourceMappingURL=tenant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant.js","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ /** A tenant (customer organization) as seen by the SDK */
2
+ export interface Tenant {
3
+ id: string;
4
+ name: string;
5
+ slug: string;
6
+ plan: PlanTier;
7
+ features: Record<string, boolean | number | string>;
8
+ config: Record<string, unknown>;
9
+ settings: TenantSettings;
10
+ }
11
+ export type PlanTier = 'free' | 'indie' | 'pro' | 'business';
12
+ export interface TenantSettings {
13
+ name?: string;
14
+ logo_url?: string;
15
+ primary_color?: string;
16
+ custom_domain?: string;
17
+ }
18
+ /** The user within a tenant context */
19
+ export interface TenantUser {
20
+ id: string;
21
+ email: string;
22
+ name: string;
23
+ role: 'owner' | 'admin' | 'member' | 'viewer';
24
+ tenant_id: string;
25
+ }
26
+ /** Options passed to the TenantScale constructor */
27
+ export interface TenantScaleOptions {
28
+ /** Your TenantScale API key (tk_xxx) */
29
+ apiKey: string;
30
+ /** Base URL for the TenantScale API (default: https://api.tenantscale.com) */
31
+ baseUrl?: string;
32
+ /** How to resolve the current tenant from the request */
33
+ tenantResolver?: TenantResolver;
34
+ /** Enable request logging */
35
+ logging?: boolean;
36
+ }
37
+ export type TenantResolver = {
38
+ type: 'header';
39
+ name: string;
40
+ } | {
41
+ type: 'subdomain';
42
+ extract: (host: string) => string | null;
43
+ } | {
44
+ type: 'jwt';
45
+ path: string[];
46
+ } | {
47
+ type: 'custom';
48
+ fn: (req: unknown) => string | null;
49
+ };
50
+ /** Audit event shape pushed from customer apps */
51
+ export interface AuditEvent {
52
+ action: string;
53
+ resource: string;
54
+ details?: Record<string, unknown>;
55
+ actor_id?: string;
56
+ }
57
+ /** Result of a feature check */
58
+ export interface FeatureCheck {
59
+ enabled: boolean;
60
+ /** If limited, how much is remaining (e.g. API calls left) */
61
+ remaining?: number;
62
+ /** The limit value from the plan */
63
+ limit?: number | null;
64
+ }
65
+ /** Impersonation session created by the admin */
66
+ export interface ImpersonationSession {
67
+ token: string;
68
+ targetUserId: string;
69
+ targetTenantId: string;
70
+ expiresAt: string;
71
+ redirectUrl: string;
72
+ }
73
+ export declare class TenantScaleError extends Error {
74
+ code: string;
75
+ status: number;
76
+ constructor(message: string, code: string, status?: number);
77
+ }
78
+ export declare class PlanLimitError extends TenantScaleError {
79
+ constructor(feature: string);
80
+ }
81
+ export declare class UnauthorizedError extends TenantScaleError {
82
+ constructor(message?: string);
83
+ }
84
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,29 @@
1
+ // ──────────────────────────────────────────────────────
2
+ // Tenant Types — the domain model for the SDK
3
+ // ──────────────────────────────────────────────────────
4
+ // ──────────────────────────────────────────────────────
5
+ // Error types
6
+ // ──────────────────────────────────────────────────────
7
+ export class TenantScaleError extends Error {
8
+ code;
9
+ status;
10
+ constructor(message, code, status = 500) {
11
+ super(message);
12
+ this.code = code;
13
+ this.status = status;
14
+ this.name = 'TenantScaleError';
15
+ }
16
+ }
17
+ export class PlanLimitError extends TenantScaleError {
18
+ constructor(feature) {
19
+ super(`Plan limit reached: ${feature}. Upgrade your plan to unlock this feature.`, 'PLAN_LIMIT_REACHED', 403);
20
+ this.name = 'PlanLimitError';
21
+ }
22
+ }
23
+ export class UnauthorizedError extends TenantScaleError {
24
+ constructor(message = 'Invalid or missing API key') {
25
+ super(message, 'UNAUTHORIZED', 401);
26
+ this.name = 'UnauthorizedError';
27
+ }
28
+ }
29
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,8CAA8C;AAC9C,yDAAyD;AA2EzD,yDAAyD;AACzD,cAAc;AACd,yDAAyD;AAEzD,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 ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@tenantscale/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Multi-tenant middleware SDK for B2B SaaS",
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
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "dev": "tsc --watch",
28
+ "test": "vitest run",
29
+ "lint": "eslint src/"
30
+ },
31
+ "dependencies": {
32
+ "superjson": "^2.2.0"
33
+ },
34
+ "peerDependencies": {
35
+ "express": "^5.0.0",
36
+ "hono": "^4.6.0",
37
+ "next": "^15.0.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "hono": {
41
+ "optional": true
42
+ },
43
+ "next": {
44
+ "optional": true
45
+ },
46
+ "express": {
47
+ "optional": true
48
+ }
49
+ },
50
+ "devDependencies": {
51
+ "@types/express": "^5.0.0",
52
+ "@types/react": "^19.0.0",
53
+ "@vitest/coverage-v8": "^3.2.6",
54
+ "hono": "^4.6.0",
55
+ "next": "^15.0.0",
56
+ "react": "^19.0.0",
57
+ "typescript": "^5.7.0",
58
+ "vitest": "^3.0.0"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }