@jantstack/adonis-authz 2.4.0-alpha.4 → 2.4.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -4
- package/build/commands/authz_reconcile.d.ts +5 -3
- package/build/commands/authz_reconcile.d.ts.map +1 -1
- package/build/commands/authz_reconcile.js +15 -3
- package/build/commands/authz_reconcile.js.map +1 -1
- package/build/commands/authz_scopes_relay.d.ts +18 -0
- package/build/commands/authz_scopes_relay.d.ts.map +1 -1
- package/build/commands/authz_scopes_relay.js +55 -7
- package/build/commands/authz_scopes_relay.js.map +1 -1
- package/build/commands/authz_scopes_requeue.d.ts +63 -0
- package/build/commands/authz_scopes_requeue.d.ts.map +1 -0
- package/build/commands/authz_scopes_requeue.js +154 -0
- package/build/commands/authz_scopes_requeue.js.map +1 -0
- package/build/commands/main.d.ts +2 -1
- package/build/commands/main.d.ts.map +1 -1
- package/build/commands/main.js +5 -3
- package/build/commands/main.js.map +1 -1
- package/build/index.d.ts +3 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/build/src/define_config.d.ts +24 -0
- package/build/src/define_config.d.ts.map +1 -1
- package/build/src/define_config.js.map +1 -1
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +24 -1
- package/build/src/drivers/openfga_driver.js.map +1 -1
- package/build/src/drivers/openfga_relations_driver.d.ts +32 -0
- package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_relations_driver.js +64 -6
- package/build/src/drivers/openfga_relations_driver.js.map +1 -1
- package/build/src/errors.d.ts +30 -0
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +32 -0
- package/build/src/errors.js.map +1 -1
- package/build/src/manager.d.ts +86 -4
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +457 -25
- package/build/src/manager.js.map +1 -1
- package/build/src/relations/manager.d.ts +7 -0
- package/build/src/relations/manager.d.ts.map +1 -1
- package/build/src/relations/manager.js +27 -0
- package/build/src/relations/manager.js.map +1 -1
- package/build/src/relations/reconcile.d.ts +14 -2
- package/build/src/relations/reconcile.d.ts.map +1 -1
- package/build/src/relations/reconcile.js +155 -77
- package/build/src/relations/reconcile.js.map +1 -1
- package/build/src/scope_outbox.d.ts +21 -0
- package/build/src/scope_outbox.d.ts.map +1 -1
- package/build/src/scope_outbox.js +211 -24
- package/build/src/scope_outbox.js.map +1 -1
- package/build/src/testing/contract.d.ts.map +1 -1
- package/build/src/testing/contract.js +4 -1
- package/build/src/testing/contract.js.map +1 -1
- package/build/src/testing/main.d.ts +2 -0
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +1 -0
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/scope_outbox_contract.d.ts +88 -0
- package/build/src/testing/scope_outbox_contract.d.ts.map +1 -0
- package/build/src/testing/scope_outbox_contract.js +607 -0
- package/build/src/testing/scope_outbox_contract.js.map +1 -0
- package/build/src/types.d.ts +240 -17
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,8 @@ Your resolver's *answer* is validated too: an element that is not a well-formed
|
|
|
97
97
|
|
|
98
98
|
`ScopeType` is an open `string`, so define your own union for type safety. The engine never queries your tables.
|
|
99
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
|
+
|
|
100
102
|
#### The tree outbox, and the relay lag you are accepting
|
|
101
103
|
|
|
102
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.
|
|
@@ -124,15 +126,29 @@ await db.transaction(async (trx) => {
|
|
|
124
126
|
```bash
|
|
125
127
|
node ace authz:scopes:relay # drain the queue and apply the edges
|
|
126
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
|
|
127
131
|
```
|
|
128
132
|
|
|
129
|
-
The package **does not impose a table**: the contract is the `ScopeOutbox` port (`enqueue`, `pending`, `markApplied`, `markFailed`, plus
|
|
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).
|
|
130
134
|
|
|
131
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).
|
|
132
136
|
|
|
133
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.
|
|
134
138
|
|
|
135
|
-
**
|
|
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 holds too (2.4.0-alpha.6).** With `relayScopeChanges({ only })` the probe that finds an earlier unapplied entry sharing a scope key with yours now **inherits the origin of the block** of that entry, the same way the full pass tracks it: if the earlier entry is — directly or through its own dependencies — waiting only on a **parked** one (it would be `parked:<id>` or `depends-on:` of one in a full pass), a requested `detached` is **applied and purges**, exactly as in the full pass (invariant 11: a purge never waits for a parked entry). If the earlier entry is an `attached`/`moved` that is genuinely pending (it would be applied by the next full pass) or is held by a **real failure** of the pass, the `detached` stays `deferred` (`depends-on:<id>`), because applying a purge ahead of a pending `moved` of the same scope leaves drift. Until alpha.6 the probe marked every such deferral as a real block, so a deployment that only ever relayed inline never purged behind a parked entry (audit 🟡 of alpha.5, measured). The probe walks back one `earlierPending` query per hop over entries it does not apply, at most `RELAY_ORIGIN_MAX_HOPS` (100) hops; past that it assumes a real block and defers — towards safe, never applying without knowing. Entries you did not request are not touched: they keep their attempts and their place.
|
|
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.
|
|
136
152
|
|
|
137
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)):
|
|
138
154
|
|
|
@@ -325,6 +341,7 @@ scopes: {
|
|
|
325
341
|
descendantsOf: sqlDescendantsOf({ table: 'org_nodes', uuidColumn: 'uuid', parentColumn: 'parent_uuid', typeColumn: 'kind' }),
|
|
326
342
|
maxScopes: 1000, // answer bound of authorizedScopes
|
|
327
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
|
|
328
345
|
}
|
|
329
346
|
```
|
|
330
347
|
|
|
@@ -614,7 +631,8 @@ analog of `authz:reconcile`, idempotent, bidirectional and never silent (it repo
|
|
|
614
631
|
`--dry-run` is the read-only verifier and also flags **model drift** (an object type in the source the
|
|
615
632
|
destination does not declare); `--prune` deletes what the source no longer backs. It migrates **facts
|
|
616
633
|
only** — there is no tree or catalog in `relations/` — and works **per partition** (`--partition-type`
|
|
617
|
-
/`--partition-uuid`; default `app
|
|
634
|
+
/`--partition-uuid`; default `app`; from code, `reconcileRelations({ partitions: [...] })` migrates several
|
|
635
|
+
in one pass — see the sweep below). **The pass that writes runs under the durable freeze** (2.4.0-alpha.2,
|
|
618
636
|
lot L-1), exactly like `authz:reconcile`: every write of the package — roles *and* relations — answers a
|
|
619
637
|
retryable 503 `E_AUTHZ_FROZEN` fleet-wide while it runs; a live **operator** window (`authz:freeze`) is its
|
|
620
638
|
own context (it runs inside, does not renew or lift it); a live freeze of another pass is 423; and the
|
|
@@ -622,6 +640,33 @@ report carries `frozen: { durable, lapsed, leaseMs, fence }` — **`lapsed: true
|
|
|
622
640
|
mid-pass and the pass is not certified** (exit ≠ 0). `--dry-run` does not freeze: the verifier is
|
|
623
641
|
read-only and is meant for a cron.
|
|
624
642
|
|
|
643
|
+
#### The source sweep, measured
|
|
644
|
+
|
|
645
|
+
In `openfga`, `enumerateRelations(partition)` — the *source* of the migration — is a `Read({})` of the
|
|
646
|
+
**whole store**, paginated by 100 (the server's cap) and filtered in memory by the whole partition key.
|
|
647
|
+
There is no cheaper `Read`: by type without `user` is a 400, and by **prefix** of the object id
|
|
648
|
+
(`document:unit|<uuid>|`) answers **200 with zero tuples** — silently, which in the source of a migration
|
|
649
|
+
means "this partition is empty" and in a purge "proved zero" without deleting anything. Never optimise it
|
|
650
|
+
that way; the suite spies the filter of every `read` and it is always `{}`. What *is* cheaper is doing the
|
|
651
|
+
sweep **once for K partitions**: `enumerateRelationsMany(partitions)` (optional port method, implemented by
|
|
652
|
+
`openfga`; `RelationsManager` exposes it and falls back to K calls for a driver without it) and
|
|
653
|
+
`reconcileRelations({ partitions })`, which enumerates source and destination once, decides the K diffs
|
|
654
|
+
first (the mass-delete guard R-17 is evaluated for every partition **before** writing anything) and then
|
|
655
|
+
applies them; the report is the sum. Same output per partition, byte for byte, as K separate calls; `app`
|
|
656
|
+
does not pull `appliance|…`, `org|U` does not pull `organization|U`, and each spelling of a uuid is its own
|
|
657
|
+
partition. Measured against OpenFGA 1.19.0 (2.4.0-alpha.6):
|
|
658
|
+
|
|
659
|
+
| Store | Partitions asked | `enumerateRelations` × K (before) | `enumerateRelationsMany` (after) |
|
|
660
|
+
|---|---|---|---|
|
|
661
|
+
| 5,000 tuples (3 × 1,000 + 2,000 foreign) | 3 | 150 `Read`, 502–558 ms | **50 `Read`, 170–209 ms** |
|
|
662
|
+
| 15,000 tuples (panel measurement) | 3 | 453 `Read`, ≈ 1.4 s | **151 `Read`, ≈ 0.46 s** |
|
|
663
|
+
| 15,000 tuples | 50 | ≈ 23 s | ≈ 0.5 s (extrapolated from the same sweep) |
|
|
664
|
+
|
|
665
|
+
The cost is O(tuples in the store) **per sweep**, whatever the size of the partitions asked, and the sweep is
|
|
666
|
+
exhaustive or loud: a page with zero tuples *and* a continuation token is not the end; a token that
|
|
667
|
+
repeats, more than 10,000 pages, or a response without `tuples` is 503 `E_AUTHZ_BACKEND_UNAVAILABLE`,
|
|
668
|
+
never a shorter enumeration. Run it from a job or a cron, **never from a tenant request**.
|
|
669
|
+
|
|
625
670
|
**Writing relations inside your transaction (2.4.0-alpha.2, lot L-4).** `relate`/`unrelate`/`purgeObject`/
|
|
626
671
|
`purgeSubject` with **`{ transaction: trx }`** on the `database` relations driver write the tuple **inside
|
|
627
672
|
your open Lucid transaction** of the driver's connection — the same rule, the same guard, the same pool ≥ 2
|
|
@@ -698,6 +743,7 @@ Every error the package raises carries `status` and `code`. A standard AdonisJS
|
|
|
698
743
|
| `E_AUTHZ_WITHIN_REQUIRED` | 422 | `requireWithin` set and a write without `within` (2.1) |
|
|
699
744
|
| `E_AUTHZ_WITHIN_ROOT_FORBIDDEN` | 422 | `requireWithin: 'non-root'` and `within: APP_SCOPE` (2.1) |
|
|
700
745
|
| `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 |
|
|
746
|
+
| `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) |
|
|
701
747
|
| `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) |
|
|
702
748
|
| `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` |
|
|
703
749
|
| `E_AUTHZ_BACKEND_TIMEOUT` | 503 | `timeoutMs` elapsed (subclass of the above) |
|
|
@@ -705,7 +751,7 @@ Every error the package raises carries `status` and `code`. A standard AdonisJS
|
|
|
705
751
|
| `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 |
|
|
706
752
|
| `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 |
|
|
707
753
|
| `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) |
|
|
708
|
-
| `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) |
|
|
754
|
+
| `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) |
|
|
709
755
|
| `E_AUTHZ_ROLE_IS_NOT_ACCESS` | 500 | `appAccess({ role })` |
|
|
710
756
|
| `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) |
|
|
711
757
|
| `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) |
|
|
@@ -717,6 +763,7 @@ Every error the package raises carries `status` and `code`. A standard AdonisJS
|
|
|
717
763
|
| `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 % |
|
|
718
764
|
| `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 |
|
|
719
765
|
| `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 |
|
|
766
|
+
| `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) |
|
|
720
767
|
| `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 |
|
|
721
768
|
| `E_AUTHZ_NO_DESCENDANTS_RESOLVER` | 500 | `authorizedScopes`/`expandExcludedSubtrees` without `scopes.descendantsOf` |
|
|
722
769
|
| `E_AUTHZ_VIEW_EXPIRED` | 500 | a `forRequest()` view used to read (`expandExcludedSubtrees` included) after its `maxAgeMs` (default 30 s, monotonic clock) |
|
|
@@ -905,6 +952,33 @@ What passing means: **for everything the suite covers, both drivers answer the s
|
|
|
905
952
|
|
|
906
953
|
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).
|
|
907
954
|
|
|
955
|
+
### The outbox contract: `runScopeOutboxContract` (2.4.0-alpha.5)
|
|
956
|
+
|
|
957
|
+
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:
|
|
958
|
+
|
|
959
|
+
```ts
|
|
960
|
+
import { runScopeOutboxContract } from '@jantstack/adonis-authz/testing'
|
|
961
|
+
|
|
962
|
+
runScopeOutboxContract({
|
|
963
|
+
name: 'my-outbox',
|
|
964
|
+
capabilities: {
|
|
965
|
+
transactionalEnqueue: true, // enqueue writes through context.transaction: rollback ⇒ nothing pending (needs `transactions`)
|
|
966
|
+
filter: true, // pending(limit, after, { ids }) filters in the port; false ⇒ `only` is 500 E_AUTHZ_CONFIG
|
|
967
|
+
transient: true, // markFailed(..., { transient: true }) does not count; false ⇒ it counts anyway
|
|
968
|
+
dead: true, // dead() exists: parked after limits.maxAttempts; false ⇒ nothing is ever parked
|
|
969
|
+
acquire: true, // acquire() is a held, exclusive lease; false ⇒ the relay never says busy
|
|
970
|
+
earlierPending: true, // the dependency probe of `only`; false ⇒ the manager pages pending(), same verdict
|
|
971
|
+
requeue: true, // requeue + laterApplied (needs dead); false ⇒ requeueScopeChanges is 500 E_AUTHZ_UNSUPPORTED
|
|
972
|
+
},
|
|
973
|
+
limits: { maxAttempts: 3 }, // with dead: true — what your outbox parks at (≥ 3)
|
|
974
|
+
makeOutbox: () => myOutbox({ maxAttempts: 3 }), // fresh, over an empty queue
|
|
975
|
+
cleanup: () => emptyMyQueue(),
|
|
976
|
+
transactions: { begin: async () => openMyTransaction() }, // with transactionalEnqueue: true
|
|
977
|
+
})
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
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`.
|
|
981
|
+
|
|
908
982
|
## OpenFGA tooling
|
|
909
983
|
|
|
910
984
|
```bash
|
|
@@ -1032,6 +1106,30 @@ legal for the `database` driver, which has no such ceiling — the same tree is
|
|
|
1032
1106
|
503 in the other, and it is a DoS within reach of whoever can nest sub-scopes. Raise
|
|
1033
1107
|
`OPENFGA_RESOLVE_NODE_LIMIT` on the server if your tree is deeper.
|
|
1034
1108
|
|
|
1109
|
+
### Limits
|
|
1110
|
+
|
|
1111
|
+
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.
|
|
1112
|
+
|
|
1113
|
+
| Limit | Constant / option | Value | Where | Past it |
|
|
1114
|
+
|---|---|---|---|---|
|
|
1115
|
+
| 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 |
|
|
1116
|
+
| 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 |
|
|
1117
|
+
| 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) |
|
|
1118
|
+
| Checks per `batchCheck` | `FGA_MAX_BATCH_CHECK` | 50 | `openfga`, `authorizeMany` | the driver splits into batches of 50 (the SDK's cap), nothing to do |
|
|
1119
|
+
| `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 page with zero tuples **and** a token is not the end; a token that repeats, more than 10,000 pages, or a response without `tuples` is 503 `E_AUTHZ_BACKEND_UNAVAILABLE`, never a shorter enumeration |
|
|
1120
|
+
| `enumerateRelations(partition)` / `enumerateRelationsMany(partitions)` cost | — | O(tuples in the store) **per sweep** — one sweep per `enumerateRelations` call, **one sweep for K partitions** with `enumerateRelationsMany` / `reconcileRelations({ partitions })` (2.4.0-alpha.6) | `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 — the suite spies the filter of every `read`, it is always `{}`); measured 15,000 tuples ⇒ 151 pages ≈ 0.5 s per sweep whatever the partition size; 3 partitions in a 5,000-tuple store: **150 reads / 502–558 ms** as three sweeps → **50 reads / 170–209 ms** as one ([the table](#the-source-sweep-measured)). Run it from a job or a cron, **never from a tenant request** |
|
|
1121
|
+
| Hops the `only` probe walks back to inherit a block's origin | `RELAY_ORIGIN_MAX_HOPS` | 100 | the manager, `relayScopeChanges({ only })` | the origin is taken as a real block and the entry stays `deferred` (the purge waits for a full pass); one `earlierPending` query per hop |
|
|
1122
|
+
| Scopes a `pending()` pass reads per round trip | `DEFAULT_RELAY_BATCH` | 100 | `authz:scopes:relay` (`--batch-size`) | the pass keeps paging |
|
|
1123
|
+
| Changes one relay pass applies | `DEFAULT_RELAY_LIMIT` | 10,000 | `authz:scopes:relay` (`--limit`) | the rest stays pending (`remaining: true`); run again |
|
|
1124
|
+
| 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 |
|
|
1125
|
+
| 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*) |
|
|
1126
|
+
| 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) |
|
|
1127
|
+
| 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) |
|
|
1128
|
+
| Local roles a `prune-orphans` pass may read | `maxLocalRoles` | 10,000 | `pruneOrphanRoles` | 500 `E_AUTHZ_TOO_MANY_LOCAL_ROLES`, never a partial list |
|
|
1129
|
+
| Ancestors `hierarchicalScopeResolver` walks | `maxDepth` (resolver option) | 64 in the README example | your `resolveChain` | 500 `E_AUTHZ_SCOPE_TOO_DEEP`, never a truncated chain |
|
|
1130
|
+
|
|
1131
|
+
**`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.
|
|
1132
|
+
|
|
1035
1133
|
### Operational notes for the SQL engines
|
|
1036
1134
|
|
|
1037
1135
|
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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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":";;;;;;;;;AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAIvD
|
|
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
|
|
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é
|
|
96
|
-
// permite retomar nada.
|
|
97
|
-
for (const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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":";;;;;;;;;AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAGvD
|
|
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"}
|