@zanzojs/drizzle 0.3.0 → 0.3.2
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 +34 -11
- package/dist/index.cjs +3 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -70,26 +70,49 @@ async function getReadableDocuments(userId: string) {
|
|
|
70
70
|
}
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
##
|
|
73
|
+
## Cloudflare D1 & Edge Runtime
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
Zanzo is fully compatible with Cloudflare Pages and D1. Since the D1 driver for Drizzle is asynchronous, ensuring your adapter is correctly configured is key.
|
|
76
|
+
|
|
77
|
+
### 1. Configuration for D1
|
|
78
|
+
When using Cloudflare D1, always set the `dialect` to `'sqlite'`.
|
|
76
79
|
|
|
77
80
|
```typescript
|
|
81
|
+
import { createZanzoAdapter } from '@zanzojs/drizzle';
|
|
82
|
+
import { engine } from './zanzo.config';
|
|
83
|
+
import { zanzoTuples } from './schema';
|
|
84
|
+
|
|
85
|
+
// The adapter is synchronous (generates SQL filters),
|
|
86
|
+
// but D1 execution is always asynchronous.
|
|
78
87
|
export const withPermissions = createZanzoAdapter(engine, zanzoTuples, {
|
|
79
|
-
dialect: '
|
|
80
|
-
debug: false, // Set to true to log generated SQL and AST
|
|
81
|
-
warnOnNestedConditions: true // Warns if materialized tuples are missing
|
|
88
|
+
dialect: 'sqlite'
|
|
82
89
|
});
|
|
83
90
|
```
|
|
84
91
|
|
|
85
|
-
###
|
|
86
|
-
|
|
92
|
+
### 2. Usage in Next.js (middleware/layout)
|
|
93
|
+
When using `@cloudflare/next-on-pages`, you access the D1 binding via the Request Context.
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
```typescript
|
|
96
|
+
import { getRequestContext } from '@cloudflare/next-on-pages';
|
|
97
|
+
import { drizzle } from 'drizzle-orm/d1';
|
|
98
|
+
|
|
99
|
+
export const runtime = 'edge';
|
|
100
|
+
|
|
101
|
+
export default async function Page() {
|
|
102
|
+
const { env } = getRequestContext();
|
|
103
|
+
const db = drizzle(env.DB);
|
|
104
|
+
|
|
105
|
+
const myDocs = await db.select()
|
|
106
|
+
.from(documents)
|
|
107
|
+
.where(withPermissions('User:alice', 'read', 'Document', documents.id));
|
|
108
|
+
|
|
109
|
+
return <pre>{JSON.stringify(myDocs)}</pre>;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
90
112
|
|
|
91
|
-
### 3.
|
|
92
|
-
|
|
113
|
+
### 3. Limitations & Differences
|
|
114
|
+
- **Async-only**: D1 does not support synchronous queries. While `withPermissions` returns a synchronous `SQL` object, the final `.where()` call must be awaited as part of the Drizzle query.
|
|
115
|
+
- **AST Complexity**: The Edge Runtime has strict CPU and memory limits. Use `zanzo check` in your CI/CD to ensure your schema doesn't generate overly complex ASTs (standard limit: 100 conditional branches).
|
|
93
116
|
|
|
94
117
|
## Write Operations
|
|
95
118
|
|
package/dist/index.cjs
CHANGED
|
@@ -26,7 +26,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
26
26
|
var import_drizzle_orm = require("drizzle-orm");
|
|
27
27
|
var import_core = require("@zanzojs/core");
|
|
28
28
|
function createZanzoAdapter(engine, tupleTable, options) {
|
|
29
|
-
const isDev = typeof process !== "undefined" && process.env
|
|
29
|
+
const isDev = typeof process !== "undefined" && !!process.env && process.env.NODE_ENV === "development";
|
|
30
30
|
const shouldWarn = options?.warnOnNestedConditions ?? isDev;
|
|
31
31
|
const isDebug = options?.debug ?? false;
|
|
32
32
|
const dialect = options?.dialect ?? "postgres";
|
|
@@ -37,7 +37,6 @@ function createZanzoAdapter(engine, tupleTable, options) {
|
|
|
37
37
|
if (ast === void 0) {
|
|
38
38
|
ast = engine.buildDatabaseQuery(actor, action, resourceType);
|
|
39
39
|
astCache.set(cacheKey, ast);
|
|
40
|
-
} else if (ast) {
|
|
41
40
|
}
|
|
42
41
|
if (isDebug) {
|
|
43
42
|
console.debug(`[Zanzo Debug] Action: ${action}, Resource: ${resourceType}`);
|
|
@@ -56,11 +55,10 @@ function createZanzoAdapter(engine, tupleTable, options) {
|
|
|
56
55
|
if (cond.type === "nested" && shouldWarn) {
|
|
57
56
|
console.warn(`[Zanzo] Nested permission path detected: '${fullRelation}'. The SQL adapter resolves this via pre-materialized tuples. Ensure you used materializeDerivedTuples() when writing this relationship to the database. See: https://zanzo.dev/docs/tuple-expansion`);
|
|
58
57
|
}
|
|
59
|
-
|
|
60
|
-
let relations = relationsBySubject.get(targetSubject);
|
|
58
|
+
let relations = relationsBySubject.get(actor);
|
|
61
59
|
if (!relations) {
|
|
62
60
|
relations = /* @__PURE__ */ new Set();
|
|
63
|
-
relationsBySubject.set(
|
|
61
|
+
relationsBySubject.set(actor, relations);
|
|
64
62
|
}
|
|
65
63
|
relations.add(fullRelation);
|
|
66
64
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { or, and, SQL, sql, AnyColumn } from 'drizzle-orm';\nimport type { QueryAST, Condition, ZanzoEngine, SchemaData, ExtractSchemaResources, ExtractSchemaActions } from '@zanzojs/core';\nimport { ENTITY_REF_SEPARATOR, RELATION_PATH_SEPARATOR, ZanzoError, ZanzoErrorCode } from '@zanzojs/core';\n\n/**\n * Ensures the passed Drizzle Table conforms to the mandatory Zanzibar Universal Tuple Structure.\n */\nexport interface ZanzoTupleTable {\n object: AnyColumn; // String e.g. \"Invoice:123\"\n relation: AnyColumn; // String e.g. \"owner\"\n subject: AnyColumn; // String e.g. \"User:1\"\n [key: string]: any; // Allow extensions like IDs or context\n}\n\nexport interface ZanzoAdapterOptions {\n /**\n * Emits a console.warn when a nested permission path (e.g. 'org.admin') is detected,\n * reminding you to use materializeDerivedTuples() when writing this relationship.\n *\n * @default true in NODE_ENV=development, false in production\n */\n warnOnNestedConditions?: boolean;\n\n /**\n * If true, logs the generated AST and SQL conditions to the console for debugging purposes.\n * @default false\n */\n debug?: boolean;\n\n /**\n * The database dialect. Used to optimize string concatenation.\n * If not provided, it defaults to 'postgres' which uses standard CONCAT.\n * @default 'postgres'\n */\n dialect?: 'mysql' | 'postgres' | 'sqlite';\n}\n\n/**\n * Creates a \"Zero-Config\" Drizzle ORM permission adapter tailored for the Zanzibar Pattern.\n * Rather than mapping individual specific columns, this queries a Universal Tuple Table resolving access instantly.\n *\n * @remarks\n * **Validation Contract**: This adapter assumes all identifiers (actor, resource IDs) have been \n * validated by the ZanzoEngine before calling `withPermissions`. Passing raw user input \n * directly without routing through the engine first may bypass validation and produce \n * unexpected query behavior.\n *\n * @param engine The initialized ZanzoEngine instance\n * @param tupleTable The central Drizzle Table where all Relation Tuples are stored\n * @param options Optional configuration for the adapter\n * @returns A bounded `withPermissions` closure\n */\nexport function createZanzoAdapter<TSchema extends SchemaData, TTable extends ZanzoTupleTable>(\n engine: ZanzoEngine<TSchema>,\n tupleTable: TTable,\n options?: ZanzoAdapterOptions\n) {\n // Smart default: auto-enable warnings in development unless explicitly configured\n const isDev = typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';\n const shouldWarn = options?.warnOnNestedConditions ?? isDev;\n const isDebug = options?.debug ?? false;\n const dialect = options?.dialect ?? 'postgres';\n\n // Performance Optimization: Cache structural ASTs per action+resourceType\n // The actor is NOT part of the key as it varies per request, but the logical \n // permission tree (AST) for a given action on a resource type is static.\n const astCache = new Map<string, QueryAST | null>();\n\n /**\n * Generates a Drizzle SQL AST (subquery strategy) resolving access against the Universal Tuple Table.\n *\n * @remarks\n * This function assumes all identifiers have been validated by the ZanzoEngine.\n */\n return function withPermissions<\n TResourceName extends Extract<ExtractSchemaResources<TSchema>, string>,\n TAction extends ExtractSchemaActions<TSchema, TResourceName>,\n >(\n actor: string,\n action: TAction,\n resourceType: TResourceName,\n resourceIdColumn: AnyColumn\n ): SQL<unknown> {\n \n // Check Cache first\n const cacheKey = `${action as string}:${resourceType as string}`;\n let ast = astCache.get(cacheKey);\n\n if (ast === undefined) {\n // Evaluate the underlying pure logical AST\n // We use a dummy actor for the initial build to get the structural conditions,\n // then we'll replace the targetSubject with the real actor if needed.\n // Actually, buildDatabaseQuery uses the actor in the conditions.\n ast = engine.buildDatabaseQuery(actor, action as any, resourceType as any);\n astCache.set(cacheKey, ast);\n } else if (ast) {\n // If we have a cached AST, we must update the targetSubject for the current actor\n // Since we are rebuilding the SQL conditions anyway, we can just use the actor \n // from the function arguments.\n }\n\n if (isDebug) {\n console.debug(`[Zanzo Debug] Action: ${action as string}, Resource: ${resourceType as string}`);\n console.debug(`[Zanzo Debug] Generated AST:`, JSON.stringify(ast, null, 2));\n }\n\n // Protection Against 'The List Problem' / Query Payload Exhaustion\n if (ast && ast.conditions.length > 100) {\n throw new ZanzoError(ZanzoErrorCode.AST_OVERFLOW, `[Zanzo] Security Exception: The resulting AST exceeds the maximum safe limit of 100 conditional branches.`);\n }\n\n if (!ast || ast.conditions.length === 0) {\n return sql`1 = 0`; \n }\n\n // Dialect-agnostic/Secure concatenation for the object identifier: \"ResourceType:ID\"\n // We avoid sql.raw to prevent injection.\n const objectString = dialect === 'sqlite'\n ? sql`${resourceType} || ${ENTITY_REF_SEPARATOR} || ${resourceIdColumn}`\n : sql`CONCAT(${resourceType}, ${ENTITY_REF_SEPARATOR}, ${resourceIdColumn})`;\n\n // OPTIMIZATION: In Zanzibar, most permissions share the same subject and object target.\n // We group all conditions that share the same targetSubject into a single EXISTS subquery using IN.\n const relationsBySubject = new Map<string, Set<string>>();\n\n for (const cond of ast.conditions) {\n // Logic for building the full relation name (e.g. \"workspace.viewer\")\n const fullRelation = cond.type === 'nested' \n ? [cond.relation, ...cond.nextRelationPath].join(RELATION_PATH_SEPARATOR) \n : cond.relation;\n\n if (cond.type === 'nested' && shouldWarn) {\n console.warn(`[Zanzo] Nested permission path detected: '${fullRelation}'. The SQL adapter resolves this via pre-materialized tuples. Ensure you used materializeDerivedTuples() when writing this relationship to the database. See: https://zanzo.dev/docs/tuple-expansion`);\n }\n\n // Use the actual actor from arguments to ensure correctness even with cached AST\n const targetSubject = cond.targetSubject === actor ? actor : cond.targetSubject;\n\n let relations = relationsBySubject.get(targetSubject);\n if (!relations) {\n relations = new Set();\n relationsBySubject.set(targetSubject, relations);\n }\n relations.add(fullRelation);\n }\n\n const sqlConditions: SQL<unknown>[] = [];\n\n for (const [subject, relations] of relationsBySubject.entries()) {\n const relationArray = Array.from(relations);\n \n const condition = relationArray.length === 1\n ? sql`EXISTS (\n SELECT 1 FROM ${tupleTable} \n WHERE ${tupleTable.object} = ${objectString} \n AND ${tupleTable.relation} = ${relationArray[0]} \n AND ${tupleTable.subject} = ${subject}\n )`\n : sql`EXISTS (\n SELECT 1 FROM ${tupleTable} \n WHERE ${tupleTable.object} = ${objectString} \n AND ${tupleTable.relation} IN (${sql.join(relationArray.map(r => sql`${r}`), sql`, `)}) \n AND ${tupleTable.subject} = ${subject}\n )`;\n \n sqlConditions.push(condition);\n }\n\n const finalFilter = (ast.operator === 'AND' ? and(...sqlConditions) : or(...sqlConditions)) as SQL<unknown>;\n\n if (isDebug) {\n console.debug(`[Zanzo Debug] Final SQL Filter Generated.`);\n }\n\n return finalFilter;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAA6C;AAE7C,kBAA0F;AAkDnF,SAAS,mBACd,QACA,YACA,SACA;AAEA,QAAM,QAAQ,OAAO,YAAY,eAAe,QAAQ,KAAK,aAAa;AAC1E,QAAM,aAAa,SAAS,0BAA0B;AACtD,QAAM,UAAU,SAAS,SAAS;AAClC,QAAM,UAAU,SAAS,WAAW;AAKpC,QAAM,WAAW,oBAAI,IAA6B;AAQlD,SAAO,SAAS,gBAId,OACA,QACA,cACA,kBACc;AAGd,UAAM,WAAW,GAAG,MAAgB,IAAI,YAAsB;AAC9D,QAAI,MAAM,SAAS,IAAI,QAAQ;AAE/B,QAAI,QAAQ,QAAW;AAKrB,YAAM,OAAO,mBAAmB,OAAO,QAAe,YAAmB;AACzE,eAAS,IAAI,UAAU,GAAG;AAAA,IAC5B,WAAW,KAAK;AAAA,IAIhB;AAEA,QAAI,SAAS;AACX,cAAQ,MAAM,yBAAyB,MAAgB,eAAe,YAAsB,EAAE;AAC9F,cAAQ,MAAM,gCAAgC,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC;AAAA,IAC5E;AAGA,QAAI,OAAO,IAAI,WAAW,SAAS,KAAK;AACtC,YAAM,IAAI,uBAAW,2BAAe,cAAc,2GAA2G;AAAA,IAC/J;AAEA,QAAI,CAAC,OAAO,IAAI,WAAW,WAAW,GAAG;AACvC,aAAO;AAAA,IACT;AAIA,UAAM,eAAe,YAAY,WAC7B,yBAAM,YAAY,OAAO,gCAAoB,OAAO,gBAAgB,KACpE,gCAAa,YAAY,KAAK,gCAAoB,KAAK,gBAAgB;AAI3E,UAAM,qBAAqB,oBAAI,IAAyB;AAExD,eAAW,QAAQ,IAAI,YAAY;AAEjC,YAAM,eAAe,KAAK,SAAS,WAC/B,CAAC,KAAK,UAAU,GAAG,KAAK,gBAAgB,EAAE,KAAK,mCAAuB,IACtE,KAAK;AAET,UAAI,KAAK,SAAS,YAAY,YAAY;AACxC,gBAAQ,KAAK,6CAA6C,YAAY,sMAAsM;AAAA,MAC9Q;AAGA,YAAM,gBAAgB,KAAK,kBAAkB,QAAQ,QAAQ,KAAK;AAElE,UAAI,YAAY,mBAAmB,IAAI,aAAa;AACpD,UAAI,CAAC,WAAW;AACd,oBAAY,oBAAI,IAAI;AACpB,2BAAmB,IAAI,eAAe,SAAS;AAAA,MACjD;AACA,gBAAU,IAAI,YAAY;AAAA,IAC5B;AAEA,UAAM,gBAAgC,CAAC;AAEvC,eAAW,CAAC,SAAS,SAAS,KAAK,mBAAmB,QAAQ,GAAG;AAC/D,YAAM,gBAAgB,MAAM,KAAK,SAAS;AAE1C,YAAM,YAAY,cAAc,WAAW,IACvC;AAAA,4BACkB,UAAU;AAAA,oBAClB,WAAW,MAAM,MAAM,YAAY;AAAA,oBACnC,WAAW,QAAQ,MAAM,cAAc,CAAC,CAAC;AAAA,oBACzC,WAAW,OAAO,MAAM,OAAO;AAAA,eAEzC;AAAA,4BACkB,UAAU;AAAA,oBAClB,WAAW,MAAM,MAAM,YAAY;AAAA,oBACnC,WAAW,QAAQ,QAAQ,uBAAI,KAAK,cAAc,IAAI,OAAK,yBAAM,CAAC,EAAE,GAAG,0BAAO,CAAC;AAAA,oBAC/E,WAAW,OAAO,MAAM,OAAO;AAAA;AAG7C,oBAAc,KAAK,SAAS;AAAA,IAC9B;AAEA,UAAM,cAAe,IAAI,aAAa,YAAQ,wBAAI,GAAG,aAAa,QAAI,uBAAG,GAAG,aAAa;AAEzF,QAAI,SAAS;AACX,cAAQ,MAAM,2CAA2C;AAAA,IAC3D;AAEA,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { or, and, SQL, sql, AnyColumn } from 'drizzle-orm';\nimport type { QueryAST, Condition, ZanzoEngine, SchemaData, ExtractSchemaResources, ExtractSchemaActions } from '@zanzojs/core';\nimport { ENTITY_REF_SEPARATOR, RELATION_PATH_SEPARATOR, ZanzoError, ZanzoErrorCode } from '@zanzojs/core';\n\n/**\n * Ensures the passed Drizzle Table conforms to the mandatory Zanzibar Universal Tuple Structure.\n */\nexport interface ZanzoTupleTable {\n object: AnyColumn; // String e.g. \"Invoice:123\"\n relation: AnyColumn; // String e.g. \"owner\"\n subject: AnyColumn; // String e.g. \"User:1\"\n [key: string]: any; // Allow extensions like IDs or context\n}\n\nexport interface ZanzoAdapterOptions {\n /**\n * Emits a console.warn when a nested permission path (e.g. 'org.admin') is detected,\n * reminding you to use materializeDerivedTuples() when writing this relationship.\n *\n * @default true in NODE_ENV=development, false in production\n */\n warnOnNestedConditions?: boolean;\n\n /**\n * If true, logs the generated AST and SQL conditions to the console for debugging purposes.\n * @default false\n */\n debug?: boolean;\n\n /**\n * The database dialect. Used to optimize string concatenation.\n * If not provided, it defaults to 'postgres' which uses standard CONCAT.\n * @default 'postgres'\n */\n dialect?: 'mysql' | 'postgres' | 'sqlite';\n}\n\n/**\n * Creates a \"Zero-Config\" Drizzle ORM permission adapter tailored for the Zanzibar Pattern.\n * Rather than mapping individual specific columns, this queries a Universal Tuple Table resolving access instantly.\n *\n * @remarks\n * **Validation Contract**: This adapter assumes all identifiers (actor, resource IDs) have been \n * validated by the ZanzoEngine before calling `withPermissions`. Passing raw user input \n * directly without routing through the engine first may bypass validation and produce \n * unexpected query behavior.\n *\n * @param engine The initialized ZanzoEngine instance\n * @param tupleTable The central Drizzle Table where all Relation Tuples are stored\n * @param options Optional configuration for the adapter\n * @returns A bounded `withPermissions` closure\n */\nexport function createZanzoAdapter<TSchema extends SchemaData, TTable extends ZanzoTupleTable>(\n engine: ZanzoEngine<TSchema>,\n tupleTable: TTable,\n options?: ZanzoAdapterOptions\n) {\n // Smart default: auto-enable warnings in development unless explicitly configured\n const isDev = typeof process !== 'undefined' && !!process.env && process.env.NODE_ENV === 'development';\n const shouldWarn = options?.warnOnNestedConditions ?? isDev;\n const isDebug = options?.debug ?? false;\n const dialect = options?.dialect ?? 'postgres';\n\n // Performance Optimization: Cache structural ASTs per action+resourceType.\n // The cache stores the structural shape of the AST (relations, paths, operator)\n // which is static for a given action+resourceType combination.\n //\n // SECURITY (v0.3.0 fix): The `targetSubject` stored inside cached AST conditions\n // is IGNORED during SQL generation. The actual actor is always injected from the\n // current invocation's `actor` argument. This prevents cross-user data leakage\n // when the adapter instance is shared across requests in persistent servers\n // (Express, Fastify, etc.). See: architectural_review.md §CRÍTICO drizzle.\n const astCache = new Map<string, QueryAST | null>();\n\n /**\n * Generates a Drizzle SQL AST (subquery strategy) resolving access against the Universal Tuple Table.\n *\n * @remarks\n * This function assumes all identifiers have been validated by the ZanzoEngine.\n */\n return function withPermissions<\n TResourceName extends Extract<ExtractSchemaResources<TSchema>, string>,\n TAction extends ExtractSchemaActions<TSchema, TResourceName>,\n >(\n actor: string,\n action: TAction,\n resourceType: TResourceName,\n resourceIdColumn: AnyColumn\n ): SQL<unknown> {\n \n // Cache key is structural: action + resourceType (actor is NOT included).\n // The AST structure (which relations grant which actions) is immutable per schema.\n const cacheKey = `${action as string}:${resourceType as string}`;\n let ast = astCache.get(cacheKey);\n\n if (ast === undefined) {\n // Build the structural AST. The actor passed here is irrelevant for the cached\n // structure — only the relations and their types matter. The targetSubject baked\n // into the AST conditions will be ignored during SQL generation below.\n ast = engine.buildDatabaseQuery(actor, action as any, resourceType as any);\n astCache.set(cacheKey, ast);\n }\n\n if (isDebug) {\n console.debug(`[Zanzo Debug] Action: ${action as string}, Resource: ${resourceType as string}`);\n console.debug(`[Zanzo Debug] Generated AST:`, JSON.stringify(ast, null, 2));\n }\n\n // Protection Against 'The List Problem' / Query Payload Exhaustion\n if (ast && ast.conditions.length > 100) {\n throw new ZanzoError(ZanzoErrorCode.AST_OVERFLOW, `[Zanzo] Security Exception: The resulting AST exceeds the maximum safe limit of 100 conditional branches.`);\n }\n\n if (!ast || ast.conditions.length === 0) {\n return sql`1 = 0`; \n }\n\n // Dialect-agnostic/Secure concatenation for the object identifier: \"ResourceType:ID\"\n // We avoid sql.raw to prevent injection.\n const objectString = dialect === 'sqlite'\n ? sql`${resourceType} || ${ENTITY_REF_SEPARATOR} || ${resourceIdColumn}`\n : sql`CONCAT(${resourceType}, ${ENTITY_REF_SEPARATOR}, ${resourceIdColumn})`;\n\n // OPTIMIZATION: In Zanzibar, most permissions share the same subject and object target.\n // We group all conditions that share the same targetSubject into a single EXISTS subquery using IN.\n const relationsBySubject = new Map<string, Set<string>>();\n\n for (const cond of ast.conditions) {\n // Logic for building the full relation name (e.g. \"workspace.viewer\")\n const fullRelation = cond.type === 'nested' \n ? [cond.relation, ...cond.nextRelationPath].join(RELATION_PATH_SEPARATOR) \n : cond.relation;\n\n if (cond.type === 'nested' && shouldWarn) {\n console.warn(`[Zanzo] Nested permission path detected: '${fullRelation}'. The SQL adapter resolves this via pre-materialized tuples. Ensure you used materializeDerivedTuples() when writing this relationship to the database. See: https://zanzo.dev/docs/tuple-expansion`);\n }\n\n // SECURITY FIX: Always use the `actor` from the current invocation, NEVER\n // `cond.targetSubject` from the cached AST. The cached AST may contain a\n // stale actor from a previous request. This is the core of the cross-user\n // data leakage fix — the actor is bound at SQL generation time, not at\n // AST construction time.\n let relations = relationsBySubject.get(actor);\n if (!relations) {\n relations = new Set();\n relationsBySubject.set(actor, relations);\n }\n relations.add(fullRelation);\n }\n\n const sqlConditions: SQL<unknown>[] = [];\n\n for (const [subject, relations] of relationsBySubject.entries()) {\n const relationArray = Array.from(relations);\n \n const condition = relationArray.length === 1\n ? sql`EXISTS (\n SELECT 1 FROM ${tupleTable} \n WHERE ${tupleTable.object} = ${objectString} \n AND ${tupleTable.relation} = ${relationArray[0]} \n AND ${tupleTable.subject} = ${subject}\n )`\n : sql`EXISTS (\n SELECT 1 FROM ${tupleTable} \n WHERE ${tupleTable.object} = ${objectString} \n AND ${tupleTable.relation} IN (${sql.join(relationArray.map(r => sql`${r}`), sql`, `)}) \n AND ${tupleTable.subject} = ${subject}\n )`;\n \n sqlConditions.push(condition);\n }\n\n const finalFilter = (ast.operator === 'AND' ? and(...sqlConditions) : or(...sqlConditions)) as SQL<unknown>;\n\n if (isDebug) {\n console.debug(`[Zanzo Debug] Final SQL Filter Generated.`);\n }\n\n return finalFilter;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAA6C;AAE7C,kBAA0F;AAkDnF,SAAS,mBACd,QACA,YACA,SACA;AAEA,QAAM,QAAQ,OAAO,YAAY,eAAe,CAAC,CAAC,QAAQ,OAAO,QAAQ,IAAI,aAAa;AAC1F,QAAM,aAAa,SAAS,0BAA0B;AACtD,QAAM,UAAU,SAAS,SAAS;AAClC,QAAM,UAAU,SAAS,WAAW;AAWpC,QAAM,WAAW,oBAAI,IAA6B;AAQlD,SAAO,SAAS,gBAId,OACA,QACA,cACA,kBACc;AAId,UAAM,WAAW,GAAG,MAAgB,IAAI,YAAsB;AAC9D,QAAI,MAAM,SAAS,IAAI,QAAQ;AAE/B,QAAI,QAAQ,QAAW;AAIrB,YAAM,OAAO,mBAAmB,OAAO,QAAe,YAAmB;AACzE,eAAS,IAAI,UAAU,GAAG;AAAA,IAC5B;AAEA,QAAI,SAAS;AACX,cAAQ,MAAM,yBAAyB,MAAgB,eAAe,YAAsB,EAAE;AAC9F,cAAQ,MAAM,gCAAgC,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC;AAAA,IAC5E;AAGA,QAAI,OAAO,IAAI,WAAW,SAAS,KAAK;AACtC,YAAM,IAAI,uBAAW,2BAAe,cAAc,2GAA2G;AAAA,IAC/J;AAEA,QAAI,CAAC,OAAO,IAAI,WAAW,WAAW,GAAG;AACvC,aAAO;AAAA,IACT;AAIA,UAAM,eAAe,YAAY,WAC7B,yBAAM,YAAY,OAAO,gCAAoB,OAAO,gBAAgB,KACpE,gCAAa,YAAY,KAAK,gCAAoB,KAAK,gBAAgB;AAI3E,UAAM,qBAAqB,oBAAI,IAAyB;AAExD,eAAW,QAAQ,IAAI,YAAY;AAEjC,YAAM,eAAe,KAAK,SAAS,WAC/B,CAAC,KAAK,UAAU,GAAG,KAAK,gBAAgB,EAAE,KAAK,mCAAuB,IACtE,KAAK;AAET,UAAI,KAAK,SAAS,YAAY,YAAY;AACxC,gBAAQ,KAAK,6CAA6C,YAAY,sMAAsM;AAAA,MAC9Q;AAOA,UAAI,YAAY,mBAAmB,IAAI,KAAK;AAC5C,UAAI,CAAC,WAAW;AACd,oBAAY,oBAAI,IAAI;AACpB,2BAAmB,IAAI,OAAO,SAAS;AAAA,MACzC;AACA,gBAAU,IAAI,YAAY;AAAA,IAC5B;AAEA,UAAM,gBAAgC,CAAC;AAEvC,eAAW,CAAC,SAAS,SAAS,KAAK,mBAAmB,QAAQ,GAAG;AAC/D,YAAM,gBAAgB,MAAM,KAAK,SAAS;AAE1C,YAAM,YAAY,cAAc,WAAW,IACvC;AAAA,4BACkB,UAAU;AAAA,oBAClB,WAAW,MAAM,MAAM,YAAY;AAAA,oBACnC,WAAW,QAAQ,MAAM,cAAc,CAAC,CAAC;AAAA,oBACzC,WAAW,OAAO,MAAM,OAAO;AAAA,eAEzC;AAAA,4BACkB,UAAU;AAAA,oBAClB,WAAW,MAAM,MAAM,YAAY;AAAA,oBACnC,WAAW,QAAQ,QAAQ,uBAAI,KAAK,cAAc,IAAI,OAAK,yBAAM,CAAC,EAAE,GAAG,0BAAO,CAAC;AAAA,oBAC/E,WAAW,OAAO,MAAM,OAAO;AAAA;AAG7C,oBAAc,KAAK,SAAS;AAAA,IAC9B;AAEA,UAAM,cAAe,IAAI,aAAa,YAAQ,wBAAI,GAAG,aAAa,QAAI,uBAAG,GAAG,aAAa;AAEzF,QAAI,SAAS;AACX,cAAQ,MAAM,2CAA2C;AAAA,IAC3D;AAEA,WAAO;AAAA,EACT;AACF;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { or, and, sql } from "drizzle-orm";
|
|
3
3
|
import { ENTITY_REF_SEPARATOR, RELATION_PATH_SEPARATOR, ZanzoError, ZanzoErrorCode } from "@zanzojs/core";
|
|
4
4
|
function createZanzoAdapter(engine, tupleTable, options) {
|
|
5
|
-
const isDev = typeof process !== "undefined" && process.env
|
|
5
|
+
const isDev = typeof process !== "undefined" && !!process.env && process.env.NODE_ENV === "development";
|
|
6
6
|
const shouldWarn = options?.warnOnNestedConditions ?? isDev;
|
|
7
7
|
const isDebug = options?.debug ?? false;
|
|
8
8
|
const dialect = options?.dialect ?? "postgres";
|
|
@@ -13,7 +13,6 @@ function createZanzoAdapter(engine, tupleTable, options) {
|
|
|
13
13
|
if (ast === void 0) {
|
|
14
14
|
ast = engine.buildDatabaseQuery(actor, action, resourceType);
|
|
15
15
|
astCache.set(cacheKey, ast);
|
|
16
|
-
} else if (ast) {
|
|
17
16
|
}
|
|
18
17
|
if (isDebug) {
|
|
19
18
|
console.debug(`[Zanzo Debug] Action: ${action}, Resource: ${resourceType}`);
|
|
@@ -32,11 +31,10 @@ function createZanzoAdapter(engine, tupleTable, options) {
|
|
|
32
31
|
if (cond.type === "nested" && shouldWarn) {
|
|
33
32
|
console.warn(`[Zanzo] Nested permission path detected: '${fullRelation}'. The SQL adapter resolves this via pre-materialized tuples. Ensure you used materializeDerivedTuples() when writing this relationship to the database. See: https://zanzo.dev/docs/tuple-expansion`);
|
|
34
33
|
}
|
|
35
|
-
|
|
36
|
-
let relations = relationsBySubject.get(targetSubject);
|
|
34
|
+
let relations = relationsBySubject.get(actor);
|
|
37
35
|
if (!relations) {
|
|
38
36
|
relations = /* @__PURE__ */ new Set();
|
|
39
|
-
relationsBySubject.set(
|
|
37
|
+
relationsBySubject.set(actor, relations);
|
|
40
38
|
}
|
|
41
39
|
relations.add(fullRelation);
|
|
42
40
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { or, and, SQL, sql, AnyColumn } from 'drizzle-orm';\nimport type { QueryAST, Condition, ZanzoEngine, SchemaData, ExtractSchemaResources, ExtractSchemaActions } from '@zanzojs/core';\nimport { ENTITY_REF_SEPARATOR, RELATION_PATH_SEPARATOR, ZanzoError, ZanzoErrorCode } from '@zanzojs/core';\n\n/**\n * Ensures the passed Drizzle Table conforms to the mandatory Zanzibar Universal Tuple Structure.\n */\nexport interface ZanzoTupleTable {\n object: AnyColumn; // String e.g. \"Invoice:123\"\n relation: AnyColumn; // String e.g. \"owner\"\n subject: AnyColumn; // String e.g. \"User:1\"\n [key: string]: any; // Allow extensions like IDs or context\n}\n\nexport interface ZanzoAdapterOptions {\n /**\n * Emits a console.warn when a nested permission path (e.g. 'org.admin') is detected,\n * reminding you to use materializeDerivedTuples() when writing this relationship.\n *\n * @default true in NODE_ENV=development, false in production\n */\n warnOnNestedConditions?: boolean;\n\n /**\n * If true, logs the generated AST and SQL conditions to the console for debugging purposes.\n * @default false\n */\n debug?: boolean;\n\n /**\n * The database dialect. Used to optimize string concatenation.\n * If not provided, it defaults to 'postgres' which uses standard CONCAT.\n * @default 'postgres'\n */\n dialect?: 'mysql' | 'postgres' | 'sqlite';\n}\n\n/**\n * Creates a \"Zero-Config\" Drizzle ORM permission adapter tailored for the Zanzibar Pattern.\n * Rather than mapping individual specific columns, this queries a Universal Tuple Table resolving access instantly.\n *\n * @remarks\n * **Validation Contract**: This adapter assumes all identifiers (actor, resource IDs) have been \n * validated by the ZanzoEngine before calling `withPermissions`. Passing raw user input \n * directly without routing through the engine first may bypass validation and produce \n * unexpected query behavior.\n *\n * @param engine The initialized ZanzoEngine instance\n * @param tupleTable The central Drizzle Table where all Relation Tuples are stored\n * @param options Optional configuration for the adapter\n * @returns A bounded `withPermissions` closure\n */\nexport function createZanzoAdapter<TSchema extends SchemaData, TTable extends ZanzoTupleTable>(\n engine: ZanzoEngine<TSchema>,\n tupleTable: TTable,\n options?: ZanzoAdapterOptions\n) {\n // Smart default: auto-enable warnings in development unless explicitly configured\n const isDev = typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';\n const shouldWarn = options?.warnOnNestedConditions ?? isDev;\n const isDebug = options?.debug ?? false;\n const dialect = options?.dialect ?? 'postgres';\n\n // Performance Optimization: Cache structural ASTs per action+resourceType\n // The actor is NOT part of the key as it varies per request, but the logical \n // permission tree (AST) for a given action on a resource type is static.\n const astCache = new Map<string, QueryAST | null>();\n\n /**\n * Generates a Drizzle SQL AST (subquery strategy) resolving access against the Universal Tuple Table.\n *\n * @remarks\n * This function assumes all identifiers have been validated by the ZanzoEngine.\n */\n return function withPermissions<\n TResourceName extends Extract<ExtractSchemaResources<TSchema>, string>,\n TAction extends ExtractSchemaActions<TSchema, TResourceName>,\n >(\n actor: string,\n action: TAction,\n resourceType: TResourceName,\n resourceIdColumn: AnyColumn\n ): SQL<unknown> {\n \n // Check Cache first\n const cacheKey = `${action as string}:${resourceType as string}`;\n let ast = astCache.get(cacheKey);\n\n if (ast === undefined) {\n // Evaluate the underlying pure logical AST\n // We use a dummy actor for the initial build to get the structural conditions,\n // then we'll replace the targetSubject with the real actor if needed.\n // Actually, buildDatabaseQuery uses the actor in the conditions.\n ast = engine.buildDatabaseQuery(actor, action as any, resourceType as any);\n astCache.set(cacheKey, ast);\n } else if (ast) {\n // If we have a cached AST, we must update the targetSubject for the current actor\n // Since we are rebuilding the SQL conditions anyway, we can just use the actor \n // from the function arguments.\n }\n\n if (isDebug) {\n console.debug(`[Zanzo Debug] Action: ${action as string}, Resource: ${resourceType as string}`);\n console.debug(`[Zanzo Debug] Generated AST:`, JSON.stringify(ast, null, 2));\n }\n\n // Protection Against 'The List Problem' / Query Payload Exhaustion\n if (ast && ast.conditions.length > 100) {\n throw new ZanzoError(ZanzoErrorCode.AST_OVERFLOW, `[Zanzo] Security Exception: The resulting AST exceeds the maximum safe limit of 100 conditional branches.`);\n }\n\n if (!ast || ast.conditions.length === 0) {\n return sql`1 = 0`; \n }\n\n // Dialect-agnostic/Secure concatenation for the object identifier: \"ResourceType:ID\"\n // We avoid sql.raw to prevent injection.\n const objectString = dialect === 'sqlite'\n ? sql`${resourceType} || ${ENTITY_REF_SEPARATOR} || ${resourceIdColumn}`\n : sql`CONCAT(${resourceType}, ${ENTITY_REF_SEPARATOR}, ${resourceIdColumn})`;\n\n // OPTIMIZATION: In Zanzibar, most permissions share the same subject and object target.\n // We group all conditions that share the same targetSubject into a single EXISTS subquery using IN.\n const relationsBySubject = new Map<string, Set<string>>();\n\n for (const cond of ast.conditions) {\n // Logic for building the full relation name (e.g. \"workspace.viewer\")\n const fullRelation = cond.type === 'nested' \n ? [cond.relation, ...cond.nextRelationPath].join(RELATION_PATH_SEPARATOR) \n : cond.relation;\n\n if (cond.type === 'nested' && shouldWarn) {\n console.warn(`[Zanzo] Nested permission path detected: '${fullRelation}'. The SQL adapter resolves this via pre-materialized tuples. Ensure you used materializeDerivedTuples() when writing this relationship to the database. See: https://zanzo.dev/docs/tuple-expansion`);\n }\n\n // Use the actual actor from arguments to ensure correctness even with cached AST\n const targetSubject = cond.targetSubject === actor ? actor : cond.targetSubject;\n\n let relations = relationsBySubject.get(targetSubject);\n if (!relations) {\n relations = new Set();\n relationsBySubject.set(targetSubject, relations);\n }\n relations.add(fullRelation);\n }\n\n const sqlConditions: SQL<unknown>[] = [];\n\n for (const [subject, relations] of relationsBySubject.entries()) {\n const relationArray = Array.from(relations);\n \n const condition = relationArray.length === 1\n ? sql`EXISTS (\n SELECT 1 FROM ${tupleTable} \n WHERE ${tupleTable.object} = ${objectString} \n AND ${tupleTable.relation} = ${relationArray[0]} \n AND ${tupleTable.subject} = ${subject}\n )`\n : sql`EXISTS (\n SELECT 1 FROM ${tupleTable} \n WHERE ${tupleTable.object} = ${objectString} \n AND ${tupleTable.relation} IN (${sql.join(relationArray.map(r => sql`${r}`), sql`, `)}) \n AND ${tupleTable.subject} = ${subject}\n )`;\n \n sqlConditions.push(condition);\n }\n\n const finalFilter = (ast.operator === 'AND' ? and(...sqlConditions) : or(...sqlConditions)) as SQL<unknown>;\n\n if (isDebug) {\n console.debug(`[Zanzo Debug] Final SQL Filter Generated.`);\n }\n\n return finalFilter;\n };\n}\n"],"mappings":";AAAA,SAAS,IAAI,KAAU,WAAsB;AAE7C,SAAS,sBAAsB,yBAAyB,YAAY,sBAAsB;AAkDnF,SAAS,mBACd,QACA,YACA,SACA;AAEA,QAAM,QAAQ,OAAO,YAAY,eAAe,QAAQ,KAAK,aAAa;AAC1E,QAAM,aAAa,SAAS,0BAA0B;AACtD,QAAM,UAAU,SAAS,SAAS;AAClC,QAAM,UAAU,SAAS,WAAW;AAKpC,QAAM,WAAW,oBAAI,IAA6B;AAQlD,SAAO,SAAS,gBAId,OACA,QACA,cACA,kBACc;AAGd,UAAM,WAAW,GAAG,MAAgB,IAAI,YAAsB;AAC9D,QAAI,MAAM,SAAS,IAAI,QAAQ;AAE/B,QAAI,QAAQ,QAAW;AAKrB,YAAM,OAAO,mBAAmB,OAAO,QAAe,YAAmB;AACzE,eAAS,IAAI,UAAU,GAAG;AAAA,IAC5B,WAAW,KAAK;AAAA,IAIhB;AAEA,QAAI,SAAS;AACX,cAAQ,MAAM,yBAAyB,MAAgB,eAAe,YAAsB,EAAE;AAC9F,cAAQ,MAAM,gCAAgC,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC;AAAA,IAC5E;AAGA,QAAI,OAAO,IAAI,WAAW,SAAS,KAAK;AACtC,YAAM,IAAI,WAAW,eAAe,cAAc,2GAA2G;AAAA,IAC/J;AAEA,QAAI,CAAC,OAAO,IAAI,WAAW,WAAW,GAAG;AACvC,aAAO;AAAA,IACT;AAIA,UAAM,eAAe,YAAY,WAC7B,MAAM,YAAY,OAAO,oBAAoB,OAAO,gBAAgB,KACpE,aAAa,YAAY,KAAK,oBAAoB,KAAK,gBAAgB;AAI3E,UAAM,qBAAqB,oBAAI,IAAyB;AAExD,eAAW,QAAQ,IAAI,YAAY;AAEjC,YAAM,eAAe,KAAK,SAAS,WAC/B,CAAC,KAAK,UAAU,GAAG,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,IACtE,KAAK;AAET,UAAI,KAAK,SAAS,YAAY,YAAY;AACxC,gBAAQ,KAAK,6CAA6C,YAAY,sMAAsM;AAAA,MAC9Q;AAGA,YAAM,gBAAgB,KAAK,kBAAkB,QAAQ,QAAQ,KAAK;AAElE,UAAI,YAAY,mBAAmB,IAAI,aAAa;AACpD,UAAI,CAAC,WAAW;AACd,oBAAY,oBAAI,IAAI;AACpB,2BAAmB,IAAI,eAAe,SAAS;AAAA,MACjD;AACA,gBAAU,IAAI,YAAY;AAAA,IAC5B;AAEA,UAAM,gBAAgC,CAAC;AAEvC,eAAW,CAAC,SAAS,SAAS,KAAK,mBAAmB,QAAQ,GAAG;AAC/D,YAAM,gBAAgB,MAAM,KAAK,SAAS;AAE1C,YAAM,YAAY,cAAc,WAAW,IACvC;AAAA,4BACkB,UAAU;AAAA,oBAClB,WAAW,MAAM,MAAM,YAAY;AAAA,oBACnC,WAAW,QAAQ,MAAM,cAAc,CAAC,CAAC;AAAA,oBACzC,WAAW,OAAO,MAAM,OAAO;AAAA,eAEzC;AAAA,4BACkB,UAAU;AAAA,oBAClB,WAAW,MAAM,MAAM,YAAY;AAAA,oBACnC,WAAW,QAAQ,QAAQ,IAAI,KAAK,cAAc,IAAI,OAAK,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC;AAAA,oBAC/E,WAAW,OAAO,MAAM,OAAO;AAAA;AAG7C,oBAAc,KAAK,SAAS;AAAA,IAC9B;AAEA,UAAM,cAAe,IAAI,aAAa,QAAQ,IAAI,GAAG,aAAa,IAAI,GAAG,GAAG,aAAa;AAEzF,QAAI,SAAS;AACX,cAAQ,MAAM,2CAA2C;AAAA,IAC3D;AAEA,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { or, and, SQL, sql, AnyColumn } from 'drizzle-orm';\nimport type { QueryAST, Condition, ZanzoEngine, SchemaData, ExtractSchemaResources, ExtractSchemaActions } from '@zanzojs/core';\nimport { ENTITY_REF_SEPARATOR, RELATION_PATH_SEPARATOR, ZanzoError, ZanzoErrorCode } from '@zanzojs/core';\n\n/**\n * Ensures the passed Drizzle Table conforms to the mandatory Zanzibar Universal Tuple Structure.\n */\nexport interface ZanzoTupleTable {\n object: AnyColumn; // String e.g. \"Invoice:123\"\n relation: AnyColumn; // String e.g. \"owner\"\n subject: AnyColumn; // String e.g. \"User:1\"\n [key: string]: any; // Allow extensions like IDs or context\n}\n\nexport interface ZanzoAdapterOptions {\n /**\n * Emits a console.warn when a nested permission path (e.g. 'org.admin') is detected,\n * reminding you to use materializeDerivedTuples() when writing this relationship.\n *\n * @default true in NODE_ENV=development, false in production\n */\n warnOnNestedConditions?: boolean;\n\n /**\n * If true, logs the generated AST and SQL conditions to the console for debugging purposes.\n * @default false\n */\n debug?: boolean;\n\n /**\n * The database dialect. Used to optimize string concatenation.\n * If not provided, it defaults to 'postgres' which uses standard CONCAT.\n * @default 'postgres'\n */\n dialect?: 'mysql' | 'postgres' | 'sqlite';\n}\n\n/**\n * Creates a \"Zero-Config\" Drizzle ORM permission adapter tailored for the Zanzibar Pattern.\n * Rather than mapping individual specific columns, this queries a Universal Tuple Table resolving access instantly.\n *\n * @remarks\n * **Validation Contract**: This adapter assumes all identifiers (actor, resource IDs) have been \n * validated by the ZanzoEngine before calling `withPermissions`. Passing raw user input \n * directly without routing through the engine first may bypass validation and produce \n * unexpected query behavior.\n *\n * @param engine The initialized ZanzoEngine instance\n * @param tupleTable The central Drizzle Table where all Relation Tuples are stored\n * @param options Optional configuration for the adapter\n * @returns A bounded `withPermissions` closure\n */\nexport function createZanzoAdapter<TSchema extends SchemaData, TTable extends ZanzoTupleTable>(\n engine: ZanzoEngine<TSchema>,\n tupleTable: TTable,\n options?: ZanzoAdapterOptions\n) {\n // Smart default: auto-enable warnings in development unless explicitly configured\n const isDev = typeof process !== 'undefined' && !!process.env && process.env.NODE_ENV === 'development';\n const shouldWarn = options?.warnOnNestedConditions ?? isDev;\n const isDebug = options?.debug ?? false;\n const dialect = options?.dialect ?? 'postgres';\n\n // Performance Optimization: Cache structural ASTs per action+resourceType.\n // The cache stores the structural shape of the AST (relations, paths, operator)\n // which is static for a given action+resourceType combination.\n //\n // SECURITY (v0.3.0 fix): The `targetSubject` stored inside cached AST conditions\n // is IGNORED during SQL generation. The actual actor is always injected from the\n // current invocation's `actor` argument. This prevents cross-user data leakage\n // when the adapter instance is shared across requests in persistent servers\n // (Express, Fastify, etc.). See: architectural_review.md §CRÍTICO drizzle.\n const astCache = new Map<string, QueryAST | null>();\n\n /**\n * Generates a Drizzle SQL AST (subquery strategy) resolving access against the Universal Tuple Table.\n *\n * @remarks\n * This function assumes all identifiers have been validated by the ZanzoEngine.\n */\n return function withPermissions<\n TResourceName extends Extract<ExtractSchemaResources<TSchema>, string>,\n TAction extends ExtractSchemaActions<TSchema, TResourceName>,\n >(\n actor: string,\n action: TAction,\n resourceType: TResourceName,\n resourceIdColumn: AnyColumn\n ): SQL<unknown> {\n \n // Cache key is structural: action + resourceType (actor is NOT included).\n // The AST structure (which relations grant which actions) is immutable per schema.\n const cacheKey = `${action as string}:${resourceType as string}`;\n let ast = astCache.get(cacheKey);\n\n if (ast === undefined) {\n // Build the structural AST. The actor passed here is irrelevant for the cached\n // structure — only the relations and their types matter. The targetSubject baked\n // into the AST conditions will be ignored during SQL generation below.\n ast = engine.buildDatabaseQuery(actor, action as any, resourceType as any);\n astCache.set(cacheKey, ast);\n }\n\n if (isDebug) {\n console.debug(`[Zanzo Debug] Action: ${action as string}, Resource: ${resourceType as string}`);\n console.debug(`[Zanzo Debug] Generated AST:`, JSON.stringify(ast, null, 2));\n }\n\n // Protection Against 'The List Problem' / Query Payload Exhaustion\n if (ast && ast.conditions.length > 100) {\n throw new ZanzoError(ZanzoErrorCode.AST_OVERFLOW, `[Zanzo] Security Exception: The resulting AST exceeds the maximum safe limit of 100 conditional branches.`);\n }\n\n if (!ast || ast.conditions.length === 0) {\n return sql`1 = 0`; \n }\n\n // Dialect-agnostic/Secure concatenation for the object identifier: \"ResourceType:ID\"\n // We avoid sql.raw to prevent injection.\n const objectString = dialect === 'sqlite'\n ? sql`${resourceType} || ${ENTITY_REF_SEPARATOR} || ${resourceIdColumn}`\n : sql`CONCAT(${resourceType}, ${ENTITY_REF_SEPARATOR}, ${resourceIdColumn})`;\n\n // OPTIMIZATION: In Zanzibar, most permissions share the same subject and object target.\n // We group all conditions that share the same targetSubject into a single EXISTS subquery using IN.\n const relationsBySubject = new Map<string, Set<string>>();\n\n for (const cond of ast.conditions) {\n // Logic for building the full relation name (e.g. \"workspace.viewer\")\n const fullRelation = cond.type === 'nested' \n ? [cond.relation, ...cond.nextRelationPath].join(RELATION_PATH_SEPARATOR) \n : cond.relation;\n\n if (cond.type === 'nested' && shouldWarn) {\n console.warn(`[Zanzo] Nested permission path detected: '${fullRelation}'. The SQL adapter resolves this via pre-materialized tuples. Ensure you used materializeDerivedTuples() when writing this relationship to the database. See: https://zanzo.dev/docs/tuple-expansion`);\n }\n\n // SECURITY FIX: Always use the `actor` from the current invocation, NEVER\n // `cond.targetSubject` from the cached AST. The cached AST may contain a\n // stale actor from a previous request. This is the core of the cross-user\n // data leakage fix — the actor is bound at SQL generation time, not at\n // AST construction time.\n let relations = relationsBySubject.get(actor);\n if (!relations) {\n relations = new Set();\n relationsBySubject.set(actor, relations);\n }\n relations.add(fullRelation);\n }\n\n const sqlConditions: SQL<unknown>[] = [];\n\n for (const [subject, relations] of relationsBySubject.entries()) {\n const relationArray = Array.from(relations);\n \n const condition = relationArray.length === 1\n ? sql`EXISTS (\n SELECT 1 FROM ${tupleTable} \n WHERE ${tupleTable.object} = ${objectString} \n AND ${tupleTable.relation} = ${relationArray[0]} \n AND ${tupleTable.subject} = ${subject}\n )`\n : sql`EXISTS (\n SELECT 1 FROM ${tupleTable} \n WHERE ${tupleTable.object} = ${objectString} \n AND ${tupleTable.relation} IN (${sql.join(relationArray.map(r => sql`${r}`), sql`, `)}) \n AND ${tupleTable.subject} = ${subject}\n )`;\n \n sqlConditions.push(condition);\n }\n\n const finalFilter = (ast.operator === 'AND' ? and(...sqlConditions) : or(...sqlConditions)) as SQL<unknown>;\n\n if (isDebug) {\n console.debug(`[Zanzo Debug] Final SQL Filter Generated.`);\n }\n\n return finalFilter;\n };\n}\n"],"mappings":";AAAA,SAAS,IAAI,KAAU,WAAsB;AAE7C,SAAS,sBAAsB,yBAAyB,YAAY,sBAAsB;AAkDnF,SAAS,mBACd,QACA,YACA,SACA;AAEA,QAAM,QAAQ,OAAO,YAAY,eAAe,CAAC,CAAC,QAAQ,OAAO,QAAQ,IAAI,aAAa;AAC1F,QAAM,aAAa,SAAS,0BAA0B;AACtD,QAAM,UAAU,SAAS,SAAS;AAClC,QAAM,UAAU,SAAS,WAAW;AAWpC,QAAM,WAAW,oBAAI,IAA6B;AAQlD,SAAO,SAAS,gBAId,OACA,QACA,cACA,kBACc;AAId,UAAM,WAAW,GAAG,MAAgB,IAAI,YAAsB;AAC9D,QAAI,MAAM,SAAS,IAAI,QAAQ;AAE/B,QAAI,QAAQ,QAAW;AAIrB,YAAM,OAAO,mBAAmB,OAAO,QAAe,YAAmB;AACzE,eAAS,IAAI,UAAU,GAAG;AAAA,IAC5B;AAEA,QAAI,SAAS;AACX,cAAQ,MAAM,yBAAyB,MAAgB,eAAe,YAAsB,EAAE;AAC9F,cAAQ,MAAM,gCAAgC,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC;AAAA,IAC5E;AAGA,QAAI,OAAO,IAAI,WAAW,SAAS,KAAK;AACtC,YAAM,IAAI,WAAW,eAAe,cAAc,2GAA2G;AAAA,IAC/J;AAEA,QAAI,CAAC,OAAO,IAAI,WAAW,WAAW,GAAG;AACvC,aAAO;AAAA,IACT;AAIA,UAAM,eAAe,YAAY,WAC7B,MAAM,YAAY,OAAO,oBAAoB,OAAO,gBAAgB,KACpE,aAAa,YAAY,KAAK,oBAAoB,KAAK,gBAAgB;AAI3E,UAAM,qBAAqB,oBAAI,IAAyB;AAExD,eAAW,QAAQ,IAAI,YAAY;AAEjC,YAAM,eAAe,KAAK,SAAS,WAC/B,CAAC,KAAK,UAAU,GAAG,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,IACtE,KAAK;AAET,UAAI,KAAK,SAAS,YAAY,YAAY;AACxC,gBAAQ,KAAK,6CAA6C,YAAY,sMAAsM;AAAA,MAC9Q;AAOA,UAAI,YAAY,mBAAmB,IAAI,KAAK;AAC5C,UAAI,CAAC,WAAW;AACd,oBAAY,oBAAI,IAAI;AACpB,2BAAmB,IAAI,OAAO,SAAS;AAAA,MACzC;AACA,gBAAU,IAAI,YAAY;AAAA,IAC5B;AAEA,UAAM,gBAAgC,CAAC;AAEvC,eAAW,CAAC,SAAS,SAAS,KAAK,mBAAmB,QAAQ,GAAG;AAC/D,YAAM,gBAAgB,MAAM,KAAK,SAAS;AAE1C,YAAM,YAAY,cAAc,WAAW,IACvC;AAAA,4BACkB,UAAU;AAAA,oBAClB,WAAW,MAAM,MAAM,YAAY;AAAA,oBACnC,WAAW,QAAQ,MAAM,cAAc,CAAC,CAAC;AAAA,oBACzC,WAAW,OAAO,MAAM,OAAO;AAAA,eAEzC;AAAA,4BACkB,UAAU;AAAA,oBAClB,WAAW,MAAM,MAAM,YAAY;AAAA,oBACnC,WAAW,QAAQ,QAAQ,IAAI,KAAK,cAAc,IAAI,OAAK,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC;AAAA,oBAC/E,WAAW,OAAO,MAAM,OAAO;AAAA;AAG7C,oBAAc,KAAK,SAAS;AAAA,IAC9B;AAEA,UAAM,cAAe,IAAI,aAAa,QAAQ,IAAI,GAAG,aAAa,IAAI,GAAG,GAAG,aAAa;AAEzF,QAAI,SAAS;AACX,cAAQ,MAAM,2CAA2C;AAAA,IAC3D;AAEA,WAAO;AAAA,EACT;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zanzojs/drizzle",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Drizzle ORM adapter for Zanzo ReBAC. Zero-config Zanzibar Tuple Pattern with parameterized SQL.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"@zanzojs/core",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"typescript": "^5.7.2",
|
|
47
47
|
"tsup": "latest",
|
|
48
48
|
"vitest": "latest",
|
|
49
|
-
"@zanzojs/core": "0.3.
|
|
49
|
+
"@zanzojs/core": "0.3.1"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "tsup",
|