@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
@@ -12,7 +12,7 @@
12
12
  * - owner SELF: <row>#owner@identity_account:<row id>
13
13
  * - creator: <row>#creator@identity_account:<creator stamp> (kinds flagged requires_creator_tuple)
14
14
  * - org-viewer: <row>#viewer@organization:<org>#viewer (visibility_org; cloud#257's shape)
15
- * - platform-viewer: none — it fans out over identity providers, a kind this edition does not serve
15
+ * - platform-viewer: none — it fans out over identity providers, which this edition does not serve: the model defines the type, and no tuple of it is derived here
16
16
  *
17
17
  * Two facts are this edition's own and are stated here, nowhere else:
18
18
  * - The ORGANIZATION's owner is a ROW, not a derivation. 2b's role
@@ -26,18 +26,39 @@
26
26
  * rules' one predicate). The person comparison itself is the
27
27
  * evaluator's, over the aliases `Person` carries.
28
28
  *
29
- * The SOURCE joins two records per (object, relation): the person's own
30
- * IamPolicy rows on the object (read ONCE per source through
31
- * `findByPrincipal` — every row open source writes names an account id,
32
- * so the person's account id is the whole key, and on the OSS adapter one
33
- * read is one scan whatever the number of organizations walked) and the
34
- * tuples derived from the object's row (loaded once per object through
35
- * the loader, which a declaration's `derived` rule also reads related
36
- * rows through). An absent row is no tuples — the target's own not-found
37
- * is the driver's arm, and a parent that no longer exists simply grants
38
- * nothing, as in the cloud; any other store fault propagates so the
39
- * driver folds it to `unavailable`. One source per check (or per list
40
- * call for the list scope): its memo is the check's.
29
+ * The SOURCE joins two records per (object, relation): the IamPolicy rows
30
+ * a check can meet on the object, and the tuples derived from the
31
+ * object's row (loaded once per object through the loader, which a
32
+ * declaration's `derived` rule also reads related rows through). The rows
33
+ * are read ONCE per source through `findByPrincipal`, keyed by the
34
+ * principals a check can reach the person through:
35
+ *
36
+ * - the person's account — every role and every grant made to them;
37
+ * - every team the person holds `member` on — a grant made to
38
+ * `team:<id>#member` names the team, never the person, so it is read
39
+ * by the team's id. The set is complete without recursion: a team's
40
+ * membership is a direct grant only and a team never contains a team,
41
+ * so every team that can admit the person appears among the person's
42
+ * own rows. The rows grant nothing by being read: the evaluator still
43
+ * asks whether the person is the team's member, which the model bounds
44
+ * by the organization (`member: [identity_account] and viewer from
45
+ * organization`), so a person who left the organization reads the
46
+ * team's rows and is admitted by none of them.
47
+ *
48
+ * Open source serves no team (the kind's tier is enterprise), so the
49
+ * person's rows name none and the second read never happens: one read per
50
+ * source, an indexed read on the OSS adapter (its list index by
51
+ * principal) whatever the number of organizations walked or rows held.
52
+ * An edition that composes teams over the built-in
53
+ * evaluator pays one read more per team the person holds. The rows are
54
+ * indexed by (object, relation) once, so a list's thousands of candidates
55
+ * each find theirs without walking the rest.
56
+ *
57
+ * An absent row is no tuples — the target's own not-found is the driver's
58
+ * arm, and a parent that no longer exists simply grants nothing, as in the
59
+ * cloud; any other store fault propagates so the driver folds it to
60
+ * `unavailable`. One source per check (or per list call for the list
61
+ * scope): its memo is the check's.
41
62
  *
42
63
  * A source may be SEEDED with the facts of objects the caller already
43
64
  * holds (the list scope's candidates, decoded by the lane and carried on
@@ -102,6 +123,10 @@ export const ROW_RECORDED_OWNER_KINDS: ReadonlySet<ApiResourceKind> = new Set([
102
123
  ApiResourceKind.organization,
103
124
  ]);
104
125
 
126
+ /** The FGA type a team grant names, and the relation that makes a person one of its members (the model's `team#member`). */
127
+ const TEAM_TYPE = kindEnumName(ApiResourceKind.team);
128
+ const TEAM_MEMBER_RELATION = "member";
129
+
105
130
  function account(id: string): Subject {
106
131
  return { form: "object", object: { type: ACCOUNT_TYPE, id } };
107
132
  }
@@ -209,7 +234,9 @@ export interface DerivedTupleSource extends TupleSource {
209
234
  * The person's own IamPolicy rows as tuples — the ONE read of them this
210
235
  * source makes, shared with `tuplesOf`. A list-shaped consumer (the
211
236
  * organization directory; the list scope) reads its candidate objects
212
- * from here instead of scanning the port a second time.
237
+ * from here instead of scanning the port a second time. The rows granted
238
+ * to the person's teams are not among them: they name the team, and
239
+ * `tuplesOf` serves them where a check meets them.
213
240
  */
214
241
  personTuples(): Promise<ReadonlyArray<Tuple>>;
215
242
  }
@@ -235,6 +262,9 @@ export function newDerivedTupleSource(
235
262
  );
236
263
  }
237
264
  let personRows: Promise<ReadonlyArray<Tuple>> | undefined;
265
+ let reachableRows:
266
+ | Promise<ReadonlyMap<string, ReadonlyArray<Tuple>>>
267
+ | undefined;
238
268
 
239
269
  const loader: RowLoader = {
240
270
  load(object) {
@@ -284,6 +314,38 @@ export function newDerivedTupleSource(
284
314
  return personRows;
285
315
  }
286
316
 
317
+ /**
318
+ * Every row a check can meet, keyed by (object, relation): the person's
319
+ * own, then the rows granted to each team the person holds `member` on
320
+ * (the module header says why that set is complete). A person in no
321
+ * team costs nothing beyond their own rows.
322
+ */
323
+ function reachableTuples(): Promise<
324
+ ReadonlyMap<string, ReadonlyArray<Tuple>>
325
+ > {
326
+ if (reachableRows === undefined) {
327
+ reachableRows = rowTuples().then(async (own) => {
328
+ const teams = new Set<string>();
329
+ for (const tuple of own) {
330
+ if (
331
+ tuple.object.type === TEAM_TYPE &&
332
+ tuple.relation === TEAM_MEMBER_RELATION &&
333
+ tuple.object.id !== ""
334
+ ) {
335
+ teams.add(tuple.object.id);
336
+ }
337
+ }
338
+ const granted = await Promise.all(
339
+ [...teams].map((team) =>
340
+ deps.policies.findByPrincipal(TEAM_TYPE, team),
341
+ ),
342
+ );
343
+ return indexByPair([...own, ...granted.flat().map(tupleOfRow)]);
344
+ });
345
+ }
346
+ return reachableRows;
347
+ }
348
+
287
349
  return {
288
350
  loader,
289
351
  personTuples: rowTuples,
@@ -292,7 +354,8 @@ export function newDerivedTupleSource(
292
354
  tuple.relation === relation &&
293
355
  tuple.object.type === object.type &&
294
356
  tuple.object.id === object.id;
295
- const fromRows = (await rowTuples()).filter(onPair);
357
+ const fromRows =
358
+ (await reachableTuples()).get(pairKey(object, relation)) ?? [];
296
359
  const declaration = model.byType(object.type);
297
360
  if (declaration === undefined) {
298
361
  return fromRows;
@@ -309,7 +372,11 @@ export function newDerivedTupleSource(
309
372
  };
310
373
  }
311
374
 
312
- /** The decoded row for an object, or undefined when the kind is not declared or the row does not exist. */
375
+ /**
376
+ * The decoded row for an object, or undefined when the kind is not
377
+ * declared, is rowless (`platform`, which resolves over tuples alone), or
378
+ * the row does not exist.
379
+ */
313
380
  async function loadRow(
314
381
  deps: Pick<DerivedTupleSourceDeps, "store" | "accounts">,
315
382
  model: Model,
@@ -319,6 +386,7 @@ async function loadRow(
319
386
  const kind = kindByEnumName(object.type);
320
387
  if (
321
388
  declaration === undefined ||
389
+ declaration.schema === undefined ||
322
390
  kind === ApiResourceKind.api_resource_kind_unknown ||
323
391
  object.id === ""
324
392
  ) {
@@ -337,6 +405,27 @@ async function loadRow(
337
405
  }
338
406
  }
339
407
 
408
+ /** The key a tuple is served under: its object and relation. */
409
+ function pairKey(object: ObjectRef, relation: string): string {
410
+ return `${formatObjectRef(object)}#${relation}`;
411
+ }
412
+
413
+ function indexByPair(
414
+ tuples: ReadonlyArray<Tuple>,
415
+ ): ReadonlyMap<string, ReadonlyArray<Tuple>> {
416
+ const index = new Map<string, Tuple[]>();
417
+ for (const tuple of tuples) {
418
+ const key = pairKey(tuple.object, tuple.relation);
419
+ const held = index.get(key);
420
+ if (held === undefined) {
421
+ index.set(key, [tuple]);
422
+ } else {
423
+ held.push(tuple);
424
+ }
425
+ }
426
+ return index;
427
+ }
428
+
340
429
  /** An IamPolicy row as the tuple it records: `resource#relation@principal`. */
341
430
  function tupleOfRow(row: IamPolicy): Tuple {
342
431
  const spec = row.spec;
@@ -30,11 +30,11 @@
30
30
  * it is being resolved is a cycle in the declarations, which is a bug,
31
31
  * and a resolution deeper than MAX_RESOLUTION_DEPTH is the same class of
32
32
  * bug; both throw, because the alternative — a quiet `false` — would turn
33
- * a broken transcript into a lockout nobody can see. A kind the model does
34
- * not declare is a fault as the check's TARGET (the driver refuses
35
- * unserved kinds before evaluation, so reaching here is a registry gap)
36
- * and simply an object with no relations when reached through a tuple
37
- * (`identity_provider#platform_user`, which no open-source tuple names).
33
+ * a broken model into a lockout nobody can see. A kind the model does
34
+ * not declare is a fault as the check's TARGET (the built-in model
35
+ * declares every kind, so reaching here is a registry gap) and simply an
36
+ * object with no relations when reached through a tuple (only a test's
37
+ * throwaway model omits a type).
38
38
  * A relation the kind does not declare is `false` — the ruled answer for
39
39
  * a wire question the model has no line for, where OpenFGA
40
40
  * itself answers a validation error.
@@ -59,14 +59,12 @@ import { ACCOUNT_TYPE, formatObjectRef, pairKey } from "./tuples.js";
59
59
  export const MAX_RESOLUTION_DEPTH = 25;
60
60
 
61
61
  /**
62
- * The faults an evaluation can raise. Two are consumer bugs by the seam
63
- * contracts (a kind the model does not declare; the `internal` class
64
- * offered to the list scope's restrict verb, which the shared helper
65
- * answers before any driver); the rest are the model's own.
62
+ * The faults an evaluation can raise. One is a consumer bug by the seam
63
+ * contracts (a kind the model does not declare, or a rowless one asked
64
+ * for its rows); the rest are the model's own.
66
65
  */
67
66
  export type EvaluationFault =
68
67
  | "undeclared-target-kind"
69
- | "internal-caller-offered"
70
68
  | "resolution-depth-exceeded"
71
69
  | "resolution-cycle";
72
70
 
@@ -40,27 +40,30 @@
40
40
  * returns the org-narrowed rows for the class before any driver is asked
41
41
  * (extensions/list-read-scope.ts, the header's contract line), the
42
42
  * Authorize step's trust-domain rule applied to a list answer. So this
43
- * driver's `restrictListEntries` REFUSES the class (`internal-caller-offered`):
44
- * being offered it means a caller reached the driver around the one
45
- * consumption idiom, and evaluating the server as a person would return
46
- * a quiet short list — the failure shape stigmer#1207 fixed. The
47
- * enumeration verb has no helper in front of it and keeps its own arm:
48
- * every id of the kind, as the built-in directory answers the class with
49
- * every organization. No in-process edge reaches an enumeration lane
50
- * today; the arm is pinned so a future one meets the rule.
43
+ * driver's `restrictListEntries` REFUSES the class with the seam's
44
+ * `InternalCallerOfferedError`: being offered it means a caller reached
45
+ * the driver around the one consumption idiom, and evaluating the server
46
+ * as a person would return a quiet short list — the failure shape
47
+ * stigmer#1207 fixed. The enumeration verb has no helper in front of it
48
+ * and keeps its own arm: every id of the kind, as the built-in directory
49
+ * answers the class with every organization. No in-process edge reaches
50
+ * an enumeration lane today; the arm is pinned so a future one meets the
51
+ * rule.
51
52
  *
52
53
  * Faults THROW and are never an empty answer (the seam: an empty set means
53
54
  * "authorized to see nothing"; an outage is the pipeline's sanitized
54
- * INTERNAL). A kind the model does not declare, like the internal class
55
- * above, is a consumer bug by the seam's contract and faults through the
56
- * evaluator, loud; the model declares exactly the open-source tier, so no
57
- * served lane can reach it.
55
+ * INTERNAL). A kind the model does not declare, or a rowless one
56
+ * (`platform`, which has no stored rows to list), is, like the internal
57
+ * class above, a consumer bug by the seam's contract, and faults through
58
+ * the evaluator's own error, loud; every served list lane lists a kind
59
+ * with rows.
58
60
  *
59
61
  * Logging. One debug line per call — kind, offered, kept, elapsed — what
60
62
  * an operator needs to answer "why is Alice's list short" beside the
61
63
  * Authorizer's per-denial lines; never the token.
62
64
  */
63
65
  import { fromBinary } from "@bufbuild/protobuf";
66
+ import type { DescMessage } from "@bufbuild/protobuf";
64
67
 
65
68
  import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
66
69
 
@@ -71,6 +74,7 @@ import type {
71
74
  ListEntryMeta,
72
75
  ListReadScope,
73
76
  } from "../extensions/list-read-scope.js";
77
+ import { InternalCallerOfferedError } from "../extensions/list-read-scope.js";
74
78
  import { kindEnumName } from "../pipeline/apiresource-meta.js";
75
79
  import type { Store } from "../store/interface.js";
76
80
  import { newDerivedTupleSource } from "./derived-tuples.js";
@@ -87,6 +91,9 @@ import type { ObjectRef, Person } from "./tuples.js";
87
91
  /** The relation every list lane reads through (the cloud's LIST_RELATION). */
88
92
  const LIST_RELATION = "can_view";
89
93
 
94
+ /** A declaration of a kind with stored rows: the only kind a list lane can list. */
95
+ type ListableDeclaration = KindDeclaration & { readonly schema: DescMessage };
96
+
90
97
  export interface BuiltInListReadScopeDeps {
91
98
  readonly store: Store;
92
99
  /** The port the grant path writes through — the person's rows are read from the same instance. */
@@ -153,8 +160,12 @@ export function newBuiltInListReadScope(
153
160
  return kept;
154
161
  }
155
162
 
156
- /** The declaration a verb evaluates against; an undeclared kind is the evaluator's own fault, raised before any read. */
157
- function requireDeclared(kind: ApiResourceKind): KindDeclaration {
163
+ /**
164
+ * The declaration a verb evaluates against, which must be a kind with
165
+ * stored rows. An undeclared kind, or a rowless one, is the evaluator's
166
+ * own fault, raised before any read.
167
+ */
168
+ function requireListable(kind: ApiResourceKind): ListableDeclaration {
158
169
  const declaration = model.byKind(kind);
159
170
  if (declaration === undefined) {
160
171
  throw new AuthorizationEvaluationError(
@@ -162,11 +173,18 @@ export function newBuiltInListReadScope(
162
173
  `no declaration for kind '${kindEnumName(kind)}' — a list lane asked the scope about a kind the model does not declare`,
163
174
  );
164
175
  }
165
- return declaration;
176
+ const schema = declaration.schema;
177
+ if (schema === undefined) {
178
+ throw new AuthorizationEvaluationError(
179
+ "undeclared-target-kind",
180
+ `kind '${kindEnumName(kind)}' has no stored rows — a list lane asked the scope about a rowless kind`,
181
+ );
182
+ }
183
+ return { ...declaration, schema };
166
184
  }
167
185
 
168
186
  /** Every row of `kind` the store holds, as facts; a row that does not decode is skipped. */
169
- async function everyRowOf(declaration: KindDeclaration): Promise<RowFacts[]> {
187
+ async function everyRowOf(declaration: ListableDeclaration): Promise<RowFacts[]> {
170
188
  const kind = declaration.kind;
171
189
  const rows = await deps.store.listResources(kind);
172
190
  const facts: RowFacts[] = [];
@@ -189,12 +207,9 @@ export function newBuiltInListReadScope(
189
207
  kind: ApiResourceKind,
190
208
  entries: ReadonlyArray<ListEntryMeta>,
191
209
  ): Promise<ReadonlySet<string>> {
192
- requireDeclared(kind);
210
+ requireListable(kind);
193
211
  if (caller.callerClass === "internal") {
194
- throw new AuthorizationEvaluationError(
195
- "internal-caller-offered",
196
- `the internal caller class was offered to the list scope for kind '${kindEnumName(kind)}' — the shared helper answers that class before any driver; a list lane reached the driver around it`,
197
- );
212
+ throw new InternalCallerOfferedError(kind);
198
213
  }
199
214
  // One candidate per unique id: a lane offers each row once, and a
200
215
  // duplicate would only spend a walk to learn the same answer.
@@ -211,7 +226,7 @@ export function newBuiltInListReadScope(
211
226
  caller: CallerIdentity,
212
227
  kind: ApiResourceKind,
213
228
  ): Promise<ReadonlySet<string>> {
214
- const candidates = await everyRowOf(requireDeclared(kind));
229
+ const candidates = await everyRowOf(requireListable(kind));
215
230
  if (caller.callerClass === "internal") {
216
231
  return new Set(candidates.map((facts) => facts.id));
217
232
  }