@jantstack/adonis-authz 2.4.0-alpha.1 → 2.4.0-alpha.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.
Files changed (105) hide show
  1. package/README.md +248 -18
  2. package/build/commands/authz_relations_reconcile.d.ts +25 -1
  3. package/build/commands/authz_relations_reconcile.d.ts.map +1 -1
  4. package/build/commands/authz_relations_reconcile.js +94 -6
  5. package/build/commands/authz_relations_reconcile.js.map +1 -1
  6. package/build/index.d.ts +12 -1
  7. package/build/index.d.ts.map +1 -1
  8. package/build/index.js +10 -0
  9. package/build/index.js.map +1 -1
  10. package/build/providers/authz_provider.d.ts.map +1 -1
  11. package/build/providers/authz_provider.js +7 -0
  12. package/build/providers/authz_provider.js.map +1 -1
  13. package/build/src/define_config.d.ts +36 -0
  14. package/build/src/define_config.d.ts.map +1 -1
  15. package/build/src/define_config.js.map +1 -1
  16. package/build/src/drivers/database_driver.d.ts +53 -14
  17. package/build/src/drivers/database_driver.d.ts.map +1 -1
  18. package/build/src/drivers/database_driver.js +104 -32
  19. package/build/src/drivers/database_driver.js.map +1 -1
  20. package/build/src/drivers/database_relations_driver.d.ts +47 -9
  21. package/build/src/drivers/database_relations_driver.d.ts.map +1 -1
  22. package/build/src/drivers/database_relations_driver.js +318 -89
  23. package/build/src/drivers/database_relations_driver.js.map +1 -1
  24. package/build/src/drivers/openfga_driver.d.ts +17 -4
  25. package/build/src/drivers/openfga_driver.d.ts.map +1 -1
  26. package/build/src/drivers/openfga_driver.js +38 -3
  27. package/build/src/drivers/openfga_driver.js.map +1 -1
  28. package/build/src/drivers/openfga_facts.d.ts +15 -0
  29. package/build/src/drivers/openfga_facts.d.ts.map +1 -1
  30. package/build/src/drivers/openfga_facts.js +25 -2
  31. package/build/src/drivers/openfga_facts.js.map +1 -1
  32. package/build/src/drivers/openfga_relations_driver.d.ts +27 -6
  33. package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -1
  34. package/build/src/drivers/openfga_relations_driver.js +140 -16
  35. package/build/src/drivers/openfga_relations_driver.js.map +1 -1
  36. package/build/src/errors.d.ts +35 -3
  37. package/build/src/errors.d.ts.map +1 -1
  38. package/build/src/errors.js +61 -3
  39. package/build/src/errors.js.map +1 -1
  40. package/build/src/freeze.d.ts +37 -16
  41. package/build/src/freeze.d.ts.map +1 -1
  42. package/build/src/freeze.js +53 -8
  43. package/build/src/freeze.js.map +1 -1
  44. package/build/src/manager.d.ts +16 -1
  45. package/build/src/manager.d.ts.map +1 -1
  46. package/build/src/manager.js +116 -25
  47. package/build/src/manager.js.map +1 -1
  48. package/build/src/models/authz_assignment.d.ts +2 -2
  49. package/build/src/models/authz_assignment.d.ts.map +1 -1
  50. package/build/src/models/authz_deny.d.ts +2 -2
  51. package/build/src/models/authz_deny.d.ts.map +1 -1
  52. package/build/src/models/authz_permission.d.ts +2 -2
  53. package/build/src/models/authz_permission.d.ts.map +1 -1
  54. package/build/src/models/authz_role.d.ts +2 -2
  55. package/build/src/models/authz_role.d.ts.map +1 -1
  56. package/build/src/models/authz_role_permission.d.ts +2 -2
  57. package/build/src/models/authz_role_permission.d.ts.map +1 -1
  58. package/build/src/relation_partition_trigger.d.ts.map +1 -1
  59. package/build/src/relation_partition_trigger.js +11 -3
  60. package/build/src/relation_partition_trigger.js.map +1 -1
  61. package/build/src/relations/define_relations_config.d.ts +17 -0
  62. package/build/src/relations/define_relations_config.d.ts.map +1 -1
  63. package/build/src/relations/define_relations_config.js +30 -1
  64. package/build/src/relations/define_relations_config.js.map +1 -1
  65. package/build/src/relations/manager.d.ts +25 -3
  66. package/build/src/relations/manager.d.ts.map +1 -1
  67. package/build/src/relations/manager.js +76 -19
  68. package/build/src/relations/manager.js.map +1 -1
  69. package/build/src/relations/reconcile.d.ts +53 -4
  70. package/build/src/relations/reconcile.d.ts.map +1 -1
  71. package/build/src/relations/reconcile.js +92 -30
  72. package/build/src/relations/reconcile.js.map +1 -1
  73. package/build/src/scope_outbox.d.ts.map +1 -1
  74. package/build/src/scope_outbox.js +17 -10
  75. package/build/src/scope_outbox.js.map +1 -1
  76. package/build/src/shared/backend_guard.d.ts +28 -0
  77. package/build/src/shared/backend_guard.d.ts.map +1 -1
  78. package/build/src/shared/backend_guard.js +52 -0
  79. package/build/src/shared/backend_guard.js.map +1 -1
  80. package/build/src/shared/transaction_guard.d.ts +50 -0
  81. package/build/src/shared/transaction_guard.d.ts.map +1 -0
  82. package/build/src/shared/transaction_guard.js +60 -0
  83. package/build/src/shared/transaction_guard.js.map +1 -0
  84. package/build/src/testing/contract.d.ts +55 -4
  85. package/build/src/testing/contract.d.ts.map +1 -1
  86. package/build/src/testing/contract.js +240 -8
  87. package/build/src/testing/contract.js.map +1 -1
  88. package/build/src/testing/main.d.ts +4 -4
  89. package/build/src/testing/main.d.ts.map +1 -1
  90. package/build/src/testing/main.js +2 -2
  91. package/build/src/testing/main.js.map +1 -1
  92. package/build/src/testing/relations_contract.d.ts +23 -1
  93. package/build/src/testing/relations_contract.d.ts.map +1 -1
  94. package/build/src/testing/relations_contract.js +432 -17
  95. package/build/src/testing/relations_contract.js.map +1 -1
  96. package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -1
  97. package/build/src/testing/relations_reconcile_contract.js +49 -1
  98. package/build/src/testing/relations_reconcile_contract.js.map +1 -1
  99. package/build/src/traits/has_uuid.d.ts +2 -2
  100. package/build/src/traits/has_uuid.d.ts.map +1 -1
  101. package/build/src/types.d.ts +197 -10
  102. package/build/src/types.d.ts.map +1 -1
  103. package/build/src/types.js.map +1 -1
  104. package/build/stubs/migration.stub +20 -10
  105. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import '@japa/assert';
2
2
  import type { Assert } from '@japa/assert';
3
- import type { AuthorizationDriver, CatalogSpec } from '../types.js';
3
+ import type { AuthorizationDriver, CatalogSpec, SubjectRef } from '../types.js';
4
4
  import type { ContractScopeTree } from './scope_tree.js';
5
5
  /**
6
6
  * Suite de contrato del sistema de autorización — EL JUEZ.
@@ -12,7 +12,7 @@ import type { ContractScopeTree } from './scope_tree.js';
12
12
  * runAuthorizationDriverContract({
13
13
  * name: 'mi-driver',
14
14
  * level: '2.0', // omitido = 'core': solo los casos de 1.x
15
- * capabilities: { hierarchyFacts: false, transactions: false, truncationSignal: false,
15
+ * capabilities: { hierarchyFacts: false, transactionalWrites: false, truncationSignal: false,
16
16
  * singleCheckAuthorize: false, injectableClock: false },
17
17
  * seedCatalog: (catalog) => ..., // materializa roles/permisos
18
18
  * makeDriver: (tree) => new MiDriver({ resolveChain: ... }), // recibe el árbol
@@ -41,8 +41,29 @@ import type { ContractScopeTree } from './scope_tree.js';
41
41
  export interface DriverCapabilities {
42
42
  /** El driver materializa el árbol como hechos propios (Fase 3b). */
43
43
  hierarchyFacts: boolean;
44
- /** Acepta `{ trx }` externa en las escrituras (Fase 2.5). */
45
- transactions: boolean;
44
+ /**
45
+ * **`{ transaction }` en `grant`/`revoke`/`deny`/`removeDeny` ESCRIBE en
46
+ * la transacción del consumidor** — «los dos o ninguno con TU transacción»,
47
+ * nunca «no se pierde» (L-2, panel `{trx}` (C); mismo nombre que
48
+ * `AuthorizationDriverCapabilities.transactionalWrites` y que su par de
49
+ * relaciones). Es el ÚNICO par del juez con las DOS caras obligatorias:
50
+ * `true` sin caso `whenTrue` Y `false` sin caso `whenFalse` hacen que la
51
+ * suite lance al registrarse (los demás pares solo exigen la cara `true`;
52
+ * la lección de 4-2 es que una `whenFalse` ausente se salta en silencio).
53
+ * Con `false` el juez fija que `{ transaction }` es 500 `E_AUTHZ_UNSUPPORTED`
54
+ * nombrando driver y operación con CERO llamadas al driver (espía), y que
55
+ * `requireTransactionalWrites: true` lo convierte en 500 `E_AUTHZ_CONFIG`
56
+ * al RESOLVER. Con `true` (L-3) el juez abre una transacción de la conexión
57
+ * del driver (`DriverContractHarness.transactions`, default Lucid +
58
+ * `authz_*`) y fija, para las cuatro escrituras, que **rollback ⇒ CERO
59
+ * filas por CENSO** (no solo `authorize`), commit ⇒ aplicado, la escritura
60
+ * sin confirmar invisible para la autoridad, y que una transacción ajena es
61
+ * 500 `E_AUTHZ_CONFIG` sin una sentencia. `database` declara `true` (con
62
+ * pool ≥ 2; en `:memory:` el harness del paquete declara `false` porque la
63
+ * autoridad no tendría por dónde leer); `openfga` no puede ser otra cosa que
64
+ * `false`: una tupla no entra en una transacción SQL.
65
+ */
66
+ transactionalWrites: boolean;
46
67
  /**
47
68
  * Los `list*` lanzan si el backend trunca. Ningún driver del paquete trunca
48
69
  * (L0.7 se cerró enumerando con `Read` paginado), así que no hay caso para
@@ -207,7 +228,31 @@ export interface DriverContractHarness {
207
228
  */
208
229
  makeTwin?(driver: AuthorizationDriver, tree: ContractScopeTree): AuthorizationDriver | Promise<AuthorizationDriver>;
209
230
  cleanup(): Promise<void>;
231
+ /**
232
+ * Solo con `transactionalWrites: true` (L-3): cómo abre el juez una
233
+ * transacción de LA CONEXIÓN DEL DRIVER y cómo cuenta los hechos de un
234
+ * holder en el backend — el CENSO: el rollback se juzga por filas, no solo
235
+ * por la respuesta de `authorize` (la lección del contrato de migración).
236
+ * Default: `db.transaction()` de Lucid (conexión primaria) y
237
+ * `authz_assignments`/`authz_denies`, lo que vale para el driver `database`.
238
+ * Un driver de terceros con otra conexión u otras tablas trae el suyo.
239
+ */
240
+ transactions?: ContractTransactions;
210
241
  }
242
+ /** Ver `DriverContractHarness.transactions`. */
243
+ export interface ContractTransactions {
244
+ begin(): Promise<{
245
+ client: unknown;
246
+ commit(): Promise<unknown>;
247
+ rollback(): Promise<unknown>;
248
+ }>;
249
+ census(subject: SubjectRef): Promise<{
250
+ assignments: number;
251
+ denies: number;
252
+ }>;
253
+ }
254
+ /** El default de `DriverContractHarness.transactions`: Lucid + `authz_*`. */
255
+ export declare function lucidContractTransactions(): ContractTransactions;
211
256
  /**
212
257
  * Lo mínimo que el juez necesita del runner. En producción es el `test` de
213
258
  * Japa; en `tests/contract_harness.spec.ts` es una API falsa que solo anota
@@ -229,4 +274,10 @@ export interface ContractTestApi {
229
274
  }
230
275
  export declare function runAuthorizationDriverContract(harness: DriverContractHarness): void;
231
276
  export declare function registerAuthorizationDriverContract(harness: DriverContractHarness, api: ContractTestApi): void;
277
+ /**
278
+ * La regla del guard, PURA (para que `contract_harness.spec.ts` la juzgue sin
279
+ * borrar caras): una capacidad `true` sin caso registrado queda sin cubrir;
280
+ * y una de `BOTH_FACES_REQUIRED` queda sin cubrir con CUALQUIER valor.
281
+ */
282
+ export declare function uncoveredCapabilities(capabilities: DriverCapabilities, covered: ReadonlySet<keyof DriverCapabilities>): (keyof DriverCapabilities)[];
232
283
  //# sourceMappingURL=contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/testing/contract.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/testing/contract.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,26 @@ import db from '@adonisjs/lucid/services/db';
7
7
  import { CatalogCache, GLOBAL_OWNER_KEY, withAuthzCatalogWrite } from '../catalog/catalog_cache.js';
8
8
  import { scopeKey } from '../identity.js';
9
9
  import { descendantsFrom, memoryScopeTree, resolveChainFrom } from './scope_tree.js';
10
+ /** El default de `DriverContractHarness.transactions`: Lucid + `authz_*`. */
11
+ export function lucidContractTransactions() {
12
+ const count = async (table, subject) => {
13
+ const rows = await db
14
+ .from(table)
15
+ .where('holder_type', subject.type)
16
+ .where('holder_uuid', subject.uuid)
17
+ .count('* as n');
18
+ return Number(rows[0]?.n ?? 0);
19
+ };
20
+ return {
21
+ async begin() {
22
+ const trx = await db.transaction();
23
+ return { client: trx, commit: () => trx.commit(), rollback: () => trx.rollback() };
24
+ },
25
+ async census(subject) {
26
+ return { assignments: await count('authz_assignments', subject), denies: await count('authz_denies', subject) };
27
+ },
28
+ };
29
+ }
10
30
  /**
11
31
  * Catálogo de prueba: roles a nivel app y a nivel organization. Los dos
12
32
  * últimos existen solo para el invariante 8: `rank` es metadata y el motor no
@@ -1689,8 +1709,9 @@ export function registerAuthorizationDriverContract(harness, api) {
1689
1709
  }
1690
1710
  });
1691
1711
  // ── Concurrencia (2.5 · J4) ──────────────────────────────────────────
1692
- // Sin `{trx}` en el puerto (`transactions: false` sigue; es el diferido
1693
- // 2.6): lo que se fija es que dos escrituras solapadas nunca dejan un
1712
+ // Sin `{trx}` en estos casos (la escritura en la transacción del
1713
+ // llamante tiene su par, `transactionalWrites`, abajo): lo que se fija
1714
+ // es que dos escrituras solapadas nunca dejan un
1694
1715
  // estado que ninguna de las dos habría dejado sola. Con el harness en
1695
1716
  // memoria (pool 1) se solapan a nivel de consulta; con `sqlite-file`, PG
1696
1717
  // y MySQL (pool ≥ 2) a nivel de conexión.
@@ -3255,9 +3276,201 @@ export function registerAuthorizationDriverContract(harness, api) {
3255
3276
  // `hierarchyFacts: false` → lo observan N1b/N2/N4/N5 (arriba): el driver
3256
3277
  // responde según el árbol que le resuelve el consumidor, y `tree.move`
3257
3278
  // cambia la respuesta sin escritura. `true` llega en Fase 3b.
3258
- // `injectableClock` tiene su par arriba (2.5 · J1). `transactions` (2.6)
3259
- // y `singleCheckAuthorize` (Fase 3b): pares en su fase; hoy solo pueden
3260
- // declararse `false`.
3279
+ // `injectableClock` tiene su par arriba (2.5 · J1). `transactionalWrites`
3280
+ // (L-2) tiene su par al final: la cara `false` hoy, la `true` con L-3.
3281
+ // ── L-2 · `transactionalWrites` (panel `{trx}`, (C)) ──────────────────
3282
+ // Las DOS caras son obligatorias en este par (`BOTH_FACES_REQUIRED`): una
3283
+ // `whenFalse` ausente se saltaba en silencio y «rechazo activo» sería
3284
+ // una etiqueta. Lo que la cara `false` fija es la PUERTA 1 (por llamada,
3285
+ // siempre activa) y la PUERTA 2 (por config, opt-in), las dos con el
3286
+ // manager construido sobre el driver del harness, en TODOS los niveles:
3287
+ // son composición del manager sobre `driver.capabilities`, así que un
3288
+ // driver de terceros de nivel `core` también las observa.
3289
+ caseFor('transactionalWrites', {
3290
+ // L-3: la cara `true`. Lo que se juzga es «los dos o ninguno con TU
3291
+ // transacción» POR CENSO: el rollback deja CERO filas nuevas (no solo
3292
+ // `authorize` false — una respuesta puede salir bien con la fila viva),
3293
+ // el commit las deja escritas, y mientras la transacción está abierta
3294
+ // la autoridad (que lee por la conexión del motor) no ve nada. Y la
3295
+ // otra mitad de la regla: una transacción que NO es la del driver no
3296
+ // recibe ni una sentencia. Cuatro escrituras, las dos caras cada una.
3297
+ whenTrue: () => {
3298
+ test('transactionalWrites: true · grant/revoke/deny/removeDeny con { transaction } + rollback ⇒ authorize sin cambio Y CERO filas (censo); + commit ⇒ aplicado; una transacción ajena, un cliente sin transacción o el `db` entero ⇒ 500 E_AUTHZ_CONFIG sin UNA sentencia por ella', async ({ assert, }) => {
3299
+ assert.equal(driver.capabilities?.transactionalWrites, true, 'el harness declara transactionalWrites: true y el driver no: declara lo observable');
3300
+ const tx = harness.transactions ?? lucidContractTransactions();
3301
+ const alice = subject();
3302
+ const org = await orgUnder(tree, APP_SCOPE);
3303
+ const authz = managerOver({});
3304
+ const can = () => driver.authorize(alice, 'docs:read', org);
3305
+ const census = () => tx.census(alice);
3306
+ // Una transacción que se queda abierta tras una aserción fallida
3307
+ // retiene una conexión del pool y arrastra a los casos siguientes:
3308
+ // el rollback va en `finally`, y el commit solo si nada falló.
3309
+ const rolledBack = async (fn) => {
3310
+ const t = await tx.begin();
3311
+ try {
3312
+ await fn(t.client);
3313
+ }
3314
+ finally {
3315
+ await t.rollback();
3316
+ }
3317
+ };
3318
+ const committed = async (fn) => {
3319
+ const t = await tx.begin();
3320
+ try {
3321
+ await fn(t.client);
3322
+ }
3323
+ catch (error) {
3324
+ await t.rollback();
3325
+ throw error;
3326
+ }
3327
+ await t.commit();
3328
+ };
3329
+ // grant: dentro no se ve desde fuera; rollback ⇒ nada; commit ⇒ concede.
3330
+ let seenInside;
3331
+ await rolledBack(async (transaction) => {
3332
+ await authz.grant(alice, 'org-editor', org, { transaction });
3333
+ seenInside = await can();
3334
+ });
3335
+ assert.deepEqual(await census(), { assignments: 0, denies: 0 }, 'ROJO: la asignación sobrevivió al rollback (la escritura NO fue por la transacción del llamante)');
3336
+ assert.isFalse(await can(), 'grant + rollback ⇒ no concede');
3337
+ assert.isFalse(seenInside, 'la autoridad lee por la conexión del motor: el grant sin confirmar no concede');
3338
+ await committed(async (transaction) => {
3339
+ await authz.grant(alice, 'org-editor', org, { transaction });
3340
+ });
3341
+ assert.isTrue(await can(), 'grant + commit ⇒ concede');
3342
+ assert.deepEqual(await census(), { assignments: 1, denies: 0 });
3343
+ // revoke: rollback ⇒ la asignación sigue; commit ⇒ se va.
3344
+ await rolledBack(async (transaction) => {
3345
+ await authz.revoke(alice, 'org-editor', org, { transaction });
3346
+ seenInside = await can();
3347
+ });
3348
+ assert.deepEqual(await census(), { assignments: 1, denies: 0 }, 'ROJO: el revoke sobrevivió al rollback');
3349
+ assert.isTrue(await can(), 'revoke + rollback ⇒ sigue concediendo');
3350
+ assert.isTrue(seenInside, 'el revoke sin confirmar no se ve desde la conexión del motor');
3351
+ await committed(async (transaction) => {
3352
+ await authz.revoke(alice, 'org-editor', org, { transaction });
3353
+ });
3354
+ assert.isFalse(await can(), 'revoke + commit ⇒ no concede');
3355
+ assert.deepEqual(await census(), { assignments: 0, denies: 0 });
3356
+ // deny (sobre un grant confirmado): rollback ⇒ sigue concediendo; commit ⇒ deniega.
3357
+ await authz.grant(alice, 'org-editor', org);
3358
+ await rolledBack(async (transaction) => {
3359
+ await authz.deny(alice, 'docs:read', org, { transaction });
3360
+ seenInside = await can();
3361
+ });
3362
+ assert.deepEqual(await census(), { assignments: 1, denies: 0 }, 'ROJO: el deny sobrevivió al rollback');
3363
+ assert.isTrue(await can(), 'deny + rollback ⇒ sigue concediendo');
3364
+ assert.isTrue(seenInside, 'el deny sin confirmar no se ve desde la conexión del motor');
3365
+ await committed(async (transaction) => {
3366
+ await authz.deny(alice, 'docs:read', org, { transaction });
3367
+ });
3368
+ assert.isFalse(await can(), 'deny + commit ⇒ deniega');
3369
+ assert.deepEqual(await census(), { assignments: 1, denies: 1 });
3370
+ // removeDeny: rollback ⇒ el deny sigue; commit ⇒ restaura.
3371
+ await rolledBack(async (transaction) => {
3372
+ await authz.removeDeny(alice, 'docs:read', org, { transaction });
3373
+ seenInside = await can();
3374
+ });
3375
+ assert.deepEqual(await census(), { assignments: 1, denies: 1 }, 'ROJO: el removeDeny sobrevivió al rollback');
3376
+ assert.isFalse(await can(), 'removeDeny + rollback ⇒ sigue denegando');
3377
+ assert.isFalse(seenInside, 'el removeDeny sin confirmar no se ve desde la conexión del motor');
3378
+ await committed(async (transaction) => {
3379
+ await authz.removeDeny(alice, 'docs:read', org, { transaction });
3380
+ });
3381
+ assert.isTrue(await can(), 'removeDeny + commit ⇒ restaura');
3382
+ assert.deepEqual(await census(), { assignments: 1, denies: 0 });
3383
+ // Una transacción que NO es la del driver (`assertCallerTransaction`):
3384
+ // 500 `E_AUTHZ_CONFIG` y ni una sentencia por ella (espía).
3385
+ const statements = [];
3386
+ const spy = (shape) => new Proxy(shape, {
3387
+ get(target, prop) {
3388
+ if (prop === 'from' || prop === 'table' || prop === 'raw' || prop === 'query' || prop === 'knexQuery') {
3389
+ return (...args) => {
3390
+ statements.push(`${String(prop)}(${String(args[0] ?? '')})`);
3391
+ return { where: () => ({}), select: async () => [], insert: async () => [], delete: async () => 0 };
3392
+ };
3393
+ }
3394
+ return Reflect.get(target, prop);
3395
+ },
3396
+ });
3397
+ const foreign = spy({ isTransaction: true, connectionName: `not-${harness.name}-${uuidv7()}` });
3398
+ const notATransaction = spy({ isTransaction: false, connectionName: 'primary' });
3399
+ const wholeDb = spy({ connection() { }, primaryConnectionName: 'primary' });
3400
+ const bob = subject();
3401
+ for (const [label, transaction] of [
3402
+ ['otra conexión', foreign],
3403
+ ['un cliente sin transacción', notATransaction],
3404
+ ['el db entero', wholeDb],
3405
+ ]) {
3406
+ for (const [operation, run] of [
3407
+ ['grant', () => authz.grant(bob, 'org-editor', org, { transaction })],
3408
+ ['revoke', () => authz.revoke(bob, 'org-editor', org, { transaction })],
3409
+ ['deny', () => authz.deny(bob, 'docs:read', org, { transaction })],
3410
+ ['removeDeny', () => authz.removeDeny(bob, 'docs:read', org, { transaction })],
3411
+ ]) {
3412
+ const error = await rejectsWith(assert, run, { status: 500, code: 'E_AUTHZ_CONFIG' });
3413
+ assert.include(error.message, operation, `${label}/${operation}: nombra la operación`);
3414
+ }
3415
+ }
3416
+ assert.deepEqual(statements, [], 'ni una sentencia por una transacción que no es la del driver');
3417
+ assert.deepEqual(await tx.census(bob), { assignments: 0, denies: 0 });
3418
+ assert.isFalse(await driver.authorize(bob, 'docs:read', org));
3419
+ })?.timeout(30_000);
3420
+ },
3421
+ whenFalse: () => {
3422
+ test('transactionalWrites: false · { transaction } en grant/revoke/deny/removeDeny es 500 E_AUTHZ_UNSUPPORTED nombrando driver y operación, con CERO llamadas al driver (espía); y requireTransactionalWrites: true ⇒ 500 E_AUTHZ_CONFIG al RESOLVER el driver', async ({ assert, }) => {
3423
+ // El harness declara `false`: el driver tiene que decir lo mismo
3424
+ // (el manager lee `driver.capabilities`, no el harness).
3425
+ assert.notEqual(driver.capabilities?.transactionalWrites, true, 'el harness declara transactionalWrites: false y el driver declara true: declara lo observable');
3426
+ const touched = [];
3427
+ const spied = new Proxy(driver, {
3428
+ get(target, prop, receiver) {
3429
+ const value = Reflect.get(target, prop, receiver);
3430
+ if (typeof value === 'function' && typeof prop === 'string') {
3431
+ return (...args) => {
3432
+ touched.push(prop);
3433
+ return value.apply(receiver, args);
3434
+ };
3435
+ }
3436
+ return value;
3437
+ },
3438
+ });
3439
+ // La forma de un `TransactionClientContract` de Lucid: la puerta
3440
+ // decide por la CAPACIDAD, antes de mirar qué transacción es.
3441
+ const trx = { from() { }, table() { }, isTransaction: true, connectionName: 'primary' };
3442
+ const alice = subject();
3443
+ const org = await orgUnder(tree, APP_SCOPE);
3444
+ const authz = managerOver({}, spied);
3445
+ const writes = [
3446
+ ['grant', () => authz.grant(alice, 'org-editor', org, { transaction: trx })],
3447
+ ['revoke', () => authz.revoke(alice, 'org-editor', org, { transaction: trx })],
3448
+ ['deny', () => authz.deny(alice, 'docs:read', org, { transaction: trx })],
3449
+ ['removeDeny', () => authz.removeDeny(alice, 'docs:read', org, { transaction: trx })],
3450
+ ];
3451
+ for (const [operation, run] of writes) {
3452
+ const error = await rejectsWith(assert, run, { status: 500, code: 'E_AUTHZ_UNSUPPORTED' });
3453
+ assert.include(error.message, `'${harness.name}'`, `${operation}: nombra el driver`);
3454
+ assert.include(error.message, operation, `${operation}: nombra la operación`);
3455
+ assert.include(error.message, 'requireTransactionalWrites', `${operation}: la letra lleva la salida`);
3456
+ }
3457
+ assert.deepEqual(touched, [], 'cero llamadas al driver: el rechazo va ANTES del backend');
3458
+ // Sin `{ transaction }` la MISMA escritura entra: la puerta es del
3459
+ // parámetro, no del driver.
3460
+ await authz.grant(alice, 'org-editor', org);
3461
+ assert.isTrue(await driver.authorize(alice, 'docs:read', org));
3462
+ assert.include(touched, 'grant');
3463
+ // Puerta 2, opt-in: quien quiera fallar al ARRANCAR lo pide, y
3464
+ // entonces el driver ni se resuelve (tampoco para leer).
3465
+ const strict = managerOver({ requireTransactionalWrites: true }, spied);
3466
+ const atResolve = await rejectsWith(assert, () => strict.driver(), { status: 500, code: 'E_AUTHZ_CONFIG' });
3467
+ assert.include(atResolve.message, `'${harness.name}'`);
3468
+ assert.include(atResolve.message, 'transactionalWrites');
3469
+ await rejectsWith(assert, () => strict.authorize(alice, 'docs:read', org), { status: 500, code: 'E_AUTHZ_CONFIG' });
3470
+ await rejectsWith(assert, () => strict.grant(alice, 'org-editor', org), { status: 500, code: 'E_AUTHZ_CONFIG' });
3471
+ });
3472
+ },
3473
+ });
3261
3474
  // ── 3b-2e · E6 · `singleCheckAuthorize` ──────────────────────────────
3262
3475
  caseFor('singleCheckAuthorize', {
3263
3476
  whenTrue: () => {
@@ -3626,11 +3839,30 @@ export function registerAuthorizationDriverContract(harness, api) {
3626
3839
  });
3627
3840
  });
3628
3841
  // Al cerrar el grupo: toda capacidad declarada `true` tiene que haber
3629
- // registrado su par. Es la regla "jamás un skip" hecha ejecutable.
3630
- const uncovered = Object.keys(harness.capabilities).filter((capability) => harness.capabilities[capability] && !covered.has(capability));
3842
+ // registrado su par y las de `BOTH_FACES_REQUIRED`, también con `false`.
3843
+ // Es la regla "jamás un skip" hecha ejecutable.
3844
+ const uncovered = uncoveredCapabilities(harness.capabilities, covered);
3631
3845
  if (uncovered.length) {
3632
- throw new Error(`[contrato ${harness.name}] declara ${uncovered.map((c) => `'${c}: true'`).join(', ')} ` +
3846
+ throw new Error(`[contrato ${harness.name}] declara ${uncovered.map((c) => `'${c}: ${String(harness.capabilities[c])}'`).join(', ')} ` +
3633
3847
  `pero el contrato no tiene caso para ese valor todavía. Declara lo observable hoy.`);
3634
3848
  }
3635
3849
  }
3850
+ /**
3851
+ * La regla del guard, PURA (para que `contract_harness.spec.ts` la juzgue sin
3852
+ * borrar caras): una capacidad `true` sin caso registrado queda sin cubrir;
3853
+ * y una de `BOTH_FACES_REQUIRED` queda sin cubrir con CUALQUIER valor.
3854
+ */
3855
+ export function uncoveredCapabilities(capabilities, covered) {
3856
+ return Object.keys(capabilities).filter((capability) => (capabilities[capability] || BOTH_FACES_REQUIRED.has(capability)) && !covered.has(capability));
3857
+ }
3858
+ /**
3859
+ * Capacidades cuyas DOS caras son obligatorias (L-2): declarar `false` sin
3860
+ * un caso `whenFalse` registrado lanza igual que declarar `true` sin
3861
+ * `whenTrue`. El guard general solo obliga a la cara `true` porque los pares
3862
+ * de nivel (`listDenies`, `purgeRole`…) no registran nada en un harness
3863
+ * `core` y `false` ahí es legítimo; `transactionalWrites` es composición del
3864
+ * manager y se observa en todos los niveles, así que su `false` sin caso es
3865
+ * un skip disfrazado — «rechazo activo» sería una etiqueta.
3866
+ */
3867
+ const BOTH_FACES_REQUIRED = new Set(['transactionalWrites']);
3636
3868
  //# sourceMappingURL=contract.js.map