@stigmer/server 3.22.1-dev.20260922095755 → 3.22.1

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 (268) hide show
  1. package/dist/authorization/authorizer.d.ts +8 -4
  2. package/dist/authorization/authorizer.d.ts.map +1 -1
  3. package/dist/authorization/authorizer.js +9 -5
  4. package/dist/authorization/authorizer.js.map +1 -1
  5. package/dist/authorization/derived-tuples.d.ts.map +1 -1
  6. package/dist/authorization/derived-tuples.js +25 -10
  7. package/dist/authorization/derived-tuples.js.map +1 -1
  8. package/dist/authorization/evaluator.d.ts +8 -7
  9. package/dist/authorization/evaluator.d.ts.map +1 -1
  10. package/dist/authorization/evaluator.js +20 -4
  11. package/dist/authorization/evaluator.js.map +1 -1
  12. package/dist/authorization/list-read-scope.d.ts.map +1 -1
  13. package/dist/authorization/list-read-scope.js +9 -5
  14. package/dist/authorization/list-read-scope.js.map +1 -1
  15. package/dist/authorization/model/agent.d.ts.map +1 -1
  16. package/dist/authorization/model/agent.js +5 -4
  17. package/dist/authorization/model/agent.js.map +1 -1
  18. package/dist/authorization/model/agent_instance.d.ts.map +1 -1
  19. package/dist/authorization/model/agent_instance.js +2 -2
  20. package/dist/authorization/model/agent_instance.js.map +1 -1
  21. package/dist/authorization/model/mcp_server.d.ts.map +1 -1
  22. package/dist/authorization/model/mcp_server.js +2 -2
  23. package/dist/authorization/model/mcp_server.js.map +1 -1
  24. package/dist/authorization/model/plugin.d.ts.map +1 -1
  25. package/dist/authorization/model/plugin.js +4 -3
  26. package/dist/authorization/model/plugin.js.map +1 -1
  27. package/dist/authorization/model/rewrite.d.ts +10 -8
  28. package/dist/authorization/model/rewrite.d.ts.map +1 -1
  29. package/dist/authorization/model/rewrite.js +3 -0
  30. package/dist/authorization/model/rewrite.js.map +1 -1
  31. package/dist/authorization/model/skill.d.ts.map +1 -1
  32. package/dist/authorization/model/skill.js +2 -2
  33. package/dist/authorization/model/skill.js.map +1 -1
  34. package/dist/authorization/model/workflow.d.ts.map +1 -1
  35. package/dist/authorization/model/workflow.js +2 -2
  36. package/dist/authorization/model/workflow.js.map +1 -1
  37. package/dist/authorization/model/workflow_instance.d.ts.map +1 -1
  38. package/dist/authorization/model/workflow_instance.js +2 -2
  39. package/dist/authorization/model/workflow_instance.js.map +1 -1
  40. package/dist/authorization/organization-directory.d.ts.map +1 -1
  41. package/dist/authorization/organization-directory.js +3 -2
  42. package/dist/authorization/organization-directory.js.map +1 -1
  43. package/dist/authorization/store-test-kit.d.ts +6 -0
  44. package/dist/authorization/store-test-kit.d.ts.map +1 -1
  45. package/dist/authorization/store-test-kit.js +62 -16
  46. package/dist/authorization/store-test-kit.js.map +1 -1
  47. package/dist/authorization/tuples.d.ts +32 -13
  48. package/dist/authorization/tuples.d.ts.map +1 -1
  49. package/dist/authorization/tuples.js +11 -3
  50. package/dist/authorization/tuples.js.map +1 -1
  51. package/dist/domain/agent/controller.d.ts.map +1 -1
  52. package/dist/domain/agent/controller.js +4 -4
  53. package/dist/domain/agent/controller.js.map +1 -1
  54. package/dist/domain/agentchannel/controller.d.ts.map +1 -1
  55. package/dist/domain/agentchannel/controller.js +1 -3
  56. package/dist/domain/agentchannel/controller.js.map +1 -1
  57. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  58. package/dist/domain/agentexecution/controller.js +1 -3
  59. package/dist/domain/agentexecution/controller.js.map +1 -1
  60. package/dist/domain/agentinstance/controller.d.ts.map +1 -1
  61. package/dist/domain/agentinstance/controller.js +4 -3
  62. package/dist/domain/agentinstance/controller.js.map +1 -1
  63. package/dist/domain/agentshare/constants.d.ts +11 -6
  64. package/dist/domain/agentshare/constants.d.ts.map +1 -1
  65. package/dist/domain/agentshare/constants.js +16 -8
  66. package/dist/domain/agentshare/constants.js.map +1 -1
  67. package/dist/domain/agentshare/controller.d.ts.map +1 -1
  68. package/dist/domain/agentshare/controller.js +1 -3
  69. package/dist/domain/agentshare/controller.js.map +1 -1
  70. package/dist/domain/agentshare/steps.d.ts +25 -18
  71. package/dist/domain/agentshare/steps.d.ts.map +1 -1
  72. package/dist/domain/agentshare/steps.js +142 -41
  73. package/dist/domain/agentshare/steps.js.map +1 -1
  74. package/dist/domain/environment/controller.d.ts.map +1 -1
  75. package/dist/domain/environment/controller.js +3 -2
  76. package/dist/domain/environment/controller.js.map +1 -1
  77. package/dist/domain/executioncontext/controller.d.ts.map +1 -1
  78. package/dist/domain/executioncontext/controller.js +1 -2
  79. package/dist/domain/executioncontext/controller.js.map +1 -1
  80. package/dist/domain/mcpserver/controller.d.ts.map +1 -1
  81. package/dist/domain/mcpserver/controller.js +4 -3
  82. package/dist/domain/mcpserver/controller.js.map +1 -1
  83. package/dist/domain/plugin/controller.d.ts.map +1 -1
  84. package/dist/domain/plugin/controller.js +3 -1
  85. package/dist/domain/plugin/controller.js.map +1 -1
  86. package/dist/domain/plugin/push.d.ts +5 -7
  87. package/dist/domain/plugin/push.d.ts.map +1 -1
  88. package/dist/domain/plugin/push.js +14 -8
  89. package/dist/domain/plugin/push.js.map +1 -1
  90. package/dist/domain/schedule/controller.d.ts.map +1 -1
  91. package/dist/domain/schedule/controller.js +1 -3
  92. package/dist/domain/schedule/controller.js.map +1 -1
  93. package/dist/domain/session/controller.d.ts.map +1 -1
  94. package/dist/domain/session/controller.js +2 -3
  95. package/dist/domain/session/controller.js.map +1 -1
  96. package/dist/domain/skill/controller.d.ts.map +1 -1
  97. package/dist/domain/skill/controller.js +2 -0
  98. package/dist/domain/skill/controller.js.map +1 -1
  99. package/dist/domain/workflow/controller.d.ts.map +1 -1
  100. package/dist/domain/workflow/controller.js +6 -14
  101. package/dist/domain/workflow/controller.js.map +1 -1
  102. package/dist/domain/workflow/registry/data/task-kind-registry.json +1 -1
  103. package/dist/domain/workflow/validation/task-config-constraints.d.ts +1 -10
  104. package/dist/domain/workflow/validation/task-config-constraints.d.ts.map +1 -1
  105. package/dist/domain/workflow/validation/task-config-constraints.js +2 -3
  106. package/dist/domain/workflow/validation/task-config-constraints.js.map +1 -1
  107. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  108. package/dist/domain/workflowexecution/controller.js +2 -4
  109. package/dist/domain/workflowexecution/controller.js.map +1 -1
  110. package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
  111. package/dist/domain/workflowinstance/controller.js +4 -3
  112. package/dist/domain/workflowinstance/controller.js.map +1 -1
  113. package/dist/extensions/authorization-queries.d.ts +5 -6
  114. package/dist/extensions/authorization-queries.d.ts.map +1 -1
  115. package/dist/extensions/list-read-scope.d.ts +1 -1
  116. package/dist/extensions/resource-authorization.d.ts +2 -3
  117. package/dist/extensions/resource-authorization.d.ts.map +1 -1
  118. package/dist/index.d.ts +3 -1
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +13 -0
  121. package/dist/index.js.map +1 -1
  122. package/dist/pipeline/apiresource-meta.d.ts +3 -12
  123. package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
  124. package/dist/pipeline/apiresource-meta.js +10 -21
  125. package/dist/pipeline/apiresource-meta.js.map +1 -1
  126. package/dist/pipeline/steps/authorization-tuples.d.ts +3 -9
  127. package/dist/pipeline/steps/authorization-tuples.d.ts.map +1 -1
  128. package/dist/pipeline/steps/authorization-tuples.js +11 -9
  129. package/dist/pipeline/steps/authorization-tuples.js.map +1 -1
  130. package/dist/pipeline/steps/references.d.ts +20 -178
  131. package/dist/pipeline/steps/references.d.ts.map +1 -1
  132. package/dist/pipeline/steps/references.js +43 -344
  133. package/dist/pipeline/steps/references.js.map +1 -1
  134. package/dist/pipeline/steps/visibility-gates.d.ts +27 -0
  135. package/dist/pipeline/steps/visibility-gates.d.ts.map +1 -0
  136. package/dist/pipeline/steps/visibility-gates.js +139 -0
  137. package/dist/pipeline/steps/visibility-gates.js.map +1 -0
  138. package/dist/query/search/controller.js +1 -0
  139. package/dist/query/search/controller.js.map +1 -1
  140. package/dist/query/search/criteria.d.ts +5 -1
  141. package/dist/query/search/criteria.d.ts.map +1 -1
  142. package/dist/query/search/criteria.js +13 -3
  143. package/dist/query/search/criteria.js.map +1 -1
  144. package/dist/query/search/handler.d.ts +4 -5
  145. package/dist/query/search/handler.d.ts.map +1 -1
  146. package/dist/query/search/handler.js +6 -7
  147. package/dist/query/search/handler.js.map +1 -1
  148. package/dist/query/search/query-store.d.ts.map +1 -1
  149. package/dist/query/search/query-store.js +2 -0
  150. package/dist/query/search/query-store.js.map +1 -1
  151. package/dist/store/interface.d.ts +6 -3
  152. package/dist/store/interface.d.ts.map +1 -1
  153. package/dist/store/interface.js.map +1 -1
  154. package/dist/store/postgres/migrations.d.ts +1 -3
  155. package/dist/store/postgres/migrations.d.ts.map +1 -1
  156. package/dist/store/postgres/migrations.js +1 -35
  157. package/dist/store/postgres/migrations.js.map +1 -1
  158. package/dist/store/postgres/store.d.ts.map +1 -1
  159. package/dist/store/postgres/store.js +9 -4
  160. package/dist/store/postgres/store.js.map +1 -1
  161. package/dist/store/sqlite/migrations.d.ts +1 -3
  162. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  163. package/dist/store/sqlite/migrations.js +1 -36
  164. package/dist/store/sqlite/migrations.js.map +1 -1
  165. package/dist/store/sqlite/store.d.ts.map +1 -1
  166. package/dist/store/sqlite/store.js +8 -4
  167. package/dist/store/sqlite/store.js.map +1 -1
  168. package/package.json +6 -6
  169. package/src/authorization/README.md +26 -24
  170. package/src/authorization/__tests__/authorizer.test.ts +9 -10
  171. package/src/authorization/__tests__/derived-tuples.test.ts +11 -4
  172. package/src/authorization/__tests__/evaluator.test.ts +73 -14
  173. package/src/authorization/__tests__/facts.test.ts +2 -2
  174. package/src/authorization/__tests__/fixtures/fga/agent-instance-creation.fga.yaml +185 -0
  175. package/src/authorization/__tests__/fixtures/fga/blueprint-private-visibility.fga.yaml +98 -6
  176. package/src/authorization/__tests__/fixtures/fga/cross-org-share-creation.fga.yaml +98 -0
  177. package/src/authorization/__tests__/fixtures/fga/default-instance-inheritance.fga.yaml +61 -4
  178. package/src/authorization/__tests__/fixtures/fga/mcp-server-authoring.fga.yaml +143 -0
  179. package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +21 -1
  180. package/src/authorization/__tests__/fixtures/fga/platform-visibility.fga.yaml +25 -1
  181. package/src/authorization/__tests__/fixtures/fga/public-visibility-setter.fga.yaml +52 -0
  182. package/src/authorization/__tests__/fixtures/fga/workflow-execution-sharing.fga.yaml +3 -1
  183. package/src/authorization/__tests__/list-read-scope.test.ts +17 -18
  184. package/src/authorization/__tests__/store-tests.test.ts +8 -1
  185. package/src/authorization/__tests__/tuples.test.ts +11 -12
  186. package/src/authorization/authorizer.ts +9 -4
  187. package/src/authorization/derived-tuples.ts +31 -14
  188. package/src/authorization/evaluator.ts +44 -8
  189. package/src/authorization/list-read-scope.ts +11 -5
  190. package/src/authorization/model/agent.ts +5 -2
  191. package/src/authorization/model/agent_instance.ts +2 -0
  192. package/src/authorization/model/mcp_server.ts +2 -0
  193. package/src/authorization/model/plugin.ts +4 -1
  194. package/src/authorization/model/rewrite.ts +13 -8
  195. package/src/authorization/model/skill.ts +2 -0
  196. package/src/authorization/model/workflow.ts +2 -0
  197. package/src/authorization/model/workflow_instance.ts +2 -0
  198. package/src/authorization/organization-directory.ts +3 -1
  199. package/src/authorization/store-test-kit.ts +75 -16
  200. package/src/authorization/tuples.ts +39 -16
  201. package/src/domain/agent/__tests__/agent.test.ts +4 -11
  202. package/src/domain/agent/controller.ts +7 -7
  203. package/src/domain/agentchannel/__tests__/agentchannel.test.ts +48 -223
  204. package/src/domain/agentchannel/controller.ts +1 -6
  205. package/src/domain/agentexecution/controller.ts +1 -6
  206. package/src/domain/agentinstance/controller.ts +12 -6
  207. package/src/domain/agentshare/__tests__/agentshare.test.ts +172 -196
  208. package/src/domain/agentshare/__tests__/create-authorization.test.ts +31 -16
  209. package/src/domain/agentshare/constants.ts +26 -14
  210. package/src/domain/agentshare/controller.ts +1 -6
  211. package/src/domain/agentshare/steps.ts +194 -41
  212. package/src/domain/environment/controller.ts +8 -5
  213. package/src/domain/executioncontext/controller.ts +1 -5
  214. package/src/domain/mcpserver/__tests__/mcpserver.test.ts +28 -115
  215. package/src/domain/mcpserver/__tests__/oauth-handshake.test.ts +18 -53
  216. package/src/domain/mcpserver/controller.ts +12 -6
  217. package/src/domain/plugin/controller.ts +8 -1
  218. package/src/domain/plugin/push.ts +16 -8
  219. package/src/domain/schedule/controller.ts +1 -6
  220. package/src/domain/session/controller.ts +4 -12
  221. package/src/domain/skill/controller.ts +7 -0
  222. package/src/domain/workflow/controller.ts +10 -28
  223. package/src/domain/workflow/registry/data/task-kind-registry.json +1 -1
  224. package/src/domain/workflow/validation/task-config-constraints.ts +3 -7
  225. package/src/domain/workflowexecution/controller.ts +2 -7
  226. package/src/domain/workflowinstance/controller.ts +12 -6
  227. package/src/extensions/__tests__/built-in-authorization-composed.test.ts +19 -32
  228. package/src/extensions/__tests__/extension-composition.test.ts +4 -4
  229. package/src/extensions/__tests__/list-read-scope-composed.test.ts +6 -9
  230. package/src/extensions/authorization-queries.ts +5 -6
  231. package/src/extensions/list-read-scope.ts +1 -1
  232. package/src/extensions/resource-authorization.ts +5 -3
  233. package/src/index.ts +14 -0
  234. package/src/pipeline/apiresource-meta.ts +10 -21
  235. package/src/pipeline/steps/__tests__/authorization-tuples.test.ts +31 -18
  236. package/src/pipeline/steps/__tests__/visibility-gates.test.ts +187 -0
  237. package/src/pipeline/steps/authorization-tuples.ts +11 -9
  238. package/src/pipeline/steps/references.ts +70 -578
  239. package/src/pipeline/steps/visibility-gates.ts +174 -0
  240. package/src/query/search/__tests__/criteria.test.ts +4 -0
  241. package/src/query/search/__tests__/query-store.test.ts +40 -11
  242. package/src/query/search/controller.ts +1 -0
  243. package/src/query/search/criteria.ts +14 -0
  244. package/src/query/search/handler.ts +7 -6
  245. package/src/query/search/query-store.ts +2 -0
  246. package/src/store/__tests__/store-contract.ts +51 -6
  247. package/src/store/interface.ts +6 -3
  248. package/src/store/postgres/__tests__/migrations.test.ts +6 -187
  249. package/src/store/postgres/__tests__/store-contract.test.ts +2 -0
  250. package/src/store/postgres/migrations.ts +1 -48
  251. package/src/store/postgres/store.ts +11 -4
  252. package/src/store/sqlite/__tests__/migrations.test.ts +5 -191
  253. package/src/store/sqlite/migrations.ts +1 -45
  254. package/src/store/sqlite/store.ts +10 -4
  255. package/dist/domain/workflow/agent-call-references.d.ts +0 -54
  256. package/dist/domain/workflow/agent-call-references.d.ts.map +0 -1
  257. package/dist/domain/workflow/agent-call-references.js +0 -97
  258. package/dist/domain/workflow/agent-call-references.js.map +0 -1
  259. package/dist/store/public-visibility-retired.d.ts +0 -67
  260. package/dist/store/public-visibility-retired.d.ts.map +0 -1
  261. package/dist/store/public-visibility-retired.js +0 -55
  262. package/dist/store/public-visibility-retired.js.map +0 -1
  263. package/src/domain/workflow/__tests__/agent-call-references.test.ts +0 -222
  264. package/src/domain/workflow/agent-call-references.ts +0 -157
  265. package/src/pipeline/__tests__/supports-visibility.test.ts +0 -112
  266. package/src/pipeline/steps/__tests__/references.test.ts +0 -703
  267. package/src/store/__tests__/public-visibility-retired.test.ts +0 -127
  268. package/src/store/public-visibility-retired.ts +0 -119
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * The evaluator — OpenFGA's check, over the built-in model and a tuple
3
3
  * source, small because the model uses four rewrite forms and nothing
4
- * else (model/rewrite.ts). `checkRelation(object, relation, person)`
4
+ * else (model/rewrite.ts). `checkRelation(object, relation, person, context)`
5
5
  * answers "does this person hold this relation on this object" the way
6
6
  * the cloud's engine answers it over stored tuples, so the two editions
7
- * share one meaning of authorization. There is no context parameter: the
8
- * model declares no condition, so a check has no posture to state.
7
+ * share one meaning of authorization.
9
8
  *
10
9
  * The walk, in the order a `.fga` line reads:
11
10
  * - `this`: the tuples on (object, relation). A tuple counts only if
@@ -13,7 +12,9 @@
13
12
  * OpenFGA makes at write time, made here at read time, so a derived
14
13
  * tuple the model no longer admits denies instead of allowing. An
15
14
  * object subject matches when it names one of the person's aliases;
16
- * a userset subject is resolved on its own object.
15
+ * a userset subject is resolved on its own object; the wildcard is
16
+ * honoured only under its condition (`allow_public` reads the
17
+ * context's `allow`; an unknown condition is a fault).
17
18
  * - `computed`: the same object, another relation.
18
19
  * - `from`: every object the tupleset relation links to (again within
19
20
  * the tupleset line's type restriction), resolved for the relation.
@@ -43,8 +44,19 @@
43
44
  */
44
45
  import type { Model } from "./model/index.js";
45
46
  import type { Rewrite, SubjectType } from "./model/rewrite.js";
46
- import type { ObjectRef, Person, Subject, TupleSource } from "./tuples.js";
47
- import { ACCOUNT_TYPE, formatObjectRef, pairKey } from "./tuples.js";
47
+ import type {
48
+ CheckContext,
49
+ ObjectRef,
50
+ Person,
51
+ Subject,
52
+ TupleSource,
53
+ } from "./tuples.js";
54
+ import {
55
+ ACCOUNT_TYPE,
56
+ ALLOW_PUBLIC_CONDITION,
57
+ formatObjectRef,
58
+ pairKey,
59
+ } from "./tuples.js";
48
60
 
49
61
  /**
50
62
  * OpenFGA's default resolution-depth limit (its `resolveNodeLimit`), so
@@ -65,7 +77,8 @@ export type EvaluationFault =
65
77
  | "undeclared-target-kind"
66
78
  | "internal-caller-offered"
67
79
  | "resolution-depth-exceeded"
68
- | "resolution-cycle";
80
+ | "resolution-cycle"
81
+ | "unknown-condition";
69
82
 
70
83
  /** A fault in the model or its evaluation — never a denial; the driver maps it to `unavailable`. */
71
84
  export class AuthorizationEvaluationError extends Error {
@@ -88,6 +101,7 @@ export async function checkRelation(
88
101
  object: ObjectRef,
89
102
  relation: string,
90
103
  person: Person,
104
+ context: CheckContext,
91
105
  ): Promise<boolean> {
92
106
  if (deps.model.byType(object.type) === undefined) {
93
107
  throw new AuthorizationEvaluationError(
@@ -95,7 +109,7 @@ export async function checkRelation(
95
109
  `no declaration for kind '${object.type}' (target ${formatObjectRef(object)})`,
96
110
  );
97
111
  }
98
- return new Walk(deps, person).resolve(object, relation, 0);
112
+ return new Walk(deps, person, context).resolve(object, relation, 0);
99
113
  }
100
114
 
101
115
  /** One check's state: the memo and the in-progress set, over one source. */
@@ -106,6 +120,7 @@ class Walk {
106
120
  constructor(
107
121
  private readonly deps: EvaluationDeps,
108
122
  private readonly person: Person,
123
+ private readonly context: CheckContext,
109
124
  ) {}
110
125
 
111
126
  resolve(
@@ -219,6 +234,15 @@ class Walk {
219
234
  allowed.relation === subject.relation,
220
235
  ) && (await this.resolve(subject.object, subject.relation, depth + 1))
221
236
  );
237
+ case "wildcard": {
238
+ const allowed = restriction.some(
239
+ (entry) =>
240
+ entry.form === "wildcard" &&
241
+ entry.type === subject.type &&
242
+ entry.condition === subject.condition,
243
+ );
244
+ return allowed && this.conditionHolds(subject.condition);
245
+ }
222
246
  default: {
223
247
  const exhaustive: never = subject;
224
248
  throw new Error(`unknown subject form: ${JSON.stringify(exhaustive)}`);
@@ -226,6 +250,18 @@ class Walk {
226
250
  }
227
251
  }
228
252
 
253
+ private conditionHolds(condition: string | undefined): boolean {
254
+ switch (condition) {
255
+ case ALLOW_PUBLIC_CONDITION:
256
+ return this.context.allow;
257
+ default:
258
+ throw new AuthorizationEvaluationError(
259
+ "unknown-condition",
260
+ `the model declares no condition '${String(condition)}'`,
261
+ );
262
+ }
263
+ }
264
+
229
265
  /** The object types the tupleset line admits — `[organization]`, `[session]` — from its `this` nodes. */
230
266
  private objectTypesOf(
231
267
  object: ObjectRef,
@@ -11,10 +11,12 @@
11
11
  * row a second time. A self-host's lists show a person exactly what a
12
12
  * `get` would let them read.
13
13
  *
14
- * Both verbs ask the same question a point check asks, `can_view` on the
15
- * candidate: the model has no subject whose answer differs between a
16
- * listing and a get, so a list is exactly the set of rows a get would
17
- * read.
14
+ * Both verbs ask under the LISTING context, `allow: false` — the public
15
+ * wildcard suppressed, the cloud's own posture (its Java
16
+ * listAuthorizedResourceIds set it): a legacy PUBLIC row reads for any
17
+ * signed-in person on a point check and never expands into an outsider's
18
+ * listing; the organization's own people still reach it through the
19
+ * org-viewer shape public also emits.
18
20
  *
19
21
  * - `restrictListEntries`: one derived source for the caller, seeded
20
22
  * with every candidate's facts; one `checkRelation` per unique id —
@@ -82,11 +84,14 @@ import { builtInModel } from "./model/index.js";
82
84
  import type { KindDeclaration } from "./model/rewrite.js";
83
85
  import type { AccountsByCaller } from "./person.js";
84
86
  import { resolvePerson } from "./person.js";
85
- import type { ObjectRef, Person } from "./tuples.js";
87
+ import type { CheckContext, ObjectRef, Person } from "./tuples.js";
86
88
 
87
89
  /** The relation every list lane reads through (the cloud's LIST_RELATION). */
88
90
  const LIST_RELATION = "can_view";
89
91
 
92
+ /** The listing context: the public wildcard suppressed (the module header). */
93
+ const LISTING: CheckContext = { allow: false };
94
+
90
95
  export interface BuiltInListReadScopeDeps {
91
96
  readonly store: Store;
92
97
  /** The port the grant path writes through — the person's rows are read from the same instance. */
@@ -133,6 +138,7 @@ export function newBuiltInListReadScope(
133
138
  object,
134
139
  LIST_RELATION,
135
140
  person,
141
+ LISTING,
136
142
  );
137
143
  return allowed ? facts.id : undefined;
138
144
  }),
@@ -4,8 +4,9 @@
4
4
  * read by whoever the visibility axis lets in. The `organization#member`
5
5
  * and `organization#viewer` usersets on `viewer` are the org-visibility
6
6
  * tuple's two shapes (the cloud writes `#viewer` since cloud#257; `#member`
7
- * is the legacy shape it still honours), and `platform_viewer` is an
8
- * identity-provider userset no open-source tuple ever names.
7
+ * is the legacy shape it still honours), the conditional wildcard is the
8
+ * public level, and `platform_viewer` is an identity-provider userset no
9
+ * open-source tuple ever names.
9
10
  */
10
11
  import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
11
12
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
@@ -16,6 +17,7 @@ import {
16
17
  direct,
17
18
  from,
18
19
  objectOf,
20
+ publicWith,
19
21
  union,
20
22
  usersetOf,
21
23
  } from "./rewrite.js";
@@ -42,6 +44,7 @@ export const agentDeclaration = declareKind({
42
44
  union(
43
45
  direct(
44
46
  objectOf("identity_account"),
47
+ publicWith("identity_account", "allow_public"),
45
48
  usersetOf("organization", "member"),
46
49
  usersetOf("organization", "viewer"),
47
50
  ),
@@ -19,6 +19,7 @@ import {
19
19
  direct,
20
20
  from,
21
21
  objectOf,
22
+ publicWith,
22
23
  union,
23
24
  usersetOf,
24
25
  } from "./rewrite.js";
@@ -37,6 +38,7 @@ export const agentInstanceDeclaration = declareKind({
37
38
  union(
38
39
  direct(
39
40
  objectOf("identity_account"),
41
+ publicWith("identity_account", "allow_public"),
40
42
  usersetOf("organization", "member"),
41
43
  usersetOf("organization", "viewer"),
42
44
  ),
@@ -12,6 +12,7 @@ import {
12
12
  direct,
13
13
  from,
14
14
  objectOf,
15
+ publicWith,
15
16
  union,
16
17
  usersetOf,
17
18
  } from "./rewrite.js";
@@ -38,6 +39,7 @@ export const mcpServerDeclaration = declareKind({
38
39
  union(
39
40
  direct(
40
41
  objectOf("identity_account"),
42
+ publicWith("identity_account", "allow_public"),
41
43
  usersetOf("organization", "member"),
42
44
  usersetOf("organization", "viewer"),
43
45
  ),
@@ -2,7 +2,8 @@
2
2
  * Transcript of fga/model/agentic/plugin.fga — the unit of install, shaped
3
3
  * exactly like a skill: an organization-owned blueprint whose owner is the
4
4
  * installer (and, through the organization, its admins), whose viewers are
5
- * the org's members and viewers and the platform viewer. Its verbs are the blueprint set: `can_view`, `can_edit`
5
+ * the org's members and viewers, public readers when published, and the
6
+ * platform viewer. Its verbs are the blueprint set: `can_view`, `can_edit`
6
7
  * (visibility), `can_delete` (uninstall), `can_grant_access`,
7
8
  * `can_view_access`; `can_use` is carried as the model's word for
8
9
  * "install from" though no wire permission names it yet.
@@ -20,6 +21,7 @@ import {
20
21
  direct,
21
22
  from,
22
23
  objectOf,
24
+ publicWith,
23
25
  union,
24
26
  usersetOf,
25
27
  } from "./rewrite.js";
@@ -46,6 +48,7 @@ export const pluginDeclaration = declareKind({
46
48
  union(
47
49
  direct(
48
50
  objectOf("identity_account"),
51
+ publicWith("identity_account", "allow_public"),
49
52
  usersetOf("organization", "member"),
50
53
  usersetOf("organization", "viewer"),
51
54
  ),
@@ -6,17 +6,12 @@
6
6
  * define organization: [organization] → direct(objectOf("organization"))
7
7
  * define owner: [identity_account] or admin from organization
8
8
  * → union(direct(objectOf("identity_account")), from("admin", "organization"))
9
- * define viewer: [identity_account, organization#member, organization#viewer]
10
- * or owner or platform_viewer
11
- * → union(direct(objectOf(...), usersetOf(...), usersetOf(...)),
9
+ * define viewer: [identity_account, identity_account:* with allow_public,
10
+ * organization#member, organization#viewer] or owner or platform_viewer
11
+ * → union(direct(objectOf(...), publicWith(...), usersetOf(...), usersetOf(...)),
12
12
  * computed("owner"), computed("platform_viewer"))
13
13
  * define can_view: viewer → computed("viewer")
14
14
  *
15
- * A subject type is an object type or a userset; the model admits no
16
- * wildcard (`type:*`) and declares no condition, so neither has a form
17
- * here. The day a line needs one, the transcript cannot be written and
18
- * the gap is a design act.
19
- *
20
15
  * A declaration file (model/<kind>.ts) is a TRANSCRIPT of its `.fga`
21
16
  * file: the same relations, in the file's order, each line rewritten in
22
17
  * these builders so a reader with the model open sees the same text, and
@@ -58,6 +53,12 @@ export type SubjectType =
58
53
  readonly form: "userset";
59
54
  readonly type: string;
60
55
  readonly relation: string;
56
+ }
57
+ /** `[identity_account:* with allow_public]` — every object of the type, under the condition. */
58
+ | {
59
+ readonly form: "wildcard";
60
+ readonly type: string;
61
+ readonly condition: string;
61
62
  };
62
63
 
63
64
  export type Rewrite =
@@ -86,6 +87,10 @@ export function usersetOf(type: string, relation: string): SubjectType {
86
87
  return { form: "userset", type, relation };
87
88
  }
88
89
 
90
+ export function publicWith(type: string, condition: string): SubjectType {
91
+ return { form: "wildcard", type, condition };
92
+ }
93
+
89
94
  export function direct(...subjects: ReadonlyArray<SubjectType>): Rewrite {
90
95
  return { node: "this", subjects };
91
96
  }
@@ -14,6 +14,7 @@ import {
14
14
  direct,
15
15
  from,
16
16
  objectOf,
17
+ publicWith,
17
18
  union,
18
19
  usersetOf,
19
20
  } from "./rewrite.js";
@@ -40,6 +41,7 @@ export const skillDeclaration = declareKind({
40
41
  union(
41
42
  direct(
42
43
  objectOf("identity_account"),
44
+ publicWith("identity_account", "allow_public"),
43
45
  usersetOf("organization", "member"),
44
46
  usersetOf("organization", "viewer"),
45
47
  ),
@@ -14,6 +14,7 @@ import {
14
14
  direct,
15
15
  from,
16
16
  objectOf,
17
+ publicWith,
17
18
  union,
18
19
  usersetOf,
19
20
  } from "./rewrite.js";
@@ -40,6 +41,7 @@ export const workflowDeclaration = declareKind({
40
41
  union(
41
42
  direct(
42
43
  objectOf("identity_account"),
44
+ publicWith("identity_account", "allow_public"),
43
45
  usersetOf("organization", "member"),
44
46
  usersetOf("organization", "viewer"),
45
47
  ),
@@ -30,6 +30,7 @@ import {
30
30
  direct,
31
31
  from,
32
32
  objectOf,
33
+ publicWith,
33
34
  union,
34
35
  usersetOf,
35
36
  } from "./rewrite.js";
@@ -83,6 +84,7 @@ export const workflowInstanceDeclaration = declareKind({
83
84
  union(
84
85
  direct(
85
86
  objectOf("identity_account"),
87
+ publicWith("identity_account", "allow_public"),
86
88
  usersetOf("organization", "member"),
87
89
  usersetOf("organization", "viewer"),
88
90
  ),
@@ -32,7 +32,8 @@
32
32
  * annotation, which is why this driver meets that caller at all: a
33
33
  * skip lane hands the directory the identity the interceptor stamped,
34
34
  * and no Authorize arm ran before it. Every other caller gets the
35
- * evaluated list.
35
+ * evaluated list. The listing context (`allow: false`) is the cloud's,
36
+ * though no organization line admits a wildcard.
36
37
  * The controller loads each id it is handed and skips the ones whose row
37
38
  * is gone (grants can outlive rows); the source has already loaded the
38
39
  * row once for the evaluation — two primary-key reads per organization,
@@ -101,6 +102,7 @@ export function newBuiltInOrganizationDirectory(
101
102
  { type: ORGANIZATION, id },
102
103
  "can_view",
103
104
  person,
105
+ { allow: false },
104
106
  )
105
107
  ) {
106
108
  visible.push(id);
@@ -41,18 +41,26 @@
41
41
  * `skipped` is therefore complete once every case has run; a consumer
42
42
  * pins it after the cases, which is the order a test file declares them.
43
43
  *
44
- * The parser is strict on purpose: an unknown key anywhere is a thrown
45
- * fault, so a change to the format upstream is loud here rather than
46
- * half-run. The model declares no condition, so the format's `condition`
47
- * on a tuple and `context` on a check are among the keys refused: a
48
- * document that still carries them is written against a model this
49
- * evaluator does not answer, and saying so beats running it half-blind.
44
+ * Context. A check's `context.allow` is the `allow_public` parameter
45
+ * (tuples.ts). A check without a context runs with `allow: false`, the
46
+ * listing posture: OpenFGA itself would fault on a conditional tuple it
47
+ * reached with the parameter missing, so the cloud's documents never do
48
+ * that — their CI would already fail — and the choice cannot change an
49
+ * answer the oracle gives. The parser is strict on purpose: an unknown
50
+ * key anywhere is a thrown fault, so a change to the format upstream is
51
+ * loud here rather than half-run.
50
52
  */
51
53
  import assert from "node:assert/strict";
52
54
 
53
55
  import { checkRelation } from "./evaluator.js";
54
56
  import type { Model } from "./model/index.js";
55
- import type { Person, Subject, Tuple, TupleSource } from "./tuples.js";
57
+ import type {
58
+ CheckContext,
59
+ Person,
60
+ Subject,
61
+ Tuple,
62
+ TupleSource,
63
+ } from "./tuples.js";
56
64
  import {
57
65
  ACCOUNT_TYPE,
58
66
  newInMemoryTupleSource,
@@ -68,17 +76,20 @@ export interface StoreTestTuple {
68
76
  readonly user: string;
69
77
  readonly relation: string;
70
78
  readonly object: string;
79
+ readonly condition?: { readonly name: string };
71
80
  }
72
81
 
73
82
  export interface StoreTestCheck {
74
83
  readonly user: string;
75
84
  readonly object: string;
85
+ readonly context?: CheckContext;
76
86
  readonly assertions: Readonly<Record<string, boolean>>;
77
87
  }
78
88
 
79
89
  export interface StoreTestListObjects {
80
90
  readonly user: string;
81
91
  readonly type: string;
92
+ readonly context?: CheckContext;
82
93
  readonly assertions: Readonly<Record<string, ReadonlyArray<string>>>;
83
94
  }
84
95
 
@@ -121,12 +132,28 @@ export function parseStoreTestDocument(
121
132
  }
122
133
 
123
134
  function parseTuple(value: unknown, where: string): StoreTestTuple {
124
- const entry = record(value, where, ["user", "relation", "object"]);
125
- return {
135
+ const entry = record(value, where, [
136
+ "user",
137
+ "relation",
138
+ "object",
139
+ "condition",
140
+ ]);
141
+ const tuple: StoreTestTuple = {
126
142
  user: text(entry["user"], `${where}.user`),
127
143
  relation: text(entry["relation"], `${where}.relation`),
128
144
  object: text(entry["object"], `${where}.object`),
129
145
  };
146
+ if (entry["condition"] === undefined) {
147
+ return tuple;
148
+ }
149
+ const condition = record(entry["condition"], `${where}.condition`, [
150
+ "name",
151
+ "context",
152
+ ]);
153
+ return {
154
+ ...tuple,
155
+ condition: { name: text(condition["name"], `${where}.condition.name`) },
156
+ };
130
157
  }
131
158
 
132
159
  function parseTest(value: unknown, where: string): StoreTest {
@@ -146,7 +173,12 @@ function parseTest(value: unknown, where: string): StoreTest {
146
173
  }
147
174
 
148
175
  function parseCheck(value: unknown, where: string): StoreTestCheck {
149
- const entry = record(value, where, ["user", "object", "assertions"]);
176
+ const entry = record(value, where, [
177
+ "user",
178
+ "object",
179
+ "context",
180
+ "assertions",
181
+ ]);
150
182
  const assertions = record(entry["assertions"], `${where}.assertions`);
151
183
  const parsed: Record<string, boolean> = {};
152
184
  for (const [relation, expected] of Object.entries(assertions)) {
@@ -155,15 +187,17 @@ function parseCheck(value: unknown, where: string): StoreTestCheck {
155
187
  }
156
188
  parsed[relation] = expected;
157
189
  }
190
+ const context = parseContext(entry["context"], `${where}.context`);
158
191
  return {
159
192
  user: text(entry["user"], `${where}.user`),
160
193
  object: text(entry["object"], `${where}.object`),
194
+ ...(context === undefined ? {} : { context }),
161
195
  assertions: parsed,
162
196
  };
163
197
  }
164
198
 
165
199
  function parseListObjects(value: unknown, where: string): StoreTestListObjects {
166
- const entry = record(value, where, ["user", "type", "assertions"]);
200
+ const entry = record(value, where, ["user", "type", "context", "assertions"]);
167
201
  const assertions = record(entry["assertions"], `${where}.assertions`);
168
202
  const parsed: Record<string, ReadonlyArray<string>> = {};
169
203
  for (const [relation, expected] of Object.entries(assertions)) {
@@ -171,13 +205,26 @@ function parseListObjects(value: unknown, where: string): StoreTestListObjects {
171
205
  (item, index) => text(item, `${where}.assertions.${relation}[${index}]`),
172
206
  );
173
207
  }
208
+ const context = parseContext(entry["context"], `${where}.context`);
174
209
  return {
175
210
  user: text(entry["user"], `${where}.user`),
176
211
  type: text(entry["type"], `${where}.type`),
212
+ ...(context === undefined ? {} : { context }),
177
213
  assertions: parsed,
178
214
  };
179
215
  }
180
216
 
217
+ function parseContext(value: unknown, where: string): CheckContext | undefined {
218
+ if (value === undefined) {
219
+ return undefined;
220
+ }
221
+ const entry = record(value, where, ["allow"]);
222
+ if (typeof entry["allow"] !== "boolean") {
223
+ throw new Error(`${where}.allow: expected a boolean`);
224
+ }
225
+ return { allow: entry["allow"] };
226
+ }
227
+
181
228
  function record(
182
229
  value: unknown,
183
230
  where: string,
@@ -250,6 +297,9 @@ export interface StoreTestKit {
250
297
  readonly skipped: ReadonlyArray<StoreTestSkip>;
251
298
  }
252
299
 
300
+ /** The listing posture, for a check the document leaves without a context (the module header). */
301
+ const CONTEXT_WHEN_ABSENT: CheckContext = { allow: false };
302
+
253
303
  /**
254
304
  * The document's tests as runnable cases over `model`, plus every
255
305
  * assertion group the kit does not run.
@@ -296,6 +346,7 @@ export function storeTestCases(
296
346
  for (const item of runnable) {
297
347
  const object = parseObjectRef(item.object);
298
348
  const person = personOf(item.user);
349
+ const context = item.context ?? CONTEXT_WHEN_ABSENT;
299
350
  for (const [relation, expected] of Object.entries(item.assertions)) {
300
351
  const recording = recordUndeclaredTypes(source, model);
301
352
  const actual = await checkRelation(
@@ -303,6 +354,7 @@ export function storeTestCases(
303
354
  object,
304
355
  relation,
305
356
  person,
357
+ context,
306
358
  );
307
359
  if (recording.reached.size > 0) {
308
360
  skipped.push({
@@ -319,7 +371,7 @@ export function storeTestCases(
319
371
  assert.equal(
320
372
  actual,
321
373
  expected,
322
- `${document.name} :: ${test.name}: ${item.user} ${relation} ${item.object} expected ${String(expected)}, got ${String(actual)}`,
374
+ `${document.name} :: ${test.name}: ${item.user} ${relation} ${item.object} (allow: ${String(context.allow)}) expected ${String(expected)}, got ${String(actual)}`,
323
375
  );
324
376
  }
325
377
  }
@@ -337,9 +389,10 @@ interface RecordingTupleSource extends TupleSource {
337
389
  /**
338
390
  * A view of `inner` that records, for one walk, every subject type the
339
391
  * evaluator read that `model` does not declare. A person
340
- * (`identity_account:<id>`) is a subject the evaluator compares, never a
341
- * type it resolves, so it is not a "reach" whatever the model declares; a
342
- * userset or a parent object of an undeclared type is.
392
+ * (`identity_account:<id>`, the wildcard) is a subject the evaluator
393
+ * compares, never a type it resolves, so it is not a "reach" whatever
394
+ * the model declares; a userset or a parent object of an undeclared
395
+ * type is.
343
396
  */
344
397
  function recordUndeclaredTypes(
345
398
  inner: TupleSource,
@@ -370,6 +423,8 @@ function resolvedTypeOf(subject: Subject): string | undefined {
370
423
  : subject.object.type;
371
424
  case "userset":
372
425
  return subject.object.type;
426
+ case "wildcard":
427
+ return undefined;
373
428
  default: {
374
429
  const exhaustive: never = subject;
375
430
  throw new Error(`unknown subject form: ${JSON.stringify(exhaustive)}`);
@@ -378,10 +433,14 @@ function resolvedTypeOf(subject: Subject): string | undefined {
378
433
  }
379
434
 
380
435
  function tupleOf(entry: StoreTestTuple): Tuple {
436
+ const subject = parseSubject(entry.user);
381
437
  return {
382
438
  object: parseObjectRef(entry.object),
383
439
  relation: entry.relation,
384
- subject: parseSubject(entry.user),
440
+ subject:
441
+ subject.form === "wildcard" && entry.condition !== undefined
442
+ ? { ...subject, condition: entry.condition.name }
443
+ : subject,
385
444
  };
386
445
  }
387
446