@jantstack/adonis-authz 2.0.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 (214) hide show
  1. package/README.md +693 -36
  2. package/build/commands/authz_catalog_diff.js +1 -1
  3. package/build/commands/authz_catalog_diff.js.map +1 -1
  4. package/build/commands/authz_catalog_prune_orphans.d.ts +78 -0
  5. package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -0
  6. package/build/commands/authz_catalog_prune_orphans.js +136 -0
  7. package/build/commands/authz_catalog_prune_orphans.js.map +1 -0
  8. package/build/commands/authz_catalog_sync.d.ts +17 -0
  9. package/build/commands/authz_catalog_sync.d.ts.map +1 -1
  10. package/build/commands/authz_catalog_sync.js +27 -4
  11. package/build/commands/authz_catalog_sync.js.map +1 -1
  12. package/build/commands/authz_freeze.d.ts +44 -0
  13. package/build/commands/authz_freeze.d.ts.map +1 -0
  14. package/build/commands/authz_freeze.js +95 -0
  15. package/build/commands/authz_freeze.js.map +1 -0
  16. package/build/commands/authz_reconcile.d.ts +102 -0
  17. package/build/commands/authz_reconcile.d.ts.map +1 -0
  18. package/build/commands/authz_reconcile.js +294 -0
  19. package/build/commands/authz_reconcile.js.map +1 -0
  20. package/build/commands/authz_relations_reconcile.d.ts +97 -0
  21. package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
  22. package/build/commands/authz_relations_reconcile.js +313 -0
  23. package/build/commands/authz_relations_reconcile.js.map +1 -0
  24. package/build/commands/authz_scopes_relay.d.ts +47 -0
  25. package/build/commands/authz_scopes_relay.d.ts.map +1 -0
  26. package/build/commands/authz_scopes_relay.js +141 -0
  27. package/build/commands/authz_scopes_relay.js.map +1 -0
  28. package/build/commands/authz_unfreeze.d.ts +37 -0
  29. package/build/commands/authz_unfreeze.d.ts.map +1 -0
  30. package/build/commands/authz_unfreeze.js +92 -0
  31. package/build/commands/authz_unfreeze.js.map +1 -0
  32. package/build/commands/main.d.ts +6 -1
  33. package/build/commands/main.d.ts.map +1 -1
  34. package/build/commands/main.js +6 -1
  35. package/build/commands/main.js.map +1 -1
  36. package/build/commands/openfga_provision.d.ts +46 -4
  37. package/build/commands/openfga_provision.d.ts.map +1 -1
  38. package/build/commands/openfga_provision.js +90 -7
  39. package/build/commands/openfga_provision.js.map +1 -1
  40. package/build/configure.d.ts +11 -0
  41. package/build/configure.d.ts.map +1 -1
  42. package/build/configure.js +37 -1
  43. package/build/configure.js.map +1 -1
  44. package/build/index.d.ts +50 -10
  45. package/build/index.d.ts.map +1 -1
  46. package/build/index.js +45 -6
  47. package/build/index.js.map +1 -1
  48. package/build/providers/authz_provider.d.ts +26 -2
  49. package/build/providers/authz_provider.d.ts.map +1 -1
  50. package/build/providers/authz_provider.js +55 -2
  51. package/build/providers/authz_provider.js.map +1 -1
  52. package/build/services/relations.d.ts +14 -0
  53. package/build/services/relations.d.ts.map +1 -0
  54. package/build/services/relations.js +17 -0
  55. package/build/services/relations.js.map +1 -0
  56. package/build/src/{catalog.d.ts → catalog/catalog.d.ts} +41 -2
  57. package/build/src/catalog/catalog.d.ts.map +1 -0
  58. package/build/src/{catalog.js → catalog/catalog.js} +120 -14
  59. package/build/src/catalog/catalog.js.map +1 -0
  60. package/build/src/{catalog_cache.d.ts → catalog/catalog_cache.d.ts} +46 -22
  61. package/build/src/catalog/catalog_cache.d.ts.map +1 -0
  62. package/build/src/{catalog_cache.js → catalog/catalog_cache.js} +53 -43
  63. package/build/src/catalog/catalog_cache.js.map +1 -0
  64. package/build/src/define_config.d.ts +137 -3
  65. package/build/src/define_config.d.ts.map +1 -1
  66. package/build/src/define_config.js.map +1 -1
  67. package/build/src/drivers/database_driver.d.ts +128 -7
  68. package/build/src/drivers/database_driver.d.ts.map +1 -1
  69. package/build/src/drivers/database_driver.js +510 -24
  70. package/build/src/drivers/database_driver.js.map +1 -1
  71. package/build/src/drivers/database_relations_driver.d.ts +113 -0
  72. package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
  73. package/build/src/drivers/database_relations_driver.js +679 -0
  74. package/build/src/drivers/database_relations_driver.js.map +1 -0
  75. package/build/src/drivers/openfga_driver.d.ts +729 -122
  76. package/build/src/drivers/openfga_driver.d.ts.map +1 -1
  77. package/build/src/drivers/openfga_driver.js +2083 -476
  78. package/build/src/drivers/openfga_driver.js.map +1 -1
  79. package/build/src/drivers/openfga_facts.d.ts +384 -0
  80. package/build/src/drivers/openfga_facts.d.ts.map +1 -0
  81. package/build/src/drivers/openfga_facts.js +836 -0
  82. package/build/src/drivers/openfga_facts.js.map +1 -0
  83. package/build/src/drivers/openfga_relations_driver.d.ts +141 -0
  84. package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
  85. package/build/src/drivers/openfga_relations_driver.js +590 -0
  86. package/build/src/drivers/openfga_relations_driver.js.map +1 -0
  87. package/build/src/errors.d.ts +290 -5
  88. package/build/src/errors.d.ts.map +1 -1
  89. package/build/src/errors.js +296 -7
  90. package/build/src/errors.js.map +1 -1
  91. package/build/src/freeze.d.ts +141 -0
  92. package/build/src/freeze.d.ts.map +1 -0
  93. package/build/src/freeze.js +217 -0
  94. package/build/src/freeze.js.map +1 -0
  95. package/build/src/http/app_access_middleware.d.ts.map +1 -0
  96. package/build/src/http/app_access_middleware.js.map +1 -0
  97. package/build/src/http/resource_access_middleware.d.ts +105 -0
  98. package/build/src/http/resource_access_middleware.d.ts.map +1 -0
  99. package/build/src/http/resource_access_middleware.js +81 -0
  100. package/build/src/http/resource_access_middleware.js.map +1 -0
  101. package/build/src/identity.d.ts +74 -1
  102. package/build/src/identity.d.ts.map +1 -1
  103. package/build/src/identity.js +100 -2
  104. package/build/src/identity.js.map +1 -1
  105. package/build/src/manager.d.ts +330 -4
  106. package/build/src/manager.d.ts.map +1 -1
  107. package/build/src/manager.js +1285 -188
  108. package/build/src/manager.js.map +1 -1
  109. package/build/src/models/authz_assignment.d.ts +7 -7
  110. package/build/src/models/authz_assignment.d.ts.map +1 -1
  111. package/build/src/models/authz_deny.d.ts +7 -7
  112. package/build/src/models/authz_deny.d.ts.map +1 -1
  113. package/build/src/models/authz_permission.d.ts +7 -7
  114. package/build/src/models/authz_permission.d.ts.map +1 -1
  115. package/build/src/models/authz_role.d.ts +7 -7
  116. package/build/src/models/authz_role.d.ts.map +1 -1
  117. package/build/src/models/authz_role_permission.d.ts +7 -7
  118. package/build/src/models/authz_role_permission.d.ts.map +1 -1
  119. package/build/src/openfga.d.ts +18 -2
  120. package/build/src/openfga.d.ts.map +1 -1
  121. package/build/src/openfga.js +15 -1
  122. package/build/src/openfga.js.map +1 -1
  123. package/build/src/reconcile.d.ts +37 -0
  124. package/build/src/reconcile.d.ts.map +1 -0
  125. package/build/src/reconcile.js +69 -0
  126. package/build/src/reconcile.js.map +1 -0
  127. package/build/src/relation_partition_trigger.d.ts +8 -0
  128. package/build/src/relation_partition_trigger.d.ts.map +1 -0
  129. package/build/src/relation_partition_trigger.js +93 -0
  130. package/build/src/relation_partition_trigger.js.map +1 -0
  131. package/build/src/relations/define_relations_config.d.ts +75 -0
  132. package/build/src/relations/define_relations_config.d.ts.map +1 -0
  133. package/build/src/relations/define_relations_config.js +173 -0
  134. package/build/src/relations/define_relations_config.js.map +1 -0
  135. package/build/src/relations/manager.d.ts +60 -0
  136. package/build/src/relations/manager.d.ts.map +1 -0
  137. package/build/src/relations/manager.js +213 -0
  138. package/build/src/relations/manager.js.map +1 -0
  139. package/build/src/relations/reconcile.d.ts +111 -0
  140. package/build/src/relations/reconcile.d.ts.map +1 -0
  141. package/build/src/relations/reconcile.js +200 -0
  142. package/build/src/relations/reconcile.js.map +1 -0
  143. package/build/src/relations_config_store.d.ts +22 -0
  144. package/build/src/relations_config_store.d.ts.map +1 -0
  145. package/build/src/relations_config_store.js +74 -0
  146. package/build/src/relations_config_store.js.map +1 -0
  147. package/build/src/scope_outbox.d.ts +69 -0
  148. package/build/src/scope_outbox.d.ts.map +1 -0
  149. package/build/src/scope_outbox.js +298 -0
  150. package/build/src/scope_outbox.js.map +1 -0
  151. package/build/src/{drivers → shared}/backend_guard.d.ts +42 -0
  152. package/build/src/shared/backend_guard.d.ts.map +1 -0
  153. package/build/src/{drivers → shared}/backend_guard.js +78 -1
  154. package/build/src/shared/backend_guard.js.map +1 -0
  155. package/build/src/shared/sql_expiry.d.ts.map +1 -0
  156. package/build/src/shared/sql_expiry.js.map +1 -0
  157. package/build/src/shared/transaction_guard.d.ts +50 -0
  158. package/build/src/shared/transaction_guard.d.ts.map +1 -0
  159. package/build/src/shared/transaction_guard.js +60 -0
  160. package/build/src/shared/transaction_guard.js.map +1 -0
  161. package/build/src/sql_descendants.d.ts +47 -1
  162. package/build/src/sql_descendants.d.ts.map +1 -1
  163. package/build/src/sql_descendants.js +75 -1
  164. package/build/src/sql_descendants.js.map +1 -1
  165. package/build/src/testing/contract.d.ts +129 -4
  166. package/build/src/testing/contract.d.ts.map +1 -1
  167. package/build/src/testing/contract.js +912 -177
  168. package/build/src/testing/contract.js.map +1 -1
  169. package/build/src/testing/main.d.ts +8 -2
  170. package/build/src/testing/main.d.ts.map +1 -1
  171. package/build/src/testing/main.js +4 -1
  172. package/build/src/testing/main.js.map +1 -1
  173. package/build/src/testing/migration_contract.d.ts +284 -0
  174. package/build/src/testing/migration_contract.d.ts.map +1 -0
  175. package/build/src/testing/migration_contract.js +586 -0
  176. package/build/src/testing/migration_contract.js.map +1 -0
  177. package/build/src/testing/relations_contract.d.ts +73 -0
  178. package/build/src/testing/relations_contract.d.ts.map +1 -0
  179. package/build/src/testing/relations_contract.js +1069 -0
  180. package/build/src/testing/relations_contract.js.map +1 -0
  181. package/build/src/testing/relations_reconcile_contract.d.ts +24 -0
  182. package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -0
  183. package/build/src/testing/relations_reconcile_contract.js +220 -0
  184. package/build/src/testing/relations_reconcile_contract.js.map +1 -0
  185. package/build/src/traits/authz_scopes.js +1 -1
  186. package/build/src/traits/authz_scopes.js.map +1 -1
  187. package/build/src/traits/has_uuid.d.ts +8 -8
  188. package/build/src/traits/has_uuid.d.ts.map +1 -1
  189. package/build/src/types.d.ts +1053 -83
  190. package/build/src/types.d.ts.map +1 -1
  191. package/build/src/types.js +10 -0
  192. package/build/src/types.js.map +1 -1
  193. package/build/stubs/config/authorization.stub +104 -4
  194. package/build/stubs/migration.stub +136 -0
  195. package/build/stubs/scopes_outbox_migration.stub +57 -0
  196. package/package.json +4 -2
  197. package/build/commands/openfga_import.d.ts +0 -34
  198. package/build/commands/openfga_import.d.ts.map +0 -1
  199. package/build/commands/openfga_import.js +0 -97
  200. package/build/commands/openfga_import.js.map +0 -1
  201. package/build/src/catalog.d.ts.map +0 -1
  202. package/build/src/catalog.js.map +0 -1
  203. package/build/src/catalog_cache.d.ts.map +0 -1
  204. package/build/src/catalog_cache.js.map +0 -1
  205. package/build/src/drivers/backend_guard.d.ts.map +0 -1
  206. package/build/src/drivers/backend_guard.js.map +0 -1
  207. package/build/src/drivers/sql_expiry.d.ts.map +0 -1
  208. package/build/src/drivers/sql_expiry.js.map +0 -1
  209. package/build/src/middleware/app_access_middleware.d.ts.map +0 -1
  210. package/build/src/middleware/app_access_middleware.js.map +0 -1
  211. /package/build/src/{middleware → http}/app_access_middleware.d.ts +0 -0
  212. /package/build/src/{middleware → http}/app_access_middleware.js +0 -0
  213. /package/build/src/{drivers → shared}/sql_expiry.d.ts +0 -0
  214. /package/build/src/{drivers → shared}/sql_expiry.js +0 -0
@@ -0,0 +1,74 @@
1
+ /**
2
+ * **La config de relaciones PERSISTIDA** (Fase 4, lote 4-5 · 🟡3) — vive en la
3
+ * tabla `authz_relations_config`, bajo el gate de versión (invariante 14), como
4
+ * el catálogo. Es la copia de la config de relaciones en la BASE, para que
5
+ * cualquier proceso —también uno SIN `defineRelationsConfig` en memoria— lea
6
+ * los tipos declarados. Hoy la CONSUME `authz:relations:reconcile` (la config
7
+ * del DESTINO, para vigilar la deriva del modelo fusionado en `--dry-run`).
8
+ *
9
+ * Este módulo vive en `src/` (NO en `src/relations/`, que la regla 2 de pureza
10
+ * mantiene aislado del `db`): es la ruta que TOCA la base, como `catalog.ts`.
11
+ * Importa el TIPO y el validador de `src/relations/` (la dirección permitida:
12
+ * `relations/` no importa hacia afuera; los demás sí importan de él).
13
+ *
14
+ * **Nota (Fase 4-8):** el modelo fusionado del store lo publica
15
+ * `openfga:provision` leyendo los tipos de relación del CONFIG estático
16
+ * (`relations.config`, como `permissions` sale de `catalogs`), no de esta
17
+ * tabla: el comando es `startApp: false` (bootstrap del appliance, sin base) y
18
+ * no puede leer la base. Por eso el antiguo `republishFusedModel` —el
19
+ * republicador basado en esta tabla, con CAS sobre `model_id`— se retiró en
20
+ * 4-8: no tenía llamador de producción (la carrera que protegía no ocurre;
21
+ * republicar el modelo es un paso manual de operador, escritor único). Ver el
22
+ * informe 4-8. La columna `model_id` queda sin uso (cleanup futuro).
23
+ */
24
+ import { systemClock } from './clock.js';
25
+ import { guardSql } from './shared/backend_guard.js';
26
+ import db from '@adonisjs/lucid/services/db';
27
+ import { invalidateAuthzCatalog, withAuthzCatalogWrite } from './catalog/catalog_cache.js';
28
+ import { defineRelationsConfig } from './relations/define_relations_config.js';
29
+ const TABLE = 'authz_relations_config';
30
+ const ROW_ID = 1;
31
+ const DEFAULT_TIMEOUT_MS = 5_000;
32
+ function connectionOf(options) {
33
+ return options?.connection ? db.connection(options.connection) : db;
34
+ }
35
+ /**
36
+ * **Guarda (persiste) la config de relaciones** bajo el gate de versión: la
37
+ * valida (⚪4/F-05 de config), la serializa a JSON y hace UPSERT de la fila
38
+ * `id=1` dentro de `withAuthzCatalogWrite` —que sube la versión compartida como
39
+ * última sentencia (invariante 14): un sync en otro proceso la ve en la
40
+ * siguiente pregunta—. Devuelve la config congelada.
41
+ */
42
+ export async function saveRelationsConfig(spec, options = {}) {
43
+ // Valida ANTES de escribir: una config inválida no ensucia la tabla.
44
+ const config = defineRelationsConfig(spec);
45
+ const json = JSON.stringify(spec);
46
+ await withAuthzCatalogWrite(async (trx) => {
47
+ const existing = await trx.from(TABLE).where('id', ROW_ID).first();
48
+ if (existing) {
49
+ await trx.from(TABLE).where('id', ROW_ID).update({ spec: json, updated_at: systemClock() });
50
+ }
51
+ else {
52
+ await trx.table(TABLE).insert({ id: ROW_ID, spec: json, model_id: null, updated_at: systemClock() });
53
+ }
54
+ }, options);
55
+ invalidateAuthzCatalog();
56
+ return config;
57
+ }
58
+ /** Lee el spec persistido (o `null` si no hay relaciones declaradas — opt-in). */
59
+ export async function readRelationsConfigSpec(options = {}) {
60
+ const row = await guardSql('relations-config', 'read', options.timeoutMs ?? DEFAULT_TIMEOUT_MS, () => connectionOf(options).from(TABLE).where('id', ROW_ID).first());
61
+ if (!row?.spec)
62
+ return null;
63
+ return JSON.parse(String(row.spec));
64
+ }
65
+ /**
66
+ * Lee la config persistida ya VALIDADA (o `null`). Es lo que hace 🟡3: un
67
+ * proceso SIN `defineRelationsConfig` en memoria recupera aquí los tipos de
68
+ * relación de la base, así que el modelo que republique los lleva.
69
+ */
70
+ export async function readRelationsConfig(options = {}) {
71
+ const spec = await readRelationsConfigSpec(options);
72
+ return spec ? defineRelationsConfig(spec) : null;
73
+ }
74
+ //# sourceMappingURL=relations_config_store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relations_config_store.js","sourceRoot":"","sources":["../../src/relations_config_store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAA;AACpD,OAAO,EAAE,MAAM,6BAA6B,CAAA;AAC5C,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAC1F,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAA;AAG9E,MAAM,KAAK,GAAG,wBAAwB,CAAA;AACtC,MAAM,MAAM,GAAG,CAAC,CAAA;AAChB,MAAM,kBAAkB,GAAG,KAAK,CAAA;AAOhC,SAAS,YAAY,CAAC,OAAqC;IACzD,OAAO,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;AACrE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAyB,EACzB,UAAuC,EAAE;IAEzC,qEAAqE;IACrE,MAAM,MAAM,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAA;IAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;IACjC,MAAM,qBAAqB,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACxC,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,CAAA;QAClE,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE,CAAC,CAAA;QAC7F,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE,CAAC,CAAA;QACtG,CAAC;IACH,CAAC,EAAE,OAAO,CAAC,CAAA;IACX,sBAAsB,EAAE,CAAA;IACxB,OAAO,MAAM,CAAA;AACf,CAAC;AAED,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,UAAuC,EAAE;IAEzC,MAAM,GAAG,GAAQ,MAAM,QAAQ,CAAC,kBAAkB,EAAE,MAAM,EAAE,OAAO,CAAC,SAAS,IAAI,kBAAkB,EAAE,GAAG,EAAE,CACxG,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,CAC9D,CAAA;IACD,IAAI,CAAC,GAAG,EAAE,IAAI;QAAE,OAAO,IAAI,CAAA;IAC3B,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAwB,CAAA;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,UAAuC,EAAE;IAEzC,MAAM,IAAI,GAAG,MAAM,uBAAuB,CAAC,OAAO,CAAC,CAAA;IACnD,OAAO,IAAI,CAAC,CAAC,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAClD,CAAC"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * **La outbox del árbol sobre Lucid** (3b-2d; panel 2, cruce 4 · S5).
3
+ *
4
+ * El puerto es `ScopeOutbox` (`src/types.ts`) y el paquete NO impone tabla:
5
+ * cualquier implementación vale. Esta es la que se publica para quien no
6
+ * quiera escribir la suya, junto con su migración
7
+ * (`stubs/scopes_outbox_migration.stub`, que hay que COPIAR al proyecto:
8
+ * `configure` no la publica sola porque la outbox es opt-in).
9
+ *
10
+ * import { sqlScopeOutbox } from '@jantstack/adonis-authz'
11
+ *
12
+ * const outbox = sqlScopeOutbox()
13
+ * export default defineConfig({
14
+ * scopes: { resolveChain, outbox },
15
+ * ...
16
+ * })
17
+ *
18
+ * // y en el sitio donde mueves tu árbol:
19
+ * await db.transaction(async (trx) => {
20
+ * await authorization.scopes.moved(unit, nuevaOrg, { within, actor, transaction: trx })
21
+ * await unit.useTransaction(trx).merge({ organizationId: nuevaOrg.uuid }).save()
22
+ * })
23
+ *
24
+ * Lo único que tiene que cumplir una implementación del puerto es que
25
+ * `enqueue` escriba DENTRO de la transacción que le pasan: es lo que hace
26
+ * que el cambio del árbol y su propagación al backend confirmen —o se vayan—
27
+ * juntos. Todo lo demás (nombres de columna, si las filas aplicadas se
28
+ * borran o se marcan, si hay reintento con backoff) es del consumidor.
29
+ *
30
+ * Aquí las filas aplicadas se MARCAN (`applied_at`), no se borran: la cola
31
+ * es también el registro de lo que se propagó y cuándo, y `authz:scopes:relay`
32
+ * no puede ser silencioso.
33
+ */
34
+ import type { Clock } from './clock.js';
35
+ import type { ScopeOutbox } from './types.js';
36
+ export interface SqlScopeOutboxOptions {
37
+ /** Tabla de la cola (default `authz_scope_outbox`, la del stub). */
38
+ table?: string;
39
+ /** Nombre de la conexión Lucid (default: la primaria). */
40
+ connection?: string;
41
+ /** Deadline de cada consulta en ms (default 5000): vencido ⇒ 503. */
42
+ timeoutMs?: number;
43
+ /**
44
+ * Reloj de pared con el que se sellan `created_at` y `applied_at`
45
+ * (2.5 · J1). Default `() => new Date()`; inyectable para fijar el
46
+ * instante en tests.
47
+ */
48
+ now?: Clock;
49
+ /**
50
+ * **Cuántas veces se reintenta una entrada antes de APARCARLA** (3b-2h ·
51
+ * 🔴 2; default 5). Alcanzado el tope, `pending()` deja de ofrecerla y sale
52
+ * por `dead()`: hay entradas que no se pueden aplicar NUNCA —el scope padre
53
+ * del `attached` encolado lo borró el tenant antes del relevo y no va a
54
+ * volver— y sin tope esa fila se reintenta en todas las pasadas para
55
+ * siempre. Aparcar no es olvidar: el relay la reporta en cada pasada y el
56
+ * comando sale ≠ 0 mientras haya alguna, porque el árbol del backend queda
57
+ * permanentemente divergente en ese nodo (lo reconcilia `authz:reconcile`,
58
+ * 3b-3, o un `scopes.detached`/`attached` nuevo del consumidor).
59
+ */
60
+ maxAttempts?: number;
61
+ }
62
+ /** Lo que se necesita del `db` de Lucid; inyectable para probar sin servidor. */
63
+ interface DatabaseLike {
64
+ from(table: string): any;
65
+ table(table: string): any;
66
+ }
67
+ export declare function sqlScopeOutbox(options?: SqlScopeOutboxOptions, database?: DatabaseLike): ScopeOutbox;
68
+ export {};
69
+ //# sourceMappingURL=scope_outbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope_outbox.d.ts","sourceRoot":"","sources":["../../src/scope_outbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAKH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAMvC,OAAO,KAAK,EAEV,WAAW,EAKZ,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,GAAG,CAAC,EAAE,KAAK,CAAA;IACX;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAgBD,iFAAiF;AACjF,UAAU,YAAY;IACpB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAA;IACxB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAA;CAC1B;AAuFD,wBAAgB,cAAc,CAC5B,OAAO,GAAE,qBAA0B,EACnC,QAAQ,GAAE,YAA4C,GACrD,WAAW,CA6Lb"}
@@ -0,0 +1,298 @@
1
+ /**
2
+ * **La outbox del árbol sobre Lucid** (3b-2d; panel 2, cruce 4 · S5).
3
+ *
4
+ * El puerto es `ScopeOutbox` (`src/types.ts`) y el paquete NO impone tabla:
5
+ * cualquier implementación vale. Esta es la que se publica para quien no
6
+ * quiera escribir la suya, junto con su migración
7
+ * (`stubs/scopes_outbox_migration.stub`, que hay que COPIAR al proyecto:
8
+ * `configure` no la publica sola porque la outbox es opt-in).
9
+ *
10
+ * import { sqlScopeOutbox } from '@jantstack/adonis-authz'
11
+ *
12
+ * const outbox = sqlScopeOutbox()
13
+ * export default defineConfig({
14
+ * scopes: { resolveChain, outbox },
15
+ * ...
16
+ * })
17
+ *
18
+ * // y en el sitio donde mueves tu árbol:
19
+ * await db.transaction(async (trx) => {
20
+ * await authorization.scopes.moved(unit, nuevaOrg, { within, actor, transaction: trx })
21
+ * await unit.useTransaction(trx).merge({ organizationId: nuevaOrg.uuid }).save()
22
+ * })
23
+ *
24
+ * Lo único que tiene que cumplir una implementación del puerto es que
25
+ * `enqueue` escriba DENTRO de la transacción que le pasan: es lo que hace
26
+ * que el cambio del árbol y su propagación al backend confirmen —o se vayan—
27
+ * juntos. Todo lo demás (nombres de columna, si las filas aplicadas se
28
+ * borran o se marcan, si hay reintento con backoff) es del consumidor.
29
+ *
30
+ * Aquí las filas aplicadas se MARCAN (`applied_at`), no se borran: la cola
31
+ * es también el registro de lo que se propagó y cuándo, y `authz:scopes:relay`
32
+ * no puede ser silencioso.
33
+ */
34
+ import db from '@adonisjs/lucid/services/db';
35
+ import { AuthorizationConfigError } from './errors.js';
36
+ import { systemClock } from './clock.js';
37
+ import { assertScope, assertSubject } from './identity.js';
38
+ import { guardSql } from './shared/backend_guard.js';
39
+ import { assertCallerTransaction } from './shared/transaction_guard.js';
40
+ import { dialectOf, isSqliteDialect } from './shared/sql_expiry.js';
41
+ import { APP_SCOPE, APP_SCOPE_TYPE } from './types.js';
42
+ const DEFAULT_TABLE = 'authz_scope_outbox';
43
+ const DEFAULT_TIMEOUT_MS = 5_000;
44
+ /** Intentos antes de aparcar una entrada (3b-2h · 🔴 2). */
45
+ const DEFAULT_MAX_ATTEMPTS = 5;
46
+ /**
47
+ * Leases tomados por ESTE proceso (SQLite y cualquier dialecto sin cerrojo de
48
+ * sesión): la exclusión es entonces de proceso, no de despliegue, y va escrita
49
+ * en el README. Con PostgreSQL y MySQL el cerrojo es del servidor.
50
+ */
51
+ const localLeases = new Set();
52
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
53
+ /** Las tres operaciones del árbol, tal como se guardan en la columna `op`. */
54
+ const OPS = ['attached', 'moved', 'detached'];
55
+ /** El scope que va a la fila: la raíz se guarda como `app` con uuid nulo. */
56
+ function columnsOf(scope) {
57
+ return { type: scope.type, uuid: scope.type === APP_SCOPE_TYPE ? null : scope.uuid };
58
+ }
59
+ /** La vuelta: `app` sin uuid es `APP_SCOPE`; el resto, la fila tal cual. */
60
+ function scopeOf(type, uuid) {
61
+ return type === APP_SCOPE_TYPE ? APP_SCOPE : { type, uuid: uuid === null ? null : String(uuid) };
62
+ }
63
+ /**
64
+ * Valida el cambio ANTES del INSERT (422): una cola es un diferido, y un
65
+ * `ScopeRef` mal formado que entrara aquí explotaría días después dentro del
66
+ * relay, lejos del call-site que lo escribió.
67
+ */
68
+ function assertChange(change) {
69
+ if (!change || !OPS.includes(change.op)) {
70
+ throw new AuthorizationConfigError(`sqlScopeOutbox: 'op' debe ser uno de ${OPS.join(', ')} (llegó ${String(change?.op)})`);
71
+ }
72
+ assertScope(change.child);
73
+ if (change.op !== 'detached')
74
+ assertScope(change.parent);
75
+ }
76
+ /** Namespace de los advisory locks del paquete en PostgreSQL. */
77
+ const ADVISORY_LOCK_NAMESPACE = 0x617a;
78
+ /** Hash de 32 bits con signo (lo que acepta `pg_try_advisory_xact_lock`). */
79
+ function hash32(value) {
80
+ let hash = 0;
81
+ for (let i = 0; i < value.length; i++)
82
+ hash = (Math.imul(hash, 31) + value.charCodeAt(i)) | 0;
83
+ return hash;
84
+ }
85
+ /** El primer valor de un `rawQuery`, sea cual sea la forma del dialecto. */
86
+ function firstValue(result, column) {
87
+ const rows = Array.isArray(result) ? result[0] : (result?.rows ?? result);
88
+ const row = Array.isArray(rows) ? rows[0] : rows;
89
+ return row?.[column];
90
+ }
91
+ /**
92
+ * El cliente con el que se toma el lease: la conexión con nombre, la primaria
93
+ * del `db` de Lucid, o `null` si lo que se inyectó no es un cliente de Lucid
94
+ * (los dobles de test) — entonces el lease es de proceso.
95
+ */
96
+ function lockClientOf(database, connection) {
97
+ if (connection)
98
+ return db.connection(connection);
99
+ const client = database;
100
+ if (typeof client?.transaction === 'function' && client?.dialect)
101
+ return client;
102
+ if (typeof client?.connection === 'function') {
103
+ const primary = client.connection();
104
+ return typeof primary?.transaction === 'function' ? primary : null;
105
+ }
106
+ return null;
107
+ }
108
+ /** Una fila de la cola, tal como la ve el puerto. */
109
+ function rowToPending(row) {
110
+ const child = scopeOf(String(row.child_type), row.child_uuid);
111
+ const change = row.op === 'detached'
112
+ ? { op: 'detached', child }
113
+ : { op: row.op, child, parent: scopeOf(String(row.parent_type), row.parent_uuid) };
114
+ const actor = row.actor_type === null || row.actor_type === undefined
115
+ ? undefined
116
+ : { type: String(row.actor_type), uuid: String(row.actor_uuid) };
117
+ return {
118
+ id: row.id,
119
+ change,
120
+ attempts: Number(row.attempts ?? 0),
121
+ actor,
122
+ ...(row.last_error === null || row.last_error === undefined ? {} : { lastError: String(row.last_error) }),
123
+ };
124
+ }
125
+ /** La cota de `pending`/`dead`: entera y positiva o 500 (config rota). */
126
+ function assertLimit(limit) {
127
+ if (!(Number.isInteger(limit) && limit >= 1)) {
128
+ throw new AuthorizationConfigError(`sqlScopeOutbox: limit debe ser un entero >= 1 (llegó ${String(limit)})`);
129
+ }
130
+ }
131
+ export function sqlScopeOutbox(options = {}, database = db) {
132
+ const table = options.table ?? DEFAULT_TABLE;
133
+ if (!IDENTIFIER.test(table)) {
134
+ throw new AuthorizationConfigError(`sqlScopeOutbox: table '${table}' no es un identificador SQL simple (letras, dígitos y _).`);
135
+ }
136
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
137
+ const connection = options.connection;
138
+ const now = options.now ?? systemClock;
139
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
140
+ if (!(Number.isInteger(maxAttempts) && maxAttempts >= 1)) {
141
+ throw new AuthorizationConfigError(`sqlScopeOutbox: maxAttempts debe ser un entero >= 1 (llegó ${String(options.maxAttempts)})`);
142
+ }
143
+ /**
144
+ * La conexión de ESTA cola: la declarada, o la primaria del `db` de Lucid
145
+ * (un doble sin `primaryConnectionName` deja `undefined`: entonces se exige
146
+ * una transacción, pero no se juzga su conexión).
147
+ */
148
+ const ownConnection = connection ??
149
+ (typeof database.primaryConnectionName === 'string' ? database.primaryConnectionName : undefined);
150
+ /**
151
+ * El cliente del encolado: la transacción del consumidor si la hay — y
152
+ * SOLO si es una transacción abierta de la conexión de la cola (L-1 · 🟠 9,
153
+ * `assertCallerTransaction`): un `trx` de otra conexión encolaba en la base
154
+ * equivocada y `db` entero escribía fuera de toda transacción, en silencio.
155
+ */
156
+ const clientOf = (context) => {
157
+ const trx = assertCallerTransaction('sqlScopeOutbox.enqueue', context?.transaction, { connection: ownConnection });
158
+ return trx ?? on(database);
159
+ };
160
+ const query = (operation, run) => guardSql('sqlScopeOutbox', operation, timeoutMs, run);
161
+ /** Igual que el resto del paquete: la conexión con nombre si se declaró. */
162
+ const on = (client) => (connection && client === database ? db.connection(connection) : client);
163
+ return {
164
+ async enqueue(change, context) {
165
+ assertChange(change);
166
+ if (context?.actor !== undefined)
167
+ assertSubject(context.actor);
168
+ const child = columnsOf(change.child);
169
+ const parent = change.op === 'detached' ? { type: null, uuid: null } : columnsOf(change.parent);
170
+ const client = clientOf(context);
171
+ await query('enqueue', () => client.table(table).insert({
172
+ op: change.op,
173
+ child_type: child.type,
174
+ child_uuid: child.uuid,
175
+ parent_type: parent.type,
176
+ parent_uuid: parent.uuid,
177
+ actor_type: context?.actor?.type ?? null,
178
+ actor_uuid: context?.actor?.uuid ?? null,
179
+ attempts: 0,
180
+ last_error: null,
181
+ created_at: now(),
182
+ applied_at: null,
183
+ }));
184
+ },
185
+ async pending(limit, after) {
186
+ assertLimit(limit);
187
+ const client = on(database);
188
+ const rows = await query('pending', () => {
189
+ // Lo APARCADO (`attempts >= maxAttempts`) sale por `dead()`, no por
190
+ // aquí: si volviera, la pasada la reintentaría eternamente.
191
+ const q = client
192
+ .from(table)
193
+ .whereNull('applied_at')
194
+ .where('attempts', '<', maxAttempts)
195
+ .orderBy('id', 'asc')
196
+ .limit(limit);
197
+ // `after` es el último id que el relay ya vio EN ESTA PASADA: lo que
198
+ // dejó sin aplicar sigue pendiente y sin esto volvería a salir el
199
+ // primero para siempre (3b-2h · 🔴 2).
200
+ return after === undefined ? q : q.where('id', '>', after);
201
+ });
202
+ return rows.map(rowToPending);
203
+ },
204
+ /**
205
+ * Las entradas APARCADAS: pendientes que agotaron sus intentos. No se
206
+ * reintentan, se MIRAN (3b-2h · 🔴 2).
207
+ */
208
+ async dead(limit) {
209
+ assertLimit(limit);
210
+ const client = on(database);
211
+ const rows = await query('dead', () => client
212
+ .from(table)
213
+ .whereNull('applied_at')
214
+ .where('attempts', '>=', maxAttempts)
215
+ .orderBy('id', 'asc')
216
+ .limit(limit));
217
+ return rows.map(rowToPending);
218
+ },
219
+ /**
220
+ * **El lease del escritor ÚNICO** (3b-2h · 🟠 4). Cerrojo del SERVIDOR en
221
+ * PostgreSQL (`pg_try_advisory_xact_lock`, que se suelta con la
222
+ * transacción) y en MySQL (`GET_LOCK(name, 0)`); en SQLite —y en
223
+ * cualquier dialecto sin cerrojo de sesión— es un cerrojo de PROCESO, y
224
+ * eso está escrito: SQLite ya serializa sus escrituras y no es el motor
225
+ * de un despliegue con dos réplicas del relay.
226
+ *
227
+ * La transacción se abre SOLO para sostener el cerrojo (no escribe nada)
228
+ * y se revierte al soltarlo: es una conexión ocupada mientras dura la
229
+ * pasada, que es el precio de que dos pasadas no se pisen.
230
+ */
231
+ async acquire() {
232
+ const name = `authz_scope_outbox:${connection ?? 'default'}:${table}`;
233
+ const client = lockClientOf(database, connection);
234
+ if (client === null || isSqliteDialect(client)) {
235
+ if (localLeases.has(name))
236
+ return null;
237
+ localLeases.add(name);
238
+ return { release: async () => void localLeases.delete(name) };
239
+ }
240
+ const dialect = dialectOf(client);
241
+ const trx = await query('acquire', () => client.transaction());
242
+ try {
243
+ if (dialect.startsWith('postgres')) {
244
+ const result = await query('acquire', () => trx.rawQuery('select pg_try_advisory_xact_lock(?, ?) as taken', [ADVISORY_LOCK_NAMESPACE, hash32(name)]));
245
+ if (!firstValue(result, 'taken')) {
246
+ await trx.rollback();
247
+ return null;
248
+ }
249
+ // El cerrojo es de la TRANSACCIÓN: revertirla lo suelta.
250
+ return { release: async () => void (await trx.rollback()) };
251
+ }
252
+ if (dialect.startsWith('mysql')) {
253
+ const result = await query('acquire', () => trx.rawQuery('select get_lock(?, 0) as taken', [name.slice(0, 64)]));
254
+ if (Number(firstValue(result, 'taken') ?? 0) !== 1) {
255
+ await trx.rollback();
256
+ return null;
257
+ }
258
+ return {
259
+ release: async () => {
260
+ try {
261
+ await trx.rawQuery('select release_lock(?)', [name.slice(0, 64)]);
262
+ }
263
+ finally {
264
+ await trx.rollback();
265
+ }
266
+ },
267
+ };
268
+ }
269
+ // Un dialecto que no sabemos cerrar: se cierra en el proceso y se
270
+ // suelta la transacción (nunca se devuelve un lease de mentira).
271
+ await trx.rollback();
272
+ if (localLeases.has(name))
273
+ return null;
274
+ localLeases.add(name);
275
+ return { release: async () => void localLeases.delete(name) };
276
+ }
277
+ catch (error) {
278
+ await trx.rollback().catch(() => { });
279
+ throw error;
280
+ }
281
+ },
282
+ async markApplied(id) {
283
+ const client = on(database);
284
+ await query('markApplied', () => client.from(table).where('id', id).update({ applied_at: now(), last_error: null }));
285
+ },
286
+ async markFailed(id, error) {
287
+ const client = on(database);
288
+ // El intento se cuenta aunque el mensaje sea larguísimo: se recorta el
289
+ // texto, nunca el hecho de que falló.
290
+ await query('markFailed', () => client
291
+ .from(table)
292
+ .where('id', id)
293
+ .update({ last_error: String(error).slice(0, 2000) })
294
+ .increment('attempts', 1));
295
+ },
296
+ };
297
+ }
298
+ //# sourceMappingURL=scope_outbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope_outbox.js","sourceRoot":"","sources":["../../src/scope_outbox.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,34 @@ export declare function isAuthzError(error: unknown): boolean;
23
23
  * (2.5-B · K12): un error crudo del cliente nunca cruza la frontera.
24
24
  */
25
25
  export declare function isSqlDriverError(error: unknown): boolean;
26
+ /**
27
+ * ¿El error (o alguna de sus causas) es un choque del UNIQUE? Por la forma de
28
+ * cada cliente: PostgreSQL SQLSTATE `23505`, MySQL `errno` 1062
29
+ * (`ER_DUP_ENTRY`), SQLite `SQLITE_CONSTRAINT_UNIQUE` (o el `SQLITE_CONSTRAINT`
30
+ * genérico con «UNIQUE» en el mensaje). Se mira la cadena entera de causas:
31
+ * `guardSql` ya lo habrá envuelto en un `AuthorizationBackendError`.
32
+ *
33
+ * Lo usa el driver `database` DENTRO de una transacción del llamante (L-3):
34
+ * fuera de ella un choque se absorbe releyendo (K4), dentro no se puede —en
35
+ * PostgreSQL la transacción ya está abortada y en REPEATABLE READ la relectura
36
+ * no vería al ganador— y sale como 409 `E_AUTHZ_WRITE_CONFLICT`.
37
+ */
38
+ export declare function isUniqueViolation(error: unknown): boolean;
39
+ /**
40
+ * ¿El error (o alguna de sus causas) es un DEADLOCK que el motor resolvió
41
+ * eligiendo esta transacción como víctima? PostgreSQL SQLSTATE `40P01`
42
+ * (`deadlock_detected`; y `40001`, `serialization_failure`, la misma familia
43
+ * de «reintenta»), MySQL `errno` 1213 (`ER_LOCK_DEADLOCK`: InnoDB deshace la
44
+ * transacción ENTERA de la víctima). SQLite no tiene deadlocks entre
45
+ * escritores: el segundo recibe `SQLITE_BUSY` (503), no un deadlock.
46
+ *
47
+ * Lo usa el driver `database` de relaciones DENTRO de una transacción del
48
+ * llamante (L-4, 🟡 12 del auditor): dos transacciones que escriben dos
49
+ * relaciones en orden cruzado ⇒ el perdedor sale con **409
50
+ * `E_AUTHZ_WRITE_CONFLICT`** («haz rollback y reintenta»), jamás un error
51
+ * crudo del motor ni un 503 disfrazado de indeterminado.
52
+ */
53
+ export declare function isDeadlock(error: unknown): boolean;
26
54
  /**
27
55
  * ¿El error viene de un deadline vencido? knex lanza `KnexTimeoutError` (o, si
28
56
  * además falló la cancelación, el error de cancelación con `timeout` puesto);
@@ -87,6 +115,20 @@ export declare function resolveChain(resolver: ScopeChainResolver, scope: ScopeR
87
115
  * sigue siendo 503: no se limpia a ciegas cuando el árbol está caído.
88
116
  */
89
117
  export declare function canonicalScope(resolver: ScopeChainResolver, scope: ScopeRef, operation: string): Promise<ScopeRef>;
118
+ /**
119
+ * **Los scopes destino de un DELETE que no exige que el scope exista**
120
+ * (`revoke`/`removeDeny`; 3b-8 · A4, mismo cierre que 3b-2h · 🟠 3 dio a
121
+ * `scopes.detached`). Con la fila viva es exactamente UNO, el canónico
122
+ * (`chain[0]`). Sin cadena con la que canonizar —la fila ya borrada, o sin
123
+ * resolutor— la ortografía del llamante a secas hacía del delete un no-op
124
+ * SILENCIOSO cuando llegaba un alias del uuid (el mismo uuid sin guiones,
125
+ * que el tipo `uuid` de PG o una collation `*_ci` funden con la fila real):
126
+ * el hecho canónico seguía escrito y volvía a conceder si el scope se
127
+ * restauraba. Se reusa el fan-out de `scopeSpellings`: la ortografía del
128
+ * llamante Y la canónica de la que puede ser alias. Quitar nunca concede,
129
+ * así que borrar de más no existe aquí.
130
+ */
131
+ export declare function canonicalScopeTargets(resolver: ScopeChainResolver, scope: ScopeRef, operation: string): Promise<ScopeRef[]>;
90
132
  /** La cadena canónica, o 422 `E_AUTHZ_UNKNOWN_SCOPE` si el scope no existe (para escrituras). */
91
133
  export declare function assertKnownScope(resolver: ScopeChainResolver, scope: ScopeRef, operation: string): Promise<ScopeRef[]>;
92
134
  //# sourceMappingURL=backend_guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend_guard.d.ts","sourceRoot":"","sources":["../../../src/shared/backend_guard.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAI/D;;;;;;;;;;;GAWG;AAEH,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQxD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAWzD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQlD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQrD;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAAC,CAAC,EAC9B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAsBZ;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAOnG;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,kBAO9B,CAAA;AAeD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,YAAY,CAChC,QAAQ,EAAE,kBAAkB,EAC5B,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,CA0C5B;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,kBAAkB,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAQxH;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,kBAAkB,EAC5B,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAQrB;AAED,iGAAiG;AACjG,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,kBAAkB,EAC5B,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,EAAE,CAAC,CASrB"}
@@ -1,6 +1,6 @@
1
1
  import { AuthorizationBackendError, AuthorizationBackendTimeoutError, NoScopeResolverError, ScopeResolverError, UnknownScopeError, } from '../errors.js';
2
2
  import { APP_SCOPE, APP_SCOPE_TYPE } from '../types.js';
3
- import { assertScope } from '../identity.js';
3
+ import { assertScope, scopeSpellings } from '../identity.js';
4
4
  /**
5
5
  * Clasificación de fallos de dependencias, compartida por ambos drivers.
6
6
  *
@@ -40,6 +40,58 @@ export function isSqlDriverError(error) {
40
40
  return true;
41
41
  return false;
42
42
  }
43
+ /**
44
+ * ¿El error (o alguna de sus causas) es un choque del UNIQUE? Por la forma de
45
+ * cada cliente: PostgreSQL SQLSTATE `23505`, MySQL `errno` 1062
46
+ * (`ER_DUP_ENTRY`), SQLite `SQLITE_CONSTRAINT_UNIQUE` (o el `SQLITE_CONSTRAINT`
47
+ * genérico con «UNIQUE» en el mensaje). Se mira la cadena entera de causas:
48
+ * `guardSql` ya lo habrá envuelto en un `AuthorizationBackendError`.
49
+ *
50
+ * Lo usa el driver `database` DENTRO de una transacción del llamante (L-3):
51
+ * fuera de ella un choque se absorbe releyendo (K4), dentro no se puede —en
52
+ * PostgreSQL la transacción ya está abortada y en REPEATABLE READ la relectura
53
+ * no vería al ganador— y sale como 409 `E_AUTHZ_WRITE_CONFLICT`.
54
+ */
55
+ export function isUniqueViolation(error) {
56
+ let current = error;
57
+ for (let depth = 0; current && depth < 6; depth++) {
58
+ if (current.code === '23505')
59
+ return true;
60
+ if (current.errno === 1062)
61
+ return true;
62
+ if (typeof current.code === 'string' && /^SQLITE_CONSTRAINT/.test(current.code)) {
63
+ if (current.code === 'SQLITE_CONSTRAINT_UNIQUE' || /UNIQUE/i.test(String(current.message ?? '')))
64
+ return true;
65
+ }
66
+ current = current.cause;
67
+ }
68
+ return false;
69
+ }
70
+ /**
71
+ * ¿El error (o alguna de sus causas) es un DEADLOCK que el motor resolvió
72
+ * eligiendo esta transacción como víctima? PostgreSQL SQLSTATE `40P01`
73
+ * (`deadlock_detected`; y `40001`, `serialization_failure`, la misma familia
74
+ * de «reintenta»), MySQL `errno` 1213 (`ER_LOCK_DEADLOCK`: InnoDB deshace la
75
+ * transacción ENTERA de la víctima). SQLite no tiene deadlocks entre
76
+ * escritores: el segundo recibe `SQLITE_BUSY` (503), no un deadlock.
77
+ *
78
+ * Lo usa el driver `database` de relaciones DENTRO de una transacción del
79
+ * llamante (L-4, 🟡 12 del auditor): dos transacciones que escriben dos
80
+ * relaciones en orden cruzado ⇒ el perdedor sale con **409
81
+ * `E_AUTHZ_WRITE_CONFLICT`** («haz rollback y reintenta»), jamás un error
82
+ * crudo del motor ni un 503 disfrazado de indeterminado.
83
+ */
84
+ export function isDeadlock(error) {
85
+ let current = error;
86
+ for (let depth = 0; current && depth < 6; depth++) {
87
+ if (current.code === '40P01' || current.code === '40001')
88
+ return true;
89
+ if (current.errno === 1213)
90
+ return true;
91
+ current = current.cause;
92
+ }
93
+ return false;
94
+ }
43
95
  /**
44
96
  * ¿El error viene de un deadline vencido? knex lanza `KnexTimeoutError` (o, si
45
97
  * además falló la cancelación, el error de cancelación con `timeout` puesto);
@@ -209,6 +261,31 @@ export async function canonicalScope(resolver, scope, operation) {
209
261
  throw error;
210
262
  }
211
263
  }
264
+ /**
265
+ * **Los scopes destino de un DELETE que no exige que el scope exista**
266
+ * (`revoke`/`removeDeny`; 3b-8 · A4, mismo cierre que 3b-2h · 🟠 3 dio a
267
+ * `scopes.detached`). Con la fila viva es exactamente UNO, el canónico
268
+ * (`chain[0]`). Sin cadena con la que canonizar —la fila ya borrada, o sin
269
+ * resolutor— la ortografía del llamante a secas hacía del delete un no-op
270
+ * SILENCIOSO cuando llegaba un alias del uuid (el mismo uuid sin guiones,
271
+ * que el tipo `uuid` de PG o una collation `*_ci` funden con la fila real):
272
+ * el hecho canónico seguía escrito y volvía a conceder si el scope se
273
+ * restauraba. Se reusa el fan-out de `scopeSpellings`: la ortografía del
274
+ * llamante Y la canónica de la que puede ser alias. Quitar nunca concede,
275
+ * así que borrar de más no existe aquí.
276
+ */
277
+ export async function canonicalScopeTargets(resolver, scope, operation) {
278
+ try {
279
+ const chain = await resolveChain(resolver, scope, operation);
280
+ if (chain)
281
+ return [chain[0]];
282
+ }
283
+ catch (error) {
284
+ if (!(error instanceof NoScopeResolverError))
285
+ throw error;
286
+ }
287
+ return scopeSpellings(scope);
288
+ }
212
289
  /** La cadena canónica, o 422 `E_AUTHZ_UNKNOWN_SCOPE` si el scope no existe (para escrituras). */
213
290
  export async function assertKnownScope(resolver, scope, operation) {
214
291
  const chain = await resolveChain(resolver, scope, operation);
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend_guard.js","sourceRoot":"","sources":["../../../src/shared/backend_guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql_expiry.d.ts","sourceRoot":"","sources":["../../../src/shared/sql_expiry.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,WAAW;IAC1B,kEAAkE;IAClE,IAAI,CAAC,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAAA;IACrC,4EAA4E;IAC5E,IAAI,CAAC,EAAE,EAAE,IAAI,GAAG,OAAO,CAAA;IACvB,yGAAyG;IACzG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC/C,kEAAkE;IAClE,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAAA;CACpC;AAID,kGAAkG;AAClG,wBAAgB,SAAS,CAAC,UAAU,EAAE,GAAG,GAAG,MAAM,CAEjD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,GAAG,GAAG,OAAO,CAGxD;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,IAAI,GAAG,MAAM,CAElD;AAED,qGAAqG;AACrG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAM3D;AA6BD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,GAAE,GAAqB,GAAG,WAAW,CAE7E"}