@stigmer/server 3.24.0 → 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 (208) 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/encryption/key-manager.d.ts +20 -3
  107. package/dist/encryption/key-manager.d.ts.map +1 -1
  108. package/dist/encryption/key-manager.js +64 -42
  109. package/dist/encryption/key-manager.js.map +1 -1
  110. package/dist/extensions/caller-guards.d.ts +8 -3
  111. package/dist/extensions/caller-guards.d.ts.map +1 -1
  112. package/dist/extensions/drivers.d.ts +37 -0
  113. package/dist/extensions/drivers.d.ts.map +1 -1
  114. package/dist/extensions/guest-token-minting.d.ts +26 -0
  115. package/dist/extensions/guest-token-minting.d.ts.map +1 -0
  116. package/dist/extensions/guest-token-minting.js +2 -0
  117. package/dist/extensions/guest-token-minting.js.map +1 -0
  118. package/dist/extensions/identity.d.ts +6 -4
  119. package/dist/extensions/identity.d.ts.map +1 -1
  120. package/dist/extensions/identity.js.map +1 -1
  121. package/dist/extensions/registry.d.ts +30 -1
  122. package/dist/extensions/registry.d.ts.map +1 -1
  123. package/dist/extensions/registry.js +34 -1
  124. package/dist/extensions/registry.js.map +1 -1
  125. package/dist/index.d.ts +13 -1
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +6 -0
  128. package/dist/index.js.map +1 -1
  129. package/dist/pipeline/interceptors/auth.d.ts +14 -0
  130. package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
  131. package/dist/pipeline/interceptors/auth.js +24 -0
  132. package/dist/pipeline/interceptors/auth.js.map +1 -1
  133. package/dist/pipeline/steps/build-update-state.d.ts +11 -1
  134. package/dist/pipeline/steps/build-update-state.d.ts.map +1 -1
  135. package/dist/pipeline/steps/build-update-state.js +5 -2
  136. package/dist/pipeline/steps/build-update-state.js.map +1 -1
  137. package/dist/platformtoken/envelope.d.ts +100 -0
  138. package/dist/platformtoken/envelope.d.ts.map +1 -0
  139. package/dist/platformtoken/envelope.js +184 -0
  140. package/dist/platformtoken/envelope.js.map +1 -0
  141. package/dist/platformtoken/key-ring.d.ts +85 -0
  142. package/dist/platformtoken/key-ring.d.ts.map +1 -0
  143. package/dist/platformtoken/key-ring.js +164 -0
  144. package/dist/platformtoken/key-ring.js.map +1 -0
  145. package/package.json +6 -6
  146. package/src/authorization/README.md +1 -1
  147. package/src/authorization/__tests__/authorizer.test.ts +6 -2
  148. package/src/authorization/__tests__/wire-permissions.test.ts +1 -0
  149. package/src/authorization/authorizer.ts +11 -4
  150. package/src/authorization/model/__tests__/registry.test.ts +8 -1
  151. package/src/authorization/model/index.ts +6 -4
  152. package/src/authorization/model/oauth_app.ts +3 -2
  153. package/src/authorization/model/platform_client.ts +43 -0
  154. package/src/boot/__tests__/auth-enabled-composition.test.ts +2 -1
  155. package/src/boot/__tests__/route-shadowing.test.ts +67 -0
  156. package/src/boot/compose.ts +90 -5
  157. package/src/boot/route-shadowing.ts +47 -0
  158. package/src/domain/iampolicy/__tests__/membership.test.ts +7 -0
  159. package/src/domain/iampolicy/membership.ts +8 -2
  160. package/src/domain/identityaccount/__tests__/platform-client-accounts.test.ts +124 -0
  161. package/src/domain/identityaccount/__tests__/resource-store.test.ts +1 -0
  162. package/src/domain/identityaccount/constants.ts +20 -0
  163. package/src/domain/identityaccount/controller.ts +16 -4
  164. package/src/domain/identityaccount/operator.ts +1 -0
  165. package/src/domain/identityaccount/provisioning.ts +17 -0
  166. package/src/domain/identityaccount/resource-store.ts +9 -5
  167. package/src/domain/identityaccount/steps.ts +58 -3
  168. package/src/domain/identityaccount/store-contract.ts +51 -0
  169. package/src/domain/identityaccount/store.ts +7 -5
  170. package/src/domain/platform/__tests__/platform.test.ts +7 -1
  171. package/src/domain/platform/controller.ts +12 -1
  172. package/src/domain/platformclient/__tests__/controller.test.ts +397 -0
  173. package/src/domain/platformclient/__tests__/credentials.test.ts +46 -0
  174. package/src/domain/platformclient/__tests__/mint.test.ts +415 -0
  175. package/src/domain/platformclient/__tests__/resource-store.test.ts +140 -0
  176. package/src/domain/platformclient/__tests__/system-managed.test.ts +101 -0
  177. package/src/domain/platformclient/__tests__/verifier-and-guard.test.ts +207 -0
  178. package/src/domain/platformclient/constants.ts +120 -0
  179. package/src/domain/platformclient/controller.ts +449 -0
  180. package/src/domain/platformclient/credentials.ts +68 -0
  181. package/src/domain/platformclient/mint.ts +412 -0
  182. package/src/domain/platformclient/origin-guard.ts +117 -0
  183. package/src/domain/platformclient/resource-store.ts +150 -0
  184. package/src/domain/platformclient/steps.ts +472 -0
  185. package/src/domain/platformclient/store-contract.ts +318 -0
  186. package/src/domain/platformclient/store.ts +63 -0
  187. package/src/domain/platformclient/system-managed.ts +115 -0
  188. package/src/domain/platformclient/token-controller.ts +49 -0
  189. package/src/domain/platformclient/verifier.ts +106 -0
  190. package/src/encryption/key-manager.ts +87 -44
  191. package/src/extensions/__tests__/extension-composition.test.ts +3 -2
  192. package/src/extensions/__tests__/platform-client-composed.test.ts +288 -0
  193. package/src/extensions/__tests__/registry.test.ts +4 -1
  194. package/src/extensions/__tests__/tier-truthfulness.test.ts +8 -0
  195. package/src/extensions/caller-guards.ts +8 -3
  196. package/src/extensions/drivers.ts +37 -0
  197. package/src/extensions/guest-token-minting.ts +32 -0
  198. package/src/extensions/identity.ts +6 -4
  199. package/src/extensions/registry.ts +77 -3
  200. package/src/index.ts +55 -0
  201. package/src/pipeline/interceptors/auth.ts +27 -0
  202. package/src/pipeline/steps/__tests__/references.test.ts +2 -0
  203. package/src/pipeline/steps/build-update-state.ts +5 -2
  204. package/src/platformtoken/__tests__/envelope.test.ts +277 -0
  205. package/src/platformtoken/__tests__/key-ring.test.ts +205 -0
  206. package/src/platformtoken/envelope.ts +276 -0
  207. package/src/platformtoken/key-ring.ts +266 -0
  208. package/src/runnerauth/__tests__/runner-subject-composed.test.ts +9 -3
@@ -49,9 +49,12 @@ import { BUILT_IN_STORAGE_TYPES } from "../artifactstorage/artifact-storage.js";
49
49
  import type { ChannelRuntime } from "../domain/agentchannel/channel-runtime.js";
50
50
  import type { IdentityAccountStore } from "../domain/identityaccount/store.js";
51
51
  import type { IamPolicyStore } from "../domain/iampolicy/store.js";
52
+ import type { PlatformClientStore } from "../domain/platformclient/store.js";
52
53
  import type { SecretCodec } from "../encryption/codec.js";
53
54
  import { V1_VERSION } from "../encryption/v1-codec.js";
55
+ import type { PlatformTokenKeyRing } from "../platformtoken/key-ring.js";
54
56
  import type { AuthorizationQueryEngine } from "./authorization-queries.js";
57
+ import type { GuestTokenMinting } from "./guest-token-minting.js";
55
58
  import type { IdentityFederation } from "./identity-federation.js";
56
59
  import type { LicenseStatusProvider } from "./license-status.js";
57
60
  import type { OutboundEgressPolicy } from "./outbound-egress.js";
@@ -88,6 +91,17 @@ import type {
88
91
  */
89
92
  export type ExtensionServiceRegistration = (router: ConnectRouter) => void;
90
93
 
94
+ /**
95
+ * A service registration with the unit that contributed it, so the routes
96
+ * stage can name the unit when a registration shadows a core route
97
+ * (compose.ts refuses that at boot: Connect's router keys handlers by
98
+ * request path and the last registration wins, silently).
99
+ */
100
+ export interface ResolvedServiceRegistration {
101
+ readonly unit: string;
102
+ readonly register: ExtensionServiceRegistration;
103
+ }
104
+
91
105
  /**
92
106
  * One named extension unit — a package's whole contribution to the server
93
107
  * composition. All fields but `name` are optional; an omitted field is
@@ -191,7 +205,7 @@ export interface ResolvedExtensions {
191
205
  readonly statusObservers: ReadonlyArray<AgentExecutionStatusObserver>;
192
206
  readonly responseDecorators: ReadonlyArray<AgentExecutionResponseDecorator>;
193
207
  readonly drivers: ResolvedExtensionDrivers;
194
- readonly services: ReadonlyArray<ExtensionServiceRegistration>;
208
+ readonly services: ReadonlyArray<ResolvedServiceRegistration>;
195
209
  readonly workers: ReadonlyArray<WorkerFactory>;
196
210
  }
197
211
 
@@ -284,6 +298,22 @@ export interface ResolvedExtensionDrivers {
284
298
  * source's relaxed posture (only the link-local range refused).
285
299
  */
286
300
  readonly outboundEgress: OutboundEgressPolicy | undefined;
301
+ /**
302
+ * The platform-client store driver — undefined = compose.ts installs the
303
+ * OSS adapter over the generic Store.
304
+ */
305
+ readonly platformClientStore: PlatformClientStore | undefined;
306
+ /**
307
+ * The platform-token key ring — undefined = compose.ts composes open
308
+ * source's ring under an authentication posture, and refuses to boot a
309
+ * non-open-source edition that supplied none.
310
+ */
311
+ readonly platformTokenKeys: PlatformTokenKeyRing | undefined;
312
+ /**
313
+ * The guest-token capability — undefined = `mintGuestToken` refuses
314
+ * UNIMPLEMENTED with the edition sentence.
315
+ */
316
+ readonly guestTokenMinting: GuestTokenMinting | undefined;
287
317
  }
288
318
 
289
319
  /**
@@ -301,7 +331,7 @@ export function resolveExtensions(
301
331
  const gateSteps = new Map<string, ReadonlyArray<PipelineStep<DescMessage>>>();
302
332
  const statusObservers: AgentExecutionStatusObserver[] = [];
303
333
  const responseDecorators: AgentExecutionResponseDecorator[] = [];
304
- const services: ExtensionServiceRegistration[] = [];
334
+ const services: ResolvedServiceRegistration[] = [];
305
335
  const workers: WorkerFactory[] = [];
306
336
 
307
337
  let edition: ServerEdition | undefined;
@@ -341,6 +371,12 @@ export function resolveExtensions(
341
371
  let licenseStatusDeclaredBy: string | undefined;
342
372
  let outboundEgress: OutboundEgressPolicy | undefined;
343
373
  let outboundEgressDeclaredBy: string | undefined;
374
+ let platformClientStore: PlatformClientStore | undefined;
375
+ let platformClientStoreDeclaredBy: string | undefined;
376
+ let platformTokenKeys: PlatformTokenKeyRing | undefined;
377
+ let platformTokenKeysDeclaredBy: string | undefined;
378
+ let guestTokenMinting: GuestTokenMinting | undefined;
379
+ let guestTokenMintingDeclaredBy: string | undefined;
344
380
  const artifactStorageDrivers = new Map<
345
381
  string,
346
382
  ArtifactStorageDriverFactory
@@ -552,6 +588,36 @@ export function resolveExtensions(
552
588
  outboundEgressDeclaredBy = unit.name;
553
589
  }
554
590
 
591
+ if (unit.drivers?.platformClientStore !== undefined) {
592
+ if (platformClientStoreDeclaredBy !== undefined) {
593
+ throw new Error(
594
+ `extension '${unit.name}' registers a PlatformClientStore, but '${platformClientStoreDeclaredBy}' already did — exactly one may be composed`,
595
+ );
596
+ }
597
+ platformClientStore = unit.drivers.platformClientStore;
598
+ platformClientStoreDeclaredBy = unit.name;
599
+ }
600
+
601
+ if (unit.drivers?.platformTokenKeys !== undefined) {
602
+ if (platformTokenKeysDeclaredBy !== undefined) {
603
+ throw new Error(
604
+ `extension '${unit.name}' registers a PlatformTokenKeyRing, but '${platformTokenKeysDeclaredBy}' already did — exactly one may be composed`,
605
+ );
606
+ }
607
+ platformTokenKeys = unit.drivers.platformTokenKeys;
608
+ platformTokenKeysDeclaredBy = unit.name;
609
+ }
610
+
611
+ if (unit.drivers?.guestTokenMinting !== undefined) {
612
+ if (guestTokenMintingDeclaredBy !== undefined) {
613
+ throw new Error(
614
+ `extension '${unit.name}' registers a GuestTokenMinting capability, but '${guestTokenMintingDeclaredBy}' already did — exactly one may be composed`,
615
+ );
616
+ }
617
+ guestTokenMinting = unit.drivers.guestTokenMinting;
618
+ guestTokenMintingDeclaredBy = unit.name;
619
+ }
620
+
555
621
  if (unit.drivers?.artifactStorageDrivers !== undefined) {
556
622
  for (const [name, factory] of unit.drivers.artifactStorageDrivers) {
557
623
  if ((BUILT_IN_STORAGE_TYPES as ReadonlyArray<string>).includes(name)) {
@@ -643,7 +709,12 @@ export function resolveExtensions(
643
709
  responseDecorators.push(
644
710
  ...(unit.statusTransitionHooks?.responseDecorators ?? []),
645
711
  );
646
- services.push(...(unit.services ?? []));
712
+ services.push(
713
+ ...(unit.services ?? []).map((register) => ({
714
+ unit: unit.name,
715
+ register,
716
+ })),
717
+ );
647
718
  workers.push(...(unit.workers ?? []));
648
719
  }
649
720
 
@@ -679,6 +750,9 @@ export function resolveExtensions(
679
750
  authorizationQueries,
680
751
  licenseStatus,
681
752
  outboundEgress,
753
+ platformClientStore,
754
+ platformTokenKeys,
755
+ guestTokenMinting,
682
756
  },
683
757
  services,
684
758
  workers,
package/src/index.ts CHANGED
@@ -59,6 +59,7 @@ export type {
59
59
  export type {
60
60
  ExtensionServiceRegistration,
61
61
  ResolvedExtensions,
62
+ ResolvedServiceRegistration,
62
63
  ServerExtension,
63
64
  } from "./extensions/registry.js";
64
65
  export type {
@@ -166,6 +167,60 @@ export type { AuthorizationQueryEngine } from "./extensions/authorization-querie
166
167
  export type { IdentityFederation } from "./extensions/identity-federation.js";
167
168
  export type { AccountsBySubject } from "./domain/identityaccount/resolve.js";
168
169
  export { identityIdForSubject } from "./domain/identityaccount/resolve.js";
170
+ // The PlatformClient seams: the store PORT a composition drives the domain
171
+ // through (drivers.platformClientStore; a driver throws
172
+ // DuplicatePlatformClientError for a held id, slug or client_id) and its
173
+ // vitest-free contract kit; the guest-token capability
174
+ // (drivers.guestTokenMinting — the one token method only an edition that
175
+ // hosts shared-agent pages serves); and the builder of the system-managed
176
+ // client such an edition keeps per organization under the reserved slug,
177
+ // so the tokens it signs for guests and schedule fires name a real client.
178
+ export type { PlatformClientStore } from "./domain/platformclient/store.js";
179
+ export { DuplicatePlatformClientError } from "./domain/platformclient/store.js";
180
+ export type {
181
+ PlatformClientStoreContractCase,
182
+ PlatformClientStoreContractFixture,
183
+ } from "./domain/platformclient/store-contract.js";
184
+ export { platformClientStoreContract } from "./domain/platformclient/store-contract.js";
185
+ export type { GuestTokenMinting } from "./extensions/guest-token-minting.js";
186
+ export type { SystemManagedPlatformClientInput } from "./domain/platformclient/system-managed.js";
187
+ export { newSystemManagedPlatformClient } from "./domain/platformclient/system-managed.js";
188
+ export { SYSTEM_SHARE_CLIENT_SLUG } from "./domain/platformclient/constants.js";
189
+ // The platform-token envelope and its key ring: the RS256 JWT every token
190
+ // the server signs for itself rides. A composition supplies its ring
191
+ // (drivers.platformTokenKeys, built from its PEMs), signs its own typed
192
+ // lanes through the envelope — each with its own lifetime when it is not
193
+ // the ring's default, the expiry handed back — and verifies them with the
194
+ // same function and refusal copy open source's user-token lane uses.
195
+ export type {
196
+ PlatformTokenClaimValue,
197
+ PlatformTokenRefusal,
198
+ PlatformTokenSigningOptions,
199
+ PlatformTokenVerification,
200
+ SignedPlatformToken,
201
+ VerifiedPlatformToken,
202
+ } from "./platformtoken/envelope.js";
203
+ export {
204
+ PLATFORM_TOKEN_ISSUER,
205
+ PLATFORM_TOKEN_REFUSAL_MESSAGES,
206
+ TOKEN_TYPE_CLAIM,
207
+ decodeVerifiedPlatformTokenPayload,
208
+ platformTokenRefusalError,
209
+ signPlatformToken,
210
+ stringClaim,
211
+ verifyPlatformToken,
212
+ } from "./platformtoken/envelope.js";
213
+ export type {
214
+ PlatformTokenKeyMaterial,
215
+ PlatformTokenKeyRing,
216
+ PlatformTokenSigner,
217
+ SigningPlatformTokenKeyRing,
218
+ } from "./platformtoken/key-ring.js";
219
+ export {
220
+ DEFAULT_PLATFORM_TOKEN_TTL_SECONDS,
221
+ canSign,
222
+ platformTokenKeyRingFromPem,
223
+ } from "./platformtoken/key-ring.js";
169
224
  // The built-in authorizer's model seams: the kind
170
225
  // declarations (transcripts of the cloud's `.fga` files), the evaluator,
171
226
  // and the OpenFGA store-test kit — exported so the cloud's drift test
@@ -216,6 +216,33 @@ export function trustedLocalIdentityFor(operator: {
216
216
  };
217
217
  }
218
218
 
219
+ /**
220
+ * The server acting for a principal it has authenticated some other way —
221
+ * the one construction of an `internal` caller outside the in-process
222
+ * transport's own stamp. Its consumer is the PlatformClient mint
223
+ * (domain/platformclient/mint.ts): the request is public and tokenless
224
+ * (so the chain stamped the trusted-local operator), the client proved
225
+ * itself with its secret, and the end-user account and role the mint
226
+ * creates must name the CLIENT as their actor, never the operator.
227
+ * `internal` because the server is the one acting — the create path's
228
+ * Authorize skips it, as for every server-composed request — and never
229
+ * reachable from the wire: no verifier produces it, and the serving
230
+ * chassis overwrites position 1 on every request.
231
+ */
232
+ export function serverActingFor(principalId: string): CallerIdentity {
233
+ if (principalId === "") {
234
+ throw new Error(
235
+ "the server acts for a named principal — refusing to build an internal caller with none",
236
+ );
237
+ }
238
+ return {
239
+ identityId: principalId,
240
+ callerClass: "internal",
241
+ issuer: "",
242
+ rawToken: "",
243
+ };
244
+ }
245
+
219
246
  /**
220
247
  * The serving chain's position-1 identity source: walks the composed
221
248
  * verifiers in order (OSS entries first, extension entries after, in
@@ -39,6 +39,7 @@ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/
39
39
  import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
40
40
  import { reference_kind } from "@stigmer/protos/ai/stigmer/commons/apiresource/field_options_pb";
41
41
  import { UpdateVisibilityInputSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
42
+ import { PlatformClientSchema } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
42
43
 
43
44
  import type { Store } from "../../../store/interface.js";
44
45
  import { SqliteStore } from "../../../store/sqlite/store.js";
@@ -97,6 +98,7 @@ const SCHEMAS_UNDER_THE_RULE: ReadonlyArray<DescMessage> = [
97
98
  AgentExecutionSchema,
98
99
  WorkflowExecutionSchema,
99
100
  ExecutionContextSchema,
101
+ PlatformClientSchema,
100
102
  AgentCallTaskConfigSchema,
101
103
  ];
102
104
 
@@ -115,9 +115,12 @@ function copyStatusFromExisting(
115
115
  /**
116
116
  * Go updateAuditFieldsReflect: spec_audit keeps the existing creation
117
117
  * identity with a fresh update stamp; status_audit is fully current (the
118
- * status was rebuilt); both carry event "updated".
118
+ * status was rebuilt); both carry event "updated". Exported for the one
119
+ * write that changes a stored resource outside this step — a
120
+ * PlatformClient secret rotation (domain/platformclient/steps.ts) — so an
121
+ * update's audit has one shape whichever step writes it.
119
122
  */
120
- function updateAuditFields(
123
+ export function updateAuditFields(
121
124
  schema: DescMessage,
122
125
  resource: Message,
123
126
  existing: Message,
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Pins the platform-token envelope against the cloud verifier's semantics,
3
+ * which every token in the field was accepted under:
4
+ * - a signed token round-trips with the envelope's claims and the lane's;
5
+ * - a lane's own lifetime replaces the ring's default, and the expiry the
6
+ * signer returns is the instant the `exp` claim names;
7
+ * - a token signed by the previous key still verifies (the rotation
8
+ * window), and the header's kid never selects a key;
9
+ * - the forgeries a JWT verifier must refuse are refused as a bad
10
+ * signature: `alg: none`, and an HMAC token keyed with the public key;
11
+ * - expiry is strict (no leeway), a missing `aud` is tolerated, a wrong
12
+ * one refused, and a missing `sub` refused — each with its pinned copy;
13
+ * - anything that is not a three-part `iss: "stigmer"` JWT is foreign,
14
+ * never refused, so the next verifier judges it;
15
+ * - a lane cannot set a claim the envelope owns.
16
+ */
17
+ import { createHmac, generateKeyPairSync } from "node:crypto";
18
+
19
+ import { Code } from "@connectrpc/connect";
20
+ import { describe, expect, it } from "vitest";
21
+
22
+ import {
23
+ PLATFORM_TOKEN_REFUSAL_MESSAGES,
24
+ decodeVerifiedPlatformTokenPayload,
25
+ platformTokenRefusalError,
26
+ signPlatformToken,
27
+ verifyPlatformToken,
28
+ } from "../envelope.js";
29
+ import { platformTokenKeyRingFromPem } from "../key-ring.js";
30
+ import type { SigningPlatformTokenKeyRing } from "../key-ring.js";
31
+
32
+ function pemPair(): { privateKeyPem: string; publicKeyPem: string } {
33
+ const { privateKey, publicKey } = generateKeyPairSync("rsa", {
34
+ modulusLength: 2048,
35
+ });
36
+ return {
37
+ privateKeyPem: privateKey
38
+ .export({ format: "pem", type: "pkcs8" })
39
+ .toString(),
40
+ publicKeyPem: publicKey.export({ format: "pem", type: "spki" }).toString(),
41
+ };
42
+ }
43
+
44
+ const active = pemPair();
45
+ const previous = pemPair();
46
+
47
+ function signingRing(
48
+ options: {
49
+ pair?: { privateKeyPem: string; publicKeyPem: string };
50
+ audience?: string;
51
+ ttlSeconds?: number;
52
+ } = {},
53
+ ): SigningPlatformTokenKeyRing {
54
+ const pair = options.pair ?? active;
55
+ const ring = platformTokenKeyRingFromPem({
56
+ privateKeyPem: pair.privateKeyPem,
57
+ kid: "stigmer-signing-key-1",
58
+ publicKeyPems: [pair.publicKeyPem],
59
+ audience: options.audience ?? "",
60
+ ttlSeconds: options.ttlSeconds ?? 900,
61
+ });
62
+ if (ring.signer === undefined) throw new Error("fixture ring must sign");
63
+ return { ...ring, signer: ring.signer };
64
+ }
65
+
66
+ const NOW = new Date("2026-09-23T10:00:00Z");
67
+
68
+ function base64UrlJson(value: object): string {
69
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
70
+ }
71
+
72
+ describe("the platform-token envelope", () => {
73
+ it("round-trips a signed token: the envelope's claims, then the lane's", () => {
74
+ const ring = signingRing({ audience: "https://api.stigmer.ai" });
75
+ const token = signPlatformToken(
76
+ ring,
77
+ { sub: "ida_1", platform_client_id: "pcl_1" },
78
+ { now: NOW },
79
+ ).token;
80
+
81
+ const result = verifyPlatformToken(ring, token, NOW);
82
+ expect(result.outcome).toBe("verified");
83
+ if (result.outcome !== "verified") return;
84
+ expect(result.token.subject).toBe("ida_1");
85
+ expect(result.token.tokenType).toBeUndefined();
86
+ expect(result.token.payload).toMatchObject({
87
+ iss: "stigmer",
88
+ iat: NOW.getTime() / 1000,
89
+ exp: NOW.getTime() / 1000 + 900,
90
+ aud: "https://api.stigmer.ai",
91
+ platform_client_id: "pcl_1",
92
+ });
93
+ const header = JSON.parse(
94
+ Buffer.from(token.split(".")[0] ?? "", "base64url").toString(),
95
+ );
96
+ expect(header).toEqual({
97
+ alg: "RS256",
98
+ typ: "JWT",
99
+ kid: "stigmer-signing-key-1",
100
+ });
101
+ });
102
+
103
+ it("signs with the lane's own lifetime when it names one, and returns the expiry the token carries", () => {
104
+ const ring = signingRing({ ttlSeconds: 900 });
105
+
106
+ const byDefault = signPlatformToken(ring, { sub: "ida_1" }, { now: NOW });
107
+ expect(byDefault.expiresAt).toEqual(new Date(NOW.getTime() + 900_000));
108
+
109
+ const sandbox = signPlatformToken(
110
+ ring,
111
+ { sub: "ida_1", token_type: "sandbox" },
112
+ { now: NOW, ttlSeconds: 14_400 },
113
+ );
114
+ expect(sandbox.expiresAt).toEqual(new Date(NOW.getTime() + 14_400_000));
115
+ const verified = verifyPlatformToken(ring, sandbox.token, NOW);
116
+ expect(verified.outcome === "verified" && verified.token.payload.exp).toBe(
117
+ sandbox.expiresAt.getTime() / 1000,
118
+ );
119
+ expect(
120
+ verifyPlatformToken(
121
+ ring,
122
+ sandbox.token,
123
+ new Date(NOW.getTime() + 900_000),
124
+ ).outcome,
125
+ ).toBe("verified");
126
+ });
127
+
128
+ it("refuses a lifetime that is not a positive whole number of seconds", () => {
129
+ for (const ttlSeconds of [0, -1, 1.5, Number.NaN]) {
130
+ expect(() =>
131
+ signPlatformToken(signingRing(), { sub: "ida_1" }, { ttlSeconds }),
132
+ ).toThrow(/positive integer number of seconds/);
133
+ }
134
+ });
135
+
136
+ it("reads token_type as the lane discriminator", () => {
137
+ const ring = signingRing();
138
+ const token = signPlatformToken(
139
+ ring,
140
+ { sub: "ida_guest", token_type: "guest" },
141
+ { now: NOW },
142
+ ).token;
143
+ const result = verifyPlatformToken(ring, token, NOW);
144
+ expect(result.outcome === "verified" && result.token.tokenType).toBe(
145
+ "guest",
146
+ );
147
+ });
148
+
149
+ it("verifies a token signed by the previous key while it is still accepted, whatever the kid says", () => {
150
+ const old = signPlatformToken(
151
+ signingRing({ pair: previous }),
152
+ { sub: "ida_1" },
153
+ { now: NOW },
154
+ ).token;
155
+ const ring = platformTokenKeyRingFromPem({
156
+ privateKeyPem: active.privateKeyPem,
157
+ kid: "stigmer-signing-key-1",
158
+ publicKeyPems: [active.publicKeyPem, previous.publicKeyPem],
159
+ });
160
+ expect(verifyPlatformToken(ring, old, NOW).outcome).toBe("verified");
161
+ });
162
+
163
+ it("refuses `alg: none` and an HMAC token keyed with the public key as a bad signature", () => {
164
+ const ring = signingRing();
165
+ const payload = base64UrlJson({
166
+ iss: "stigmer",
167
+ sub: "ida_1",
168
+ exp: NOW.getTime() / 1000 + 60,
169
+ });
170
+
171
+ const unsigned = `${base64UrlJson({ alg: "none", typ: "JWT" })}.${payload}.`;
172
+ expect(verifyPlatformToken(ring, unsigned, NOW)).toEqual({
173
+ outcome: "refused",
174
+ refusal: "signature",
175
+ });
176
+
177
+ const hmacHeader = base64UrlJson({ alg: "HS256", typ: "JWT" });
178
+ const forged = createHmac("sha256", active.publicKeyPem)
179
+ .update(`${hmacHeader}.${payload}`)
180
+ .digest("base64url");
181
+ expect(
182
+ verifyPlatformToken(ring, `${hmacHeader}.${payload}.${forged}`, NOW),
183
+ ).toEqual({
184
+ outcome: "refused",
185
+ refusal: "signature",
186
+ });
187
+ });
188
+
189
+ it("refuses a token signed by a key the ring does not hold", () => {
190
+ const stranger = signPlatformToken(
191
+ signingRing({ pair: pemPair() }),
192
+ { sub: "ida_1" },
193
+ { now: NOW },
194
+ ).token;
195
+ expect(verifyPlatformToken(signingRing(), stranger, NOW)).toEqual({
196
+ outcome: "refused",
197
+ refusal: "signature",
198
+ });
199
+ });
200
+
201
+ it("refuses an expired token strictly, with no leeway", () => {
202
+ const ring = signingRing({ ttlSeconds: 60 });
203
+ const token = signPlatformToken(ring, { sub: "ida_1" }, { now: NOW }).token;
204
+ const atExpiry = new Date(NOW.getTime() + 60_000);
205
+ expect(verifyPlatformToken(ring, token, atExpiry)).toEqual({
206
+ outcome: "refused",
207
+ refusal: "expired",
208
+ });
209
+ });
210
+
211
+ it("tolerates a missing aud and refuses one that names another audience", () => {
212
+ const noAudience = signPlatformToken(
213
+ signingRing(),
214
+ { sub: "ida_1" },
215
+ { now: NOW },
216
+ ).token;
217
+ const expecting = signingRing({ audience: "https://api.stigmer.ai" });
218
+ expect(verifyPlatformToken(expecting, noAudience, NOW).outcome).toBe(
219
+ "verified",
220
+ );
221
+
222
+ const elsewhere = signPlatformToken(
223
+ signingRing({ audience: "https://staging.stigmer.ai" }),
224
+ { sub: "ida_1" },
225
+ { now: NOW },
226
+ ).token;
227
+ expect(verifyPlatformToken(expecting, elsewhere, NOW)).toEqual({
228
+ outcome: "refused",
229
+ refusal: "audience",
230
+ });
231
+ });
232
+
233
+ it("refuses a token with no subject", () => {
234
+ const ring = signingRing();
235
+ const token = signPlatformToken(
236
+ ring,
237
+ { platform_client_id: "pcl_1" },
238
+ { now: NOW },
239
+ ).token;
240
+ expect(verifyPlatformToken(ring, token, NOW)).toEqual({
241
+ outcome: "refused",
242
+ refusal: "subject",
243
+ });
244
+ });
245
+
246
+ it("answers foreign for anything that is not a stigmer-issued three-part JWT", () => {
247
+ const ring = signingRing();
248
+ expect(verifyPlatformToken(ring, "stk_an_api_key", NOW).outcome).toBe(
249
+ "foreign",
250
+ );
251
+ expect(verifyPlatformToken(ring, "a.b", NOW).outcome).toBe("foreign");
252
+ const otherIssuer = `${base64UrlJson({ alg: "RS256" })}.${base64UrlJson({ iss: "https://issuer.example", sub: "x" })}.sig`;
253
+ expect(verifyPlatformToken(ring, otherIssuer, NOW).outcome).toBe("foreign");
254
+ expect(decodeVerifiedPlatformTokenPayload(otherIssuer)).toBeUndefined();
255
+ expect(decodeVerifiedPlatformTokenPayload("")).toBeUndefined();
256
+ });
257
+
258
+ it("refuses a lane claim that collides with an envelope claim", () => {
259
+ expect(
260
+ () =>
261
+ signPlatformToken(signingRing(), { sub: "ida_1", exp: 1 }, { now: NOW })
262
+ .token,
263
+ ).toThrow(/belongs to the envelope/);
264
+ });
265
+
266
+ it("maps every refusal to UNAUTHENTICATED with the cloud verifier's copy", () => {
267
+ expect(PLATFORM_TOKEN_REFUSAL_MESSAGES).toEqual({
268
+ signature: "platform token signature verification failed",
269
+ expired: "platform token is expired",
270
+ audience: "platform token was minted for another environment",
271
+ subject: "platform token carries no subject",
272
+ });
273
+ const error = platformTokenRefusalError("expired");
274
+ expect(error.code).toBe(Code.Unauthenticated);
275
+ expect(error.rawMessage).toBe("platform token is expired");
276
+ });
277
+ });