@stigmer/server 3.27.1 → 3.28.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 (245) hide show
  1. package/README.md +2 -0
  2. package/dist/authorization/authorizer.d.ts +7 -5
  3. package/dist/authorization/authorizer.d.ts.map +1 -1
  4. package/dist/authorization/authorizer.js +7 -5
  5. package/dist/authorization/authorizer.js.map +1 -1
  6. package/dist/authorization/derived-tuples.d.ts +3 -1
  7. package/dist/authorization/derived-tuples.d.ts.map +1 -1
  8. package/dist/authorization/derived-tuples.js +52 -2
  9. package/dist/authorization/derived-tuples.js.map +1 -1
  10. package/dist/authorization/evaluator.d.ts +9 -10
  11. package/dist/authorization/evaluator.d.ts.map +1 -1
  12. package/dist/authorization/evaluator.js.map +1 -1
  13. package/dist/authorization/list-read-scope.d.ts.map +1 -1
  14. package/dist/authorization/list-read-scope.js +29 -18
  15. package/dist/authorization/list-read-scope.js.map +1 -1
  16. package/dist/authorization/model/bindings.d.ts +41 -0
  17. package/dist/authorization/model/bindings.d.ts.map +1 -0
  18. package/dist/authorization/model/bindings.js +89 -0
  19. package/dist/authorization/model/bindings.js.map +1 -0
  20. package/dist/authorization/model/data/authorization-model.json +3114 -0
  21. package/dist/authorization/model/default-of.d.ts +2 -2
  22. package/dist/authorization/model/execution-viewer.d.ts +3 -0
  23. package/dist/authorization/model/execution-viewer.d.ts.map +1 -0
  24. package/dist/authorization/model/execution-viewer.js +50 -0
  25. package/dist/authorization/model/execution-viewer.js.map +1 -0
  26. package/dist/authorization/model/index.d.ts +33 -21
  27. package/dist/authorization/model/index.d.ts.map +1 -1
  28. package/dist/authorization/model/index.js +74 -52
  29. package/dist/authorization/model/index.js.map +1 -1
  30. package/dist/authorization/model/openfga-json.d.ts +18 -0
  31. package/dist/authorization/model/openfga-json.d.ts.map +1 -0
  32. package/dist/authorization/model/openfga-json.js +266 -0
  33. package/dist/authorization/model/openfga-json.js.map +1 -0
  34. package/dist/authorization/model/rewrite.d.ts +39 -67
  35. package/dist/authorization/model/rewrite.d.ts.map +1 -1
  36. package/dist/authorization/model/rewrite.js +1 -117
  37. package/dist/authorization/model/rewrite.js.map +1 -1
  38. package/dist/authorization/strict-json.d.ts +20 -0
  39. package/dist/authorization/strict-json.d.ts.map +1 -0
  40. package/dist/authorization/strict-json.js +45 -0
  41. package/dist/authorization/strict-json.js.map +1 -0
  42. package/dist/boot/compose.d.ts.map +1 -1
  43. package/dist/boot/compose.js +9 -7
  44. package/dist/boot/compose.js.map +1 -1
  45. package/dist/boot/list-indexes.d.ts.map +1 -1
  46. package/dist/boot/list-indexes.js +6 -0
  47. package/dist/boot/list-indexes.js.map +1 -1
  48. package/dist/domain/iampolicy/list-index.d.ts +2 -0
  49. package/dist/domain/iampolicy/list-index.d.ts.map +1 -0
  50. package/dist/domain/iampolicy/list-index.js +32 -0
  51. package/dist/domain/iampolicy/list-index.js.map +1 -0
  52. package/dist/domain/iampolicy/resource-store.d.ts.map +1 -1
  53. package/dist/domain/iampolicy/resource-store.js +24 -11
  54. package/dist/domain/iampolicy/resource-store.js.map +1 -1
  55. package/dist/domain/workflowexecution/list-index.d.ts +1 -1
  56. package/dist/extensions/list-read-scope.d.ts +20 -3
  57. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  58. package/dist/extensions/list-read-scope.js +22 -2
  59. package/dist/extensions/list-read-scope.js.map +1 -1
  60. package/dist/index.d.ts +8 -8
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +20 -5
  63. package/dist/index.js.map +1 -1
  64. package/package.json +7 -6
  65. package/src/authorization/README.md +48 -39
  66. package/src/authorization/__tests__/authorizer.test.ts +5 -14
  67. package/src/authorization/__tests__/derived-tuples.test.ts +192 -17
  68. package/src/authorization/__tests__/evaluator.test.ts +53 -160
  69. package/src/authorization/__tests__/facts.test.ts +5 -11
  70. package/src/authorization/__tests__/list-read-scope.measure.test.ts +424 -62
  71. package/src/authorization/__tests__/list-read-scope.test.ts +15 -14
  72. package/src/authorization/__tests__/organization-directory.test.ts +2 -2
  73. package/src/authorization/__tests__/store-test-kit.ts +246 -0
  74. package/src/authorization/__tests__/store-tests.test.ts +91 -223
  75. package/src/authorization/__tests__/support.ts +30 -2
  76. package/src/authorization/__tests__/throwaway-model.ts +75 -0
  77. package/src/authorization/__tests__/wire-permissions.test.ts +141 -107
  78. package/src/authorization/authorizer.ts +7 -5
  79. package/src/authorization/derived-tuples.ts +105 -16
  80. package/src/authorization/evaluator.ts +8 -10
  81. package/src/authorization/list-read-scope.ts +37 -22
  82. package/src/authorization/model/__tests__/openfga-json.test.ts +353 -0
  83. package/src/authorization/model/__tests__/registry.test.ts +102 -276
  84. package/src/authorization/model/bindings.ts +131 -0
  85. package/src/authorization/model/data/authorization-model.json +3114 -0
  86. package/src/authorization/model/default-of.ts +2 -2
  87. package/src/authorization/model/execution-viewer.ts +56 -0
  88. package/src/authorization/model/index.ts +81 -72
  89. package/src/authorization/model/openfga-json.ts +310 -0
  90. package/src/authorization/model/rewrite.ts +39 -188
  91. package/src/authorization/strict-json.ts +52 -0
  92. package/src/boot/__tests__/list-indexes.test.ts +4 -0
  93. package/src/boot/compose.ts +9 -7
  94. package/src/boot/list-indexes.ts +6 -0
  95. package/src/domain/iampolicy/__tests__/resource-store.test.ts +65 -5
  96. package/src/domain/iampolicy/list-index.ts +33 -0
  97. package/src/domain/iampolicy/resource-store.ts +24 -11
  98. package/src/extensions/__tests__/built-in-authorization-composed.test.ts +3 -6
  99. package/src/extensions/list-read-scope.ts +32 -4
  100. package/src/index.ts +20 -44
  101. package/dist/authorization/model/agent.d.ts +0 -2
  102. package/dist/authorization/model/agent.d.ts.map +0 -1
  103. package/dist/authorization/model/agent.js +0 -43
  104. package/dist/authorization/model/agent.js.map +0 -1
  105. package/dist/authorization/model/agent_channel.d.ts +0 -2
  106. package/dist/authorization/model/agent_channel.d.ts.map +0 -1
  107. package/dist/authorization/model/agent_channel.js +0 -39
  108. package/dist/authorization/model/agent_channel.js.map +0 -1
  109. package/dist/authorization/model/agent_execution.d.ts +0 -2
  110. package/dist/authorization/model/agent_execution.d.ts.map +0 -1
  111. package/dist/authorization/model/agent_execution.js +0 -25
  112. package/dist/authorization/model/agent_execution.js.map +0 -1
  113. package/dist/authorization/model/agent_instance.d.ts +0 -2
  114. package/dist/authorization/model/agent_instance.d.ts.map +0 -1
  115. package/dist/authorization/model/agent_instance.js +0 -48
  116. package/dist/authorization/model/agent_instance.js.map +0 -1
  117. package/dist/authorization/model/agent_share.d.ts +0 -2
  118. package/dist/authorization/model/agent_share.d.ts.map +0 -1
  119. package/dist/authorization/model/agent_share.js +0 -29
  120. package/dist/authorization/model/agent_share.js.map +0 -1
  121. package/dist/authorization/model/api_key.d.ts +0 -2
  122. package/dist/authorization/model/api_key.d.ts.map +0 -1
  123. package/dist/authorization/model/api_key.js +0 -27
  124. package/dist/authorization/model/api_key.js.map +0 -1
  125. package/dist/authorization/model/artifact.d.ts +0 -2
  126. package/dist/authorization/model/artifact.d.ts.map +0 -1
  127. package/dist/authorization/model/artifact.js +0 -27
  128. package/dist/authorization/model/artifact.js.map +0 -1
  129. package/dist/authorization/model/channel_app.d.ts +0 -2
  130. package/dist/authorization/model/channel_app.d.ts.map +0 -1
  131. package/dist/authorization/model/channel_app.js +0 -28
  132. package/dist/authorization/model/channel_app.js.map +0 -1
  133. package/dist/authorization/model/environment.d.ts +0 -2
  134. package/dist/authorization/model/environment.d.ts.map +0 -1
  135. package/dist/authorization/model/environment.js +0 -33
  136. package/dist/authorization/model/environment.js.map +0 -1
  137. package/dist/authorization/model/execution_context.d.ts +0 -2
  138. package/dist/authorization/model/execution_context.d.ts.map +0 -1
  139. package/dist/authorization/model/execution_context.js +0 -19
  140. package/dist/authorization/model/execution_context.js.map +0 -1
  141. package/dist/authorization/model/iam_policy.d.ts +0 -2
  142. package/dist/authorization/model/iam_policy.d.ts.map +0 -1
  143. package/dist/authorization/model/iam_policy.js +0 -31
  144. package/dist/authorization/model/iam_policy.js.map +0 -1
  145. package/dist/authorization/model/identity_account.d.ts +0 -2
  146. package/dist/authorization/model/identity_account.d.ts.map +0 -1
  147. package/dist/authorization/model/identity_account.js +0 -25
  148. package/dist/authorization/model/identity_account.js.map +0 -1
  149. package/dist/authorization/model/mcp_server.d.ts +0 -2
  150. package/dist/authorization/model/mcp_server.d.ts.map +0 -1
  151. package/dist/authorization/model/mcp_server.js +0 -38
  152. package/dist/authorization/model/mcp_server.js.map +0 -1
  153. package/dist/authorization/model/memory.d.ts +0 -2
  154. package/dist/authorization/model/memory.d.ts.map +0 -1
  155. package/dist/authorization/model/memory.js +0 -26
  156. package/dist/authorization/model/memory.js.map +0 -1
  157. package/dist/authorization/model/oauth_app.d.ts +0 -2
  158. package/dist/authorization/model/oauth_app.d.ts.map +0 -1
  159. package/dist/authorization/model/oauth_app.js +0 -31
  160. package/dist/authorization/model/oauth_app.js.map +0 -1
  161. package/dist/authorization/model/organization.d.ts +0 -2
  162. package/dist/authorization/model/organization.d.ts.map +0 -1
  163. package/dist/authorization/model/organization.js +0 -58
  164. package/dist/authorization/model/organization.js.map +0 -1
  165. package/dist/authorization/model/platform_client.d.ts +0 -2
  166. package/dist/authorization/model/platform_client.d.ts.map +0 -1
  167. package/dist/authorization/model/platform_client.js +0 -31
  168. package/dist/authorization/model/platform_client.js.map +0 -1
  169. package/dist/authorization/model/plugin.d.ts +0 -2
  170. package/dist/authorization/model/plugin.d.ts.map +0 -1
  171. package/dist/authorization/model/plugin.js +0 -45
  172. package/dist/authorization/model/plugin.js.map +0 -1
  173. package/dist/authorization/model/schedule.d.ts +0 -2
  174. package/dist/authorization/model/schedule.d.ts.map +0 -1
  175. package/dist/authorization/model/schedule.js +0 -34
  176. package/dist/authorization/model/schedule.js.map +0 -1
  177. package/dist/authorization/model/session.d.ts +0 -2
  178. package/dist/authorization/model/session.d.ts.map +0 -1
  179. package/dist/authorization/model/session.js +0 -38
  180. package/dist/authorization/model/session.js.map +0 -1
  181. package/dist/authorization/model/skill.d.ts +0 -2
  182. package/dist/authorization/model/skill.d.ts.map +0 -1
  183. package/dist/authorization/model/skill.js +0 -39
  184. package/dist/authorization/model/skill.js.map +0 -1
  185. package/dist/authorization/model/workflow.d.ts +0 -2
  186. package/dist/authorization/model/workflow.d.ts.map +0 -1
  187. package/dist/authorization/model/workflow.js +0 -39
  188. package/dist/authorization/model/workflow.js.map +0 -1
  189. package/dist/authorization/model/workflow_execution.d.ts +0 -2
  190. package/dist/authorization/model/workflow_execution.d.ts.map +0 -1
  191. package/dist/authorization/model/workflow_execution.js +0 -33
  192. package/dist/authorization/model/workflow_execution.js.map +0 -1
  193. package/dist/authorization/model/workflow_instance.d.ts +0 -2
  194. package/dist/authorization/model/workflow_instance.d.ts.map +0 -1
  195. package/dist/authorization/model/workflow_instance.js +0 -96
  196. package/dist/authorization/model/workflow_instance.js.map +0 -1
  197. package/dist/authorization/store-test-kit.d.ts +0 -62
  198. package/dist/authorization/store-test-kit.d.ts.map +0 -1
  199. package/dist/authorization/store-test-kit.js +0 -265
  200. package/dist/authorization/store-test-kit.js.map +0 -1
  201. package/src/authorization/__tests__/fixtures/fga/agent-channel-owner.fga.yaml +0 -112
  202. package/src/authorization/__tests__/fixtures/fga/agent-instance-creation.fga.yaml +0 -177
  203. package/src/authorization/__tests__/fixtures/fga/agent-share-owner.fga.yaml +0 -109
  204. package/src/authorization/__tests__/fixtures/fga/artifact-org-and-owner.fga.yaml +0 -128
  205. package/src/authorization/__tests__/fixtures/fga/blueprint-private-visibility.fga.yaml +0 -318
  206. package/src/authorization/__tests__/fixtures/fga/channel-app-owner.fga.yaml +0 -125
  207. package/src/authorization/__tests__/fixtures/fga/channel-participation.fga.yaml +0 -201
  208. package/src/authorization/__tests__/fixtures/fga/channel-session-visibility.fga.yaml +0 -187
  209. package/src/authorization/__tests__/fixtures/fga/default-instance-inheritance.fga.yaml +0 -214
  210. package/src/authorization/__tests__/fixtures/fga/mcp-server-authoring.fga.yaml +0 -129
  211. package/src/authorization/__tests__/fixtures/fga/memory-subject-only.fga.yaml +0 -72
  212. package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +0 -308
  213. package/src/authorization/__tests__/fixtures/fga/org-shared-environment.fga.yaml +0 -144
  214. package/src/authorization/__tests__/fixtures/fga/platform-visibility.fga.yaml +0 -199
  215. package/src/authorization/__tests__/fixtures/fga/provider-standing-viewer.fga.yaml +0 -40
  216. package/src/authorization/__tests__/fixtures/fga/reserved-label-writer.fga.yaml +0 -41
  217. package/src/authorization/__tests__/fixtures/fga/schedule-owner.fga.yaml +0 -113
  218. package/src/authorization/__tests__/fixtures/fga/schedule-session-visibility.fga.yaml +0 -214
  219. package/src/authorization/__tests__/fixtures/fga/session-personal-resource.fga.yaml +0 -117
  220. package/src/authorization/__tests__/fixtures/fga/workflow-execution-sharing.fga.yaml +0 -190
  221. package/src/authorization/model/agent.ts +0 -64
  222. package/src/authorization/model/agent_channel.ts +0 -57
  223. package/src/authorization/model/agent_execution.ts +0 -33
  224. package/src/authorization/model/agent_instance.ts +0 -66
  225. package/src/authorization/model/agent_share.ts +0 -40
  226. package/src/authorization/model/api_key.ts +0 -28
  227. package/src/authorization/model/artifact.ts +0 -38
  228. package/src/authorization/model/channel_app.ts +0 -39
  229. package/src/authorization/model/environment.ts +0 -48
  230. package/src/authorization/model/execution_context.ts +0 -20
  231. package/src/authorization/model/iam_policy.ts +0 -42
  232. package/src/authorization/model/identity_account.ts +0 -26
  233. package/src/authorization/model/mcp_server.ts +0 -59
  234. package/src/authorization/model/memory.ts +0 -27
  235. package/src/authorization/model/oauth_app.ts +0 -43
  236. package/src/authorization/model/organization.ts +0 -59
  237. package/src/authorization/model/platform_client.ts +0 -43
  238. package/src/authorization/model/plugin.ts +0 -66
  239. package/src/authorization/model/schedule.ts +0 -49
  240. package/src/authorization/model/session.ts +0 -50
  241. package/src/authorization/model/skill.ts +0 -60
  242. package/src/authorization/model/workflow.ts +0 -60
  243. package/src/authorization/model/workflow_execution.ts +0 -46
  244. package/src/authorization/model/workflow_instance.ts +0 -125
  245. package/src/authorization/store-test-kit.ts +0 -395
@@ -14,6 +14,10 @@
14
14
  * source, an absent object answering nothing, a store fault propagating
15
15
  * as the fault it is — the ratified store-fault mapping, which is what
16
16
  * lets the driver above fold it to `unavailable` and never to a denial.
17
+ * Its grants-to-teams arm evaluates the built-in model's Enterprise team
18
+ * type: a grant to a team reaches its members and nobody
19
+ * who left the organization, and a person in no team — every open-source
20
+ * caller — still costs the source exactly one read.
17
21
  */
18
22
  import { create } from "@bufbuild/protobuf";
19
23
  import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
@@ -40,20 +44,12 @@ import { deriveTuples, newDerivedTupleSource } from "../derived-tuples.js";
40
44
  import { checkRelation } from "../evaluator.js";
41
45
  import { rowFactsOf } from "../facts.js";
42
46
  import { builtInModel } from "../model/index.js";
43
- import type { KindDeclaration } from "../model/rewrite.js";
44
47
  import type { Person } from "../tuples.js";
45
48
  import { formatTuple, parseObjectRef } from "../tuples.js";
46
49
  import { driverFixtures, dropPostgresFixture } from "./drivers.js";
47
50
  import type { OpenedStore } from "./drivers.js";
48
- import { fixtureRow } from "./support.js";
49
-
50
- function declared(type: string): KindDeclaration {
51
- const declaration = builtInModel.byType(type);
52
- if (declaration === undefined) {
53
- throw new Error(`${type} is declared in this slice`);
54
- }
55
- return declaration;
56
- }
51
+ import { fixtureRow, storedDeclaration } from "./support.js";
52
+ import type { StoredKindDeclaration } from "./support.js";
57
53
 
58
54
  function derivedFor(
59
55
  type: string,
@@ -64,7 +60,7 @@ function derivedFor(
64
60
  spec?: Readonly<Record<string, unknown>>;
65
61
  },
66
62
  ): string[] {
67
- const declaration = declared(type);
63
+ const declaration = storedDeclaration(type);
68
64
  const row = fixtureRow(declaration, {
69
65
  id: `${type}-1`,
70
66
  org: facts.org ?? "acme",
@@ -244,6 +240,7 @@ const SEEDED_KINDS: ReadonlyArray<ApiResourceKind> = [
244
240
  ApiResourceKind.workflow_instance,
245
241
  ApiResourceKind.workflow_execution,
246
242
  ApiResourceKind.identity_account,
243
+ ApiResourceKind.team,
247
244
  ];
248
245
 
249
246
  afterAll(dropPostgresFixture);
@@ -295,7 +292,7 @@ describe.each(driverFixtures(SEEDED_KINDS))(
295
292
  opened = await fixture.open();
296
293
  counted = countingPolicies(newResourceIamPolicyStore(opened.store));
297
294
  accounts = newResourceIdentityAccountStore(opened.store);
298
- const organization = declared("organization");
295
+ const organization = storedDeclaration("organization");
299
296
  await opened.store.saveResource(
300
297
  ApiResourceKind.organization,
301
298
  "acme",
@@ -307,7 +304,7 @@ describe.each(driverFixtures(SEEDED_KINDS))(
307
304
  createdBy: ROOT.accountId,
308
305
  }),
309
306
  );
310
- const agent = declared("agent");
307
+ const agent = storedDeclaration("agent");
311
308
  await opened.store.saveResource(
312
309
  ApiResourceKind.agent,
313
310
  "agt_team",
@@ -433,17 +430,28 @@ describe.each(driverFixtures(SEEDED_KINDS))(
433
430
  ).toEqual([]);
434
431
  });
435
432
 
436
- it("an absent object, or an object of a kind this edition does not declare, answers no tuples", async () => {
433
+ it("an absent object, a declared kind this edition stores no row of, a rowless kind, or a kind the model does not declare answers no tuples", async () => {
437
434
  const source = sourceFor(ROOT);
438
435
  expect(
439
436
  await source.tuplesOf(parseObjectRef("agent:agt_missing"), "owner"),
440
437
  ).toEqual([]);
438
+ // Declared, served by the wider editions: the store has no such row.
441
439
  expect(
442
440
  await source.tuplesOf(
443
441
  parseObjectRef("identity_provider:idp"),
444
442
  "owner",
445
443
  ),
446
444
  ).toEqual([]);
445
+ // Rowless: no row is loaded for it at all.
446
+ expect(
447
+ await source.tuplesOf(parseObjectRef("platform:stigmer"), "operator"),
448
+ ).toEqual([]);
449
+ expect(
450
+ await source.tuplesOf(
451
+ parseObjectRef("api_resource_version:v1"),
452
+ "owner",
453
+ ),
454
+ ).toEqual([]);
447
455
  expect(
448
456
  await source.tuplesOf(parseObjectRef("nonsense:x"), "owner"),
449
457
  ).toEqual([]);
@@ -462,7 +470,7 @@ describe.each(driverFixtures(SEEDED_KINDS))(
462
470
 
463
471
  it("a source seeded with a candidate's facts derives that object's tuples WITHOUT loading its row; the parent hop still loads", async () => {
464
472
  let rowReads = 0;
465
- const agent = declared("agent");
473
+ const agent = storedDeclaration("agent");
466
474
  const seeded = newDerivedTupleSource(
467
475
  {
468
476
  store: {
@@ -517,7 +525,7 @@ describe.each(driverFixtures(SEEDED_KINDS))(
517
525
  // carry, so the rule loads the row — one read, stated in the list
518
526
  // scope's cost pin as well.
519
527
  let rowReads = 0;
520
- const instance = declared("agent_instance");
528
+ const instance = storedDeclaration("agent_instance");
521
529
  const row = fixtureRow(instance, {
522
530
  id: "ai_seeded",
523
531
  org: "acme",
@@ -567,6 +575,173 @@ describe.each(driverFixtures(SEEDED_KINDS))(
567
575
  });
568
576
  });
569
577
 
578
+ /**
579
+ * The grants made to a person's teams, through the real source and the
580
+ * evaluator over the built-in model's Enterprise team type. Seeded:
581
+ * `acme` with Dave a member and Root its owner; `tm_core` and
582
+ * `tm_other` in `acme`; Dave and Erin granted `member` on `tm_core`,
583
+ * Erin holding no organization role (she left); `agt_core` shared with
584
+ * `tm_core#member` and `agt_other` with `tm_other#member`, both private
585
+ * and Carol's.
586
+ */
587
+ describe.skipIf(fixture.skip)("the grants made to a person's teams", () => {
588
+ let opened: OpenedStore;
589
+ let counted: ReturnType<typeof countingPolicies>;
590
+ const ERIN: Person = {
591
+ accountId: "ida_erin",
592
+ aliases: new Set(["ida_erin"]),
593
+ };
594
+
595
+ async function seed(
596
+ declaration: StoredKindDeclaration,
597
+ facts: Parameters<typeof fixtureRow>[1],
598
+ ): Promise<void> {
599
+ await opened.store.saveResource(
600
+ declaration.kind,
601
+ facts.id,
602
+ declaration.schema,
603
+ fixtureRow(declaration, facts),
604
+ );
605
+ }
606
+
607
+ beforeEach(async () => {
608
+ opened = await fixture.open();
609
+ counted = countingPolicies(newResourceIamPolicyStore(opened.store));
610
+ await seed(storedDeclaration("organization"), {
611
+ id: "acme",
612
+ org: "",
613
+ visibility: ApiResourceVisibility.visibility_private,
614
+ createdBy: ROOT.accountId,
615
+ });
616
+ for (const id of ["tm_core", "tm_other"]) {
617
+ await seed(storedDeclaration("team"), {
618
+ id,
619
+ org: "acme",
620
+ visibility: ApiResourceVisibility.visibility_private,
621
+ createdBy: ROOT.accountId,
622
+ });
623
+ }
624
+ for (const [id, team] of [
625
+ ["agt_core", "tm_core"],
626
+ ["agt_other", "tm_other"],
627
+ ] as const) {
628
+ await seed(storedDeclaration("agent"), {
629
+ id,
630
+ org: "acme",
631
+ visibility: ApiResourceVisibility.visibility_private,
632
+ createdBy: "ida_carol",
633
+ });
634
+ await grant(
635
+ counted.policies,
636
+ triple({ kind: "team", id: team, relation: "member" }, "viewer", {
637
+ kind: "agent",
638
+ id,
639
+ }),
640
+ );
641
+ }
642
+ await grant(counted.policies, orgRole(ROOT.accountId, "owner", "acme"));
643
+ await grant(
644
+ counted.policies,
645
+ orgRole(DAVE.accountId, "member", "acme"),
646
+ );
647
+ for (const member of [DAVE, ERIN]) {
648
+ await grant(
649
+ counted.policies,
650
+ triple(
651
+ { kind: "identity_account", id: member.accountId },
652
+ "member",
653
+ { kind: "team", id: "tm_core" },
654
+ ),
655
+ );
656
+ }
657
+ });
658
+
659
+ afterEach(async () => {
660
+ await opened.close();
661
+ });
662
+
663
+ function sourceFor(person: Person) {
664
+ return newDerivedTupleSource(
665
+ {
666
+ store: opened.store,
667
+ policies: counted.policies,
668
+ accounts: newResourceIdentityAccountStore(opened.store),
669
+ model: builtInModel,
670
+ },
671
+ person,
672
+ );
673
+ }
674
+
675
+ async function allowed(
676
+ person: Person,
677
+ object: string,
678
+ relation: string,
679
+ ): Promise<boolean> {
680
+ return checkRelation(
681
+ { model: builtInModel, source: sourceFor(person) },
682
+ parseObjectRef(object),
683
+ relation,
684
+ person,
685
+ );
686
+ }
687
+
688
+ it("a grant to a team reaches its member: the source serves the team's row on the object, and the member views it", async () => {
689
+ expect(
690
+ (
691
+ await sourceFor(DAVE).tuplesOf(
692
+ parseObjectRef("agent:agt_core"),
693
+ "viewer",
694
+ )
695
+ ).map(formatTuple),
696
+ ).toEqual(["agent:agt_core#viewer@team:tm_core#member"]);
697
+ expect(await allowed(DAVE, "agent:agt_core", "can_view")).toBe(true);
698
+ });
699
+
700
+ it("a member who left the organization reads the team's rows and is admitted by none of them — the model's bound, not a cleanup", async () => {
701
+ expect(
702
+ (
703
+ await sourceFor(ERIN).tuplesOf(
704
+ parseObjectRef("agent:agt_core"),
705
+ "viewer",
706
+ )
707
+ ).map(formatTuple),
708
+ ).toEqual(["agent:agt_core#viewer@team:tm_core#member"]);
709
+ expect(await allowed(ERIN, "agent:agt_core", "can_view")).toBe(false);
710
+ });
711
+
712
+ it("a grant to a team the person is not in never enters their source", async () => {
713
+ expect(
714
+ await sourceFor(DAVE).tuplesOf(
715
+ parseObjectRef("agent:agt_other"),
716
+ "viewer",
717
+ ),
718
+ ).toEqual([]);
719
+ expect(await allowed(DAVE, "agent:agt_other", "can_view")).toBe(false);
720
+ expect(await allowed(ROOT, "agent:agt_core", "can_view")).toBe(true);
721
+ });
722
+
723
+ it("reads once for a person in no team and once more per team held, however many objects are asked", async () => {
724
+ const root = sourceFor(ROOT);
725
+ await root.tuplesOf(parseObjectRef("agent:agt_core"), "viewer");
726
+ await root.tuplesOf(parseObjectRef("agent:agt_other"), "viewer");
727
+ expect(counted.reads()).toBe(1);
728
+ const dave = sourceFor(DAVE);
729
+ await dave.tuplesOf(parseObjectRef("agent:agt_core"), "viewer");
730
+ await dave.tuplesOf(parseObjectRef("agent:agt_other"), "viewer");
731
+ await dave.tuplesOf(parseObjectRef("organization:acme"), "member");
732
+ expect(counted.reads()).toBe(1 + 2);
733
+ });
734
+
735
+ it("personTuples stays the person's own rows — the organization directory's candidates do not grow with team grants", async () => {
736
+ expect(
737
+ (await sourceFor(DAVE).personTuples()).map(formatTuple).sort(),
738
+ ).toEqual([
739
+ "organization:acme#member@identity_account:ida_dave",
740
+ "team:tm_core#member@identity_account:ida_dave",
741
+ ]);
742
+ });
743
+ });
744
+
570
745
  /**
571
746
  * The two derived rules through the real source and the evaluator:
572
747
  * `default_of` from the blueprint's pointer, `execution_viewer` from
@@ -589,7 +764,7 @@ describe.each(driverFixtures(SEEDED_KINDS))(
589
764
  id: string,
590
765
  facts: Omit<Parameters<typeof fixtureRow>[1], "id">,
591
766
  ): Promise<void> {
592
- const declaration = declared(type);
767
+ const declaration = storedDeclaration(type);
593
768
  await opened.store.saveResource(
594
769
  declaration.kind,
595
770
  id,
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * Pins the evaluator's semantics the store tests cannot express (the
3
- * cloud's `.fga.yaml` documents prove the model's answers; this file
4
- * proves the machinery under them): how a person matches a tuple, that a
5
- * direct tuple outside the line's type restriction is ignored (model drift
6
- * fails closed), how far a resolution may walk, that an intersection
7
- * holds only while every member does (the team bound) and stops at its
8
- * first false member, that `declareKind` refuses a line a `.fga` file
9
- * could not hold, and that a declaration cycle or an undeclared target is
10
- * a thrown fault and never a silent deny.
3
+ * model's `.fga.yaml` suites prove its answers; this file proves the
4
+ * machinery under them): how a person matches a tuple, that a direct
5
+ * tuple outside the line's type restriction is ignored (model drift fails
6
+ * closed), how far a resolution may walk, that an intersection holds only
7
+ * while every member does (the team bound) and stops at its first false
8
+ * member, and that a declaration cycle or an undeclared target is a
9
+ * thrown fault and never a silent deny. What the model file may hold is
10
+ * the reader's to refuse (model/__tests__/openfga-json.test.ts).
11
11
  *
12
12
  * The fixture source is the in-memory one the store-test kit uses, so the
13
13
  * two proofs share one tuple vocabulary. Where a case needs a shape the
14
- * real model does not have (a cycle, a chain past the depth bound), it
15
- * declares a throwaway model with `declareKind`; everything else runs
16
- * over the built-in model so the fixtures are real relations.
14
+ * real model does not have (a cycle, a chain past the depth bound, a type
15
+ * the model omits), it declares a throwaway model (throwaway-model.ts);
16
+ * everything else runs over the built-in model so the fixtures are real
17
+ * relations.
17
18
  */
18
19
  import { describe, expect, it } from "vitest";
19
20
 
@@ -26,23 +27,21 @@ import {
26
27
  checkRelation,
27
28
  } from "../evaluator.js";
28
29
  import { builtInModel, newModel } from "../model/index.js";
30
+ import type { Person, Tuple, TupleSource } from "../tuples.js";
31
+ import {
32
+ newInMemoryTupleSource,
33
+ parseObjectRef,
34
+ parseSubject,
35
+ } from "../tuples.js";
29
36
  import {
30
37
  computed,
31
- declareKind,
32
38
  direct,
33
39
  from,
34
- intersection,
35
40
  objectOf,
41
+ throwawayDeclaration,
36
42
  union,
37
43
  usersetOf,
38
- } from "../model/rewrite.js";
39
- import type { Rewrite } from "../model/rewrite.js";
40
- import type { Person, Tuple, TupleSource } from "../tuples.js";
41
- import {
42
- newInMemoryTupleSource,
43
- parseObjectRef,
44
- parseSubject,
45
- } from "../tuples.js";
44
+ } from "./throwaway-model.js";
46
45
 
47
46
  /** `object#relation@subject` in the FGA tuple notation, the fixtures' own spelling. */
48
47
  function tuple(text: string): Tuple {
@@ -157,13 +156,13 @@ describe("relations and kinds the model does not declare", () => {
157
156
  ).toBe(false);
158
157
  });
159
158
 
160
- it("an undeclared kind as the TARGET is a thrown fault — the driver refused unserved kinds before, so reaching here is a registry gap", async () => {
159
+ it("an undeclared kind as the TARGET is a thrown fault — the model declares every kind, so reaching here is a registry gap", async () => {
161
160
  await expect(
162
- allowed(source, "identity_provider:idp", "can_view", person("ida_carol")),
161
+ allowed(source, "api_resource_version:v1", "can_view", person("ida_carol")),
163
162
  ).rejects.toMatchObject({ reason: "undeclared-target-kind" });
164
163
  });
165
164
 
166
- it("an undeclared kind reached through a hop yields nothing — `agent.platform_viewer` walks `identity_provider#platform_user`", async () => {
165
+ it("a declared kind reached through a hop with no tuple on it yields nothing — `agent.platform_viewer` walks `identity_provider#platform_user`", async () => {
167
166
  const platform = newInMemoryTupleSource([
168
167
  tuple("agent:p#organization@organization:acme"),
169
168
  tuple("agent:p#platform_viewer@identity_provider:idp#platform_user"),
@@ -172,6 +171,30 @@ describe("relations and kinds the model does not declare", () => {
172
171
  await allowed(platform, "agent:p", "can_view", person("ida_zed")),
173
172
  ).toBe(false);
174
173
  });
174
+
175
+ it("a type the model omits, reached through a hop, is an object with no relations — nothing, never a fault", async () => {
176
+ const model = newModel([
177
+ throwawayDeclaration({
178
+ kind: ApiResourceKind.agent,
179
+ schema: AgentSchema,
180
+ relations: [
181
+ ["viewer", direct(usersetOf("api_resource_version", "member"))],
182
+ ["can_view", computed("viewer")],
183
+ ],
184
+ }),
185
+ ]);
186
+ const hop = newInMemoryTupleSource([
187
+ tuple("agent:p#viewer@api_resource_version:v1#member"),
188
+ ]);
189
+ expect(
190
+ await checkRelation(
191
+ { model, source: hop },
192
+ parseObjectRef("agent:p"),
193
+ "can_view",
194
+ person("ida_zed"),
195
+ ),
196
+ ).toBe(false);
197
+ });
175
198
  });
176
199
 
177
200
  describe("the walk itself", () => {
@@ -203,10 +226,9 @@ describe("the walk itself", () => {
203
226
 
204
227
  it("bounds the resolution at OpenFGA's own limit and reports the overrun as a fault", async () => {
205
228
  // A chain of `next from link` deeper than the bound, on a throwaway model.
206
- const deep = declareKind({
229
+ const deep = throwawayDeclaration({
207
230
  kind: ApiResourceKind.agent,
208
231
  schema: AgentSchema,
209
- source: "test/deep.fga",
210
232
  relations: [
211
233
  ["link", direct(objectOf("agent"))],
212
234
  [
@@ -230,23 +252,8 @@ describe("the walk itself", () => {
230
252
  });
231
253
 
232
254
  it("an intersection holds only while every member does — a team member reaches a team grant only while still an organization viewer", async () => {
233
- // The Enterprise team type, declared here because open source serves
234
- // no team: membership bounded by the organization's viewers.
235
- const team = declareKind({
236
- kind: ApiResourceKind.team,
237
- schema: AgentSchema,
238
- source: "test/team.fga",
239
- relations: [
240
- ["organization", direct(objectOf("organization"))],
241
- [
242
- "member",
243
- intersection(
244
- direct(objectOf("identity_account")),
245
- from("viewer", "organization"),
246
- ),
247
- ],
248
- ],
249
- });
255
+ // The model's Enterprise team type: `member` is a person granted the
256
+ // role AND still one of the organization's viewers.
250
257
  const source = newInMemoryTupleSource([
251
258
  tuple("organization:acme#viewer@identity_account:ida_vic"),
252
259
  tuple("team:sre#organization@organization:acme"),
@@ -255,10 +262,9 @@ describe("the walk itself", () => {
255
262
  tuple("agent:a#organization@organization:acme"),
256
263
  tuple("agent:a#viewer@team:sre#member"),
257
264
  ]);
258
- const model = newModel([...builtInModel.declarations, team]);
259
265
  const check = (who: Person): Promise<boolean> =>
260
266
  checkRelation(
261
- { model, source },
267
+ { model: builtInModel, source },
262
268
  parseObjectRef("agent:a"),
263
269
  "can_view",
264
270
  who,
@@ -270,21 +276,6 @@ describe("the walk itself", () => {
270
276
  });
271
277
 
272
278
  it("an intersection stops at its first false member — the direct arm is read before the hop that bounds it", async () => {
273
- const team = declareKind({
274
- kind: ApiResourceKind.team,
275
- schema: AgentSchema,
276
- source: "test/team.fga",
277
- relations: [
278
- ["organization", direct(objectOf("organization"))],
279
- [
280
- "member",
281
- intersection(
282
- direct(objectOf("identity_account")),
283
- from("viewer", "organization"),
284
- ),
285
- ],
286
- ],
287
- });
288
279
  const asked: string[] = [];
289
280
  const inner = newInMemoryTupleSource([
290
281
  tuple("team:sre#organization@organization:acme"),
@@ -297,7 +288,7 @@ describe("the walk itself", () => {
297
288
  };
298
289
  expect(
299
290
  await checkRelation(
300
- { model: newModel([team]), source: counting },
291
+ { model: builtInModel, source: counting },
301
292
  parseObjectRef("team:sre"),
302
293
  "member",
303
294
  person("ida_nobody"),
@@ -307,10 +298,9 @@ describe("the walk itself", () => {
307
298
  });
308
299
 
309
300
  it("a declaration that re-enters itself on one object is a cycle fault, never a silent false", async () => {
310
- const cyclic = declareKind({
301
+ const cyclic = throwawayDeclaration({
311
302
  kind: ApiResourceKind.agent,
312
303
  schema: AgentSchema,
313
- source: "test/cyclic.fga",
314
304
  relations: [
315
305
  ["a", computed("b")],
316
306
  ["b", computed("a")],
@@ -326,100 +316,3 @@ describe("the walk itself", () => {
326
316
  ).rejects.toBeInstanceOf(AuthorizationEvaluationError);
327
317
  });
328
318
  });
329
-
330
- describe("declareKind validates a transcript at load", () => {
331
- it("refuses a computed or tupleset reference to a relation the kind does not declare", () => {
332
- expect(() =>
333
- declareKind({
334
- kind: ApiResourceKind.agent,
335
- schema: AgentSchema,
336
- source: "test/bad.fga",
337
- relations: [["can_view", computed("viewer")]],
338
- }),
339
- ).toThrow("relation 'viewer'");
340
- expect(() =>
341
- declareKind({
342
- kind: ApiResourceKind.agent,
343
- schema: AgentSchema,
344
- source: "test/bad.fga",
345
- relations: [["owner", from("admin", "organization")]],
346
- }),
347
- ).toThrow("tupleset 'organization'");
348
- });
349
-
350
- it("refuses a relation declared twice", () => {
351
- expect(() =>
352
- declareKind({
353
- kind: ApiResourceKind.agent,
354
- schema: AgentSchema,
355
- source: "test/bad.fga",
356
- relations: [
357
- ["viewer", direct(objectOf("identity_account"))],
358
- ["viewer", direct(usersetOf("organization", "viewer"))],
359
- ],
360
- }),
361
- ).toThrow("declared twice");
362
- });
363
-
364
- it("refuses a line no .fga file could hold without parentheses, and an operator over one member", () => {
365
- const declare = (rewrite: Rewrite) =>
366
- declareKind({
367
- kind: ApiResourceKind.agent,
368
- schema: AgentSchema,
369
- source: "test/bad.fga",
370
- relations: [
371
- ["organization", direct(objectOf("organization"))],
372
- ["viewer", rewrite],
373
- ],
374
- });
375
- expect(() =>
376
- declare(
377
- intersection(
378
- union(direct(objectOf("identity_account")), computed("organization")),
379
- from("viewer", "organization"),
380
- ),
381
- ),
382
- ).toThrow("nests or inside and");
383
- expect(() =>
384
- declare(
385
- union(
386
- intersection(
387
- direct(objectOf("identity_account")),
388
- from("viewer", "organization"),
389
- ),
390
- computed("organization"),
391
- ),
392
- ),
393
- ).toThrow("nests and inside or");
394
- expect(() =>
395
- declare(intersection(direct(objectOf("identity_account")))),
396
- ).toThrow("fewer than two members");
397
- expect(() =>
398
- declare(
399
- intersection(
400
- direct(objectOf("identity_account")),
401
- from("viewer", "organization"),
402
- ),
403
- ),
404
- ).not.toThrow();
405
- });
406
-
407
- it("carries the FGA type name of its kind and its relations in file order", () => {
408
- const declaration = declareKind({
409
- kind: ApiResourceKind.mcp_server,
410
- schema: AgentSchema,
411
- source: "test/order.fga",
412
- relations: [
413
- ["organization", direct(objectOf("organization"))],
414
- ["owner", direct(usersetOf("organization", "admin"))],
415
- ["can_edit", computed("owner")],
416
- ],
417
- });
418
- expect(declaration.type).toBe("mcp_server");
419
- expect([...declaration.relations.keys()]).toEqual([
420
- "organization",
421
- "owner",
422
- "can_edit",
423
- ]);
424
- });
425
- });
@@ -36,7 +36,7 @@ import {
36
36
  } from "../../pipeline/steps/authorization-tuples.js";
37
37
  import { rowFactsOf, rowFactsOfEntry } from "../facts.js";
38
38
  import { builtInModel } from "../model/index.js";
39
- import { fixtureRow } from "./support.js";
39
+ import { fixtureRow, hasStoredRows, storedDeclaration } from "./support.js";
40
40
 
41
41
  const CREATOR: CallerIdentity = {
42
42
  identityId: "ida_carol",
@@ -46,7 +46,7 @@ const CREATOR: CallerIdentity = {
46
46
  };
47
47
 
48
48
  describe("rowFactsOf against the tuple lifecycle's own resolution", () => {
49
- for (const declaration of builtInModel.declarations) {
49
+ for (const declaration of builtInModel.declarations.filter(hasStoredRows)) {
50
50
  for (const visibility of [
51
51
  ApiResourceVisibility.visibility_private,
52
52
  ApiResourceVisibility.visibility_org,
@@ -77,10 +77,7 @@ describe("rowFactsOf against the tuple lifecycle's own resolution", () => {
77
77
  }
78
78
 
79
79
  it("carries a legacy row's missing organization as no scope link, where the create-time resolver throws", () => {
80
- const agent = builtInModel.byType("agent");
81
- if (agent === undefined) {
82
- throw new Error("agent is declared in this slice");
83
- }
80
+ const agent = storedDeclaration("agent");
84
81
  const row = fixtureRow(agent, {
85
82
  id: "agent-legacy",
86
83
  org: "",
@@ -94,10 +91,7 @@ describe("rowFactsOf against the tuple lifecycle's own resolution", () => {
94
91
  });
95
92
 
96
93
  it("reads an absent creator stamp as the empty string", () => {
97
- const agent = builtInModel.byType("agent");
98
- if (agent === undefined) {
99
- throw new Error("agent is declared in this slice");
100
- }
94
+ const agent = storedDeclaration("agent");
101
95
  const bare = create(agent.schema, {
102
96
  metadata: { id: "agent-bare", org: "acme" },
103
97
  });
@@ -129,7 +123,7 @@ describe("a list candidate against the loaded row — the facts' two producers",
129
123
  return offered;
130
124
  }
131
125
 
132
- for (const declaration of builtInModel.declarations) {
126
+ for (const declaration of builtInModel.declarations.filter(hasStoredRows)) {
133
127
  for (const visibility of [
134
128
  ApiResourceVisibility.visibility_private,
135
129
  ApiResourceVisibility.visibility_org,