@nebutra/tenant 0.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -372,6 +372,22 @@ interface TenantConfig {
372
372
 
373
373
  - `withRls(prisma, tenantId)` — Apply RLS extension to Prisma
374
374
  - `generateRlsPolicySql(options)` — Generate deterministic PostgreSQL RLS policy SQL
375
+ - `applyTenantSession(tx, tenantId, options?)` / `tenantSessionOperations(prisma, tenantId, options?)` —
376
+ The tenant session core: the transaction-local `SET LOCAL ROLE` (from `APP_DB_ROLE`) and
377
+ `set_config('app.current_tenant_id', …, true)` statements. `withRls` runs it as a batch
378
+ transaction; `withTenantContext` in `@nebutra/db/rls` runs it inside an interactive
379
+ transaction. One implementation, two shapes — new wrappers run the core rather than
380
+ restating the statements. (`getTenantDb` in `@nebutra/db` still carries its own copy
381
+ until the P1.2 follow-up routes it through `tenantSessionOperations`.)
382
+ - `resolveRlsRole(env?)` / `isValidDbRole(role)` — `APP_DB_ROLE` resolved as a bare SQL identifier
383
+ (permissive: `null` on invalid)
384
+ - `resolveRlsRoleOrThrow(env?)` — the same resolution, but throws `TenantIsolationError` when
385
+ `APP_DB_ROLE` is set to something that is not a bare SQL identifier, instead of returning
386
+ `null`. `applyTenantSession`/`tenantSessionOperations` resolve the role through this when no
387
+ explicit `role` option is given, and so does `withRls` (closure P1.3 — an unusable
388
+ `APP_DB_ROLE` refuses to run rather than silently dropping the role switch)
389
+ - `TENANT_SESSION_SETTING` / `TENANT_SESSION_EXPRESSION` — the setting key the policies read and
390
+ the wrappers write
375
391
  - `getTenantSchema(tenantId)` — Get schema name for schema-per-tenant
376
392
  - `getTenantDatabaseUrl(tenantId, baseUrl?)` — Get DB URL for database-per-tenant
377
393
  - `TenantAwarePrismaClient` — Wrapper class for tenant isolation
@@ -485,6 +501,6 @@ function AdvancedAnalytics() {
485
501
 
486
502
  ## See Also
487
503
 
488
- - [@nebutra/logger](../logger) — Structured logging
489
- - [@nebutra/queue](../queue) — Message queue with tenant support
504
+ - [@nebutra/logger](../../platform/logger/) — Structured logging
505
+ - [@nebutra/queue](../../integrations/queue/) — Message queue with tenant support
490
506
  - Prisma RLS [Documentation](https://www.prisma.io/docs/orm/prisma-client/queries/raw-database-access)
package/dist/context.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TenantContext } from "./types";
1
+ import type { TenantContext } from "./types.js";
2
2
  /**
3
3
  * Execute a function with a tenant context set.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAW7C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAS7F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAWhD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,IAAI,aAAa,GAAG,IAAI,CAEtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EACxC,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,IAAI,aAAa,CASjC;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAEjD"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAWhD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAS7F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAWhD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,IAAI,aAAa,GAAG,IAAI,CAEtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EACxC,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,IAAI,aAAa,CASjC;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAEjD"}
package/dist/context.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { logger } from "@nebutra/logger";
3
- import { TenantRequiredError } from "./types";
3
+ import { TenantRequiredError } from "./types.js";
4
4
  /**
5
5
  * AsyncLocalStorage-based tenant context — request-scoped, zero-copy across async boundaries.
6
6
  *
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- export { getCurrentTenant, getCurrentTenantId, getTenantIdOrNull, getTenantOrNull, requireTenant, runWithTenant, } from "./context";
2
- export { compose, fromApiKey, fromHeader, fromJwtClaim, fromPath, fromSubdomain, } from "./resolvers";
3
- export type { AuthSessionLike, SessionGetter } from "./resolvers/from-auth-session";
4
- export { fromAuthSession } from "./resolvers/from-auth-session";
5
- export type { IsolationStrategy, PlanTier, TenantConfig, TenantContext, TenantInfo, TenantResolver, } from "./types";
6
- export { TenantConfigSchema, TenantContextSchema, TenantInfoSchema, TenantIsolationError, TenantRequiredError, } from "./types";
7
- export type { RlsPolicyCommand, RlsPolicySqlOptions } from "./isolation";
8
- export { createTenantPrismaProxy, generateRlsPolicySql, getTenantDatabaseUrl, getTenantSchema, TenantAwarePrismaClient, withRls, } from "./isolation";
1
+ export { getCurrentTenant, getCurrentTenantId, getTenantIdOrNull, getTenantOrNull, requireTenant, runWithTenant, } from "./context.js";
2
+ export type { AuthSessionLike, SessionGetter } from "./resolvers/from-auth-session.js";
3
+ export { fromAuthSession } from "./resolvers/from-auth-session.js";
4
+ export { compose, fromApiKey, fromHeader, fromJwtClaim, fromPath, fromSubdomain, } from "./resolvers.js";
5
+ export type { IsolationStrategy, PlanTier, TenantConfig, TenantContext, TenantInfo, TenantResolver, } from "./types.js";
6
+ export { TenantConfigSchema, TenantContextSchema, TenantInfoSchema, TenantIsolationError, TenantRequiredError, } from "./types.js";
7
+ export type { RlsPolicyCommand, RlsPolicySqlOptions, TenantSessionExecutor, TenantSessionOptions, } from "./isolation.js";
8
+ export { applyTenantSession, createTenantPrismaProxy, generateRlsPolicySql, getTenantDatabaseUrl, getTenantSchema, isValidDbRole, resolveRlsRole, TENANT_SESSION_EXPRESSION, TENANT_SESSION_SETTING, TenantAwarePrismaClient, tenantSessionOperations, withRls, } from "./isolation.js";
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,OAAO,EACP,UAAU,EACV,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACpF,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEhE,YAAY,EACV,iBAAiB,EACjB,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,UAAU,EACV,cAAc,GACf,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,SAAS,CAAC;AAKjB,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEzE,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,uBAAuB,EACvB,OAAO,GACR,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAEnE,OAAO,EACL,OAAO,EACP,UAAU,EACV,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,aAAa,GACd,MAAM,gBAAgB,CAAC;AAExB,YAAY,EACV,iBAAiB,EACjB,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,UAAU,EACV,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAKpB,YAAY,EACV,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,aAAa,EACb,cAAc,EACd,yBAAyB,EACzB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,OAAO,GACR,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -2,12 +2,13 @@
2
2
  // @nebutra/tenant — Multi-tenancy context and isolation
3
3
  // =============================================================================
4
4
  // Re-export context functions
5
- export { getCurrentTenant, getCurrentTenantId, getTenantIdOrNull, getTenantOrNull, requireTenant, runWithTenant, } from "./context";
5
+ export { getCurrentTenant, getCurrentTenantId, getTenantIdOrNull, getTenantOrNull, requireTenant, runWithTenant, } from "./context.js";
6
+ export { fromAuthSession } from "./resolvers/from-auth-session.js";
6
7
  // Re-export resolvers
7
- export { compose, fromApiKey, fromHeader, fromJwtClaim, fromPath, fromSubdomain, } from "./resolvers";
8
- export { fromAuthSession } from "./resolvers/from-auth-session";
9
- export { TenantConfigSchema, TenantContextSchema, TenantInfoSchema, TenantIsolationError, TenantRequiredError, } from "./types";
10
- // Re-export isolation helpers
11
- export { createTenantPrismaProxy, generateRlsPolicySql, getTenantDatabaseUrl, getTenantSchema, TenantAwarePrismaClient, withRls, } from "./isolation";
8
+ export { compose, fromApiKey, fromHeader, fromJwtClaim, fromPath, fromSubdomain, } from "./resolvers.js";
9
+ export { TenantConfigSchema, TenantContextSchema, TenantInfoSchema, TenantIsolationError, TenantRequiredError, } from "./types.js";
10
+ // Re-export isolation helpers — including the tenant session core shared with
11
+ // `@nebutra/db/rls` (closure P1.2: one implementation behind both wrappers).
12
+ export { applyTenantSession, createTenantPrismaProxy, generateRlsPolicySql, getTenantDatabaseUrl, getTenantSchema, isValidDbRole, resolveRlsRole, TENANT_SESSION_EXPRESSION, TENANT_SESSION_SETTING, TenantAwarePrismaClient, tenantSessionOperations, withRls, } from "./isolation.js";
12
13
  // Re-export React hooks (as subpath export ./react)
13
14
  // These are exported via package.json "exports" for tree-shaking
@@ -1,9 +1,12 @@
1
- import type { IsolationStrategy } from "./types";
1
+ import type { IsolationStrategy } from "./types.js";
2
+ export { applyTenantSession, isValidDbRole, resolveRlsRole, resolveRlsRoleOrThrow, TENANT_SESSION_EXPRESSION, TENANT_SESSION_SETTING, type TenantSessionExecutor, type TenantSessionOptions, tenantSessionOperations, } from "./rls-session.js";
2
3
  /** Minimal Prisma-like client interface for RLS extension support. */
3
4
  interface PrismaLikeClient {
4
5
  $extends?: (extension: unknown) => unknown;
5
6
  $executeRaw?: (query: TemplateStringsArray, ...values: unknown[]) => Promise<number>;
6
7
  $queryRaw?: (query: TemplateStringsArray, ...values: unknown[]) => Promise<unknown[]>;
8
+ $transaction?: (operations: unknown[]) => Promise<unknown[]>;
9
+ $executeRawUnsafe?: (query: string) => Promise<number>;
7
10
  }
8
11
  export type RlsPolicyCommand = "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
9
12
  export interface RlsPolicySqlOptions {
@@ -35,8 +38,10 @@ export declare function generateRlsPolicySql(options: RlsPolicySqlOptions): stri
35
38
  * Works with PostgreSQL RLS policies that check `app.current_tenant_id`.
36
39
  * This is the standard pattern for shared-schema multi-tenancy.
37
40
  *
38
- * The Prisma client middleware intercepts all queries and sets the
39
- * application-level variable before executing.
41
+ * The Prisma client extension intercepts all queries and sets the
42
+ * application-level variable before executing. Missing `$extends`,
43
+ * `$transaction`, or `$executeRaw` throws `TenantIsolationError` instead
44
+ * of running an unisolated query. Production also requires `APP_DB_ROLE`.
40
45
  *
41
46
  * @param prisma The Prisma client to extend
42
47
  * @param tenantId The tenant ID to set in RLS context
@@ -130,13 +135,20 @@ export declare class TenantAwarePrismaClient {
130
135
  */
131
136
  getDatabaseUrl(baseUrl?: string): string;
132
137
  /**
133
- * Execute a raw SQL query with tenant context.
138
+ * Execute a raw SQL statement with tenant context.
139
+ *
140
+ * Tagged-template only — interpolated values are bound as parameters by
141
+ * Prisma, never concatenated into the SQL string:
142
+ * client.executeRaw`UPDATE users SET active = ${flag} WHERE id = ${id}`
134
143
  */
135
- executeRaw(query: string): Promise<number>;
144
+ executeRaw(query: TemplateStringsArray, ...values: unknown[]): Promise<number>;
136
145
  /**
137
- * Execute a raw query and return results.
146
+ * Execute a raw SQL query with tenant context and return rows.
147
+ *
148
+ * Tagged-template only — interpolated values are bound as parameters:
149
+ * client.queryRaw`SELECT * FROM users WHERE id = ${id}`
138
150
  */
139
- queryRaw(query: string): Promise<unknown[]>;
151
+ queryRaw(query: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]>;
140
152
  }
141
153
  /**
142
154
  * Create a tenant-aware Prisma proxy that applies isolation based on strategy.
@@ -153,5 +165,4 @@ export declare class TenantAwarePrismaClient {
153
165
  * ```
154
166
  */
155
167
  export declare function createTenantPrismaProxy(prisma: PrismaLikeClient, tenantId: string, strategy?: IsolationStrategy): PrismaLikeClient;
156
- export {};
157
168
  //# sourceMappingURL=isolation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"isolation.d.ts","sourceRoot":"","sources":["../src/isolation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAOjD,sEAAsE;AACtE,UAAU,gBAAgB;IACxB,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,OAAO,CAAC;IAC3C,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrF,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CACvF;AAED,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEjF,MAAM,WAAW,mBAAmB;IAClC,4FAA4F;IAC5F,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yFAAyF;IACzF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,oDAAoD;IACpD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAyCD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CA2DzE;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,CAiClF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAmBxD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA6B/E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,QAAQ;gBADR,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,MAAM;IAK1B;;OAEG;IACH,IAAI,MAAM,qBAET;IAED;;OAEG;IACH,SAAS,IAAI,MAAM;IAInB;;OAEG;IACH,cAAc,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM;IAIxC;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwBhD;;OAEG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CAoBlD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,iBAAmC,GAC5C,gBAAgB,CAmBlB"}
1
+ {"version":3,"file":"isolation.d.ts","sourceRoot":"","sources":["../src/isolation.ts"],"names":[],"mappings":"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"}
package/dist/isolation.js CHANGED
@@ -1,8 +1,15 @@
1
1
  import { logger } from "@nebutra/logger";
2
- import { TenantIsolationError } from "./types";
2
+ import { resolveRlsRoleOrThrow, TENANT_SESSION_EXPRESSION, tenantSessionOperations, } from "./rls-session.js";
3
+ import { TenantIsolationError } from "./types.js";
4
+ // The tenant session core is the single implementation behind `withRls` here
5
+ // and `withTenantContext` in `@nebutra/db/rls`. Re-exported so consumers of
6
+ // `@nebutra/tenant/isolation` reach it without a second entry point.
7
+ export { applyTenantSession, isValidDbRole, resolveRlsRole, resolveRlsRoleOrThrow, TENANT_SESSION_EXPRESSION, TENANT_SESSION_SETTING, tenantSessionOperations, } from "./rls-session.js";
3
8
  const DEFAULT_TENANT_COLUMN = "tenant_id";
4
9
  const DEFAULT_POLICY_PREFIX = "tenant_isolation";
5
- const DEFAULT_TENANT_EXPRESSION = "current_setting('app.current_tenant_id', true)";
10
+ // Same key the session core writes with set_config — policies and wrappers
11
+ // cannot disagree about which setting carries the tenant.
12
+ const DEFAULT_TENANT_EXPRESSION = TENANT_SESSION_EXPRESSION;
6
13
  function assertNonEmptyIdentifier(value, label) {
7
14
  if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
8
15
  throw new TenantIsolationError(`Invalid ${label} for RLS policy generation`, "shared_schema");
@@ -79,8 +86,10 @@ export function generateRlsPolicySql(options) {
79
86
  * Works with PostgreSQL RLS policies that check `app.current_tenant_id`.
80
87
  * This is the standard pattern for shared-schema multi-tenancy.
81
88
  *
82
- * The Prisma client middleware intercepts all queries and sets the
83
- * application-level variable before executing.
89
+ * The Prisma client extension intercepts all queries and sets the
90
+ * application-level variable before executing. Missing `$extends`,
91
+ * `$transaction`, or `$executeRaw` throws `TenantIsolationError` instead
92
+ * of running an unisolated query. Production also requires `APP_DB_ROLE`.
84
93
  *
85
94
  * @param prisma The Prisma client to extend
86
95
  * @param tenantId The tenant ID to set in RLS context
@@ -104,31 +113,51 @@ export function generateRlsPolicySql(options) {
104
113
  * ```
105
114
  */
106
115
  export function withRls(prisma, tenantId) {
116
+ const extendClient = prisma.$extends;
117
+ const runTransaction = prisma.$transaction;
118
+ const executeRaw = prisma.$executeRaw;
119
+ if (typeof extendClient !== "function") {
120
+ throw new TenantIsolationError("Prisma client does not support $extends; refusing unisolated queries", "shared_schema");
121
+ }
122
+ if (typeof runTransaction !== "function" || typeof executeRaw !== "function") {
123
+ throw new TenantIsolationError("Prisma client cannot apply transaction-local RLS; refusing unisolated queries", "shared_schema");
124
+ }
125
+ // Closure P1.3: an unusable APP_DB_ROLE (not a bare SQL identifier) throws
126
+ // here instead of resolving to null and silently running without RLS.
127
+ const rlsRole = resolveRlsRoleOrThrow();
128
+ if (process.env.NODE_ENV === "production" && !rlsRole) {
129
+ throw new TenantIsolationError("APP_DB_ROLE is required in production so withRls cannot run as a BYPASSRLS owner", "shared_schema");
130
+ }
131
+ // Prisma's `$executeRaw*` and `$transaction` are prototype methods that read
132
+ // `this`, so the session core is handed bound adapters rather than the
133
+ // detached functions captured above for the capability checks.
134
+ const executeRawUnsafe = prisma.$executeRawUnsafe;
135
+ const executor = {
136
+ $executeRaw: (query, ...values) => executeRaw.call(prisma, query, ...values),
137
+ $executeRawUnsafe: typeof executeRawUnsafe === "function"
138
+ ? (query) => executeRawUnsafe.call(prisma, query)
139
+ : undefined,
140
+ };
107
141
  try {
108
- // Check if Prisma client supports $extends (v5+)
109
- if (typeof prisma.$extends === "function") {
110
- // Create a Prisma client extension that sets RLS context on each query
111
- const extended = prisma.$extends({
112
- query: {
113
- $allOperations: {
114
- async $before() {
115
- // Execute SET command to set application variable
116
- if (typeof prisma.$executeRaw === "function") {
117
- await prisma.$executeRaw `SELECT set_config('app.current_tenant_id', ${tenantId}, false)`;
118
- }
119
- logger.debug("RLS context set", { tenantId });
120
- },
121
- },
142
+ const extended = extendClient.call(prisma, {
143
+ query: {
144
+ async $allOperations({ args, query, }) {
145
+ // One batch transaction: role switch (if configured) → tenant
146
+ // setting → the model query, all on the same connection.
147
+ const ops = tenantSessionOperations(executor, tenantId, { role: rlsRole });
148
+ ops.push(query(args));
149
+ const results = (await runTransaction.call(prisma, ops));
150
+ logger.debug("RLS context set", { tenantId });
151
+ return results[results.length - 1];
122
152
  },
123
- });
124
- return extended;
125
- }
126
- logger.debug("withRls: Prisma client does not support $extends, returning original client", {
127
- tenantId,
153
+ },
128
154
  });
129
- return prisma;
155
+ return extended;
130
156
  }
131
157
  catch (err) {
158
+ if (err instanceof TenantIsolationError) {
159
+ throw err;
160
+ }
132
161
  logger.error("Failed to apply RLS extension", err, { tenantId });
133
162
  throw new TenantIsolationError(`Failed to apply RLS isolation for tenant ${tenantId}`, "shared_schema");
134
163
  }
@@ -247,19 +276,22 @@ export class TenantAwarePrismaClient {
247
276
  return getTenantDatabaseUrl(this.tenantId, baseUrl);
248
277
  }
249
278
  /**
250
- * Execute a raw SQL query with tenant context.
279
+ * Execute a raw SQL statement with tenant context.
280
+ *
281
+ * Tagged-template only — interpolated values are bound as parameters by
282
+ * Prisma, never concatenated into the SQL string:
283
+ * client.executeRaw`UPDATE users SET active = ${flag} WHERE id = ${id}`
251
284
  */
252
- async executeRaw(query) {
285
+ async executeRaw(query, ...values) {
253
286
  try {
254
- logger.debug("Executing raw query with tenant context", {
287
+ logger.debug("Executing raw statement with tenant context", {
255
288
  tenantId: this.tenantId,
256
- queryLength: query.length,
257
289
  });
258
290
  const client = this.client;
259
291
  if (!client.$executeRaw) {
260
292
  throw new TenantIsolationError("Prisma client does not support $executeRaw", "shared_schema");
261
293
  }
262
- return await client.$executeRaw `${query}`;
294
+ return await client.$executeRaw(query, ...values);
263
295
  }
264
296
  catch (err) {
265
297
  logger.error("Failed to execute raw query", err, { tenantId: this.tenantId });
@@ -267,19 +299,21 @@ export class TenantAwarePrismaClient {
267
299
  }
268
300
  }
269
301
  /**
270
- * Execute a raw query and return results.
302
+ * Execute a raw SQL query with tenant context and return rows.
303
+ *
304
+ * Tagged-template only — interpolated values are bound as parameters:
305
+ * client.queryRaw`SELECT * FROM users WHERE id = ${id}`
271
306
  */
272
- async queryRaw(query) {
307
+ async queryRaw(query, ...values) {
273
308
  try {
274
- logger.debug("Executing query with tenant context", {
309
+ logger.debug("Executing raw query with tenant context", {
275
310
  tenantId: this.tenantId,
276
- queryLength: query.length,
277
311
  });
278
312
  const client = this.client;
279
313
  if (!client.$queryRaw) {
280
314
  throw new TenantIsolationError("Prisma client does not support $queryRaw", "shared_schema");
281
315
  }
282
- return await client.$queryRaw `${query}`;
316
+ return await client.$queryRaw(query, ...values);
283
317
  }
284
318
  catch (err) {
285
319
  logger.error("Failed to execute query", err, { tenantId: this.tenantId });
@@ -1,4 +1,4 @@
1
- import type { TenantConfig } from "./types";
1
+ import type { TenantConfig } from "./types.js";
2
2
  /** Minimal Hono-like context — matches Hono's `Context` without importing it. */
3
3
  interface HonoLikeContext {
4
4
  req: {
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,SAAS,CAAC;AAO5D,iFAAiF;AACjF,UAAU,eAAe;IACvB,GAAG,EAAE;QACH,GAAG,EAAE;YAAE,OAAO,EAAE,OAAO,CAAA;SAAE,CAAC;QAC1B,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;KAC9C,CAAC;IACF,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,QAAQ,CAAC;CACnD;AAYD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,YAAY,CAAM,IAuBnD,GAAG,eAAe,EAAE,MAAM,MAAM,OAAO,CAAC,QAAQ,CAAC,uBAyDhE;AAMD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,GAAG,EAAE,EAAE,CAAC,EAC3C,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EACvC,MAAM,GAAE,OAAO,CAAC,YAAY,CAAM,IAQpB,GAAG,MAAM,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC,CAsDtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,GAAG,EAAE,EAAE,CAAC,EACjD,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EACvC,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,IAE3C,GAAG,MAAM,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC,CAiBtC"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,YAAY,CAAC;AAO/D,iFAAiF;AACjF,UAAU,eAAe;IACvB,GAAG,EAAE;QACH,GAAG,EAAE;YAAE,OAAO,EAAE,OAAO,CAAA;SAAE,CAAC;QAC1B,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;KAC9C,CAAC;IACF,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,QAAQ,CAAC;CACnD;AAYD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,YAAY,CAAM,IAuBnD,GAAG,eAAe,EAAE,MAAM,MAAM,OAAO,CAAC,QAAQ,CAAC,uBAyDhE;AAMD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,GAAG,EAAE,EAAE,CAAC,EAC3C,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EACvC,MAAM,GAAE,OAAO,CAAC,YAAY,CAAM,IAQpB,GAAG,MAAM,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC,CAsDtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,GAAG,EAAE,EAAE,CAAC,EACjD,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EACvC,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,IAE3C,GAAG,MAAM,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC,CAiBtC"}
@@ -1,7 +1,7 @@
1
1
  import { logger } from "@nebutra/logger";
2
- import { runWithTenant } from "./context";
3
- import { fromHeader, fromJwtClaim, fromPath, fromSubdomain } from "./resolvers";
4
- import { TenantRequiredError } from "./types";
2
+ import { runWithTenant } from "./context.js";
3
+ import { fromHeader, fromJwtClaim, fromPath, fromSubdomain } from "./resolvers.js";
4
+ import { TenantRequiredError } from "./types.js";
5
5
  // =============================================================================
6
6
  // Hono Middleware for Multi-Tenant Context
7
7
  // =============================================================================
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type { TenantContext } from "./types";
2
+ import type { TenantContext } from "./types.js";
3
3
  /**
4
4
  * Provider component that wraps your app with tenant context.
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAEA,OAAO,KAAoC,MAAM,OAAO,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAe7C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,EAAE,aAAa,CAAC;CACtB;;;;;;;WAMA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,IAAI,aAAa,CAUzC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,IAAI,aAAa,GAAG,IAAI,CAEtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAGpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAEjD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,8CAG5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG3F;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAC9C,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,EACjC,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;YAEpB,CAAC;eAFW,KAAK;;;EAqBnD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,uDAYA"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAEA,OAAO,KAAoC,MAAM,OAAO,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAehD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,EAAE,aAAa,CAAC;CACtB;;;;;;;WAMA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,IAAI,aAAa,CAUzC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,IAAI,aAAa,GAAG,IAAI,CAEtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAGpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAEjD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,8CAG5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG3F;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAC9C,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,EACjC,aAAa,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;YAEpB,CAAC;eAFW,KAAK;;;EAqBnD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,uDAYA"}
package/dist/react.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import React, { createContext, useContext } from "react";
3
- import { TenantRequiredError } from "./types";
3
+ import { TenantRequiredError } from "./types.js";
4
4
  // =============================================================================
5
5
  // React Context for Tenant — Client-side multi-tenancy
6
6
  // =============================================================================
@@ -1,4 +1,4 @@
1
- import type { TenantResolver } from "../types";
1
+ import type { TenantResolver } from "../types.js";
2
2
  /**
3
3
  * Minimal shape this resolver needs from a session object.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"from-auth-session.d.ts","sourceRoot":"","sources":["../../src/resolvers/from-auth-session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAM/C;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,GAAG,EAAE,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,KAC/B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,eAAe,GAAG,IAAI,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,aAAa,GAAG,cAAc,CAgBzE"}
1
+ {"version":3,"file":"from-auth-session.d.ts","sourceRoot":"","sources":["../../src/resolvers/from-auth-session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAMlD;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,GAAG,EAAE,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,KAC/B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,eAAe,GAAG,IAAI,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,aAAa,GAAG,cAAc,CAgBzE"}
@@ -1,4 +1,4 @@
1
- import type { TenantResolver } from "./types";
1
+ import type { TenantResolver } from "./types.js";
2
2
  /**
3
3
  * Extract tenant ID from an HTTP header.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../src/resolvers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAsD9C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,UAAU,GAAE,MAAsB,GAAG,cAAc,CAW7E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAsB7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAwBvD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAsB9D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,cAAc,CAiB/F;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,OAAO,CAAC,GAAG,SAAS,EAAE,cAAc,EAAE,GAAG,cAAc,CAiBtE"}
1
+ {"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../src/resolvers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAsDjD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,UAAU,GAAE,MAAsB,GAAG,cAAc,CAW7E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAsB7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAwBvD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAsB9D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,cAAc,CAiB/F;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,OAAO,CAAC,GAAG,SAAS,EAAE,cAAc,EAAE,GAAG,cAAc,CAiBtE"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Tenant session core — how a PostgreSQL transaction is scoped to a tenant for
3
+ * row-level security, shared by both public wrappers:
4
+ *
5
+ * - `withRls(prisma, tenantId)` (`@nebutra/tenant/isolation`)
6
+ * - `withTenantContext(prisma, tenantId, cb)` (`@nebutra/db/rls`)
7
+ *
8
+ * Both issue exactly the statements this module produces. The RLS policies
9
+ * (`generateRlsPolicySql`, migration `20260313000000_enable_rls`) read
10
+ * `current_setting('app.current_tenant_id', true)`; the wrappers write the same
11
+ * key through `set_config(..., true)`, so the value is transaction-local and
12
+ * cannot leak across pooled connections. Keeping the key and the statements
13
+ * here — rather than once per wrapper — is closure item P1.2: two copies of a
14
+ * security invariant drift, one copy cannot.
15
+ *
16
+ * Closure P1.3: an `APP_DB_ROLE` that is configured but unusable must refuse
17
+ * to run rather than quietly drop the role switch and execute as the
18
+ * connection's own (possibly BYPASSRLS) role. Two shapes of "unusable" are
19
+ * handled here — `resolveSessionRole` refuses a value that fails
20
+ * `isValidDbRole`, and `planTenantSession` refuses when the executor cannot
21
+ * run `$executeRawUnsafe` at all, so the role switch has nowhere to go.
22
+ * Neither case matters to `getTenantDb` in `@nebutra/db` (`src/client.ts`):
23
+ * it still carries its own copy of the RLS statements, closes the same gap
24
+ * with its own verification (`rls-role.ts`), and is unaffected either way.
25
+ *
26
+ * Not yet routed through here: `getTenantDb` in `@nebutra/db` (`src/client.ts`)
27
+ * still carries its own copy of these statements, with a
28
+ * `SET LOCAL statement_timeout` between the role switch and `set_config`. The
29
+ * P1.2 follow-up moves it onto `tenantSessionOperations`; until then that copy
30
+ * is the one other place these statements exist, and it must not gain siblings.
31
+ *
32
+ * This module imports nothing outside `@nebutra/tenant` (only `./types`, whose
33
+ * sole dependency is zod): `@nebutra/db` consumes it, and it must stay usable
34
+ * from any Prisma-like executor (interactive transaction, batch transaction, or
35
+ * a client extension).
36
+ */
37
+ /** PostgreSQL session setting the RLS policies compare `tenant_id` against. */
38
+ export declare const TENANT_SESSION_SETTING = "app.current_tenant_id";
39
+ /**
40
+ * SQL expression the generated RLS policies use to read the tenant. The second
41
+ * argument (`missing_ok = true`) makes an unset session yield NULL — which the
42
+ * policy predicate then rejects — instead of raising.
43
+ */
44
+ export declare const TENANT_SESSION_EXPRESSION = "current_setting('app.current_tenant_id', true)";
45
+ /** True when `role` is a bare SQL identifier safe to interpolate into `SET LOCAL ROLE`. */
46
+ export declare function isValidDbRole(role: unknown): role is string;
47
+ /**
48
+ * Resolve the optional non-BYPASSRLS role tenant-scoped transactions assume —
49
+ * e.g. `app_user` on Supabase, whose `postgres` connection role bypasses RLS.
50
+ *
51
+ * Resolved at call time (not module load) so every wrapper sees the same
52
+ * environment and tests can exercise both shapes.
53
+ *
54
+ * @returns the validated role, or `null` when unset or not a bare identifier
55
+ */
56
+ export declare function resolveRlsRole(env?: {
57
+ APP_DB_ROLE?: string | undefined;
58
+ }): string | null;
59
+ /**
60
+ * Resolve `APP_DB_ROLE` the way `resolveRlsRole` does, but fail closed:
61
+ * when it is set to something that is not a bare SQL identifier, throw
62
+ * `TenantIsolationError` instead of silently returning `null`.
63
+ *
64
+ * Closure P1.3 — `resolveRlsRole`'s null-on-invalid contract is what let an
65
+ * unusable `APP_DB_ROLE` disable RLS silently: every caller that treated
66
+ * `null` as "no role configured" ran the query as the connection's own
67
+ * (possibly BYPASSRLS) role instead of refusing. `resolveRlsRole` keeps that
68
+ * permissive contract for callers that genuinely want it (diagnostics,
69
+ * tooling); every tenant-scoped code path resolves the role through this
70
+ * function instead.
71
+ *
72
+ * @throws TenantIsolationError when `APP_DB_ROLE` is set but not a bare SQL
73
+ * identifier.
74
+ */
75
+ export declare function resolveRlsRoleOrThrow(env?: {
76
+ APP_DB_ROLE?: string | undefined;
77
+ }): string | null;
78
+ /**
79
+ * The subset of a Prisma client (or interactive-transaction client) the tenant
80
+ * session needs. `$executeRaw` is tagged-template only so `tenantId` is always
81
+ * bound as a parameter; `$executeRawUnsafe` carries the role switch, whose
82
+ * identifier is validated by `isValidDbRole` before it is interpolated.
83
+ */
84
+ export interface TenantSessionExecutor {
85
+ $executeRaw: (query: TemplateStringsArray, ...values: unknown[]) => PromiseLike<number>;
86
+ $executeRawUnsafe?: ((query: string) => PromiseLike<number>) | undefined;
87
+ }
88
+ export interface TenantSessionOptions {
89
+ /**
90
+ * Role to assume before scoping the transaction.
91
+ *
92
+ * - `undefined` (default): resolve from `APP_DB_ROLE` via `resolveRlsRoleOrThrow()`
93
+ * - `null`: never switch role (admin / migration paths that must run as owner)
94
+ * - a string: use this validated identifier
95
+ */
96
+ role?: string | null | undefined;
97
+ }
98
+ /**
99
+ * Issue the tenant-session statements through `executor` and return them in
100
+ * order, without awaiting. Prisma promises are lazy, so the returned array can
101
+ * be spread into a batch `$transaction([...ops, query])` and will run inside
102
+ * that transaction, in order, ahead of the query.
103
+ */
104
+ export declare function tenantSessionOperations(executor: TenantSessionExecutor, tenantId: string, options?: TenantSessionOptions): PromiseLike<number>[];
105
+ /**
106
+ * Run the tenant-session statements sequentially on `executor` — an interactive
107
+ * transaction client — and resolve once the transaction is scoped to `tenantId`.
108
+ */
109
+ export declare function applyTenantSession(executor: TenantSessionExecutor, tenantId: string, options?: TenantSessionOptions): Promise<void>;
110
+ //# sourceMappingURL=rls-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rls-session.d.ts","sourceRoot":"","sources":["../src/rls-session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAIH,+EAA+E;AAC/E,eAAO,MAAM,sBAAsB,0BAA0B,CAAC;AAE9D;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,mDAAuD,CAAC;AAQ9F,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,MAAM,CAE3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,GAAG,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAgB,GACtD,MAAM,GAAG,IAAI,CAGf;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAgB,GACtD,MAAM,GAAG,IAAI,CAaf;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,CAAC,KAAK,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,WAAW,CAAC,MAAM,CAAC,CAAC;IACxF,iBAAiB,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;CAC1E;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAClC;AAoED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,qBAAqB,EAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,oBAAyB,GACjC,WAAW,CAAC,MAAM,CAAC,EAAE,CAGvB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,qBAAqB,EAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAKf"}