@jantstack/adonis-authz 2.4.0-alpha.3 → 2.4.0-alpha.5

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 (67) hide show
  1. package/README.md +92 -3
  2. package/build/commands/authz_reconcile.d.ts +5 -3
  3. package/build/commands/authz_reconcile.d.ts.map +1 -1
  4. package/build/commands/authz_reconcile.js +15 -3
  5. package/build/commands/authz_reconcile.js.map +1 -1
  6. package/build/commands/authz_scopes_relay.d.ts +18 -0
  7. package/build/commands/authz_scopes_relay.d.ts.map +1 -1
  8. package/build/commands/authz_scopes_relay.js +55 -7
  9. package/build/commands/authz_scopes_relay.js.map +1 -1
  10. package/build/commands/authz_scopes_requeue.d.ts +63 -0
  11. package/build/commands/authz_scopes_requeue.d.ts.map +1 -0
  12. package/build/commands/authz_scopes_requeue.js +154 -0
  13. package/build/commands/authz_scopes_requeue.js.map +1 -0
  14. package/build/commands/main.d.ts +16 -13
  15. package/build/commands/main.d.ts.map +1 -1
  16. package/build/commands/main.js +59 -11
  17. package/build/commands/main.js.map +1 -1
  18. package/build/commands/openfga_provision.d.ts +49 -3
  19. package/build/commands/openfga_provision.d.ts.map +1 -1
  20. package/build/commands/openfga_provision.js +72 -7
  21. package/build/commands/openfga_provision.js.map +1 -1
  22. package/build/index.d.ts +3 -3
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +2 -2
  25. package/build/index.js.map +1 -1
  26. package/build/src/define_config.d.ts +24 -0
  27. package/build/src/define_config.d.ts.map +1 -1
  28. package/build/src/define_config.js.map +1 -1
  29. package/build/src/drivers/openfga_driver.d.ts +4 -2
  30. package/build/src/drivers/openfga_driver.d.ts.map +1 -1
  31. package/build/src/drivers/openfga_driver.js +38 -5
  32. package/build/src/drivers/openfga_driver.js.map +1 -1
  33. package/build/src/errors.d.ts +30 -0
  34. package/build/src/errors.d.ts.map +1 -1
  35. package/build/src/errors.js +32 -0
  36. package/build/src/errors.js.map +1 -1
  37. package/build/src/manager.d.ts +106 -5
  38. package/build/src/manager.d.ts.map +1 -1
  39. package/build/src/manager.js +425 -26
  40. package/build/src/manager.js.map +1 -1
  41. package/build/src/relations/manager.d.ts +13 -1
  42. package/build/src/relations/manager.d.ts.map +1 -1
  43. package/build/src/relations/manager.js +15 -1
  44. package/build/src/relations/manager.js.map +1 -1
  45. package/build/src/scope_outbox.d.ts +21 -0
  46. package/build/src/scope_outbox.d.ts.map +1 -1
  47. package/build/src/scope_outbox.js +211 -24
  48. package/build/src/scope_outbox.js.map +1 -1
  49. package/build/src/sql_descendants.d.ts +8 -0
  50. package/build/src/sql_descendants.d.ts.map +1 -1
  51. package/build/src/sql_descendants.js.map +1 -1
  52. package/build/src/testing/contract.d.ts.map +1 -1
  53. package/build/src/testing/contract.js +4 -1
  54. package/build/src/testing/contract.js.map +1 -1
  55. package/build/src/testing/main.d.ts +2 -0
  56. package/build/src/testing/main.d.ts.map +1 -1
  57. package/build/src/testing/main.js +1 -0
  58. package/build/src/testing/main.js.map +1 -1
  59. package/build/src/testing/scope_outbox_contract.d.ts +88 -0
  60. package/build/src/testing/scope_outbox_contract.d.ts.map +1 -0
  61. package/build/src/testing/scope_outbox_contract.js +607 -0
  62. package/build/src/testing/scope_outbox_contract.js.map +1 -0
  63. package/build/src/types.d.ts +255 -18
  64. package/build/src/types.d.ts.map +1 -1
  65. package/build/src/types.js +51 -0
  66. package/build/src/types.js.map +1 -1
  67. package/package.json +1 -1
package/README.md CHANGED
@@ -31,6 +31,8 @@ node ace authz:catalog:sync
31
31
 
32
32
  `configure` registers the provider, the commands and the `appAccess` middleware, defines the env variables, and **publishes into your project** what belongs to you: the migration for the `authz_*` tables and two config files (`config/authorization.ts` for drivers and the scope tree, `config/app_acl.ts` for your role catalog). The suite compiles both published configs against the package on every run, so they cannot drift from it.
33
33
 
34
+ > **Upgrading to 2.4.0-alpha.4 — how the commands are registered.** `@jantstack/adonis-authz/commands` is now a real ace loader (it exports `getMetaData()`/`getCommand()`, built with `ListLoader` over the nine command classes, next to the named class exports). Until alpha.4 it only re-exported the classes, so the line `configure` writes — `commands: [() => import('@jantstack/adonis-authz/commands')]` — made **the whole `node ace` crash** at boot (`TypeError: loaderInstance.getMetaData is not a function`). If you registered the commands by wrapping the barrel in a `ListLoader(Object.values(m))` (the only way it worked before alpha.4), remove the wrapper and register the module directly: `() => import('@jantstack/adonis-authz/commands')`, which is what `node ace configure` writes. The wrapper now fails with `TypeError: command.serialize is not a function`, because the module also exports the loader functions: `Object.values(m)` includes them and `ListLoader.getMetaData()` calls `serialize()` on each value. The module has no `default` export on purpose — ace calls `getMetaData()` on the module namespace and never unwraps `default` (same shape as `@adonisjs/core/commands`).
35
+
34
36
  For the OpenFGA driver, also install its SDK (optional peer): `npm i @openfga/sdk`. Everything that touches the SDK lives behind the **`@jantstack/adonis-authz/openfga` subpath** (`OpenFgaAuthorizationDriver`, `provisionOpenFgaStore`, `importAuthzFactsToOpenFga`, `openFgaAuthorizationModel`, `assertHolderTypes`); the main entry never imports it, so a database-only install boots without the SDK (the suite loads `index.ts` in a child process with `@openfga/sdk` blocked to prove it: *"index.ts (la entrada principal) carga con el SDK bloqueado"*). The published config imports the driver from that subpath *inside* the factory.
35
37
 
36
38
  ## Semantics (what every driver guarantees)
@@ -95,6 +97,8 @@ Your resolver's *answer* is validated too: an element that is not a well-formed
95
97
 
96
98
  `ScopeType` is an open `string`, so define your own union for type safety. The engine never queries your tables.
97
99
 
100
+ **How deep can the tree go?** In `database`, as deep as your resolver walks. With the `openfga` driver the `facts` model resolves a chain reliably up to **22 `parent` hops** (`FACTS_MAX_RESOLVE_DEPTH`) and answers 503 — never `false` — past it, so design the tree with that number in front of you and, if you want the engine to refuse the 23rd level before it is written, declare `scopes.maxDepth` (422 `E_AUTHZ_SCOPE_DEPTH_LIMIT`, partial: it does not see the height of a moved subtree). Every bound the package enforces is in one table: [Limits](#limits).
101
+
98
102
  #### The tree outbox, and the relay lag you are accepting
99
103
 
100
104
  Those three notifications write to the backend **inside your transaction, and they do not roll back with it**. If a later statement of that transaction fails — a constraint, a validation, a pool timeout; no crash needed — your database keeps the old tree and the backend keeps the new one. With the `openfga` driver the backend *is* the PDP, so what is left is a **persistent escalation your own database cannot show you**: every holder with a role in the new parent authorises over a scope that, in SQL, still belongs to the old tenant. This is not misuse; correct use leaks. The suite demonstrates it against a real server — the rollback happens and the escalation stays.
@@ -122,15 +126,29 @@ await db.transaction(async (trx) => {
122
126
  ```bash
123
127
  node ace authz:scopes:relay # drain the queue and apply the edges
124
128
  node ace authz:scopes:relay --dry-run # list what is still unpropagated
129
+ node ace authz:scopes:relay --only=12,13 # apply only these entries (the receipt of scopes.*), 2.4.0-alpha.5
130
+ node ace authz:scopes:requeue --id=12 # return a parked entry to the queue (refuses to revert later applied changes), 2.4.0-alpha.5
125
131
  ```
126
132
 
127
- The package **does not impose a table**: the contract is the `ScopeOutbox` port (`enqueue`, `pending`, `markApplied`, `markFailed`, plus two optional ones: `dead` for parked entries and `acquire` for the single-writer lease). `sqlScopeOutbox` is the published implementation over Lucid and `stubs/scopes_outbox_migration.stub` is its migration — **copy it into your migrations yourself**; `node ace configure` does not publish it, because the outbox is opt-in. The only thing an implementation must do is write `enqueue` inside the transaction it is handed.
133
+ The package **does not impose a table**: the contract is the `ScopeOutbox` port (`enqueue`, `pending`, `markApplied`, `markFailed`, plus five optional ones: `dead` for parked entries, `acquire` for the single-writer lease and, since 2.4.0-alpha.5, `earlierPending` — the dependency probe of `only` —, `requeue` and `laterApplied` — the pair behind `authz:scopes:requeue`; in the same release `enqueue` may return `{ id }`, `pending` takes an optional `{ ids }` filter and `markFailed` an optional `{ transient }`, all three covariant: an implementation written against alpha.4 still compiles and works). Since 2.4.0-alpha.5 the port has a published judge, [`runScopeOutboxContract`](#the-outbox-contract-runscopeoutboxcontract-240-alpha5). `sqlScopeOutbox` is the published implementation over Lucid and `stubs/scopes_outbox_migration.stub` is its migration — **copy it into your migrations yourself**; `node ace configure` does not publish it, because the outbox is opt-in. Two things an implementation must do: write `enqueue` inside the transaction it is handed, and **`pending()` must return the whole unapplied queue, oldest first** — it must not hide an entry the relay did not filter through `{ ids }`, by tenant, by scope or by anything else: the order guarantee of the relay (a `moved` before the `attached` that depends on it) rests on the manager seeing the whole queue, and no guard of the manager can tell when a port hides rows on its own (that is exactly how the cross-tenant escalation measured in the audit is reproduced with a "filtering" outbox). It is a duty of the port and the contract judges it (O-08).
128
134
 
129
135
  **Two rules about that transaction (2.4.0-alpha.2, lot L-1).** First, **`{ transaction }` carries the write, never the authority**: the freeze barrier that precedes every write (row `id = 2` of `authz_catalog_version`, see [the freeze](#migrating-and-verifying-authzreconcile-23)) is read through the engine's own connection, **never through your transaction** — a transaction opened *before* a freeze does not see it in its snapshot (InnoDB's REPEATABLE READ, PostgreSQL under that level, SQLite in WAL), and a barrier decided by the caller's snapshot is not a barrier (the suite reproduces the bypass on all three engines and pins the 503). The price is declared: **`{ transaction }` requires a connection pool of at least 2**. With a pool of 1 (SQLite `:memory:`) the barrier cannot get a connection while you hold the only one, and the notification is **503 `E_AUTHZ_BACKEND_TIMEOUT` at `freezeTimeoutMs`** (config, default 5000 ms) — fail-closed, never a hang, never a bypass. Second, `sqlScopeOutbox` **checks whose transaction it is**: it must be an *open* Lucid transaction (`isTransaction === true`) of the **outbox's own connection** (`connection`, or the primary one). A transaction of another connection, a `QueryClient` (`db.connection()`) or the whole `db` service are **500 `E_AUTHZ_CONFIG`** naming both connections, before any statement — until L-1 a transaction of another connection silently enqueued in *its* database (where no relay reads and `dead()` never shows it), and `db` passed the duck-check and wrote outside any transaction, in silence. The same check (`assertCallerTransaction`) is the rule for every `{ transaction }` of the package. The whole story — what `{ transaction }` means on each call, what the engines do, the `resolveChain` limit and what `openfga` offers instead — is in [Writing inside your transaction](#writing-inside-your-transaction-240-alpha2).
130
136
 
131
137
  The relay is resumable and never silent: the report says *which* changes were applied, not a count. A change that cannot be applied **defers what depends on it and lets the rest through**: the failure poisons the scopes that change names, every later change naming one of them is reported as `deferred` and not attempted (transitively), and everything else is applied. That is what keeps the tree order — `attached(P, org)` before `attached(C, P)`, a `moved` before the `detached` of the same node — without letting one tenant freeze the queue for all of them. Until 2.0 the pass stopped at the first failure, and a single change that can never be applied (its parent scope was deleted before the pass) then blocked every later change of every tenant, indefinitely. `sqlScopeOutbox` also **parks** an entry after `maxAttempts` failures (default 5): it stops being retried, it is reported in `dead` on every pass, and the command exits non-zero while any exists — a parked entry is a permanent divergence of the backend's tree, not a resolved incident. The command exits non-zero on any failure too, so a supervisor notices. Applying a queued `detached` runs `purgeScope` and only then removes the edge, and it emits the `scope_purged` audit event at that point, carrying the actor that ordered it.
132
138
 
133
- **The relay is a single writer.** `pending()` reserves nothing, so two passes at once (a Kubernetes `CronJob` with the default `concurrencyPolicy: Allow`, two replicas, a pass that lasts longer than its interval) work on the same batch: the straggler re-applies an old `attached` after the other applied the new `moved`, and the store is left with the **old parent and a single edge, so nothing denounces it** — the old tenant keeps access to a subtree that is no longer theirs (measured). `sqlScopeOutbox.acquire()` takes a lease for the pass — a server-side lock on PostgreSQL and MySQL, a process-wide one on SQLite — and a second simultaneous pass does nothing and says so (`busy`). If you implement the port yourself and skip `acquire`, run the relay one pass at a time.
139
+ **"Was mine applied?" — the receipt and `only` (2.4.0-alpha.5).** `scopes.attached/moved/detached` return a receipt, **`{ queued: [id] }`** — the outbox ids of what they just enqueued (plural on purpose: a `detached` whose row is already gone enqueues one entry per spelling of the uuid). Empty means there is nothing to relay: without an outbox the change was applied inline; with a third-party `ScopeOutbox` whose `enqueue` still returns `void` there is no id, and then there is no `only` either. Hand the receipt to the relay on the interactive path, right after your commit: **`await authorization.relayScopeChanges({ only: queued })`**. That is the fix for the cost a consumer measured: with 500 entries of another tenant ahead in the queue, the inline pass cost **4,507 ms and 2,004 OpenFGA calls** to land one change; with `only` it costs **19 ms and 4 calls**, because the filter lives in the port (`pending(limit, after, { ids })` — the primary key) and the pass never reads the other 500 rows (the suite spies on `pending` to pin that; a port implementation that ignores the filter is 500 `E_AUTHZ_CONFIG` before anything is applied, never a silent drain of the backlog inside your request). **The order by scope key is not relaxed — deferred, never out of order.** Before applying each requested entry the relay probes the *whole* queue (`earlierPending`: one existence query against the `(applied_at, id)` index, 0.69 ms measured; a port without it is paged through `pending()`): if an earlier unapplied entry names one of the same scopes and was not requested, yours is reported `deferred` with `depends-on:<id>` and not applied. Why, measured: with `#1 moved(unit → orgB)` pending and `only: [#2 attached(child, unit)]`, a filter without the probe wrote `child → unit` while the store still said `unit → orgA`, and the editor of the *old* organization authorised over the child (`authorize(A@orgA, docs:read, child) = true`, report all green). One level is enough — the last link of any chain of pending dependencies shares a scope with yours by definition of the key contamination — so the probe costs one query, not the queue. The report is **per entry**: `entries` names every entry the pass looked at, `applied` / `deferred` (reason `depends-on:<id>` or `parked:<id>`) / `failed` / `dead`, with or without `only`; and `missing` names the ids of `only` that were neither pending nor parked — already applied by an earlier pass, or unknown; the relay cannot tell — never an empty report in green. `only: []` is 500 `E_AUTHZ_CONFIG`: an empty `filter` on your side, or a receipt from an outbox without ids, cannot end up draining the whole queue under the lease of an HTTP request. **`remaining` and `dead` are always global**, with or without `only`: a filtered report that kept quiet about a queue that does not converge would be a green report over a broken tree. Three things this is **not**. `only` takes the ids from the receipt your own process just got, **never from the request body** — an endpoint that accepts `only` from a client is policy in an API that has no policy (the relay skips `requireActor`/`requireWithin` on purpose). **There is no filter by tenant or organization**: a `detached` whose row is already gone belongs to no organization (excluding it always is a permanent fail-open — its facts are never purged; including it always is "anyone relays anyone's `detached`"), and a `moved` would be attributed to its *destination*, which is the bias that keeps accesses. And `only` does not pull its dependencies along — that is transitive and ends up being the whole queue; if what you want is "the queue landed up to here", run the relay without `only`. **Do not return the relay report over HTTP**: it enumerates the tree activity of every tenant (ids, scope uuids, actors, the backend's error text); read it, log it, act on `entries`, and hand the client your own answer. Finally, **the inline pass returns a report, it does not throw**: a `deferred` or a `failures` does not abort your request unless you look at it — if `entries` says your change is `deferred`, it has not landed, and that is the fail-closed window described below.
140
+
141
+ **A 503 does not consume an attempt (2.4.0-alpha.5).** `maxAttempts` exists to converge on the *irreparable* — the parent of an enqueued `attached` was deleted and will not come back — and until alpha.4 a backend outage spent the same budget: measured, with the store down, **five requests of another tenant** running the inline relay parked an entry that was not theirs (one attempt per request: the entry at the head of the queue pays for everyone's pass), and from then on its dependants were applied without it. Now the manager tells the two apart by the nature of the error: an `AuthorizationBackendError` — the store did not answer, the deadline expired, any 503 — is reported in `failures` with `transient: true` and passed to `markFailed(id, error, { transient: true })`, and `sqlScopeOutbox` writes `last_error` **without** counting the attempt; a 422, a 500, `E_AUTHZ_SCOPE_TREE_DRIFT`, a missing parent or a bare error from a third-party driver count as before. A port implementation that ignores the third parameter counts everything — exactly the alpha.4 behaviour, degrading towards safe. For the inline recipe, with the numbers: an outage of the store lasting `N` requests leaves `N−1` entries deferred behind the failing head on request `N`, and none of them parked.
142
+
143
+ **A parked entry blocks, by scope key, what grants or re-parents (`attached`, `moved`) and lets through what retires (`detached`) (2.4.0-alpha.5).** Until alpha.4 a parked entry left `pending()` and stopped contaminating, so what depended on it was applied *without* it — for an `attached` behind a parked `moved` that is the store keeping the **old parent, permanently**, the persistent escalation this outbox exists to prevent (measured; and live in a consumer running the inline relay, with no change of theirs involved). Now the keys of `dead()` seed the blocked set at the start of every pass: an `attached` or `moved` naming one of those scopes is reported `deferred` with `parked:<id>`, on every pass, until someone looks at the parked entry (the command names it; the way out is `authz:reconcile --to=<driver>`, which recomputes with today's tree, or a new notification from your side). A `detached` of that key is applied anyway — a purge never waits (invariant 11: a half-applied tree may leave denies *in excess*, never facts alive on a node nobody purges) — **and that holds transitively** (closing lot of alpha.5, auditor 🟠 1): a `detached` behind an `attached` that is itself deferred *because of* a parked entry is applied too, because the manager tracks the *origin* of every key block; only a block born from a real failure of the pass (an entry that threw) still holds a purge back, as it did since 2.0, so the order between a failed `attached` and the purge of that same node is kept. The parked keys are read **in full on every pass, whatever `--limit` or `batchSize`** (🟠 2: a census bounded by `limit` let the 101st parked entry go unnoticed with `--limit 50` or the inline `{ only, limit: 1 }`, and the `attached` landed under the old parent); more than `PARKED_CENSUS_MAX` (50,000) parked entries is a 500 before anything is applied, never a blind plug. The price is declared: one irreparable parked entry freezes the *grants and moves* of its scope key until a human acts — bounded to the key, not the queue, and loud (`deferred` + `dead` + exit code ≠ 0).
144
+
145
+ **When the parked set grows.** Nothing in the package shrinks it for you, on purpose: `authz:reconcile` never touches the outbox (the counter does not go down), `authz:scopes:requeue` has no `--all` (re-queueing an irreparable entry parks it again in one pass, and re-queueing a stale one is the reversal the probe refuses), and past `PARKED_CENSUS_MAX` (50,000) every pass is a 500 before applying anything. The remedy is an operator's SQL on **your** table: list `authz_scope_outbox` rows with `attempts >= maxAttempts` and `applied_at IS NULL`, read `last_error`, and either `authz:scopes:requeue --id` the ones whose cause is gone (a parent that exists again) or **delete** the irreparable ones (a parent that will never exist) — then `authz:reconcile --to=<driver>` rebuilds the tree from today's rows. Deleting a parked entry is safe for the tree only because the reconcile recomputes it; do not delete and skip the reconcile.
146
+
147
+ **Under `only` the purge exemption does not apply yet.** With `relayScopeChanges({ only })` a `detached` whose key has an earlier unapplied entry is reported `deferred` (`depends-on`) even when that entry is itself waiting on a parked one: the purge lands on the next **full** pass. A deployment that only ever relays inline must keep a full pass (the `authz:scopes:relay` cron) or its purges wait for as long as the parked entry does. Scheduled for 2.5 (audit 🟡 of alpha.5).
148
+
149
+ **Requeueing a parked entry: `node ace authz:scopes:requeue --id=<id>` (2.4.0-alpha.5).** A parked entry is never retried on its own. When you have fixed the cause — the missing parent is back, the store was reconciled — `authorization.requeueScopeChanges([id])` (platform API, like the relay; the command is its ace face) returns it to `pending()` with its old id, and **`attempts` is set to `maxAttempts − 1`**, neither `0` nor kept (owner's decision): if the cause is still broken the entry is parked again after **one** pass — and it contaminates its scope key only during that pass, so an automated requeue of something irreparable is not a permanent deferral tap for that tenant's new changes —, and if you fixed it, it lands on the first pass. No new column. It is all or nothing: an id that does not exist, is still pending, or was already applied is a 500 naming it and **no row is touched** (an applied entry never comes back: requeueing an applied `detached` would purge again). **The anti-reversion probe lives in the manager and has no exception.** Before touching the queue it asks the port (`laterApplied`) whether an entry *applied after* the parked one shares a scope key with it; if one does, requeueing would re-apply the old change on top of the new — measured: a parked `moved(unit → orgB)` requeued after `moved(unit → orgC)` was applied leaves the store with the **old** parent and a single edge, so nothing denounces it while the old tenant regains access — and the call is **500 `E_AUTHZ_REQUEUE_REFUSED`** naming the later entry and the key, with the right remedy (`authz:reconcile --to=<driver>`, which recomputes with today's tree). **`--force` exists only in the ace command**: it prints the diff — the parked entry and every applied entry it would revert — and then requeues anyway; `requeueScopeChanges(ids, { force: true })` is what the command calls and nothing reachable over HTTP should. The probe is the manager's, not the table's, so a third-party `ScopeOutbox` gets it for free by implementing the two optional methods (`requeue` and `laterApplied`); without them the command says so (500 `E_AUTHZ_UNSUPPORTED`). Two limits of the probe, written: the diff lists at most 100 later entries per parked one and says when it is cut (`truncated`, "… y más"); and **`laterApplied` only sees the outbox** — `authz:reconcile` does not write to it, so a requeue *after* a reconcile can revert what the reconcile just certified and the probe has nothing to compare against: **do not requeue after reconciling**; if you already did, the remedy is to reconcile again.
150
+
151
+ **The relay is a single writer.** `pending()` reserves nothing, so two passes at once (a Kubernetes `CronJob` with the default `concurrencyPolicy: Allow`, two replicas, a pass that lasts longer than its interval) work on the same batch: the straggler re-applies an old `attached` after the other applied the new `moved`, and the store is left with the **old parent and a single edge, so nothing denounces it** — the old tenant keeps access to a subtree that is no longer theirs (measured). `sqlScopeOutbox.acquire()` takes a lease for the pass — a server-side lock on PostgreSQL and MySQL, a process-wide one on SQLite — and a second simultaneous pass does nothing and says so (`busy`). Since 2.4.0-alpha.5 that is **observed between real processes** (`tests/scope_outbox_multiprocess.spec.ts`: a second process relays the same queue while the first holds the lease ⇒ `busy`, zero writes, on PostgreSQL and MySQL), and the SQLite limitation is pinned the same way — the second process *does* apply over the first: **SQLite is not the engine for two replicas of the relay**. If you implement the port yourself and skip `acquire`, run the relay one pass at a time.
134
152
 
135
153
  **What the outbox does not fix, in plain words.** Between your commit and the relay pass there is a lag of **seconds during which FGA decides with the old tree**. What that costs you depends on the change, and the two directions have **opposite signs** (2.0, the `rooted` relation of the `facts` model — see [What a scope that is not attached grants](#what-a-scope-that-is-not-attached-grants)):
136
154
 
@@ -323,6 +341,7 @@ scopes: {
323
341
  descendantsOf: sqlDescendantsOf({ table: 'org_nodes', uuidColumn: 'uuid', parentColumn: 'parent_uuid', typeColumn: 'kind' }),
324
342
  maxScopes: 1000, // answer bound of authorizedScopes
325
343
  maxDescendants: 10000, // maxNodes handed to descendantsOf
344
+ maxDepth: 22, // optional (2.4.0-alpha.5): refuse attached/moved past this many parent hops from app — see Limits
326
345
  }
327
346
  ```
328
347
 
@@ -696,6 +715,7 @@ Every error the package raises carries `status` and `code`. A standard AdonisJS
696
715
  | `E_AUTHZ_WITHIN_REQUIRED` | 422 | `requireWithin` set and a write without `within` (2.1) |
697
716
  | `E_AUTHZ_WITHIN_ROOT_FORBIDDEN` | 422 | `requireWithin: 'non-root'` and `within: APP_SCOPE` (2.1) |
698
717
  | `E_AUTHZ_ACTOR_REQUIRED` | 422 | `requireActor: true` and a write without `actor` (2.1); on the relations port (`relations.requireActor ?? requireActor`) the four writes, `purgeObject`/`purgeSubject` included since 2.4.0-alpha.3 |
718
+ | `E_AUTHZ_SCOPE_DEPTH_LIMIT` | 422 | `scopes.attached`/`scopes.moved` with `scopes.maxDepth` declared: the child would end up more than `maxDepth` `parent` hops from `app` (the parent's chain + 1). Refused **before** writing or enqueueing; the gate is opt-in and partial — see [Limits](#limits) (2.4.0-alpha.5) |
699
719
  | `E_AUTHZ_TOO_MANY_SCOPES` | 422 | `authorizedScopes`/`expandExcludedSubtrees` over `maxScopes`, or `descendantsOf` over `maxNodes` (`sqlDescendantsOf`: also a possible cycle) — never a partial list (2.1) |
700
720
  | `E_AUTHZ_BACKEND_UNAVAILABLE` | 503 | facts backend or SQL catalog did not answer (both drivers, catalog sync/diff and the `authz_catalog_version` check included); the version row is missing or unreadable ("migración 2.0 no aplicada": fail-closed, never version 0); a per-check `error` in an OpenFGA `batchCheck` |
701
721
  | `E_AUTHZ_BACKEND_TIMEOUT` | 503 | `timeoutMs` elapsed (subclass of the above) |
@@ -703,7 +723,7 @@ Every error the package raises carries `status` and `code`. A standard AdonisJS
703
723
  | `E_AUTHZ_FREEZE_HELD` | 423 | `freeze()` (or a second `authz:reconcile`) found a live freeze owned by someone else: two windows never interleave, and only the owner's token — or `authz:unfreeze` — lifts one. The message names the holder, the reason and the fence |
704
724
  | `E_AUTHZ_RESOLVER_FAILED` | 503 | your `resolveChain`, `parentOf` or `descendantsOf` threw or answered a malformed scope; `descendantsOf` and `resolveChain` disagree in `authorizedScopes`; a subtree to exclude cannot be enumerated |
705
725
  | `E_AUTHZ_WRITE_CONFLICT` | 409 | an `openfga` write kept clashing with another transaction over the same tuples (FGA answers `Aborted`/409, or 400 "cannot write a tuple which already exists"): the driver re-reads and re-applies, and only gives up after three rounds. The backend answered, so this is never a 503 — retry the write. Also (2.4.0-alpha.2, lot L-3) a `database` `grant`/`deny` **inside your transaction** (`{ transaction }`) that hits the UNIQUE because another transaction committed the same fact while yours was open: outside a transaction the driver re-reads and keeps the winner's row; inside yours it cannot (PostgreSQL has already aborted your transaction — every later statement is `25P02` until you roll back — and under REPEATABLE READ the re-read would not see the winner), so it **poisons your transaction**: roll back and retry. Measured on all three engines, see [writing inside your transaction](#writing-inside-your-transaction-240-alpha2). The same for a `database` `relate`/`unrelate`/`purge*` inside your transaction (lot L-4) — on a UNIQUE clash of a tuple (userset, and since lot L-4b holder too: `subject_relation` is `''`, never `NULL`), and on a **deadlock** (two transactions writing two tuples in crossed order: PostgreSQL `40P01`, MySQL `1213`; the engine's victim gets this 409, never a hang nor the raw error) |
706
- | `E_AUTHZ_CONFIG` | 500 | contradictory config (`holderTypes` not injective or a holder type not declared in it, `scopes.*` without resolver, `appAccess` without `permission`, `catalog` together with `catalogRevalidate`, an invalid `maxAgeMs`); `bumpAuthzCatalogVersion` called without the writing transaction's client; a `{ transaction }` handed to `sqlScopeOutbox` that is not an open Lucid transaction of the outbox's own connection (another connection, a `QueryClient`, or the whole `db` — the message names both connections); `requireTransactionalWrites: true` (root, or `relations.requireTransactionalWrites`) with an active driver that does not declare `transactionalWrites: true` — raised **when the driver is resolved**, so reads fail too and the deployment does not start (2.4); a `relations.assertWrite` that returns a promise — or any value at all: `false`, `true`, a thenable function (it must be synchronous and it does not return verdicts: it throws or it does not; the write is refused before the driver, 2.4.0-alpha.3) |
726
+ | `E_AUTHZ_CONFIG` | 500 | `relayScopeChanges({ only: [] })`, a `ScopeOutbox` that ignores `pending(limit, after, { ids })` and returns an entry that was not asked for, `requeueScopeChanges` with an id that is not parked (or an empty list), or a `scopes.maxDepth` that is not an integer ≥ 1 (2.4.0-alpha.5); contradictory config (`holderTypes` not injective or a holder type not declared in it, `scopes.*` without resolver, `appAccess` without `permission`, `catalog` together with `catalogRevalidate`, an invalid `maxAgeMs`); `bumpAuthzCatalogVersion` called without the writing transaction's client; a `{ transaction }` handed to `sqlScopeOutbox` that is not an open Lucid transaction of the outbox's own connection (another connection, a `QueryClient`, or the whole `db` — the message names both connections); `requireTransactionalWrites: true` (root, or `relations.requireTransactionalWrites`) with an active driver that does not declare `transactionalWrites: true` — raised **when the driver is resolved**, so reads fail too and the deployment does not start (2.4); a `relations.assertWrite` that returns a promise — or any value at all: `false`, `true`, a thenable function (it must be synchronous and it does not return verdicts: it throws or it does not; the write is refused before the driver, 2.4.0-alpha.3) |
707
727
  | `E_AUTHZ_ROLE_IS_NOT_ACCESS` | 500 | `appAccess({ role })` |
708
728
  | `E_AUTHZ_INTERNAL` | 500 | package invariant violated (empty scope set on a write, misaligned batch, a third-party `authorizeMany` answering the wrong shape, a `Read` continuation token that never advances or more than 10,000 pages, a corrupt `assignable_at`/`owner_scope_key` row) |
709
729
  | `E_AUTHZ_PURGE_INCOMPLETE` | 500 | `purgeScope` could not prove zero; on the relations port, `purgeObject`/`purgeSubject` of `openfga` could not prove zero after deleting — the error carries the `markPartialWrite` mark and the event was emitted with `indeterminate: true` (2.4.0-alpha.3) |
@@ -715,6 +735,7 @@ Every error the package raises carries `status` and `code`. A standard AdonisJS
715
735
  | `E_AUTHZ_MODEL_TOO_LARGE` | 500 | the catalog does not fit in an OpenFGA authorization model (262,144 **bytes** — around **450 permissions with realistic slugs**; see [the model ceiling](#the-model-ceiling-is-bytes-not-permissions)): checked in `syncAuthzCatalog` **before** writing, with a warning past 80 % |
716
736
  | `E_AUTHZ_RELATION_CONFIG` | 422 | a `relations/` object type or relation cannot be **fused** into the shared `facts` model: it duplicates a reserved `facts` type (`scope`/`role`/`role_binding`/`deny_binding`/`group`) or relation family (`can_<P>`/`denied_<P>`/`permits_<P>`/`parent`/`rooted`/`assignee`…), or a relation name collides with a catalog permission (F-04). The generator raises it — not OpenFGA's opaque 400 — because in the shared store the id-spaces overlap |
717
737
  | `E_AUTHZ_SCOPE_DRIFT_UNGUARDED` | 500 | the `openfga` driver was constructed without `scopes.outbox` and without `acceptScopeDriftRisk: true`. Thrown at construction: a rollback of your transaction would otherwise leave the store's tree ahead of yours, and that escalation is invisible from your database |
738
+ | `E_AUTHZ_REQUEUE_REFUSED` | 500 | `requeueScopeChanges` / `authz:scopes:requeue`: an entry applied *after* the parked one shares a scope key with it, so requeueing would re-apply the old change on top of the new (the store reverted, with a single edge). Nothing was requeued; the error carries `conflicts` (the diff); `authz:reconcile --to=<driver>` is the remedy and `--force` (ace command only) the override (2.4.0-alpha.5) |
718
739
  | `E_AUTHZ_SCOPE_TREE_DRIFT` | 500 | the materialized tree has more than one `parent` edge for the same scope: someone else writes to the store. A write never "fixes" it by guessing; `authz:reconcile` rebuilds the tree from yours and reports the scope |
719
740
  | `E_AUTHZ_NO_DESCENDANTS_RESOLVER` | 500 | `authorizedScopes`/`expandExcludedSubtrees` without `scopes.descendantsOf` |
720
741
  | `E_AUTHZ_VIEW_EXPIRED` | 500 | a `forRequest()` view used to read (`expandExcludedSubtrees` included) after its `maxAgeMs` (default 30 s, monotonic clock) |
@@ -765,6 +786,22 @@ Every driver **declares** this through `driver.capabilities`, and each declared
765
786
 
766
787
  **The anti-cycle checks are the package's, in both modes, and they are not optional.** Measured against OpenFGA v1.19: the server *accepts* an edge that closes a cycle, does not hang, answers in 2–7 ms, and from then on inheritance runs both ways — a grant on a descendant grants on its ancestor, and with the root inside the cycle it grants everywhere. Nothing is logged and there is no error to catch. That is why `child ≠ app`, "the parent exists" and `child ∉ ancestors(parent)` are checked before anything is written (422, no edge), and why you should not expect the backend to be a second line of defence.
767
788
 
789
+ ### Reading capabilities at runtime (2.4.0-alpha.4)
790
+
791
+ The table above is what each driver **declares**; your service code can read it from the manager instead of guessing from the driver name — and instead of calling `driver()`:
792
+
793
+ ```ts
794
+ // The same service code serves both drivers, without a single driver() in it.
795
+ if ((await authorization.capabilities()).transactionalWrites) {
796
+ await authorization.grant(user, 'editor', unit, { transaction: trx }) // both or neither, with YOUR transaction
797
+ } else {
798
+ await authorization.grant(user, 'editor', unit) // openfga: the recipe by direction
799
+ }
800
+ if (relations.capabilities().transactionalWrites) { /* relate(…, { transaction: trx }) */ }
801
+ ```
802
+
803
+ `authorization.capabilities()` is `async` — it resolves the active driver through `driver()`, so it pays the driver factory once (cached from then on) and **inherits its two gates**: with `requireTransactionalWrites: true` and a driver that does not comply it is the same 500 `E_AUTHZ_CONFIG` the deployment already fails with, and a `facts` driver without `scopes.outbox` or the signature is the same 500 `E_AUTHZ_SCOPE_DRIFT_UNGUARDED`. That is deliberate: capabilities belong to the *instance* (`database` decides `transactionalWrites` from its options), and a reading that resolved the driver by any other path would leave `driver()` serving an instance that never passed the gates. Call it once at boot (a `booted`/`ready` hook), not per request. `relations.capabilities()` is synchronous: the relations driver is built eagerly and its gate already ran. Both return a **read-only, frozen, normalised copy** typed as `AuthorizationDriverCapabilities` / `RelationsDriverCapabilities` (both exported): every known key is present, only `true` is `true` — the same reading the write gates apply — so a third-party driver with partial or no `capabilities` reads as `false`, **never `undefined`**; mutating the copy throws in strict mode and changes nothing. The read does not soften anything: `{ transaction }` to a driver that declares `false` is still 500 `E_AUTHZ_UNSUPPORTED` whether or not you asked first, and **`driver()` is still platform API** — tenant code should not call it, and a code review can keep grepping for it.
804
+
768
805
  ## Performance
769
806
 
770
807
  Two optimisations landed in 2.1, both measured and both **without changing a single answer** (the contract suite is the proof: same cases, both drivers, before and after). Reproduce the numbers with `OPENFGA_TEST_URL=http://localhost:8101 node --import @poppinss/ts-exec scripts/bench_authorize.mjs` (chain of 3 scopes through your resolver, 5 roles per level, 20 permissions, N=200 after 30 warm-up calls, HTTP round-trip included; OpenFGA v1.19.0 on the same machine):
@@ -887,6 +924,33 @@ What passing means: **for everything the suite covers, both drivers answer the s
887
924
 
888
925
  The package runs that suite on itself: `npm test` judges the `database` driver over in-memory SQLite — no host application — and `OPENFGA_TEST_URL=… npm test` adds the `openfga` driver to the same verdict. `npm run test:pg` and `npm run test:mysql` run the **same** suite over PostgreSQL 18 and MySQL 8.4 (`TEST_PG_URL` / `TEST_MYSQL_URL`; each run creates a database with a random suffix and drops it), `npm run test:sqlite-file` over a SQLite file with a pool of 2–5 connections (real connection-level concurrency: a case pins `pool.max ≥ 2` and a read that answers while another connection holds an open transaction — *"una lectura responde mientras OTRA conexión mantiene una transacción abierta"*; the two-concurrent-grants case itself is a JavaScript check-then-insert race and dies with a single connection too). On PostgreSQL and MySQL the judge additionally runs with the scope tree in a real SQL table (`hierarchicalScopeResolver` + `sqlDescendantsOf` over `demo_scopes`), which is where the uuid-alias bypass lived. CI runs all of it: SQLite in memory and as a file, PostgreSQL and MySQL, each with and without OpenFGA, plus a second OpenFGA server with `ListObjects`/`ListUsers` capped at 3; a case also checks that the child process the suite spawns leaves no database behind. Two capability pairs are exercised on both drivers: `listDenies` and **`injectableClock`** (`true` ⇒ the judge fixes the instant through `withClock(now)` and observes exact expiry, renewal and "expires right now" without waiting; `false` ⇒ it can only observe the three states of `expiresAt` in real time, with a 1.5 s wait).
889
926
 
927
+ ### The outbox contract: `runScopeOutboxContract` (2.4.0-alpha.5)
928
+
929
+ The `ScopeOutbox` port has duties nobody judged until alpha.5 — the receipt, oldest first, `after` as a continuation, an idempotent `markApplied`, a `markFailed` that counts and keeps the row, the actor that travels — and since alpha.5 every optional method is a **capability pair**: a `true` face that judges the duty and a `false` face that judges that the manager *says so* (or that the implementation degrades towards safe), never a skip. A third-party outbox proves itself the same way the package's does:
930
+
931
+ ```ts
932
+ import { runScopeOutboxContract } from '@jantstack/adonis-authz/testing'
933
+
934
+ runScopeOutboxContract({
935
+ name: 'my-outbox',
936
+ capabilities: {
937
+ transactionalEnqueue: true, // enqueue writes through context.transaction: rollback ⇒ nothing pending (needs `transactions`)
938
+ filter: true, // pending(limit, after, { ids }) filters in the port; false ⇒ `only` is 500 E_AUTHZ_CONFIG
939
+ transient: true, // markFailed(..., { transient: true }) does not count; false ⇒ it counts anyway
940
+ dead: true, // dead() exists: parked after limits.maxAttempts; false ⇒ nothing is ever parked
941
+ acquire: true, // acquire() is a held, exclusive lease; false ⇒ the relay never says busy
942
+ earlierPending: true, // the dependency probe of `only`; false ⇒ the manager pages pending(), same verdict
943
+ requeue: true, // requeue + laterApplied (needs dead); false ⇒ requeueScopeChanges is 500 E_AUTHZ_UNSUPPORTED
944
+ },
945
+ limits: { maxAttempts: 3 }, // with dead: true — what your outbox parks at (≥ 3)
946
+ makeOutbox: () => myOutbox({ maxAttempts: 3 }), // fresh, over an empty queue
947
+ cleanup: () => emptyMyQueue(),
948
+ transactions: { begin: async () => openMyTransaction() }, // with transactionalEnqueue: true
949
+ })
950
+ ```
951
+
952
+ Fourteen cases per harness: seven core duties and one face per capability. Declaring `dead: true` without `limits.maxAttempts`, `requeue: true` without `dead: true`, or `transactionalEnqueue: true` without `transactions` throws at registration — a promise without a judge does not pass. The package runs it on `sqlScopeOutbox` over the four engines and on an in-memory double (`makeScopeOutbox`, exported too) with all capabilities and with none, so both faces of every pair run on every `npm test`.
953
+
890
954
  ## OpenFGA tooling
891
955
 
892
956
  ```bash
@@ -926,6 +990,8 @@ This is the **only** migration and verification primitive of the package, and th
926
990
 
927
991
  **It needs `scopes.enumerateEdges`** in your config: the whole tree, paginated with a cursor. `sqlScopeEdges({ table, uuidColumn, parentColumn, typeColumn })` implements it over a table with a parent column, exactly like `sqlDescendantsOf`. Without it the command refuses (500 `E_AUTHZ_CONFIG`) instead of assuming a flat tree — a flat tree would be an invented hierarchy, and an invented hierarchy grants.
928
992
 
993
+ **`sqlScopeEdges` covers ONE table, and concatenating several does not cover a tree that lives in more than one (2.4.0-alpha.4).** The helper does a `LEFT JOIN` of the table *against itself* on `parentColumn = uuidColumn`, and a row whose declared parent has no match in *that* table is **skipped in silence** — that is the documented "a parent that does not exist is not emitted", and it has no way to tell "orphan" from "the parent lives in another table". So if your tree is spread over several tables (say `organizations` → `organization_units` → `ia_spaces`), the obvious composition — one `sqlScopeEdges` per table, drained one after the other — **does not emit the edges between tables**: every `ia_space → organization_unit` and `organization_unit → organization` edge is dropped without a word, and the parent's *type* would come out wrong anyway (it is the child table's fixed `scopeType`). With `authz:reconcile --to=openfga` that is not "some edges are missing": the destination sees them as surplus and deletes them, and by invariant 9 (`can_<P>` requires `rooted`) **the whole subtree denies**. Fail-closed, noisy, nobody escalates — and a day lost. Write your own `enumerateEdges` instead: one query that concatenates the tables (`UNION ALL` of `(child_uuid, child_type, parent_uuid, parent_type)` across the tables, `parent IS NULL` meaning `app`), ordered by a total and stable key (the child uuid works) with the cursor over that key, and emitting an edge only when the parent exists in *any* of the tables; then compare with `authz:reconcile --to=<driver> --dry-run` before the first real pass. Alternatively chain the tables in a fixed order and encode *which table* inside the opaque cursor — the contract only asks for a total, stable order across calls. *Recipe not verified by the suite* (it has no multi-table fixture); a composition helper (`composeScopeEdges`, or `parentTable` on `sqlScopeEdges`) is deferred to 2.6.
994
+
929
995
  What one pass does, in order: the **root marker** (`scope:app#rooted`, without which the whole store denies), the **catalog projection** (`role:<uuid>#permits_<P>`, read with the very same function `syncAuthzCatalog` uses), the **tree** from `enumerateEdges`, and the **facts** from `authz_assignments` / `authz_denies` — each assignment as its `assignee` tuple plus the two (c2) edges, each deny as `scope:<key>#denied_<P>`. It is **idempotent** (a second pass writes zero — the word the requirement used, and it has its case), **resumable** (the source is read in batches of 100 with a cursor over the primary key; repeating a pass converges) and **never silent**: the report carries `{ written, updated, unchanged, extra, deleted, skipped{reason} }` per phase, plus each row that did not migrate with its reason (`unknown-scope`, `unknown-role`, `unknown-permission`, `unknown-holder-type`, `expired`, `role-not-visible`, `cycle`, …).
930
996
 
931
997
  **The source is read in one consistent snapshot — in the direction where the source is `authz_*` (2.3).** The two sweeps (`authz_assignments`, then `authz_denies`) used to run on the global connection, one after the other, so the gap between them — the time it takes to walk the first table in batches of 100 — let *composite* business operations slip through. An offboarding is `revoke` + `removeDeny`: landing in that gap, the pass kept one half of each and wrote **the role without its deny** into the destination, granting a permission that **neither the previous nor the following state granted**, while the report said `written=13 extra=0 skipped={} clean=true`. That is not a lost permission, it is a fabricated one — an escalation, with nothing in the report to distrust. Both sweeps now run inside **one repeatable-read transaction**, so the worst outcome of the window is *the consistent state of `t0`* — recoverable drift that the next pass repairs — instead of a state that never existed. What each engine guarantees is not the same and is stated rather than assumed: PostgreSQL takes the snapshot with `BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ`; MySQL/InnoDB is sent `SET TRANSACTION ISOLATION LEVEL REPEATABLE READ` explicitly (it is InnoDB's default, but a server setting is not a promise of this package) and fixes the consistent read on the first query; SQLite takes no isolation level at all (knex warns and ignores it) because a read transaction is already a snapshot there, so the package does not send it one.
@@ -1012,6 +1078,29 @@ legal for the `database` driver, which has no such ceiling — the same tree is
1012
1078
  503 in the other, and it is a DoS within reach of whoever can nest sub-scopes. Raise
1013
1079
  `OPENFGA_RESOLVE_NODE_LIMIT` on the server if your tree is deeper.
1014
1080
 
1081
+ ### Limits
1082
+
1083
+ Every bound the package enforces or measured, in one place, each with the constant or option that carries it — the suite imports those constants and asserts that **these numbers are the ones in the code** (a constant that changes without this table turns red). What lies past each bound is written next to it, because "past the limit" is never a silent `false` here.
1084
+
1085
+ | Limit | Constant / option | Value | Where | Past it |
1086
+ |---|---|---|---|---|
1087
+ | Scope-chain depth the `facts` model resolves reliably (`parent` hops from a scope to `app`) | `FACTS_MAX_RESOLVE_DEPTH` | 22 | `openfga` only, `database` has no ceiling | `authorize` is 503, never `false`, for the whole subtree below (measured: 23 fails between 4 % and 26 % of the time, 24 always). Raise `OPENFGA_RESOLVE_NODE_LIMIT` on the server if your tree is deeper — and declare `scopes.maxDepth`, below |
1088
+ | Depth gate on writes (opt-in, 2.4.0-alpha.5) | `scopes.maxDepth` | none by default; set it to 22 for `facts` | the manager, `scopes.attached`/`scopes.moved`, every driver | 422 `E_AUTHZ_SCOPE_DEPTH_LIMIT` **before** writing or enqueueing; see below |
1089
+ | Authorization-model size | `FACTS_MODEL_MAX_BYTES` | 262,144 | `openfga`, `syncAuthzCatalog` | 500 `E_AUTHZ_MODEL_TOO_LARGE` before writing, warning past 80 % (`FACTS_MODEL_WARN_RATIO`); how many permissions those bytes are is [not a property of the model](#the-model-ceiling-is-bytes-not-permissions) |
1090
+ | Checks per `batchCheck` | `FGA_MAX_BATCH_CHECK` | 50 | `openfga`, `authorizeMany` | the driver splits into batches of 50 (the SDK's cap), nothing to do |
1091
+ | `Read` page size (enumerations, `enumerateFacts`, `enumerateRelations`) | — | 100 | `openfga` | the server's hard cap: `pageSize: 1000` is a 400; the driver pages until the continuation token is empty (a token that repeats, or more than 10,000 pages, is 500 `E_AUTHZ_INTERNAL`) |
1092
+ | `enumerateRelations(partition)` cost | — | O(tuples in the store) per partition and per call | `openfga`, the *source* of `authz:relations:reconcile` | there is no prefix `Read` in OpenFGA (a prefix object answers 200 with **zero** tuples, measured: never "optimise" it that way); measured 15,000 tuples ⇒ 151 pages ≈ 0.5 s whatever the partition size, and K partitions cost K sweeps. Run it from a job or a cron, **never from a tenant request**; a single sweep for K partitions is scheduled for 2.5 |
1093
+ | Scopes a `pending()` pass reads per round trip | `DEFAULT_RELAY_BATCH` | 100 | `authz:scopes:relay` (`--batch-size`) | the pass keeps paging |
1094
+ | Changes one relay pass applies | `DEFAULT_RELAY_LIMIT` | 10,000 | `authz:scopes:relay` (`--limit`) | the rest stays pending (`remaining: true`); run again |
1095
+ | Failed attempts before an outbox entry is parked | `maxAttempts` | 5 | `sqlScopeOutbox` | the entry leaves `pending()`, shows in `dead()` on every pass and the command exits ≠ 0; a 503 of the backend does not count; `authz:scopes:requeue --id` brings it back |
1096
+ | Parked outbox entries the relay can census per pass | `PARKED_CENSUS_MAX` | 50,000 | the manager, every relay pass (read in full, whatever `--limit`) | 500 `E_AUTHZ_CONFIG` before anything is applied — the relay of **every** tenant stops, loudly, never a blind plug. Measured cost of the census under the lease: 1,000 parked ⇒ 16 ms, 10,000 ⇒ 69 ms, 49,000 ⇒ 199 ms per pass. The way out is below (*When the parked set grows*) |
1097
+ | Answer size of `authorizedScopes` / `expandExcludedSubtrees` | `maxScopes` | 1,000 | the manager | 422 `E_AUTHZ_TOO_MANY_SCOPES`, never a partial list (per call it can only be lowered) |
1098
+ | Nodes one `descendantsOf` call may return | `maxDescendants` | 10,000 | the manager (`maxNodes` handed to your resolver) | 422 `E_AUTHZ_TOO_MANY_SCOPES`; both bounds are capped at `MAX_SCOPE_BOUND` (10,000,000) |
1099
+ | Local roles a `prune-orphans` pass may read | `maxLocalRoles` | 10,000 | `pruneOrphanRoles` | 500 `E_AUTHZ_TOO_MANY_LOCAL_ROLES`, never a partial list |
1100
+ | Ancestors `hierarchicalScopeResolver` walks | `maxDepth` (resolver option) | 64 in the README example | your `resolveChain` | 500 `E_AUTHZ_SCOPE_TOO_DEEP`, never a truncated chain |
1101
+
1102
+ **`scopes.maxDepth` — the depth gate (2.4.0-alpha.5).** Declare it in `config.scopes` and `scopes.attached`/`scopes.moved` refuse, with **422 `E_AUTHZ_SCOPE_DEPTH_LIMIT`**, any child that would end up more than `maxDepth` `parent` hops from `app` — the parent's chain plus one — **before writing or enqueueing** (zero driver calls, zero outbox rows) and at zero extra I/O: the parent's chain is already resolved for the cycle check. Without the option there is no gate: a tree of any depth stays legal, and the `database` driver pays nothing. It lives in the config and not in `capabilities()` because a capability is a boolean by doctrine (`capabilities()` normalises with `=== true`); and it is the same knob `authz:reconcile` uses to report chains above the ceiling as drift (`drift.tooDeep`, exit ≠ 0 — a scope legal in `database` and a 503 in `openfga` belongs in CI, not in the first tenant that reaches it; without the option the verifier uses `FACTS_MAX_RESOLVE_DEPTH`). Set it to `22` for the `facts` model: then every question stays within the measured ceiling whichever ancestor carries the binding. **The gate is **partial**, and sold as such**: it looks at the *destination* of the child, not at the height of the subtree a `moved` drags along — a subtree of height *h* moved under a parent at depth *d* leaves its leaves at *d + h + 1*, and the gate does not see them; measuring that would need `descendantsOf` on the write path, which is forbidden. The reconcile report is what catches those afterwards. Why a gate at all: past the ceiling the model answers 503 for the whole subtree (a DoS within reach of whoever can nest sub-scopes), and the cost of each relayed `attached` grows with the depth of its neighbour (measured ×5.8 from depth 1 to 34) on the queue every tenant shares.
1103
+
1015
1104
  ### Operational notes for the SQL engines
1016
1105
 
1017
1106
  The published migration (`stubs/migration.stub`) carries three decisions that were **observed** by running the suite on PostgreSQL and MySQL, not guessed — each one was a red test first — plus, since 2.2, `authz_roles.owner_scope_key` (`varchar(80)`, byte-wise like the other identity columns, `DEFAULT 'global'`, in the role unique index) and `authz_permissions.assignable_at` (see [Scoped roles](#scoped-roles-22)). The suite also **executes** the migration on a scratch database of each engine and compares what the engine reports for every column (type, length, precision, nullability, collation) with the schema the tests run on (*"el esquema que CONSTRUYE el stub y el espejo del harness son el mismo"*).
@@ -10,9 +10,11 @@ import type { ReconcileReport } from '../src/types.js';
10
10
  * - en `--dry-run`, **cualquier cosa que la pasada haría** (escribir,
11
11
  * actualizar o borrar): es el verificador, y para eso está en CI;
12
12
  * - siempre, lo que una pasada NO puede arreglar sola: **ciclos** en el
13
- * árbol del consumidor, **entradas aparcadas** de la outbox y hechos que
14
- * se quedan fuera (`skipped`) — incluidos los del scope que ya no
15
- * resuelve, que solo se van con `--prune`.
13
+ * árbol del consumidor, **entradas aparcadas** de la outbox, **cadenas
14
+ * por encima del techo de profundidad** (`drift.tooDeep`, alpha.5 · R0: un
15
+ * 503 esperando en `facts`) y hechos que se quedan fuera (`skipped`) —
16
+ * incluidos los del scope que ya no resuelve, que solo se van con
17
+ * `--prune`.
16
18
  *
17
19
  * La ventana del relay (`pendingRelay`) se AVISA pero no tumba la pasada: es
18
20
  * una ventana de segundos por diseño, no una divergencia. Y **`expired`
@@ -1 +1 @@
1
- {"version":3,"file":"authz_reconcile.d.ts","sourceRoot":"","sources":["../../commands/authz_reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAS,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG;IACvD,KAAK,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACjF,KAAK,EAAE,OAAO,CAAA;CACf,CAyIA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,WAAW;IACrD,MAAM,CAAC,WAAW,SAAoB;IACtC,MAAM,CAAC,WAAW,SAAqG;IAEvH,MAAM,CAAC,OAAO,EAAE,cAAc,CAE7B;IAGO,EAAE,EAAE,MAAM,GAAG,SAAS,CAAA;IAKtB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IAGxB,MAAM,EAAE,OAAO,GAAG,SAAS,CAAA;IAG3B,KAAK,EAAE,OAAO,GAAG,SAAS,CAAA;IAM1B,eAAe,EAAE,OAAO,GAAG,SAAS,CAAA;IAGpC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAM7B,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAE/B,GAAG;CA6CV"}
1
+ {"version":3,"file":"authz_reconcile.d.ts","sourceRoot":"","sources":["../../commands/authz_reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAS,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG;IACvD,KAAK,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACjF,KAAK,EAAE,OAAO,CAAA;CACf,CAoJA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,WAAW;IACrD,MAAM,CAAC,WAAW,SAAoB;IACtC,MAAM,CAAC,WAAW,SAAqG;IAEvH,MAAM,CAAC,OAAO,EAAE,cAAc,CAE7B;IAGO,EAAE,EAAE,MAAM,GAAG,SAAS,CAAA;IAKtB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IAGxB,MAAM,EAAE,OAAO,GAAG,SAAS,CAAA;IAG3B,KAAK,EAAE,OAAO,GAAG,SAAS,CAAA;IAM1B,eAAe,EAAE,OAAO,GAAG,SAAS,CAAA;IAGpC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAM7B,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAE/B,GAAG;CA6CV"}
@@ -17,9 +17,11 @@ import { BaseCommand, flags } from '@adonisjs/core/ace';
17
17
  * - en `--dry-run`, **cualquier cosa que la pasada haría** (escribir,
18
18
  * actualizar o borrar): es el verificador, y para eso está en CI;
19
19
  * - siempre, lo que una pasada NO puede arreglar sola: **ciclos** en el
20
- * árbol del consumidor, **entradas aparcadas** de la outbox y hechos que
21
- * se quedan fuera (`skipped`) — incluidos los del scope que ya no
22
- * resuelve, que solo se van con `--prune`.
20
+ * árbol del consumidor, **entradas aparcadas** de la outbox, **cadenas
21
+ * por encima del techo de profundidad** (`drift.tooDeep`, alpha.5 · R0: un
22
+ * 503 esperando en `facts`) y hechos que se quedan fuera (`skipped`) —
23
+ * incluidos los del scope que ya no resuelve, que solo se van con
24
+ * `--prune`.
23
25
  *
24
26
  * La ventana del relay (`pendingRelay`) se AVISA pero no tumba la pasada: es
25
27
  * una ventana de segundos por diseño, no una divergencia. Y **`expired`
@@ -112,6 +114,14 @@ export function reconcileLines(report) {
112
114
  message: `${report.drift.deadRelay} cambio(s) del árbol APARCADOS en la outbox: eso no es una ventana, es divergencia permanente.`,
113
115
  });
114
116
  }
117
+ for (const scope of report.drift.tooDeep ?? []) {
118
+ lines.push({
119
+ level: 'error',
120
+ message: `${scope} está a MÁS de ${report.drift.tooDeepLimit ?? '?'} saltos de app: el modelo facts no resuelve esa cadena de ` +
121
+ 'forma fiable (authorize responde 503, nunca false, para todo ese subárbol). Es legal en database y una caída en ' +
122
+ 'openfga. Aplana el árbol, o declara scopes.maxDepth (y sube OPENFGA_RESOLVE_NODE_LIMIT en el servidor).',
123
+ });
124
+ }
115
125
  if (report.frozen?.lapsed) {
116
126
  lines.push({
117
127
  level: 'error',
@@ -146,6 +156,8 @@ export function reconcileLines(report) {
146
156
  const clean = report.cycles.length === 0 &&
147
157
  report.drift.deadRelay === 0 &&
148
158
  report.drift.multiParent.length === 0 &&
159
+ // Una cadena por encima del techo es un 503 esperando (alpha.5 · R0): deriva.
160
+ (report.drift.tooDeep?.length ?? 0) === 0 &&
149
161
  drifting.length === 0 &&
150
162
  // La garantía del freeze se DEMUESTRA (3b-7, juez C4): un lease perdido a
151
163
  // mitad es una ventana en la que otros pudieron escribir ⇒ exit ≠ 0.
@@ -1 +1 @@
1
- {"version":3,"file":"authz_reconcile.js","sourceRoot":"","sources":["../../commands/authz_reconcile.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"authz_reconcile.js","sourceRoot":"","sources":["../../commands/authz_reconcile.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,21 @@ import { CommandOptions } from '@adonisjs/core/types/ace';
11
11
  * node ace authz:scopes:relay # drena
12
12
  * node ace authz:scopes:relay --dry-run # solo dice qué haría
13
13
  * node ace authz:scopes:relay --limit 500 # una pasada acotada
14
+ * node ace authz:scopes:relay --only=12,13 # SOLO esas entradas (2.4.0-alpha.5)
15
+ *
16
+ * **`--only`** (alpha.5 · R1): releva solo los ids del recibo que devolvió
17
+ * `scopes.attached/moved/detached` (`{ queued }`). El orden por clave no se
18
+ * relaja: una entrada pedida que dependa de una anterior sin aplicar y no
19
+ * pedida sale «aplazada» (`depends-on:<id>`), nunca fuera de orden; `--only`
20
+ * vacío es un error, no «todo». La salida es POR ENTRADA (`entries`), y los
21
+ * ids que no estaban ni pendientes ni aparcados se dicen (`missing`).
22
+ *
23
+ * **Lo aparcado tapona por clave** (alpha.5 · R2a): un `attached`/`moved`
24
+ * que nombre un scope de una entrada APARCADA sale «aplazado» con
25
+ * `parked:<id>` en todas las pasadas hasta que alguien mire la aparcada; un
26
+ * `detached` de esa clave se aplica igual (una purga nunca queda bloqueada).
27
+ * Y un 503 del backend NO consume intento: la caída del store es común a
28
+ * todas las entradas y no dice nada de ninguna.
14
29
  *
15
30
  * Pensado para un supervisor (un bucle, un cron corto, un worker): la pasada
16
31
  * es REANUDABLE —lo que no entra sigue pendiente—. Un cambio que falla
@@ -42,6 +57,9 @@ export default class AuthzScopesRelay extends BaseCommand {
42
57
  dryRun: boolean | undefined;
43
58
  limit: number | undefined;
44
59
  batchSize: number | undefined;
60
+ only: string[] | undefined;
61
+ /** `--only=12,13` o `--only=12 --only=13`: ids numéricos como número, el resto como cadena. */
62
+ static parseOnly(values: string[] | undefined): Array<string | number> | undefined;
45
63
  run(): Promise<void>;
46
64
  }
47
65
  //# sourceMappingURL=authz_scopes_relay.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"authz_scopes_relay.d.ts","sourceRoot":"","sources":["../../commands/authz_scopes_relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAS,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,WAAW;IACvD,MAAM,CAAC,WAAW,SAAuB;IACzC,MAAM,CAAC,WAAW,SAAiF;IAEnG,MAAM,CAAC,OAAO,EAAE,cAAc,CAE7B;IAMO,MAAM,EAAE,OAAO,GAAG,SAAS,CAAA;IAG3B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;IAGzB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAE/B,GAAG;CAkFV"}
1
+ {"version":3,"file":"authz_scopes_relay.d.ts","sourceRoot":"","sources":["../../commands/authz_scopes_relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAS,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,WAAW;IACvD,MAAM,CAAC,WAAW,SAAuB;IACzC,MAAM,CAAC,WAAW,SAAiF;IAEnG,MAAM,CAAC,OAAO,EAAE,cAAc,CAE7B;IAMO,MAAM,EAAE,OAAO,GAAG,SAAS,CAAA;IAG3B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;IAGzB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAM7B,IAAI,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IAElC,+FAA+F;IAC/F,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,SAAS;IAS5E,GAAG;CAiGV"}
@@ -19,6 +19,21 @@ import { BaseCommand, flags } from '@adonisjs/core/ace';
19
19
  * node ace authz:scopes:relay # drena
20
20
  * node ace authz:scopes:relay --dry-run # solo dice qué haría
21
21
  * node ace authz:scopes:relay --limit 500 # una pasada acotada
22
+ * node ace authz:scopes:relay --only=12,13 # SOLO esas entradas (2.4.0-alpha.5)
23
+ *
24
+ * **`--only`** (alpha.5 · R1): releva solo los ids del recibo que devolvió
25
+ * `scopes.attached/moved/detached` (`{ queued }`). El orden por clave no se
26
+ * relaja: una entrada pedida que dependa de una anterior sin aplicar y no
27
+ * pedida sale «aplazada» (`depends-on:<id>`), nunca fuera de orden; `--only`
28
+ * vacío es un error, no «todo». La salida es POR ENTRADA (`entries`), y los
29
+ * ids que no estaban ni pendientes ni aparcados se dicen (`missing`).
30
+ *
31
+ * **Lo aparcado tapona por clave** (alpha.5 · R2a): un `attached`/`moved`
32
+ * que nombre un scope de una entrada APARCADA sale «aplazado» con
33
+ * `parked:<id>` en todas las pasadas hasta que alguien mire la aparcada; un
34
+ * `detached` de esa clave se aplica igual (una purga nunca queda bloqueada).
35
+ * Y un 503 del backend NO consume intento: la caída del store es común a
36
+ * todas las entradas y no dice nada de ninguna.
22
37
  *
23
38
  * Pensado para un supervisor (un bucle, un cron corto, un worker): la pasada
24
39
  * es REANUDABLE —lo que no entra sigue pendiente—. Un cambio que falla
@@ -49,12 +64,29 @@ export default class AuthzScopesRelay extends BaseCommand {
49
64
  static options = {
50
65
  startApp: true,
51
66
  };
67
+ /** `--only=12,13` o `--only=12 --only=13`: ids numéricos como número, el resto como cadena. */
68
+ static parseOnly(values) {
69
+ if (values === undefined)
70
+ return undefined;
71
+ return values
72
+ .flatMap((value) => String(value).split(','))
73
+ .map((value) => value.trim())
74
+ .filter((value) => value.length > 0)
75
+ .map((value) => (/^\d+$/.test(value) ? Number(value) : value));
76
+ }
52
77
  async run() {
53
78
  const { default: authorization } = await import('../services/main.js');
79
+ const only = AuthzScopesRelay.parseOnly(this.only);
80
+ if (only !== undefined && only.length === 0) {
81
+ this.logger.error('--only está vacío: relevar «nada» no es relevar «todo». Quita --only para drenar la cola entera.');
82
+ this.exitCode = 1;
83
+ return;
84
+ }
54
85
  const report = await authorization.relayScopeChanges({
55
86
  dryRun: this.dryRun === true,
56
87
  limit: this.limit,
57
88
  batchSize: this.batchSize,
89
+ only,
58
90
  });
59
91
  const linea = (item) => {
60
92
  const { change } = item;
@@ -92,17 +124,26 @@ export default class AuthzScopesRelay extends BaseCommand {
92
124
  `Mientras tanto el backend decide con el árbol VIEJO.`);
93
125
  return;
94
126
  }
95
- // Qué se aplicó, uno a uno: la pasada no es atómica y un contador no
96
- // permite retomar nada.
97
- for (const item of report.applied)
98
- this.logger.log(`aplicado: ${linea(item)}`);
99
- for (const item of report.deferred) {
100
- this.logger.warning(`aplazado: ${linea(item)}${item.error ? ` — ${item.error}` : ''}`);
127
+ // Qué fue de cada entrada, una a una (`entries`): la pasada no es atómica
128
+ // y un contador no permite retomar nada. Lo aparcado ya se dijo arriba.
129
+ for (const entry of report.entries) {
130
+ if (entry.status === 'applied')
131
+ this.logger.log(`aplicado: ${linea(entry)}`);
132
+ if (entry.status === 'deferred') {
133
+ const motivo = entry.reason?.startsWith('parked:')
134
+ ? `aplazado por APARCADO (${entry.reason}): no se aplica hasta que alguien mire esa entrada`
135
+ : `aplazado (${entry.reason ?? 'depende de una entrada anterior sin aplicar'}): corre el relay completo`;
136
+ this.logger.warning(`${motivo}: ${linea(entry)}`);
137
+ }
138
+ }
139
+ for (const id of report.missing) {
140
+ this.logger.warning(`#${id} no estaba ni pendiente ni aparcado: ya lo aplicó otra pasada, o no existe.`);
101
141
  }
102
142
  if (report.failures.length > 0) {
103
143
  for (const item of report.failures) {
104
144
  this.logger.error(`falló #${item.id} (${item.change.op} ` +
105
- `${item.change.child.type}:${item.change.child.uuid ?? ''}): ${item.error}`);
145
+ `${item.change.child.type}:${item.change.child.uuid ?? ''}): ${item.error}` +
146
+ (item.transient ? ' — el backend no respondió: este fallo NO consume intento' : ''));
106
147
  }
107
148
  this.logger.warning('Lo que DEPENDE de un cambio fallido no se adelanta (el orden del árbol importa: aparece como «aplazado»), ' +
108
149
  'pero el resto de la cola sí avanza. Arregla la causa y vuelve a ejecutar; retoma donde lo dejó. ' +
@@ -138,4 +179,11 @@ __decorate([
138
179
  flags.number({ name: 'batch-size', description: 'How many pending changes to read per round trip' }),
139
180
  __metadata("design:type", Object)
140
181
  ], AuthzScopesRelay.prototype, "batchSize", void 0);
182
+ __decorate([
183
+ flags.array({
184
+ name: 'only',
185
+ description: 'Apply ONLY these outbox ids (the receipt of scopes.*), comma-separated or repeated; dependencies are deferred, never skipped',
186
+ }),
187
+ __metadata("design:type", Object)
188
+ ], AuthzScopesRelay.prototype, "only", void 0);
141
189
  //# sourceMappingURL=authz_scopes_relay.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"authz_scopes_relay.js","sourceRoot":"","sources":["../../commands/authz_scopes_relay.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"authz_scopes_relay.js","sourceRoot":"","sources":["../../commands/authz_scopes_relay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ import { BaseCommand } from '@adonisjs/core/ace';
2
+ import { CommandOptions } from '@adonisjs/core/types/ace';
3
+ import type { ScopeRequeueConflict, ScopeRequeueReport } from '../src/types.js';
4
+ /**
5
+ * Las líneas del comando y si la ejecución es «limpia», en una función PURA
6
+ * (mismo patrón que `reconcileLines`/`orphanLines`): es lo único del comando
7
+ * que decide algo, y así tiene su caso sin montar un ace.
8
+ *
9
+ * - `refused`: el manager se NEGÓ (500 `E_AUTHZ_REQUEUE_REFUSED`). Se imprime
10
+ * el DIFF entero —cada aparcada y cada entrada APLICADA posterior que
11
+ * reaplicarla revertiría— y se sale ≠ 0, con las dos salidas: `--force`
12
+ * (que entra igual, con este mismo diff delante) y `authz:reconcile`.
13
+ * - `report`: se reencoló. Con `forced` y `overrides`, el diff se imprime
14
+ * como AVISO: forzar no es un éxito mudo.
15
+ */
16
+ export declare function requeueLines(input: {
17
+ refused?: ScopeRequeueConflict[];
18
+ report?: ScopeRequeueReport;
19
+ }): {
20
+ lines: Array<{
21
+ level: 'log' | 'warning' | 'error' | 'success';
22
+ message: string;
23
+ }>;
24
+ clean: boolean;
25
+ };
26
+ /**
27
+ * **Devuelve entradas APARCADAS de la outbox a la cola** (2.4.0-alpha.5 · R2b;
28
+ * panel relay, cuestión 2).
29
+ *
30
+ * node ace authz:scopes:requeue --id=12 # una
31
+ * node ace authz:scopes:requeue --id=12,13 # varias (o --id repetido)
32
+ * node ace authz:scopes:requeue --id=12 --force # aunque revierta lo aplicado después (diff impreso)
33
+ *
34
+ * Una entrada aparcada (`sqlScopeOutbox({ maxAttempts })`, 5 por defecto)
35
+ * no se reintenta sola: el relay la reporta en cada pasada y sale ≠ 0. Cuando
36
+ * el operador arregla la causa —el padre que faltaba vuelve, el store se
37
+ * reconcilia— este comando la devuelve a `pending()` con **`attempts =
38
+ * maxAttempts − 1`**: entra a la primera si está arreglada y vuelve a
39
+ * aparcarse en UNA sola pasada si no (así un requeue automatizado de lo
40
+ * irreparable no es un grifo de aplazamiento para el resto del scope).
41
+ *
42
+ * **La sonda anti-reversión es del manager y no tiene excepción**: si después
43
+ * de la aparcada se APLICÓ otra entrada sobre la misma clave de scope,
44
+ * reencolarla reaplicaría el cambio viejo por encima del nuevo —un `moved`
45
+ * viejo tras el nuevo deja el store con el padre VIEJO y un solo padre—. El
46
+ * comando imprime ese diff y sale ≠ 0. **`--force` existe SOLO aquí**
47
+ * (decisión del dueño del 2026-09-09 (2)): imprime el diff y entra igual. Nunca
48
+ * en una ruta alcanzable por HTTP.
49
+ *
50
+ * Es una operación de PLATAFORMA (como `authz:scopes:relay`): sin actor, sin
51
+ * `within`, y no se expone por HTTP.
52
+ */
53
+ export default class AuthzScopesRequeue extends BaseCommand {
54
+ static commandName: string;
55
+ static description: string;
56
+ static options: CommandOptions;
57
+ id: string[] | undefined;
58
+ force: boolean | undefined;
59
+ /** `--id=12,13` o `--id=12 --id=13`: ids numéricos como número, el resto como cadena. */
60
+ static parseIds(values: string[] | undefined): Array<string | number> | undefined;
61
+ run(): Promise<void>;
62
+ }
63
+ //# sourceMappingURL=authz_scopes_requeue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authz_scopes_requeue.d.ts","sourceRoot":"","sources":["../../commands/authz_scopes_requeue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAS,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAA0B,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAYvG;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAAE,OAAO,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,kBAAkB,CAAA;CAAE,GAAG;IACtG,KAAK,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACjF,KAAK,EAAE,OAAO,CAAA;CACf,CAyCA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,WAAW;IACzD,MAAM,CAAC,WAAW,SAAyB;IAC3C,MAAM,CAAC,WAAW,SAAkH;IAEpI,MAAM,CAAC,OAAO,EAAE,cAAc,CAE7B;IAMO,EAAE,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IAKxB,KAAK,EAAE,OAAO,GAAG,SAAS,CAAA;IAElC,yFAAyF;IACzF,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,SAAS;IAS3E,GAAG;CAmBV"}