@stigmer/server 3.23.1-dev.20260923075205 → 3.25.0

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 (226) hide show
  1. package/dist/authorization/authorizer.d.ts +10 -4
  2. package/dist/authorization/authorizer.d.ts.map +1 -1
  3. package/dist/authorization/authorizer.js +11 -4
  4. package/dist/authorization/authorizer.js.map +1 -1
  5. package/dist/authorization/model/index.d.ts +4 -4
  6. package/dist/authorization/model/index.d.ts.map +1 -1
  7. package/dist/authorization/model/index.js +2 -0
  8. package/dist/authorization/model/index.js.map +1 -1
  9. package/dist/authorization/model/oauth_app.d.ts.map +1 -1
  10. package/dist/authorization/model/oauth_app.js +3 -2
  11. package/dist/authorization/model/oauth_app.js.map +1 -1
  12. package/dist/authorization/model/platform_client.d.ts +2 -0
  13. package/dist/authorization/model/platform_client.d.ts.map +1 -0
  14. package/dist/authorization/model/platform_client.js +31 -0
  15. package/dist/authorization/model/platform_client.js.map +1 -0
  16. package/dist/boot/compose.d.ts.map +1 -1
  17. package/dist/boot/compose.js +88 -5
  18. package/dist/boot/compose.js.map +1 -1
  19. package/dist/boot/route-shadowing.d.ts +25 -0
  20. package/dist/boot/route-shadowing.d.ts.map +1 -0
  21. package/dist/boot/route-shadowing.js +17 -0
  22. package/dist/boot/route-shadowing.js.map +1 -0
  23. package/dist/domain/iampolicy/membership.d.ts +6 -1
  24. package/dist/domain/iampolicy/membership.d.ts.map +1 -1
  25. package/dist/domain/iampolicy/membership.js +8 -2
  26. package/dist/domain/iampolicy/membership.js.map +1 -1
  27. package/dist/domain/identityaccount/constants.d.ts +13 -0
  28. package/dist/domain/identityaccount/constants.d.ts.map +1 -1
  29. package/dist/domain/identityaccount/constants.js +17 -0
  30. package/dist/domain/identityaccount/constants.js.map +1 -1
  31. package/dist/domain/identityaccount/controller.d.ts +3 -1
  32. package/dist/domain/identityaccount/controller.d.ts.map +1 -1
  33. package/dist/domain/identityaccount/controller.js +15 -6
  34. package/dist/domain/identityaccount/controller.js.map +1 -1
  35. package/dist/domain/identityaccount/operator.d.ts.map +1 -1
  36. package/dist/domain/identityaccount/operator.js +1 -0
  37. package/dist/domain/identityaccount/operator.js.map +1 -1
  38. package/dist/domain/identityaccount/provisioning.d.ts +18 -0
  39. package/dist/domain/identityaccount/provisioning.d.ts.map +1 -1
  40. package/dist/domain/identityaccount/provisioning.js +1 -0
  41. package/dist/domain/identityaccount/provisioning.js.map +1 -1
  42. package/dist/domain/identityaccount/resource-store.js +9 -5
  43. package/dist/domain/identityaccount/resource-store.js.map +1 -1
  44. package/dist/domain/identityaccount/steps.d.ts +11 -2
  45. package/dist/domain/identityaccount/steps.d.ts.map +1 -1
  46. package/dist/domain/identityaccount/steps.js +42 -4
  47. package/dist/domain/identityaccount/steps.js.map +1 -1
  48. package/dist/domain/identityaccount/store-contract.d.ts.map +1 -1
  49. package/dist/domain/identityaccount/store-contract.js +33 -0
  50. package/dist/domain/identityaccount/store-contract.js.map +1 -1
  51. package/dist/domain/identityaccount/store.d.ts +7 -5
  52. package/dist/domain/identityaccount/store.d.ts.map +1 -1
  53. package/dist/domain/identityaccount/store.js.map +1 -1
  54. package/dist/domain/platform/controller.d.ts +10 -0
  55. package/dist/domain/platform/controller.d.ts.map +1 -1
  56. package/dist/domain/platform/controller.js +2 -1
  57. package/dist/domain/platform/controller.js.map +1 -1
  58. package/dist/domain/platformclient/constants.d.ts +60 -0
  59. package/dist/domain/platformclient/constants.d.ts.map +1 -0
  60. package/dist/domain/platformclient/constants.js +78 -0
  61. package/dist/domain/platformclient/constants.js.map +1 -0
  62. package/dist/domain/platformclient/controller.d.ts +59 -0
  63. package/dist/domain/platformclient/controller.d.ts.map +1 -0
  64. package/dist/domain/platformclient/controller.js +191 -0
  65. package/dist/domain/platformclient/controller.js.map +1 -0
  66. package/dist/domain/platformclient/credentials.d.ts +13 -0
  67. package/dist/domain/platformclient/credentials.d.ts.map +1 -0
  68. package/dist/domain/platformclient/credentials.js +56 -0
  69. package/dist/domain/platformclient/credentials.js.map +1 -0
  70. package/dist/domain/platformclient/mint.d.ts +23 -0
  71. package/dist/domain/platformclient/mint.d.ts.map +1 -0
  72. package/dist/domain/platformclient/mint.js +254 -0
  73. package/dist/domain/platformclient/mint.js.map +1 -0
  74. package/dist/domain/platformclient/origin-guard.d.ts +9 -0
  75. package/dist/domain/platformclient/origin-guard.d.ts.map +1 -0
  76. package/dist/domain/platformclient/origin-guard.js +81 -0
  77. package/dist/domain/platformclient/origin-guard.js.map +1 -0
  78. package/dist/domain/platformclient/resource-store.d.ts +4 -0
  79. package/dist/domain/platformclient/resource-store.d.ts.map +1 -0
  80. package/dist/domain/platformclient/resource-store.js +117 -0
  81. package/dist/domain/platformclient/resource-store.js.map +1 -0
  82. package/dist/domain/platformclient/steps.d.ts +51 -0
  83. package/dist/domain/platformclient/steps.d.ts.map +1 -0
  84. package/dist/domain/platformclient/steps.js +339 -0
  85. package/dist/domain/platformclient/steps.js.map +1 -0
  86. package/dist/domain/platformclient/store-contract.d.ts +8 -0
  87. package/dist/domain/platformclient/store-contract.d.ts.map +1 -0
  88. package/dist/domain/platformclient/store-contract.js +208 -0
  89. package/dist/domain/platformclient/store-contract.js.map +1 -0
  90. package/dist/domain/platformclient/store.d.ts +58 -0
  91. package/dist/domain/platformclient/store.d.ts.map +1 -0
  92. package/dist/domain/platformclient/store.js +13 -0
  93. package/dist/domain/platformclient/store.js.map +1 -0
  94. package/dist/domain/platformclient/system-managed.d.ts +18 -0
  95. package/dist/domain/platformclient/system-managed.d.ts.map +1 -0
  96. package/dist/domain/platformclient/system-managed.js +80 -0
  97. package/dist/domain/platformclient/system-managed.js.map +1 -0
  98. package/dist/domain/platformclient/token-controller.d.ts +11 -0
  99. package/dist/domain/platformclient/token-controller.d.ts.map +1 -0
  100. package/dist/domain/platformclient/token-controller.js +32 -0
  101. package/dist/domain/platformclient/token-controller.js.map +1 -0
  102. package/dist/domain/platformclient/verifier.d.ts +10 -0
  103. package/dist/domain/platformclient/verifier.d.ts.map +1 -0
  104. package/dist/domain/platformclient/verifier.js +76 -0
  105. package/dist/domain/platformclient/verifier.js.map +1 -0
  106. package/dist/domain/workflow/agent-call-references.d.ts +60 -33
  107. package/dist/domain/workflow/agent-call-references.d.ts.map +1 -1
  108. package/dist/domain/workflow/agent-call-references.js +43 -23
  109. package/dist/domain/workflow/agent-call-references.js.map +1 -1
  110. package/dist/domain/workflow/registry/data/task-kind-registry.json +2 -2
  111. package/dist/encryption/key-manager.d.ts +20 -3
  112. package/dist/encryption/key-manager.d.ts.map +1 -1
  113. package/dist/encryption/key-manager.js +64 -42
  114. package/dist/encryption/key-manager.js.map +1 -1
  115. package/dist/extensions/caller-guards.d.ts +8 -3
  116. package/dist/extensions/caller-guards.d.ts.map +1 -1
  117. package/dist/extensions/drivers.d.ts +37 -0
  118. package/dist/extensions/drivers.d.ts.map +1 -1
  119. package/dist/extensions/guest-token-minting.d.ts +26 -0
  120. package/dist/extensions/guest-token-minting.d.ts.map +1 -0
  121. package/dist/extensions/guest-token-minting.js +2 -0
  122. package/dist/extensions/guest-token-minting.js.map +1 -0
  123. package/dist/extensions/identity.d.ts +6 -4
  124. package/dist/extensions/identity.d.ts.map +1 -1
  125. package/dist/extensions/identity.js.map +1 -1
  126. package/dist/extensions/registry.d.ts +30 -1
  127. package/dist/extensions/registry.d.ts.map +1 -1
  128. package/dist/extensions/registry.js +34 -1
  129. package/dist/extensions/registry.js.map +1 -1
  130. package/dist/index.d.ts +13 -1
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +6 -0
  133. package/dist/index.js.map +1 -1
  134. package/dist/pipeline/apiresource-labels.d.ts +10 -9
  135. package/dist/pipeline/apiresource-labels.d.ts.map +1 -1
  136. package/dist/pipeline/apiresource-labels.js +10 -9
  137. package/dist/pipeline/apiresource-labels.js.map +1 -1
  138. package/dist/pipeline/interceptors/auth.d.ts +14 -0
  139. package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
  140. package/dist/pipeline/interceptors/auth.js +24 -0
  141. package/dist/pipeline/interceptors/auth.js.map +1 -1
  142. package/dist/pipeline/steps/build-update-state.d.ts +11 -1
  143. package/dist/pipeline/steps/build-update-state.d.ts.map +1 -1
  144. package/dist/pipeline/steps/build-update-state.js +5 -2
  145. package/dist/pipeline/steps/build-update-state.js.map +1 -1
  146. package/dist/pipeline/steps/references.d.ts +30 -4
  147. package/dist/pipeline/steps/references.d.ts.map +1 -1
  148. package/dist/pipeline/steps/references.js +13 -1
  149. package/dist/pipeline/steps/references.js.map +1 -1
  150. package/dist/platformtoken/envelope.d.ts +100 -0
  151. package/dist/platformtoken/envelope.d.ts.map +1 -0
  152. package/dist/platformtoken/envelope.js +184 -0
  153. package/dist/platformtoken/envelope.js.map +1 -0
  154. package/dist/platformtoken/key-ring.d.ts +85 -0
  155. package/dist/platformtoken/key-ring.d.ts.map +1 -0
  156. package/dist/platformtoken/key-ring.js +164 -0
  157. package/dist/platformtoken/key-ring.js.map +1 -0
  158. package/package.json +6 -6
  159. package/src/authorization/README.md +1 -1
  160. package/src/authorization/__tests__/authorizer.test.ts +6 -2
  161. package/src/authorization/__tests__/wire-permissions.test.ts +1 -0
  162. package/src/authorization/authorizer.ts +11 -4
  163. package/src/authorization/model/__tests__/registry.test.ts +8 -1
  164. package/src/authorization/model/index.ts +6 -4
  165. package/src/authorization/model/oauth_app.ts +3 -2
  166. package/src/authorization/model/platform_client.ts +43 -0
  167. package/src/boot/__tests__/auth-enabled-composition.test.ts +2 -1
  168. package/src/boot/__tests__/route-shadowing.test.ts +67 -0
  169. package/src/boot/compose.ts +90 -5
  170. package/src/boot/route-shadowing.ts +47 -0
  171. package/src/domain/iampolicy/__tests__/membership.test.ts +7 -0
  172. package/src/domain/iampolicy/membership.ts +8 -2
  173. package/src/domain/identityaccount/__tests__/platform-client-accounts.test.ts +124 -0
  174. package/src/domain/identityaccount/__tests__/resource-store.test.ts +1 -0
  175. package/src/domain/identityaccount/constants.ts +20 -0
  176. package/src/domain/identityaccount/controller.ts +16 -4
  177. package/src/domain/identityaccount/operator.ts +1 -0
  178. package/src/domain/identityaccount/provisioning.ts +17 -0
  179. package/src/domain/identityaccount/resource-store.ts +9 -5
  180. package/src/domain/identityaccount/steps.ts +58 -3
  181. package/src/domain/identityaccount/store-contract.ts +51 -0
  182. package/src/domain/identityaccount/store.ts +7 -5
  183. package/src/domain/platform/__tests__/platform.test.ts +7 -1
  184. package/src/domain/platform/controller.ts +12 -1
  185. package/src/domain/platformclient/__tests__/controller.test.ts +397 -0
  186. package/src/domain/platformclient/__tests__/credentials.test.ts +46 -0
  187. package/src/domain/platformclient/__tests__/mint.test.ts +415 -0
  188. package/src/domain/platformclient/__tests__/resource-store.test.ts +140 -0
  189. package/src/domain/platformclient/__tests__/system-managed.test.ts +101 -0
  190. package/src/domain/platformclient/__tests__/verifier-and-guard.test.ts +207 -0
  191. package/src/domain/platformclient/constants.ts +120 -0
  192. package/src/domain/platformclient/controller.ts +449 -0
  193. package/src/domain/platformclient/credentials.ts +68 -0
  194. package/src/domain/platformclient/mint.ts +412 -0
  195. package/src/domain/platformclient/origin-guard.ts +117 -0
  196. package/src/domain/platformclient/resource-store.ts +150 -0
  197. package/src/domain/platformclient/steps.ts +472 -0
  198. package/src/domain/platformclient/store-contract.ts +318 -0
  199. package/src/domain/platformclient/store.ts +63 -0
  200. package/src/domain/platformclient/system-managed.ts +115 -0
  201. package/src/domain/platformclient/token-controller.ts +49 -0
  202. package/src/domain/platformclient/verifier.ts +106 -0
  203. package/src/domain/workflow/__tests__/agent-call-references.test.ts +178 -27
  204. package/src/domain/workflow/agent-call-references.ts +95 -48
  205. package/src/domain/workflow/registry/data/task-kind-registry.json +2 -2
  206. package/src/encryption/key-manager.ts +87 -44
  207. package/src/extensions/__tests__/extension-composition.test.ts +3 -2
  208. package/src/extensions/__tests__/platform-client-composed.test.ts +288 -0
  209. package/src/extensions/__tests__/registry.test.ts +4 -1
  210. package/src/extensions/__tests__/tier-truthfulness.test.ts +8 -0
  211. package/src/extensions/caller-guards.ts +8 -3
  212. package/src/extensions/drivers.ts +37 -0
  213. package/src/extensions/guest-token-minting.ts +32 -0
  214. package/src/extensions/identity.ts +6 -4
  215. package/src/extensions/registry.ts +77 -3
  216. package/src/index.ts +55 -0
  217. package/src/pipeline/apiresource-labels.ts +10 -9
  218. package/src/pipeline/interceptors/auth.ts +27 -0
  219. package/src/pipeline/steps/__tests__/references.test.ts +37 -1
  220. package/src/pipeline/steps/build-update-state.ts +5 -2
  221. package/src/pipeline/steps/references.ts +49 -5
  222. package/src/platformtoken/__tests__/envelope.test.ts +277 -0
  223. package/src/platformtoken/__tests__/key-ring.test.ts +205 -0
  224. package/src/platformtoken/envelope.ts +276 -0
  225. package/src/platformtoken/key-ring.ts +266 -0
  226. package/src/runnerauth/__tests__/runner-subject-composed.test.ts +9 -3
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Transcript of fga/model/iam/platform_client.fga — relation for relation
3
+ * the oauth_app shape: organization-scoped, an `owner` with no admin arm
4
+ * and a `viewer` that has one. A platform client holds credential material,
5
+ * so the model is restricted: its creator (the owner) edits, deletes,
6
+ * rotates and grants; the organization's admins may view it; ordinary
7
+ * members have no access at all, which is why listByOrg narrows through
8
+ * the list read scope. There is no public level.
9
+ */
10
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
11
+ import { PlatformClientSchema } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
12
+
13
+ import {
14
+ computed,
15
+ declareKind,
16
+ direct,
17
+ from,
18
+ objectOf,
19
+ union,
20
+ } from "./rewrite.js";
21
+
22
+ export const platformClientDeclaration = declareKind({
23
+ kind: ApiResourceKind.platform_client,
24
+ schema: PlatformClientSchema,
25
+ source: "fga/model/iam/platform_client.fga",
26
+ relations: [
27
+ ["organization", direct(objectOf("organization"))],
28
+ ["owner", direct(objectOf("identity_account"))],
29
+ [
30
+ "viewer",
31
+ union(
32
+ direct(objectOf("identity_account")),
33
+ computed("owner"),
34
+ from("admin", "organization"),
35
+ ),
36
+ ],
37
+ ["can_view", computed("viewer")],
38
+ ["can_edit", computed("owner")],
39
+ ["can_delete", computed("owner")],
40
+ ["can_grant_access", computed("owner")],
41
+ ["can_view_access", computed("viewer")],
42
+ ],
43
+ });
@@ -236,10 +236,11 @@ describe("the require-authentication posture on the wire", () => {
236
236
  );
237
237
  });
238
238
 
239
- it("is_public methods stay reachable tokenless (getServerInfo)", async () => {
239
+ it("is_public methods stay reachable tokenless (getServerInfo), and it reports the posture", async () => {
240
240
  const platform = createClient(PlatformQueryController, transportWith());
241
241
  const info = await platform.getServerInfo({});
242
242
  expect(info.edition).not.toBe("");
243
+ expect(info.authenticationRequired).toBe(true);
243
244
  });
244
245
 
245
246
  it("the gRPC health service stays reachable tokenless — a Kubernetes grpc probe survives the posture (stigmer#974)", async () => {
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Pins the routes stage's refusal of a shadowing registration: Connect's
3
+ * router lets the last registration of a path win silently, so an
4
+ * extension that registers a service the core already serves — or that
5
+ * another extension already registered — must fail boot, naming itself and
6
+ * whoever serves the path. Distinct services pass untouched, in order.
7
+ */
8
+ import { createConnectRouter } from "@connectrpc/connect";
9
+ import { describe, expect, it } from "vitest";
10
+
11
+ import { OAuthAppQueryController } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/query_pb";
12
+ import { PlatformClientQueryController } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/query_pb";
13
+ import { PlatformClientTokenController } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/token_pb";
14
+
15
+ import { registerExtensionServicesUnshadowed } from "../route-shadowing.js";
16
+
17
+ describe("registerExtensionServicesUnshadowed", () => {
18
+ it("refuses an extension service the core already serves, naming the unit and the path", () => {
19
+ const router = createConnectRouter();
20
+ router.service(PlatformClientTokenController, {});
21
+ expect(() =>
22
+ registerExtensionServicesUnshadowed(router, [
23
+ {
24
+ unit: "cloud-iam",
25
+ register: (r) => void r.service(PlatformClientTokenController, {}),
26
+ },
27
+ ]),
28
+ ).toThrow(
29
+ /extension 'cloud-iam' registers \/ai\.stigmer\.iam\.platformclient\.v1\.PlatformClientTokenController\/mintUserToken, which the core already serves/,
30
+ );
31
+ });
32
+
33
+ it("refuses a second extension registering what a first one did", () => {
34
+ const router = createConnectRouter();
35
+ expect(() =>
36
+ registerExtensionServicesUnshadowed(router, [
37
+ {
38
+ unit: "first",
39
+ register: (r) => void r.service(OAuthAppQueryController, {}),
40
+ },
41
+ {
42
+ unit: "second",
43
+ register: (r) =>
44
+ void r.rpc(OAuthAppQueryController.method.get, () => {
45
+ throw new Error("never called");
46
+ }),
47
+ },
48
+ ]),
49
+ ).toThrow(
50
+ /extension 'second' registers .*OAuthAppQueryController\/get, which extension 'first' already serves/,
51
+ );
52
+ });
53
+
54
+ it("registers distinct services from every unit", () => {
55
+ const router = createConnectRouter();
56
+ router.service(OAuthAppQueryController, {});
57
+ registerExtensionServicesUnshadowed(router, [
58
+ {
59
+ unit: "billing",
60
+ register: (r) => void r.service(PlatformClientQueryController, {}),
61
+ },
62
+ ]);
63
+ expect(router.handlers.map((handler) => handler.requestPath)).toContain(
64
+ "/ai.stigmer.iam.platformclient.v1.PlatformClientQueryController/listByOrg",
65
+ );
66
+ });
67
+ });
@@ -108,6 +108,11 @@ import { registerExecutionContextServices } from "../domain/executioncontext/con
108
108
  import { registerGitHubServices } from "../domain/github/controller.js";
109
109
  import { registerMemoryServices } from "../domain/memory/controller.js";
110
110
  import { registerOAuthAppServices } from "../domain/oauthapp/controller.js";
111
+ import { registerPlatformClientServices } from "../domain/platformclient/controller.js";
112
+ import { newPlatformClientOriginGuard } from "../domain/platformclient/origin-guard.js";
113
+ import { newResourcePlatformClientStore } from "../domain/platformclient/resource-store.js";
114
+ import { registerPlatformClientTokenService } from "../domain/platformclient/token-controller.js";
115
+ import { newPlatformClientTokenVerifier } from "../domain/platformclient/verifier.js";
111
116
  import { registerOrganizationServices } from "../domain/organization/controller.js";
112
117
  import { registerMcpServerServices } from "../domain/mcpserver/controller.js";
113
118
  import { registerPlatformServices } from "../domain/platform/controller.js";
@@ -144,6 +149,7 @@ import {
144
149
  import type { SecretCodec } from "../encryption/codec.js";
145
150
  import { getOrCreateNamedKey } from "../encryption/key-manager.js";
146
151
  import { V1_VERSION } from "../encryption/v1-codec.js";
152
+ import { resolvePlatformTokenKeys } from "../platformtoken/key-ring.js";
147
153
  import { registerActivityServices } from "../query/activity/controller.js";
148
154
  import { ActivityHandler } from "../query/activity/handler.js";
149
155
  import { registerSearchServices } from "../query/search/controller.js";
@@ -203,6 +209,7 @@ import {
203
209
  } from "../transport/console/handler.js";
204
210
  import { createRegistryLanes } from "../transport/registry/lanes.js";
205
211
  import { createUnifiedPortServer } from "../transport/server.js";
212
+ import { registerExtensionServicesUnshadowed } from "./route-shadowing.js";
206
213
  import type { ServerConfig } from "./config.js";
207
214
  import { createInProcessClients } from "./inprocess.js";
208
215
  import type { InProcessClients } from "./inprocess.js";
@@ -413,6 +420,14 @@ export async function composeServer(
413
420
  // bindings are what the IamPolicy controller registers over (routes).
414
421
  const iamPolicies =
415
422
  extensions.drivers.iamPolicyStore ?? newResourceIamPolicyStore(store);
423
+ // The PlatformClient store PORT, bound the same way
424
+ // (`drivers.platformClientStore`; open source's adapter over `store`
425
+ // when absent): the chains, the mint and the verifier's liveness read all
426
+ // go through this one binding, so a composed driver is followed
427
+ // everywhere by construction.
428
+ const platformClients =
429
+ extensions.drivers.platformClientStore ??
430
+ newResourcePlatformClientStore(store);
416
431
  const iamPolicyGrantPath = newIamPolicyGrantPath({
417
432
  policies: iamPolicies,
418
433
  lifecycle: extensions.drivers.resourceAuthorizationLifecycle,
@@ -690,6 +705,26 @@ export async function composeServer(
690
705
  writeVersionValue === "" ? DEFAULT_WRITE_VERSION : writeVersionValue,
691
706
  registeredCodecs: [...secretCodecs.keys()].sort(),
692
707
  });
708
+ // The platform-token key ring joins the FATAL side of the asymmetry:
709
+ // under an authentication posture a server that cannot sign or verify
710
+ // its own tokens would refuse every PlatformClient-minted caller, and an
711
+ // explicitly configured key that does not load is configuration the
712
+ // operator meant (the ladder's rule). No posture, no ring
713
+ // (resolvePlatformTokenKeys carries the four arms).
714
+ const platformTokenKeys = resolvePlatformTokenKeys({
715
+ requireAuthentication,
716
+ supplied: extensions.drivers.platformTokenKeys,
717
+ edition: extensions.edition,
718
+ });
719
+ logger.info("platform-token keys resolved", {
720
+ source:
721
+ platformTokenKeys === undefined
722
+ ? "none (no authentication posture)"
723
+ : extensions.drivers.platformTokenKeys !== undefined
724
+ ? "extension"
725
+ : "open-source ladder",
726
+ signing: platformTokenKeys?.signer !== undefined,
727
+ });
693
728
  // Stage: temporal (#18). Construction is sync and connection-free —
694
729
  // initialConnect/startWorkers/startHealthMonitor run in start(), all
695
730
  // NON-fatal (Go server.go: the server boots and serves with the engine
@@ -1284,6 +1319,34 @@ export async function composeServer(
1284
1319
  authorizer,
1285
1320
  authorizationLifecycle,
1286
1321
  });
1322
+ // PlatformClient, OAuthApp's inbound counterpart: served in every
1323
+ // composition over the store PORT bound above. The token service is
1324
+ // registered here too — mintGuestToken dispatches to the composed
1325
+ // capability — so nothing else may register it (route-shadowing.ts).
1326
+ // The mint writes end users through the identity-account create path
1327
+ // and the one grant path, as the platform client (mint.ts), and signs
1328
+ // with the ring the keys stage resolved (none without a posture: the
1329
+ // mint then refuses).
1330
+ registerPlatformClientServices(router, {
1331
+ clients: platformClients,
1332
+ store,
1333
+ logger,
1334
+ authorizer,
1335
+ authorizationLifecycle,
1336
+ listReadScope,
1337
+ });
1338
+ registerPlatformClientTokenService(router, {
1339
+ mint: {
1340
+ clients: platformClients,
1341
+ accounts: identityAccounts,
1342
+ createAccount: createIdentityAccount,
1343
+ grantPath: iamPolicyGrantPath,
1344
+ keys: platformTokenKeys,
1345
+ logger,
1346
+ now: () => new Date(),
1347
+ },
1348
+ guestTokenMinting: extensions.drivers.guestTokenMinting,
1349
+ });
1287
1350
  registerExecutionContextServices(router, {
1288
1351
  store,
1289
1352
  logger,
@@ -1549,6 +1612,7 @@ export async function composeServer(
1549
1612
  runnerAuthService: runnerCredentials,
1550
1613
  edition: extensions.edition,
1551
1614
  version: options.version ?? SERVER_VERSION,
1615
+ authenticationRequired: requireAuthentication,
1552
1616
  // The built-in `absent` answer installs here, at the consumer, when
1553
1617
  // no unit registered a license-status provider: open source and the
1554
1618
  // cloud hold no key, and only an Enterprise unit ever has one.
@@ -1559,10 +1623,9 @@ export async function composeServer(
1559
1623
  // Extension services register after the whole OSS set, inside the ONE
1560
1624
  // routes closure — so the serving router AND the in-process transport
1561
1625
  // see them by construction (blueprint §2a; DD-002's parity doctrine
1562
- // extends to extension services unchanged).
1563
- for (const registerExtensionServices of extensions.services) {
1564
- registerExtensionServices(router);
1565
- }
1626
+ // extends to extension services unchanged). A registration that would
1627
+ // shadow a route already served is a boot throw (route-shadowing.ts).
1628
+ registerExtensionServicesUnshadowed(router, extensions.services);
1566
1629
  };
1567
1630
  const inProcessWiring = createInProcessClients(routes, logger);
1568
1631
  inProcess = inProcessWiring.clients;
@@ -1610,6 +1673,18 @@ export async function composeServer(
1610
1673
  ...(requireAuthentication
1611
1674
  ? [newApiKeyIdentityVerifier({ store, accounts: identityAccounts })]
1612
1675
  : []),
1676
+ // The PlatformClient user-token lane rides the ring, which the keys
1677
+ // stage resolves only under an authentication posture. It sits before
1678
+ // `oidc` for the runner-subject verifier's reason (stigmer#1137): the
1679
+ // OIDC verifier would claim the platform token and throw on it.
1680
+ ...(platformTokenKeys !== undefined
1681
+ ? [
1682
+ newPlatformClientTokenVerifier({
1683
+ keys: platformTokenKeys,
1684
+ clients: platformClients,
1685
+ }),
1686
+ ]
1687
+ : []),
1613
1688
  ...(authorizationPosture === "built-in"
1614
1689
  ? [
1615
1690
  newRunnerSubjectIdentityVerifier({
@@ -1652,6 +1727,16 @@ export async function composeServer(
1652
1727
  ? [`extension '${extensions.requireAuthentication.declaredBy}'`]
1653
1728
  : []),
1654
1729
  ];
1730
+ // Caller guards: open source's own first, then the extensions' — the
1731
+ // rule the verifier list follows ("OSS entries first"). The origin guard
1732
+ // exists only where PlatformClient tokens can be presented, under the
1733
+ // posture that verifies them.
1734
+ const callerGuards = [
1735
+ ...(platformTokenKeys !== undefined
1736
+ ? [newPlatformClientOriginGuard({ clients: platformClients, logger })]
1737
+ : []),
1738
+ ...extensions.callerGuards,
1739
+ ];
1655
1740
  logger.info("authentication posture resolved", {
1656
1741
  posture: requireAuthentication ? "required" : "trusted-local",
1657
1742
  source: postureSources.length > 0 ? postureSources.join(" + ") : "none",
@@ -1680,7 +1765,7 @@ export async function composeServer(
1680
1765
  logger,
1681
1766
  createVerifierChainInterceptor(
1682
1767
  identityVerifiers,
1683
- extensions.callerGuards,
1768
+ callerGuards,
1684
1769
  logger,
1685
1770
  requireAuthentication,
1686
1771
  ),
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The routes stage's one refusal: an extension may not register a route
3
+ * the router already serves.
4
+ *
5
+ * Connect's router keeps every registration, and both the serving adapter
6
+ * and the in-process transport build their path table with a plain map
7
+ * set, so a second registration of `/<service>/<method>` silently wins.
8
+ * Extensions register after the whole core set (compose.ts), so a
9
+ * composition that re-registers a service the core has come to serve —
10
+ * the moment a cloud domain moves into open source and the cloud's pin
11
+ * advances without deleting its own handlers — would shadow the core with
12
+ * no error, and serve from whatever store its old handlers read. Refusing
13
+ * at boot turns that into the loudest possible failure; the fix is always
14
+ * the domain's driver point, never a second service.
15
+ *
16
+ * Checked per request path, because `router.service` registers every
17
+ * method of a descriptor (the unimplemented ones answering UNIMPLEMENTED)
18
+ * and `router.rpc` registers one; two extensions colliding with each
19
+ * other are refused the same way.
20
+ */
21
+ import type { ConnectRouter } from "@connectrpc/connect";
22
+
23
+ import type { ResolvedServiceRegistration } from "../extensions/registry.js";
24
+
25
+ /** Runs each extension registration, refusing any path already routed. */
26
+ export function registerExtensionServicesUnshadowed(
27
+ router: ConnectRouter,
28
+ registrations: ReadonlyArray<ResolvedServiceRegistration>,
29
+ ): void {
30
+ // Request path → who serves it, as the refusal names them.
31
+ const servedBy = new Map<string, string>(
32
+ router.handlers.map((handler) => [handler.requestPath, "the core"]),
33
+ );
34
+ for (const { unit, register } of registrations) {
35
+ const before = router.handlers.length;
36
+ register(router);
37
+ for (const handler of router.handlers.slice(before)) {
38
+ const server = servedBy.get(handler.requestPath);
39
+ if (server !== undefined) {
40
+ throw new Error(
41
+ `extension '${unit}' registers ${handler.requestPath}, which ${server} already serves — a route is never registered twice; an edition changes a core domain's behaviour through its driver points`,
42
+ );
43
+ }
44
+ servedBy.set(handler.requestPath, `extension '${unit}'`);
45
+ }
46
+ }
47
+ }
@@ -639,6 +639,13 @@ describe("membership rules", () => {
639
639
  IdentityAccountProvisioningMode.machine;
640
640
  expect(isPersonAccount(machineMode)).toBe(false);
641
641
  });
642
+
643
+ it("a platform-client end user is not: the mint grants it its client's role and nothing else", () => {
644
+ const endUser = account("stgm_pc|acme|user-7", OPERATOR_EMAIL);
645
+ endUser.spec!.provisioningMode =
646
+ IdentityAccountProvisioningMode.platform_client;
647
+ expect(isPersonAccount(endUser)).toBe(false);
648
+ });
642
649
  });
643
650
 
644
651
  describe("ensureRolesForExistingAccounts", () => {
@@ -195,14 +195,20 @@ export function isPersonStamp(stamp: string): boolean {
195
195
  * `user`-class gate, for the reconciliation, which has a row and no
196
196
  * caller. A machine account (`is_machine_account`, or the `machine`
197
197
  * provisioning mode) earns no role, exactly as a `machine`-class caller
198
- * earns none at the hook.
198
+ * earns none at the hook. Nor does a platform-client end user: it is a
199
+ * person on its platform's product, not on this install — the mint grants
200
+ * it exactly its client's auto-grant role on the owning organization and
201
+ * never runs these rules, and the reconciliation must not either, or every
202
+ * product user would become a member of every organization (and the
203
+ * operator-email arm would read an email the platform asserted).
199
204
  */
200
205
  export function isPersonAccount(account: IdentityAccount): boolean {
201
206
  const spec = account.spec;
202
207
  return (
203
208
  spec !== undefined &&
204
209
  !spec.isMachineAccount &&
205
- spec.provisioningMode !== IdentityAccountProvisioningMode.machine
210
+ spec.provisioningMode !== IdentityAccountProvisioningMode.machine &&
211
+ spec.provisioningMode !== IdentityAccountProvisioningMode.platform_client
206
212
  );
207
213
  }
208
214
 
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Pins the identity-account create path's platform-client arm over the OSS
3
+ * adapter on sqlite:
4
+ * - `platform_client` provisioning writes the mode, the owning
5
+ * organization and the derived id, with the actor the caller named;
6
+ * - the `stgm_pc|` subject namespace is reserved in both directions: a
7
+ * direct account refuses it INVALID_ARGUMENT before anything is written,
8
+ * and a platform-client account without it is the caller's bug;
9
+ * - the direct lookups never answer the platform-client account, while the
10
+ * any-mode lookup (the mint's) does.
11
+ */
12
+ import { Code, ConnectError } from "@connectrpc/connect";
13
+ import { create } from "@bufbuild/protobuf";
14
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
15
+
16
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
17
+ import { IdentityAccountProvisioningMode } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/enum_pb";
18
+ import { IdentityAccountSpecSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/spec_pb";
19
+
20
+ import { silentLogger } from "../../../extensions/__tests__/composed-support.js";
21
+ import { serverActingFor } from "../../../pipeline/interceptors/auth.js";
22
+ import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
23
+ import { tempStore } from "../../../store/sqlite/__tests__/support.js";
24
+ import type { TempStore } from "../../../store/sqlite/__tests__/support.js";
25
+ import { accountIdFor, reservedSubjectMessage } from "../constants.js";
26
+ import { newCreateAccountPath } from "../controller.js";
27
+ import type { CreateAccount } from "../provisioning.js";
28
+ import { newResourceIdentityAccountStore } from "../resource-store.js";
29
+ import type { IdentityAccountStore } from "../store.js";
30
+
31
+ let temp: TempStore;
32
+ let accounts: IdentityAccountStore;
33
+ let createAccount: CreateAccount;
34
+
35
+ beforeEach(() => {
36
+ temp = tempStore();
37
+ accounts = newResourceIdentityAccountStore(temp.store);
38
+ createAccount = newCreateAccountPath(
39
+ {
40
+ accounts,
41
+ logger: silentLogger,
42
+ authorizer: newPermissiveSingleTeamAuthorizer(),
43
+ authorizationLifecycle: undefined,
44
+ },
45
+ ApiResourceKind.identity_account,
46
+ );
47
+ });
48
+
49
+ afterEach(() => {
50
+ temp.cleanup();
51
+ });
52
+
53
+ const SUBJECT = "stgm_pc|acme|user-7";
54
+ const CLIENT = serverActingFor("pcl_dashboard");
55
+
56
+ async function refusal(promise: Promise<unknown>): Promise<ConnectError> {
57
+ try {
58
+ await promise;
59
+ } catch (error) {
60
+ if (error instanceof ConnectError) return error;
61
+ throw error;
62
+ }
63
+ throw new Error("expected a ConnectError refusal");
64
+ }
65
+
66
+ describe("the create path's platform-client arm", () => {
67
+ it("writes the mode, the owning organization and the derived id, stamped as the client", async () => {
68
+ const account = await createAccount(
69
+ {
70
+ name: "pat@example.com",
71
+ spec: create(IdentityAccountSpecSchema, {
72
+ idpId: SUBJECT,
73
+ email: "pat@example.com",
74
+ }),
75
+ provisioning: { mode: "platform_client", org: "acme" },
76
+ },
77
+ CLIENT,
78
+ );
79
+ expect(account.metadata?.id).toBe(accountIdFor(SUBJECT));
80
+ expect(account.metadata?.org).toBe("acme");
81
+ expect(account.spec?.provisioningMode).toBe(
82
+ IdentityAccountProvisioningMode.platform_client,
83
+ );
84
+ expect(account.status?.audit?.specAudit?.createdBy?.id).toBe(
85
+ "pcl_dashboard",
86
+ );
87
+
88
+ expect(await accounts.findDirectByIdpId(SUBJECT)).toBeUndefined();
89
+ expect(await accounts.findDirectByEmail("pat@example.com")).toBeUndefined();
90
+ expect((await accounts.findByIdpId(SUBJECT))?.metadata?.id).toBe(
91
+ accountIdFor(SUBJECT),
92
+ );
93
+ });
94
+
95
+ it("refuses a direct account under the reserved subject prefix, writing nothing", async () => {
96
+ const denied = await refusal(
97
+ createAccount(
98
+ {
99
+ name: "impostor",
100
+ spec: create(IdentityAccountSpecSchema, { idpId: SUBJECT }),
101
+ provisioning: { mode: "direct" },
102
+ },
103
+ CLIENT,
104
+ ),
105
+ );
106
+ expect(denied.code).toBe(Code.InvalidArgument);
107
+ expect(denied.rawMessage).toBe(reservedSubjectMessage(SUBJECT));
108
+ expect(await accounts.findByIdpId(SUBJECT)).toBeUndefined();
109
+ });
110
+
111
+ it("refuses a platform-client account without the reserved prefix as a server fault", async () => {
112
+ const denied = await refusal(
113
+ createAccount(
114
+ {
115
+ name: "stray",
116
+ spec: create(IdentityAccountSpecSchema, { idpId: "auth0|stray" }),
117
+ provisioning: { mode: "platform_client", org: "acme" },
118
+ },
119
+ CLIENT,
120
+ ),
121
+ );
122
+ expect(denied.code).toBe(Code.Internal);
123
+ });
124
+ });
@@ -54,6 +54,7 @@ const CONTRACT_CASE_NAMES = [
54
54
  "a disconnected store is an infrastructure fault, never 'not found'",
55
55
  "two concurrent saves of one subject end in fulfilments and DuplicateAccountErrors only, and the winner is readable by subject",
56
56
  "findDirectByIdpId and findDirectByEmail never answer a federated account, while findByIdpId (any mode) does",
57
+ "findDirectByIdpId and findDirectByEmail never answer a platform-client account, while findByIdpId (any mode) does",
57
58
  "findByIds answers one row per distinct id, in first-occurrence order; an empty request answers an empty list",
58
59
  "update of an unknown id is a no-op: no row appears",
59
60
  "deleteById of an unknown id resolves",
@@ -110,6 +110,26 @@ export function isMachineSubject(idpId: string): boolean {
110
110
  return idpId.endsWith(MACHINE_ACCOUNT_SUFFIX);
111
111
  }
112
112
 
113
+ /**
114
+ * The subject namespace of PlatformClient end users:
115
+ * `stgm_pc|<org>|<external_user_id>` (the composite every client of one
116
+ * organization resolves a user to; domain/platformclient/mint.ts builds
117
+ * it). Reserved in both directions, because derived account ids make every
118
+ * subject one namespace: a direct account may not take a `stgm_pc|`
119
+ * subject (an issuer emitting one would otherwise provision an account the
120
+ * mint then resolves), and the mint refuses a `stgm_pc|` account in any
121
+ * other mode.
122
+ */
123
+ export const PLATFORM_CLIENT_SUBJECT_PREFIX = "stgm_pc|";
124
+
125
+ export function isPlatformClientSubject(idpId: string): boolean {
126
+ return idpId.startsWith(PLATFORM_CLIENT_SUBJECT_PREFIX);
127
+ }
128
+
129
+ export function reservedSubjectMessage(idpId: string): string {
130
+ return `subject '${idpId}' uses the '${PLATFORM_CLIENT_SUBJECT_PREFIX}' prefix reserved for accounts a platform client provisions`;
131
+ }
132
+
113
133
  // ---------------------------------------------------------------------------
114
134
  // Byte-pinned copy (the cloud handlers' sentences, moved as-is).
115
135
  // ---------------------------------------------------------------------------
@@ -108,6 +108,7 @@ import {
108
108
  import type {
109
109
  AccountCreatedHook,
110
110
  CreateAccount,
111
+ AccountProvisioning,
111
112
  CreateAccountInput,
112
113
  DirectAccountProvisioner,
113
114
  } from "./provisioning.js";
@@ -230,6 +231,7 @@ async function runCreateChain(
230
231
  account: IdentityAccount,
231
232
  caller: CallerIdentity,
232
233
  kind: ApiResourceKind,
234
+ provisioning: AccountProvisioning,
233
235
  ): Promise<IdentityAccount> {
234
236
  const reqCtx = new RequestContext(
235
237
  IdentityAccountSchema,
@@ -254,7 +256,7 @@ async function runCreateChain(
254
256
  .addStep(newCheckDuplicateStep(deps.accounts))
255
257
  .addStep(newBuildNewStateStep())
256
258
  .addStep(newGuardReservedLabelsStep(deps.authorizer))
257
- .addStep(newAssignBackendFieldsStep())
259
+ .addStep(newAssignBackendFieldsStep(provisioning))
258
260
  .addStep(newPersistNewAccountStep(deps.accounts))
259
261
  .addStep(
260
262
  newCreateAuthorizationTuplesStep(
@@ -270,7 +272,9 @@ async function runCreateChain(
270
272
  /**
271
273
  * The create path as its in-process callers see it (provisioning.ts
272
274
  * `CreateAccount`): the input becomes the resource envelope the chain
273
- * expects, tagged with the kind this controller serves.
275
+ * expects, tagged with the kind this controller serves. A platform-client
276
+ * account carries its owning organization, as the cloud has always
277
+ * written it; a direct account belongs to no organization.
274
278
  */
275
279
  export function newCreateAccountPath(
276
280
  deps: CreateAccountPathDeps,
@@ -282,11 +286,17 @@ export function newCreateAccountPath(
282
286
  create(IdentityAccountSchema, {
283
287
  apiVersion: "iam.stigmer.ai/v1",
284
288
  kind: "IdentityAccount",
285
- metadata: { name: input.name },
289
+ metadata: {
290
+ name: input.name,
291
+ ...(input.provisioning.mode === "platform_client"
292
+ ? { org: input.provisioning.org }
293
+ : {}),
294
+ },
286
295
  spec: input.spec,
287
296
  }),
288
297
  caller,
289
298
  kind,
299
+ input.provisioning,
290
300
  );
291
301
  }
292
302
 
@@ -303,7 +313,9 @@ function createRpc(
303
313
  ): Promise<IdentityAccount> {
304
314
  const caller = callerIdentityOf(ctx);
305
315
  guardInternalRpc(caller);
306
- return runCreateChain(deps, account, caller, kindOf(ctx));
316
+ // The RPC creates the platform's own accounts only: a platform-client
317
+ // account is the mint's to provision, never a wire caller's.
318
+ return runCreateChain(deps, account, caller, kindOf(ctx), { mode: "direct" });
307
319
  }
308
320
 
309
321
  function guardInternalRpc(caller: CallerIdentity): void {
@@ -83,6 +83,7 @@ export async function ensureOperatorAccount(
83
83
  email: operator.email,
84
84
  }),
85
85
  ),
86
+ provisioning: { mode: "direct" },
86
87
  },
87
88
  caller,
88
89
  );
@@ -74,11 +74,27 @@ export class UserInfoFetchError extends Error {
74
74
  }
75
75
  }
76
76
 
77
+ /**
78
+ * How the account being created was provisioned — the one input that
79
+ * decides its backend-assigned fields (steps.ts AssignBackendFields):
80
+ * - `direct`: the platform's own subject (a first sign-in, the boot
81
+ * operator, a machine account created over the RPC). Its subject may
82
+ * not use the `stgm_pc|` prefix platform-client accounts reserve.
83
+ * - `platform_client`: an end user a PlatformClient's mint provisioned
84
+ * (domain/platformclient/mint.ts). Its subject is the reserved
85
+ * composite, and it belongs to the client's owning organization, which
86
+ * only this arm carries.
87
+ */
88
+ export type AccountProvisioning =
89
+ | { readonly mode: "direct" }
90
+ | { readonly mode: "platform_client"; readonly org: string };
91
+
77
92
  /** What a caller of the create path supplies: the rest is the chain's. */
78
93
  export interface CreateAccountInput {
79
94
  /** `metadata.name`; the chain defaults an empty one. */
80
95
  readonly name: string;
81
96
  readonly spec: IdentityAccountSpec;
97
+ readonly provisioning: AccountProvisioning;
82
98
  }
83
99
 
84
100
  /**
@@ -152,6 +168,7 @@ export function newDirectAccountProvisioner(
152
168
  pictureUrl: profile.pictureUrl,
153
169
  }),
154
170
  ),
171
+ provisioning: { mode: "direct" },
155
172
  };
156
173
 
157
174
  try {