@jantstack/adonis-authz 2.5.0-alpha.2 → 2.5.0-alpha.3
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 +343 -7
- package/build/commands/authz_relations_reconcile.d.ts +6 -0
- package/build/commands/authz_relations_reconcile.d.ts.map +1 -1
- package/build/commands/authz_relations_reconcile.js +113 -3
- package/build/commands/authz_relations_reconcile.js.map +1 -1
- package/build/commands/openfga_provision.d.ts +62 -0
- package/build/commands/openfga_provision.d.ts.map +1 -1
- package/build/commands/openfga_provision.js +193 -1
- package/build/commands/openfga_provision.js.map +1 -1
- package/build/index.d.ts +6 -6
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -3
- package/build/index.js.map +1 -1
- package/build/src/drivers/database_relations_driver.d.ts +36 -1
- package/build/src/drivers/database_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/database_relations_driver.js +211 -45
- package/build/src/drivers/database_relations_driver.js.map +1 -1
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +102 -36
- package/build/src/drivers/openfga_driver.js.map +1 -1
- package/build/src/drivers/openfga_facts.d.ts +21 -0
- package/build/src/drivers/openfga_facts.d.ts.map +1 -1
- package/build/src/drivers/openfga_facts.js +74 -8
- package/build/src/drivers/openfga_facts.js.map +1 -1
- package/build/src/drivers/openfga_relations_driver.d.ts +74 -1
- package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_relations_driver.js +599 -29
- package/build/src/drivers/openfga_relations_driver.js.map +1 -1
- package/build/src/errors.d.ts +112 -0
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +130 -0
- package/build/src/errors.js.map +1 -1
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +9 -4
- package/build/src/manager.js.map +1 -1
- package/build/src/openfga.d.ts +1 -1
- package/build/src/openfga.d.ts.map +1 -1
- package/build/src/openfga.js +1 -1
- package/build/src/openfga.js.map +1 -1
- package/build/src/relations/check_many.d.ts +1 -1
- package/build/src/relations/check_many.d.ts.map +1 -1
- package/build/src/relations/check_many.js +6 -4
- package/build/src/relations/check_many.js.map +1 -1
- package/build/src/relations/define_relations_config.d.ts +115 -2
- package/build/src/relations/define_relations_config.d.ts.map +1 -1
- package/build/src/relations/define_relations_config.js +197 -3
- package/build/src/relations/define_relations_config.js.map +1 -1
- package/build/src/relations/manager.d.ts +33 -2
- package/build/src/relations/manager.d.ts.map +1 -1
- package/build/src/relations/manager.js +120 -3
- package/build/src/relations/manager.js.map +1 -1
- package/build/src/relations/reconcile.d.ts +219 -1
- package/build/src/relations/reconcile.d.ts.map +1 -1
- package/build/src/relations/reconcile.js +745 -155
- package/build/src/relations/reconcile.js.map +1 -1
- package/build/src/relations/tuple_order.d.ts +126 -0
- package/build/src/relations/tuple_order.d.ts.map +1 -0
- package/build/src/relations/tuple_order.js +598 -0
- package/build/src/relations/tuple_order.js.map +1 -0
- package/build/src/shared/backend_guard.d.ts +8 -0
- package/build/src/shared/backend_guard.d.ts.map +1 -1
- package/build/src/shared/backend_guard.js +15 -0
- package/build/src/shared/backend_guard.js.map +1 -1
- package/build/src/testing/main.d.ts +1 -1
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +1 -1
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/relations_contract.d.ts +18 -0
- package/build/src/testing/relations_contract.d.ts.map +1 -1
- package/build/src/testing/relations_contract.js +824 -44
- package/build/src/testing/relations_contract.js.map +1 -1
- package/build/src/testing/relations_reconcile_contract.d.ts +9 -0
- package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -1
- package/build/src/testing/relations_reconcile_contract.js +207 -0
- package/build/src/testing/relations_reconcile_contract.js.map +1 -1
- package/build/src/types.d.ts +83 -3
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js +2 -0
- package/build/src/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -249,7 +249,13 @@ The *row* is inside your transaction; the *decision* that authorises writing it
|
|
|
249
249
|
|
|
250
250
|
#### Third-party drivers, and what the suite can and cannot prove
|
|
251
251
|
|
|
252
|
-
"Both or neither" is **a guarantee of this package's `database` driver**, measured on its own schema on three engines. For a driver of yours declaring `transactionalWrites: true`, the runner requires the census (the `transactions` hook: how to open a transaction of *your* connection and how to count *your* rows) and judges rollback ⇒ zero, commit ⇒ applied, the uncommitted write invisible to the authority and three foreign transactions ⇒ 500 with zero statements — but it **cannot prove that a third-party driver's write really lives and dies with the transaction it is handed**: the census it counts is the one your hook reports, and a driver that opens its own transaction underneath passes the answer checks. Two more honest boundaries: the `true` face only runs where a second connection exists (`sqlite-file`, PostgreSQL, MySQL — the engine jobs of CI are not optional for this capability), so a driver judged only on `:memory:` never sees those cases; and the 409/503 classification of the table above is the `database` driver's — a third-party driver that returns the raw engine error is not caught by the suite. The same honesty for relations: the `listObjectsTruncation` pair of `runRelationsDriverContract` judges the **cap** only (`limits.listMaxResults`); **the runner does not judge the deadline** signal of a third-party `listObjects` (there is no way to observe it without sleeping), so a driver that does not implement it passes — the deadline cause is a property of this package's `openfga` driver, pinned by its own cases and by the surface guard. And the B3 case (a userset other than `group#member` as subject) observes your backend **through the port** — `listSubjects` and, under `enumerateRelations: true`, the whole partition — so a driver that stores the inert row and then throws the 422 goes red only through a port it declares: the minimal harness (`usersetSubjects: false`, `enumerateRelations: false`) has no port through which the runner can see that row, and such a driver passes (measured with that mutant on the double).
|
|
252
|
+
"Both or neither" is **a guarantee of this package's `database` driver**, measured on its own schema on three engines. For a driver of yours declaring `transactionalWrites: true`, the runner requires the census (the `transactions` hook: how to open a transaction of *your* connection and how to count *your* rows) and judges rollback ⇒ zero, commit ⇒ applied, the uncommitted write invisible to the authority and three foreign transactions ⇒ 500 with zero statements — but it **cannot prove that a third-party driver's write really lives and dies with the transaction it is handed**: the census it counts is the one your hook reports, and a driver that opens its own transaction underneath passes the answer checks. Two more honest boundaries: the `true` face only runs where a second connection exists (`sqlite-file`, PostgreSQL, MySQL — the engine jobs of CI are not optional for this capability), so a driver judged only on `:memory:` never sees those cases; and the 409/503 classification of the table above is the `database` driver's — a third-party driver that returns the raw engine error is not caught by the suite. The same honesty for relations: the `listObjectsTruncation` pair of `runRelationsDriverContract` judges the **cap** only (`limits.listMaxResults`); **the runner does not judge the deadline** signal of a third-party `listObjects` (there is no way to observe it without sleeping), so a driver that does not implement it passes — the deadline cause is a property of this package's `openfga` driver, pinned by its own cases and by the surface guard. And the B3 case (a userset other than `group#member` as subject) observes your backend **through the port** — `listSubjects` and, under `enumerateRelations: true`, the whole partition — so a driver that stores the inert row and then throws the 422 goes red only through a port it declares: the minimal harness (`usersetSubjects: false`, `enumerateRelations: false`) has no port through which the runner can see that row, and such a driver passes (measured with that mutant on the double). **`denyPhase` (2.5.0-alpha.3) is judged by the driver
|
|
253
|
+
runner**: with `relationDenies: true`, `runRelationsDriverContract` asks your driver, built on the contract's deny config,
|
|
254
|
+
for the phase of each type (`document` 'write', `workspace` 'read', `note` none) and checks that it is the truth about
|
|
255
|
+
`deny()` ('write' ⇒ the deny lands and `check` is false; 'read' and none ⇒ 422 `E_AUTHZ_RELATION_NOT_DENYABLE`, nothing
|
|
256
|
+
written) plus F-05 (undeclared type, undeclared or missing relation ⇒ 422); with `relationDenies: false`, a `denyPhase` you
|
|
257
|
+
still expose must claim no phase (`null`). It is the gate `authz:relations:reconcile` uses before writing a deny into your
|
|
258
|
+
driver; what the runner cannot see is a driver whose `deny()` and `check` agree with each other and both ignore the opt-in.
|
|
253
259
|
|
|
254
260
|
### Identity is validated, once and everywhere
|
|
255
261
|
|
|
@@ -597,6 +603,166 @@ nothing asked); a per-item `error` inside a 200 `batchCheck` is a **503 for the
|
|
|
597
603
|
`decideMany` (2.5.0, lot D). A third-party driver's answer is validated: anything but a `boolean[]` with one
|
|
598
604
|
position per object is 500 `E_AUTHZ_INTERNAL` naming the driver.
|
|
599
605
|
|
|
606
|
+
**Deny on relations (2.5.0).** A relation can be **denied** to a subject on an object: `deny(subject, relation,
|
|
607
|
+
object, partition)` writes the tuple `object#denied_<relation>@subject` (in `authz_relations` on `database`, in the store
|
|
608
|
+
on `openfga`) and **every read honours it** — `check`, `checkMany`, `decideMany`, `listObjects`. It is opt-in **per
|
|
609
|
+
type and in two phases** (`deny: 'read' | 'write'` on the object type in `defineRelationsConfig`; there is no `deny:
|
|
610
|
+
true` — that would be the single switch with no safe rollout order):
|
|
611
|
+
|
|
612
|
+
- `deny: 'read'` (phase 1): the published model declares `denied_<rel>` (`<rel> … but not denied_<rel>`) and all reads
|
|
613
|
+
honour it; `deny()` on that type is still 422 `E_AUTHZ_RELATION_NOT_DENYABLE`.
|
|
614
|
+
- `deny: 'write'` (phase 2): writes are accepted too.
|
|
615
|
+
|
|
616
|
+
The rollout is two deployments: (1) the whole fleet on ≥ 2.5.0-alpha.3 with the type in `'read'`, the model
|
|
617
|
+
republished (`openfga:provision`) and **every pod pinned to the new `modelId`**; (2) verify; (3) a second deployment
|
|
618
|
+
moves the type to `'write'`. With any type in either phase the `openfga` relations driver **requires a pinned
|
|
619
|
+
`modelId`** (500 `E_AUTHZ_CONFIG` at construction otherwise) and checks, once per process, that the pinned model
|
|
620
|
+
declares `denied_<rel>` for every relation of those types **and that `<rel>` subtracts it** (`<rel> … but not
|
|
621
|
+
denied_<rel>`) — a pinned model without it is 500 `E_AUTHZ_CONFIG` on first use, never a grant over a denied object.
|
|
622
|
+
**`decideMany` on `openfga` requires a pinned `modelId` always**, also without deny types (500 `E_AUTHZ_CONFIG`
|
|
623
|
+
naming D14 and the recipe "provision → pin `modelId` → deploy", with zero calls): it picks which `denied_*` to ask
|
|
624
|
+
with the model it read once, and unpinned the server checks with the *latest* model — after a republish with deny it
|
|
625
|
+
would answer `none` where `check` answers `false`. A relation of a deniable type measures **at most 43 characters**
|
|
626
|
+
(`DENIABLE_RELATION_NAME_MAX`: `denied_<rel>` has to fit FGA's 50); a type without `deny` keeps the 50.
|
|
627
|
+
|
|
628
|
+
**The semantics is a path cut** (decision D5, the 64-state table T1 in the suite): a deny on `Y` cuts every grant of
|
|
629
|
+
`T` with `Y ∈ up*(T)` (everything that includes `T`), with all that derives from it. With `owner ⊆ editor ⊆ viewer`:
|
|
630
|
+
|
|
631
|
+
| allows | deny | `viewer` / `editor` / `owner` | case |
|
|
632
|
+
|---|---|---|---|
|
|
633
|
+
| owner | viewer | F / F / F | *"D-06 · un deny en viewer bloquea al owner"* |
|
|
634
|
+
| owner | editor | F / F / F | *"D-07 · un deny en editor anula el grant de owner ENTERO"* |
|
|
635
|
+
| owner + viewer | editor | T / F / F | *"D-08 · …salvo un viewer directo"* |
|
|
636
|
+
| viewer | editor | T / F / F | *"D-11 · un deny en una relación MÁS FUERTE no toca un grant más débil"* |
|
|
637
|
+
|
|
638
|
+
A deny reaches the subject **directly or through `group#member`** (nested groups too), and a group membership that
|
|
639
|
+
expires stops carrying it (a deny itself does not expire). Rules that go with it, each with its case: **no expiry**
|
|
640
|
+
(`expiresAt` on `deny`/`removeDeny` is 422 `E_AUTHZ_DENY_EXPIRY_UNSUPPORTED` — measured on OpenFGA 1.19: a live
|
|
641
|
+
deny's condition cannot be changed without a window in which it stops blocking); no `*` subject and no deny on
|
|
642
|
+
`group` (422 `E_AUTHZ_RELATION_NOT_DENYABLE`); a userset subject must be `group#member`; `deny`/`removeDeny` go
|
|
643
|
+
through the same gates as `relate` (`{ transaction }`, the freeze, F-05, `actor`, `assertWrite` with `operation:
|
|
644
|
+
'deny' | 'removeDeny'`, invariant 13) and notify `onRelationWrite`; `removeDeny` and `listDenies` accept **any declared
|
|
645
|
+
type** (retiring the opt-in must be reversible); `listSubjects` **does not** subtract (it lists facts) and
|
|
646
|
+
`listDenies(relation, object, partition)` lists the direct denies; `listObjects` **does** subtract (on `openfga` the
|
|
647
|
+
server does, so a page cut at the server's cap is a subset — never a denied object), and so does `membersOf` on
|
|
648
|
+
`database` (one `check` per candidate, only when the object has a deny row — at most `RELATIONS_MEMBERS_DENY_CHECK_MAX`
|
|
649
|
+
candidates, 1,000, past it 500 `E_AUTHZ_MEMBERS_DENY_LIMIT` and never a partial list, under one total deadline, 503);
|
|
650
|
+
the purges delete denies too. **On `openfga` a purge that fits in one `Write` is one `Write`**
|
|
651
|
+
(`RELATIONS_PURGE_WRITE_MAX`, 100 tuples — the server's `--max-tuples-per-write` default, measured: 101 is a 400 and
|
|
652
|
+
nothing applies), which FGA applies atomically; a larger one deletes the allows first, then **the subject's group
|
|
653
|
+
memberships in a `Write` of their own** (a membership carries denies too), then the denies — so **a purge cut halfway
|
|
654
|
+
grants nothing that neither the state before nor the state after granted**
|
|
655
|
+
(*"purgeSubject cortada, deny por grupo (holder)"*, *"purgeSubject cortada, deny anidado (userset g#member)"*, and the
|
|
656
|
+
symmetric row — the membership carries the allow, the deny is direct —
|
|
657
|
+
*"purgeSubject cortada, allow por grupo × deny DIRECTO (holder)"*; the same table runs without a server in the surface
|
|
658
|
+
guard, with the five order mutants in `npm run check:guard-mutants`).
|
|
659
|
+
**Declared row:** with more than 100 group memberships of the subject, a membership that carries a deny can fall in an earlier `Write` than one that carries an
|
|
660
|
+
allow, and a purge cut between the two grants until it is repeated (it stays marked `indeterminate: true`; the purge is
|
|
661
|
+
not refused before deleting — a purge does not wait) (*"FILA DECLARADA · purgeSubject con más de 100 membresías de grupo"*).
|
|
662
|
+
**The order of the purge is the same function as the reconcile's** (`relationTransitionOrder`,
|
|
663
|
+
`src/relations/tuple_order.ts`; closure 2 of D2): allows, then memberships (by the precedence graph), then denies; one
|
|
664
|
+
ordering point for every multi-request write or delete of relations.
|
|
665
|
+
**Renewing a membership that carries a deny is refused on `openfga`** (closure 2 of sublot D2, owner's decision): `relate`
|
|
666
|
+
on an EXISTING `group:*#member` tuple with another expiry is, on `openfga`, two `Write`s (delete, write: FGA does not
|
|
667
|
+
replace the same key in one `Write`), and between them the membership is not there — if that group, or a group that
|
|
668
|
+
contains it, is the subject of a live deny of the partition, the subject would be granted (for good, if the second
|
|
669
|
+
`Write` failed). So that renewal is **409 `E_AUTHZ_RENEWAL_UNSAFE` with zero writes** (always zero: closure 4) and an
|
|
670
|
+
honest recipe in the message (closure 5 of D2): on `openfga` **there is no window-free way** to change that expiry (see
|
|
671
|
+
*The limit* below); the recommended pattern is memberships WITHOUT expiry in groups that carry a deny, ended with
|
|
672
|
+
`unrelate`; if the expiry must change, `unrelate` + `relate` **accepting the window**, said as such. (Until closure 5 the
|
|
673
|
+
message also offered "retire the deny first, renew, write it again" as if it were the safe way out: it opens the same
|
|
674
|
+
window.) Cost: only on a membership renewal, one `Read` per deny type and one for the parents, per group up the nesting.
|
|
675
|
+
Every other renewal stays two `Write`s, and if the second fails the error is marked as a **partial write**
|
|
676
|
+
(`isPartialWrite`; `onRelationWrite` gets `indeterminate: true`) — until closure 2 it was a 503 that said nothing was
|
|
677
|
+
written while the tuple was gone. **Declared window** (closure 3 of D2): the carrier check reads, then the two `Write`s
|
|
678
|
+
follow; a `deny` on that group (or one containing it) written CONCURRENTLY, between the check and the delete, does not
|
|
679
|
+
reach the subject during the two `Write`s — and for good if the second one fails, because the membership is gone. The
|
|
680
|
+
driver re-checks after the two `Write`s (and after a failed second one) and the window stops being silent: if the renewal
|
|
681
|
+
went through, it throws **409 `E_AUTHZ_RENEWAL_RACED`** (`RenewalRacedError`; its own code since closure 4 of D2 —
|
|
682
|
+
until then it shared `E_AUTHZ_RENEWAL_UNSAFE`, and automating that code's recipe, `unrelate` + `relate`, here would
|
|
683
|
+
reopen the window with the deny already inside the group) marked partial with `renewalApplied: true` and `carriedDeny`
|
|
684
|
+
naming the deny (the final state is right: there is nothing to rewrite; review that subject's decisions in the window);
|
|
685
|
+
if the second `Write` failed, the 503
|
|
686
|
+
keeps its class and code, is marked partial and carries `carriedDeny` and the recipe (write the membership again: it is
|
|
687
|
+
gone, so `relate` is ONE `Write`). It does not close the window: FGA has no primitive to serialise a `deny` against a
|
|
688
|
+
renewal. A membership that is ALREADY EXPIRED carries nothing (it connects nothing), so rewriting it is not refused,
|
|
689
|
+
and neither is a membership whose only path to a deny is an expired nesting edge. `database` renews in one transaction
|
|
690
|
+
and does not refuse.
|
|
691
|
+
The same rule holds for the role facts' twin: `grant` with another expiry on `openfga` is `deleteTuples` +
|
|
692
|
+
`writeTuples`; if the second fails the `assignee` is already gone (fail-closed: the grant is lost), and since closure 3
|
|
693
|
+
of D2 the 503 is marked partial and `onWrite` gets `indeterminate: true` (until then: "not written", no event); so is
|
|
694
|
+
`purgeScope` of roles on `openfga` (several `Write`s: structure, facts, denies, batches of 100) cut after its first one
|
|
695
|
+
(closure 4 of D2; `indeterminate: true`, as its relations twin). The capability is
|
|
696
|
+
**`atomicRenewal`** (optional, normalised to `false`; `database` `true`, `openfga` `false`): a driver without it must
|
|
697
|
+
refuse that renewal the same way — the runner judges both faces — and `authz:relations:reconcile` does not ask it to renew
|
|
698
|
+
a carrying membership: the plan lists it as the destination residue `renewal-unsafe` (same numbers in `--dry-run` and the
|
|
699
|
+
real pass, a line starting with `sin renovar (renewal-unsafe) destino:` with the recipe, exit ≠ 0). **Not renewing is not
|
|
700
|
+
neutral over time**: the destination would keep the OLD expiry, and from it (an extension) the membership no longer
|
|
701
|
+
carries its deny, or until it (a shortening) it still grants what the source stopped granting. **So the PARTITION is
|
|
702
|
+
refused** (closure 5 of D2, owner's decision: option A): a partition with at least one `renewal-unsafe` or
|
|
703
|
+
`deny-conditional` (below) gets **zero writes and zero deletes** — with `--prune` too, expired leftovers included —, its
|
|
704
|
+
destination stays **exactly as it was**, with its previous drift in view, and **every other partition migrates**. The plan
|
|
705
|
+
decides it, so `--dry-run` and the real pass give the same numbers; `report.partitionsRefused` lists each one with its
|
|
706
|
+
causes, its recipe and what is pending (`write`/`update`/`delete`), its row in `report.partitions` has `status:
|
|
707
|
+
'refused'`, and the command prints one line starting with `partición NEGADA <partition>:` that names the partition and
|
|
708
|
+
the causes and says that its destination stays as it was (exit ≠ 0 while it exists). Refusing does not make the
|
|
709
|
+
destination more restrictive than the source: it leaves it as it was — what it already granted, it still grants, and
|
|
710
|
+
the line shows what is pending. **And "as it was" is not static** (closure 6 of D2): a refused partition IS "not
|
|
711
|
+
renewing", so the drift over time stays — from the old expiry of an extended carrying membership the destination no
|
|
712
|
+
longer carries its deny and **grants what that deny cuts** (if another path grants it) while the source still denies.
|
|
713
|
+
The line says when and what (`a partir de <date> el destino concederá a <subject> lo que cortan <denies>`; in the report,
|
|
714
|
+
`partitionsRefused[].evolves`). Ways out for a refused partition, none free: `unrelate` the membership in the SOURCE and
|
|
715
|
+
repeat; or `unrelate` + `relate` it in the DESTINATION with the source's expiry (or none), accepting the window; or wait
|
|
716
|
+
for the destination's old expiry — then the tuple is absent for the plan, the rewrite is a plain write and the partition
|
|
717
|
+
migrates on its own, with the interval above in between. Making the membership expiry-less in the SOURCE does NOT unlock
|
|
718
|
+
it (the destination's still expires: that is still an expiry change). (Closure 4 withheld, tuple by tuple, what the lost deny would let grant, with a
|
|
719
|
+
pass-wide refusal `carrier-conflict`; measured, that left grants the destination already gave in place without saying so,
|
|
720
|
+
and one tenant could block every other partition with `--prune`. Both are gone.)
|
|
721
|
+
**The limit: changing the expiry of a deny-carrying membership on `openfga`.** FGA cannot replace the same key in one
|
|
722
|
+
`Write` (no upsert: [openfga/openfga#2151](https://github.com/openfga/openfga/issues/2151)), so a renewal is two `Write`s,
|
|
723
|
+
and between them the subject does not get the group's deny. Therefore, on `openfga`: `relate` with another expiry on a
|
|
724
|
+
membership whose group (or a group containing it) carries a live deny is **409 `E_AUTHZ_RENEWAL_UNSAFE`, zero writes**;
|
|
725
|
+
the reconcile refuses that partition when its plan counts that deny (a deny EXPIRED at the pass's `now` carries nothing,
|
|
726
|
+
for the plan and for the driver alike — closure 6 of D2; until then the driver counted it and the real pass aborted with
|
|
727
|
+
a 409 the dry run had not announced). A deny the plan does NOT see can still reach the driver's 409 and abort the pass
|
|
728
|
+
instead (not measured; e.g. a leftover deny on a relation the destination no longer declares).
|
|
729
|
+
**There is no window-free way to do it.** The recommended pattern, for the future: memberships
|
|
730
|
+
**without expiry** in groups that carry a deny, ended with `unrelate`. If an expiry must change: `unrelate` + `relate`,
|
|
731
|
+
**accepting the window** — while it lasts the subject does not get the group's deny and is granted whatever another path
|
|
732
|
+
gives it and that deny cuts. **Declared window (TOCTOU of N1):** a NEW deny on a group written while ANOTHER membership of
|
|
733
|
+
that group is being renewed (its two `Write`s) does not reach that subject in between — the driver re-checks after the
|
|
734
|
+
renewal and says so (`E_AUTHZ_RENEWAL_RACED`), but it cannot close it. `database` renews in one transaction and has
|
|
735
|
+
none of this. **When OpenFGA offers an upsert, the renewal will be one `Write` and `atomicRenewal` will become `true`**
|
|
736
|
+
on `openfga`, without touching the model. **A destination tuple
|
|
737
|
+
that is expired at the pass's `now` is ABSENT for the plan** (closure 3 of D2): rewriting it is a plain write (never a
|
|
738
|
+
renewal, never `renewal-unsafe`), it is not part of the order's context (no carrier, no edge of the self-cut, no
|
|
739
|
+
"renewal" in phase 4), and with `--prune` the expired leftovers are deleted first and outside the order (they connect
|
|
740
|
+
nothing). Until closure 3 an expired carrying membership was taken as live: the pass refused to "renew" it while it
|
|
741
|
+
wrote the new allow, and the destination granted to a subject the source denied, pass after pass.
|
|
742
|
+
**`database` honours a `denied_*` row always**, also on a type without the opt-in (a deny row is a fact): retiring
|
|
743
|
+
`deny` from a type with live denies does not switch them off — remove them with `removeDeny`. Both drivers declare the
|
|
744
|
+
capability `relationDenies` (optional, normalised to `false`); a driver without it answers the four deny operations
|
|
745
|
+
with 500 `E_AUTHZ_UNSUPPORTED` and zero calls.
|
|
746
|
+
|
|
747
|
+
**`decideMany` is the chain primitive.** `decideMany(subject, relation, objects, partition)` →
|
|
748
|
+
`('allow' | 'deny' | 'none')[]` by position, with the same guarantees as `checkMany` (1..50 objects, `[]` and 51 are 422
|
|
749
|
+
`E_AUTHZ_CHECK_MANY_BATCH`, F-05 per item, one `now` per batch, a per-item error is a 503 for the whole batch — never
|
|
750
|
+
`none`). `allow` if `check` grants on that object; otherwise `deny` if the subject has a live deny (direct or through a
|
|
751
|
+
group) on any relation of `up*(down*(R))` on it; otherwise `none`. *`decideMany` is the chain primitive: walk from the
|
|
752
|
+
leaf up and stop at the first non-`none`. `deny` means "do not inherit past this object"; it is broader than `check` on
|
|
753
|
+
purpose (fail-closed). A deny given to `org#member` does not reach a guest who is not a member: to make a folder grant
|
|
754
|
+
only what it holds directly, break inheritance in your evaluator (`inherits = false`).* It answers **by the partition of
|
|
755
|
+
the object**: a guest holder with an allow in organisation A and a deny on the same object id in organisation B gets
|
|
756
|
+
`allow` in A and `deny` in B (and `check`/`checkMany` agree). Do not use `checkMany` for a chain: with the deny on the
|
|
757
|
+
middle object and the allow on its parent, `checkMany` says `[false, false, true]` and `decideMany` says `[none, deny,
|
|
758
|
+
allow]`.
|
|
759
|
+
|
|
760
|
+
**Writes that remove access can GIVE access back — declared rows, each with its case.** With a deny on `g#member`:
|
|
761
|
+
`unrelate(u, member, g)` gives `u` back the allow it holds directly (S8), `purgeObject(group:g)` does the same (S9), and
|
|
762
|
+
`purgeSubject(g#member)` deletes the group's denies (S10) (*"S8–S10 · FILAS DECLARADAS"*). A policy that treats "leave a
|
|
763
|
+
group" or "purge" as safe has to know it. And an `openfga` pod still pinned to a model **without** `denied_*` grants
|
|
764
|
+
over a denied object (S11, *"S11 · FILA DECLARADA"*): that is why the rollout above pins every pod before phase 2.
|
|
765
|
+
|
|
600
766
|
**A pure, stable key for a tuple (2.5.0).** `relationTupleKey({ partition, object, relation, subject, kind? })`
|
|
601
767
|
(root export) is deterministic and injective over exactly those five components — the expiry is not part of it
|
|
602
768
|
(renewing a share keeps its key, even though `database` rewrites the row with a new uuid) and neither is any
|
|
@@ -647,8 +813,9 @@ that grammar (2.5.0 · B3)**: the only userset the fused model admits as a subje
|
|
|
647
813
|
that takes a subject, through the same single function in the manager, both drivers and the published double — until
|
|
648
814
|
2.5.0 `database` stored that row inert and `openfga` surfaced the server's 400 as a 503.
|
|
649
815
|
|
|
650
|
-
**And since `2.4.0-alpha.3` F-05 covers **all
|
|
651
|
-
`checkMany` (per item) since 2.5.0
|
|
816
|
+
**And since `2.4.0-alpha.3` F-05 covers **all thirteen operations of the port** — the eight of alpha.3 plus
|
|
817
|
+
`checkMany` (per item) since 2.5.0 and `deny`, `removeDeny`, `listDenies` and `decideMany` (per item) since
|
|
818
|
+
2.5.0-alpha.3 —, not only the two writes that compose a tuple.** The alpha.3 audit found the chokepoint one call-site short (pre-existing in alpha.2, in
|
|
652
819
|
code alpha.3 had just rewritten): `purgeObject`/`purgeSubject`/`check`/`listObjects`/`listSubjects`/`membersOf`
|
|
653
820
|
never called `assertRelationDeclared`, so in the shared store `purgeObject({ type: 'role_binding', id: R }, S)`
|
|
654
821
|
composed — byte for byte — the id of a real binding and **deleted the real binding** with proof of zero
|
|
@@ -722,7 +889,84 @@ destination does not declare); `--prune` deletes what the source no longer backs
|
|
|
722
889
|
does not admit (`details`: `invalid-identity`, `userset-unsupported`, `undeclared` — a relation or type the
|
|
723
890
|
destination no longer declares), which no flag touches: listed, exit ≠ 0 in every mode, deleted by hand or, for
|
|
724
891
|
`undeclared`, declared again ([Upgrading, 6](#upgrading-to-250)); a 422 of the relations family from the destination's
|
|
725
|
-
`unrelate` is classified the same way and never aborts the pass. **
|
|
892
|
+
`unrelate` is classified the same way and never aborts the pass. **Denies migrate as denies (2.5.0-alpha.3):**
|
|
893
|
+
`enumerateRelations` marks every tuple with `kind` (`denied_viewer` is enumerated as `viewer` + `kind: 'deny'`), the key
|
|
894
|
+
carries it, and the pass applies the WHOLE transition of each partition —writes, renewals and, with `--prune`, deletes—
|
|
895
|
+
as **one sequence**, in the order of ONE function shared with the `openfga` purges (`relationTransitionOrder`,
|
|
896
|
+
`src/relations/tuple_order.ts`), in five phases: (1) new denies (`deny`); (2) with `--prune`, leftover allows on
|
|
897
|
+
non-`group` objects; (3) the group memberships (`group:*#member`) that come in and go out, in the order of a
|
|
898
|
+
**precedence graph** (computed in the union of both states: between two memberships of a common subject, one that brings
|
|
899
|
+
a deny on X goes before one that brings an allow on X; one that leaves carrying an allow on X before one that leaves
|
|
900
|
+
carrying a deny on X; one that leaves before one that comes in on the same nested path when the composed path would
|
|
901
|
+
carry an allow; one that comes in carrying a deny before one that leaves carrying a deny that protects an allow of the
|
|
902
|
+
subject; since closure 4 of D2 a membership RENEWAL is in that graph too, as the entry —or, if it shortens, the exit— it
|
|
903
|
+
becomes at the shorter of its two expiries); (4) new allows and the renewals of allows that extend (one that shortens
|
|
904
|
+
goes with the deletes of phase 2); (5) with `--prune`, the leftover denies **last** (`removeDeny`, never
|
|
905
|
+
`unrelate`). Never the source's enumeration order (`database` enumerates by creation: a document shared on Monday, its
|
|
906
|
+
user put in a denied group on Tuesday). Until closure 2 of sublot D2 every write went before every delete, and a former
|
|
907
|
+
member composed with a new allow (U ∈ g leaves, `g#member viewer X` arrives: cut after the first write, U was granted X).
|
|
908
|
+
**What that guarantees, exactly — demonstrated by brute force, not by hand-made tables**: **if the precedence graph has
|
|
909
|
+
no cycle, no state of a pass cut after any write grants something neither the initial nor the final state granted, and
|
|
910
|
+
no subject denied in both is granted in any.** The **residue is the cycle** — two memberships changed in the same pass
|
|
911
|
+
whose order constraints contradict each other; the typical shape: the same subject joins (or leaves) two groups at once,
|
|
912
|
+
and each brings a deny that cuts an allow the other brings — and there, in general, **no order of the diff is safe**;
|
|
913
|
+
the pass applies them in a fixed order and reports them (`report.orderResidue`, a warning line starting with
|
|
914
|
+
`orden SIN garantía`: the final state is right, so it is not drift; if the window matters, apply them in two passes). The
|
|
915
|
+
demonstration (`tests/relations_order_exhaustive.spec.ts`, oracle = the package's own path cut, both with `--prune` and
|
|
916
|
+
without): **exhaustive** over 1 user, 2 groups with one nesting level, 2 objects and one relation with deny (10 possible
|
|
917
|
+
tuples, every (initial, final) pair: 2,097,152 pairs, ~23 s) — 0 pairs grant without a declared residue; 4,176 pairs have
|
|
918
|
+
a cycle, 816 of them grant in some prefix and 352 of those admit no safe order of the diff at all — plus a seeded sample
|
|
919
|
+
of 30,000 pairs per mode of the universe with 2 users and direct/group allows and denies (21 tuples) and of one with three
|
|
920
|
+
groups and two nesting levels (20 tuples): 0 without residue. **Group membership counts as a grant** (closure 5 of D2):
|
|
921
|
+
the oracle also judges `(user, group:G)` — being a member is itself a grant (`check(member)`, `membersOf`) —, so a pass
|
|
922
|
+
cut between `+U ∈ h` and `−h ⊂ g` must not make U a member of g, which neither state did; the order gives that pair a
|
|
923
|
+
precedence (the price, declared: more pairs with a cycle in the two-level universe, 4,188 of 60,000 sampled, all of them
|
|
924
|
+
reported as order residue). **Validity and the time HORIZON are part of the universe**
|
|
925
|
+
(closures 3 and 4 of D2): every destination tuple is live (expiring at `now + 1 h`), expired or absent, and the source
|
|
926
|
+
has it absent or live with the same, a LATER or an EARLIER expiry (8 states per tuple); the plan is derived by the same
|
|
927
|
+
rules as the pass (`liveInDestination`: expired = absent; `deny-conditional` in the plan; a partition with a
|
|
928
|
+
`renewal-unsafe` or a `deny-conditional` is refused — zero writes, so its state is the initial one at every instant: the
|
|
929
|
+
ONLY exclusion), renewals are steps (one write on an `atomicRenewal` destination; two `Write`s with the gap checked as a
|
|
930
|
+
prefix on `openfga`), and **every prefix and the final state are judged at
|
|
931
|
+
`now` and at every later expiry** (`+30 min`, `+1 h`), each tuple with ITS expiry (the destination's if the pass did not
|
|
932
|
+
touch it, the source's if it wrote or renewed it, none for a deny it wrote). Property: at no instant of the horizon does a
|
|
933
|
+
prefix grant outside initial ∪ source, or to a subject denied in both, without a declared order residue; and the final
|
|
934
|
+
state never grants what the source denies at that instant — no exception (closure 4 counted a `preexisting` grant as
|
|
935
|
+
declared; it is gone with the withholding). **Exhaustive** over the reduced
|
|
936
|
+
universe (8 tuples: 33,554,432 combinations per destination kind, `AUTHZ_ORDER_VIGENCIA_EXHAUSTIVE=1`) and seeded
|
|
937
|
+
samples of the four universes in the default suite — 0 grant without a declared residue. Without the destination's config (`toConfig`) the order does
|
|
938
|
+
not assume any allow is cut by a deny (more precedences, more residue, never less guarantee). Surface-guard properties
|
|
939
|
+
`relations.order.exhaustive` (the brute force over a reduced universe, 131,072 pairs, plus a declared sample of 50,000
|
|
940
|
+
combinations per mode with validity and horizon, on every mutant of the order),
|
|
941
|
+
`relations.reconcile.order` and `openfga.purge.order`, the last two judged with the source enumerated in both orders. **The destination driver decides, before the first write, whether it can take each deny**
|
|
942
|
+
(`capabilities.relationDenies` and its own `denyPhase(type, relation)`: the opt-in of ITS config, and on `openfga` its
|
|
943
|
+
pinned model, D-31 — never the persisted spec): a source deny that the destination cannot write (`no-capability`,
|
|
944
|
+
`no-deny-phase`, `phase` — the type is not in `deny: 'write'` there —, `undeclared`, `model`, `unmarked` — a source driver
|
|
945
|
+
that does not mark `kind`, on either side —, `invalid`) makes the pass refuse with 500 `E_AUTHZ_DENY_MIGRATION_REFUSED` and **zero
|
|
946
|
+
writes** in every partition — never `skipped`: writing the allows without the deny would grant what the source denies.
|
|
947
|
+
`--dry-run` does not throw: one line per refusal starting with `deny NO migrable`, and exit ≠ 0. **Each refusal carries
|
|
948
|
+
the recipe of ITS reason** (`DenyRefusal.recipe`): `phase` — the type in `deny: 'write'` in the destination's config
|
|
949
|
+
(provision → pin → deploy); `undeclared` — a relation RETIRED with live denies: re-declare the relation temporarily in the
|
|
950
|
+
config of the process that writes the source (the relation is enough; `removeDeny` needs no opt-in), `removeDeny` those
|
|
951
|
+
denies, retire it again and repeat (`removeDeny` on an undeclared pair is 422, F-05; executed in the suite); `model` —
|
|
952
|
+
publish and pin the destination's model; `unmarked` — update the driver that does not mark `kind`; `invalid` — delete
|
|
953
|
+
the row by hand (the API would reject `removeDeny` too); `no-capability`/`no-deny-phase` — a destination that can write
|
|
954
|
+
denies. (A partition refused by the plan, above, writes nothing, so its denies do not ask for a phase.) A deny already present in a destination WITHOUT the opt-in is refused too (`phase`) although `database` would
|
|
955
|
+
honour the row (D8): `denyPhase = null` means «no opt-in» and the pass does not certify what the destination does not
|
|
956
|
+
declare. **A destination deny with an expiry** (only writable from outside the package, D3) —live with another expiry, or
|
|
957
|
+
ALREADY EXPIRED— that the source has is rewritten permanent on `database`; on a destination without `atomicRenewal`
|
|
958
|
+
(`openfga`: it cannot replace the same key in a `Write`, M5, and its `deny` there is 500 `E_AUTHZ_CONFIG` with the recipe
|
|
959
|
+
`removeDeny` + `deny`, a declared window between the two) **the plan decides it** (closure 4 of D2; same numbers in
|
|
960
|
+
`--dry-run` and the real pass): the destination residue `deny-conditional` (exit ≠ 0), not rewritten, and **its partition
|
|
961
|
+
is refused** (closure 5, above) — an expired one denies nothing, and until closure 4 the pass still wrote the allow it cut
|
|
962
|
+
(the destination granted, and `--dry-run` reported a different count). A driver that declares
|
|
963
|
+
`atomicRenewal` and still answers that 500 contradicts its capability: the error propagates. The report carries
|
|
964
|
+
`denies: { written, updated, deleted, unchanged, extra }` (a subset of the totals, printed as its own `denies:` line),
|
|
965
|
+
`denyRefused` (dry-run) and `partitions` (one row per partition with its status); a pass that blows up halfway throws
|
|
966
|
+
with `error.report` (`partial: true`: the partitions `done`, the one that `failed` with what it wrote, the `pending`
|
|
967
|
+
ones) and the command prints it, one `pasada PARCIAL:` line per partition; one that fails before writing anything has
|
|
968
|
+
`partial: false` and prints `pasada FALLIDA sin escribir nada:`. **The totals `written`/`unchanged`/`extra` include the
|
|
969
|
+
denies** (2.5.0-alpha.3, a change of meaning): subtract `denies` to count allows only. **Never towards the
|
|
726
970
|
ACTIVE relations driver** (2.4.0-alpha.10): once `relations.default ?? config.default` is `--to`, any
|
|
727
971
|
other source — with `--from` or picked by elimination — is 500 `E_AUTHZ_STALE_SOURCE_REFUSED` (it
|
|
728
972
|
would resurrect shares revoked since the cutover and, with `--prune`, delete the new ones); `--dry-run`
|
|
@@ -814,8 +1058,14 @@ driver tolerates old `NULL` rows on read meanwhile.
|
|
|
814
1058
|
| `unrelate` | `subject`, `relation`, `object`, `partition`, `actor` | — |
|
|
815
1059
|
| `purgeObject` | `object`, `partition`, `actor` | no `subject`, no `relation` |
|
|
816
1060
|
| `purgeSubject` | `subject`, `partition`, `actor` | no `object`, no `relation` |
|
|
1061
|
+
| `deny` (2.5.0) | `subject`, `relation` (the public name, never `denied_<rel>`), `object`, `partition`, `actor` | no `expiresAt` (a deny does not expire) |
|
|
1062
|
+
| `removeDeny` (2.5.0) | `subject`, `relation`, `object`, `partition`, `actor` | no `expiresAt` |
|
|
817
1063
|
|
|
818
|
-
|
|
1064
|
+
**`operation` widened in 2.5.0 with `deny` and `removeDeny`** — on the event and on the `RelationRef` that `assertWrite`
|
|
1065
|
+
receives. TypeScript does not warn about a widened union inside an `if`/`else`: **a handler that treats everything that is
|
|
1066
|
+
not `unrelate` as an addition grants too much** (it would index a deny as a share). Switch on the operation.
|
|
1067
|
+
|
|
1068
|
+
A purge notifies **one event per call** — even when the driver sweeps both spellings of the partition uuid underneath, and even when it deletes many tuples — and it **does not count**: the port returns `void` (a count would break the port for third-party drivers, and inside `{ transaction }` it would be a number your rollback contradicts). If you need the "how many", call `enumerateRelations` (or `listSubjects`) before purging — inside your transaction, through your transaction. And a purge that deletes nothing notifies all the same: the auditable fact is *someone asked to delete the keys of X*, whether or not there were any. `scopes.detached` on the roles port emits one `scope_purged` per spelling because there the **manager** canonicalises; the relations manager does not, so the divergence is on purpose and pinned (*"E6 · UN evento por llamada aunque el driver barra DOS ortografías"*). A purge does not go through `assertWrite` — `RelationRef.operation` is `'relate' | 'unrelate' | 'deny' | 'removeDeny'` (the four writes on ONE tuple), the published type of the R-13 hook — and that is pinned too (*"E9 · negativo declarado"*). But F-05 runs first on the purges too (2.4.0-alpha.3, closing the audit's critical finding): `purgeObject` validates the object type and `purgeSubject` the userset of its subject **before** the actor, the scope and the driver, so a purge of a `role_binding` is 422 with zero driver calls and zero events — see [F-05 on the seven operations](#relations-rebac-24) above. `purgeSubject` also validates its subject like `relate` does (a malformed holder or `{}` is 422 `E_AUTHZ_INVALID_IDENTITY` and never reaches the event).
|
|
819
1069
|
|
|
820
1070
|
**The four guarantees, on the four writes** (`relate`, `unrelate`, `purgeObject`, `purgeSubject`):
|
|
821
1071
|
|
|
@@ -867,7 +1117,7 @@ Every error the package raises carries `status` and `code`. A standard AdonisJS
|
|
|
867
1117
|
| `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 |
|
|
868
1118
|
| `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 |
|
|
869
1119
|
| `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) |
|
|
870
|
-
| `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), **or one that is already committed or rolled back** (`isCompleted`, 2.4.0-alpha.8 — the same check in the roles and relations drivers; it used to surface as a 503); `sqlScopeOutbox()` enqueueing without `{ transaction }` (`requireTransaction`, 2.4.0-alpha.8; `true` by default since 2.5.0 · B6, `false` is the explicit opt-out); `relayScopeChanges({ deadDetail })` with a value other than `'full'`; `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) |
|
|
1120
|
+
| `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), **or one that is already committed or rolled back** (`isCompleted`, 2.4.0-alpha.8 — the same check in the roles and relations drivers; it used to surface as a 503); `sqlScopeOutbox()` enqueueing without `{ transaction }` (`requireTransaction`, 2.4.0-alpha.8; `true` by default since 2.5.0 · B6, `false` is the explicit opt-out); `relayScopeChanges({ deadDetail })` with a value other than `'full'`; `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); an `openfga` relations purge against a server whose `--max-tuples-per-write` is below `RELATIONS_PURGE_WRITE_MAX` (100) — the server's 400 `exceeded_entity_limit`, marked as a partial write if an earlier `Write` already deleted (2.5.0) |
|
|
871
1121
|
| `E_AUTHZ_ROLE_IS_NOT_ACCESS` | 500 | `appAccess({ role })` |
|
|
872
1122
|
| `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) |
|
|
873
1123
|
| `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) |
|
|
@@ -875,7 +1125,11 @@ Every error the package raises carries `status` and `code`. A standard AdonisJS
|
|
|
875
1125
|
| `E_AUTHZ_STALE_SOURCE_REFUSED` | 500 | `authz:reconcile --to=<config.default>` (or `authz:relations:reconcile --to=<relations.default ?? default>`) with any resolved source other than the destination itself — `--from`, picked by elimination, or `authz_*` (2.4.0-alpha.10): the stale direction — the source stopped seeing the active driver's facts at the cutover, so the pass would resurrect every revocation made since. Nothing was read, frozen or written; for roles the maintenance pass is `--to=<active>` without `--from`, and `--allow-stale-source` is the human decision (`--dry-run` flags `report.staleSource` instead of throwing) |
|
|
876
1126
|
| `E_AUTHZ_OPERATOR_WINDOW_EXPIRED` | 503 | `freeze()` — and with it `authz:reconcile`, `authz:relations:reconcile`, `withFrozenWrites`, `authz:freeze` — found the OPERATOR window expired (2.4.0-alpha.10): taking a freeze over it would erase the trace that writers resumed mid-cutover. Acknowledge it with `authz:unfreeze`, reopen with `authz:freeze` |
|
|
877
1127
|
| `E_AUTHZ_MASS_RECONCILE_REFUSED` | 500 | `authz:reconcile --prune` would delete facts from the destination while the source has not contributed a single **usable** one — empty, or every fact it returned was skipped (expired, on scopes your tree no longer resolves): the signature of a wrong connection, of the wrong source, or of a blind resolver. Nothing was deleted; pass `--allow-mass-delete` if you really mean to empty it (`--dry-run` flags it instead of throwing). A source that is merely **stale** but still usable is covered by reading the facts from whoever owns them |
|
|
1128
|
+
| `E_AUTHZ_RENEWAL_UNSAFE` | 409 | `relate` on `openfga` (closure 2 of sublot D2) renewing — another expiry on an existing tuple — a `group:*#member` membership whose group, or a group containing it, is the subject of a live deny of the partition: the renewal is two `Write`s there and between them the subject would be granted. **Always zero writes**; the message carries the honest recipe (closure 5 of D2): there is no window-free way on `openfga` (openfga/openfga#2151); memberships without expiry in groups with a deny, or `unrelate` + `relate` accepting the window. A driver declaring `atomicRenewal: false` must answer it the same way. An already EXPIRED membership is not refused (closure 3 of D2) |
|
|
1129
|
+
| `E_AUTHZ_RENEWAL_RACED` | 409 | `RenewalRacedError` (closure 4 of D2; until then the same `E_AUTHZ_RENEWAL_UNSAFE`): an allowed two-`Write` renewal on `openfga` WENT THROUGH, and the re-check after it finds a deny that arrived concurrently and did not reach the subject between the two `Write`s. Marked partial (`indeterminate: true`), `renewalApplied: true`, `carriedDeny`. The final state is right: do NOT apply the `E_AUTHZ_RENEWAL_UNSAFE` recipe (it would reopen the window with the deny inside); review the subject's decisions in the window |
|
|
1130
|
+
| `E_AUTHZ_DENY_MIGRATION_REFUSED` | 500 | `authz:relations:reconcile` / `runRelationsReconcile` (2.5.0-alpha.3): the source carries a live deny the **destination driver** cannot take — no `relationDenies`, no `denyPhase`, the type not in `deny: 'write'` in ITS config, the pair undeclared there, its pinned model without `denied_*`, or a source that does not mark `kind`. Decided in the plan phase: **zero writes** in every partition, never `skipped` (the allows alone would grant what the source denies). `error.refusals` lists them; `--dry-run` flags `report.denyRefused` instead of throwing (`deny NO migrable (<reason>)`, exit ≠ 0) |
|
|
878
1131
|
| `E_AUTHZ_MASS_PURGE_REFUSED` | 500 | `pruneOrphanRoles({ force: true })` would purge every distinct owner (or more than half the local roles): the signature of a blind `resolveChain`. Nothing was deleted; pass `allowMassPurge: true` (`--allow-mass-purge`) if the prune is real |
|
|
1132
|
+
| `E_AUTHZ_MEMBERS_DENY_LIMIT` | 500 | `membersOf` on `database` over an object with a live deny and more than `RELATIONS_MEMBERS_DENY_CHECK_MAX` (1,000) candidates; never a partial list. 500 and not 422: it is not an invalid question from the caller but a bound of the deployment (precedent `E_AUTHZ_TOO_MANY_LOCAL_ROLES`) (2.5.0) |
|
|
879
1133
|
| `E_AUTHZ_TOO_MANY_LOCAL_ROLES` | 500 | more local roles than `maxLocalRoles` (10 000) in a `prune-orphans` pass; never a partial list |
|
|
880
1134
|
| `E_AUTHZ_UNSUPPORTED` | 500 | a primitive needs an optional port method the active driver lacks: `listDenies` (2.1; also behind `defineScopedRole`), `purgeRole` (2.2 — behind `deleteScopedRole`, `authz:catalog:prune-orphans` and, before writing anything, `defineScopedRole`; the `openfga` driver until 2.3); `reconcile` (2.3 — `authz:reconcile --to=<driver>`); `enumerateFacts` (2.3 — being the *source* of `authz:reconcile`; the `database` driver does not implement it: its facts are `authz_*`. Also raised when `--to` is the **active** driver and declares `hierarchyFacts`: its facts are its own, so the pass has to be able to read them instead of rebuilding it from `authz_*`). Also `{ transaction }` on `grant`/`revoke`/`deny`/`removeDeny` or `relate`/`unrelate`/`purge*` when the active driver declares `transactionalWrites: false` (2.4 — the message names the driver and the operation, nothing was called; the way out is the `database` driver or `requireTransactionalWrites`; the `openfga` drivers raise it themselves too, so `manager.driver()` is not a way out — see [the recipe by direction](#openfga-the-recipe-by-direction)), and `{ transaction }` on the delegation API (`defineScopedRole`/`updateScopedRole`/`deleteScopedRole`: the catalog is written through `withAuthzCatalogWrite`, the cross-process serializer, which a caller's transaction would defeat) |
|
|
881
1135
|
| `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 % |
|
|
@@ -1297,7 +1551,12 @@ Every bound the package enforces or measured, in one place, each with the consta
|
|
|
1297
1551
|
| 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) |
|
|
1298
1552
|
| Checks per `batchCheck` | `FGA_MAX_BATCH_CHECK` | 50 | `openfga`, `authorizeMany` | the driver splits into batches of 50 (the SDK's cap), nothing to do |
|
|
1299
1553
|
| `ListObjects` effective cut of the server (relations `listObjects`) | `listObjectsDeadlineMs` (`DEFAULT_LIST_OBJECTS_DEADLINE_MS`) | 3,000 ms | the `openfga` relations driver (2.5.0 · B4) | a call that takes ≥ `LIST_OBJECTS_DEADLINE_RATIO` (0.9) × this value returns `truncated: true` — the server cuts at its deadline with a 200 and no signal; set it to the **lowest** of your server's `--listObjects-deadline` (when not 0), `--request-timeout` (3 s by default) and, over HTTP, `--http-upstream-timeout` (5 s by default) — declare that lowest one: with `--listObjects-deadline=0` the request timeout still cuts with a mute 200 (measured on OpenFGA v1.19.0), there is no "no deadline", `Infinity` is 500, both at 0 is not supported (the gateway's cut is not measured for `ListObjects`), and the driver's `timeoutMs` (5 s by default) cannot be lower than `listObjectsDeadlineMs` (500 at construction: the signal has to fire before the client cuts; `Number.MAX_SAFE_INTEGER` is caught by that). **The price of the conservative signal**: with the default, every `ListObjects` that takes 2,700 ms or more comes back `truncated` even when complete; a consumer that treats `truncated` as a 503 pays that p99 as user-facing 503s — raise the server's deadline (and this value) or handle `truncated` as a partial list, not as an outage |
|
|
1300
|
-
| Objects per `checkMany` | `RELATIONS_CHECK_MANY_MAX` | 50 | the `RelationsManager` and both relations drivers (2.5.0) | 422 `E_AUTHZ_CHECK_MANY_BATCH` with zero backend calls — split the list yourself; `[]` is 422 too. One batch is one `batchCheck` in `openfga` and one query in `database` |
|
|
1554
|
+
| Objects per `checkMany` / `decideMany` | `RELATIONS_CHECK_MANY_MAX` | 50 | the `RelationsManager` and both relations drivers (2.5.0) | 422 `E_AUTHZ_CHECK_MANY_BATCH` with zero backend calls — split the list yourself; `[]` is 422 too. One batch is one `batchCheck` in `openfga` and one query in `database` |
|
|
1555
|
+
| `membersOf` candidates judged one by one when the object has a deny (`database`) | `RELATIONS_MEMBERS_DENY_CHECK_MAX` | 1,000 | the `database` relations driver (2.5.0) | 500 `E_AUTHZ_MEMBERS_DENY_LIMIT` before the first `check`, never a partial list; the whole loop also runs under one total deadline (`timeoutMs`), 503 `E_AUTHZ_BACKEND_TIMEOUT` past it. Without a deny row on the object there is no extra query and no bound |
|
|
1556
|
+
| Tuples a relations purge deletes in one atomic `Write` | `RELATIONS_PURGE_WRITE_MAX` | 100 | the `openfga` relations driver (2.5.0) | the server's `--max-tuples-per-write` default (101 is a 400, measured on v1.19.0). Up to it a purge is one `Write`; past it, allows → the subject's group memberships in their own `Write` → denies. With more than 100 memberships a cut purge can grant until repeated (declared row, [Relations](#relations-rebac-24), «Deny on relations»). **Requires `--max-tuples-per-write` ≥ 100 on the server**: below it, the `Write` that passes the server's cap — not necessarily the first — is 500 `E_AUTHZ_CONFIG` naming the requirement (never a 503 that looks transient: repeated, the purge would fail the same way forever), marked as a partial write (`indeterminate: true`) if an earlier `Write` already deleted |
|
|
1557
|
+
| `openfga:provision --store-id` sweep for live denies of a dropped relation | — (no cap: streamed page by page) | — | `openfga:provision --store-id` (2.5.0-alpha.3), only when some model of the store declared a `denied_*` the new one does not | it reads the WHOLE store, page by page, keeping only a count and three examples (memory does not grow with the store, time does: O(store) per run); a server that does not advance (a repeated continuation token) or a page without `tuples` is exit 1 (`provision NEGADO:`) without writing the model — "zero live denies" is never assumed from a sweep that did not finish. Until closure 2 of sublot D2 the sweep stopped at 1,000,000 tuples, and since the store's model history keeps a retired opt-in forever, a bigger store could never be provisioned again |
|
|
1558
|
+
| Models of the store `openfga:provision --store-id` reads to judge the store's state (every published model is one) | `PROVISION_MODEL_HISTORY_MAX` | 10,000 | `openfga:provision --store-id` (2.5.0-alpha.3) | exit 1 (`provision NEGADO:`) without writing the model — never judged on a truncated history |
|
|
1559
|
+
| Relation name in a type with `deny` | `DENIABLE_RELATION_NAME_MAX` | 43 | `defineRelationsConfig` and the `openfga` model generator (2.5.0) | 422 `E_AUTHZ_RELATION_CONFIG` at definition: `denied_<rel>` has to fit FGA's 50. A type without `deny` keeps 50 |
|
|
1301
1560
|
| `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 |
|
|
1302
1561
|
| `enumerateRelations(partition)` / `enumerateRelationsMany(partitions)` cost | — | O(tuples in the store) **per sweep** — one sweep per `enumerateRelations` call, **one sweep for K partitions** with `enumerateRelationsMany` / `runRelationsReconcile(…, { 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** |
|
|
1303
1562
|
| 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 |
|
|
@@ -1800,6 +2059,83 @@ DDL of the published migration (`stubs/migration.stub`) and copy the rows. The d
|
|
|
1800
2059
|
row per tuple: if the two rows raced with different expiries, the survivor is the first written. The `openfga`
|
|
1801
2060
|
driver needs nothing — a tuple is unique by construction.
|
|
1802
2061
|
|
|
2062
|
+
### Upgrading to 2.5.0-alpha.3: rolling out the deny on relations, migrating it, and why there is no rollback below 2.5
|
|
2063
|
+
|
|
2064
|
+
Nothing changes until a type opts in: with no `deny` in `defineRelationsConfig` the fused model is byte for byte the
|
|
2065
|
+
one of 2.4.0-alpha.10 and there is no DDL (a deny is a row of `authz_relations` with `relation = 'denied_<rel>'`).
|
|
2066
|
+
|
|
2067
|
+
**Turning the deny on is two deployments, in this order: provision → pin `modelId` → deploy.** A pod still pinned to a
|
|
2068
|
+
model without `denied_*` grants over a denied object (S11, declared row), and a pod running < 2.5.0-alpha.3 on
|
|
2069
|
+
`database` never looks at the `denied_*` rows — so no deny may be written until the whole fleet serves the new model with
|
|
2070
|
+
the new code:
|
|
2071
|
+
|
|
2072
|
+
1. Put the type in **`deny: 'read'`** and publish the model from that config: `node ace openfga:provision
|
|
2073
|
+
--store-id=$OPENFGA_STORE_ID` (it prints a new `OPENFGA_MODEL_ID`).
|
|
2074
|
+
2. Pin **every** pod to that `modelId` and deploy the fleet on ≥ 2.5.0-alpha.3 with `deny: 'read'`: reads honour
|
|
2075
|
+
denies, `deny()` is still 422 `E_AUTHZ_RELATION_NOT_DENYABLE`. A pod with the new config and the old pin is 500 on
|
|
2076
|
+
first use (D-31), never a grant.
|
|
2077
|
+
3. Verify, then a **second** deployment moves the type to **`deny: 'write'`** (same model: the two phases publish the
|
|
2078
|
+
same rules). Only now are denies written.
|
|
2079
|
+
|
|
2080
|
+
The job `cutover-runbook` runs exactly this (steps 20 and 21, against pg and sqlite-file with a real OpenFGA): phase 1 ⇒
|
|
2081
|
+
`deny` 422 on both drivers; phase 2 ⇒ the deny lands, `authz:relations:reconcile` carries it to the store (`denies:
|
|
2082
|
+
escritos 1`), `check` is false on both drivers; and the S11 table — a pod that was not redeployed, pinned to the old
|
|
2083
|
+
model, grants; the same pod with the new `modelId` does not (the server subtracts); a pod with the new config and the
|
|
2084
|
+
old pin is 500.
|
|
2085
|
+
|
|
2086
|
+
**`openfga:provision --store-id` refuses to mutilate the model.** Publishing from a process whose config lacks the opt-in
|
|
2087
|
+
(a refactor, another branch of the config, an old binary) would drop `denied_*` and switch every deny of that type off,
|
|
2088
|
+
silently. Before writing, the command judges the **state of the store**, not just its last model: it reads every model the
|
|
2089
|
+
store has (`PROVISION_MODEL_HISTORY_MAX`) and, if any of them declared a `<type>#denied_<rel>` of a relation type that the
|
|
2090
|
+
new one does not, it sweeps the store (all of it, streamed page by page with no tuple cap — closure 2 of sublot D2; it
|
|
2091
|
+
costs O(store) per run, and a store with a retired opt-in pays it on every `provision --store-id`) — a live deny of those
|
|
2092
|
+
relations is exit 1
|
|
2093
|
+
(`provision NEGADO: …`) and nothing is written, also when a model without `denied_*` was published in between (a rollback
|
|
2094
|
+
rehearsal with an older binary). Dropping one the **latest** model declares also needs `--allow-drop-deny` (the way to
|
|
2095
|
+
retire an opt-in for real, said out loud); the flag **never switches off a live deny**. Right before
|
|
2096
|
+
`writeAuthorizationModel` the sweep is repeated (`provision NEGADO: el RECUENTO …` if a deny arrived meanwhile: the fleet
|
|
2097
|
+
is still writing denies). **The recount narrows the window, it does not close it**: a deny that lands on a page the
|
|
2098
|
+
recount already read, or between the recount and `writeAuthorizationModel`, is not seen — **step 0 (the whole fleet on
|
|
2099
|
+
`deny: 'read'` before counting) is what closes it**. A sweep that fails halfway is exit 1 too. (`database` honours `denied_*` rows always, also
|
|
2100
|
+
without the opt-in: retiring it there does not switch them off either — remove them with `removeDeny`.)
|
|
2101
|
+
|
|
2102
|
+
**Rolling back below 2.5 with live denies is not supported (D12 · S17, declared row).** A process < 2.5.0-alpha.3 ignores
|
|
2103
|
+
every deny: on `database` its `check` never reads `denied_*`; its `defineRelationsConfig` drops the `deny` key in
|
|
2104
|
+
silence; its `openfga:provision` republishes a model without `but not`; its `authz:relations:reconcile` counts the denies
|
|
2105
|
+
as undeclared relations and migrates the allows. There is no command that makes that safe. **Before any rollback, stop
|
|
2106
|
+
the denies, count them and take them to zero — in this order:**
|
|
2107
|
+
|
|
2108
|
+
0. **Move the whole fleet to `deny: 'read'`** for every type with the opt-in, and deploy it, **before** counting, sweeping
|
|
2109
|
+
or publishing anything: every `deny()` is then 422 `E_AUTHZ_RELATION_NOT_DENYABLE` on every pod and reads still honour
|
|
2110
|
+
the denies. Skipping this leaves a window — counting, publishing, redeploying — in which a tenant's `deny()` lands and
|
|
2111
|
+
is then switched off in silence, for good.
|
|
2112
|
+
1. Count the live denies (below).
|
|
2113
|
+
2. Retire them: `removeDeny` (or the purges) on the active driver and `authz:relations:reconcile … --prune` on the mirror,
|
|
2114
|
+
until the count is 0.
|
|
2115
|
+
3. Publish the model without the opt-in: `node ace openfga:provision --store-id=… --allow-drop-deny` (it sweeps, and
|
|
2116
|
+
re-counts right before writing the model — the recount narrows the window, it does not close it: step 0 does).
|
|
2117
|
+
4. **Count again: it must still be 0.** If it is not, a pod was still in `'write'`: do not pin the new model, go back to 0.
|
|
2118
|
+
5. Pin, redeploy, roll back.
|
|
2119
|
+
|
|
2120
|
+
The count, on `database` (the three engines; executed by the suite):
|
|
2121
|
+
|
|
2122
|
+
```sql
|
|
2123
|
+
-- D12 · live relation denies (rollback below 2.5 requires 0)
|
|
2124
|
+
SELECT count(*) AS n FROM authz_relations WHERE substr(relation, 1, 7) = 'denied_';
|
|
2125
|
+
```
|
|
2126
|
+
|
|
2127
|
+
On `openfga`, `node ace authz:relations:reconcile --to=<other> --from=openfga --dry-run` per partition prints a
|
|
2128
|
+
`denies:` line (`escritos + iguales` are the store's live denies). Retire them with `removeDeny` (or the purges) on the
|
|
2129
|
+
active driver and `authz:relations:reconcile … --prune` on the mirror until the count is 0. Step 21 of the runbook executes
|
|
2130
|
+
this procedure with its EFFECT: with the denies alive both `provision` attempts exit 1 without touching the store; an
|
|
2131
|
+
operator who skips step 0 retires the denies to zero and a pod still in `'write'` writes one more — the count is 1 again
|
|
2132
|
+
and `provision --allow-drop-deny` exits 1 instead of publishing; with the fleet in `'read'` the concurrent `deny` is 422 on
|
|
2133
|
+
both drivers, the count reaches 0 and stays 0, and only then the model is published.
|
|
2134
|
+
|
|
2135
|
+
**A pass of 2.5 never classifies a deny as `undeclared`**: it migrates it as a deny or refuses the whole pass (above).
|
|
2136
|
+
Never run `authz:relations:reconcile` from a process < 2.5.0-alpha.3 once there are denies in a store: it counts them as
|
|
2137
|
+
undeclared, and its `--prune` hands them to `unrelate`.
|
|
2138
|
+
|
|
1803
2139
|
## Compatibility
|
|
1804
2140
|
|
|
1805
2141
|
| | |
|
|
@@ -41,6 +41,12 @@ export declare function relationsReconcileLines(report: RelationsReconcileReport
|
|
|
41
41
|
}>;
|
|
42
42
|
clean: boolean;
|
|
43
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* **La pasada que revienta A MITAD dice qué quedó escrito, por partición** (plan 2.5 · lote D · D2; roadmap 2.5, 🟡 3
|
|
46
|
+
* del auditor de alpha.6): con el reporte parcial que viaja en el error (`error.report`, `partial: true`). Prefijo
|
|
47
|
+
* ESTABLE «pasada PARCIAL:» (punto 7). Función PURA.
|
|
48
|
+
*/
|
|
49
|
+
export declare function partialReportLines(report: RelationsReconcileReport): string[];
|
|
44
50
|
/**
|
|
45
51
|
* **`authz:relations:reconcile`** (Fase 4, lote 4-6) — migra las tuplas de
|
|
46
52
|
* RELACIÓN de un `RelationsDriver` a otro, idempotente y bidireccional, nunca
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authz_relations_reconcile.d.ts","sourceRoot":"","sources":["../../commands/authz_relations_reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAS,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAO7E;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,CAAC,EAAE,UAAU,GAAG,KAAK,GAAG,WAAW,CAAA;CAC1C,GAAG,MAAM,CAaT;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,wBAAwB,GAAG;IACzE,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,
|
|
1
|
+
{"version":3,"file":"authz_relations_reconcile.d.ts","sourceRoot":"","sources":["../../commands/authz_relations_reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAS,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAO7E;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,CAAC,EAAE,UAAU,GAAG,KAAK,GAAG,WAAW,CAAA;CAC1C,GAAG,MAAM,CAaT;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,wBAAwB,GAAG;IACzE,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,CA+QA;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,EAAE,CAoB7E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,OAAO,OAAO,uBAAwB,SAAQ,WAAW;IAC9D,MAAM,CAAC,WAAW,SAA8B;IAChD,MAAM,CAAC,WAAW,SAAgG;IAElH,MAAM,CAAC,OAAO,EAAE,cAAc,CAE7B;IAGO,EAAE,EAAE,MAAM,GAAG,SAAS,CAAA;IAGtB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IAGxB,MAAM,EAAE,OAAO,GAAG,SAAS,CAAA;IAG3B,KAAK,EAAE,OAAO,GAAG,SAAS,CAAA;IAK1B,eAAe,EAAE,OAAO,GAAG,SAAS,CAAA;IAMpC,gBAAgB,EAAE,OAAO,GAAG,SAAS,CAAA;IAGrC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IAGjC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IAEnC,GAAG;CAyNV"}
|