@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,191 @@
1
+ /**
2
+ * Access-list machinery (20260913.01 slice 5): the row-driven answer to
3
+ * "who has access, with which roles" behind `listResourceAccessByPrincipal`
4
+ * — the Java ResourceHierarchyResolver and PrincipalEnricher ports, moved
5
+ * as-is from the cloud's iam/policy/access-lists.ts so every edition
6
+ * renders an access list the same way over its own IamPolicyStore.
7
+ *
8
+ * The hierarchy walk climbs the resource's scope tuples (child →
9
+ * organization → platform, at most five steps, `platform` terminal) in the
10
+ * policy rows themselves — a structural link is a row whose principal is
11
+ * not a person and whose relation is not a role (store.ts `findScopeTuple`
12
+ * states the exclusions). Inherited grants carry their owning level so the
13
+ * console can say "inherited from organization". On open source no chain
14
+ * seeds scope links (a policy is the authorization record, not a protected
15
+ * resource — the IamPolicy chains splice no tuple step), so the walk is one
16
+ * level unless the platform's own pipeline bootstrapped a link.
17
+ *
18
+ * Display enrichment is a seam (`PrincipalDisplayResolver`) because the
19
+ * data lives in the identity-account domain; display-resolver.ts fills it
20
+ * over that domain's PORT. A principal the resolver does not answer for —
21
+ * any kind that is not an identity account, or an account whose row is
22
+ * gone — renders as kind/id with the id standing in as the display name,
23
+ * the Java `enrich()` fallback shape.
24
+ */
25
+ import { create } from "@bufbuild/protobuf";
26
+
27
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
28
+ import type { IamPolicy } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
29
+ import type {
30
+ ApiResourceRefView,
31
+ PrincipalAccess,
32
+ RoleGrant,
33
+ } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/io_pb";
34
+ import {
35
+ ApiResourceRefViewSchema,
36
+ PrincipalAccessSchema,
37
+ RoleGrantSchema,
38
+ } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/io_pb";
39
+ import { ApiResourceRefSchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
40
+
41
+ import { kindEnumName } from "../../pipeline/apiresource-meta.js";
42
+ import { assignableRelations, roleInfoFromRelation } from "./roles.js";
43
+ import type { IamPolicyStore } from "./store.js";
44
+
45
+ /** The Java resolver's bounds: ≤5 steps, platform terminates the walk. */
46
+ const MAX_HIERARCHY_DEPTH = 5;
47
+ const TERMINAL_KINDS: ReadonlySet<string> = new Set([
48
+ kindEnumName(ApiResourceKind.platform),
49
+ ]);
50
+
51
+ export interface HierarchyLevel {
52
+ readonly kind: string;
53
+ readonly id: string;
54
+ }
55
+
56
+ /**
57
+ * Child-first hierarchy via scope-tuple rows (Java resolve): index 0 is
58
+ * the resource itself; each later level is the previous one's structural
59
+ * scope parent.
60
+ */
61
+ export async function resolveHierarchy(
62
+ policies: IamPolicyStore,
63
+ resourceKind: string,
64
+ resourceId: string,
65
+ includeInherited: boolean,
66
+ ): Promise<ReadonlyArray<HierarchyLevel>> {
67
+ const hierarchy: HierarchyLevel[] = [{ kind: resourceKind, id: resourceId }];
68
+ if (!includeInherited) {
69
+ return hierarchy;
70
+ }
71
+ let currentKind = resourceKind;
72
+ let currentId = resourceId;
73
+ for (
74
+ let depth = 0;
75
+ depth < MAX_HIERARCHY_DEPTH && !TERMINAL_KINDS.has(currentKind);
76
+ depth++
77
+ ) {
78
+ const scopeTuple = await policies.findScopeTuple(currentKind, currentId);
79
+ const parent = scopeTuple?.spec?.principal;
80
+ if (parent === undefined) {
81
+ break;
82
+ }
83
+ currentKind = parent.kind;
84
+ currentId = parent.id;
85
+ hierarchy.push({ kind: currentKind, id: currentId });
86
+ }
87
+ return hierarchy;
88
+ }
89
+
90
+ /**
91
+ * The display-enrichment seam (the Java PrincipalEnricher's account half):
92
+ * the views for the identity-account ids it can resolve, keyed by id. Ids
93
+ * it does not answer for fall back below.
94
+ */
95
+ export interface PrincipalDisplayResolver {
96
+ resolveIdentityAccounts(
97
+ ids: ReadonlyArray<string>,
98
+ ): Promise<ReadonlyMap<string, ApiResourceRefView>>;
99
+ }
100
+
101
+ /**
102
+ * Groups assignable-role policies by principal in first-seen order (the
103
+ * Java LinkedHashMap contract), enriched where the resolver answers and
104
+ * the Java fallback shape (name = id) everywhere else.
105
+ */
106
+ export async function buildPrincipalAccessList(
107
+ policies: IamPolicyStore,
108
+ displayResolver: PrincipalDisplayResolver,
109
+ hierarchy: ReadonlyArray<HierarchyLevel>,
110
+ ): Promise<ReadonlyArray<PrincipalAccess>> {
111
+ interface GrantAtLevel {
112
+ readonly policy: IamPolicy;
113
+ readonly isInherited: boolean;
114
+ readonly level: HierarchyLevel;
115
+ }
116
+ const grants: GrantAtLevel[] = [];
117
+ for (const [index, level] of hierarchy.entries()) {
118
+ const rows = await policies.findByResourceWithRelations(
119
+ level.kind,
120
+ level.id,
121
+ assignableRelations(),
122
+ );
123
+ for (const policy of rows) {
124
+ grants.push({ policy, isInherited: index > 0, level });
125
+ }
126
+ }
127
+ if (grants.length === 0) {
128
+ return [];
129
+ }
130
+
131
+ const accountKind = kindEnumName(ApiResourceKind.identity_account);
132
+ const accountIds = grants
133
+ .filter((g) => g.policy.spec?.principal?.kind === accountKind)
134
+ .map((g) => g.policy.spec?.principal?.id ?? "");
135
+ const enriched = await displayResolver.resolveIdentityAccounts(accountIds);
136
+
137
+ const grouped = new Map<
138
+ string,
139
+ { principal: ApiResourceRefView; roles: RoleGrant[] }
140
+ >();
141
+ for (const grant of grants) {
142
+ const principal = grant.policy.spec?.principal;
143
+ if (principal === undefined) {
144
+ continue;
145
+ }
146
+ const key = `${principal.kind}:${principal.id}`;
147
+ let entry = grouped.get(key);
148
+ if (entry === undefined) {
149
+ entry = {
150
+ // The Java enrich() fallback shape for unresolved principals of
151
+ // ANY kind: kind/id with the id standing in as the display name.
152
+ principal:
153
+ enriched.get(principal.id) ??
154
+ create(ApiResourceRefViewSchema, {
155
+ kind: principal.kind,
156
+ id: principal.id,
157
+ name: principal.id,
158
+ }),
159
+ roles: [],
160
+ };
161
+ grouped.set(key, entry);
162
+ }
163
+ entry.roles.push(buildGrant(grant.policy, grant.isInherited, grant.level));
164
+ }
165
+
166
+ return [...grouped.values()].map((entry) =>
167
+ create(PrincipalAccessSchema, {
168
+ principal: entry.principal,
169
+ roles: entry.roles,
170
+ }),
171
+ );
172
+ }
173
+
174
+ function buildGrant(
175
+ policy: IamPolicy,
176
+ isInherited: boolean,
177
+ level: HierarchyLevel,
178
+ ): RoleGrant {
179
+ return create(RoleGrantSchema, {
180
+ role: roleInfoFromRelation(policy.spec?.relation ?? ""),
181
+ isInherited,
182
+ ...(isInherited
183
+ ? {
184
+ ownerResource: create(ApiResourceRefSchema, {
185
+ kind: level.kind,
186
+ id: level.id,
187
+ }),
188
+ }
189
+ : {}),
190
+ });
191
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * IamPolicy domain constants (20260913.01, T01_0_plan.md §3a;
3
+ * T01_1_review.md Q-OR-6b, Q-OR-9; slice 2 ruling Q-S2-1): the derived
4
+ * policy id and the one spelling of a triple it hashes, the kinds the
5
+ * legacy-creator rule scans, and the byte-pinned wire copy.
6
+ *
7
+ * The derived id (Q-OR-9). A policy's id is a pure function of its triple:
8
+ * `iamp_` + `derivedId` (pipeline/steps/defaults.ts, the encoder shared
9
+ * with the identity account) over the triple's canonical text. The text
10
+ * IS the tuple notation the contract's own comment documents
11
+ * (`principal.kind:principal.id#principal.relation@resource.kind:resource.id#relation`,
12
+ * spec.proto), the principal's `#` present even when its qualifier is
13
+ * empty, so the natural key has one spelling and the primary key is the
14
+ * one home of "one row per triple" in open source: two concurrent grants
15
+ * converge by primary key with no secondary index and no scan. The golden
16
+ * vectors in __tests__/constants.test.ts are wire-adjacent contract: a
17
+ * change here re-addresses every policy open source ever wrote.
18
+ *
19
+ * The text's one weakness (Q-S2-1): `ApiResourceRef` fields carry no
20
+ * character pattern, so an id or relation holding one of the three
21
+ * delimiters could spell another triple's text (`a#b` with an empty
22
+ * qualifier and `a` with qualifier `b#` read alike). `malformedTripleField`
23
+ * names such a field; `policyIdFor` refuses to hash such a spec; the grant
24
+ * path turns the same check into INVALID_ARGUMENT before any read or write.
25
+ * No legitimate reference carries a delimiter: every kind is an enum member
26
+ * name, every id a `prefix_ulid`, a slug or the public wildcard `*`, every
27
+ * relation an FGA relation name — and OpenFGA itself rejects `:` and `#`
28
+ * in ids, so the cloud loses nothing it could ever have written.
29
+ *
30
+ * `BLUEPRINT_KINDS` (Q-OR-6b) is the legacy-creator rule's whole scan: the
31
+ * kinds an admin authors. Sessions, executions, keys and memories are
32
+ * personal and stay with their creator by DD-002 rule 2; they never appear
33
+ * here.
34
+ *
35
+ * The copy moved from the cloud's iam/policy/handlers.ts as-is: the CLI,
36
+ * console and SDK show these sentences verbatim, so they are contract. The
37
+ * new sentences are the two edition refusals (the federation shape), the
38
+ * unknown permission, the unknown principal kind (the resource sentence's
39
+ * shape) and the malformed triple.
40
+ */
41
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
42
+ import type {
43
+ ApiResourceRef,
44
+ IamPolicySpec,
45
+ } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
46
+
47
+ import { kindEnumName } from "../../pipeline/apiresource-meta.js";
48
+ import { derivedId } from "../../pipeline/steps/defaults.js";
49
+
50
+ /** The kind's id prefix (kind_meta `iam_policy.id_prefix`). */
51
+ export const POLICY_ID_PREFIX = "iamp";
52
+
53
+ /**
54
+ * The proto's `api_version` const (api.proto). The cloud's Java-era rows
55
+ * carry `iam.stigmer.com/v1` and are read as they are (Q-OR-9); every row
56
+ * this domain writes carries the contract's value.
57
+ */
58
+ export const IAM_POLICY_API_VERSION = "iam.stigmer.ai/v1";
59
+ export const IAM_POLICY_KIND = "IamPolicy";
60
+
61
+ /** The three characters the canonical text is built from; none may appear inside a field. */
62
+ export const TRIPLE_DELIMITERS: ReadonlyArray<string> = [":", "#", "@"];
63
+
64
+ /** A triple with both references present — the shape the text and the id are defined over. */
65
+ interface CompleteTriple {
66
+ readonly principal: ApiResourceRef;
67
+ readonly resource: ApiResourceRef;
68
+ readonly relation: string;
69
+ }
70
+
71
+ function completeTriple(spec: IamPolicySpec): CompleteTriple {
72
+ if (spec.principal === undefined || spec.resource === undefined) {
73
+ throw new Error("policy spec must carry principal and resource");
74
+ }
75
+ return {
76
+ principal: spec.principal,
77
+ resource: spec.resource,
78
+ relation: spec.relation,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * The proto's tuple notation, the one spelling of a triple:
84
+ * `principal.kind:principal.id#principal.relation@resource.kind:resource.id#relation`.
85
+ * The principal's `#` is always present (an empty qualifier reads `#@`),
86
+ * so the same triple never has two texts.
87
+ */
88
+ export function canonicalTripleText(spec: IamPolicySpec): string {
89
+ const { principal, resource, relation } = completeTriple(spec);
90
+ return `${principal.kind}:${principal.id}#${principal.relation}@${resource.kind}:${resource.id}#${relation}`;
91
+ }
92
+
93
+ /**
94
+ * The first field, in the text's order, holding a delimiter — or
95
+ * `undefined` for a well-formed triple. Named in the proto's field paths so
96
+ * a refusal tells the caller exactly what to fix. Total over a spec
97
+ * missing a reference: that is `completeTriple`'s refusal, not this one.
98
+ */
99
+ export function malformedTripleField(spec: IamPolicySpec): string | undefined {
100
+ const fields: ReadonlyArray<readonly [path: string, value: string]> = [
101
+ ["principal.kind", spec.principal?.kind ?? ""],
102
+ ["principal.id", spec.principal?.id ?? ""],
103
+ ["principal.relation", spec.principal?.relation ?? ""],
104
+ ["resource.kind", spec.resource?.kind ?? ""],
105
+ ["resource.id", spec.resource?.id ?? ""],
106
+ ["relation", spec.relation],
107
+ ];
108
+ for (const [path, value] of fields) {
109
+ if (TRIPLE_DELIMITERS.some((delimiter) => value.includes(delimiter))) {
110
+ return path;
111
+ }
112
+ }
113
+ return undefined;
114
+ }
115
+
116
+ /**
117
+ * The derived policy id of a triple. Pure; refuses a spec missing either
118
+ * reference (a triple is the whole key) and a spec whose text would be
119
+ * ambiguous (Q-S2-1) — both are the assert behind the grant path's gate,
120
+ * which is where a caller hears INVALID_ARGUMENT.
121
+ */
122
+ export function policyIdFor(spec: IamPolicySpec): string {
123
+ const text = canonicalTripleText(spec);
124
+ const malformed = malformedTripleField(spec);
125
+ if (malformed !== undefined) {
126
+ throw new Error(malformedTripleMessage(malformed));
127
+ }
128
+ return derivedId(POLICY_ID_PREFIX, text);
129
+ }
130
+
131
+ /**
132
+ * The kinds an admin authors — the legacy-creator rule's scan (Q-OR-6b),
133
+ * in registry order. A caller who created one of these idp-shaped, before
134
+ * an account existed, becomes admin on first sign-in.
135
+ */
136
+ export const BLUEPRINT_KINDS: ReadonlyArray<ApiResourceKind> = [
137
+ ApiResourceKind.agent,
138
+ ApiResourceKind.workflow,
139
+ ApiResourceKind.skill,
140
+ ApiResourceKind.mcp_server,
141
+ ApiResourceKind.environment,
142
+ ApiResourceKind.schedule,
143
+ ];
144
+
145
+ /**
146
+ * The principal kinds a PERSON may grant a role to — the user `create`
147
+ * lane's grantee vocabulary (2026-09-14, session 9 ruling Q-S9-2; the
148
+ * security read's finding 41). A role names a person, so the one grantee
149
+ * is the identity account. A row whose principal is a RESOURCE
150
+ * (`organization:A#organization@platform_client:X`, `#managed_org`) is a
151
+ * structural link, and structural links are `bootstrapPolicy`'s — the
152
+ * platform's own lane, which skips role validation by design.
153
+ *
154
+ * Why the rule is load-bearing: the read side treats every principal
155
+ * outside this vocabulary as a structural parent (`findScopeTuple`,
156
+ * resource-store.ts, which derives its exclusion FROM this constant so the
157
+ * two cannot drift), and the hierarchy walk follows it. Without the arm, a
158
+ * person with `can_grant_access` on organization B could write
159
+ * `organization:A#member@organization:B` and B's Members page, asked for
160
+ * inherited access, would list A's people. The cloud's `create` had the
161
+ * same gap; the store's exclusion also keeps the cloud's Java-era `team`,
162
+ * which is not an ApiResourceKind and so no wire spec can name.
163
+ */
164
+ export const USER_GRANT_PRINCIPAL_KINDS: ReadonlyArray<ApiResourceKind> = [
165
+ ApiResourceKind.identity_account,
166
+ ];
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Byte-pinned copy (the cloud handlers' sentences, moved as-is).
170
+ // ---------------------------------------------------------------------------
171
+
172
+ /** get on a missing policy (NOT_FOUND). */
173
+ export function policyNotFoundMessage(id: string): string {
174
+ return `IAM policy not found: ${id}`;
175
+ }
176
+
177
+ /** A grant on a kind whose kind_meta lists no grantable roles (INVALID_ARGUMENT, every edition). */
178
+ export function noGrantableRolesMessage(kind: string): string {
179
+ return `No roles can be granted on resource kind '${kind}'. Role assignments for this resource kind are system-managed.`;
180
+ }
181
+
182
+ /** A role the kind does not list (INVALID_ARGUMENT); the grantable list in kind_meta order. */
183
+ export function roleNotGrantableMessage(
184
+ role: string,
185
+ kind: string,
186
+ grantable: ReadonlyArray<string>,
187
+ ): string {
188
+ return `Role '${role}' cannot be granted on resource kind '${kind}'. Grantable roles: [${grantable.join(", ")}]`;
189
+ }
190
+
191
+ /** checkMyPermission for a caller with no identity (UNAUTHENTICATED). */
192
+ export const AUTHENTICATION_REQUIRED_MESSAGE =
193
+ "Authentication required to check permissions";
194
+
195
+ /**
196
+ * A spec whose resource kind is not an ApiResourceKind member name
197
+ * (INVALID_ARGUMENT; the cloud's `kindFromSpecString` sentence). The grant
198
+ * path is where a permissive Authorizer's caller hears it; an enforcing
199
+ * Authorizer denies the unknown kind at position 1 first (slice 1, ruling 2).
200
+ */
201
+ export function unknownResourceKindMessage(kind: string): string {
202
+ return `Unknown resource kind: '${kind}'`;
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // New copy (this entry's refusals).
207
+ // ---------------------------------------------------------------------------
208
+
209
+ /**
210
+ * A grant on a kind outside the composed grant scope (UNIMPLEMENTED) — the
211
+ * federation and kind-registry degrade-by-probe posture: the sentence a
212
+ * person can act on, naming the editions that serve it.
213
+ */
214
+ export const PER_RESOURCE_GRANTS_UNIMPLEMENTED_MESSAGE =
215
+ "per-resource access grants are served by the Enterprise and Cloud editions";
216
+
217
+ /** The tuple-half queries on a composition without an AuthorizationQueryEngine (UNIMPLEMENTED). */
218
+ export const AUTHORIZATION_QUERIES_UNIMPLEMENTED_MESSAGE =
219
+ "authorization queries are served by the Enterprise and Cloud editions";
220
+
221
+ /** checkMyPermission with a relation that is no IamPermission name (INVALID_ARGUMENT). */
222
+ export function unknownPermissionMessage(relation: string): string {
223
+ return `unknown permission '${relation}'`;
224
+ }
225
+
226
+ /** A spec whose principal kind is not an ApiResourceKind member name (INVALID_ARGUMENT). */
227
+ export function unknownPrincipalKindMessage(kind: string): string {
228
+ return `Unknown principal kind: '${kind}'`;
229
+ }
230
+
231
+ /**
232
+ * A user grant whose principal is not a person (INVALID_ARGUMENT; Q-S9-2),
233
+ * in the role sentence's shape: the offending kind, then the vocabulary.
234
+ */
235
+ export function principalNotGrantableMessage(kind: string): string {
236
+ const grantable = USER_GRANT_PRINCIPAL_KINDS.map((k) => kindEnumName(k));
237
+ return `Principal kind '${kind}' cannot be granted a role. Grantable principal kinds: [${grantable.join(", ")}]`;
238
+ }
239
+
240
+ /** A triple field holding a canonical-text delimiter (INVALID_ARGUMENT; Q-S2-1). */
241
+ export function malformedTripleMessage(field: string): string {
242
+ return `policy ${field} must not contain ':', '#' or '@'`;
243
+ }