@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,532 @@
1
+ /**
2
+ * Pins the IamPolicy domain through a composed OSS server in the
3
+ * trusted-local posture — the open-source behaviours the conformance suite
4
+ * cannot express cross-edition (T01_0_plan.md §3a, §5; T01_1_review.md
5
+ * Q-OR-3, Q-OR-5, Q-OR-6a, Q-OR-7, Q-OR-8, Q-OR-9):
6
+ *
7
+ * - creating an organization makes its creator the owner: the built-in
8
+ * role lifecycle's one write arm, read back through the row-driven
9
+ * access list and count with the operator's display fields;
10
+ * - a grant on a kind no edition grants on (kind_meta lists no roles) is
11
+ * INVALID_ARGUMENT with the cloud's system-managed copy; a grant on
12
+ * any other kind but the organization is UNIMPLEMENTED with the
13
+ * edition named (the grant scope); a role the organization does not
14
+ * admit is INVALID_ARGUMENT with the cloud's copy — the proto first,
15
+ * the scope second, the role third;
16
+ * - create is idempotent by derived id and stamps the proto's apiVersion;
17
+ * a role change is delete then create; revokeOrgAccess empties the
18
+ * account's rows; deleting the organization cleans its rows;
19
+ * - the three system RPCs are PERMISSION_DENIED over the wire and served
20
+ * on the in-process transport (the `internal` class);
21
+ * - checkMyPermission's three arms: an enterprise-tiered kind is false
22
+ * with the Authorizer never consulted (the settings navigation stays
23
+ * hidden); can_grant_access outside the scope is false; an
24
+ * organization permission is the composed Authorizer's answer; an
25
+ * unknown permission is INVALID_ARGUMENT;
26
+ * - the tuple-half queries are UNIMPLEMENTED with the edition named;
27
+ * - the byte-pinned NOT_FOUND copy on get; delete of an absent triple is
28
+ * the default instance.
29
+ */
30
+ import { mkdtempSync, rmSync } from "node:fs";
31
+ import { tmpdir } from "node:os";
32
+ import path from "node:path";
33
+
34
+ import { create } from "@bufbuild/protobuf";
35
+ import { Code, ConnectError, createClient } from "@connectrpc/connect";
36
+ import type { Client, Transport } from "@connectrpc/connect";
37
+ import { createGrpcTransport } from "@connectrpc/connect-node";
38
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
39
+
40
+ import { IdentityAccountCommandController } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/command_pb";
41
+ import { IamPolicyCommandController } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/command_pb";
42
+ import { IamPolicyQueryController } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/query_pb";
43
+ import { ApiResourceRefSchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
44
+ import { OrganizationCommandController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/command_pb";
45
+
46
+ import { loadConfig } from "../../../boot/config.js";
47
+ import { composeServer } from "../../../boot/compose.js";
48
+ import type { ComposedServer } from "../../../boot/compose.js";
49
+ import { createLogger } from "../../../boot/logger.js";
50
+ import {
51
+ resetOperatorIdentityForTests,
52
+ setOperatorIdentity,
53
+ } from "../../../pipeline/steps/defaults.js";
54
+ import {
55
+ accountIdFor,
56
+ localIdpIdFor,
57
+ } from "../../identityaccount/constants.js";
58
+ import {
59
+ AUTHORIZATION_QUERIES_UNIMPLEMENTED_MESSAGE,
60
+ IAM_POLICY_API_VERSION,
61
+ PER_RESOURCE_GRANTS_UNIMPLEMENTED_MESSAGE,
62
+ noGrantableRolesMessage,
63
+ policyIdFor,
64
+ policyNotFoundMessage,
65
+ roleNotGrantableMessage,
66
+ unknownPermissionMessage,
67
+ } from "../constants.js";
68
+ import { orgRole, triple } from "./support.js";
69
+
70
+ const OPERATOR_EMAIL = "operator@example.com";
71
+ const OPERATOR_NAME = "The Operator";
72
+
73
+ async function grpcError(run: () => Promise<unknown>): Promise<ConnectError> {
74
+ try {
75
+ await run();
76
+ } catch (error) {
77
+ if (error instanceof ConnectError) {
78
+ return error;
79
+ }
80
+ throw error;
81
+ }
82
+ throw new Error("expected the call to fail");
83
+ }
84
+
85
+ describe("iampolicy domain (composed server, trusted-local posture)", () => {
86
+ let dir: string;
87
+ let server: ComposedServer;
88
+ let transport: Transport;
89
+ let command: Client<typeof IamPolicyCommandController>;
90
+ let query: Client<typeof IamPolicyQueryController>;
91
+ let organizations: Client<typeof OrganizationCommandController>;
92
+ /** The platform's own pipeline: the in-process transport's `internal` caller. */
93
+ let platform: Client<typeof IamPolicyCommandController>;
94
+ let platformAccounts: Client<typeof IdentityAccountCommandController>;
95
+ /** The operator's account id — the trusted-local caller resolves to it. */
96
+ const operatorId = accountIdFor(localIdpIdFor(OPERATOR_EMAIL));
97
+ let seq = 0;
98
+
99
+ beforeAll(async () => {
100
+ dir = mkdtempSync(path.join(tmpdir(), "iampolicy-domain-test-"));
101
+ setOperatorIdentity(OPERATOR_EMAIL, OPERATOR_NAME);
102
+ server = await composeServer({
103
+ config: loadConfig({
104
+ STIGMER_MODEL_REGISTRY_REFRESH: "off",
105
+ TEMPORAL_HOST_PORT: "127.0.0.1:1",
106
+ DB_PATH: path.join(dir, "stigmer.db"),
107
+ STORAGE_PATH: path.join(dir, "storage"),
108
+ ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
109
+ STIGMER_OPERATOR_EMAIL: OPERATOR_EMAIL,
110
+ STIGMER_OPERATOR_NAME: OPERATOR_NAME,
111
+ }),
112
+ logger: createLogger({ level: "error", pretty: false, write: () => {} }),
113
+ portOverride: 0,
114
+ host: "127.0.0.1",
115
+ });
116
+ const port = await server.start();
117
+ transport = createGrpcTransport({ baseUrl: `http://127.0.0.1:${port}` });
118
+ command = createClient(IamPolicyCommandController, transport);
119
+ query = createClient(IamPolicyQueryController, transport);
120
+ organizations = createClient(OrganizationCommandController, transport);
121
+ platform = createClient(
122
+ IamPolicyCommandController,
123
+ server.inProcessTransport,
124
+ );
125
+ platformAccounts = createClient(
126
+ IdentityAccountCommandController,
127
+ server.inProcessTransport,
128
+ );
129
+ });
130
+
131
+ afterAll(async () => {
132
+ await server.shutdown();
133
+ resetOperatorIdentityForTests();
134
+ rmSync(dir, { recursive: true, force: true });
135
+ });
136
+
137
+ async function newOrganization(): Promise<string> {
138
+ seq += 1;
139
+ const slug = `roles-org-${seq}`;
140
+ await organizations.create({
141
+ apiVersion: "tenancy.stigmer.ai/v1",
142
+ kind: "Organization",
143
+ metadata: { name: slug, slug, org: "" },
144
+ spec: { description: "created by the iampolicy domain test" },
145
+ });
146
+ return slug;
147
+ }
148
+
149
+ /** A second account, created the way the platform creates one (in-process). */
150
+ async function newAccount(): Promise<string> {
151
+ seq += 1;
152
+ const created = await platformAccounts.create({
153
+ apiVersion: "iam.stigmer.ai/v1",
154
+ kind: "IdentityAccount",
155
+ metadata: { name: `person${seq}@example.com` },
156
+ spec: {
157
+ idpId: `auth0|person${seq}`,
158
+ email: `person${seq}@example.com`,
159
+ firstName: "Person",
160
+ lastName: `${seq}`,
161
+ },
162
+ });
163
+ return created.metadata?.id ?? "";
164
+ }
165
+
166
+ describe("the creator owns the organization (the built-in role lifecycle)", () => {
167
+ it("lists the operator as owner with display fields, and counts one principal", async () => {
168
+ const org = await newOrganization();
169
+
170
+ const access = await query.listResourceAccessByPrincipal({
171
+ resource: create(ApiResourceRefSchema, {
172
+ kind: "organization",
173
+ id: org,
174
+ }),
175
+ });
176
+ const count = await query.getPrincipalsCount({
177
+ orgId: org,
178
+ principalKind: "identity_account",
179
+ });
180
+
181
+ expect(access.entries).toHaveLength(1);
182
+ const [entry] = access.entries;
183
+ expect(entry?.principal?.id).toBe(operatorId);
184
+ expect(entry?.principal?.email).toBe(OPERATOR_EMAIL);
185
+ expect(entry?.principal?.name).toBe(OPERATOR_NAME);
186
+ expect(entry?.roles.map((grant) => grant.role?.code)).toEqual(["owner"]);
187
+ expect(entry?.roles[0]?.isInherited).toBe(false);
188
+ expect(count.count).toBe(1);
189
+ });
190
+
191
+ it("deleting the organization cleans its rows", async () => {
192
+ const org = await newOrganization();
193
+ await organizations.delete({ value: org });
194
+ const count = await query.getPrincipalsCount({
195
+ orgId: org,
196
+ principalKind: "identity_account",
197
+ });
198
+ expect(count.count).toBe(0);
199
+ });
200
+ });
201
+
202
+ describe("grants on the organization", () => {
203
+ it("create is idempotent by derived id and stamps the proto's apiVersion", async () => {
204
+ const org = await newOrganization();
205
+ const bob = await newAccount();
206
+ const spec = orgRole(bob, "member", org);
207
+
208
+ const first = await command.create(spec);
209
+ const second = await command.create(spec);
210
+
211
+ expect(first.metadata?.id).toBe(policyIdFor(spec));
212
+ expect(first.apiVersion).toBe(IAM_POLICY_API_VERSION);
213
+ expect(second.metadata?.id).toBe(first.metadata?.id);
214
+ expect(
215
+ (
216
+ await query.getPrincipalsCount({
217
+ orgId: org,
218
+ principalKind: "identity_account",
219
+ })
220
+ ).count,
221
+ ).toBe(2);
222
+ });
223
+
224
+ it("a role change is delete then create, and the access list follows", async () => {
225
+ const org = await newOrganization();
226
+ const bob = await newAccount();
227
+ await command.create(orgRole(bob, "member", org));
228
+
229
+ const deleted = await command.delete(orgRole(bob, "member", org));
230
+ await command.create(orgRole(bob, "admin", org));
231
+
232
+ expect(deleted.metadata?.id).toBe(
233
+ policyIdFor(orgRole(bob, "member", org)),
234
+ );
235
+ const roles = await query.getPrincipalResourceRoles({
236
+ principal: create(ApiResourceRefSchema, {
237
+ kind: "identity_account",
238
+ id: bob,
239
+ }),
240
+ resource: create(ApiResourceRefSchema, {
241
+ kind: "organization",
242
+ id: org,
243
+ }),
244
+ });
245
+ expect(roles.roles.map((role) => role.code)).toEqual(["admin"]);
246
+ });
247
+
248
+ it("every kind_meta role of the organization is grantable (Q-OR-4); a role it does not list is INVALID_ARGUMENT with the cloud's copy", async () => {
249
+ const org = await newOrganization();
250
+ const bob = await newAccount();
251
+ for (const role of ["owner", "admin", "member", "viewer"]) {
252
+ await command.create(orgRole(bob, role, org));
253
+ }
254
+ const error = await grpcError(() =>
255
+ command.create(orgRole(bob, "editor", org)),
256
+ );
257
+ expect(error.code).toBe(Code.InvalidArgument);
258
+ expect(error.rawMessage).toBe(
259
+ roleNotGrantableMessage("editor", "organization", [
260
+ "owner",
261
+ "admin",
262
+ "member",
263
+ "viewer",
264
+ ]),
265
+ );
266
+ });
267
+
268
+ it("revokeOrgAccess empties the account's rows on that organization only", async () => {
269
+ const org = await newOrganization();
270
+ const other = await newOrganization();
271
+ const bob = await newAccount();
272
+ await command.create(orgRole(bob, "admin", org));
273
+ await command.create(orgRole(bob, "viewer", org));
274
+ await command.create(orgRole(bob, "member", other));
275
+
276
+ await command.revokeOrgAccess({
277
+ identityAccountId: bob,
278
+ organizationId: org,
279
+ });
280
+
281
+ const onOrg = await query.getPrincipalResourceRoles({
282
+ principal: create(ApiResourceRefSchema, {
283
+ kind: "identity_account",
284
+ id: bob,
285
+ }),
286
+ resource: create(ApiResourceRefSchema, {
287
+ kind: "organization",
288
+ id: org,
289
+ }),
290
+ });
291
+ const onOther = await query.getPrincipalResourceRoles({
292
+ principal: create(ApiResourceRefSchema, {
293
+ kind: "identity_account",
294
+ id: bob,
295
+ }),
296
+ resource: create(ApiResourceRefSchema, {
297
+ kind: "organization",
298
+ id: other,
299
+ }),
300
+ });
301
+ expect(onOrg.roles).toEqual([]);
302
+ expect(onOther.roles.map((role) => role.code)).toEqual(["member"]);
303
+ });
304
+
305
+ it("delete of an absent triple answers the default instance (Java's contract)", async () => {
306
+ const org = await newOrganization();
307
+ const deleted = await command.delete(
308
+ orgRole("ida_nobody", "member", org),
309
+ );
310
+ expect(deleted.metadata?.id ?? "").toBe("");
311
+ });
312
+
313
+ it("get answers the row by id and the byte-pinned NOT_FOUND copy for an unknown id", async () => {
314
+ const org = await newOrganization();
315
+ const bob = await newAccount();
316
+ const created = await command.create(orgRole(bob, "member", org));
317
+ const got = await query.get({ value: created.metadata?.id ?? "" });
318
+ expect(got.spec).toEqual(created.spec);
319
+ const error = await grpcError(() =>
320
+ query.get({ value: "iamp_00000000000000000000000000" }),
321
+ );
322
+ expect(error.code).toBe(Code.NotFound);
323
+ expect(error.rawMessage).toBe(
324
+ policyNotFoundMessage("iamp_00000000000000000000000000"),
325
+ );
326
+ });
327
+ });
328
+
329
+ describe("the grant scope: open source grants on organizations only (Q-OR-3)", () => {
330
+ it("a grant on a kind whose kind_meta lists no roles is INVALID_ARGUMENT with the cloud's system-managed copy — no edition grants on it, so no edition is named", async () => {
331
+ // The proto is read BEFORE the scope (Q-OR-3 refinement, 2026-09-13):
332
+ // an identity account is system-managed in the cloud too, and
333
+ // "served by the Enterprise and Cloud editions" would be a lie here.
334
+ const bob = await newAccount();
335
+ const error = await grpcError(() =>
336
+ command.create(
337
+ triple({ kind: "identity_account", id: bob }, "viewer", {
338
+ kind: "identity_account",
339
+ id: operatorId,
340
+ }),
341
+ ),
342
+ );
343
+ expect(error.code).toBe(Code.InvalidArgument);
344
+ expect(error.rawMessage).toBe(
345
+ noGrantableRolesMessage("identity_account"),
346
+ );
347
+ });
348
+
349
+ it("a grant on an agent is UNIMPLEMENTED with the edition named, never INVALID_ARGUMENT or INTERNAL", async () => {
350
+ const bob = await newAccount();
351
+ const error = await grpcError(() =>
352
+ command.create(
353
+ triple({ kind: "identity_account", id: bob }, "viewer", {
354
+ kind: "agent",
355
+ id: "agt_01hzzzzzzzzzzzzzzzzzzzzzzz",
356
+ }),
357
+ ),
358
+ );
359
+ expect(error.code).toBe(Code.Unimplemented);
360
+ expect(error.rawMessage).toBe(PER_RESOURCE_GRANTS_UNIMPLEMENTED_MESSAGE);
361
+ });
362
+ });
363
+
364
+ describe("the three system RPCs (Q-OR-7)", () => {
365
+ it("are PERMISSION_DENIED for a wire user, with the annotation's copy", async () => {
366
+ const org = await newOrganization();
367
+ const structural = triple(
368
+ { kind: "organization", id: org },
369
+ "organization",
370
+ { kind: "agent", id: "agt_01hzzzzzzzzzzzzzzzzzzzzzzz" },
371
+ );
372
+ const denied = await grpcError(() => command.bootstrapPolicy(structural));
373
+ expect(denied.code).toBe(Code.PermissionDenied);
374
+ expect(denied.rawMessage).toBe(
375
+ "unauthorized to bootstrap policy - can_bootstrap_iam permission required",
376
+ );
377
+ expect(
378
+ (
379
+ await grpcError(() =>
380
+ command.cleanupResourcePolicies(
381
+ create(ApiResourceRefSchema, { kind: "organization", id: org }),
382
+ ),
383
+ )
384
+ ).code,
385
+ ).toBe(Code.PermissionDenied);
386
+ expect(
387
+ (
388
+ await grpcError(() =>
389
+ command.bootstrapRevokeOrgAccess({
390
+ identityAccountId: operatorId,
391
+ organizationId: org,
392
+ }),
393
+ )
394
+ ).code,
395
+ ).toBe(Code.PermissionDenied);
396
+ });
397
+
398
+ it("serve the platform's own pipeline on the in-process transport, structural relations included", async () => {
399
+ const org = await newOrganization();
400
+ const structural = triple(
401
+ { kind: "organization", id: org },
402
+ "organization",
403
+ { kind: "agent", id: "agt_01hzzzzzzzzzzzzzzzzzzzzzzz" },
404
+ );
405
+
406
+ const seeded = await platform.bootstrapPolicy(structural);
407
+ expect(seeded.metadata?.id).toBe(policyIdFor(structural));
408
+ // A structural row never shows in the access list: only assignable roles do.
409
+ const access = await query.listResourceAccessByPrincipal({
410
+ resource: create(ApiResourceRefSchema, {
411
+ kind: "agent",
412
+ id: "agt_01hzzzzzzzzzzzzzzzzzzzzzzz",
413
+ }),
414
+ });
415
+ expect(access.entries).toEqual([]);
416
+
417
+ await platform.cleanupResourcePolicies(
418
+ create(ApiResourceRefSchema, {
419
+ kind: "agent",
420
+ id: "agt_01hzzzzzzzzzzzzzzzzzzzzzzz",
421
+ }),
422
+ );
423
+ const error = await grpcError(() =>
424
+ query.get({ value: policyIdFor(structural) }),
425
+ );
426
+ expect(error.code).toBe(Code.NotFound);
427
+ });
428
+ });
429
+
430
+ describe("checkMyPermission has one definition (Q-OR-8)", () => {
431
+ it("an enterprise-tiered kind answers false — the operator-only settings navigation stays hidden on open source", async () => {
432
+ const result = await query.checkMyPermission({
433
+ resource: create(ApiResourceRefSchema, {
434
+ kind: "platform",
435
+ id: "stigmer",
436
+ }),
437
+ relation: "can_manage_model_pricing",
438
+ });
439
+ expect(result.isAuthorized).toBe(false);
440
+ });
441
+
442
+ it("can_grant_access outside the grant scope answers false; on an organization it is the Authorizer's answer", async () => {
443
+ const org = await newOrganization();
444
+ const onAgent = await query.checkMyPermission({
445
+ resource: create(ApiResourceRefSchema, {
446
+ kind: "agent",
447
+ id: "agt_01hzzzzzzzzzzzzzzzzzzzzzzz",
448
+ }),
449
+ relation: "can_grant_access",
450
+ });
451
+ const onOrg = await query.checkMyPermission({
452
+ resource: create(ApiResourceRefSchema, {
453
+ kind: "organization",
454
+ id: org,
455
+ }),
456
+ relation: "can_grant_access",
457
+ });
458
+ const viewAgent = await query.checkMyPermission({
459
+ resource: create(ApiResourceRefSchema, {
460
+ kind: "agent",
461
+ id: "agt_01hzzzzzzzzzzzzzzzzzzzzzzz",
462
+ }),
463
+ relation: "can_view_access",
464
+ });
465
+ expect(onAgent.isAuthorized).toBe(false);
466
+ // The permissive single-team Authorizer allows everything it is asked.
467
+ expect(onOrg.isAuthorized).toBe(true);
468
+ expect(viewAgent.isAuthorized).toBe(true);
469
+ });
470
+
471
+ it("an unknown permission is INVALID_ARGUMENT, quoted", async () => {
472
+ const error = await grpcError(() =>
473
+ query.checkMyPermission({
474
+ resource: create(ApiResourceRefSchema, {
475
+ kind: "organization",
476
+ id: "acme",
477
+ }),
478
+ relation: "can_fly",
479
+ }),
480
+ );
481
+ expect(error.code).toBe(Code.InvalidArgument);
482
+ expect(error.rawMessage).toBe(unknownPermissionMessage("can_fly"));
483
+ });
484
+ });
485
+
486
+ describe("the tuple-half queries without a composed engine", () => {
487
+ it("answer UNIMPLEMENTED with the edition named — never INTERNAL", async () => {
488
+ const principal = create(ApiResourceRefSchema, {
489
+ kind: "identity_account",
490
+ id: operatorId,
491
+ });
492
+ const resource = create(ApiResourceRefSchema, {
493
+ kind: "organization",
494
+ id: "acme",
495
+ });
496
+ for (const call of [
497
+ () =>
498
+ query.checkAuthorization({
499
+ policy: triple(
500
+ { kind: "identity_account", id: operatorId },
501
+ "admin",
502
+ { kind: "organization", id: "acme" },
503
+ ),
504
+ }),
505
+ () =>
506
+ query.listAuthorizedResourceIds({
507
+ principal,
508
+ resourceKind: "organization",
509
+ relation: "can_view",
510
+ }),
511
+ () =>
512
+ query.listAuthorizedPrincipalIds({
513
+ resource,
514
+ principalKind: "identity_account",
515
+ relation: "can_view",
516
+ }),
517
+ () =>
518
+ query.checkMyPermission({
519
+ resource,
520
+ relation: "can_view",
521
+ contextualPolicies: [orgRole(operatorId, "viewer", "acme")],
522
+ }),
523
+ ]) {
524
+ const error = await grpcError(call);
525
+ expect(error.code).toBe(Code.Unimplemented);
526
+ expect(error.rawMessage).toContain(
527
+ AUTHORIZATION_QUERIES_UNIMPLEMENTED_MESSAGE,
528
+ );
529
+ }
530
+ });
531
+ });
532
+ });