@jantstack/adonis-authz 2.4.0-alpha.9 → 2.5.0-alpha.1

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 (109) hide show
  1. package/README.md +296 -15
  2. package/build/commands/authz_freeze.js +1 -1
  3. package/build/commands/authz_freeze.js.map +1 -1
  4. package/build/commands/authz_reconcile.d.ts +6 -2
  5. package/build/commands/authz_reconcile.d.ts.map +1 -1
  6. package/build/commands/authz_reconcile.js +129 -13
  7. package/build/commands/authz_reconcile.js.map +1 -1
  8. package/build/commands/authz_relations_reconcile.d.ts +66 -19
  9. package/build/commands/authz_relations_reconcile.d.ts.map +1 -1
  10. package/build/commands/authz_relations_reconcile.js +212 -39
  11. package/build/commands/authz_relations_reconcile.js.map +1 -1
  12. package/build/commands/authz_scopes_relay.d.ts.map +1 -1
  13. package/build/commands/authz_scopes_relay.js +81 -1
  14. package/build/commands/authz_scopes_relay.js.map +1 -1
  15. package/build/commands/authz_unfreeze.d.ts +12 -4
  16. package/build/commands/authz_unfreeze.d.ts.map +1 -1
  17. package/build/commands/authz_unfreeze.js +74 -9
  18. package/build/commands/authz_unfreeze.js.map +1 -1
  19. package/build/index.d.ts +6 -4
  20. package/build/index.d.ts.map +1 -1
  21. package/build/index.js +5 -3
  22. package/build/index.js.map +1 -1
  23. package/build/providers/authz_provider.d.ts +1 -1
  24. package/build/providers/authz_provider.d.ts.map +1 -1
  25. package/build/providers/authz_provider.js +2 -1
  26. package/build/providers/authz_provider.js.map +1 -1
  27. package/build/src/define_config.d.ts +9 -0
  28. package/build/src/define_config.d.ts.map +1 -1
  29. package/build/src/define_config.js +11 -0
  30. package/build/src/define_config.js.map +1 -1
  31. package/build/src/drivers/database_driver.d.ts.map +1 -1
  32. package/build/src/drivers/database_driver.js +1 -0
  33. package/build/src/drivers/database_driver.js.map +1 -1
  34. package/build/src/drivers/database_relations_driver.d.ts +18 -6
  35. package/build/src/drivers/database_relations_driver.d.ts.map +1 -1
  36. package/build/src/drivers/database_relations_driver.js +91 -44
  37. package/build/src/drivers/database_relations_driver.js.map +1 -1
  38. package/build/src/drivers/openfga_driver.d.ts +16 -1
  39. package/build/src/drivers/openfga_driver.d.ts.map +1 -1
  40. package/build/src/drivers/openfga_driver.js +66 -20
  41. package/build/src/drivers/openfga_driver.js.map +1 -1
  42. package/build/src/drivers/openfga_relations_driver.d.ts +16 -4
  43. package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -1
  44. package/build/src/drivers/openfga_relations_driver.js +71 -34
  45. package/build/src/drivers/openfga_relations_driver.js.map +1 -1
  46. package/build/src/errors.d.ts +50 -0
  47. package/build/src/errors.d.ts.map +1 -1
  48. package/build/src/errors.js +50 -0
  49. package/build/src/errors.js.map +1 -1
  50. package/build/src/freeze.d.ts +63 -4
  51. package/build/src/freeze.d.ts.map +1 -1
  52. package/build/src/freeze.js +107 -18
  53. package/build/src/freeze.js.map +1 -1
  54. package/build/src/identity.d.ts +120 -9
  55. package/build/src/identity.d.ts.map +1 -1
  56. package/build/src/identity.js +164 -12
  57. package/build/src/identity.js.map +1 -1
  58. package/build/src/manager.d.ts +64 -2
  59. package/build/src/manager.d.ts.map +1 -1
  60. package/build/src/manager.js +262 -35
  61. package/build/src/manager.js.map +1 -1
  62. package/build/src/openfga.d.ts +1 -1
  63. package/build/src/openfga.d.ts.map +1 -1
  64. package/build/src/openfga.js +1 -1
  65. package/build/src/openfga.js.map +1 -1
  66. package/build/src/relations/check_many.d.ts +17 -0
  67. package/build/src/relations/check_many.d.ts.map +1 -0
  68. package/build/src/relations/check_many.js +57 -0
  69. package/build/src/relations/check_many.js.map +1 -0
  70. package/build/src/relations/manager.d.ts +20 -0
  71. package/build/src/relations/manager.d.ts.map +1 -1
  72. package/build/src/relations/manager.js +84 -13
  73. package/build/src/relations/manager.js.map +1 -1
  74. package/build/src/relations/reconcile.d.ts +67 -0
  75. package/build/src/relations/reconcile.d.ts.map +1 -1
  76. package/build/src/relations/reconcile.js +123 -30
  77. package/build/src/relations/reconcile.js.map +1 -1
  78. package/build/src/relations/tuple_key.d.ts +25 -0
  79. package/build/src/relations/tuple_key.d.ts.map +1 -0
  80. package/build/src/relations/tuple_key.js +60 -0
  81. package/build/src/relations/tuple_key.js.map +1 -0
  82. package/build/src/shared/backend_guard.d.ts +30 -11
  83. package/build/src/shared/backend_guard.d.ts.map +1 -1
  84. package/build/src/shared/backend_guard.js +45 -15
  85. package/build/src/shared/backend_guard.js.map +1 -1
  86. package/build/src/shared/sql_expiry.d.ts +2 -0
  87. package/build/src/shared/sql_expiry.d.ts.map +1 -1
  88. package/build/src/shared/sql_expiry.js +4 -0
  89. package/build/src/shared/sql_expiry.js.map +1 -1
  90. package/build/src/testing/main.d.ts +3 -1
  91. package/build/src/testing/main.d.ts.map +1 -1
  92. package/build/src/testing/main.js +2 -1
  93. package/build/src/testing/main.js.map +1 -1
  94. package/build/src/testing/migration_contract.d.ts +9 -0
  95. package/build/src/testing/migration_contract.d.ts.map +1 -1
  96. package/build/src/testing/migration_contract.js +5 -2
  97. package/build/src/testing/migration_contract.js.map +1 -1
  98. package/build/src/testing/relations_contract.d.ts +14 -0
  99. package/build/src/testing/relations_contract.d.ts.map +1 -1
  100. package/build/src/testing/relations_contract.js +297 -21
  101. package/build/src/testing/relations_contract.js.map +1 -1
  102. package/build/src/testing/test_clock.d.ts +15 -0
  103. package/build/src/testing/test_clock.d.ts.map +1 -0
  104. package/build/src/testing/test_clock.js +65 -0
  105. package/build/src/testing/test_clock.js.map +1 -0
  106. package/build/src/types.d.ts +108 -9
  107. package/build/src/types.d.ts.map +1 -1
  108. package/build/src/types.js.map +1 -1
  109. package/package.json +5 -1
@@ -1 +1 @@
1
- {"version":3,"file":"freeze.d.ts","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,aAAa,IAAI,CAAA;AAE9B,wIAAwI;AACxI,eAAO,MAAM,uBAAuB,QAAS,CAAA;AAI7C,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,CAAA;AAE9D,iGAAiG;AACjG,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAA;CACf;AAED,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAyCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAkBtF;AAED,mHAAmH;AACnH,wBAAgB,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAGnE;AAED,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB;IAC5D,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQxG;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAChF,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA8B7B;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EACzC,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAYlB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,EACxB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC,CAejD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,UAAU,GAAG,SAAS,CAG1E"}
1
+ {"version":3,"file":"freeze.d.ts","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  import db from '@adonisjs/lucid/services/db';
2
2
  import { guardSql, withDeadline } from './shared/backend_guard.js';
3
- import { AuthorizationBackendError, AuthorizationBackendTimeoutError, AuthorizationFrozenError } from './errors.js';
3
+ import { AuthorizationBackendError, AuthorizationBackendTimeoutError, AuthorizationFrozenError, UnsupportedDialectError, } from './errors.js';
4
4
  import { CATALOG_VERSION_TABLE } from './catalog/catalog_cache.js';
5
+ import { dialectOf, isMysqlDialect, isSqliteDialect } from './shared/sql_expiry.js';
5
6
  /**
6
7
  * El **freeze DURABLE** (3b-7; decisión del dueño del 2026-08-31 (3b):
7
8
  * B + E-analista). Primitivas SQL puras; la policy (quién congela, el
@@ -134,21 +135,64 @@ export async function assertNotFrozenRow(operation, options) {
134
135
  throw new AuthorizationFrozenError(`${operation}: el motor de autorización está congelado (${row.reason})${lift} y no acepta escrituras. ` +
135
136
  `Las lecturas siguen funcionando; reintenta esta escritura cuando la ventana termine.`);
136
137
  }
138
+ /** El holder con el que se publica una fila con motivo y SIN holder (legada o a mano); también es su token. */
139
+ export const UNKNOWN_HOLDER = '?';
140
+ /** Prefijo del holder de la ventana del OPERADOR (`freeze()` con `kind: 'operator'`). */
141
+ export const OPERATOR_HOLDER_PREFIX = 'operator:';
142
+ /**
143
+ * `isOperatorHolder` en SQL, por motor (cierre-2 de alpha.10 · C3): los
144
+ * `OPERATOR_HOLDER_PREFIX.length` primeros CARACTERES de `freeze_holder` igual a
145
+ * `operator:`, sensible a mayúsculas y acentos. SQLite: `substr()` no hereda
146
+ * collation ⇒ `BINARY`. PostgreSQL: `COLLATE "C"` (byte a byte aunque la base
147
+ * declare otra). MySQL: la collation de la columna es la de la tabla (`_ai_ci` por
148
+ * defecto: `OPERATOR:` y `opérator:` serían iguales), así que se compara como
149
+ * BINARIO. El prefijo es ASCII, así que «9 caracteres» y «9 unidades UTF-16» (JS)
150
+ * coinciden siempre que la comparación sea verdadera. Sin `LIKE`: ni comodines ni
151
+ * collation. Cualquier otro dialecto ⇒ 500 `E_AUTHZ_UNSUPPORTED_DIALECT` ANTES de
152
+ * tocar la fila (re-ataque del cierre-2 · ⚪ 3: el `substr` de SQLite en MSSQL
153
+ * sería un error del motor envuelto como 503, que invita a reintentar para siempre).
154
+ */
155
+ export function operatorHolderSql(connection = db.connection()) {
156
+ const length = OPERATOR_HOLDER_PREFIX.length;
157
+ if (isMysqlDialect(connection)) {
158
+ return { sql: `CAST(SUBSTRING(freeze_holder, 1, ${length}) AS BINARY) = CAST(? AS BINARY)`, bindings: [OPERATOR_HOLDER_PREFIX] };
159
+ }
160
+ if (dialectOf(connection).startsWith('postgres')) {
161
+ return { sql: `substr(freeze_holder, 1, ${length}) COLLATE "C" = ?`, bindings: [OPERATOR_HOLDER_PREFIX] };
162
+ }
163
+ if (isSqliteDialect(connection)) {
164
+ return { sql: `substr(freeze_holder, 1, ${length}) = ?`, bindings: [OPERATOR_HOLDER_PREFIX] };
165
+ }
166
+ throw new UnsupportedDialectError(`freeze: dialecto '${dialectOf(connection)}' sin observación en la suite (hoy: PostgreSQL, MySQL 8 y SQLite)`);
167
+ }
137
168
  /**
138
169
  * Toma el freeze si NADIE lo sostiene vivo (condicional y atómico: dos
139
- * `acquire` a la vez acaban en exactamente uno). Devuelve el token, o `null`
140
- * si hay un freeze vivo de otro dueño (el llamante decide si eso es 423 o un
141
- * contexto que reconoce).
170
+ * `acquire` a la vez acaban en exactamente uno). Una fila con el lease VENCIDO
171
+ * se toma encima (el dueño murió: la flota vuelve sola) **salvo que sea una
172
+ * ventana de OPERADOR** (cierre del auditor de alpha.10 · R2): esa exclusión va
173
+ * DENTRO del `WHERE` del mismo `UPDATE`, en los tres motores. Hasta el cierre
174
+ * vivía en `freeze()` como «leo la fila, compruebo, tomo», y con un reloj que
175
+ * cruzaba el vencimiento entre la comprobación y el UPDATE la pasada pisaba la
176
+ * ventana del operador sin 503 — el TOCTOU que el atómico existe para cerrar.
142
177
  */
143
- export async function acquireFreeze(input, options = {}) {
178
+ export async function acquireFreezeOutcome(input, options = {}, attempt = 0) {
144
179
  const { driver, timeoutMs } = opts(options);
180
+ // Fuera de `freezeSql`: un dialecto no soportado es config (500), no un backend caído (503).
181
+ const operator = operatorHolderSql();
145
182
  const updated = await freezeSql(driver, 'freeze.acquire', timeoutMs, () => db
146
183
  .from(CATALOG_VERSION_TABLE)
147
184
  .where('id', FREEZE_ROW_ID)
148
185
  .where((q) => {
149
- // Libre, o con el lease VENCIDO (un dueño muerto no bloquea la toma).
186
+ // Libre, o con el lease VENCIDO (un dueño muerto no bloquea la toma)…
150
187
  q.whereNull('freeze_reason').orWhere((expired) => {
151
- expired.whereNotNull('freeze_until_ms').andWhere('freeze_until_ms', '<=', input.nowMs);
188
+ expired
189
+ .whereNotNull('freeze_until_ms')
190
+ .andWhere('freeze_until_ms', '<=', input.nowMs)
191
+ // …salvo la ventana del OPERADOR vencida: es evidencia, no espacio libre (R2).
192
+ .andWhere((notOperator) => {
193
+ // La regla ÚNICA (`isOperatorHolder`), sin `LIKE`: comparación EXACTA de prefijo (cierre-2 · C3).
194
+ notOperator.whereNull('freeze_holder').orWhereRaw(`NOT (${operator.sql})`, operator.bindings);
195
+ });
152
196
  });
153
197
  })
154
198
  .update({
@@ -159,17 +203,36 @@ export async function acquireFreeze(input, options = {}) {
159
203
  updated_at: new Date(input.nowMs),
160
204
  }));
161
205
  if (Number(updated) === 0) {
162
- // O hay un freeze vivo, o la fila no existe: distinguirlo es leerla.
206
+ // O hay un freeze vivo, o una ventana de operador vencida, o la fila no existe: distinguirlo es leerla.
163
207
  const row = await readFreezeRow(options);
164
208
  if (freezeIsLive(row, input.nowMs))
165
- return null;
166
- // Carrera minúscula (se liberó entre el UPDATE y el SELECT): reintenta una vez.
167
- return acquireFreeze(input, options);
209
+ return { status: 'held', row };
210
+ if (row.reason !== null && freezeKindOf(row.holder) === 'operator')
211
+ return { status: 'expired-operator', row };
212
+ // Carrera minúscula (se liberó entre el UPDATE y el SELECT): reintenta UNA vez.
213
+ // Cierre-2 · C3: tras el reintento, una fila que NO está viva no es `held` (su
214
+ // letra dice «VIVO»). Con la regla única del prefijo esta rama solo la alcanza
215
+ // una fila que cambió dos veces entre UPDATE y SELECT: 503 reintentable que lo dice.
216
+ if (attempt >= 1) {
217
+ const error = new AuthorizationBackendError(driver, 'freeze.acquire', new Error('la fila del freeze cambió entre el UPDATE condicional y su relectura dos veces seguidas'));
218
+ error.message =
219
+ `${driver}: no se pudo tomar el freeze: la fila (${row.holder ?? 'sin holder'}, fence ${row.fence}) NO está viva ` +
220
+ `y aun así el UPDATE condicional no casó dos veces seguidas — otro proceso la está cambiando a la vez ` +
221
+ `(rama «contención tras reintento», no «freeze VIVO»). Reintenta.`;
222
+ throw error;
223
+ }
224
+ return acquireFreezeOutcome(input, options, attempt + 1);
168
225
  }
169
226
  const row = await readFreezeRow(options);
227
+ // Otro acquire ganó justo después (no debería: el nuestro dejó la fila viva).
170
228
  if (row.holder !== input.holder)
171
- return null; // otro acquire ganó justo después (no debería: el nuestro dejó la fila viva)
172
- return { fence: row.fence, holder: input.holder };
229
+ return { status: 'held', row };
230
+ return { status: 'acquired', token: { fence: row.fence, holder: input.holder } };
231
+ }
232
+ /** `acquireFreezeOutcome` reducido a «el token o `null`» (lo usan los casos que siembran una fila ajena). */
233
+ export async function acquireFreeze(input, options = {}) {
234
+ const outcome = await acquireFreezeOutcome(input, options);
235
+ return outcome.status === 'acquired' ? outcome.token : null;
173
236
  }
174
237
  /**
175
238
  * Renueva el lease, CONDICIONAL por token y solo si aún no venció
@@ -197,7 +260,16 @@ export async function renewFreeze(token, input, options = {}) {
197
260
  export async function releaseFreeze(token, input, options = {}) {
198
261
  const { driver, timeoutMs } = opts(options);
199
262
  const row = await readFreezeRow(options);
200
- const mine = row.fence === token.fence && row.holder === token.holder;
263
+ // La fila SIN holder (legada o escrita a mano) se publica como `UNKNOWN_HOLDER`
264
+ // en `freezeStatus()`/`lastFreezeStatus()`, así que ese es su token: sin esto
265
+ // `authz:unfreeze` no casaba nunca y repetía su aviso para siempre (cierre del
266
+ // auditor de alpha.10 · ⚪ 5). `freeze()` jamás escribe ese holder.
267
+ // Cierre-2 · C4: `'?'` publica DOS filas —holder NULL y holder LITERAL `'?'` (a
268
+ // mano)—, así que el token casa las dos (`IS NULL OR = '?'`); casar solo NULL
269
+ // dejaba la literal sin salida y `authz:unfreeze` en bucle con exit 1. Sigue
270
+ // atado al fence y nunca casa un holder real.
271
+ const unknownHolder = token.holder === UNKNOWN_HOLDER;
272
+ const mine = row.fence === token.fence && (unknownHolder ? row.holder === null || row.holder === UNKNOWN_HOLDER : row.holder === token.holder);
201
273
  const lapsed = !mine || (row.untilMs !== null && row.untilMs <= input.nowMs);
202
274
  if (!mine)
203
275
  return { released: false, lapsed };
@@ -205,13 +277,30 @@ export async function releaseFreeze(token, input, options = {}) {
205
277
  .from(CATALOG_VERSION_TABLE)
206
278
  .where('id', FREEZE_ROW_ID)
207
279
  .where('freeze_fence', token.fence)
208
- .where('freeze_holder', token.holder)
280
+ .where((q) => unknownHolder ? q.whereNull('freeze_holder').orWhere('freeze_holder', UNKNOWN_HOLDER) : q.where('freeze_holder', token.holder))
209
281
  .update({ freeze_reason: null, freeze_holder: null, freeze_until_ms: null, updated_at: new Date(input.nowMs) }));
210
282
  return { released: Number(updated) > 0, lapsed };
211
283
  }
212
- /** El `kind` que el holder lleva escrito (`operator:123:ab…` → `operator`). */
284
+ /**
285
+ * **La regla ÚNICA del prefijo de operador** (cierre-2 de alpha.10 · C3): los
286
+ * `OPERATOR_HOLDER_PREFIX.length` primeros caracteres del holder son EXACTAMENTE
287
+ * `operator:` — sensible a mayúsculas y a acentos, sin comodines. La MISMA
288
+ * comparación va en el `WHERE` de `acquireFreezeOutcome` (`operatorHolderSql`) y
289
+ * aquí; hasta el cierre-2 el SQL usaba `LIKE` (insensible a mayúsculas en SQLite y
290
+ * en MySQL `_ci`) y JS partía por `:`, así que un holder `OPERATOR:…` caducado era
291
+ * «de operador» para el UPDATE y `unknown` para JS ⇒ 423 «VIVO» sobre una fila
292
+ * caducada, distinto por motor.
293
+ */
294
+ export function isOperatorHolder(holder) {
295
+ return typeof holder === 'string' && holder.slice(0, OPERATOR_HOLDER_PREFIX.length) === OPERATOR_HOLDER_PREFIX;
296
+ }
297
+ /** El `kind` que el holder lleva escrito (`operator:123:ab…` → `operator`); el de operador, por `isOperatorHolder`. */
213
298
  export function freezeKindOf(holder) {
214
- const prefix = holder?.split(':', 1)[0];
215
- return prefix === 'operator' || prefix === 'reconcile' || prefix === 'platform' ? prefix : 'unknown';
299
+ if (isOperatorHolder(holder))
300
+ return 'operator';
301
+ if (typeof holder !== 'string' || !holder.includes(':'))
302
+ return 'unknown';
303
+ const prefix = holder.slice(0, holder.indexOf(':'));
304
+ return prefix === 'reconcile' || prefix === 'platform' ? prefix : 'unknown';
216
305
  }
217
306
  //# sourceMappingURL=freeze.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"freeze.js","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"freeze.js","sourceRoot":"","sources":["../../src/freeze.ts"],"names":[],"mappings":"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"}
@@ -97,6 +97,36 @@ export declare function isValidRelationId(value: unknown): value is string;
97
97
  * 422 aquí, antes de serializarse a nada.
98
98
  */
99
99
  export declare function scopeKey(scope: ScopeRef): string;
100
+ /**
101
+ * **Las formas que PostgreSQL funde con `uuid` — y solo esas** (cierre-3 del
102
+ * lote A, decisión del dueño 2026-10-15 (3)): dado el uuid de un scope tal
103
+ * como lo escribió el llamante, la ortografía CANÓNICA (8-4-4-4-12) con la
104
+ * que un motor pudo fundirlo, o `[]` si no es alias de nada (ya es canónica,
105
+ * o no es un uuid, o es una forma que ningún motor del README funde).
106
+ *
107
+ * Es la regla del FAN-OUT de roles (`scopeSpellings`: la purga sin cadena
108
+ * de `scopes.detached`, `revoke`/`removeDeny`), y es a propósito MÁS
109
+ * ESTRECHA que `isNonCanonicalUuid`, la del 422 de las particiones de
110
+ * relaciones: son DOS propósitos. Rechazar de más (422 por `-<32hex>`) es
111
+ * inocuo, el llamante lo ve y lo corrige; purgar de más NO lo es, porque la
112
+ * canónica que se añade puede ser un scope VIVO de otro tenant en un
113
+ * consumidor con ids de texto elegibles por el usuario (🟡 C2-1/C2-2 del
114
+ * re-ataque al cierre-2: hasta el cierre-3 `scopeSpellings` usaba la ancha y
115
+ * `a0e-<hex de la víctima>` arrastraba a la víctima a la purga). Expandir
116
+ * una forma que ningún motor funde es coste sin beneficio: nunca hay hechos
117
+ * canónicos que sean «suyos».
118
+ *
119
+ * **Formas de más de 36 caracteres** (⚪ C2-4): PG funde también `4×8`
120
+ * (39 caracteres, el ejemplo literal de su doc), pero `IDENTITY_LIMITS.uuid`
121
+ * es 36 —la columna publicada— y `assertScope` la rechaza ANTES de llegar
122
+ * aquí con 422 `E_AUTHZ_INVALID_IDENTITY`: ruidoso y visible para el
123
+ * consumidor, nunca una purga a medias en silencio. El conjunto de este
124
+ * fan-out es, por tanto, «lo que PG funde Y cabe en 36».
125
+ *
126
+ * La inversa NO existe: dada la canónica no se enumeran sus alias (serían
127
+ * 2^7 ortografías); el fan-out va siempre de alias ⇒ canónica.
128
+ */
129
+ export declare function pgFusedSpellings(uuid: string): string[];
100
130
  /**
101
131
  * **Las ORTOGRAFÍAS de un scope con las que hay que limpiar cuando ya no hay
102
132
  * cadena con la que canonizar** (3b-2h · 🟠 3, auditor R2).
@@ -114,17 +144,98 @@ export declare function scopeKey(scope: ScopeRef): string;
114
144
  * fail-closed; en escritura era fail-OPEN.
115
145
  *
116
146
  * La salida es no elegir: una purga sin cadena limpia la ortografía del
117
- * llamante **y** la canónica de la que puede ser alias. Solo se expande el
118
- * alias SIN guiones (32 hex ⇒ también la forma 8-4-4-4-12), que es la única
119
- * que un motor produce fundiendo —PostgreSQL canoniza CON guiones; el alias
120
- * por mayúsculas ya muere en la puerta (`assertScope`)—; así el consumidor
121
- * que guarda sus uuids con guiones (el caso normal) no paga NADA, y el que
122
- * los guarda sin guiones sigue purgando su clave y, además, una que nunca
123
- * tiene hechos. Normalizar en vez de expandir no vale: sin la fila no se sabe
124
- * cuál de las dos es la buena, y elegir mal deja los hechos vivos, que es el
125
- * fallo que se está cerrando.
147
+ * llamante **y** la canónica de la que puede ser alias. **La regla de «¿de
148
+ * qué canónica puede ser alias?» es `pgFusedSpellings`**: exactamente lo que
149
+ * el tipo `uuid` de PostgreSQL funde (sin guiones, `8-4-4-16`, `8-8-8-8`,
150
+ * `12-4-4-12`…, medido), ni una forma más. Hasta el cierre-2 del lote A solo
151
+ * se expandía la forma SIN guiones y `8-4-4-16` dejaba la canónica viva
152
+ * (🟠 R1); el cierre-2 pasó a `isNonCanonicalUuid`, que es MÁS ANCHA que lo
153
+ * que PG funde (`-<32hex>`, `<32hex>-`, `a0e-ebc99…`, `--`: ids legales de
154
+ * la gramática que PG rechaza), y eso ampliaba el alcance de una purga sin
155
+ * cadena a scopes VIVOS ajenos (🟡 C2-1); el cierre-3 separa las dos reglas
156
+ * por propósito. El alias por mayúsculas ya muere en la puerta
157
+ * (`assertScope`). Así el consumidor que guarda sus uuids canónicos (el caso
158
+ * normal) no paga NADA, y el que los guarda de otra forma sigue purgando su
159
+ * clave y, además, la canónica. Normalizar en vez de expandir no vale: sin
160
+ * la fila no se sabe cuál de las dos es la buena, y elegir mal deja los
161
+ * hechos vivos, que es el fallo que se está cerrando.
162
+ *
163
+ * Esta función es PURA (no mira el árbol). Quien la consume es UN solo punto
164
+ * de paso, `canonicalScopeTargets` (`backend_guard.ts`; lo usan
165
+ * `scopes.detached` sin cadena en el manager y el fan-out de
166
+ * `revoke`/`removeDeny` en los dos drivers, 3b-8 · A4), que además DESCARTA
167
+ * toda ortografía que el resolutor reconozca como scope VIVO distinto del
168
+ * pedido (cierre-3 · 🟡 C2-2/C2-3): nunca se purga un scope vivo que no es
169
+ * el que se pidió.
126
170
  */
127
171
  export declare function scopeSpellings(scope: ScopeRef): ScopeRef[];
172
+ /** ¿Es un uuid canónico escrito SIN guiones (32 hex en minúsculas)? */
173
+ export declare function isDashlessUuid(value: unknown): value is string;
174
+ /** La forma 8-4-4-4-12 de un uuid de 32 hex (la ortografía CANÓNICA). */
175
+ export declare function dashedUuid(dashless: string): string;
176
+ /**
177
+ * **¿Es una ortografía NO canónica de un uuid?** (D4 + cierre 🟠 A2 del
178
+ * auditor del lote A, decisión del dueño 2026-10-15): todo valor que, sin
179
+ * guiones, sea 32 hex y NO esté exactamente en la forma 8-4-4-4-12. Cubre el
180
+ * 32 hex sin guiones (lo único que D4 cerraba) Y cualquier otra colocación de
181
+ * guiones —`8-4-4-16`, cuatro grupos, la canónica sin su último guion…—.
182
+ *
183
+ * **Es la regla ANCHA, y se usa SOLO para el 422 de las particiones de
184
+ * relaciones** (`assertPartition`; cierre-3 del lote A, decisión del dueño
185
+ * 2026-10-15 (3)): es un SUPERCONJUNTO de lo que el tipo `uuid` de
186
+ * PostgreSQL funde —incluye `-<32hex>`, `<32hex>-`, `a0e-ebc99…` o `8--24`,
187
+ * que PG rechaza con «invalid input syntax» (medido, PG 18)— y ahí ser más
188
+ * ancha es inocuo: un 422 de más lo ve y lo corrige el llamante, y una
189
+ * partición que «parece un uuid» sin serlo tampoco merece ser una clave
190
+ * distinta de la canónica. Lo que PG funde DE VERDAD es `pgFusedSpellings`,
191
+ * la regla del fan-out de ROLES, donde expandir de más sí cuesta (purgar un
192
+ * scope vivo ajeno). Un valor que sin guiones no sea 32 hex (31, 33, un
193
+ * carácter no hex, `_`/`.`) NO es un uuid y es un id legal.
194
+ * Es la MISMA función que usa el detector de la receta del README, por
195
+ * construcción (el `upgrade_recipe.spec` lo mide: lo que `assertPartition`
196
+ * rechaza, el detector lo lista).
197
+ */
198
+ export declare function isNonCanonicalUuid(value: unknown): value is string;
199
+ /**
200
+ * **La partición de una relación se escribe en la forma CANÓNICA 8-4-4-4-12,
201
+ * o es 422** (D4, decisión del dueño 2026-10-14; `panel-tenencia-2-juez.md`
202
+ * §5; ampliado el 2026-10-15 a toda ortografía no canónica, 🟠 A2).
203
+ * `assertScope` vale para un scope de ROLES porque ahí la identidad la
204
+ * canoniza la cadena del resolutor (invariante 17) y, donde no hay cadena,
205
+ * `scopeSpellings` barre la ortografía del llamante y la canónica de la que
206
+ * puede ser alias, con SU regla (`pgFusedSpellings`, lo que PG funde de
207
+ * verdad: cierre-3). En RELACIONES no hay cadena con
208
+ * la que canonizar: la clave de partición es un string comparado byte a
209
+ * byte, así que `unit|aaaa…` (32 hex) y `unit|aaaa-…` eran DOS particiones,
210
+ * la purga canónica no veía la otra y, con el 422, las tuplas viejas sin
211
+ * guiones quedaban inalcanzables para siempre (tester H5). Y la clase no era
212
+ * solo «sin guiones»: PostgreSQL funde también `8-4-4-16`, cuatro grupos de
213
+ * ocho, la canónica sin un guion… (🟠 A2) — cualquier colocación de guiones
214
+ * sobre los mismos 32 hex —, así que la regla es «32 hex sin guiones y no
215
+ * exactamente 8-4-4-4-12 ⇒ 422» (`isNonCanonicalUuid`). La regla es una sola
216
+ * y va en un solo sitio: toda operación que recibe una partición —manager,
217
+ * los dos drivers, el doble publicado y `reconcileRelations`— pasa por aquí
218
+ * (o por `partitionKey`, que la llama), y la guardia de superficie
219
+ * (`tests/guard_surface.spec.ts`, columna `partition`) lo sondea en cada
220
+ * operación con las DOS formas (sin guiones y `8-4-4-16`). Las filas escritas
221
+ * antes de 2.5.0 con una ortografía no canónica se detectan, se listan y se
222
+ * BORRAN con la receta del README («Upgrading to 2.5.0», fail-closed por
223
+ * defecto: eran concesiones invisibles a la purga canónica); re-clavarlas es
224
+ * el paso opt-in `--rekey`. La ejecuta `tests/upgrade_recipe.spec.ts` en los
225
+ * tres motores, con las dos ramas.
226
+ *
227
+ * Un id de partición que no sea un uuid sigue valiendo (`[a-z0-9._-]{1,36}`:
228
+ * 32 caracteres no todos hex, 31 o 33 hex…). Los ids de OBJETO y de SUJETO
229
+ * no se rechazan: se comparan byte a byte y el README lo declara.
230
+ */
231
+ export declare function assertPartition(partition: ScopeRef): void;
232
+ /**
233
+ * La clave de partición de una RELACIÓN: `scopeKey` con la regla de
234
+ * `assertPartition` delante. Es lo que los dos drivers de relaciones (y el
235
+ * doble) usan para componer `partition_key` / el id de objeto del store: no
236
+ * hay camino hacia el backend que no pase por aquí.
237
+ */
238
+ export declare function partitionKey(partition: ScopeRef): string;
128
239
  /**
129
240
  * La inversa de `scopeKey` para claves que vienen de la BASE (el owner de un
130
241
  * rol): `app` ⇒ la raíz; `<tipo>|<uuid>` ⇒ el scope; cualquier otra forma
@@ -1 +1 @@
1
- {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"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"}
@@ -199,6 +199,54 @@ export function scopeKey(scope) {
199
199
  }
200
200
  /** Un uuid canónico escrito SIN guiones: 32 dígitos hexadecimales. */
201
201
  const DASHLESS_UUID = /^[0-9a-f]{32}$/;
202
+ /**
203
+ * **Lo que el tipo `uuid` de PostgreSQL FUNDE de verdad**: 32 hex con, como
204
+ * mucho, UN guion detrás de cada grupo de cuatro. Es la letra de la doc
205
+ * (postgresql.org/docs/current/datatype-uuid.html, «PostgreSQL also accepts
206
+ * the following alternative forms for input: […] omitting some or all
207
+ * hyphens, adding a hyphen after any group of four digits») y está MEDIDO
208
+ * contra PostgreSQL 18 (`select '<forma>'::uuid`, cierre-3 del lote A):
209
+ * funde la canónica, `32 hex`, `8-4-4-16`, `8-8-8-8`, `12-4-4-12`, `8-24` y
210
+ * `4×8` (39 caracteres); RECHAZA `-<32hex>`, `<32hex>-`, `a0e-ebc99…` (guion
211
+ * tras tres) y `8--24` (dos guiones seguidos) con «invalid input syntax for
212
+ * type uuid». Mayúsculas y llaves también las funde, pero no pasan
213
+ * `assertScope` (minúsculas, `[a-z0-9._-]`).
214
+ */
215
+ const PG_FUSED_UUID = /^([0-9a-f]{4}-?){7}[0-9a-f]{4}$/;
216
+ /**
217
+ * **Las formas que PostgreSQL funde con `uuid` — y solo esas** (cierre-3 del
218
+ * lote A, decisión del dueño 2026-10-15 (3)): dado el uuid de un scope tal
219
+ * como lo escribió el llamante, la ortografía CANÓNICA (8-4-4-4-12) con la
220
+ * que un motor pudo fundirlo, o `[]` si no es alias de nada (ya es canónica,
221
+ * o no es un uuid, o es una forma que ningún motor del README funde).
222
+ *
223
+ * Es la regla del FAN-OUT de roles (`scopeSpellings`: la purga sin cadena
224
+ * de `scopes.detached`, `revoke`/`removeDeny`), y es a propósito MÁS
225
+ * ESTRECHA que `isNonCanonicalUuid`, la del 422 de las particiones de
226
+ * relaciones: son DOS propósitos. Rechazar de más (422 por `-<32hex>`) es
227
+ * inocuo, el llamante lo ve y lo corrige; purgar de más NO lo es, porque la
228
+ * canónica que se añade puede ser un scope VIVO de otro tenant en un
229
+ * consumidor con ids de texto elegibles por el usuario (🟡 C2-1/C2-2 del
230
+ * re-ataque al cierre-2: hasta el cierre-3 `scopeSpellings` usaba la ancha y
231
+ * `a0e-<hex de la víctima>` arrastraba a la víctima a la purga). Expandir
232
+ * una forma que ningún motor funde es coste sin beneficio: nunca hay hechos
233
+ * canónicos que sean «suyos».
234
+ *
235
+ * **Formas de más de 36 caracteres** (⚪ C2-4): PG funde también `4×8`
236
+ * (39 caracteres, el ejemplo literal de su doc), pero `IDENTITY_LIMITS.uuid`
237
+ * es 36 —la columna publicada— y `assertScope` la rechaza ANTES de llegar
238
+ * aquí con 422 `E_AUTHZ_INVALID_IDENTITY`: ruidoso y visible para el
239
+ * consumidor, nunca una purga a medias en silencio. El conjunto de este
240
+ * fan-out es, por tanto, «lo que PG funde Y cabe en 36».
241
+ *
242
+ * La inversa NO existe: dada la canónica no se enumeran sus alias (serían
243
+ * 2^7 ortografías); el fan-out va siempre de alias ⇒ canónica.
244
+ */
245
+ export function pgFusedSpellings(uuid) {
246
+ if (typeof uuid !== 'string' || !PG_FUSED_UUID.test(uuid) || CANONICAL_UUID.test(uuid))
247
+ return [];
248
+ return [dashedUuid(uuid.replace(/-/g, ''))];
249
+ }
202
250
  /**
203
251
  * **Las ORTOGRAFÍAS de un scope con las que hay que limpiar cuando ya no hay
204
252
  * cadena con la que canonizar** (3b-2h · 🟠 3, auditor R2).
@@ -216,23 +264,127 @@ const DASHLESS_UUID = /^[0-9a-f]{32}$/;
216
264
  * fail-closed; en escritura era fail-OPEN.
217
265
  *
218
266
  * La salida es no elegir: una purga sin cadena limpia la ortografía del
219
- * llamante **y** la canónica de la que puede ser alias. Solo se expande el
220
- * alias SIN guiones (32 hex ⇒ también la forma 8-4-4-4-12), que es la única
221
- * que un motor produce fundiendo —PostgreSQL canoniza CON guiones; el alias
222
- * por mayúsculas ya muere en la puerta (`assertScope`)—; así el consumidor
223
- * que guarda sus uuids con guiones (el caso normal) no paga NADA, y el que
224
- * los guarda sin guiones sigue purgando su clave y, además, una que nunca
225
- * tiene hechos. Normalizar en vez de expandir no vale: sin la fila no se sabe
226
- * cuál de las dos es la buena, y elegir mal deja los hechos vivos, que es el
227
- * fallo que se está cerrando.
267
+ * llamante **y** la canónica de la que puede ser alias. **La regla de «¿de
268
+ * qué canónica puede ser alias?» es `pgFusedSpellings`**: exactamente lo que
269
+ * el tipo `uuid` de PostgreSQL funde (sin guiones, `8-4-4-16`, `8-8-8-8`,
270
+ * `12-4-4-12`…, medido), ni una forma más. Hasta el cierre-2 del lote A solo
271
+ * se expandía la forma SIN guiones y `8-4-4-16` dejaba la canónica viva
272
+ * (🟠 R1); el cierre-2 pasó a `isNonCanonicalUuid`, que es MÁS ANCHA que lo
273
+ * que PG funde (`-<32hex>`, `<32hex>-`, `a0e-ebc99…`, `--`: ids legales de
274
+ * la gramática que PG rechaza), y eso ampliaba el alcance de una purga sin
275
+ * cadena a scopes VIVOS ajenos (🟡 C2-1); el cierre-3 separa las dos reglas
276
+ * por propósito. El alias por mayúsculas ya muere en la puerta
277
+ * (`assertScope`). Así el consumidor que guarda sus uuids canónicos (el caso
278
+ * normal) no paga NADA, y el que los guarda de otra forma sigue purgando su
279
+ * clave y, además, la canónica. Normalizar en vez de expandir no vale: sin
280
+ * la fila no se sabe cuál de las dos es la buena, y elegir mal deja los
281
+ * hechos vivos, que es el fallo que se está cerrando.
282
+ *
283
+ * Esta función es PURA (no mira el árbol). Quien la consume es UN solo punto
284
+ * de paso, `canonicalScopeTargets` (`backend_guard.ts`; lo usan
285
+ * `scopes.detached` sin cadena en el manager y el fan-out de
286
+ * `revoke`/`removeDeny` en los dos drivers, 3b-8 · A4), que además DESCARTA
287
+ * toda ortografía que el resolutor reconozca como scope VIVO distinto del
288
+ * pedido (cierre-3 · 🟡 C2-2/C2-3): nunca se purga un scope vivo que no es
289
+ * el que se pidió.
228
290
  */
229
291
  export function scopeSpellings(scope) {
230
292
  assertScope(scope);
231
293
  const uuid = scope.uuid;
232
- if (uuid === null || !DASHLESS_UUID.test(uuid))
294
+ if (uuid === null)
233
295
  return [scope];
234
- const dashed = `${uuid.slice(0, 8)}-${uuid.slice(8, 12)}-${uuid.slice(12, 16)}-${uuid.slice(16, 20)}-${uuid.slice(20)}`;
235
- return [scope, { type: scope.type, uuid: dashed }];
296
+ return [scope, ...pgFusedSpellings(uuid).map((canonical) => ({ type: scope.type, uuid: canonical }))];
297
+ }
298
+ /** ¿Es un uuid canónico escrito SIN guiones (32 hex en minúsculas)? */
299
+ export function isDashlessUuid(value) {
300
+ return typeof value === 'string' && DASHLESS_UUID.test(value);
301
+ }
302
+ /** La forma 8-4-4-4-12 de un uuid de 32 hex (la ortografía CANÓNICA). */
303
+ export function dashedUuid(dashless) {
304
+ return `${dashless.slice(0, 8)}-${dashless.slice(8, 12)}-${dashless.slice(12, 16)}-${dashless.slice(16, 20)}-${dashless.slice(20)}`;
305
+ }
306
+ /* ── Partición de RELACIONES (D4, plan 2.5 · lote A) ───────────────────── */
307
+ /** La ortografía CANÓNICA de un uuid: exactamente 8-4-4-4-12, minúsculas. */
308
+ const CANONICAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
309
+ /**
310
+ * **¿Es una ortografía NO canónica de un uuid?** (D4 + cierre 🟠 A2 del
311
+ * auditor del lote A, decisión del dueño 2026-10-15): todo valor que, sin
312
+ * guiones, sea 32 hex y NO esté exactamente en la forma 8-4-4-4-12. Cubre el
313
+ * 32 hex sin guiones (lo único que D4 cerraba) Y cualquier otra colocación de
314
+ * guiones —`8-4-4-16`, cuatro grupos, la canónica sin su último guion…—.
315
+ *
316
+ * **Es la regla ANCHA, y se usa SOLO para el 422 de las particiones de
317
+ * relaciones** (`assertPartition`; cierre-3 del lote A, decisión del dueño
318
+ * 2026-10-15 (3)): es un SUPERCONJUNTO de lo que el tipo `uuid` de
319
+ * PostgreSQL funde —incluye `-<32hex>`, `<32hex>-`, `a0e-ebc99…` o `8--24`,
320
+ * que PG rechaza con «invalid input syntax» (medido, PG 18)— y ahí ser más
321
+ * ancha es inocuo: un 422 de más lo ve y lo corrige el llamante, y una
322
+ * partición que «parece un uuid» sin serlo tampoco merece ser una clave
323
+ * distinta de la canónica. Lo que PG funde DE VERDAD es `pgFusedSpellings`,
324
+ * la regla del fan-out de ROLES, donde expandir de más sí cuesta (purgar un
325
+ * scope vivo ajeno). Un valor que sin guiones no sea 32 hex (31, 33, un
326
+ * carácter no hex, `_`/`.`) NO es un uuid y es un id legal.
327
+ * Es la MISMA función que usa el detector de la receta del README, por
328
+ * construcción (el `upgrade_recipe.spec` lo mide: lo que `assertPartition`
329
+ * rechaza, el detector lo lista).
330
+ */
331
+ export function isNonCanonicalUuid(value) {
332
+ if (typeof value !== 'string')
333
+ return false;
334
+ return DASHLESS_UUID.test(value.replace(/-/g, '')) && !CANONICAL_UUID.test(value);
335
+ }
336
+ /**
337
+ * **La partición de una relación se escribe en la forma CANÓNICA 8-4-4-4-12,
338
+ * o es 422** (D4, decisión del dueño 2026-10-14; `panel-tenencia-2-juez.md`
339
+ * §5; ampliado el 2026-10-15 a toda ortografía no canónica, 🟠 A2).
340
+ * `assertScope` vale para un scope de ROLES porque ahí la identidad la
341
+ * canoniza la cadena del resolutor (invariante 17) y, donde no hay cadena,
342
+ * `scopeSpellings` barre la ortografía del llamante y la canónica de la que
343
+ * puede ser alias, con SU regla (`pgFusedSpellings`, lo que PG funde de
344
+ * verdad: cierre-3). En RELACIONES no hay cadena con
345
+ * la que canonizar: la clave de partición es un string comparado byte a
346
+ * byte, así que `unit|aaaa…` (32 hex) y `unit|aaaa-…` eran DOS particiones,
347
+ * la purga canónica no veía la otra y, con el 422, las tuplas viejas sin
348
+ * guiones quedaban inalcanzables para siempre (tester H5). Y la clase no era
349
+ * solo «sin guiones»: PostgreSQL funde también `8-4-4-16`, cuatro grupos de
350
+ * ocho, la canónica sin un guion… (🟠 A2) — cualquier colocación de guiones
351
+ * sobre los mismos 32 hex —, así que la regla es «32 hex sin guiones y no
352
+ * exactamente 8-4-4-4-12 ⇒ 422» (`isNonCanonicalUuid`). La regla es una sola
353
+ * y va en un solo sitio: toda operación que recibe una partición —manager,
354
+ * los dos drivers, el doble publicado y `reconcileRelations`— pasa por aquí
355
+ * (o por `partitionKey`, que la llama), y la guardia de superficie
356
+ * (`tests/guard_surface.spec.ts`, columna `partition`) lo sondea en cada
357
+ * operación con las DOS formas (sin guiones y `8-4-4-16`). Las filas escritas
358
+ * antes de 2.5.0 con una ortografía no canónica se detectan, se listan y se
359
+ * BORRAN con la receta del README («Upgrading to 2.5.0», fail-closed por
360
+ * defecto: eran concesiones invisibles a la purga canónica); re-clavarlas es
361
+ * el paso opt-in `--rekey`. La ejecuta `tests/upgrade_recipe.spec.ts` en los
362
+ * tres motores, con las dos ramas.
363
+ *
364
+ * Un id de partición que no sea un uuid sigue valiendo (`[a-z0-9._-]{1,36}`:
365
+ * 32 caracteres no todos hex, 31 o 33 hex…). Los ids de OBJETO y de SUJETO
366
+ * no se rechazan: se comparan byte a byte y el README lo declara.
367
+ */
368
+ export function assertPartition(partition) {
369
+ assertScope(partition);
370
+ if (partition.uuid !== null && isNonCanonicalUuid(partition.uuid)) {
371
+ const canonical = dashedUuid(partition.uuid.replace(/-/g, ''));
372
+ throw new InvalidIdentityError(`Partición inválida: el uuid '${partition.uuid}' del scope '${partition.type}' no está en la forma canónica ` +
373
+ `8-4-4-4-12 (${partition.uuid.includes('-') ? 'guiones mal colocados' : 'sin guiones, 32 hex'}). ` +
374
+ `Las particiones de relaciones se comparan byte a byte y no se canonizan: escríbelo como '${canonical}'. ` +
375
+ `Las tuplas escritas con una ortografía no canónica antes de 2.5.0 se detectan y se borran con la receta ` +
376
+ `del README («Upgrading to 2.5.0»; re-clavarlas es el paso opt-in --rekey).`);
377
+ }
378
+ }
379
+ /**
380
+ * La clave de partición de una RELACIÓN: `scopeKey` con la regla de
381
+ * `assertPartition` delante. Es lo que los dos drivers de relaciones (y el
382
+ * doble) usan para componer `partition_key` / el id de objeto del store: no
383
+ * hay camino hacia el backend que no pase por aquí.
384
+ */
385
+ export function partitionKey(partition) {
386
+ assertPartition(partition);
387
+ return scopeKey(partition);
236
388
  }
237
389
  /**
238
390
  * La inversa de `scopeKey` para claves que vienen de la BASE (el owner de un