@stigmer/server 3.27.2 → 3.29.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 (227) hide show
  1. package/README.md +2 -0
  2. package/dist/authorization/authorizer.d.ts +3 -3
  3. package/dist/authorization/authorizer.js +3 -3
  4. package/dist/authorization/derived-tuples.js +6 -1
  5. package/dist/authorization/derived-tuples.js.map +1 -1
  6. package/dist/authorization/evaluator.d.ts +9 -10
  7. package/dist/authorization/evaluator.d.ts.map +1 -1
  8. package/dist/authorization/evaluator.js.map +1 -1
  9. package/dist/authorization/list-read-scope.d.ts.map +1 -1
  10. package/dist/authorization/list-read-scope.js +29 -18
  11. package/dist/authorization/list-read-scope.js.map +1 -1
  12. package/dist/authorization/model/bindings.d.ts +41 -0
  13. package/dist/authorization/model/bindings.d.ts.map +1 -0
  14. package/dist/authorization/model/bindings.js +89 -0
  15. package/dist/authorization/model/bindings.js.map +1 -0
  16. package/dist/authorization/model/data/authorization-model.json +3141 -0
  17. package/dist/authorization/model/default-of.d.ts +2 -2
  18. package/dist/authorization/model/execution-viewer.d.ts +3 -0
  19. package/dist/authorization/model/execution-viewer.d.ts.map +1 -0
  20. package/dist/authorization/model/execution-viewer.js +50 -0
  21. package/dist/authorization/model/execution-viewer.js.map +1 -0
  22. package/dist/authorization/model/index.d.ts +33 -21
  23. package/dist/authorization/model/index.d.ts.map +1 -1
  24. package/dist/authorization/model/index.js +74 -52
  25. package/dist/authorization/model/index.js.map +1 -1
  26. package/dist/authorization/model/openfga-json.d.ts +18 -0
  27. package/dist/authorization/model/openfga-json.d.ts.map +1 -0
  28. package/dist/authorization/model/openfga-json.js +266 -0
  29. package/dist/authorization/model/openfga-json.js.map +1 -0
  30. package/dist/authorization/model/rewrite.d.ts +39 -67
  31. package/dist/authorization/model/rewrite.d.ts.map +1 -1
  32. package/dist/authorization/model/rewrite.js +1 -117
  33. package/dist/authorization/model/rewrite.js.map +1 -1
  34. package/dist/authorization/strict-json.d.ts +20 -0
  35. package/dist/authorization/strict-json.d.ts.map +1 -0
  36. package/dist/authorization/strict-json.js +45 -0
  37. package/dist/authorization/strict-json.js.map +1 -0
  38. package/dist/boot/compose.d.ts.map +1 -1
  39. package/dist/boot/compose.js +9 -7
  40. package/dist/boot/compose.js.map +1 -1
  41. package/dist/domain/workflowexecution/list-index.d.ts +1 -1
  42. package/dist/extensions/list-read-scope.d.ts +20 -3
  43. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  44. package/dist/extensions/list-read-scope.js +20 -1
  45. package/dist/extensions/list-read-scope.js.map +1 -1
  46. package/dist/index.d.ts +7 -8
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +15 -5
  49. package/dist/index.js.map +1 -1
  50. package/package.json +7 -6
  51. package/src/authorization/README.md +41 -34
  52. package/src/authorization/__tests__/authorizer.test.ts +5 -14
  53. package/src/authorization/__tests__/derived-tuples.test.ts +29 -27
  54. package/src/authorization/__tests__/evaluator.test.ts +53 -160
  55. package/src/authorization/__tests__/facts.test.ts +5 -11
  56. package/src/authorization/__tests__/list-read-scope.measure.test.ts +12 -20
  57. package/src/authorization/__tests__/list-read-scope.test.ts +15 -14
  58. package/src/authorization/__tests__/organization-directory.test.ts +2 -2
  59. package/src/authorization/__tests__/store-test-kit.ts +246 -0
  60. package/src/authorization/__tests__/store-tests.test.ts +92 -223
  61. package/src/authorization/__tests__/support.ts +30 -2
  62. package/src/authorization/__tests__/throwaway-model.ts +75 -0
  63. package/src/authorization/__tests__/wire-permissions.test.ts +141 -107
  64. package/src/authorization/authorizer.ts +3 -3
  65. package/src/authorization/derived-tuples.ts +7 -2
  66. package/src/authorization/evaluator.ts +8 -10
  67. package/src/authorization/list-read-scope.ts +37 -22
  68. package/src/authorization/model/__tests__/openfga-json.test.ts +353 -0
  69. package/src/authorization/model/__tests__/registry.test.ts +345 -277
  70. package/src/authorization/model/bindings.ts +131 -0
  71. package/src/authorization/model/data/authorization-model.json +3141 -0
  72. package/src/authorization/model/default-of.ts +2 -2
  73. package/src/authorization/model/execution-viewer.ts +56 -0
  74. package/src/authorization/model/index.ts +81 -72
  75. package/src/authorization/model/openfga-json.ts +310 -0
  76. package/src/authorization/model/rewrite.ts +39 -188
  77. package/src/authorization/strict-json.ts +52 -0
  78. package/src/boot/compose.ts +9 -7
  79. package/src/extensions/__tests__/built-in-authorization-composed.test.ts +3 -6
  80. package/src/extensions/list-read-scope.ts +32 -4
  81. package/src/index.ts +15 -44
  82. package/dist/authorization/model/agent.d.ts +0 -2
  83. package/dist/authorization/model/agent.d.ts.map +0 -1
  84. package/dist/authorization/model/agent.js +0 -43
  85. package/dist/authorization/model/agent.js.map +0 -1
  86. package/dist/authorization/model/agent_channel.d.ts +0 -2
  87. package/dist/authorization/model/agent_channel.d.ts.map +0 -1
  88. package/dist/authorization/model/agent_channel.js +0 -39
  89. package/dist/authorization/model/agent_channel.js.map +0 -1
  90. package/dist/authorization/model/agent_execution.d.ts +0 -2
  91. package/dist/authorization/model/agent_execution.d.ts.map +0 -1
  92. package/dist/authorization/model/agent_execution.js +0 -25
  93. package/dist/authorization/model/agent_execution.js.map +0 -1
  94. package/dist/authorization/model/agent_instance.d.ts +0 -2
  95. package/dist/authorization/model/agent_instance.d.ts.map +0 -1
  96. package/dist/authorization/model/agent_instance.js +0 -48
  97. package/dist/authorization/model/agent_instance.js.map +0 -1
  98. package/dist/authorization/model/agent_share.d.ts +0 -2
  99. package/dist/authorization/model/agent_share.d.ts.map +0 -1
  100. package/dist/authorization/model/agent_share.js +0 -29
  101. package/dist/authorization/model/agent_share.js.map +0 -1
  102. package/dist/authorization/model/api_key.d.ts +0 -2
  103. package/dist/authorization/model/api_key.d.ts.map +0 -1
  104. package/dist/authorization/model/api_key.js +0 -27
  105. package/dist/authorization/model/api_key.js.map +0 -1
  106. package/dist/authorization/model/artifact.d.ts +0 -2
  107. package/dist/authorization/model/artifact.d.ts.map +0 -1
  108. package/dist/authorization/model/artifact.js +0 -27
  109. package/dist/authorization/model/artifact.js.map +0 -1
  110. package/dist/authorization/model/channel_app.d.ts +0 -2
  111. package/dist/authorization/model/channel_app.d.ts.map +0 -1
  112. package/dist/authorization/model/channel_app.js +0 -28
  113. package/dist/authorization/model/channel_app.js.map +0 -1
  114. package/dist/authorization/model/environment.d.ts +0 -2
  115. package/dist/authorization/model/environment.d.ts.map +0 -1
  116. package/dist/authorization/model/environment.js +0 -33
  117. package/dist/authorization/model/environment.js.map +0 -1
  118. package/dist/authorization/model/execution_context.d.ts +0 -2
  119. package/dist/authorization/model/execution_context.d.ts.map +0 -1
  120. package/dist/authorization/model/execution_context.js +0 -19
  121. package/dist/authorization/model/execution_context.js.map +0 -1
  122. package/dist/authorization/model/iam_policy.d.ts +0 -2
  123. package/dist/authorization/model/iam_policy.d.ts.map +0 -1
  124. package/dist/authorization/model/iam_policy.js +0 -31
  125. package/dist/authorization/model/iam_policy.js.map +0 -1
  126. package/dist/authorization/model/identity_account.d.ts +0 -2
  127. package/dist/authorization/model/identity_account.d.ts.map +0 -1
  128. package/dist/authorization/model/identity_account.js +0 -25
  129. package/dist/authorization/model/identity_account.js.map +0 -1
  130. package/dist/authorization/model/mcp_server.d.ts +0 -2
  131. package/dist/authorization/model/mcp_server.d.ts.map +0 -1
  132. package/dist/authorization/model/mcp_server.js +0 -38
  133. package/dist/authorization/model/mcp_server.js.map +0 -1
  134. package/dist/authorization/model/memory.d.ts +0 -2
  135. package/dist/authorization/model/memory.d.ts.map +0 -1
  136. package/dist/authorization/model/memory.js +0 -26
  137. package/dist/authorization/model/memory.js.map +0 -1
  138. package/dist/authorization/model/oauth_app.d.ts +0 -2
  139. package/dist/authorization/model/oauth_app.d.ts.map +0 -1
  140. package/dist/authorization/model/oauth_app.js +0 -31
  141. package/dist/authorization/model/oauth_app.js.map +0 -1
  142. package/dist/authorization/model/organization.d.ts +0 -2
  143. package/dist/authorization/model/organization.d.ts.map +0 -1
  144. package/dist/authorization/model/organization.js +0 -58
  145. package/dist/authorization/model/organization.js.map +0 -1
  146. package/dist/authorization/model/platform_client.d.ts +0 -2
  147. package/dist/authorization/model/platform_client.d.ts.map +0 -1
  148. package/dist/authorization/model/platform_client.js +0 -31
  149. package/dist/authorization/model/platform_client.js.map +0 -1
  150. package/dist/authorization/model/plugin.d.ts +0 -2
  151. package/dist/authorization/model/plugin.d.ts.map +0 -1
  152. package/dist/authorization/model/plugin.js +0 -45
  153. package/dist/authorization/model/plugin.js.map +0 -1
  154. package/dist/authorization/model/schedule.d.ts +0 -2
  155. package/dist/authorization/model/schedule.d.ts.map +0 -1
  156. package/dist/authorization/model/schedule.js +0 -34
  157. package/dist/authorization/model/schedule.js.map +0 -1
  158. package/dist/authorization/model/session.d.ts +0 -2
  159. package/dist/authorization/model/session.d.ts.map +0 -1
  160. package/dist/authorization/model/session.js +0 -38
  161. package/dist/authorization/model/session.js.map +0 -1
  162. package/dist/authorization/model/skill.d.ts +0 -2
  163. package/dist/authorization/model/skill.d.ts.map +0 -1
  164. package/dist/authorization/model/skill.js +0 -39
  165. package/dist/authorization/model/skill.js.map +0 -1
  166. package/dist/authorization/model/workflow.d.ts +0 -2
  167. package/dist/authorization/model/workflow.d.ts.map +0 -1
  168. package/dist/authorization/model/workflow.js +0 -39
  169. package/dist/authorization/model/workflow.js.map +0 -1
  170. package/dist/authorization/model/workflow_execution.d.ts +0 -2
  171. package/dist/authorization/model/workflow_execution.d.ts.map +0 -1
  172. package/dist/authorization/model/workflow_execution.js +0 -33
  173. package/dist/authorization/model/workflow_execution.js.map +0 -1
  174. package/dist/authorization/model/workflow_instance.d.ts +0 -2
  175. package/dist/authorization/model/workflow_instance.d.ts.map +0 -1
  176. package/dist/authorization/model/workflow_instance.js +0 -96
  177. package/dist/authorization/model/workflow_instance.js.map +0 -1
  178. package/dist/authorization/store-test-kit.d.ts +0 -62
  179. package/dist/authorization/store-test-kit.d.ts.map +0 -1
  180. package/dist/authorization/store-test-kit.js +0 -265
  181. package/dist/authorization/store-test-kit.js.map +0 -1
  182. package/src/authorization/__tests__/enterprise-model.ts +0 -59
  183. package/src/authorization/__tests__/fixtures/fga/agent-channel-owner.fga.yaml +0 -112
  184. package/src/authorization/__tests__/fixtures/fga/agent-instance-creation.fga.yaml +0 -177
  185. package/src/authorization/__tests__/fixtures/fga/agent-share-owner.fga.yaml +0 -109
  186. package/src/authorization/__tests__/fixtures/fga/artifact-org-and-owner.fga.yaml +0 -128
  187. package/src/authorization/__tests__/fixtures/fga/blueprint-private-visibility.fga.yaml +0 -318
  188. package/src/authorization/__tests__/fixtures/fga/channel-app-owner.fga.yaml +0 -125
  189. package/src/authorization/__tests__/fixtures/fga/channel-participation.fga.yaml +0 -201
  190. package/src/authorization/__tests__/fixtures/fga/channel-session-visibility.fga.yaml +0 -187
  191. package/src/authorization/__tests__/fixtures/fga/default-instance-inheritance.fga.yaml +0 -214
  192. package/src/authorization/__tests__/fixtures/fga/mcp-server-authoring.fga.yaml +0 -129
  193. package/src/authorization/__tests__/fixtures/fga/memory-subject-only.fga.yaml +0 -72
  194. package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +0 -308
  195. package/src/authorization/__tests__/fixtures/fga/org-shared-environment.fga.yaml +0 -144
  196. package/src/authorization/__tests__/fixtures/fga/platform-visibility.fga.yaml +0 -199
  197. package/src/authorization/__tests__/fixtures/fga/provider-standing-viewer.fga.yaml +0 -40
  198. package/src/authorization/__tests__/fixtures/fga/reserved-label-writer.fga.yaml +0 -41
  199. package/src/authorization/__tests__/fixtures/fga/schedule-owner.fga.yaml +0 -113
  200. package/src/authorization/__tests__/fixtures/fga/schedule-session-visibility.fga.yaml +0 -214
  201. package/src/authorization/__tests__/fixtures/fga/session-personal-resource.fga.yaml +0 -117
  202. package/src/authorization/__tests__/fixtures/fga/workflow-execution-sharing.fga.yaml +0 -190
  203. package/src/authorization/model/agent.ts +0 -64
  204. package/src/authorization/model/agent_channel.ts +0 -57
  205. package/src/authorization/model/agent_execution.ts +0 -33
  206. package/src/authorization/model/agent_instance.ts +0 -66
  207. package/src/authorization/model/agent_share.ts +0 -40
  208. package/src/authorization/model/api_key.ts +0 -28
  209. package/src/authorization/model/artifact.ts +0 -38
  210. package/src/authorization/model/channel_app.ts +0 -39
  211. package/src/authorization/model/environment.ts +0 -48
  212. package/src/authorization/model/execution_context.ts +0 -20
  213. package/src/authorization/model/iam_policy.ts +0 -42
  214. package/src/authorization/model/identity_account.ts +0 -26
  215. package/src/authorization/model/mcp_server.ts +0 -59
  216. package/src/authorization/model/memory.ts +0 -27
  217. package/src/authorization/model/oauth_app.ts +0 -43
  218. package/src/authorization/model/organization.ts +0 -59
  219. package/src/authorization/model/platform_client.ts +0 -43
  220. package/src/authorization/model/plugin.ts +0 -66
  221. package/src/authorization/model/schedule.ts +0 -49
  222. package/src/authorization/model/session.ts +0 -50
  223. package/src/authorization/model/skill.ts +0 -60
  224. package/src/authorization/model/workflow.ts +0 -60
  225. package/src/authorization/model/workflow_execution.ts +0 -46
  226. package/src/authorization/model/workflow_instance.ts +0 -125
  227. package/src/authorization/store-test-kit.ts +0 -395
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Pins the compiled model's reader (openfga-json.ts): what it builds from
3
+ * OpenFGA's JSON, and every refusal its header names, one case each, over
4
+ * a small model in the compiled file's own canonical shape. The real model
5
+ * goes through the same reader whenever model/index.ts loads, so a model
6
+ * this server cannot evaluate fails every suite that imports it; these
7
+ * cases say which input fails, and with what message.
8
+ */
9
+ import { describe, expect, it } from "vitest";
10
+
11
+ import { readOpenFgaModel } from "../openfga-json.js";
12
+
13
+ type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
14
+
15
+ /** The compiled file's shape, as far as these fixtures edit it. */
16
+ interface CompiledModel {
17
+ schema_version: string;
18
+ conditions?: Json;
19
+ type_definitions: TypeDefinition[];
20
+ }
21
+
22
+ interface TypeDefinition {
23
+ type: string;
24
+ relations?: { [relation: string]: Json };
25
+ metadata: {
26
+ module: string;
27
+ source_info: { file: string };
28
+ relations?: { [relation: string]: { directly_related_user_types?: Json[] } };
29
+ };
30
+ }
31
+
32
+ /**
33
+ * A three-type model as the generator writes it: an organization with a
34
+ * direct `admin`, and a doc whose `viewer` unions a direct list (a person
35
+ * or an organization's admins), its `owner`, and `admin from org`.
36
+ */
37
+ function baseModel(): CompiledModel {
38
+ return {
39
+ schema_version: "1.2",
40
+ type_definitions: [
41
+ {
42
+ metadata: {
43
+ module: "core",
44
+ relations: { admin: { directly_related_user_types: [{ type: "identity_account" }] } },
45
+ source_info: { file: "core.fga" },
46
+ },
47
+ relations: { admin: { this: {} } },
48
+ type: "organization",
49
+ },
50
+ {
51
+ metadata: { module: "core", source_info: { file: "core.fga" } },
52
+ type: "identity_account",
53
+ },
54
+ {
55
+ metadata: {
56
+ module: "docs",
57
+ relations: {
58
+ org: { directly_related_user_types: [{ type: "organization" }] },
59
+ owner: { directly_related_user_types: [{ type: "identity_account" }] },
60
+ viewer: {
61
+ directly_related_user_types: [
62
+ { type: "identity_account" },
63
+ { relation: "admin", type: "organization" },
64
+ ],
65
+ },
66
+ },
67
+ source_info: { file: "docs/doc.fga" },
68
+ },
69
+ relations: {
70
+ org: { this: {} },
71
+ owner: { this: {} },
72
+ viewer: {
73
+ union: {
74
+ child: [
75
+ { this: {} },
76
+ { computedUserset: { relation: "owner" } },
77
+ {
78
+ tupleToUserset: {
79
+ computedUserset: { relation: "admin" },
80
+ tupleset: { relation: "org" },
81
+ },
82
+ },
83
+ ],
84
+ },
85
+ },
86
+ },
87
+ type: "doc",
88
+ },
89
+ ],
90
+ };
91
+ }
92
+
93
+ /** The base model with one edit applied to a fresh copy. */
94
+ function edited(edit: (model: CompiledModel) => void): unknown {
95
+ const model = baseModel();
96
+ edit(model);
97
+ return model;
98
+ }
99
+
100
+ /** The doc type's relations and their metadata, both present in the base model. */
101
+ function docOf(model: CompiledModel): {
102
+ relations: { [relation: string]: Json };
103
+ metadata: { [relation: string]: { directly_related_user_types?: Json[] } };
104
+ } {
105
+ const doc = model.type_definitions[2];
106
+ if (doc?.relations === undefined || doc.metadata.relations === undefined) {
107
+ throw new Error("the base model's doc type has relations");
108
+ }
109
+ return { relations: doc.relations, metadata: doc.metadata.relations };
110
+ }
111
+
112
+ describe("readOpenFgaModel", () => {
113
+ it("reads every type in file order, its source under fga/model, and its relations as the evaluator's vocabulary", () => {
114
+ const types = readOpenFgaModel(baseModel());
115
+ expect(types.map((t) => t.type)).toEqual(["organization", "identity_account", "doc"]);
116
+ expect(types.map((t) => t.source)).toEqual([
117
+ "fga/model/core.fga",
118
+ "fga/model/core.fga",
119
+ "fga/model/docs/doc.fga",
120
+ ]);
121
+ const doc = types[2];
122
+ expect([...(doc?.relations.keys() ?? [])]).toEqual(["org", "owner", "viewer"]);
123
+ expect(doc?.relations.get("viewer")).toEqual({
124
+ node: "union",
125
+ members: [
126
+ {
127
+ node: "this",
128
+ subjects: [
129
+ { form: "object", type: "identity_account" },
130
+ { form: "userset", type: "organization", relation: "admin" },
131
+ ],
132
+ },
133
+ { node: "computed", relation: "owner" },
134
+ { node: "from", relation: "admin", tupleset: "org" },
135
+ ],
136
+ });
137
+ expect(types[1]?.relations.size).toBe(0);
138
+ });
139
+
140
+ it("keeps a union's children in the file's order, and accepts nesting to any depth", () => {
141
+ const types = readOpenFgaModel(
142
+ edited((model) => {
143
+ docOf(model).relations["viewer"] = {
144
+ intersection: {
145
+ child: [
146
+ {
147
+ union: {
148
+ child: [{ computedUserset: { relation: "owner" } }, { this: {} }],
149
+ },
150
+ },
151
+ {
152
+ tupleToUserset: {
153
+ computedUserset: { relation: "admin" },
154
+ tupleset: { relation: "org" },
155
+ },
156
+ },
157
+ ],
158
+ },
159
+ };
160
+ }),
161
+ );
162
+ const viewer = types[2]?.relations.get("viewer");
163
+ expect(viewer?.node).toBe("intersection");
164
+ if (viewer?.node !== "intersection") return;
165
+ expect(viewer.members.map((m) => m.node)).toEqual(["union", "from"]);
166
+ const inner = viewer.members[0];
167
+ expect(inner?.node === "union" ? inner.members.map((m) => m.node) : []).toEqual([
168
+ "computed",
169
+ "this",
170
+ ]);
171
+ });
172
+
173
+ describe("refuses what the evaluator does not implement", () => {
174
+ it("a schema version other than 1.2", () => {
175
+ expect(() =>
176
+ readOpenFgaModel(edited((model) => (model.schema_version = "1.1"))),
177
+ ).toThrow("schema_version '1.1' is not 1.2");
178
+ });
179
+
180
+ it("conditions", () => {
181
+ expect(() =>
182
+ readOpenFgaModel(edited((model) => (model.conditions = { in_hours: {} }))),
183
+ ).toThrow("conditions are not evaluated by this server");
184
+ expect(() =>
185
+ readOpenFgaModel(
186
+ edited((model) => {
187
+ const types = docOf(model).metadata["owner"]?.directly_related_user_types;
188
+ types?.push({ type: "identity_account", condition: "in_hours" });
189
+ }),
190
+ ),
191
+ ).toThrow("conditional subject types are not evaluated by this server");
192
+ });
193
+
194
+ it("a wildcard subject type", () => {
195
+ expect(() =>
196
+ readOpenFgaModel(
197
+ edited((model) => {
198
+ docOf(model).metadata["owner"]?.directly_related_user_types?.push({
199
+ type: "identity_account",
200
+ wildcard: {},
201
+ });
202
+ }),
203
+ ),
204
+ ).toThrow("wildcard subject types are not evaluated by this server");
205
+ });
206
+
207
+ it("'but not' (difference)", () => {
208
+ expect(() =>
209
+ readOpenFgaModel(
210
+ edited((model) => {
211
+ docOf(model).relations["owner"] = {
212
+ difference: {
213
+ base: { this: {} },
214
+ subtract: { computedUserset: { relation: "viewer" } },
215
+ },
216
+ };
217
+ }),
218
+ ),
219
+ ).toThrow("'but not' (difference) is not evaluated by this server");
220
+ });
221
+
222
+ it("a computed relation on another object", () => {
223
+ expect(() =>
224
+ readOpenFgaModel(
225
+ edited((model) => {
226
+ docOf(model).relations["viewer"] = {
227
+ computedUserset: { object: "doc:x", relation: "owner" },
228
+ };
229
+ delete docOf(model).metadata["viewer"];
230
+ }),
231
+ ),
232
+ ).toThrow("a relation on another object is not evaluated by this server");
233
+ });
234
+
235
+ it("an unknown key anywhere, named by where it is", () => {
236
+ expect(() =>
237
+ readOpenFgaModel(edited((model) => Object.assign(model, { id: "01ABC" }))),
238
+ ).toThrow("authorization-model.json: unknown key 'id'");
239
+ expect(() =>
240
+ readOpenFgaModel(
241
+ edited((model) => {
242
+ docOf(model).relations["org"] = { this: { extra: true } };
243
+ }),
244
+ ),
245
+ ).toThrow("relations.org.this: unknown key 'extra'");
246
+ });
247
+
248
+ it("a node with more than one form, and an operator with no children", () => {
249
+ expect(() =>
250
+ readOpenFgaModel(
251
+ edited((model) => {
252
+ docOf(model).relations["org"] = { this: {}, computedUserset: { relation: "owner" } };
253
+ }),
254
+ ),
255
+ ).toThrow("a rewrite node has exactly one form, found 2");
256
+ expect(() =>
257
+ readOpenFgaModel(
258
+ edited((model) => {
259
+ docOf(model).relations["viewer"] = { union: { child: [] } };
260
+ delete docOf(model).metadata["viewer"];
261
+ }),
262
+ ),
263
+ ).toThrow("an operator with no children");
264
+ });
265
+
266
+ it("a type defined twice", () => {
267
+ expect(() =>
268
+ readOpenFgaModel(
269
+ edited((model) => {
270
+ model.type_definitions.push({
271
+ metadata: { module: "core", source_info: { file: "core.fga" } },
272
+ type: "identity_account",
273
+ });
274
+ }),
275
+ ),
276
+ ).toThrow("type 'identity_account' is defined twice");
277
+ });
278
+ });
279
+
280
+ describe("re-asserts what OpenFGA proved, so a hand edit fails at load", () => {
281
+ it("a computed target or a tupleset the type does not define", () => {
282
+ expect(() =>
283
+ readOpenFgaModel(
284
+ edited((model) => {
285
+ docOf(model).relations["owner"] = { computedUserset: { relation: "editor" } };
286
+ delete docOf(model).metadata["owner"];
287
+ }),
288
+ ),
289
+ ).toThrow("doc#owner: names relation 'editor', which doc does not define");
290
+ expect(() =>
291
+ readOpenFgaModel(
292
+ edited((model) => {
293
+ docOf(model).relations["owner"] = {
294
+ tupleToUserset: {
295
+ computedUserset: { relation: "admin" },
296
+ tupleset: { relation: "parent" },
297
+ },
298
+ };
299
+ delete docOf(model).metadata["owner"];
300
+ }),
301
+ ),
302
+ ).toThrow("doc#owner: names tupleset 'parent', which doc does not define");
303
+ });
304
+
305
+ it("a subject type the model does not define, or a userset relation its type does not define", () => {
306
+ expect(() =>
307
+ readOpenFgaModel(
308
+ edited((model) => {
309
+ docOf(model).metadata["owner"]?.directly_related_user_types?.push({
310
+ type: "team",
311
+ });
312
+ }),
313
+ ),
314
+ ).toThrow("doc#owner: admits subject type 'team', which the model does not define");
315
+ expect(() =>
316
+ readOpenFgaModel(
317
+ edited((model) => {
318
+ docOf(model).metadata["owner"]?.directly_related_user_types?.push({
319
+ type: "organization",
320
+ relation: "member",
321
+ });
322
+ }),
323
+ ),
324
+ ).toThrow("doc#owner: admits 'organization#member', which organization does not define");
325
+ });
326
+
327
+ it("a direct list exactly where the rewrite has a direct node", () => {
328
+ expect(() =>
329
+ readOpenFgaModel(
330
+ edited((model) => {
331
+ delete docOf(model).metadata["owner"];
332
+ }),
333
+ ),
334
+ ).toThrow("relations.owner: a direct relation with no directly_related_user_types");
335
+ expect(() =>
336
+ readOpenFgaModel(
337
+ edited((model) => {
338
+ docOf(model).relations["owner"] = { computedUserset: { relation: "org" } };
339
+ }),
340
+ ),
341
+ ).toThrow("relations.owner: directly_related_user_types on a relation with no direct node");
342
+ expect(() =>
343
+ readOpenFgaModel(
344
+ edited((model) => {
345
+ docOf(model).metadata["editor"] = {
346
+ directly_related_user_types: [{ type: "identity_account" }],
347
+ };
348
+ }),
349
+ ),
350
+ ).toThrow("metadata.relations.editor: metadata for a relation doc does not define");
351
+ });
352
+ });
353
+ });