@ddd-ts-kit/openfga 0.0.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -1
  3. package/dist/authorizer/open-fga-authorizer-options.d.ts +18 -0
  4. package/dist/authorizer/open-fga-authorizer-options.d.ts.map +1 -0
  5. package/dist/authorizer/open-fga-authorizer-options.js +2 -0
  6. package/dist/authorizer/open-fga-authorizer-options.js.map +1 -0
  7. package/dist/authorizer/open-fga-authorizer.d.ts +17 -0
  8. package/dist/authorizer/open-fga-authorizer.d.ts.map +1 -0
  9. package/dist/authorizer/open-fga-authorizer.js +53 -0
  10. package/dist/authorizer/open-fga-authorizer.js.map +1 -0
  11. package/dist/client/open-fga-api.d.ts +56 -0
  12. package/dist/client/open-fga-api.d.ts.map +1 -0
  13. package/dist/client/open-fga-api.js +2 -0
  14. package/dist/client/open-fga-api.js.map +1 -0
  15. package/dist/client/open-fga-client-options.d.ts +10 -0
  16. package/dist/client/open-fga-client-options.d.ts.map +1 -0
  17. package/dist/client/open-fga-client-options.js +2 -0
  18. package/dist/client/open-fga-client-options.js.map +1 -0
  19. package/dist/client/open-fga-client.d.ts +5 -0
  20. package/dist/client/open-fga-client.d.ts.map +1 -0
  21. package/dist/client/open-fga-client.js +13 -0
  22. package/dist/client/open-fga-client.js.map +1 -0
  23. package/dist/index.d.ts +9 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +6 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/model/authorization-model-of.d.ts +10 -0
  28. package/dist/model/authorization-model-of.d.ts.map +1 -0
  29. package/dist/model/authorization-model-of.js +34 -0
  30. package/dist/model/authorization-model-of.js.map +1 -0
  31. package/dist/model/canonical-model.d.ts +7 -0
  32. package/dist/model/canonical-model.d.ts.map +1 -0
  33. package/dist/model/canonical-model.js +57 -0
  34. package/dist/model/canonical-model.js.map +1 -0
  35. package/dist/model/ensure-authorization-model.d.ts +13 -0
  36. package/dist/model/ensure-authorization-model.d.ts.map +1 -0
  37. package/dist/model/ensure-authorization-model.js +19 -0
  38. package/dist/model/ensure-authorization-model.js.map +1 -0
  39. package/dist/model/model-drift.d.ts +8 -0
  40. package/dist/model/model-drift.d.ts.map +1 -0
  41. package/dist/model/model-drift.js +28 -0
  42. package/dist/model/model-drift.js.map +1 -0
  43. package/dist/model/open-fga-model.d.ts +45 -0
  44. package/dist/model/open-fga-model.d.ts.map +1 -0
  45. package/dist/model/open-fga-model.js +2 -0
  46. package/dist/model/open-fga-model.js.map +1 -0
  47. package/dist/model/userset-of.d.ts +8 -0
  48. package/dist/model/userset-of.d.ts.map +1 -0
  49. package/dist/model/userset-of.js +25 -0
  50. package/dist/model/userset-of.js.map +1 -0
  51. package/dist/sdk.d.ts +3 -0
  52. package/dist/sdk.d.ts.map +1 -0
  53. package/dist/sdk.js +2 -0
  54. package/dist/sdk.js.map +1 -0
  55. package/dist/store/open-fga-relation-tuple-store.d.ts +25 -0
  56. package/dist/store/open-fga-relation-tuple-store.d.ts.map +1 -0
  57. package/dist/store/open-fga-relation-tuple-store.js +77 -0
  58. package/dist/store/open-fga-relation-tuple-store.js.map +1 -0
  59. package/package.json +44 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gary Bourgeat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1 +1,106 @@
1
- Placeholder of @ddd-ts-kit/openfga. See https://github.com/gbourgeat/ddd-ts-kit.
1
+ # @ddd-ts-kit/openfga
2
+
3
+ The relationships of [`@ddd-ts-kit/auth`](../auth/README.md) on [OpenFGA](https://openfga.dev),
4
+ the CNCF implementation of Zanzibar: the `RelationTupleStore` and the `Authorizer` ports answered
5
+ by an OpenFGA server, and its authorization model generated from the relationship schema the
6
+ project already declares with `defineSchema`. One source of truth: the schema; the model is
7
+ derived from it and checked against what the server holds.
8
+
9
+ ```bash
10
+ npm install @ddd-ts-kit/openfga @openfga/sdk
11
+ ```
12
+
13
+ `@openfga/sdk` (0.9.7, the official client) is an optional peer, imported by the `/sdk` entry
14
+ only. The main entry takes any client that fits `OpenFgaApi`, the subset of the official
15
+ client the adapters call.
16
+
17
+ ## The model, from the schema
18
+
19
+ ```ts
20
+ import { authorizationModelOf, ensureAuthorizationModel, modelDrift } from '@ddd-ts-kit/openfga';
21
+ import { relationshipSchema } from '@/platform/auth/relationship-schema';
22
+
23
+ authorizationModelOf(relationshipSchema); // the body of WriteAuthorizationModel, schema 1.1
24
+
25
+ const { modelId, changes } = await ensureAuthorizationModel(client, relationshipSchema);
26
+ ```
27
+
28
+ | Schema of `@ddd-ts-kit/auth/rebac` | OpenFGA |
29
+ |---|---|
30
+ | a relation and the subjects it lists: `editor: ['user', 'group#member']` | a directly assignable relation (`this`) with `directly_related_user_types` |
31
+ | a name in a rewrite: `union('owner', 'editor')` | a computed userset |
32
+ | `union(...)`, `intersection(...)` | `union`, `intersection` |
33
+ | `exclusion(base, subtract)` | `difference` |
34
+ | `from('parent', 'edit')` | a tuple to userset |
35
+
36
+ `ensureAuthorizationModel(client, schema)` writes the generated model when the latest one of the
37
+ store differs, and answers the id in force and the differences (OpenFGA keeps every model; checks
38
+ use the latest unless a client pins one). Run it where the application is deployed or starts.
39
+ `modelDrift(authorizationModelOf(schema), served)` only compares: one line per type or
40
+ `type#relation` missing, extra or changed, empty when the two decide alike. A pipeline step reads
41
+ the latest model of the store and fails on any line, so the model is never edited by hand next to
42
+ the schema.
43
+
44
+ ## The store and the authorizer
45
+
46
+ ```ts
47
+ import { OpenFgaAuthorizer, OpenFgaRelationTupleStore } from '@ddd-ts-kit/openfga';
48
+ import { openFgaClient } from '@ddd-ts-kit/openfga/sdk';
49
+
50
+ const client = openFgaClient({ apiUrl: config.openfga.url, storeId: config.openfga.storeId });
51
+
52
+ const tuples = new OpenFgaRelationTupleStore(client, relationshipSchema);
53
+ const authorizer = new OpenFgaAuthorizer({ client, mapping: permissionMapping() });
54
+ ```
55
+
56
+ - `OpenFgaRelationTupleStore` passes `relationTupleStoreContract`, as the PostgreSQL store does:
57
+ writing a tuple twice and deleting one that is not there change nothing (the conflict options
58
+ of OpenFGA 1.10 and later), writes and deletes go by 100, reads follow every page. A read by
59
+ subject is one read per relation of the schema that accepts the kind of the subject, since
60
+ OpenFGA reads by user within one object type.
61
+ - `OpenFgaAuthorizer` passes `authorizerContract`, with the mapping and the denial codes of
62
+ `RelationBasedAuthorizer` (`ANONYMOUS`, `RESOURCE_REQUIRED`, `UNMAPPED_PERMISSION`,
63
+ `NO_RELATION`): `Check` decides, `ListObjects` gives the scope of a search (`{ kind: 'ids' }`).
64
+ A project moves between the two by binding, nothing else changes.
65
+
66
+ **The limits of `ListObjects`.** The server answers at most `OPENFGA_LIST_OBJECTS_MAX_RESULTS`
67
+ objects (1000 by default) and stops at `OPENFGA_LIST_OBJECTS_DEADLINE` (3 seconds by default),
68
+ and in both cases returns the objects found so far **without an error**. A search narrowed by a
69
+ cut list would lose elements in silence, so `OpenFgaAuthorizer` refuses an answer as long as
70
+ `listObjectsLimit` (default 1000: set it to the server's value) with
71
+ `ReverseExpansionLimitError`, as the in-process authorizer does beyond its own limit. A list cut
72
+ by the deadline cannot be told from a complete one by its answer: give the server a deadline
73
+ above the slowest listing you expect, and watch its latency. When the lists are that long,
74
+ narrow them first (a projection of the accessible ids, a coarser parent such as the clinic)
75
+ rather than raising the limits.
76
+
77
+ **A write is a call to a service.** It does not join the transaction of the unit of work: an
78
+ aggregate can commit without its tuple, or a tuple be written for an aggregate whose transaction
79
+ rolled back. Write tuples after the commit, from a domain event handler of phase `after-commit`
80
+ or from the consumer of an integration event (both retried, and the store is idempotent), never
81
+ from inside the transaction of the command; and decide what a request sees during the short
82
+ window when the aggregate exists without its relationship.
83
+
84
+ ## Testing
85
+
86
+ The specs of this package run the two contract suites against an in-memory double of the tuple
87
+ endpoints, and against a real server when `DDD_TS_KIT_OPENFGA_URL` is set: the integration job
88
+ of the repository starts `openfga/openfga` v1.22.0, pinned by digest. A project holds its own
89
+ adapters to the same suites, each test on a store of its own (`createStore`, then
90
+ `ensureAuthorizationModel`).
91
+
92
+ ## Limits
93
+
94
+ - No consistency token is carried: a check uses the default preference of the server
95
+ (`MINIMIZE_LATENCY`), so a tuple just written may not be seen at once by every replica.
96
+ - No conditional tuples (OpenFGA conditions): the schema of the kit has no caveats either.
97
+ - The subject of an actor is `<type>:<id>`; an id holding a character OpenFGA refuses in an
98
+ object id needs `subjectOf`.
99
+ - `expand` of `RelationBasedAuthorizer` has no equivalent here: use the `Expand` API of OpenFGA
100
+ for debugging.
101
+
102
+ ## See also
103
+
104
+ - [Authorization engines](../../docs/tools/authorization-engines.md): the PostgreSQL tuple store
105
+ or OpenFGA, and when to choose which
106
+ - [Choosing access control](../../docs/guides/choosing-access-control.md)
@@ -0,0 +1,18 @@
1
+ import type { Actor } from '@ddd-ts-kit/kernel';
2
+ import type { PermissionMapping } from '@ddd-ts-kit/auth/rebac';
3
+ import type { OpenFgaApi } from '../client/open-fga-api.js';
4
+ export interface OpenFgaAuthorizerOptions {
5
+ readonly client: OpenFgaApi;
6
+ /** Per resource type, the relation of the model each permission of the kit stands for, as for `RelationBasedAuthorizer`. */
7
+ readonly mapping: PermissionMapping;
8
+ /** The subject an actor is in the tuples. Default `<actor type>:<actor id>`, such as `user:ada`. */
9
+ readonly subjectOf?: (actor: Actor) => string;
10
+ /**
11
+ * The most objects the server answers to one `ListObjects`: its `OPENFGA_LIST_OBJECTS_MAX_RESULTS`,
12
+ * 1000 by default. The server cuts a longer list without saying so, so an answer that reaches it
13
+ * is refused (`ReverseExpansionLimitError`) rather than taken as the whole scope. Set it to the
14
+ * value the server runs with.
15
+ */
16
+ readonly listObjectsLimit?: number;
17
+ }
18
+ //# sourceMappingURL=open-fga-authorizer-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-authorizer-options.d.ts","sourceRoot":"","sources":["../../src/authorizer/open-fga-authorizer-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,4HAA4H;IAC5H,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,oGAAoG;IACpG,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;IAC9C;;;;;OAKG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=open-fga-authorizer-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-authorizer-options.js","sourceRoot":"","sources":["../../src/authorizer/open-fga-authorizer-options.ts"],"names":[],"mappings":""}
@@ -0,0 +1,17 @@
1
+ import { type AccessScope, type Authorizer, type CheckRequest, type Decision, type ListRequest } from '@ddd-ts-kit/auth';
2
+ import type { OpenFgaAuthorizerOptions } from './open-fga-authorizer-options.js';
3
+ /**
4
+ * The `Authorizer` port of `@ddd-ts-kit/auth` answered by OpenFGA: `Check` for a decision,
5
+ * `ListObjects` for the scope of a search. The same permissions, mapping and denial codes as the
6
+ * in-process `RelationBasedAuthorizer`, so a project moves from one to the other by binding. Like
7
+ * it, a listing that may be incomplete is refused: an answer as long as `listObjectsLimit` may
8
+ * have been cut by the server, and a search narrowed by it would lose elements in silence.
9
+ */
10
+ export declare class OpenFgaAuthorizer implements Authorizer {
11
+ private readonly options;
12
+ constructor(options: OpenFgaAuthorizerOptions);
13
+ check({ actor, permission, resource }: CheckRequest): Promise<Decision>;
14
+ listAccessible({ actor, permission, resourceType }: ListRequest): Promise<AccessScope>;
15
+ private subjectOf;
16
+ }
17
+ //# sourceMappingURL=open-fga-authorizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-authorizer.d.ts","sourceRoot":"","sources":["../../src/authorizer/open-fga-authorizer.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,WAAW,EACjB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAKjF;;;;;;GAMG;AACH,qBAAa,iBAAkB,YAAW,UAAU;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAApC,YAA6B,OAAO,EAAE,wBAAwB,EAAI;IAE5D,KAAK,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAmB5E;IAEK,cAAc,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAoB3F;IAED,OAAO,CAAC,SAAS;CAGlB"}
@@ -0,0 +1,53 @@
1
+ import { ReverseExpansionLimitError } from '@ddd-ts-kit/auth/rebac';
2
+ import { ALLOWED, DenialReason, deny, } from '@ddd-ts-kit/auth';
3
+ import { isAnonymous } from '@ddd-ts-kit/kernel';
4
+ /** `OPENFGA_LIST_OBJECTS_MAX_RESULTS` of a server that leaves it at its default. */
5
+ const SERVER_LIST_OBJECTS_LIMIT = 1000;
6
+ /**
7
+ * The `Authorizer` port of `@ddd-ts-kit/auth` answered by OpenFGA: `Check` for a decision,
8
+ * `ListObjects` for the scope of a search. The same permissions, mapping and denial codes as the
9
+ * in-process `RelationBasedAuthorizer`, so a project moves from one to the other by binding. Like
10
+ * it, a listing that may be incomplete is refused: an answer as long as `listObjectsLimit` may
11
+ * have been cut by the server, and a search narrowed by it would lose elements in silence.
12
+ */
13
+ export class OpenFgaAuthorizer {
14
+ options;
15
+ constructor(options) {
16
+ this.options = options;
17
+ }
18
+ async check({ actor, permission, resource }) {
19
+ if (isAnonymous(actor)) {
20
+ return deny(DenialReason.anonymous);
21
+ }
22
+ if (resource === undefined) {
23
+ return deny(DenialReason.resourceRequired);
24
+ }
25
+ const relation = this.options.mapping[resource.type]?.[permission.id];
26
+ if (relation === undefined) {
27
+ return deny(DenialReason.unmappedPermission);
28
+ }
29
+ const object = `${resource.type}:${resource.id}`;
30
+ const { allowed } = await this.options.client.check({ user: this.subjectOf(actor), relation, object });
31
+ return allowed === true ? ALLOWED : deny(DenialReason.noRelation);
32
+ }
33
+ async listAccessible({ actor, permission, resourceType }) {
34
+ const relation = this.options.mapping[resourceType]?.[permission.id];
35
+ if (isAnonymous(actor) || relation === undefined) {
36
+ return { kind: 'none' };
37
+ }
38
+ const { objects } = await this.options.client.listObjects({
39
+ user: this.subjectOf(actor),
40
+ relation,
41
+ type: resourceType,
42
+ });
43
+ const limit = this.options.listObjectsLimit ?? SERVER_LIST_OBJECTS_LIMIT;
44
+ if (objects.length >= limit) {
45
+ throw new ReverseExpansionLimitError(limit);
46
+ }
47
+ return { kind: 'ids', ids: objects.map((object) => object.slice(resourceType.length + 1)).sort() };
48
+ }
49
+ subjectOf(actor) {
50
+ return this.options.subjectOf?.(actor) ?? `${actor.type}:${actor.id}`;
51
+ }
52
+ }
53
+ //# sourceMappingURL=open-fga-authorizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-authorizer.js","sourceRoot":"","sources":["../../src/authorizer/open-fga-authorizer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /** One tuple as OpenFGA writes it: the subject is its `user`. */
2
+ export interface OpenFgaTupleKey {
3
+ readonly user: string;
4
+ readonly relation: string;
5
+ readonly object: string;
6
+ }
7
+ /**
8
+ * What the adapters of this package ask of an OpenFGA client, bound to one store: the subset of
9
+ * the `OpenFgaClient` of `@openfga/sdk` they call, which that client fits as it is
10
+ * (`openFgaClient` of `@ddd-ts-kit/openfga/sdk` builds one). Kept structural, so that this entry
11
+ * imports nothing of the SDK and a test may give a double.
12
+ */
13
+ export interface OpenFgaApi {
14
+ check(body: OpenFgaTupleKey): Promise<{
15
+ readonly allowed?: boolean;
16
+ }>;
17
+ listObjects(body: {
18
+ readonly user: string;
19
+ readonly relation: string;
20
+ readonly type: string;
21
+ }): Promise<{
22
+ readonly objects: readonly string[];
23
+ }>;
24
+ read(body: {
25
+ readonly user?: string;
26
+ readonly relation?: string;
27
+ readonly object?: string;
28
+ }, options?: {
29
+ readonly continuationToken?: string;
30
+ readonly pageSize?: number;
31
+ }): Promise<{
32
+ readonly tuples: readonly {
33
+ readonly key: OpenFgaTupleKey;
34
+ }[];
35
+ readonly continuation_token?: string;
36
+ }>;
37
+ write(body: {
38
+ readonly writes?: OpenFgaTupleKey[];
39
+ readonly deletes?: OpenFgaTupleKey[];
40
+ }, options?: {
41
+ readonly conflict?: {
42
+ readonly onDuplicateWrites?: string;
43
+ readonly onMissingDeletes?: string;
44
+ };
45
+ readonly transaction?: {
46
+ readonly maxPerChunk?: number;
47
+ };
48
+ }): Promise<unknown>;
49
+ readLatestAuthorizationModel(): Promise<{
50
+ readonly authorization_model?: unknown;
51
+ }>;
52
+ writeAuthorizationModel(body: unknown): Promise<{
53
+ readonly authorization_model_id: string;
54
+ }>;
55
+ }
56
+ //# sourceMappingURL=open-fga-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-api.d.ts","sourceRoot":"","sources":["../../src/client/open-fga-api.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACtE,WAAW,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QACtG,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;KACrC,CAAC,CAAC;IACH,IAAI,CACF,IAAI,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,EACtF,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5E,OAAO,CAAC;QAAE,QAAQ,CAAC,MAAM,EAAE,SAAS;YAAE,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAA;SAAE,EAAE,CAAC;QAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpH,KAAK,CACH,IAAI,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;QAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,EAAE,CAAA;KAAE,EACnF,OAAO,CAAC,EAAE;QACR,QAAQ,CAAC,QAAQ,CAAC,EAAE;YAAE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAChG,QAAQ,CAAC,WAAW,CAAC,EAAE;YAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC1D,GACA,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,4BAA4B,IAAI,OAAO,CAAC;QAAE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACpF,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9F"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=open-fga-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-api.js","sourceRoot":"","sources":["../../src/client/open-fga-api.ts"],"names":[],"mappings":""}
@@ -0,0 +1,10 @@
1
+ export interface OpenFgaClientOptions {
2
+ /** `http://localhost:8080`. */
3
+ readonly apiUrl: string;
4
+ readonly storeId: string;
5
+ /** Pin a model; left out, every check uses the latest model of the store. */
6
+ readonly authorizationModelId?: string;
7
+ /** A preshared key, when the server asks for one. Read from configuration, never written in code. */
8
+ readonly apiToken?: string;
9
+ }
10
+ //# sourceMappingURL=open-fga-client-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-client-options.d.ts","sourceRoot":"","sources":["../../src/client/open-fga-client-options.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,+BAA+B;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,qGAAqG;IACrG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=open-fga-client-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-client-options.js","sourceRoot":"","sources":["../../src/client/open-fga-client-options.ts"],"names":[],"mappings":""}
@@ -0,0 +1,5 @@
1
+ import type { OpenFgaApi } from './open-fga-api.js';
2
+ import type { OpenFgaClientOptions } from './open-fga-client-options.js';
3
+ /** The client of the official SDK, bound to one store, as the adapters of this package take it. */
4
+ export declare function openFgaClient({ apiUrl, storeId, authorizationModelId, apiToken }: OpenFgaClientOptions): OpenFgaApi;
5
+ //# sourceMappingURL=open-fga-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-client.d.ts","sourceRoot":"","sources":["../../src/client/open-fga-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAEzE,mGAAmG;AACnG,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,EAAE,oBAAoB,GAAG,UAAU,CASnH"}
@@ -0,0 +1,13 @@
1
+ import { CredentialsMethod, OpenFgaClient } from '@openfga/sdk';
2
+ /** The client of the official SDK, bound to one store, as the adapters of this package take it. */
3
+ export function openFgaClient({ apiUrl, storeId, authorizationModelId, apiToken }) {
4
+ return new OpenFgaClient({
5
+ apiUrl,
6
+ storeId,
7
+ ...(authorizationModelId === undefined ? {} : { authorizationModelId }),
8
+ ...(apiToken === undefined
9
+ ? {}
10
+ : { credentials: { method: CredentialsMethod.ApiToken, config: { token: apiToken } } }),
11
+ });
12
+ }
13
+ //# sourceMappingURL=open-fga-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-client.js","sourceRoot":"","sources":["../../src/client/open-fga-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAIhE,mGAAmG;AACnG,MAAM,UAAU,aAAa,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAwB;IACrG,OAAO,IAAI,aAAa,CAAC;QACvB,MAAM;QACN,OAAO;QACP,GAAG,CAAC,oBAAoB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,oBAAoB,EAAE,CAAC;QACvE,GAAG,CAAC,QAAQ,KAAK,SAAS;YACxB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,WAAW,EAAE,EAAE,MAAM,EAAE,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;KAC1F,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,9 @@
1
+ export { OpenFgaAuthorizer } from './authorizer/open-fga-authorizer.js';
2
+ export type { OpenFgaAuthorizerOptions } from './authorizer/open-fga-authorizer-options.js';
3
+ export type { OpenFgaApi, OpenFgaTupleKey } from './client/open-fga-api.js';
4
+ export { authorizationModelOf } from './model/authorization-model-of.js';
5
+ export { ensureAuthorizationModel } from './model/ensure-authorization-model.js';
6
+ export { modelDrift } from './model/model-drift.js';
7
+ export type { OpenFgaModel, OpenFgaRelationReference, OpenFgaTypeDefinition, OpenFgaUserset, } from './model/open-fga-model.js';
8
+ export { OpenFgaRelationTupleStore } from './store/open-fga-relation-tuple-store.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AACxE,YAAY,EAAE,wBAAwB,EAAE,MAAM,6CAA6C,CAAC;AAC5F,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,YAAY,EACV,YAAY,EACZ,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,GACf,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { OpenFgaAuthorizer } from './authorizer/open-fga-authorizer.js';
2
+ export { authorizationModelOf } from './model/authorization-model-of.js';
3
+ export { ensureAuthorizationModel } from './model/ensure-authorization-model.js';
4
+ export { modelDrift } from './model/model-drift.js';
5
+ export { OpenFgaRelationTupleStore } from './store/open-fga-relation-tuple-store.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAGxE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAOpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { Schema } from '@ddd-ts-kit/auth/rebac';
2
+ import type { OpenFgaModel } from './open-fga-model.js';
3
+ /**
4
+ * The authorization model of OpenFGA for a relationship schema: one source of truth, the schema
5
+ * the project declares with `defineSchema`, from which the model is generated and checked
6
+ * (`modelDrift`), never written by hand next to it. A relation of the schema is directly
7
+ * assignable to the subjects it lists; a permission is the rewrite that derives it.
8
+ */
9
+ export declare function authorizationModelOf(schema: Schema): OpenFgaModel;
10
+ //# sourceMappingURL=authorization-model-of.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorization-model-of.d.ts","sourceRoot":"","sources":["../../src/model/authorization-model-of.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EACV,YAAY,EAIb,MAAM,qBAAqB,CAAC;AAG7B;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAEjE"}
@@ -0,0 +1,34 @@
1
+ import { usersetOf } from './userset-of.js';
2
+ /**
3
+ * The authorization model of OpenFGA for a relationship schema: one source of truth, the schema
4
+ * the project declares with `defineSchema`, from which the model is generated and checked
5
+ * (`modelDrift`), never written by hand next to it. A relation of the schema is directly
6
+ * assignable to the subjects it lists; a permission is the rewrite that derives it.
7
+ */
8
+ export function authorizationModelOf(schema) {
9
+ return { schema_version: '1.1', type_definitions: schema.types().map((type) => typeDefinitionOf(schema, type)) };
10
+ }
11
+ function typeDefinitionOf(schema, type) {
12
+ const relations = Object.entries(schema.relations(type));
13
+ const permissions = Object.entries(schema.permissions(type));
14
+ if (relations.length === 0 && permissions.length === 0) {
15
+ return { type };
16
+ }
17
+ const direct = relations.map(([name]) => [name, { this: {} }]);
18
+ const derived = permissions.map(([name, rewrite]) => [name, usersetOf(rewrite)]);
19
+ const assignable = relations.map(([name, subjects]) => [
20
+ name,
21
+ { directly_related_user_types: subjects.map(referenceOf) },
22
+ ]);
23
+ return {
24
+ type,
25
+ relations: Object.fromEntries([...direct, ...derived]),
26
+ metadata: { relations: Object.fromEntries(assignable) },
27
+ };
28
+ }
29
+ /** `'group#member'` is the userset `{ type: 'group', relation: 'member' }`. */
30
+ function referenceOf(subject) {
31
+ const [type = subject, relation] = subject.split('#');
32
+ return relation === undefined ? { type } : { type, relation };
33
+ }
34
+ //# sourceMappingURL=authorization-model-of.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorization-model-of.js","sourceRoot":"","sources":["../../src/model/authorization-model-of.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;AACnH,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAc,EAAE,IAAY;IACpD,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IACzD,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;IAE7D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvD,OAAO,EAAE,IAAI,EAAE,CAAC;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAA4B,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IACzF,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,EAA4B,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAE3G,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,EAA0B,EAAE,CAAC;QAC7E,IAAI;QACJ,EAAE,2BAA2B,EAAE,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE;KAC3D,CAAC,CAAC;IAEH,OAAO;QACL,IAAI;QACJ,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC;QACtD,QAAQ,EAAE,EAAE,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE;KACxD,CAAC;AACJ,CAAC;AAMD,+EAA+E;AAC/E,SAAS,WAAW,CAAC,OAAe;IAClC,MAAM,CAAC,IAAI,GAAG,OAAO,EAAE,QAAQ,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEtD,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAChE,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A model reduced to what decides a check: per `type#relation`, its rewrite and the subjects it
3
+ * accepts, as stable text. What a server adds (ids, source positions, empty fields) is left
4
+ * out, so a model read back compares equal to the one written.
5
+ */
6
+ export declare function canonicalModel(model: unknown): ReadonlyMap<string, string>;
7
+ //# sourceMappingURL=canonical-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-model.d.ts","sourceRoot":"","sources":["../../src/model/canonical-model.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAkB1E"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A model reduced to what decides a check: per `type#relation`, its rewrite and the subjects it
3
+ * accepts, as stable text. What a server adds (ids, source positions, empty fields) is left
4
+ * out, so a model read back compares equal to the one written.
5
+ */
6
+ export function canonicalModel(model) {
7
+ const entries = new Map();
8
+ for (const definition of listOf(fieldOf(model, 'type_definitions'))) {
9
+ const type = String(fieldOf(definition, 'type'));
10
+ const relations = recordOf(fieldOf(definition, 'relations'));
11
+ const metadata = recordOf(fieldOf(fieldOf(definition, 'metadata'), 'relations'));
12
+ entries.set(type, 'type');
13
+ for (const [relation, userset] of Object.entries(relations)) {
14
+ const accepted = listOf(fieldOf(metadata[relation], 'directly_related_user_types')).map(referenceText).sort();
15
+ entries.set(`${type}#${relation}`, `${usersetText(userset)} [${accepted.join(', ')}]`);
16
+ }
17
+ }
18
+ return entries;
19
+ }
20
+ function usersetText(userset) {
21
+ const union = fieldOf(userset, 'union');
22
+ const intersection = fieldOf(userset, 'intersection');
23
+ const difference = fieldOf(userset, 'difference');
24
+ const tupleToUserset = fieldOf(userset, 'tupleToUserset');
25
+ if (union !== undefined) {
26
+ return `union(${listOf(fieldOf(union, 'child')).map(usersetText).join(', ')})`;
27
+ }
28
+ if (intersection !== undefined) {
29
+ return `intersection(${listOf(fieldOf(intersection, 'child')).map(usersetText).join(', ')})`;
30
+ }
31
+ if (difference !== undefined) {
32
+ return `difference(${usersetText(fieldOf(difference, 'base'))}, ${usersetText(fieldOf(difference, 'subtract'))})`;
33
+ }
34
+ if (tupleToUserset !== undefined) {
35
+ const followed = relationOf(fieldOf(tupleToUserset, 'tupleset'));
36
+ return `from(${followed}, ${relationOf(fieldOf(tupleToUserset, 'computedUserset'))})`;
37
+ }
38
+ const computed = fieldOf(userset, 'computedUserset');
39
+ return computed === undefined ? 'this' : relationOf(computed);
40
+ }
41
+ function referenceText(reference) {
42
+ const relation = fieldOf(reference, 'relation');
43
+ return `${String(fieldOf(reference, 'type'))}${typeof relation === 'string' && relation !== '' ? `#${relation}` : ''}`;
44
+ }
45
+ function relationOf(value) {
46
+ return String(fieldOf(value, 'relation'));
47
+ }
48
+ function fieldOf(value, key) {
49
+ return typeof value === 'object' && value !== null ? Reflect.get(value, key) : undefined;
50
+ }
51
+ function listOf(value) {
52
+ return Array.isArray(value) ? value : [];
53
+ }
54
+ function recordOf(value) {
55
+ return typeof value === 'object' && value !== null ? { ...value } : {};
56
+ }
57
+ //# sourceMappingURL=canonical-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-model.js","sourceRoot":"","sources":["../../src/model/canonical-model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { Schema } from '@ddd-ts-kit/auth/rebac';
2
+ import type { OpenFgaApi } from '../client/open-fga-api.js';
3
+ /**
4
+ * Makes the latest model of the store the one the schema generates, writing a new one only when
5
+ * they differ (OpenFGA keeps every model it was given; a check uses the latest unless told
6
+ * otherwise). Answers the id of the model in force and what changed. For a deployment step or
7
+ * the start of the application; a pipeline checks without writing, with `modelDrift`.
8
+ */
9
+ export declare function ensureAuthorizationModel(client: OpenFgaApi, schema: Schema): Promise<{
10
+ readonly modelId: string;
11
+ readonly changes: readonly string[];
12
+ }>;
13
+ //# sourceMappingURL=ensure-authorization-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ensure-authorization-model.d.ts","sourceRoot":"","sources":["../../src/model/ensure-authorization-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAI5D;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC,CAY5E"}
@@ -0,0 +1,19 @@
1
+ import { authorizationModelOf } from './authorization-model-of.js';
2
+ import { modelDrift } from './model-drift.js';
3
+ /**
4
+ * Makes the latest model of the store the one the schema generates, writing a new one only when
5
+ * they differ (OpenFGA keeps every model it was given; a check uses the latest unless told
6
+ * otherwise). Answers the id of the model in force and what changed. For a deployment step or
7
+ * the start of the application; a pipeline checks without writing, with `modelDrift`.
8
+ */
9
+ export async function ensureAuthorizationModel(client, schema) {
10
+ const expected = authorizationModelOf(schema);
11
+ const { authorization_model: held } = await client.readLatestAuthorizationModel();
12
+ const changes = modelDrift(expected, held);
13
+ if (changes.length === 0) {
14
+ return { modelId: String(Reflect.get(Object(held), 'id')), changes };
15
+ }
16
+ const { authorization_model_id: modelId } = await client.writeAuthorizationModel(expected);
17
+ return { modelId, changes };
18
+ }
19
+ //# sourceMappingURL=ensure-authorization-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ensure-authorization-model.js","sourceRoot":"","sources":["../../src/model/ensure-authorization-model.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,MAAkB,EAClB,MAAc;IAEd,MAAM,QAAQ,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,EAAE,mBAAmB,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,4BAA4B,EAAE,CAAC;IAClF,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAE3C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;IACvE,CAAC;IAED,MAAM,EAAE,sBAAsB,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAE3F,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { OpenFgaModel } from './open-fga-model.js';
2
+ /**
3
+ * What differs between the model the schema generates and the one a store holds: one line per
4
+ * type or `type#relation` missing, extra or changed. Empty when they decide alike. The store
5
+ * holds none at all: one line says so.
6
+ */
7
+ export declare function modelDrift(expected: OpenFgaModel, actual: unknown): readonly string[];
8
+ //# sourceMappingURL=model-drift.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-drift.d.ts","sourceRoot":"","sources":["../../src/model/model-drift.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CAUrF"}
@@ -0,0 +1,28 @@
1
+ import { canonicalModel } from './canonical-model.js';
2
+ /**
3
+ * What differs between the model the schema generates and the one a store holds: one line per
4
+ * type or `type#relation` missing, extra or changed. Empty when they decide alike. The store
5
+ * holds none at all: one line says so.
6
+ */
7
+ export function modelDrift(expected, actual) {
8
+ if (actual === undefined || actual === null) {
9
+ return ['the store holds no authorization model'];
10
+ }
11
+ const wanted = canonicalModel(expected);
12
+ const held = canonicalModel(actual);
13
+ const names = [...new Set([...wanted.keys(), ...held.keys()])].sort();
14
+ return names.flatMap((name) => differenceAt(name, wanted.get(name), held.get(name)));
15
+ }
16
+ function differenceAt(name, wanted, held) {
17
+ if (wanted === held) {
18
+ return [];
19
+ }
20
+ if (held === undefined) {
21
+ return [`${name} is in the schema, not in the store: ${String(wanted)}`];
22
+ }
23
+ if (wanted === undefined) {
24
+ return [`${name} is in the store, not in the schema: ${held}`];
25
+ }
26
+ return [`${name} differs: the schema says ${wanted}, the store ${held}`];
27
+ }
28
+ //# sourceMappingURL=model-drift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-drift.js","sourceRoot":"","sources":["../../src/model/model-drift.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGtD;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,QAAsB,EAAE,MAAe;IAChE,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QAC5C,OAAO,CAAC,wCAAwC,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,MAAM,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAEtE,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACvF,CAAC;AAED,SAAS,YAAY,CAAC,IAAY,EAAE,MAA0B,EAAE,IAAwB;IACtF,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,IAAI,wCAAwC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,IAAI,wCAAwC,IAAI,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,OAAO,CAAC,GAAG,IAAI,6BAA6B,MAAM,eAAe,IAAI,EAAE,CAAC,CAAC;AAC3E,CAAC"}
@@ -0,0 +1,45 @@
1
+ /** A userset rewrite of OpenFGA, as its API writes it (schema 1.1). */
2
+ export interface OpenFgaUserset {
3
+ readonly this?: Record<string, never>;
4
+ readonly computedUserset?: {
5
+ readonly relation: string;
6
+ };
7
+ readonly tupleToUserset?: {
8
+ readonly tupleset: {
9
+ readonly relation: string;
10
+ };
11
+ readonly computedUserset: {
12
+ readonly relation: string;
13
+ };
14
+ };
15
+ readonly union?: {
16
+ readonly child: readonly OpenFgaUserset[];
17
+ };
18
+ readonly intersection?: {
19
+ readonly child: readonly OpenFgaUserset[];
20
+ };
21
+ readonly difference?: {
22
+ readonly base: OpenFgaUserset;
23
+ readonly subtract: OpenFgaUserset;
24
+ };
25
+ }
26
+ /** Who may be the subject of a directly assignable relation: a type, or the userset `<type>#<relation>`. */
27
+ export interface OpenFgaRelationReference {
28
+ readonly type: string;
29
+ readonly relation?: string;
30
+ }
31
+ export interface OpenFgaTypeDefinition {
32
+ readonly type: string;
33
+ readonly relations?: Readonly<Record<string, OpenFgaUserset>>;
34
+ readonly metadata?: {
35
+ readonly relations?: Readonly<Record<string, {
36
+ readonly directly_related_user_types?: readonly OpenFgaRelationReference[];
37
+ }>>;
38
+ };
39
+ }
40
+ /** The body of `WriteAuthorizationModel`: what `authorizationModelOf` generates from a relationship schema. */
41
+ export interface OpenFgaModel {
42
+ readonly schema_version: '1.1';
43
+ readonly type_definitions: readonly OpenFgaTypeDefinition[];
44
+ }
45
+ //# sourceMappingURL=open-fga-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-model.d.ts","sourceRoot":"","sources":["../../src/model/open-fga-model.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACtC,QAAQ,CAAC,eAAe,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,QAAQ,CAAC,cAAc,CAAC,EAAE;QACxB,QAAQ,CAAC,QAAQ,EAAE;YAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC;QACjD,QAAQ,CAAC,eAAe,EAAE;YAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC;KACzD,CAAC;IACF,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAA;KAAE,CAAC;IAC/D,QAAQ,CAAC,YAAY,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAA;KAAE,CAAC;IACtE,QAAQ,CAAC,UAAU,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAA;KAAE,CAAC;CAC5F;AAED,4GAA4G;AAC5G,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;IAC9D,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAClB,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAC3B,MAAM,CAAC,MAAM,EAAE;YAAE,QAAQ,CAAC,2BAA2B,CAAC,EAAE,SAAS,wBAAwB,EAAE,CAAA;SAAE,CAAC,CAC/F,CAAC;KACH,CAAC;CACH;AAED,+GAA+G;AAC/G,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,cAAc,EAAE,KAAK,CAAC;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,SAAS,qBAAqB,EAAE,CAAC;CAC7D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=open-fga-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-model.js","sourceRoot":"","sources":["../../src/model/open-fga-model.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ import type { Rewrite } from '@ddd-ts-kit/auth/rebac';
2
+ import type { OpenFgaUserset } from './open-fga-model.js';
3
+ /**
4
+ * A rewrite of the relationship schema in the words of OpenFGA: a name is a computed userset,
5
+ * `from` a tuple to userset, `exclusion` a difference; union and intersection keep their name.
6
+ */
7
+ export declare function usersetOf(rewrite: Rewrite): OpenFgaUserset;
8
+ //# sourceMappingURL=userset-of.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userset-of.d.ts","sourceRoot":"","sources":["../../src/model/userset-of.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,cAAc,CAoB1D"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A rewrite of the relationship schema in the words of OpenFGA: a name is a computed userset,
3
+ * `from` a tuple to userset, `exclusion` a difference; union and intersection keep their name.
4
+ */
5
+ export function usersetOf(rewrite) {
6
+ if (typeof rewrite === 'string') {
7
+ return { computedUserset: { relation: rewrite } };
8
+ }
9
+ switch (rewrite.kind) {
10
+ case 'from':
11
+ return {
12
+ tupleToUserset: {
13
+ tupleset: { relation: rewrite.relation },
14
+ computedUserset: { relation: rewrite.permission },
15
+ },
16
+ };
17
+ case 'exclusion':
18
+ return { difference: { base: usersetOf(rewrite.base), subtract: usersetOf(rewrite.subtract) } };
19
+ case 'union':
20
+ return { union: { child: rewrite.children.map(usersetOf) } };
21
+ case 'intersection':
22
+ return { intersection: { child: rewrite.children.map(usersetOf) } };
23
+ }
24
+ }
25
+ //# sourceMappingURL=userset-of.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userset-of.js","sourceRoot":"","sources":["../../src/model/userset-of.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,OAAgB;IACxC,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,EAAE,eAAe,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC;IACpD,CAAC;IAED,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;QACrB,KAAK,MAAM;YACT,OAAO;gBACL,cAAc,EAAE;oBACd,QAAQ,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE;oBACxC,eAAe,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,UAAU,EAAE;iBAClD;aACF,CAAC;QACJ,KAAK,WAAW;YACd,OAAO,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QAClG,KAAK,OAAO;YACV,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;QAC/D,KAAK,cAAc;YACjB,OAAO,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;IACxE,CAAC;AACH,CAAC"}
package/dist/sdk.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { openFgaClient } from './client/open-fga-client.js';
2
+ export type { OpenFgaClientOptions } from './client/open-fga-client-options.js';
3
+ //# sourceMappingURL=sdk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../src/sdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,YAAY,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC"}
package/dist/sdk.js ADDED
@@ -0,0 +1,2 @@
1
+ export { openFgaClient } from './client/open-fga-client.js';
2
+ //# sourceMappingURL=sdk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk.js","sourceRoot":"","sources":["../src/sdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { type RelationTuple, type RelationTupleStore, type Schema } from '@ddd-ts-kit/auth/rebac';
2
+ import type { OpenFgaApi } from '../client/open-fga-api.js';
3
+ /**
4
+ * The relation tuples of `@ddd-ts-kit/auth` in an OpenFGA store, held to the same contract as the
5
+ * tuple store of PostgreSQL. Writing a tuple twice and deleting one that is not there change
6
+ * nothing (the conflict options of OpenFGA 1.10 and later). A write is a call to a service: it
7
+ * does not join the transaction of the unit of work, so a tuple written with an aggregate goes
8
+ * through the outbox or is retried, and must be idempotent, as it is here.
9
+ */
10
+ export declare class OpenFgaRelationTupleStore implements RelationTupleStore {
11
+ private readonly client;
12
+ private readonly schema;
13
+ constructor(client: OpenFgaApi, schema: Schema);
14
+ write(tuples: readonly RelationTuple[]): Promise<void>;
15
+ delete(tuples: readonly RelationTuple[]): Promise<void>;
16
+ readByObject(object: string, relation: string): Promise<readonly RelationTuple[]>;
17
+ /**
18
+ * OpenFGA reads by subject only within one object type: one read per relation of the schema
19
+ * that accepts the kind of the subject (`user`, or the userset `group#member`).
20
+ */
21
+ readBySubject(subject: string): Promise<readonly RelationTuple[]>;
22
+ private relationsAccepting;
23
+ private readAll;
24
+ }
25
+ //# sourceMappingURL=open-fga-relation-tuple-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-relation-tuple-store.d.ts","sourceRoot":"","sources":["../../src/store/open-fga-relation-tuple-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,MAAM,EACZ,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,2BAA2B,CAAC;AAQ7E;;;;;;GAMG;AACH,qBAAa,yBAA0B,YAAW,kBAAkB;IAEhE,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFzB,YACmB,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,EAC7B;IAEE,KAAK,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI3D;IAEK,MAAM,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI5D;IAED,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAEhF;IAED;;;OAGG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAMtE;IAED,OAAO,CAAC,kBAAkB;YAWZ,OAAO;CAgBtB"}
@@ -0,0 +1,77 @@
1
+ import { parseSubject, tupleKey, } from '@ddd-ts-kit/auth/rebac';
2
+ /** OpenFGA answers a page of tuples at a time; this many, at most, per read. */
3
+ const PAGE_SIZE = 100;
4
+ /** How many tuples one write sends: the limit of a write of the OpenFGA API. */
5
+ const CHUNK = 100;
6
+ /**
7
+ * The relation tuples of `@ddd-ts-kit/auth` in an OpenFGA store, held to the same contract as the
8
+ * tuple store of PostgreSQL. Writing a tuple twice and deleting one that is not there change
9
+ * nothing (the conflict options of OpenFGA 1.10 and later). A write is a call to a service: it
10
+ * does not join the transaction of the unit of work, so a tuple written with an aggregate goes
11
+ * through the outbox or is retried, and must be idempotent, as it is here.
12
+ */
13
+ export class OpenFgaRelationTupleStore {
14
+ client;
15
+ schema;
16
+ constructor(client, schema) {
17
+ this.client = client;
18
+ this.schema = schema;
19
+ }
20
+ async write(tuples) {
21
+ for (const chunk of chunksOf(distinct(tuples))) {
22
+ await this.client.write({ writes: chunk }, { conflict: { onDuplicateWrites: 'ignore' } });
23
+ }
24
+ }
25
+ async delete(tuples) {
26
+ for (const chunk of chunksOf(distinct(tuples))) {
27
+ await this.client.write({ deletes: chunk }, { conflict: { onMissingDeletes: 'ignore' } });
28
+ }
29
+ }
30
+ readByObject(object, relation) {
31
+ return this.readAll({ object, relation });
32
+ }
33
+ /**
34
+ * OpenFGA reads by subject only within one object type: one read per relation of the schema
35
+ * that accepts the kind of the subject (`user`, or the userset `group#member`).
36
+ */
37
+ async readBySubject(subject) {
38
+ const read = this.relationsAccepting(subject).map(([type, relation]) => this.readAll({ user: subject, relation, object: `${type}:` }));
39
+ return (await Promise.all(read)).flat();
40
+ }
41
+ relationsAccepting(subject) {
42
+ const { type, relation } = parseSubject(subject);
43
+ const kind = relation === undefined ? type : `${type}#${relation}`;
44
+ return this.schema.types().flatMap((object) => Object.entries(this.schema.relations(object))
45
+ .filter(([, subjects]) => subjects.includes(kind))
46
+ .map(([name]) => [object, name]));
47
+ }
48
+ async readAll(filter) {
49
+ const tuples = [];
50
+ let continuationToken;
51
+ do {
52
+ const page = await this.client.read(filter, {
53
+ pageSize: PAGE_SIZE,
54
+ ...(continuationToken === undefined ? {} : { continuationToken }),
55
+ });
56
+ tuples.push(...page.tuples.map(({ key }) => ({ object: key.object, relation: key.relation, subject: key.user })));
57
+ continuationToken = page.continuation_token === '' ? undefined : page.continuation_token;
58
+ } while (continuationToken !== undefined);
59
+ return tuples;
60
+ }
61
+ }
62
+ /** Each tuple once: OpenFGA refuses a request that names one tuple twice, whatever its conflict options. */
63
+ function distinct(tuples) {
64
+ const keys = new Map(tuples.map(({ object, relation, subject }) => [
65
+ tupleKey({ object, relation, subject }),
66
+ { object, relation, user: subject },
67
+ ]));
68
+ return [...keys.values()];
69
+ }
70
+ function chunksOf(keys) {
71
+ const chunks = [];
72
+ for (let start = 0; start < keys.length; start += CHUNK) {
73
+ chunks.push(keys.slice(start, start + CHUNK));
74
+ }
75
+ return chunks;
76
+ }
77
+ //# sourceMappingURL=open-fga-relation-tuple-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-fga-relation-tuple-store.js","sourceRoot":"","sources":["../../src/store/open-fga-relation-tuple-store.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,QAAQ,GAIT,MAAM,wBAAwB,CAAC;AAGhC,gFAAgF;AAChF,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB,gFAAgF;AAChF,MAAM,KAAK,GAAG,GAAG,CAAC;AAElB;;;;;;GAMG;AACH,MAAM,OAAO,yBAAyB;IAEjB,MAAM;IACN,MAAM;IAFzB,YACmB,MAAkB,EAClB,MAAc;sBADd,MAAM;sBACN,MAAM;IACtB,CAAC;IAEJ,KAAK,CAAC,KAAK,CAAC,MAAgC;QAC1C,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YAC/C,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,iBAAiB,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,MAAgC;QAC3C,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YAC/C,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC5F,CAAC;IACH,CAAC;IAED,YAAY,CAAC,MAAc,EAAE,QAAgB;QAC3C,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,aAAa,CAAC,OAAe;QACjC,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,EAAE,CACrE,IAAI,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAC9D,CAAC;QAEF,OAAO,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1C,CAAC;IAEO,kBAAkB,CAAC,OAAe;QACxC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,IAAI,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,QAAQ,EAAE,CAAC;QAEnE,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAC5C,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;aAC1C,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;aACjD,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,CAAU,CAAC,CAC5C,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,MAAsF;QAC1G,MAAM,MAAM,GAAoB,EAAE,CAAC;QACnC,IAAI,iBAAqC,CAAC;QAE1C,GAAG,CAAC;YACF,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE;gBAC1C,QAAQ,EAAE,SAAS;gBACnB,GAAG,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC;aAClE,CAAC,CAAC;YAEH,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAClH,iBAAiB,GAAG,IAAI,CAAC,kBAAkB,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC;QAC3F,CAAC,QAAQ,iBAAiB,KAAK,SAAS,EAAE;QAE1C,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAED,4GAA4G;AAC5G,SAAS,QAAQ,CAAC,MAAgC;IAChD,MAAM,IAAI,GAAG,IAAI,GAAG,CAClB,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QAC5C,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;QACvC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE;KACpC,CAAC,CACH,CAAC;IAEF,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,QAAQ,CAAC,IAAgC;IAChD,MAAM,MAAM,GAAwB,EAAE,CAAC;IAEvC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,KAAK,EAAE,CAAC;QACxD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,48 @@
1
1
  {
2
2
  "name": "@ddd-ts-kit/openfga",
3
- "version": "0.0.0",
4
- "description": "Placeholder that reserves the name; the real versions are published by the release workflow of gbourgeat/ddd-ts-kit.",
3
+ "version": "0.9.0",
4
+ "description": "The relation tuple store and the authorizer of @ddd-ts-kit/auth on OpenFGA, with the authorization model generated from the relationship schema.",
5
5
  "license": "MIT",
6
- "repository": { "type": "git", "url": "git+https://github.com/gbourgeat/ddd-ts-kit.git" }
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/gbourgeat/ddd-ts-kit.git",
9
+ "directory": "packages/openfga"
10
+ },
11
+ "homepage": "https://github.com/gbourgeat/ddd-ts-kit/tree/main/packages/openfga#readme",
12
+ "bugs": "https://github.com/gbourgeat/ddd-ts-kit/issues",
13
+ "publishConfig": {
14
+ "access": "public",
15
+ "provenance": true
16
+ },
17
+ "type": "module",
18
+ "sideEffects": false,
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./sdk": {
25
+ "types": "./dist/sdk.d.ts",
26
+ "default": "./dist/sdk.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "dependencies": {
33
+ "@ddd-ts-kit/auth": "0.9.0",
34
+ "@ddd-ts-kit/kernel": "0.9.0"
35
+ },
36
+ "peerDependencies": {
37
+ "@openfga/sdk": "^0.9.7"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@openfga/sdk": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "devDependencies": {
45
+ "@ddd-ts-kit/testing": "0.9.0",
46
+ "@openfga/sdk": "0.9.7"
47
+ }
7
48
  }