@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
@@ -15,6 +15,11 @@
15
15
  * parent), which is what lets the parity pin run over all of them.
16
16
  * `spec` and `status` overrides sit on top for the arms that need a
17
17
  * pointer or a level set.
18
+ *
19
+ * A fixture row exists only for a kind this edition stores rows of: every
20
+ * type of the model but a rowless one (`platform`, model/bindings.ts), so
21
+ * the builders take a StoredKindDeclaration and `storedDeclaration`
22
+ * refuses a rowless type by name.
18
23
  */
19
24
  import { create } from "@bufbuild/protobuf";
20
25
  import type { DescMessage, Message } from "@bufbuild/protobuf";
@@ -26,6 +31,7 @@ import {
26
31
  getKindMeta,
27
32
  kindEnumName,
28
33
  } from "../../pipeline/apiresource-meta.js";
34
+ import { builtInModel } from "../model/index.js";
29
35
  import type { KindDeclaration } from "../model/rewrite.js";
30
36
 
31
37
  export interface FixtureRowFacts {
@@ -39,6 +45,28 @@ export interface FixtureRowFacts {
39
45
  readonly status?: Readonly<Record<string, unknown>>;
40
46
  }
41
47
 
48
+ /** A declaration of a kind with stored rows: what a fixture row is built, saved and decoded with. */
49
+ export type StoredKindDeclaration = KindDeclaration & { readonly schema: DescMessage };
50
+
51
+ /** Whether rows of the declaration's kind are stored: every type but a rowless one. */
52
+ export function hasStoredRows(
53
+ declaration: KindDeclaration,
54
+ ): declaration is StoredKindDeclaration {
55
+ return declaration.schema !== undefined;
56
+ }
57
+
58
+ /** The built-in model's declaration of `type`, which must be a kind with stored rows. */
59
+ export function storedDeclaration(type: string): StoredKindDeclaration {
60
+ const declaration = builtInModel.byType(type);
61
+ if (declaration === undefined) {
62
+ throw new Error(`${type} is not a type of the model`);
63
+ }
64
+ if (!hasStoredRows(declaration)) {
65
+ throw new Error(`${type} is rowless: no row of it is stored`);
66
+ }
67
+ return declaration;
68
+ }
69
+
42
70
  /** The id the fixture gives the parent of `parentKind`: `<type>-1`. */
43
71
  export function fixtureParentId(parentKind: string): string {
44
72
  return `${kindEnumName(getKindEnum(parentKind))}-1`;
@@ -46,7 +74,7 @@ export function fixtureParentId(parentKind: string): string {
46
74
 
47
75
  /** A row of `declaration.kind` carrying exactly the given facts, its parents filled. */
48
76
  export function fixtureRow(
49
- declaration: KindDeclaration,
77
+ declaration: StoredKindDeclaration,
50
78
  facts: FixtureRowFacts,
51
79
  ): Message {
52
80
  // Typed loosely on purpose: the schema is any declared kind's, and
@@ -70,7 +98,7 @@ export function fixtureRow(
70
98
  }
71
99
 
72
100
  /** `kind_meta`'s parent spec fields for the kind, by generated name, each naming `<parentKind>-1`. */
73
- function parentFieldsOf(declaration: KindDeclaration): Record<string, string> {
101
+ function parentFieldsOf(declaration: StoredKindDeclaration): Record<string, string> {
74
102
  const config = getKindMeta(declaration.kind).authorization;
75
103
  const spec = specSchemaOf(declaration.schema);
76
104
  const fields: Record<string, string> = {};
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Builders for tests' throwaway models: a declaration written by hand in
3
+ * the evaluator's vocabulary (model/rewrite.ts), for the shapes the one
4
+ * real model must never hold (a cycle, a chain past the depth bound) and
5
+ * for proving a mechanism over a model small enough to read in the test.
6
+ * The built-in model is never written this way: it is the compiled
7
+ * OpenFGA JSON, read by model/openfga-json.ts.
8
+ *
9
+ * Nothing here validates: a test that declares a cycle means to. The
10
+ * builders are named so a declaration reads like the `.fga` line it
11
+ * stands for:
12
+ *
13
+ * define viewer: [identity_account, organization#viewer] or owner
14
+ * → union(direct(objectOf("identity_account"), usersetOf("organization", "viewer")), computed("owner"))
15
+ */
16
+ import type { DescMessage } from "@bufbuild/protobuf";
17
+
18
+ import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
19
+
20
+ import { kindEnumName } from "../../pipeline/apiresource-meta.js";
21
+ import type {
22
+ DerivedRelation,
23
+ KindDeclaration,
24
+ Rewrite,
25
+ SubjectType,
26
+ } from "../model/rewrite.js";
27
+
28
+ export function objectOf(type: string): SubjectType {
29
+ return { form: "object", type };
30
+ }
31
+
32
+ export function usersetOf(type: string, relation: string): SubjectType {
33
+ return { form: "userset", type, relation };
34
+ }
35
+
36
+ export function direct(...subjects: ReadonlyArray<SubjectType>): Rewrite {
37
+ return { node: "this", subjects };
38
+ }
39
+
40
+ export function computed(relation: string): Rewrite {
41
+ return { node: "computed", relation };
42
+ }
43
+
44
+ export function from(relation: string, tupleset: string): Rewrite {
45
+ return { node: "from", relation, tupleset };
46
+ }
47
+
48
+ export function union(...members: ReadonlyArray<Rewrite>): Rewrite {
49
+ return { node: "union", members };
50
+ }
51
+
52
+ export function intersection(...members: ReadonlyArray<Rewrite>): Rewrite {
53
+ return { node: "intersection", members };
54
+ }
55
+
56
+ export interface ThrowawayDeclarationInput {
57
+ readonly kind: ApiResourceKind;
58
+ /** Omitted for a rowless type. */
59
+ readonly schema?: DescMessage;
60
+ readonly source?: string;
61
+ readonly relations: ReadonlyArray<readonly [name: string, rewrite: Rewrite]>;
62
+ readonly derived?: ReadonlyArray<readonly [name: string, rule: DerivedRelation]>;
63
+ }
64
+
65
+ /** A declaration exactly as written, for `newModel`. */
66
+ export function throwawayDeclaration(input: ThrowawayDeclarationInput): KindDeclaration {
67
+ return {
68
+ kind: input.kind,
69
+ type: kindEnumName(input.kind),
70
+ schema: input.schema,
71
+ source: input.source ?? "throwaway",
72
+ relations: new Map(input.relations),
73
+ derived: new Map(input.derived ?? []),
74
+ };
75
+ }
@@ -1,83 +1,136 @@
1
1
  /**
2
- * Pins that every question the wire can ask this edition's authorizer
3
- * has a line in the model: for every `rpc.config`-annotated method the
4
- * open-source server SERVES whose annotation names a static kind this
5
- * edition serves, the permission is a relation that kind's transcript
6
- * declares. A relation the kind does not define answers `deny` here
7
- * (evaluator.ts) — an RPC annotated with such a pair would refuse every
8
- * caller, green on the permissive `local*` targets and dark only under
9
- * enforcement, so the gap is caught at the registry instead of on a
10
- * self-host.
2
+ * Pins the contract to the model over the whole wire: every question an
3
+ * RPC annotation asks the authorization model is a relation the model
4
+ * defines. The model (fga/model) and the contract (apis/, reaching this
5
+ * package as `@stigmer/protos`) are both this repository's, so their
6
+ * agreement is this repository's to keep, whichever edition serves the
7
+ * RPC. That is why the scope here is every service in `@stigmer/protos`,
8
+ * where authorize-annotation-completeness.test.ts scopes itself to the
9
+ * methods open source serves: that test governs the Authorize step, and a
10
+ * composition's own services are its own conformance suite's business;
11
+ * this one asks whether the model and the contract say the same thing.
11
12
  *
12
- * Scope follows authorize-annotation-completeness.test.ts: the empty
13
- * composition's routes, replayed into a recorder, are exactly the methods
14
- * the OSS Authorize step governs. Outside the pin, each for a stated
15
- * reason: `is_public` and `is_skip_authorization` methods (the annotation
16
- * is never resolved); a kind the edition does not serve (`platform`; the
17
- * driver refuses it before any evaluation, so under the built-in posture
18
- * nobody holds a platform permission); a kind named by
19
- * `resource_kind_path` (the IamPolicy lane's — the kind is the request's,
20
- * and the target's own declaration answers at run time).
13
+ * Two invariants:
14
+ * - every `rpc.config` annotation that names a static kind asks a
15
+ * permission that kind's type defines. A relation the type does not
16
+ * define answers `deny` in the built-in evaluator and a validation
17
+ * error in OpenFGA, so such an RPC refuses every caller, dark until
18
+ * someone runs it under enforcement;
19
+ * - the permission of every `resource_kind_path` lane (the IamPolicy
20
+ * lanes, whose kind is the request's) is defined on every kind whose
21
+ * `kind_meta` lists grantable roles, because each such kind is one the
22
+ * grant path admits.
23
+ *
24
+ * Outside the pin, as in the annotation-completeness precedent: `is_public`
25
+ * and `is_skip_authorization` methods (their annotation is never
26
+ * resolved), and a `config` that names no kind (`IamPolicyQueryController.get`,
27
+ * which authorizes the loaded policy's resource).
28
+ *
29
+ * What does not hold today is pinned, never dropped: `KNOWN_GAPS` lists
30
+ * each disagreement, and the test requires the computed list to equal it,
31
+ * so a fix to the model or the contract fails here until its line is
32
+ * removed.
33
+ *
34
+ * The walk reads the stubs' built `dist`. `make test-server` builds them
35
+ * first; a bare `vitest` run in a checkout whose `dist` is stale reads
36
+ * services the contract no longer has.
21
37
  */
22
- import { mkdtempSync, rmSync } from "node:fs";
23
- import { tmpdir } from "node:os";
38
+ import { readdirSync } from "node:fs";
39
+ import { createRequire } from "node:module";
24
40
  import path from "node:path";
25
41
 
42
+ import type { DescService } from "@bufbuild/protobuf";
26
43
  import { getOption, hasOption } from "@bufbuild/protobuf";
27
- import { afterAll, beforeAll, describe, expect, it } from "vitest";
44
+ import { beforeAll, describe, expect, it } from "vitest";
28
45
 
29
- import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
46
+ import {
47
+ ApiResourceKind,
48
+ ApiResourceKindSchema,
49
+ } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
30
50
  import {
31
51
  config as rpcAuthorizationConfig,
32
52
  is_public,
33
53
  is_skip_authorization,
34
54
  } from "@stigmer/protos/ai/stigmer/commons/rpc/method_options_pb";
35
55
  import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
36
- import { ServerEdition } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
37
56
 
38
- import { loadConfig } from "../../boot/config.js";
39
- import { composeServer } from "../../boot/compose.js";
40
- import type { ComposedServer } from "../../boot/compose.js";
41
- import {
42
- baseConfig,
43
- servedServices,
44
- silentLogger,
45
- } from "../../extensions/__tests__/composed-support.js";
46
- import {
47
- kindEnumName,
48
- kindServedByEdition,
49
- } from "../../pipeline/apiresource-meta.js";
50
- import { declarationFor } from "../model/index.js";
57
+ import { grantableRolesFor, kindEnumName } from "../../pipeline/apiresource-meta.js";
58
+ import { builtInModel } from "../model/index.js";
59
+
60
+ /**
61
+ * Today's disagreements between the contract and the model, each with its
62
+ * reason. Removing a gap from the model or the contract fails the test
63
+ * until its line is removed here.
64
+ */
65
+ const KNOWN_GAPS: ReadonlyArray<string> = [
66
+ // Contract ahead of model, on purpose: Plan's proto says the relation
67
+ // lands in the model with the entry that serves the kind
68
+ // (apis/ai/stigmer/billing/plan/v1/command.proto), and no edition serves
69
+ // Plan yet.
70
+ "ai.stigmer.billing.plan.v1.PlanCommandController/create asks can_manage_plans on platform",
71
+ "ai.stigmer.billing.plan.v1.PlanCommandController/retire asks can_manage_plans on platform",
72
+ // Artifacts are grantable in kind_meta, but the model defines no access
73
+ // relations on them: https://github.com/stigmer/stigmer/issues/1268.
74
+ "the resource_kind_path lanes ask can_grant_access on artifact",
75
+ "the resource_kind_path lanes ask can_view_access on artifact",
76
+ ];
51
77
 
52
- /** One annotated, served, statically-targeted method: the question the wire asks. */
78
+ /** One annotated method: the question it asks the model. */
53
79
  interface WireQuestion {
54
80
  readonly method: string;
81
+ /** The static kind, or unknown when the kind is the request's (`kindPath`). */
55
82
  readonly kind: ApiResourceKind;
83
+ readonly kindPath: string;
56
84
  readonly permission: string;
57
85
  }
58
86
 
59
- describe("every static (kind, permission) the served RPCs ask about is a declared relation", () => {
60
- let dir: string;
61
- let server: ComposedServer;
87
+ /** Every service descriptor the stubs package exports, loaded through its own specifiers. */
88
+ async function everyService(): Promise<ReadonlyArray<DescService>> {
89
+ const require = createRequire(import.meta.url);
90
+ const anchor = "ai/stigmer/iam/v1/enum_pb";
91
+ const resolved = require.resolve(`@stigmer/protos/${anchor}`);
92
+ const dist = resolved.slice(0, resolved.length - `${anchor}.js`.length);
93
+ const modules = readdirSync(path.join(dist, "ai"), { recursive: true, encoding: "utf8" })
94
+ .filter((file) => file.endsWith("_pb.js"))
95
+ .map((file) => `ai/${file.slice(0, -".js".length).split(path.sep).join("/")}`)
96
+ .sort();
97
+ const services: DescService[] = [];
98
+ for (const specifier of modules) {
99
+ const loaded: Record<string, unknown> = await import(`@stigmer/protos/${specifier}`);
100
+ for (const value of Object.values(loaded)) {
101
+ if (isService(value)) {
102
+ services.push(value);
103
+ }
104
+ }
105
+ }
106
+ return services;
107
+ }
62
108
 
63
- beforeAll(async () => {
64
- dir = mkdtempSync(path.join(tmpdir(), "wire-permissions-test-"));
65
- server = await composeServer({
66
- config: loadConfig(baseConfig(dir)),
67
- logger: silentLogger,
68
- portOverride: 0,
69
- host: "127.0.0.1",
70
- });
71
- });
109
+ function isService(value: unknown): value is DescService {
110
+ return (
111
+ typeof value === "object" &&
112
+ value !== null &&
113
+ "kind" in value &&
114
+ value.kind === "service" &&
115
+ "methods" in value
116
+ );
117
+ }
72
118
 
73
- afterAll(async () => {
74
- await server.shutdown();
75
- rmSync(dir, { recursive: true, force: true });
119
+ /** The model's relations on a kind, or none when the model does not define it. */
120
+ function relationsOf(kind: ApiResourceKind): ReadonlySet<string> {
121
+ return new Set(builtInModel.byKind(kind)?.relations.keys() ?? []);
122
+ }
123
+
124
+ describe("the contract asks the model only what the model defines", () => {
125
+ let services: ReadonlyArray<DescService>;
126
+
127
+ beforeAll(async () => {
128
+ services = await everyService();
76
129
  });
77
130
 
78
- function wireQuestions(): WireQuestion[] {
79
- const questions: WireQuestion[] = [];
80
- for (const service of servedServices(server.routes)) {
131
+ function annotated(): ReadonlyArray<WireQuestion> {
132
+ const out: WireQuestion[] = [];
133
+ for (const service of services) {
81
134
  for (const method of service.methods) {
82
135
  if (
83
136
  !hasOption(method, rpcAuthorizationConfig) ||
@@ -87,68 +140,49 @@ describe("every static (kind, permission) the served RPCs ask about is a declare
87
140
  continue;
88
141
  }
89
142
  const config = getOption(method, rpcAuthorizationConfig);
90
- if (
91
- config.resourceKind === ApiResourceKind.api_resource_kind_unknown ||
92
- !kindServedByEdition(config.resourceKind, ServerEdition.oss)
93
- ) {
94
- continue;
95
- }
96
- questions.push({
143
+ out.push({
97
144
  method: `${service.typeName}/${method.name}`,
98
145
  kind: config.resourceKind,
146
+ kindPath: config.resourceKindPath,
99
147
  permission: IamPermission[config.permission] ?? "",
100
148
  });
101
149
  }
102
150
  }
103
- return questions;
151
+ return out;
104
152
  }
105
153
 
106
- it("names a declared relation for every question, with none undeclared", () => {
107
- const questions = wireQuestions();
108
- expect(questions.length).toBeGreaterThan(60);
109
- const undeclared = questions
110
- .filter(
111
- (q) => declarationFor(q.kind)?.relations.has(q.permission) !== true,
112
- )
113
- .map(
114
- (q) =>
115
- `${q.method} asks ${q.permission} on ${kindEnumName(q.kind)}, which its transcript does not declare`,
116
- );
117
- expect(undeclared).toEqual([]);
154
+ it("reads the whole contract, the IamPolicy lanes included", () => {
155
+ const questions = annotated();
156
+ expect(services.length).toBeGreaterThan(60);
157
+ expect(questions.length).toBeGreaterThan(150);
158
+ expect(questions.some((q) => q.kindPath !== "")).toBe(true);
118
159
  });
119
160
 
120
- it("asks about every served kind the model declares except the one no RPC targets statically", () => {
121
- // iam_policy's RPCs target the policy's RESOURCE (`resource_kind_path`
122
- // or the load-then-authorize `get`). Every other declared kind is asked
123
- // about by at least one static annotation — execution_context through
124
- // its `get` and `delete`; its runner-token lane stays a skip.
125
- const asked = new Set(wireQuestions().map((q) => kindEnumName(q.kind)));
126
- expect([...asked].sort()).toEqual(
127
- [
128
- "agent",
129
- "agent_channel",
130
- "agent_execution",
131
- "agent_instance",
132
- "agent_share",
133
- "api_key",
134
- "artifact",
135
- "channel_app",
136
- "environment",
137
- "execution_context",
138
- "identity_account",
139
- "mcp_server",
140
- "memory",
141
- "oauth_app",
142
- "organization",
143
- "platform_client",
144
- "plugin",
145
- "schedule",
146
- "session",
147
- "skill",
148
- "workflow",
149
- "workflow_execution",
150
- "workflow_instance",
151
- ].sort(),
161
+ it("defines every relation the contract asks about, except the known gaps", () => {
162
+ const questions = annotated();
163
+ const gaps: string[] = [];
164
+
165
+ for (const q of questions) {
166
+ if (q.kind !== ApiResourceKind.api_resource_kind_unknown && !relationsOf(q.kind).has(q.permission)) {
167
+ gaps.push(`${q.method} asks ${q.permission} on ${kindEnumName(q.kind)}`);
168
+ }
169
+ }
170
+
171
+ const pathPermissions = new Set(
172
+ questions.filter((q) => q.kindPath !== "").map((q) => q.permission),
152
173
  );
174
+ for (const value of ApiResourceKindSchema.values) {
175
+ const kind = value.number as ApiResourceKind;
176
+ if (kind === ApiResourceKind.api_resource_kind_unknown || grantableRolesFor(kind).length === 0) {
177
+ continue;
178
+ }
179
+ for (const permission of [...pathPermissions].sort()) {
180
+ if (!relationsOf(kind).has(permission)) {
181
+ gaps.push(`the resource_kind_path lanes ask ${permission} on ${kindEnumName(kind)}`);
182
+ }
183
+ }
184
+ }
185
+
186
+ expect(gaps.sort()).toEqual([...KNOWN_GAPS].sort());
153
187
  });
154
188
  });
@@ -5,8 +5,8 @@
5
5
  * tuples this row would have had, derived on the fly (evaluator.ts over
6
6
  * derived-tuples.ts). Every arm below mirrors the cloud's driver
7
7
  * (stigmer-cloud src/authorizer/fga-authorizer.ts) by name and copy, so
8
- * the two editions answer one question the same way and the drift test
9
- * can pin the arms and not only the model.
8
+ * the two editions answer one question the same way, over the one model
9
+ * both read (model/index.ts).
10
10
  *
11
11
  * The arms, in order:
12
12
  * 1. The cloud's four pre-checks with the Java service's copy, each a
@@ -47,8 +47,10 @@
47
47
  *
48
48
  * Cost, per check (measured at slice 4): the target row (1), the caller's
49
49
  * account (1 primary-key read; 2 for a caller a composition verifier left
50
- * idp-shaped), the person's IamPolicy rows (1 scan of the port, once per
51
- * source), and one row per parent hop the model walks.
50
+ * idp-shaped), the person's IamPolicy rows (1 read of the port, once per
51
+ * source, indexed by principal on the OSS adapter; one more per team the
52
+ * person holds, which open source never has — derived-tuples.ts), and one
53
+ * row per parent hop the model walks.
52
54
  */
53
55
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
54
56
  import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
@@ -85,7 +87,7 @@ import { formatObjectRef } from "./tuples.js";
85
87
  * their rows live in its own tables, out of its probe's reach; matching
86
88
  * the set here keeps the wire identical across editions (an unknown
87
89
  * platform client's id is PERMISSION_DENIED with the annotation's copy in
88
- * both). The cloud's model drift test pins every kind here inside its
90
+ * both). The cloud pins every kind here inside its own
89
91
  * `PROBE_EXEMPT_KINDS`, which is why this set is exported; a kind that
90
92
  * moves into open source joins this set in the same change.
91
93
  *