@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,123 @@
1
+ /**
2
+ * The access-list display resolver (20260913.01 slice 5; Q-S5-4, Q-S5-8):
3
+ * the account half of the Java PrincipalEnricher — batch-loads identity
4
+ * accounts through the identity-account domain's PORT (`findByIds`, one
5
+ * row per distinct id, the port's own line) and renders the
6
+ * ApiResourceRefView shape access listings carry: display name, email,
7
+ * avatar, identity origin. Moved from the cloud's iam/account/
8
+ * display-resolver.ts; built by the IamPolicy controller over the accounts
9
+ * port it already holds, never composed as a seam — the data is a domain
10
+ * READ, the same in every edition.
11
+ *
12
+ * The display-name precedence is `first + last > first > last >
13
+ * metadata.name > email`. The `metadata.name` arm is this domain's one
14
+ * deliberate divergence from the Java code (Q-S5-4): the trusted-local
15
+ * operator account carries its display name THERE and empty first/last
16
+ * names (identityaccount/operator.ts), while the cloud names every
17
+ * provisioned account by its EMAIL and fills first/last from userinfo
18
+ * (identityaccount/provisioning.ts; the DefaultAccountName step) — so on
19
+ * the cloud this arm and the email arm answer the same string, and on a
20
+ * laptop the Members row reads the name the operator configured instead
21
+ * of an address. `email` stays as the last resort for a row whose name
22
+ * was never defaulted.
23
+ *
24
+ * The federated provider label stays empty (the cloud's 3C provider
25
+ * display-name lookup never landed in the shared shape); direct and
26
+ * machine accounts carry the "Stigmer" label, the Java constant.
27
+ */
28
+ import { create } from "@bufbuild/protobuf";
29
+
30
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
31
+ import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
32
+ import { IdentityAccountProvisioningMode } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/enum_pb";
33
+ import type { ApiResourceRefView } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/io_pb";
34
+ import {
35
+ ApiResourceRefViewSchema,
36
+ IdentityOriginSchema,
37
+ } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/io_pb";
38
+
39
+ import { kindEnumName } from "../../pipeline/apiresource-meta.js";
40
+ import type { IdentityAccountStore } from "../identityaccount/store.js";
41
+ import type { PrincipalDisplayResolver } from "./access-lists.js";
42
+
43
+ /** The Java STIGMER_PROVIDER_LABEL constant. */
44
+ const STIGMER_PROVIDER_LABEL = "Stigmer";
45
+
46
+ /** The one read enrichment needs from the identity-account port. */
47
+ export type AccountsByIds = Pick<IdentityAccountStore, "findByIds">;
48
+
49
+ export function newAccountDisplayResolver(
50
+ accounts: AccountsByIds,
51
+ ): PrincipalDisplayResolver {
52
+ return {
53
+ async resolveIdentityAccounts(
54
+ ids,
55
+ ): Promise<ReadonlyMap<string, ApiResourceRefView>> {
56
+ const rows = await accounts.findByIds(ids);
57
+ const resolved = new Map<string, ApiResourceRefView>();
58
+ for (const account of rows) {
59
+ const id = account.metadata?.id ?? "";
60
+ resolved.set(id, viewOf(id, account));
61
+ }
62
+ return resolved;
63
+ },
64
+ };
65
+ }
66
+
67
+ function viewOf(id: string, account: IdentityAccount): ApiResourceRefView {
68
+ const spec = account.spec;
69
+ return create(ApiResourceRefViewSchema, {
70
+ kind: kindEnumName(ApiResourceKind.identity_account),
71
+ id,
72
+ name: displayNameOf(
73
+ spec?.firstName ?? "",
74
+ spec?.lastName ?? "",
75
+ account.metadata?.name ?? "",
76
+ spec?.email ?? "",
77
+ ),
78
+ email: spec?.email ?? "",
79
+ avatar: spec?.pictureUrl ?? "",
80
+ identityOrigin: create(IdentityOriginSchema, {
81
+ provisioningMode:
82
+ spec?.provisioningMode ??
83
+ IdentityAccountProvisioningMode.identity_account_provisioning_mode_unspecified,
84
+ providerDisplayName: providerLabelOf(spec?.provisioningMode),
85
+ }),
86
+ });
87
+ }
88
+
89
+ /** The Java buildDisplayName precedence, with the account's own name before the email (Q-S5-4). */
90
+ function displayNameOf(
91
+ firstName: string,
92
+ lastName: string,
93
+ accountName: string,
94
+ email: string,
95
+ ): string {
96
+ if (firstName !== "" && lastName !== "") {
97
+ return `${firstName} ${lastName}`;
98
+ }
99
+ if (firstName !== "") {
100
+ return firstName;
101
+ }
102
+ if (lastName !== "") {
103
+ return lastName;
104
+ }
105
+ if (accountName !== "") {
106
+ return accountName;
107
+ }
108
+ return email;
109
+ }
110
+
111
+ function providerLabelOf(
112
+ mode: IdentityAccountProvisioningMode | undefined,
113
+ ): string {
114
+ switch (mode) {
115
+ case IdentityAccountProvisioningMode.direct:
116
+ case IdentityAccountProvisioningMode.machine:
117
+ return STIGMER_PROVIDER_LABEL;
118
+ default:
119
+ // federated / platform_client / unspecified carry no external
120
+ // provider label (the Java switch arms).
121
+ return "";
122
+ }
123
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * The ONE grant and revoke path of the IamPolicy domain (20260913.01,
3
+ * T01_0_plan.md §3a; T01_1_review.md Q-OR-1), moved from the cloud's
4
+ * iam/policy/service.ts into open source so every edition writes and
5
+ * deletes a policy row the same way. Its callers: the IamPolicy command
6
+ * controller (user grants and the three system RPCs), the built-in role
7
+ * lifecycle (the organization creator's `owner` row; cleanup on delete)
8
+ * and the membership rules (first-sign-in roles). Nothing else builds,
9
+ * saves or deletes a policy row; a composition reaches this path as
10
+ * in-process RPCs, never through an exported constructor.
11
+ *
12
+ * The two cloud#425 ordering invariants live here, nowhere else:
13
+ * CREATE: row before tuple — a crash between the two leaves
14
+ * row-without-tuple (granted on paper, denied in practice), healed by
15
+ * the caller's natural retry (the tuple write deliberately runs on
16
+ * duplicates too — the inline heal) or the boot backfill.
17
+ * DELETE: tuple before row — a crash leaves row+tuple (revoke failed,
18
+ * retry works); the reverse order was the fail-open incident class
19
+ * ("revoke access" silently becoming "keep access forever").
20
+ * "Tuple" here is whatever the composed ResourceAuthorizationLifecycle does
21
+ * in `onPolicyGranted` and `onPolicyRevoked`; with none composed, or one
22
+ * without the two optional hooks, the path writes rows and notifies
23
+ * nothing (the OSS posture: rows are the record).
24
+ *
25
+ * The row for a triple is found BY TRIPLE, never by derived id. Open
26
+ * source's rows carry the derived id (constants.ts policyIdFor), so the
27
+ * two lookups would agree here; the cloud's Java-era rows carry random
28
+ * `iamp_<ulid>` ids and must revoke and deduplicate through this same
29
+ * code, so `findByTriple` (the pair's rows filtered by relation) is the
30
+ * one definition of "the row for this triple" and `findById` serves the
31
+ * `get` RPC alone. On the OSS adapter that read is one scan of the kind
32
+ * (resource-store.ts); §4 measures it.
33
+ *
34
+ * The path is the one writer, so it refuses before it writes (Q-S2-1;
35
+ * slice 1 ruling 2): before any read, write or hook, `grant` and
36
+ * `revokeBySpec` run the domain's wire refusals (wire-refusals.ts — a spec
37
+ * whose resource or principal kind is not an ApiResourceKind member name,
38
+ * or whose fields hold a canonical-text delimiter, is INVALID_ARGUMENT
39
+ * with the pinned copy; slice 5 made that module the one home of the rule
40
+ * the controller and the ValidateGrantableRole step share). The
41
+ * controller refuses the same kinds BEFORE position 1 (Q-S6-1), so a wire
42
+ * caller never reaches this check; it stands for the callers that enter
43
+ * here without the controller — the built-in lifecycle and the membership
44
+ * rules — and is what keeps a garbage row out of the store and a garbage
45
+ * spec away from a composition's tuple delete from every door.
46
+ * `cleanupResource` and `revokeOrgAccess` take refs and ids their callers
47
+ * read from rows, so they do not refuse.
48
+ *
49
+ * The row this path builds: the proto's apiVersion const and kind, the
50
+ * derived id, the spec as given, and the caller's audit stamp through the
51
+ * platform's one stamper (setAuditFieldsForCreate) — the cloud's
52
+ * `buildNewPolicy` stamped neither org nor creator; this path stamps the
53
+ * creator and, like the cloud, no `metadata.org`, because a policy may
54
+ * span organizations and none owns it.
55
+ */
56
+ import { create } from "@bufbuild/protobuf";
57
+
58
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
59
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
60
+ import type { IamPolicy } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
61
+ import { IamPolicySchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
62
+ import type {
63
+ ApiResourceRef,
64
+ IamPolicySpec,
65
+ } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
66
+
67
+ import type { Logger } from "../../boot/logger.js";
68
+ import type { CallerIdentity } from "../../extensions/identity.js";
69
+ import type { ResourceAuthorizationLifecycle } from "../../extensions/resource-authorization.js";
70
+ import { kindEnumName } from "../../pipeline/apiresource-meta.js";
71
+ import { setAuditFieldsForCreate } from "../../pipeline/steps/defaults.js";
72
+ import {
73
+ IAM_POLICY_API_VERSION,
74
+ IAM_POLICY_KIND,
75
+ policyIdFor,
76
+ } from "./constants.js";
77
+ import { DuplicatePolicyError } from "./store.js";
78
+ import type { IamPolicyStore } from "./store.js";
79
+ import { refsOf, requireWellFormedTriple } from "./wire-refusals.js";
80
+
81
+ export interface GrantResult {
82
+ readonly policy: IamPolicy;
83
+ /** True when the triple was already held: no row was written, the hook still fired. */
84
+ readonly duplicate: boolean;
85
+ }
86
+
87
+ export interface IamPolicyGrantPath {
88
+ /**
89
+ * Grant the triple as `caller`: gate, find by triple, write the row when
90
+ * absent (the caller's audit stamp), then `onPolicyGranted` — on the
91
+ * duplicate arm too. A hook throw fails the grant with the row in place.
92
+ */
93
+ grant(spec: IamPolicySpec, caller: CallerIdentity): Promise<GrantResult>;
94
+ /**
95
+ * Revoke the triple: gate, find by triple, `onPolicyRevoked` (with the
96
+ * row, or with the spec alone on the absent arm), then delete the row.
97
+ * A hook throw leaves row and tuple. Answers the revoked row, or
98
+ * `undefined` when there was none.
99
+ */
100
+ revokeBySpec(spec: IamPolicySpec): Promise<IamPolicy | undefined>;
101
+ /**
102
+ * Revoke every row naming the ref as its resource, then every row naming
103
+ * it as its principal, each through the revoke order; a row that is both
104
+ * is revoked once. Nothing to clean is a no-op.
105
+ */
106
+ cleanupResource(ref: ApiResourceRef): Promise<void>;
107
+ /** Revoke every relation the account holds directly on the organization. */
108
+ revokeOrgAccess(
109
+ identityAccountId: string,
110
+ organizationId: string,
111
+ ): Promise<void>;
112
+ }
113
+
114
+ export interface IamPolicyGrantPathDeps {
115
+ readonly policies: IamPolicyStore;
116
+ /** The composed driver, if any; a driver without the two policy hooks is the same as none. */
117
+ readonly lifecycle: ResourceAuthorizationLifecycle | undefined;
118
+ readonly logger: Logger;
119
+ }
120
+
121
+ export function newIamPolicyGrantPath(
122
+ deps: IamPolicyGrantPathDeps,
123
+ ): IamPolicyGrantPath {
124
+ const { policies, lifecycle, logger } = deps;
125
+
126
+ /** The row holding this exact triple — the pair's rows, filtered by relation. */
127
+ async function findByTriple(
128
+ spec: IamPolicySpec,
129
+ ): Promise<IamPolicy | undefined> {
130
+ const { principal, resource } = refsOf(spec);
131
+ const onPair = await policies.findByPrincipalAndResource(
132
+ principal.kind,
133
+ principal.id,
134
+ resource.kind,
135
+ resource.id,
136
+ );
137
+ return onPair.find(
138
+ (policy) =>
139
+ policy.spec !== undefined &&
140
+ policy.spec.relation === spec.relation &&
141
+ policy.spec.principal?.relation === principal.relation,
142
+ );
143
+ }
144
+
145
+ /** The revoke order for one row: the hook, then the delete. */
146
+ async function revokeRow(policy: IamPolicy): Promise<void> {
147
+ const spec = policy.spec;
148
+ if (spec === undefined) {
149
+ throw new Error(
150
+ `policy ${policy.metadata?.id ?? "?"} has no spec — corrupt row`,
151
+ );
152
+ }
153
+ await lifecycle?.onPolicyRevoked?.({ spec, policy });
154
+ const id = policy.metadata?.id ?? "";
155
+ await policies.deleteById(id);
156
+ logger.info("iam policy revoked", fieldsOf(id, spec));
157
+ }
158
+
159
+ return {
160
+ async grant(spec, caller): Promise<GrantResult> {
161
+ requireWellFormedTriple(spec);
162
+ let policy = await findByTriple(spec);
163
+ let duplicate = policy !== undefined;
164
+ if (policy === undefined) {
165
+ const fresh = buildRow(spec, caller);
166
+ try {
167
+ await policies.save(fresh);
168
+ policy = fresh;
169
+ logger.info("iam policy granted", fieldsOf(policyIdFor(spec), spec));
170
+ } catch (error) {
171
+ if (!(error instanceof DuplicatePolicyError)) {
172
+ throw error;
173
+ }
174
+ // Lost the race to another writer of the same triple: the
175
+ // winner's row is the grant, exactly as if it had been found
176
+ // above. A winner that cannot be read back means the refusal
177
+ // was not a race — propagate it.
178
+ const winner = await findByTriple(spec);
179
+ if (winner === undefined) {
180
+ throw error;
181
+ }
182
+ policy = winner;
183
+ duplicate = true;
184
+ }
185
+ }
186
+ if (duplicate) {
187
+ logger.debug(
188
+ "iam policy already held",
189
+ fieldsOf(policy.metadata?.id ?? "", spec),
190
+ );
191
+ }
192
+ // Deliberately unconditional (cloud#425): a duplicate re-grant is the
193
+ // inline heal for a row whose tuple never landed.
194
+ await lifecycle?.onPolicyGranted?.({ policy, duplicate });
195
+ return { policy, duplicate };
196
+ },
197
+
198
+ async revokeBySpec(spec): Promise<IamPolicy | undefined> {
199
+ requireWellFormedTriple(spec);
200
+ const existing = await findByTriple(spec);
201
+ if (existing === undefined) {
202
+ // No row: a composition still deletes the bare tuple (one written
203
+ // before the row mirror existed); converges the store either way.
204
+ await lifecycle?.onPolicyRevoked?.({ spec, policy: undefined });
205
+ return undefined;
206
+ }
207
+ await revokeRow(existing);
208
+ return existing;
209
+ },
210
+
211
+ async cleanupResource(ref): Promise<void> {
212
+ const asResource = await policies.findByResource(ref.kind, ref.id);
213
+ const asPrincipal = await policies.findByPrincipal(ref.kind, ref.id);
214
+ for (const policy of distinctById([...asResource, ...asPrincipal])) {
215
+ await revokeRow(policy);
216
+ }
217
+ },
218
+
219
+ async revokeOrgAccess(identityAccountId, organizationId): Promise<void> {
220
+ const direct = await policies.findByPrincipalAndResource(
221
+ kindEnumName(ApiResourceKind.identity_account),
222
+ identityAccountId,
223
+ kindEnumName(ApiResourceKind.organization),
224
+ organizationId,
225
+ );
226
+ for (const policy of direct) {
227
+ await revokeRow(policy);
228
+ }
229
+ },
230
+ };
231
+ }
232
+
233
+ /** The row for a fresh grant: the contract's identity strings, the derived id, the caller's stamp. */
234
+ function buildRow(spec: IamPolicySpec, caller: CallerIdentity): IamPolicy {
235
+ const policy = create(IamPolicySchema, {
236
+ apiVersion: IAM_POLICY_API_VERSION,
237
+ kind: IAM_POLICY_KIND,
238
+ metadata: create(ApiResourceMetadataSchema, { id: policyIdFor(spec) }),
239
+ spec,
240
+ });
241
+ setAuditFieldsForCreate(IamPolicySchema, policy, caller);
242
+ return policy;
243
+ }
244
+
245
+ /** One row per id, at its first position. */
246
+ function distinctById(
247
+ policies: ReadonlyArray<IamPolicy>,
248
+ ): ReadonlyArray<IamPolicy> {
249
+ const seen = new Set<string>();
250
+ return policies.filter((policy) => {
251
+ const id = policy.metadata?.id ?? "";
252
+ if (seen.has(id)) {
253
+ return false;
254
+ }
255
+ seen.add(id);
256
+ return true;
257
+ });
258
+ }
259
+
260
+ /** The structured fields every log line carries — ids and kinds, never a token. */
261
+ function fieldsOf(id: string, spec: IamPolicySpec): Record<string, string> {
262
+ return {
263
+ policyId: id,
264
+ relation: spec.relation,
265
+ principalKind: spec.principal?.kind ?? "",
266
+ principalId: spec.principal?.id ?? "",
267
+ resourceKind: spec.resource?.kind ?? "",
268
+ resourceId: spec.resource?.id ?? "",
269
+ };
270
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Open source's PolicyGrantScope (20260913.01, T01_1_review.md Q-OR-3 and
3
+ * Q-OR-4): the ORGANIZATION grants the roles its `kind_meta` lists —
4
+ * owner, admin, member, viewer — and no other kind grants anything. This
5
+ * is what lights the console's Members page in every edition while a
6
+ * viewer on one agent, the per-resource grant, stays what the Enterprise
7
+ * and Cloud editions add by composing a wider scope
8
+ * (extensions/policy-grant-scope.ts carries the contract every scope is
9
+ * held to: narrows the proto, total over the enum, synchronous).
10
+ *
11
+ * Installed by the composition root when no unit registers
12
+ * `drivers.policyGrantScope` — the `newPermissiveSingleTeamAuthorizer`
13
+ * shape: the default lives with the domain that defines its semantics and
14
+ * is called at the `??` site, never held by the registry.
15
+ *
16
+ * The organization's roles are READ from the proto through
17
+ * `grantableRolesFor`, not written here: the four words the Members page
18
+ * shows and entry 3's authorizer enforces have one source, and a proto
19
+ * change is the only way to change them (grant-scope.test.ts pins the
20
+ * equality; grantable-roles-for.test.ts pins the four).
21
+ */
22
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
23
+
24
+ import type { PolicyGrantScope } from "../../extensions/policy-grant-scope.js";
25
+ import { grantableRolesFor } from "../../pipeline/apiresource-meta.js";
26
+
27
+ /** The open-source default: organization → its proto roles; every other kind → none. */
28
+ export function newOrganizationOnlyGrantScope(): PolicyGrantScope {
29
+ return {
30
+ grantableRoles(kind) {
31
+ return kind === ApiResourceKind.organization
32
+ ? grantableRolesFor(kind)
33
+ : [];
34
+ },
35
+ };
36
+ }