@stigmer/server 3.15.2-dev.20260913223433 → 3.15.3-dev.20260914100347

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 (201) 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/policy-grant-scope.d.ts +50 -0
  102. package/dist/extensions/policy-grant-scope.d.ts.map +1 -0
  103. package/dist/extensions/policy-grant-scope.js +2 -0
  104. package/dist/extensions/policy-grant-scope.js.map +1 -0
  105. package/dist/extensions/registry.d.ts +23 -1
  106. package/dist/extensions/registry.d.ts.map +1 -1
  107. package/dist/extensions/registry.js +30 -0
  108. package/dist/extensions/registry.js.map +1 -1
  109. package/dist/extensions/resource-authorization.d.ts +63 -0
  110. package/dist/extensions/resource-authorization.d.ts.map +1 -1
  111. package/dist/index.d.ts +7 -1
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +2 -0
  114. package/dist/index.js.map +1 -1
  115. package/dist/pipeline/apiresource-meta.d.ts +56 -1
  116. package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
  117. package/dist/pipeline/apiresource-meta.js +155 -1
  118. package/dist/pipeline/apiresource-meta.js.map +1 -1
  119. package/dist/pipeline/interceptors/auth.d.ts +12 -0
  120. package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
  121. package/dist/pipeline/interceptors/auth.js +13 -1
  122. package/dist/pipeline/interceptors/auth.js.map +1 -1
  123. package/dist/pipeline/steps/authorize.d.ts +20 -5
  124. package/dist/pipeline/steps/authorize.d.ts.map +1 -1
  125. package/dist/pipeline/steps/authorize.js +37 -17
  126. package/dist/pipeline/steps/authorize.js.map +1 -1
  127. package/dist/pipeline/steps/defaults.d.ts +10 -0
  128. package/dist/pipeline/steps/defaults.d.ts.map +1 -1
  129. package/dist/pipeline/steps/defaults.js +39 -0
  130. package/dist/pipeline/steps/defaults.js.map +1 -1
  131. package/dist/store/port-contract.d.ts +61 -0
  132. package/dist/store/port-contract.d.ts.map +1 -0
  133. package/dist/store/port-contract.js +67 -0
  134. package/dist/store/port-contract.js.map +1 -0
  135. package/package.json +4 -4
  136. package/src/boot/compose.ts +107 -10
  137. package/src/domain/iampolicy/__tests__/access-lists.test.ts +238 -0
  138. package/src/domain/iampolicy/__tests__/constants.test.ts +388 -0
  139. package/src/domain/iampolicy/__tests__/controller.test.ts +960 -0
  140. package/src/domain/iampolicy/__tests__/display-resolver.test.ts +138 -0
  141. package/src/domain/iampolicy/__tests__/grant-path.test.ts +474 -0
  142. package/src/domain/iampolicy/__tests__/grant-scope.test.ts +61 -0
  143. package/src/domain/iampolicy/__tests__/iampolicy.test.ts +532 -0
  144. package/src/domain/iampolicy/__tests__/membership.test.ts +447 -0
  145. package/src/domain/iampolicy/__tests__/permissions.test.ts +41 -0
  146. package/src/domain/iampolicy/__tests__/resource-store.test.ts +165 -0
  147. package/src/domain/iampolicy/__tests__/role-lifecycle.test.ts +361 -0
  148. package/src/domain/iampolicy/__tests__/roles.test.ts +53 -0
  149. package/src/domain/iampolicy/__tests__/specs.test.ts +41 -0
  150. package/src/domain/iampolicy/__tests__/steps.test.ts +278 -0
  151. package/src/domain/iampolicy/__tests__/support.ts +195 -0
  152. package/src/domain/iampolicy/__tests__/wire-refusals.test.ts +117 -0
  153. package/src/domain/iampolicy/access-lists.ts +191 -0
  154. package/src/domain/iampolicy/constants.ts +243 -0
  155. package/src/domain/iampolicy/controller.ts +798 -0
  156. package/src/domain/iampolicy/display-resolver.ts +123 -0
  157. package/src/domain/iampolicy/grant-path.ts +270 -0
  158. package/src/domain/iampolicy/grant-scope.ts +36 -0
  159. package/src/domain/iampolicy/membership.ts +298 -0
  160. package/src/domain/iampolicy/permissions.ts +35 -0
  161. package/src/domain/iampolicy/resource-store.ts +186 -0
  162. package/src/domain/iampolicy/role-lifecycle.ts +132 -0
  163. package/src/domain/iampolicy/roles.ts +95 -0
  164. package/src/domain/iampolicy/specs.ts +53 -0
  165. package/src/domain/iampolicy/steps.ts +178 -0
  166. package/src/domain/iampolicy/store-contract.ts +450 -0
  167. package/src/domain/iampolicy/store.ts +103 -0
  168. package/src/domain/iampolicy/wire-refusals.ts +110 -0
  169. package/src/domain/identityaccount/__tests__/provisioning.test.ts +19 -7
  170. package/src/domain/identityaccount/__tests__/resolve.test.ts +113 -2
  171. package/src/domain/identityaccount/constants.ts +16 -31
  172. package/src/domain/identityaccount/controller.ts +65 -35
  173. package/src/domain/identityaccount/operator.ts +5 -5
  174. package/src/domain/identityaccount/provisioning.ts +43 -5
  175. package/src/domain/identityaccount/resolve.ts +43 -0
  176. package/src/domain/identityaccount/store-contract.ts +22 -70
  177. package/src/extensions/__tests__/composed-support.ts +87 -0
  178. package/src/extensions/__tests__/iam-policy-composed.test.ts +373 -0
  179. package/src/extensions/__tests__/iam-policy-points.test.ts +169 -0
  180. package/src/extensions/__tests__/identity-account-composed.test.ts +12 -78
  181. package/src/extensions/__tests__/identity.test.ts +62 -0
  182. package/src/extensions/__tests__/tier-truthfulness.test.ts +8 -0
  183. package/src/extensions/authorization-queries.ts +97 -0
  184. package/src/extensions/drivers.ts +49 -2
  185. package/src/extensions/identity.ts +21 -0
  186. package/src/extensions/policy-grant-scope.ts +50 -0
  187. package/src/extensions/registry.ts +62 -1
  188. package/src/extensions/resource-authorization.ts +65 -0
  189. package/src/index.ts +20 -0
  190. package/src/pipeline/__tests__/grantable-roles-for.test.ts +87 -0
  191. package/src/pipeline/__tests__/kind-by-enum-name.test.ts +107 -0
  192. package/src/pipeline/__tests__/kind-served-by-edition.test.ts +107 -0
  193. package/src/pipeline/apiresource-meta.ts +181 -1
  194. package/src/pipeline/interceptors/auth.ts +14 -1
  195. package/src/pipeline/steps/__tests__/authorize-annotation-completeness.test.ts +73 -3
  196. package/src/pipeline/steps/__tests__/authorize.test.ts +134 -9
  197. package/src/pipeline/steps/__tests__/derived-id.test.ts +49 -0
  198. package/src/pipeline/steps/authorize.ts +46 -21
  199. package/src/pipeline/steps/defaults.ts +42 -0
  200. package/src/store/__tests__/port-contract.test.ts +123 -0
  201. package/src/store/port-contract.ts +102 -0
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Pins the built-in role lifecycle in isolation (T01_0_plan.md §3a
3
+ * "role-lifecycle.ts"; T01_1_review.md Q-OR-6a): the
4
+ * ResourceAuthorizationLifecycle open source installs when no unit
5
+ * registers one, over the real grant path, a fake policy store and a fake
6
+ * account port. It is the ROW writer of the built-in authorization posture
7
+ * — the one arm that makes an organization's creator its owner without an
8
+ * administrator — and nothing else:
9
+ *
10
+ * onResourceCreated
11
+ * - `organization` with DIRECT attribution, for a caller whose identityId
12
+ * IS an account: one `owner` row for that account, stamped created_by
13
+ * the caller; the same event again writes nothing more and does not
14
+ * throw (the grant path's duplicate arm heals);
15
+ * - a caller whose identityId is no account — an idp-shaped subject the
16
+ * verifier admitted before any row existed, the in-process `internal`
17
+ * class, a runner — writes nothing: the membership rules heal the
18
+ * creator's ownership at first provisioning through the creator stamp
19
+ * (Q-OR-6b), so the guard loses nobody;
20
+ * - an organization whose event carries any other attribution writes
21
+ * nothing (the proto's owner_type is the truth; the driver follows it
22
+ * exactly as the cloud's tuple driver does);
23
+ * - every other kind writes nothing, DIRECT attribution or not: a
24
+ * per-resource owner row would be the per-resource grant the scope
25
+ * keeps Enterprise (Q-OR-3).
26
+ *
27
+ * onResourceDeleted
28
+ * - `organization` and `identity_account`: every row naming the deleted
29
+ * resource on EITHER side goes, through the grant path's bidirectional
30
+ * cleanupResource; every other kind is a no-op.
31
+ *
32
+ * The rest of the contract
33
+ * - onVisibilityChanged is a no-op; onDefaultInstanceLinked is not
34
+ * defined (absent = no structural link, the interface's own word);
35
+ * - onPolicyGranted / onPolicyRevoked are NOT defined: those hooks are a
36
+ * composed driver's tuple half, fired by the grant path, and the grant
37
+ * path's lifecycle is that driver (undefined in open source) — never
38
+ * this object, which sits ON TOP of the grant path. Acyclic by
39
+ * construction; this pin keeps it so;
40
+ * - a store fault propagates out of both resource hooks: the caller
41
+ * (pipeline/steps/authorization-tuples.ts) fails the create and logs
42
+ * the delete, and those semantics are the step's to pin, not this
43
+ * module's.
44
+ */
45
+ import { describe, expect, it } from "vitest";
46
+
47
+ import { create } from "@bufbuild/protobuf";
48
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
49
+ import { OwnerAttributionType } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/authorization_config_pb";
50
+ import { IdentityAccountSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
51
+ import { IdentityAccountProvisioningMode } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/enum_pb";
52
+
53
+ import type { CallerIdentity } from "../../../extensions/identity.js";
54
+ import type {
55
+ ResourceCreatedEvent,
56
+ ResourceDeletedEvent,
57
+ } from "../../../extensions/resource-authorization.js";
58
+ import { accountIdFor } from "../../identityaccount/constants.js";
59
+ import { fakeIdentityAccountStore } from "../../identityaccount/__tests__/support.js";
60
+ import { policyIdFor } from "../constants.js";
61
+ import { newIamPolicyGrantPath } from "../grant-path.js";
62
+ import { newBuiltInRoleLifecycle } from "../role-lifecycle.js";
63
+ import type { IamPolicyStore } from "../store.js";
64
+ import { fakeIamPolicyStore, orgRole, triple } from "./support.js";
65
+ import type { RecordedEvent } from "./support.js";
66
+
67
+ const ALICE_SUBJECT = "auth0|alice";
68
+ const ALICE = accountIdFor(ALICE_SUBJECT);
69
+ const STRANGER_SUBJECT = "auth0|stranger";
70
+ const AGENT = "agt_01hzzzzzzzzzzzzzzzzzzzzzzz";
71
+
72
+ const silentLogger = { debug() {}, info() {}, warn() {}, error() {} };
73
+
74
+ function caller(
75
+ identityId: string,
76
+ callerClass: CallerIdentity["callerClass"] = "user",
77
+ ): CallerIdentity {
78
+ return {
79
+ identityId,
80
+ callerClass,
81
+ issuer: "https://issuer.example.com",
82
+ rawToken: "",
83
+ };
84
+ }
85
+
86
+ /** The event the OSS step hands a driver after an organization row persists. */
87
+ function organizationCreated(
88
+ slug: string,
89
+ by: CallerIdentity,
90
+ ownerAttribution = OwnerAttributionType.DIRECT,
91
+ ): ResourceCreatedEvent {
92
+ return {
93
+ kind: ApiResourceKind.organization,
94
+ resourceId: slug,
95
+ orgId: "",
96
+ caller: by,
97
+ ownerAttribution,
98
+ requiresCreatorTuple: false,
99
+ parentLinks: [],
100
+ visibilityShapes: [],
101
+ };
102
+ }
103
+
104
+ function deleted(
105
+ kind: ApiResourceKind,
106
+ resourceId: string,
107
+ by: CallerIdentity,
108
+ ): ResourceDeletedEvent {
109
+ return { kind, resourceId, orgId: "", caller: by };
110
+ }
111
+
112
+ /** A lifecycle over the real grant path (no composed driver, the OSS shape), a fake store and one provisioned account. */
113
+ function lifecycleOver(
114
+ recorded: RecordedEvent[] = [],
115
+ store: IamPolicyStore = fakeIamPolicyStore(recorded),
116
+ ) {
117
+ const accounts = fakeIdentityAccountStore();
118
+ accounts.rows.set(
119
+ ALICE,
120
+ create(IdentityAccountSchema, {
121
+ apiVersion: "iam.stigmer.ai/v1",
122
+ kind: "IdentityAccount",
123
+ metadata: { id: ALICE, name: "alice@example.com" },
124
+ spec: {
125
+ idpId: ALICE_SUBJECT,
126
+ email: "alice@example.com",
127
+ provisioningMode: IdentityAccountProvisioningMode.direct,
128
+ },
129
+ }),
130
+ );
131
+ const grantPath = newIamPolicyGrantPath({
132
+ policies: store,
133
+ lifecycle: undefined,
134
+ logger: silentLogger,
135
+ });
136
+ return {
137
+ lifecycle: newBuiltInRoleLifecycle({ grantPath, accounts }),
138
+ grantPath,
139
+ store,
140
+ recorded,
141
+ };
142
+ }
143
+
144
+ describe("onResourceCreated: the creator owns the organization", () => {
145
+ it("writes one owner row for the caller's account, stamped created_by the caller", async () => {
146
+ const recorded: RecordedEvent[] = [];
147
+ const { lifecycle, store } = lifecycleOver(recorded);
148
+
149
+ await lifecycle.onResourceCreated(
150
+ organizationCreated("acme", caller(ALICE)),
151
+ );
152
+
153
+ const id = policyIdFor(orgRole(ALICE, "owner", "acme"));
154
+ const row = await store.findById(id);
155
+ expect(row?.spec).toEqual(orgRole(ALICE, "owner", "acme"));
156
+ expect(row?.status?.audit?.specAudit?.createdBy?.id).toBe(ALICE);
157
+ expect(recorded).toEqual([{ kind: "row-save", id }]);
158
+ });
159
+
160
+ it("the same event again writes nothing more and does not throw — the chains retry whole requests", async () => {
161
+ const recorded: RecordedEvent[] = [];
162
+ const { lifecycle } = lifecycleOver(recorded);
163
+ const event = organizationCreated("acme", caller(ALICE));
164
+
165
+ await lifecycle.onResourceCreated(event);
166
+ await expect(lifecycle.onResourceCreated(event)).resolves.toBeUndefined();
167
+
168
+ expect(recorded.filter((e) => e.kind === "row-save")).toHaveLength(1);
169
+ });
170
+
171
+ it.each([
172
+ ["an idp-shaped subject no account holds", caller(STRANGER_SUBJECT)],
173
+ ["the in-process internal class", caller("internal", "internal")],
174
+ ["a runner", caller("runner-1", "runner")],
175
+ ])(
176
+ "writes nothing for %s — the membership rules heal a real person's ownership at provisioning",
177
+ async (_label, by) => {
178
+ const recorded: RecordedEvent[] = [];
179
+ const { lifecycle } = lifecycleOver(recorded);
180
+
181
+ await lifecycle.onResourceCreated(organizationCreated("acme", by));
182
+
183
+ expect(recorded).toEqual([]);
184
+ },
185
+ );
186
+
187
+ it.each([
188
+ ["SELF", OwnerAttributionType.SELF],
189
+ ["INHERITED", OwnerAttributionType.INHERITED],
190
+ ["NONE", OwnerAttributionType.NONE],
191
+ ["UNSPECIFIED", OwnerAttributionType.UNSPECIFIED],
192
+ ])(
193
+ "writes nothing for an organization event with %s attribution — the proto's owner_type is the truth",
194
+ async (_label, attribution) => {
195
+ const recorded: RecordedEvent[] = [];
196
+ const { lifecycle } = lifecycleOver(recorded);
197
+
198
+ await lifecycle.onResourceCreated(
199
+ organizationCreated("acme", caller(ALICE), attribution),
200
+ );
201
+
202
+ expect(recorded).toEqual([]);
203
+ },
204
+ );
205
+
206
+ it.each([
207
+ ApiResourceKind.agent,
208
+ ApiResourceKind.workflow,
209
+ ApiResourceKind.session,
210
+ ApiResourceKind.identity_account,
211
+ ApiResourceKind.project,
212
+ ])(
213
+ "writes nothing for kind %s even under DIRECT attribution — a per-resource owner row is the grant the scope keeps Enterprise",
214
+ async (kind) => {
215
+ const recorded: RecordedEvent[] = [];
216
+ const { lifecycle } = lifecycleOver(recorded);
217
+
218
+ await lifecycle.onResourceCreated({
219
+ ...organizationCreated("x", caller(ALICE)),
220
+ kind,
221
+ resourceId: "res_1",
222
+ orgId: "acme",
223
+ });
224
+
225
+ expect(recorded).toEqual([]);
226
+ },
227
+ );
228
+ });
229
+
230
+ describe("onResourceDeleted: the rows die with the resource", () => {
231
+ it("an organization's deletion removes every row on it, as resource and as principal", async () => {
232
+ const recorded: RecordedEvent[] = [];
233
+ const { lifecycle, grantPath, store } = lifecycleOver(recorded);
234
+ await grantPath.grant(orgRole(ALICE, "owner", "acme"), caller(ALICE));
235
+ await grantPath.grant(orgRole(ALICE, "member", "acme"), caller(ALICE));
236
+ await grantPath.grant(
237
+ triple({ kind: "organization", id: "acme" }, "organization", {
238
+ kind: "agent",
239
+ id: AGENT,
240
+ }),
241
+ caller(ALICE),
242
+ );
243
+ const unrelated = orgRole(ALICE, "member", "globex");
244
+ await grantPath.grant(unrelated, caller(ALICE));
245
+ recorded.length = 0;
246
+
247
+ await lifecycle.onResourceDeleted(
248
+ deleted(ApiResourceKind.organization, "acme", caller(ALICE)),
249
+ );
250
+
251
+ expect(
252
+ [...(await store.findByPrincipal("identity_account", ALICE))].map(
253
+ (p) => p.metadata?.id,
254
+ ),
255
+ ).toEqual([policyIdFor(unrelated)]);
256
+ expect(await store.findByPrincipal("organization", "acme")).toEqual([]);
257
+ expect(recorded.map((e) => e.kind)).toEqual([
258
+ "row-delete",
259
+ "row-delete",
260
+ "row-delete",
261
+ ]);
262
+ });
263
+
264
+ it("an identity account's deletion removes every row it holds anywhere", async () => {
265
+ const recorded: RecordedEvent[] = [];
266
+ const { lifecycle, grantPath, store } = lifecycleOver(recorded);
267
+ await grantPath.grant(orgRole(ALICE, "owner", "acme"), caller(ALICE));
268
+ await grantPath.grant(orgRole(ALICE, "member", "globex"), caller(ALICE));
269
+ const bob = accountIdFor("auth0|bob");
270
+ await grantPath.grant(orgRole(bob, "member", "acme"), caller(ALICE));
271
+ recorded.length = 0;
272
+
273
+ await lifecycle.onResourceDeleted(
274
+ deleted(ApiResourceKind.identity_account, ALICE, caller(ALICE)),
275
+ );
276
+
277
+ expect(await store.findByPrincipal("identity_account", ALICE)).toEqual([]);
278
+ expect(
279
+ (await store.findByPrincipal("identity_account", bob)).map(
280
+ (p) => p.metadata?.id,
281
+ ),
282
+ ).toEqual([policyIdFor(orgRole(bob, "member", "acme"))]);
283
+ });
284
+
285
+ it.each([
286
+ ApiResourceKind.agent,
287
+ ApiResourceKind.workflow,
288
+ ApiResourceKind.session,
289
+ ])(
290
+ "kind %s is a no-op — the rows of other kinds are a composed driver's to clean",
291
+ async (kind) => {
292
+ const recorded: RecordedEvent[] = [];
293
+ const { lifecycle, grantPath } = lifecycleOver(recorded);
294
+ await grantPath.grant(
295
+ triple({ kind: "identity_account", id: ALICE }, "viewer", {
296
+ kind: "agent",
297
+ id: AGENT,
298
+ }),
299
+ caller(ALICE),
300
+ );
301
+ recorded.length = 0;
302
+
303
+ await lifecycle.onResourceDeleted(deleted(kind, AGENT, caller(ALICE)));
304
+
305
+ expect(recorded).toEqual([]);
306
+ },
307
+ );
308
+ });
309
+
310
+ describe("the rest of the contract", () => {
311
+ it("onVisibilityChanged is a no-op and onDefaultInstanceLinked is not defined", async () => {
312
+ const recorded: RecordedEvent[] = [];
313
+ const { lifecycle } = lifecycleOver(recorded);
314
+
315
+ await lifecycle.onVisibilityChanged({
316
+ kind: ApiResourceKind.agent,
317
+ resourceId: AGENT,
318
+ orgId: "acme",
319
+ shapesToCreate: ["org-viewer"],
320
+ shapesToDelete: [],
321
+ });
322
+
323
+ expect(recorded).toEqual([]);
324
+ expect(lifecycle.onDefaultInstanceLinked).toBeUndefined();
325
+ });
326
+
327
+ it("defines neither policy hook — it sits on top of the grant path, never inside it", () => {
328
+ const { lifecycle } = lifecycleOver();
329
+ expect(lifecycle.onPolicyGranted).toBeUndefined();
330
+ expect(lifecycle.onPolicyRevoked).toBeUndefined();
331
+ });
332
+
333
+ it("a store fault on the owner row propagates out of onResourceCreated — the step fails the create", async () => {
334
+ const healthy = fakeIamPolicyStore();
335
+ const faulty: IamPolicyStore = {
336
+ ...healthy,
337
+ save: () => Promise.reject(new Error("disk full")),
338
+ };
339
+ const { lifecycle } = lifecycleOver([], faulty);
340
+
341
+ await expect(
342
+ lifecycle.onResourceCreated(organizationCreated("acme", caller(ALICE))),
343
+ ).rejects.toThrow("disk full");
344
+ });
345
+
346
+ it("a store fault during cleanup propagates out of onResourceDeleted — the step logs and continues", async () => {
347
+ const healthy = fakeIamPolicyStore();
348
+ const faulty: IamPolicyStore = {
349
+ ...healthy,
350
+ deleteById: () => Promise.reject(new Error("disk full")),
351
+ };
352
+ const { lifecycle, grantPath } = lifecycleOver([], faulty);
353
+ await grantPath.grant(orgRole(ALICE, "owner", "acme"), caller(ALICE));
354
+
355
+ await expect(
356
+ lifecycle.onResourceDeleted(
357
+ deleted(ApiResourceKind.organization, "acme", caller(ALICE)),
358
+ ),
359
+ ).rejects.toThrow("disk full");
360
+ });
361
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Pins roles.ts (20260913.01 slice 5): the five assignable roles' display
3
+ * metadata and the allowlist that keeps structural relations out of every
4
+ * access listing — moved from the cloud's iam/policy/roles.ts (the Java
5
+ * IamRoleMetadata port). The allowlist is proven by construction here:
6
+ * `participant` must be in it (channel-conversations DD-010), and a
7
+ * structural relation (`organization`, `owner_of`-style links) never is.
8
+ */
9
+ import { describe, expect, it } from "vitest";
10
+
11
+ import {
12
+ assignableRelations,
13
+ isAssignableRole,
14
+ roleInfoFromRelation,
15
+ } from "../roles.js";
16
+
17
+ describe("the assignable-role allowlist", () => {
18
+ it("is exactly the five roles a person can hold, in display order", () => {
19
+ expect(assignableRelations()).toEqual([
20
+ "owner",
21
+ "admin",
22
+ "member",
23
+ "viewer",
24
+ "participant",
25
+ ]);
26
+ });
27
+
28
+ it("names a role, never a structural relation", () => {
29
+ expect(isAssignableRole("admin")).toBe(true);
30
+ expect(isAssignableRole("participant")).toBe(true);
31
+ expect(isAssignableRole("organization")).toBe(false);
32
+ expect(isAssignableRole("creator")).toBe(false);
33
+ expect(isAssignableRole("")).toBe(false);
34
+ });
35
+ });
36
+
37
+ describe("roleInfoFromRelation", () => {
38
+ it("carries the Java display metadata: id and code are the relation, name and description the copy", () => {
39
+ const admin = roleInfoFromRelation("admin");
40
+ expect(admin.id).toBe("admin");
41
+ expect(admin.code).toBe("admin");
42
+ expect(admin.name).toBe("Administrator");
43
+ expect(admin.description).toBe(
44
+ "Administrative access with most permissions except ownership transfer",
45
+ );
46
+ });
47
+
48
+ it("answers the default instance for a relation that is no role (the Java fromRelationString contract)", () => {
49
+ const unknown = roleInfoFromRelation("organization");
50
+ expect(unknown.code).toBe("");
51
+ expect(unknown.name).toBe("");
52
+ });
53
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Pins the one production spelling of an organization-role spec (specs.ts;
3
+ * 20260913.01 slice 4) against the test support's twin, so the rows the
4
+ * built-in role lifecycle and the membership rules write are the rows
5
+ * every test in this domain reads back by derived id — and the zero role
6
+ * is refused before it can be hashed into a row.
7
+ */
8
+ import { describe, expect, it } from "vitest";
9
+
10
+ import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
11
+
12
+ import { policyIdFor } from "../constants.js";
13
+ import { organizationRole, relationOf } from "../specs.js";
14
+ import { orgRole } from "./support.js";
15
+
16
+ describe("organizationRole — the production spelling of an organization role", () => {
17
+ it.each([
18
+ [IamRole.owner, "owner"],
19
+ [IamRole.admin, "admin"],
20
+ [IamRole.member, "member"],
21
+ [IamRole.viewer, "viewer"],
22
+ ])(
23
+ "role %s spells its relation as %s and derives the same id as the test twin",
24
+ (role, relation) => {
25
+ const spec = organizationRole("ida_alice", role, "acme");
26
+ expect(spec).toEqual(orgRole("ida_alice", relation, "acme"));
27
+ expect(policyIdFor(spec)).toBe(
28
+ policyIdFor(orgRole("ida_alice", relation, "acme")),
29
+ );
30
+ },
31
+ );
32
+
33
+ it("the zero role is not a relation — refused, never hashed", () => {
34
+ expect(() => relationOf(IamRole.iam_role_unspecified)).toThrow(
35
+ "iam_role_unspecified is not a relation",
36
+ );
37
+ expect(() =>
38
+ organizationRole("ida_alice", IamRole.iam_role_unspecified, "acme"),
39
+ ).toThrow();
40
+ });
41
+ });