@stigmer/server 3.15.2 → 3.15.3-dev.20260914121148

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 (217) hide show
  1. package/dist/boot/compose.d.ts.map +1 -1
  2. package/dist/boot/compose.js +103 -9
  3. package/dist/boot/compose.js.map +1 -1
  4. package/dist/domain/iampolicy/access-lists.d.ts +27 -0
  5. package/dist/domain/iampolicy/access-lists.d.ts.map +1 -0
  6. package/dist/domain/iampolicy/access-lists.js +124 -0
  7. package/dist/domain/iampolicy/access-lists.js.map +1 -0
  8. package/dist/domain/iampolicy/constants.d.ts +135 -0
  9. package/dist/domain/iampolicy/constants.d.ts.map +1 -0
  10. package/dist/domain/iampolicy/constants.js +203 -0
  11. package/dist/domain/iampolicy/constants.js.map +1 -0
  12. package/dist/domain/iampolicy/controller.d.ts +29 -0
  13. package/dist/domain/iampolicy/controller.d.ts.map +1 -0
  14. package/dist/domain/iampolicy/controller.js +451 -0
  15. package/dist/domain/iampolicy/controller.js.map +1 -0
  16. package/dist/domain/iampolicy/display-resolver.d.ts +6 -0
  17. package/dist/domain/iampolicy/display-resolver.d.ts.map +1 -0
  18. package/dist/domain/iampolicy/display-resolver.js +90 -0
  19. package/dist/domain/iampolicy/display-resolver.js.map +1 -0
  20. package/dist/domain/iampolicy/grant-path.d.ts +42 -0
  21. package/dist/domain/iampolicy/grant-path.d.ts.map +1 -0
  22. package/dist/domain/iampolicy/grant-path.js +183 -0
  23. package/dist/domain/iampolicy/grant-path.js.map +1 -0
  24. package/dist/domain/iampolicy/grant-scope.d.ts +4 -0
  25. package/dist/domain/iampolicy/grant-scope.d.ts.map +1 -0
  26. package/dist/domain/iampolicy/grant-scope.js +34 -0
  27. package/dist/domain/iampolicy/grant-scope.js.map +1 -0
  28. package/dist/domain/iampolicy/membership.d.ts +111 -0
  29. package/dist/domain/iampolicy/membership.d.ts.map +1 -0
  30. package/dist/domain/iampolicy/membership.js +152 -0
  31. package/dist/domain/iampolicy/membership.js.map +1 -0
  32. package/dist/domain/iampolicy/permissions.d.ts +19 -0
  33. package/dist/domain/iampolicy/permissions.d.ts.map +1 -0
  34. package/dist/domain/iampolicy/permissions.js +27 -0
  35. package/dist/domain/iampolicy/permissions.js.map +1 -0
  36. package/dist/domain/iampolicy/resource-store.d.ts +4 -0
  37. package/dist/domain/iampolicy/resource-store.d.ts.map +1 -0
  38. package/dist/domain/iampolicy/resource-store.js +135 -0
  39. package/dist/domain/iampolicy/resource-store.js.map +1 -0
  40. package/dist/domain/iampolicy/role-lifecycle.d.ts +9 -0
  41. package/dist/domain/iampolicy/role-lifecycle.d.ts.map +1 -0
  42. package/dist/domain/iampolicy/role-lifecycle.js +106 -0
  43. package/dist/domain/iampolicy/role-lifecycle.js.map +1 -0
  44. package/dist/domain/iampolicy/roles.d.ts +11 -0
  45. package/dist/domain/iampolicy/roles.d.ts.map +1 -0
  46. package/dist/domain/iampolicy/roles.js +78 -0
  47. package/dist/domain/iampolicy/roles.js.map +1 -0
  48. package/dist/domain/iampolicy/specs.d.ts +7 -0
  49. package/dist/domain/iampolicy/specs.d.ts.map +1 -0
  50. package/dist/domain/iampolicy/specs.js +45 -0
  51. package/dist/domain/iampolicy/specs.js.map +1 -0
  52. package/dist/domain/iampolicy/steps.d.ts +18 -0
  53. package/dist/domain/iampolicy/steps.d.ts.map +1 -0
  54. package/dist/domain/iampolicy/steps.js +117 -0
  55. package/dist/domain/iampolicy/steps.js.map +1 -0
  56. package/dist/domain/iampolicy/store-contract.d.ts +8 -0
  57. package/dist/domain/iampolicy/store-contract.d.ts.map +1 -0
  58. package/dist/domain/iampolicy/store-contract.js +258 -0
  59. package/dist/domain/iampolicy/store-contract.js.map +1 -0
  60. package/dist/domain/iampolicy/store.d.ts +78 -0
  61. package/dist/domain/iampolicy/store.d.ts.map +1 -0
  62. package/dist/domain/iampolicy/store.js +13 -0
  63. package/dist/domain/iampolicy/store.js.map +1 -0
  64. package/dist/domain/iampolicy/wire-refusals.d.ts +25 -0
  65. package/dist/domain/iampolicy/wire-refusals.d.ts.map +1 -0
  66. package/dist/domain/iampolicy/wire-refusals.js +76 -0
  67. package/dist/domain/iampolicy/wire-refusals.js.map +1 -0
  68. package/dist/domain/identityaccount/constants.d.ts +33 -0
  69. package/dist/domain/identityaccount/constants.d.ts.map +1 -1
  70. package/dist/domain/identityaccount/constants.js +2 -48
  71. package/dist/domain/identityaccount/constants.js.map +1 -1
  72. package/dist/domain/identityaccount/controller.d.ts +8 -1
  73. package/dist/domain/identityaccount/controller.d.ts.map +1 -1
  74. package/dist/domain/identityaccount/controller.js +54 -29
  75. package/dist/domain/identityaccount/controller.js.map +1 -1
  76. package/dist/domain/identityaccount/operator.d.ts.map +1 -1
  77. package/dist/domain/identityaccount/operator.js +2 -4
  78. package/dist/domain/identityaccount/operator.js.map +1 -1
  79. package/dist/domain/identityaccount/provisioning.d.ts +20 -1
  80. package/dist/domain/identityaccount/provisioning.d.ts.map +1 -1
  81. package/dist/domain/identityaccount/provisioning.js +17 -3
  82. package/dist/domain/identityaccount/provisioning.js.map +1 -1
  83. package/dist/domain/identityaccount/resolve.d.ts +23 -0
  84. package/dist/domain/identityaccount/resolve.d.ts.map +1 -1
  85. package/dist/domain/identityaccount/resolve.js +17 -0
  86. package/dist/domain/identityaccount/resolve.js.map +1 -1
  87. package/dist/domain/identityaccount/store-contract.d.ts +4 -18
  88. package/dist/domain/identityaccount/store-contract.d.ts.map +1 -1
  89. package/dist/domain/identityaccount/store-contract.js +12 -47
  90. package/dist/domain/identityaccount/store-contract.js.map +1 -1
  91. package/dist/extensions/authorization-queries.d.ts +81 -0
  92. package/dist/extensions/authorization-queries.d.ts.map +1 -0
  93. package/dist/extensions/authorization-queries.js +2 -0
  94. package/dist/extensions/authorization-queries.js.map +1 -0
  95. package/dist/extensions/drivers.d.ts +49 -2
  96. package/dist/extensions/drivers.d.ts.map +1 -1
  97. package/dist/extensions/identity.d.ts +14 -0
  98. package/dist/extensions/identity.d.ts.map +1 -1
  99. package/dist/extensions/identity.js +18 -1
  100. package/dist/extensions/identity.js.map +1 -1
  101. package/dist/extensions/list-read-scope.d.ts +41 -4
  102. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  103. package/dist/extensions/list-read-scope.js +44 -7
  104. package/dist/extensions/list-read-scope.js.map +1 -1
  105. package/dist/extensions/policy-grant-scope.d.ts +50 -0
  106. package/dist/extensions/policy-grant-scope.d.ts.map +1 -0
  107. package/dist/extensions/policy-grant-scope.js +2 -0
  108. package/dist/extensions/policy-grant-scope.js.map +1 -0
  109. package/dist/extensions/registry.d.ts +23 -1
  110. package/dist/extensions/registry.d.ts.map +1 -1
  111. package/dist/extensions/registry.js +30 -0
  112. package/dist/extensions/registry.js.map +1 -1
  113. package/dist/extensions/resource-authorization.d.ts +63 -0
  114. package/dist/extensions/resource-authorization.d.ts.map +1 -1
  115. package/dist/index.d.ts +7 -1
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +2 -0
  118. package/dist/index.js.map +1 -1
  119. package/dist/pipeline/apiresource-meta.d.ts +77 -1
  120. package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
  121. package/dist/pipeline/apiresource-meta.js +183 -1
  122. package/dist/pipeline/apiresource-meta.js.map +1 -1
  123. package/dist/pipeline/interceptors/auth.d.ts +12 -0
  124. package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
  125. package/dist/pipeline/interceptors/auth.js +13 -1
  126. package/dist/pipeline/interceptors/auth.js.map +1 -1
  127. package/dist/pipeline/steps/authorization-tuples.d.ts.map +1 -1
  128. package/dist/pipeline/steps/authorization-tuples.js +7 -26
  129. package/dist/pipeline/steps/authorization-tuples.js.map +1 -1
  130. package/dist/pipeline/steps/authorize.d.ts +20 -5
  131. package/dist/pipeline/steps/authorize.d.ts.map +1 -1
  132. package/dist/pipeline/steps/authorize.js +37 -17
  133. package/dist/pipeline/steps/authorize.js.map +1 -1
  134. package/dist/pipeline/steps/defaults.d.ts +10 -0
  135. package/dist/pipeline/steps/defaults.d.ts.map +1 -1
  136. package/dist/pipeline/steps/defaults.js +39 -0
  137. package/dist/pipeline/steps/defaults.js.map +1 -1
  138. package/dist/pipeline/steps/shapes.d.ts +18 -0
  139. package/dist/pipeline/steps/shapes.d.ts.map +1 -1
  140. package/dist/pipeline/steps/shapes.js +29 -0
  141. package/dist/pipeline/steps/shapes.js.map +1 -1
  142. package/dist/store/port-contract.d.ts +61 -0
  143. package/dist/store/port-contract.d.ts.map +1 -0
  144. package/dist/store/port-contract.js +67 -0
  145. package/dist/store/port-contract.js.map +1 -0
  146. package/package.json +4 -4
  147. package/src/boot/compose.ts +107 -10
  148. package/src/domain/iampolicy/__tests__/access-lists.test.ts +238 -0
  149. package/src/domain/iampolicy/__tests__/constants.test.ts +388 -0
  150. package/src/domain/iampolicy/__tests__/controller.test.ts +960 -0
  151. package/src/domain/iampolicy/__tests__/display-resolver.test.ts +138 -0
  152. package/src/domain/iampolicy/__tests__/grant-path.test.ts +474 -0
  153. package/src/domain/iampolicy/__tests__/grant-scope.test.ts +61 -0
  154. package/src/domain/iampolicy/__tests__/iampolicy.test.ts +532 -0
  155. package/src/domain/iampolicy/__tests__/membership.test.ts +447 -0
  156. package/src/domain/iampolicy/__tests__/permissions.test.ts +41 -0
  157. package/src/domain/iampolicy/__tests__/resource-store.test.ts +165 -0
  158. package/src/domain/iampolicy/__tests__/role-lifecycle.test.ts +361 -0
  159. package/src/domain/iampolicy/__tests__/roles.test.ts +53 -0
  160. package/src/domain/iampolicy/__tests__/specs.test.ts +41 -0
  161. package/src/domain/iampolicy/__tests__/steps.test.ts +278 -0
  162. package/src/domain/iampolicy/__tests__/support.ts +195 -0
  163. package/src/domain/iampolicy/__tests__/wire-refusals.test.ts +117 -0
  164. package/src/domain/iampolicy/access-lists.ts +191 -0
  165. package/src/domain/iampolicy/constants.ts +243 -0
  166. package/src/domain/iampolicy/controller.ts +798 -0
  167. package/src/domain/iampolicy/display-resolver.ts +123 -0
  168. package/src/domain/iampolicy/grant-path.ts +270 -0
  169. package/src/domain/iampolicy/grant-scope.ts +36 -0
  170. package/src/domain/iampolicy/membership.ts +298 -0
  171. package/src/domain/iampolicy/permissions.ts +35 -0
  172. package/src/domain/iampolicy/resource-store.ts +186 -0
  173. package/src/domain/iampolicy/role-lifecycle.ts +132 -0
  174. package/src/domain/iampolicy/roles.ts +95 -0
  175. package/src/domain/iampolicy/specs.ts +53 -0
  176. package/src/domain/iampolicy/steps.ts +178 -0
  177. package/src/domain/iampolicy/store-contract.ts +450 -0
  178. package/src/domain/iampolicy/store.ts +103 -0
  179. package/src/domain/iampolicy/wire-refusals.ts +110 -0
  180. package/src/domain/identityaccount/__tests__/provisioning.test.ts +19 -7
  181. package/src/domain/identityaccount/__tests__/resolve.test.ts +113 -2
  182. package/src/domain/identityaccount/constants.ts +16 -31
  183. package/src/domain/identityaccount/controller.ts +65 -35
  184. package/src/domain/identityaccount/operator.ts +5 -5
  185. package/src/domain/identityaccount/provisioning.ts +43 -5
  186. package/src/domain/identityaccount/resolve.ts +43 -0
  187. package/src/domain/identityaccount/store-contract.ts +22 -70
  188. package/src/extensions/__tests__/composed-support.ts +87 -0
  189. package/src/extensions/__tests__/iam-policy-composed.test.ts +373 -0
  190. package/src/extensions/__tests__/iam-policy-points.test.ts +169 -0
  191. package/src/extensions/__tests__/identity-account-composed.test.ts +12 -78
  192. package/src/extensions/__tests__/identity.test.ts +62 -0
  193. package/src/extensions/__tests__/list-read-scope.test.ts +138 -0
  194. package/src/extensions/__tests__/tier-truthfulness.test.ts +8 -0
  195. package/src/extensions/authorization-queries.ts +97 -0
  196. package/src/extensions/drivers.ts +49 -2
  197. package/src/extensions/identity.ts +21 -0
  198. package/src/extensions/list-read-scope.ts +87 -9
  199. package/src/extensions/policy-grant-scope.ts +50 -0
  200. package/src/extensions/registry.ts +62 -1
  201. package/src/extensions/resource-authorization.ts +65 -0
  202. package/src/index.ts +20 -0
  203. package/src/pipeline/__tests__/grantable-roles-for.test.ts +87 -0
  204. package/src/pipeline/__tests__/inherited-authorization-parent.test.ts +51 -0
  205. package/src/pipeline/__tests__/kind-by-enum-name.test.ts +107 -0
  206. package/src/pipeline/__tests__/kind-served-by-edition.test.ts +107 -0
  207. package/src/pipeline/apiresource-meta.ts +218 -1
  208. package/src/pipeline/interceptors/auth.ts +14 -1
  209. package/src/pipeline/steps/__tests__/authorize-annotation-completeness.test.ts +73 -3
  210. package/src/pipeline/steps/__tests__/authorize.test.ts +134 -9
  211. package/src/pipeline/steps/__tests__/derived-id.test.ts +49 -0
  212. package/src/pipeline/steps/authorization-tuples.ts +7 -28
  213. package/src/pipeline/steps/authorize.ts +46 -21
  214. package/src/pipeline/steps/defaults.ts +42 -0
  215. package/src/pipeline/steps/shapes.ts +31 -0
  216. package/src/store/__tests__/port-contract.test.ts +123 -0
  217. package/src/store/port-contract.ts +102 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Pins the three registry points the IamPolicy domain adds and the one
3
+ * contract it widens (20260913.01, T01_1_review.md Q-OR-10), in the O5
4
+ * driver-point shape the registry already enforces for
5
+ * identityAccountStore and identityFederation:
6
+ *
7
+ * - `drivers.iamPolicyStore` — the store driver (the cloud serves the
8
+ * domain over cloud.iam_policy through it); single instance; absent =
9
+ * the OSS adapter over the generic Store;
10
+ * - `drivers.policyGrantScope` — which kinds a user may grant on and
11
+ * with which roles (P1 gate Q7 iii; Q-OR-3); single instance; absent =
12
+ * open source's default, the organization and nothing else;
13
+ * - `drivers.authorizationQueries` — the tuple-half query engine
14
+ * (Q-OR-8); single instance; absent = the three tuple-half RPCs and a
15
+ * contextual checkMyPermission refuse UNIMPLEMENTED with the edition
16
+ * reason (the domain suite pins the refusal; this file pins the
17
+ * registry);
18
+ * - `ResourceAuthorizationLifecycle` gains the OPTIONAL `onPolicyGranted`
19
+ * and `onPolicyRevoked` (Q7 i): a unit's lifecycle that carries them
20
+ * resolves through the existing single-instance point with both hooks
21
+ * intact, and one that implements only the three required methods
22
+ * still resolves — the widening is additive.
23
+ */
24
+ import { describe, expect, it } from "vitest";
25
+
26
+ import type { IamPolicyStore } from "../../domain/iampolicy/store.js";
27
+ import type { AuthorizationQueryEngine } from "../authorization-queries.js";
28
+ import type { PolicyGrantScope } from "../policy-grant-scope.js";
29
+ import { resolveExtensions } from "../registry.js";
30
+ import type { ResourceAuthorizationLifecycle } from "../resource-authorization.js";
31
+
32
+ const unimplemented = () => Promise.reject(new Error("not exercised here"));
33
+
34
+ const store: IamPolicyStore = {
35
+ save: unimplemented,
36
+ deleteById: unimplemented,
37
+ findById: unimplemented,
38
+ findByPrincipal: unimplemented,
39
+ findByResource: unimplemented,
40
+ findByPrincipalAndResource: unimplemented,
41
+ findByResourceWithRelations: unimplemented,
42
+ countDistinctPrincipalsByResource: unimplemented,
43
+ findScopeTuple: unimplemented,
44
+ };
45
+
46
+ const scope: PolicyGrantScope = {
47
+ grantableRoles: () => [],
48
+ };
49
+
50
+ const engine: AuthorizationQueryEngine = {
51
+ check: unimplemented,
52
+ listResourceIds: unimplemented,
53
+ listPrincipalIds: unimplemented,
54
+ };
55
+
56
+ describe("the iamPolicyStore driver point", () => {
57
+ it("is undefined with no extensions — the OSS adapter serves", () => {
58
+ expect(resolveExtensions([]).drivers.iamPolicyStore).toBeUndefined();
59
+ });
60
+
61
+ it("carries the one registered instance through", () => {
62
+ const resolved = resolveExtensions([
63
+ { name: "cloud-iam", drivers: { iamPolicyStore: store } },
64
+ ]);
65
+ expect(resolved.drivers.iamPolicyStore).toBe(store);
66
+ });
67
+
68
+ it("throws on a second registration, naming both units", () => {
69
+ expect(() =>
70
+ resolveExtensions([
71
+ { name: "store-a", drivers: { iamPolicyStore: store } },
72
+ { name: "store-b", drivers: { iamPolicyStore: store } },
73
+ ]),
74
+ ).toThrowError(
75
+ /extension 'store-b' registers an IamPolicyStore, but 'store-a' already did/,
76
+ );
77
+ });
78
+ });
79
+
80
+ describe("the policyGrantScope driver point", () => {
81
+ it("is undefined with no extensions — open source grants on organizations only", () => {
82
+ expect(resolveExtensions([]).drivers.policyGrantScope).toBeUndefined();
83
+ });
84
+
85
+ it("carries the one registered instance through", () => {
86
+ const resolved = resolveExtensions([
87
+ { name: "cloud-iam", drivers: { policyGrantScope: scope } },
88
+ ]);
89
+ expect(resolved.drivers.policyGrantScope).toBe(scope);
90
+ });
91
+
92
+ it("throws on a second registration, naming both units", () => {
93
+ expect(() =>
94
+ resolveExtensions([
95
+ { name: "scope-a", drivers: { policyGrantScope: scope } },
96
+ { name: "scope-b", drivers: { policyGrantScope: scope } },
97
+ ]),
98
+ ).toThrowError(
99
+ /extension 'scope-b' registers a PolicyGrantScope, but 'scope-a' already did/,
100
+ );
101
+ });
102
+ });
103
+
104
+ describe("the authorizationQueries capability point", () => {
105
+ it("is undefined with no extensions — the tuple-half RPCs refuse", () => {
106
+ expect(resolveExtensions([]).drivers.authorizationQueries).toBeUndefined();
107
+ });
108
+
109
+ it("carries the one registered instance through", () => {
110
+ const resolved = resolveExtensions([
111
+ { name: "cloud-iam", drivers: { authorizationQueries: engine } },
112
+ ]);
113
+ expect(resolved.drivers.authorizationQueries).toBe(engine);
114
+ });
115
+
116
+ it("throws on a second registration, naming both units", () => {
117
+ expect(() =>
118
+ resolveExtensions([
119
+ { name: "engine-a", drivers: { authorizationQueries: engine } },
120
+ { name: "engine-b", drivers: { authorizationQueries: engine } },
121
+ ]),
122
+ ).toThrowError(
123
+ /extension 'engine-b' registers an AuthorizationQueryEngine, but 'engine-a' already did/,
124
+ );
125
+ });
126
+ });
127
+
128
+ describe("the widened ResourceAuthorizationLifecycle contract", () => {
129
+ const resourceHooks = {
130
+ onResourceCreated: unimplemented,
131
+ onResourceDeleted: unimplemented,
132
+ onVisibilityChanged: unimplemented,
133
+ };
134
+
135
+ it("a lifecycle carrying the two policy hooks resolves with both intact — the cloud's tuple driver", () => {
136
+ const tupleDriver: ResourceAuthorizationLifecycle = {
137
+ ...resourceHooks,
138
+ onPolicyGranted: unimplemented,
139
+ onPolicyRevoked: unimplemented,
140
+ };
141
+ const resolved = resolveExtensions([
142
+ {
143
+ name: "cloud-iam",
144
+ drivers: { resourceAuthorizationLifecycle: tupleDriver },
145
+ },
146
+ ]);
147
+ expect(resolved.drivers.resourceAuthorizationLifecycle).toBe(tupleDriver);
148
+ expect(
149
+ resolved.drivers.resourceAuthorizationLifecycle?.onPolicyGranted,
150
+ ).toBe(unimplemented);
151
+ expect(
152
+ resolved.drivers.resourceAuthorizationLifecycle?.onPolicyRevoked,
153
+ ).toBe(unimplemented);
154
+ });
155
+
156
+ it("a lifecycle with only the three required methods still resolves — the widening is additive", () => {
157
+ const minimal: ResourceAuthorizationLifecycle = resourceHooks;
158
+ const resolved = resolveExtensions([
159
+ {
160
+ name: "some-unit",
161
+ drivers: { resourceAuthorizationLifecycle: minimal },
162
+ },
163
+ ]);
164
+ expect(resolved.drivers.resourceAuthorizationLifecycle).toBe(minimal);
165
+ expect(
166
+ resolved.drivers.resourceAuthorizationLifecycle?.onPolicyGranted,
167
+ ).toBeUndefined();
168
+ });
169
+ });
@@ -37,7 +37,8 @@
37
37
  * The registered driver is the REAL OSS adapter over a SECOND SqliteStore
38
38
  * behind a call recorder, so the proof of consumption is WHERE the rows
39
39
  * land, and the pair under test is one that runs in production. The fake
40
- * verifier of (B) and (C) vouches for an unsigned JWT-shaped token with no
40
+ * verifier of (B) and (C) (composed-support.ts, shared with the other
41
+ * composed proofs) vouches for an unsigned JWT-shaped token with no
41
42
  * issuer: idpIdOf reads the `sub` from the raw token and the provisioner
42
43
  * takes the identity's own claims when there is no issuer, so a real first
43
44
  * provisioning runs with no network. The OSS OIDC verifier over a driver is
@@ -52,8 +53,7 @@ import path from "node:path";
52
53
  import { create, fromBinary } from "@bufbuild/protobuf";
53
54
  import type { DescMessage } from "@bufbuild/protobuf";
54
55
  import { Code, ConnectError, createClient } from "@connectrpc/connect";
55
- import type { Client, Interceptor, Transport } from "@connectrpc/connect";
56
- import { createGrpcTransport } from "@connectrpc/connect-node";
56
+ import type { Client } from "@connectrpc/connect";
57
57
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
58
58
 
59
59
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
@@ -68,7 +68,6 @@ import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
68
68
  import { loadConfig } from "../../boot/config.js";
69
69
  import { composeServer } from "../../boot/compose.js";
70
70
  import type { ComposedServer } from "../../boot/compose.js";
71
- import { createLogger } from "../../boot/logger.js";
72
71
  import { accountIdFor } from "../../domain/identityaccount/constants.js";
73
72
  import { newResourceIdentityAccountStore } from "../../domain/identityaccount/resource-store.js";
74
73
  import type { IdentityAccountStore } from "../../domain/identityaccount/store.js";
@@ -81,18 +80,19 @@ import type { Store } from "../../store/interface.js";
81
80
  import { SqliteStore } from "../../store/sqlite/store.js";
82
81
  import type { Authorizer } from "../authorizer.js";
83
82
  import type { GateSlotName } from "../gate-slots.js";
84
- import type { CallerIdentity, IdentityVerifier } from "../identity.js";
83
+ import type { CallerIdentity } from "../identity.js";
85
84
  import type { IdentityFederation } from "../identity-federation.js";
86
85
  import type { ServerExtension } from "../registry.js";
87
-
88
- const silentLogger = createLogger({
89
- level: "error",
90
- pretty: false,
91
- write: () => {},
92
- });
86
+ import {
87
+ baseConfig,
88
+ fakeJwt,
89
+ fakeVerifier,
90
+ silentLogger,
91
+ transportFor,
92
+ } from "./composed-support.js";
93
93
 
94
94
  // ---------------------------------------------------------------------------
95
- // Shared fakes
95
+ // Shared fakes (the composition-shaped ones live in composed-support.ts)
96
96
  // ---------------------------------------------------------------------------
97
97
 
98
98
  /** The port's eight methods, delegating to the real adapter and recording each call by name. */
@@ -161,62 +161,6 @@ async function accountIdsIn(store: Store): Promise<ReadonlyArray<string>> {
161
161
  );
162
162
  }
163
163
 
164
- /**
165
- * An unsigned JWT-shaped token (`header.payload.unsigned`): the fake
166
- * verifier below vouches for it, and idpIdOf reads its `sub` exactly as it
167
- * reads a real token's — the signature was the verifier's business.
168
- */
169
- function fakeJwt(sub: string, email: string): string {
170
- const segment = (value: unknown): string =>
171
- Buffer.from(JSON.stringify(value)).toString("base64url");
172
- return `${segment({ alg: "none" })}.${segment({ sub, email })}.unsigned`;
173
- }
174
-
175
- /**
176
- * The composition's own verifier (the cloud's shape): admits the fake
177
- * token as a `user` whose identityId is the raw subject and whose issuer is
178
- * empty — it does NOT resolve to an account, exactly like a verifier that
179
- * runs before any row exists. Passes on everything else, so the OSS API-key
180
- * lane composed ahead of it keeps its claim.
181
- */
182
- const fakeVerifier: IdentityVerifier = {
183
- name: "fake-composition-verifier",
184
- verify: (token) => {
185
- const segments = token.split(".");
186
- if (segments.length !== 3 || segments[2] !== "unsigned") {
187
- return Promise.resolve(null);
188
- }
189
- const claims = JSON.parse(
190
- Buffer.from(segments[1] ?? "", "base64url").toString("utf8"),
191
- ) as { sub?: unknown; email?: unknown };
192
- if (typeof claims.sub !== "string") {
193
- return Promise.resolve(null);
194
- }
195
- const identity: CallerIdentity = {
196
- identityId: claims.sub,
197
- callerClass: "user",
198
- issuer: "",
199
- rawToken: token,
200
- ...(typeof claims.email === "string" ? { email: claims.email } : {}),
201
- };
202
- return Promise.resolve(identity);
203
- },
204
- };
205
-
206
- function bearer(token: string): Interceptor {
207
- return (next) => (request) => {
208
- request.header.set("authorization", `Bearer ${token}`);
209
- return next(request);
210
- };
211
- }
212
-
213
- function transportFor(port: number, token?: string): Transport {
214
- return createGrpcTransport({
215
- baseUrl: `http://127.0.0.1:${port}`,
216
- interceptors: token !== undefined ? [bearer(token)] : [],
217
- });
218
- }
219
-
220
164
  async function connectErrorOf(
221
165
  promise: Promise<unknown>,
222
166
  ): Promise<ConnectError> {
@@ -244,16 +188,6 @@ function accountInput(idpId: string) {
244
188
  };
245
189
  }
246
190
 
247
- function baseConfig(dir: string): Record<string, string> {
248
- return {
249
- STIGMER_MODEL_REGISTRY_REFRESH: "off",
250
- TEMPORAL_HOST_PORT: "127.0.0.1:1",
251
- DB_PATH: path.join(dir, "stigmer.db"),
252
- STORAGE_PATH: path.join(dir, "storage"),
253
- ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
254
- };
255
- }
256
-
257
191
  // ---------------------------------------------------------------------------
258
192
  // (A) trusted-local: the store driver, the federation capability
259
193
  // ---------------------------------------------------------------------------
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Pins `isPlatformPipelineCaller` (extensions/identity.ts; 20260913.01
3
+ * slice 5, Q-S5-10): the ONE definition of "the platform's own pipelines"
4
+ * — a `machine` or `internal` caller, or any identity that entered
5
+ * through the in-process transport — read by the identity-account
6
+ * `create` RPC's admission rule (2a A7) and the three IamPolicy system
7
+ * RPCs' (Q-OR-7). A wire `user`, `runner` or a composition's own class
8
+ * (guest) is never one.
9
+ */
10
+ import { describe, expect, it } from "vitest";
11
+
12
+ import { isPlatformPipelineCaller } from "../identity.js";
13
+ import type { CallerIdentity } from "../identity.js";
14
+
15
+ function caller(fields: Partial<CallerIdentity>): CallerIdentity {
16
+ return {
17
+ identityId: "ida_x",
18
+ callerClass: "user",
19
+ issuer: "",
20
+ rawToken: "",
21
+ ...fields,
22
+ };
23
+ }
24
+
25
+ describe("isPlatformPipelineCaller", () => {
26
+ it("admits machine and internal callers whatever their origin", () => {
27
+ expect(isPlatformPipelineCaller(caller({ callerClass: "machine" }))).toBe(
28
+ true,
29
+ );
30
+ expect(isPlatformPipelineCaller(caller({ callerClass: "internal" }))).toBe(
31
+ true,
32
+ );
33
+ expect(
34
+ isPlatformPipelineCaller(
35
+ caller({ callerClass: "machine", origin: "wire" }),
36
+ ),
37
+ ).toBe(true);
38
+ });
39
+
40
+ it("admits any identity that entered through the in-process transport — a propagated user included", () => {
41
+ expect(
42
+ isPlatformPipelineCaller(
43
+ caller({ callerClass: "user", origin: "in-process" }),
44
+ ),
45
+ ).toBe(true);
46
+ });
47
+
48
+ it("refuses wire users, runners and a composition's own classes", () => {
49
+ expect(isPlatformPipelineCaller(caller({ callerClass: "user" }))).toBe(
50
+ false,
51
+ );
52
+ expect(
53
+ isPlatformPipelineCaller(caller({ callerClass: "user", origin: "wire" })),
54
+ ).toBe(false);
55
+ expect(isPlatformPipelineCaller(caller({ callerClass: "runner" }))).toBe(
56
+ false,
57
+ );
58
+ expect(isPlatformPipelineCaller(caller({ callerClass: "guest" }))).toBe(
59
+ false,
60
+ );
61
+ });
62
+ });
@@ -14,6 +14,12 @@
14
14
  * a fraction of the candidates);
15
15
  * - the candidates carry {id, org, labels} — the driver's guest
16
16
  * cookie rule keys on labels;
17
+ * - a kind whose authorization is its parent's (kind_meta PARENT scope
18
+ * + INHERITED owner; agent_execution → session) carries
19
+ * `authorizationParent` on every candidate whose spec names it, the
20
+ * same ResolvedParentLink the tuple lifecycle wrote; every other kind
21
+ * and every parentless row carries none (stigmer-cloud 20260913.04
22
+ * T04);
17
23
  * - a scope failure PROPAGATES — never an empty result (the outage
18
24
  * contract: empty means "authorized to see nothing", outage means
19
25
  * INTERNAL through the caller's sanitized arm).
@@ -155,6 +161,138 @@ describe("restrictListByReadScope", () => {
155
161
  ]);
156
162
  });
157
163
 
164
+ describe("authorizationParent — the parent a kind's authorization is (20260913.04 T04)", () => {
165
+ function recordingScope(keep: ReadonlyArray<string>) {
166
+ const seen: ReadonlyArray<ListEntryMeta>[] = [];
167
+ const scope: ListReadScope = {
168
+ authorizedResourceIds: () => Promise.resolve(new Set<string>()),
169
+ restrictListEntries: (_caller, _kind, entries) => {
170
+ seen.push(entries);
171
+ return Promise.resolve(new Set(keep));
172
+ },
173
+ };
174
+ return { scope, seen };
175
+ }
176
+
177
+ it("an agent_execution candidate carries its session as the ResolvedParentLink the tuple lifecycle wrote", async () => {
178
+ const executions = [
179
+ {
180
+ metadata: { id: "aex_1", org: "acme", labels: {} },
181
+ spec: { sessionId: "ses_a" },
182
+ },
183
+ {
184
+ metadata: { id: "aex_2", org: "acme", labels: {} },
185
+ spec: { sessionId: "ses_a" },
186
+ },
187
+ {
188
+ metadata: { id: "aex_3", org: "acme", labels: {} },
189
+ spec: { sessionId: "ses_b" },
190
+ },
191
+ ];
192
+ const { scope, seen } = recordingScope(["aex_1", "aex_3"]);
193
+ const kept = await restrictListByReadScope(
194
+ scope,
195
+ caller,
196
+ ApiResourceKind.agent_execution,
197
+ executions,
198
+ "",
199
+ );
200
+ expect(kept.map((r) => r.metadata.id)).toEqual(["aex_1", "aex_3"]);
201
+ expect(seen[0]).toEqual([
202
+ {
203
+ id: "aex_1",
204
+ org: "acme",
205
+ labels: {},
206
+ authorizationParent: {
207
+ relation: "session",
208
+ parentKind: ApiResourceKind.session,
209
+ parentId: "ses_a",
210
+ },
211
+ },
212
+ {
213
+ id: "aex_2",
214
+ org: "acme",
215
+ labels: {},
216
+ authorizationParent: {
217
+ relation: "session",
218
+ parentKind: ApiResourceKind.session,
219
+ parentId: "ses_a",
220
+ },
221
+ },
222
+ {
223
+ id: "aex_3",
224
+ org: "acme",
225
+ labels: {},
226
+ authorizationParent: {
227
+ relation: "session",
228
+ parentKind: ApiResourceKind.session,
229
+ parentId: "ses_b",
230
+ },
231
+ },
232
+ ]);
233
+ });
234
+
235
+ it("a row whose spec does not name its parent carries no parent — offered as itself, never dropped", async () => {
236
+ const executions = [
237
+ { metadata: { id: "aex_orphan", org: "acme", labels: {} }, spec: {} },
238
+ { metadata: { id: "aex_specless", org: "acme", labels: {} } },
239
+ ];
240
+ const { scope, seen } = recordingScope(["aex_orphan", "aex_specless"]);
241
+ const kept = await restrictListByReadScope(
242
+ scope,
243
+ caller,
244
+ ApiResourceKind.agent_execution,
245
+ executions,
246
+ "",
247
+ );
248
+ expect(kept.map((r) => r.metadata.id)).toEqual([
249
+ "aex_orphan",
250
+ "aex_specless",
251
+ ]);
252
+ expect(seen[0]).toEqual([
253
+ { id: "aex_orphan", org: "acme", labels: {} },
254
+ { id: "aex_specless", org: "acme", labels: {} },
255
+ ]);
256
+ });
257
+
258
+ it("a kind whose authorization is its own carries no parent even when its spec names one", async () => {
259
+ // A workflow_execution links its workflow_instance (an additional
260
+ // parent, partial opt-in inheritance) and owns itself: not the pair.
261
+ const runs = [
262
+ {
263
+ metadata: { id: "wex_1", org: "acme", labels: {} },
264
+ spec: { workflowInstanceId: "wfi_1" },
265
+ },
266
+ ];
267
+ const { scope, seen } = recordingScope(["wex_1"]);
268
+ await restrictListByReadScope(
269
+ scope,
270
+ caller,
271
+ ApiResourceKind.workflow_execution,
272
+ runs,
273
+ "",
274
+ );
275
+ expect(seen[0]).toEqual([{ id: "wex_1", org: "acme", labels: {} }]);
276
+ });
277
+
278
+ it("no scope composed = the input unchanged, the parent never resolved", async () => {
279
+ const executions = [
280
+ {
281
+ metadata: { id: "aex_1", org: "acme", labels: {} },
282
+ spec: { sessionId: "ses_a" },
283
+ },
284
+ ];
285
+ const kept = await restrictListByReadScope(
286
+ undefined,
287
+ caller,
288
+ ApiResourceKind.agent_execution,
289
+ executions,
290
+ "acme",
291
+ );
292
+ expect(kept).toEqual(executions);
293
+ });
294
+ });
295
+
158
296
  it("a scope failure propagates — never an empty result", async () => {
159
297
  const failing: ListReadScope = {
160
298
  authorizedResourceIds: () => Promise.reject(new Error("fga down")),
@@ -218,6 +218,14 @@ describe("tier truthfulness against the empty composition", () => {
218
218
  );
219
219
  expect(served.has("identityaccount")).toBe(true);
220
220
  });
221
+
222
+ it("iam_policy is open_source and served — the row half lives in open source (20260913.01, Q-EC-2b again)", () => {
223
+ const served = servedKindSegments(server.routes);
224
+ expect(getKindMeta(ApiResourceKind.iam_policy).tier).toBe(
225
+ ResourceTier.open_source,
226
+ );
227
+ expect(served.has("iampolicy")).toBe(true);
228
+ });
221
229
  });
222
230
 
223
231
  describe("the check bites (mutation proofs over the same function)", () => {
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The authorization-query capability (20260913.01, T01_1_review.md Q-OR-8):
3
+ * the TUPLE half of the IamPolicy query surface — the questions only an
4
+ * authorization engine can answer over its own graph, as opposed to the
5
+ * ROW half (who holds which role on what) that every edition answers from
6
+ * IamPolicy rows. Single instance, registered as
7
+ * `drivers.authorizationQueries` (the identityFederation shape: a driver
8
+ * point that is open source's own behaviour when absent).
9
+ *
10
+ * Absent, the IamPolicy query controller refuses `checkAuthorization`,
11
+ * `listAuthorizedResourceIds`, `listAuthorizedPrincipalIds`, and a
12
+ * `checkMyPermission` that carries contextual policies, UNIMPLEMENTED with
13
+ * the edition reason (domain/iampolicy/constants.ts
14
+ * AUTHORIZATION_QUERIES_UNIMPLEMENTED_MESSAGE) — never INTERNAL, the
15
+ * organization directory's absent-method precedent. Present, the
16
+ * controller still owns everything that is not the graph question: the
17
+ * annotation's Authorize step, `checkAuthorization`'s principal-trust rule
18
+ * (a user asks only about their own account; machine and internal about
19
+ * anyone), the UNAUTHENTICATED refusal, and the plain
20
+ * `checkMyPermission` that rides the composed Authorizer instead. The
21
+ * engine receives boundary-validated refs and never asks who is calling.
22
+ *
23
+ * The engine speaks the CONTRACT's vocabulary, not a backend's (Q-S3-1,
24
+ * 2026-09-13): `ApiResourceRef` for a principal or resource (kind = the
25
+ * enum member name, id, and for a principal an optional relation — the
26
+ * userset form `organization:X#member`), `IamPolicySpec` for a policy and
27
+ * for the what-if policies every query may carry, and the relation and
28
+ * kind strings as the wire sends them. The cloud's driver renders these to
29
+ * OpenFGA's `type:id#relation` strings itself; that grammar belongs to the
30
+ * store that runs it and never enters this library.
31
+ *
32
+ * Semantics the controller and the conformance suite rely on (the Java
33
+ * check-handler posture the cloud's client already implements):
34
+ *
35
+ * - `check` answers a public grant (a wildcard principal resolves), so
36
+ * "may I read this public agent" is true for anyone.
37
+ * - the two listings never expand wildcards, and `listPrincipalIds`
38
+ * answers DIRECT principals only — a listing is an inventory of who
39
+ * was named, never a resolution of who could reach.
40
+ * - contextual policies are evaluated as if held, and nothing is written.
41
+ *
42
+ * Error doctrine (the ListReadScope rule, verbatim in spirit: a seam that
43
+ * cannot answer THROWS — never an empty set, never false): `false` and
44
+ * `[]` are REAL answers. A backend outage surfaces as a throw; a
45
+ * ConnectError keeps its code (the cloud maps its three OpenFGA wire arms —
46
+ * DEADLINE_EXCEEDED, UNAVAILABLE, INTERNAL — before throwing), any other
47
+ * throw is INTERNAL through the pipeline's boundary. A denial dressed as
48
+ * an outage, or an outage dressed as a denial, is the fail-open class this
49
+ * contract exists to refuse.
50
+ *
51
+ * Why this is not `ListReadScope`: the read scope narrows the server's OWN
52
+ * list lanes for the calling identity with no what-if input; this engine
53
+ * answers the wire's explicit queries about an ARBITRARY principal with
54
+ * contextual policies. Both are ratified seams with distinct consumers;
55
+ * a cloud driver may wrap one client for both, and that is its business.
56
+ */
57
+ import type {
58
+ ApiResourceRef,
59
+ IamPolicySpec,
60
+ } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
61
+
62
+ /** The query-engine contract (single-instance point, ExtensionDrivers.authorizationQueries). */
63
+ export interface AuthorizationQueryEngine {
64
+ /**
65
+ * Whether `policy` is effectively held — its principal has its relation
66
+ * on its resource — given `contextualPolicies` evaluated as if held.
67
+ * The proto's CheckAuthorizationInput shape; `checkMyPermission` builds
68
+ * the spec from the caller's account ref. The spec's refs are present
69
+ * (boundary-validated, or controller-built).
70
+ */
71
+ check(
72
+ policy: IamPolicySpec,
73
+ contextualPolicies: ReadonlyArray<IamPolicySpec>,
74
+ ): Promise<boolean>;
75
+ /**
76
+ * The ids of `resourceKind` resources on which `principal` holds
77
+ * `relation` (ListAuthorizedResourceIdsInput's field order). Wildcards
78
+ * are not expanded.
79
+ */
80
+ listResourceIds(
81
+ principal: ApiResourceRef,
82
+ relation: string,
83
+ resourceKind: string,
84
+ contextualPolicies: ReadonlyArray<IamPolicySpec>,
85
+ ): Promise<ReadonlyArray<string>>;
86
+ /**
87
+ * The ids of `principalKind` principals holding `relation` on `resource`
88
+ * (ListAuthorizedPrincipalIdsInput's field order). Direct principals
89
+ * only; wildcards are not expanded.
90
+ */
91
+ listPrincipalIds(
92
+ resource: ApiResourceRef,
93
+ relation: string,
94
+ principalKind: string,
95
+ contextualPolicies: ReadonlyArray<IamPolicySpec>,
96
+ ): Promise<ReadonlyArray<string>>;
97
+ }