@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
@@ -0,0 +1,266 @@
1
+ /**
2
+ * The platform-token key ring: the RS256 key material every token the
3
+ * server signs for itself rides (the envelope in envelope.ts). One ring
4
+ * per server, supplied once:
5
+ *
6
+ * - Open source composes `openSourcePlatformTokenKeyRing()`: one RSA key
7
+ * on the key-manager ladder (STIGMER_PLATFORM_TOKEN_KEY, else
8
+ * ~/.stigmer/platform-token.key, else generated and persisted — the
9
+ * runner-token key's convention, under the ladder's own rules). It
10
+ * signs and verifies with the same key; no audience; a kid derived
11
+ * from the key, so two installations never claim the same kid.
12
+ * - A composition supplies its own ring through the `platformTokenKeys`
13
+ * driver point, built from its configured PEMs with
14
+ * `platformTokenKeyRingFromPem`: the active public key first, then any
15
+ * still accepted (the rotation window), an optional private key (a
16
+ * ring without one verifies and never signs), its kid and audience.
17
+ *
18
+ * Only RSA keys of at least MIN_RSA_MODULUS_BITS are accepted: the
19
+ * envelope signs PKCS#1 v1.5 over SHA-256 (RS256) and asks the key for
20
+ * nothing else, so an EC key would silently change the algorithm.
21
+ */
22
+ import {
23
+ createHash,
24
+ createPrivateKey,
25
+ createPublicKey,
26
+ generateKeyPairSync,
27
+ } from "node:crypto";
28
+ import type { KeyObject } from "node:crypto";
29
+
30
+ import { ServerEdition } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
31
+
32
+ import { getOrCreateKey } from "../encryption/key-manager.js";
33
+ import type { KeyCodec, KeyLoaderOptions } from "../encryption/key-manager.js";
34
+
35
+ /** Env var carrying open source's signing key: Base64 of a PKCS#8 PEM. */
36
+ export const PLATFORM_TOKEN_KEY_ENV_VAR = "STIGMER_PLATFORM_TOKEN_KEY";
37
+
38
+ /** Key file under ~/.stigmer for the generated signing key (PKCS#8 PEM). */
39
+ export const PLATFORM_TOKEN_KEY_FILE_NAME = "platform-token.key";
40
+
41
+ /**
42
+ * The default lifetime of a signed token: the cloud's user-token lifetime
43
+ * since the Java issuer (STIGMER_JWT_USER_TOKEN_TTL_SECONDS' default). It
44
+ * bounds how long a user token outlives a rotated secret or a lost key; a
45
+ * deleted client's tokens stop at the next request regardless (the
46
+ * verifier's liveness).
47
+ */
48
+ export const DEFAULT_PLATFORM_TOKEN_TTL_SECONDS = 900;
49
+
50
+ /** The smallest RSA modulus the ring accepts (NIST SP 800-131A's floor). */
51
+ export const MIN_RSA_MODULUS_BITS = 2048;
52
+
53
+ /** The key that signs, and the kid its tokens' header names. */
54
+ export interface PlatformTokenSigner {
55
+ readonly key: KeyObject;
56
+ readonly kid: string;
57
+ }
58
+
59
+ export interface PlatformTokenKeyRing {
60
+ /** Absent: the ring verifies and never signs (minting is disabled). */
61
+ readonly signer?: PlatformTokenSigner;
62
+ /** Every public key a token may verify against, the active one first. */
63
+ readonly verificationKeys: ReadonlyArray<KeyObject>;
64
+ /** Stamped as `aud` and checked when present; "" means none. */
65
+ readonly audience: string;
66
+ /**
67
+ * The lifetime of a token signed without one of its own: the user
68
+ * token's. A lane that lives longer or shorter passes its own to
69
+ * `signPlatformToken`.
70
+ */
71
+ readonly ttlSeconds: number;
72
+ }
73
+
74
+ /** A ring that can sign — what `signPlatformToken` takes. */
75
+ export type SigningPlatformTokenKeyRing = PlatformTokenKeyRing & {
76
+ readonly signer: PlatformTokenSigner;
77
+ };
78
+
79
+ export function canSign(
80
+ ring: PlatformTokenKeyRing,
81
+ ): ring is SigningPlatformTokenKeyRing {
82
+ return ring.signer !== undefined;
83
+ }
84
+
85
+ /** A composition's configured key material, as PEM text. */
86
+ export interface PlatformTokenKeyMaterial {
87
+ /** PKCS#8 or PKCS#1 private key; absent disables signing. */
88
+ readonly privateKeyPem?: string;
89
+ /** The header kid; defaults to one derived from the private key. */
90
+ readonly kid?: string;
91
+ /** SPKI public keys, the active one first; at least one. */
92
+ readonly publicKeyPems: ReadonlyArray<string>;
93
+ readonly audience?: string;
94
+ readonly ttlSeconds?: number;
95
+ }
96
+
97
+ /**
98
+ * Builds a ring from PEM text, refusing loudly at boot what would fail
99
+ * quietly at the first request: a non-RSA or undersized key, an empty
100
+ * verification set, a non-positive TTL, and a private key whose public
101
+ * half is not among the verification keys (it would sign tokens nothing
102
+ * accepts).
103
+ */
104
+ export function platformTokenKeyRingFromPem(
105
+ material: PlatformTokenKeyMaterial,
106
+ ): PlatformTokenKeyRing {
107
+ if (material.publicKeyPems.length === 0) {
108
+ throw new Error(
109
+ "a platform-token key ring needs at least one public key to verify against",
110
+ );
111
+ }
112
+ const verificationKeys = material.publicKeyPems.map((pem, index) =>
113
+ requireRsa(
114
+ parseKey(() => createPublicKey(pem), `public key ${index + 1}`),
115
+ `public key ${index + 1}`,
116
+ ),
117
+ );
118
+ const ttlSeconds = material.ttlSeconds ?? DEFAULT_PLATFORM_TOKEN_TTL_SECONDS;
119
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
120
+ throw new Error(
121
+ `platform-token TTL must be a positive integer number of seconds, got ${ttlSeconds}`,
122
+ );
123
+ }
124
+ const base = {
125
+ verificationKeys,
126
+ audience: material.audience ?? "",
127
+ ttlSeconds,
128
+ };
129
+ if (material.privateKeyPem === undefined) {
130
+ return base;
131
+ }
132
+ const privateKey = requireRsa(
133
+ parseKey(
134
+ () => createPrivateKey(material.privateKeyPem ?? ""),
135
+ "private key",
136
+ ),
137
+ "private key",
138
+ );
139
+ const derivedPublic = spkiOf(createPublicKey(privateKey));
140
+ if (!verificationKeys.some((key) => spkiOf(key).equals(derivedPublic))) {
141
+ throw new Error(
142
+ "the platform-token private key's public half is not among the verification keys — tokens it signed would verify nowhere",
143
+ );
144
+ }
145
+ return {
146
+ ...base,
147
+ signer: { key: privateKey, kid: material.kid ?? kidOf(privateKey) },
148
+ };
149
+ }
150
+
151
+ /**
152
+ * The ring a composition runs with, decided once at the keys stage:
153
+ * - no authentication posture: none — a server that trusts every
154
+ * request verifies nothing, so it signs nothing (the mint refuses);
155
+ * - a supplied ring: that ring;
156
+ * - none supplied, open source: open source's own ring on the ladder;
157
+ * - none supplied, any other edition: a boot throw, so a hosted edition
158
+ * can never sign with a key generated into a pod's home directory,
159
+ * which no other replica shares and no restart keeps.
160
+ */
161
+ export function resolvePlatformTokenKeys(input: {
162
+ readonly requireAuthentication: boolean;
163
+ readonly supplied: PlatformTokenKeyRing | undefined;
164
+ readonly edition: ServerEdition;
165
+ readonly options?: KeyLoaderOptions;
166
+ }): PlatformTokenKeyRing | undefined {
167
+ if (!input.requireAuthentication) {
168
+ return undefined;
169
+ }
170
+ if (input.supplied !== undefined) {
171
+ return input.supplied;
172
+ }
173
+ if (input.edition !== ServerEdition.oss) {
174
+ throw new Error(
175
+ `the ${ServerEdition[input.edition]} edition requires authentication but registers no platform-token key ring — register drivers.platformTokenKeys; only open source generates its own key`,
176
+ );
177
+ }
178
+ return openSourcePlatformTokenKeyRing(input.options);
179
+ }
180
+
181
+ /**
182
+ * Open source's ring: one RSA key on the key-manager ladder, signing and
183
+ * verifying, no audience, the default TTL. Throws when an explicitly
184
+ * configured STIGMER_PLATFORM_TOKEN_KEY is unusable (the ladder's rule).
185
+ */
186
+ export function openSourcePlatformTokenKeyRing(
187
+ options: KeyLoaderOptions = {},
188
+ ): SigningPlatformTokenKeyRing {
189
+ const privateKey = getOrCreateKey(
190
+ RSA_PRIVATE_KEY,
191
+ PLATFORM_TOKEN_KEY_ENV_VAR,
192
+ PLATFORM_TOKEN_KEY_FILE_NAME,
193
+ options,
194
+ );
195
+ return {
196
+ signer: { key: privateKey, kid: kidOf(privateKey) },
197
+ verificationKeys: [createPublicKey(privateKey)],
198
+ audience: "",
199
+ ttlSeconds: DEFAULT_PLATFORM_TOKEN_TTL_SECONDS,
200
+ };
201
+ }
202
+
203
+ /** The ladder codec for an RSA private key stored as PKCS#8 PEM. */
204
+ export const RSA_PRIVATE_KEY: KeyCodec<KeyObject> = {
205
+ fromEnv(envVar, decoded) {
206
+ const key = parseKey(
207
+ () => createPrivateKey(decoded.toString("utf8")),
208
+ envVar,
209
+ );
210
+ return requireRsa(key, envVar);
211
+ },
212
+ fromFile(content) {
213
+ try {
214
+ const key = createPrivateKey(content.toString("utf8"));
215
+ return isUsableRsa(key) ? key : undefined;
216
+ } catch {
217
+ return undefined;
218
+ }
219
+ },
220
+ generate() {
221
+ return generateKeyPairSync("rsa", { modulusLength: MIN_RSA_MODULUS_BITS })
222
+ .privateKey;
223
+ },
224
+ toFile(key) {
225
+ return Buffer.from(key.export({ format: "pem", type: "pkcs8" }));
226
+ },
227
+ };
228
+
229
+ /** A key's kid: the first 16 characters of its public half's SHA-256. */
230
+ function kidOf(key: KeyObject): string {
231
+ const publicKey = key.type === "private" ? createPublicKey(key) : key;
232
+ return createHash("sha256")
233
+ .update(spkiOf(publicKey))
234
+ .digest("base64url")
235
+ .slice(0, 16);
236
+ }
237
+
238
+ function spkiOf(publicKey: KeyObject): Buffer {
239
+ return publicKey.export({ format: "der", type: "spki" });
240
+ }
241
+
242
+ function parseKey(parse: () => KeyObject, what: string): KeyObject {
243
+ try {
244
+ return parse();
245
+ } catch (error) {
246
+ throw new Error(
247
+ `${what} is not a readable PEM key: ${error instanceof Error ? error.message : String(error)}`,
248
+ );
249
+ }
250
+ }
251
+
252
+ function requireRsa(key: KeyObject, what: string): KeyObject {
253
+ if (!isUsableRsa(key)) {
254
+ throw new Error(
255
+ `${what} must be an RSA key of at least ${MIN_RSA_MODULUS_BITS} bits`,
256
+ );
257
+ }
258
+ return key;
259
+ }
260
+
261
+ function isUsableRsa(key: KeyObject): boolean {
262
+ return (
263
+ key.asymmetricKeyType === "rsa" &&
264
+ (key.asymmetricKeyDetails?.modulusLength ?? 0) >= MIN_RSA_MODULUS_BITS
265
+ );
266
+ }
@@ -245,9 +245,13 @@ describe("the built-in posture (OIDC, no unit Authorizer): the runner acts as th
245
245
  );
246
246
  }
247
247
 
248
- it("the chain is apikey → runner → oidc: ours claims the server's own token before the OIDC verifier can fault on it", () => {
248
+ it("the chain is apikey → platform-client → runner → oidc: ours claims the server's own token before the OIDC verifier can fault on it", () => {
249
+ // The platform-client verifier claims only `iss: "stigmer"` user tokens
250
+ // and a runner token carries no `iss`, so the two lanes are disjoint and
251
+ // both sit ahead of the OIDC verifier for the same reason.
249
252
  expect(server.identityVerifiers.map((verifier) => verifier.name)).toEqual([
250
253
  "apikey",
254
+ "platform-client",
251
255
  RUNNER_VERIFIER_NAME,
252
256
  "oidc",
253
257
  ]);
@@ -489,9 +493,11 @@ describe("a unit's own Authorizer: no runner verifier is composed", () => {
489
493
  rmSync(dir, { recursive: true, force: true });
490
494
  });
491
495
 
492
- it("the chain is apikey then the unit's verifiers — the composition owns its credential story", () => {
496
+ it("the chain is apikey, platform-client, then the unit's verifiers — the composition owns its credential story", () => {
493
497
  const names = server.identityVerifiers.map((verifier) => verifier.name);
494
- expect(names).toEqual(["apikey", fakeVerifier.name]);
498
+ // The PlatformClient lane is core in every posture that verifies
499
+ // callers; the runner-subject lane is the built-in Authorizer's alone.
500
+ expect(names).toEqual(["apikey", "platform-client", fakeVerifier.name]);
495
501
  expect(names).not.toContain(RUNNER_VERIFIER_NAME);
496
502
  });
497
503
  });