@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
@@ -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 {
@@ -144,14 +144,18 @@ export function newResourceIdentityAccountStore(
144
144
  }
145
145
 
146
146
  /**
147
- * Non-federated: the platform's own subject. A federated account carries
148
- * its identity_provider_ref; open source never writes one (its create
149
- * path assigns `direct`), so this is the port's contract stated, not a
150
- * branch the OSS store expects to take.
147
+ * The platform's own subject: neither federated nor a platform-client end
148
+ * user. A federated account carries its identity_provider_ref (open source
149
+ * never writes one, so that arm is the port's contract stated); a
150
+ * platform-client account is the mint's, whose email and name the platform
151
+ * asserted, so no direct lookup — a verifier's subject resolve, getByEmail —
152
+ * may answer it.
151
153
  */
152
154
  function isDirect(account: IdentityAccount): boolean {
155
+ const mode = account.spec?.provisioningMode;
153
156
  return (
154
157
  account.spec?.identityProviderRef === undefined &&
155
- account.spec?.provisioningMode !== IdentityAccountProvisioningMode.federated
158
+ mode !== IdentityAccountProvisioningMode.federated &&
159
+ mode !== IdentityAccountProvisioningMode.platform_client
156
160
  );
157
161
  }
@@ -26,7 +26,8 @@
26
26
  * subject, never its slug (the guideline's "domains with different
27
27
  * uniqueness rules bring their own step under the same name").
28
28
  * - AssignBackendFields: `is_machine_account` from the `@clients`
29
- * suffix and `provisioning_mode direct` on create (the spec's
29
+ * suffix and the provisioning mode the path was given (`direct`, or
30
+ * `platform_client` for the mint's end users) on create (the spec's
30
31
  * "assigned by backend" fields); on update, every backend-assigned
31
32
  * field and the email are preserved from the existing row (A9: the
32
33
  * cloud's writable surface — first/last name, picture, preferences —
@@ -63,11 +64,15 @@ import { EXISTING_RESOURCE_KEY } from "../../pipeline/steps/load-existing.js";
63
64
  import { TARGET_RESOURCE_KEY } from "../../pipeline/steps/load-target.js";
64
65
  import { idValueOf, metadataOf } from "../../pipeline/steps/shapes.js";
65
66
  import {
67
+ PLATFORM_CLIENT_SUBJECT_PREFIX,
66
68
  accountIdFor,
67
69
  accountNotFoundMessage,
68
70
  idpIdImmutableMessage,
69
71
  isMachineSubject,
72
+ isPlatformClientSubject,
73
+ reservedSubjectMessage,
70
74
  } from "./constants.js";
75
+ import type { AccountProvisioning } from "./provisioning.js";
71
76
  import { DuplicateAccountError } from "./store.js";
72
77
  import type { IdentityAccountStore } from "./store.js";
73
78
 
@@ -158,11 +163,61 @@ export function assignDirectBackendFields(
158
163
  return spec;
159
164
  }
160
165
 
161
- export function newAssignBackendFieldsStep(): AccountStep {
166
+ /**
167
+ * The backend-assigned fields of a PLATFORM-CLIENT account: the machine
168
+ * flag from the subject's shape (the cloud's rule for every mode), mode
169
+ * `platform_client`, no provider ref — the PlatformClient admits the user
170
+ * at mint time and is no ongoing authentication authority.
171
+ */
172
+ function assignPlatformClientBackendFields(
173
+ spec: IdentityAccountSpec,
174
+ ): IdentityAccountSpec {
175
+ spec.isMachineAccount = isMachineSubject(spec.idpId);
176
+ spec.provisioningMode = IdentityAccountProvisioningMode.platform_client;
177
+ spec.identityProviderRef = undefined;
178
+ return spec;
179
+ }
180
+
181
+ /**
182
+ * AssignBackendFields on create, by how the account was provisioned. The
183
+ * `stgm_pc|` subject namespace is reserved in both directions: a direct
184
+ * account refuses it (INVALID_ARGUMENT, before anything is written), and a
185
+ * platform-client account must carry it — its absence is the mint's bug,
186
+ * never a client's input.
187
+ */
188
+ export function newAssignBackendFieldsStep(
189
+ provisioning: AccountProvisioning,
190
+ ): AccountStep {
162
191
  return {
163
192
  name: "AssignBackendFields",
164
193
  execute(ctx: AccountContext): void {
165
- assignDirectBackendFields(specOf(ctx));
194
+ const spec = specOf(ctx);
195
+ switch (provisioning.mode) {
196
+ case "direct":
197
+ if (isPlatformClientSubject(spec.idpId)) {
198
+ throw invalidArgumentError(reservedSubjectMessage(spec.idpId));
199
+ }
200
+ assignDirectBackendFields(spec);
201
+ return;
202
+ case "platform_client":
203
+ if (!isPlatformClientSubject(spec.idpId)) {
204
+ throw internalError(
205
+ new Error(
206
+ `a platform-client account's subject must carry the '${PLATFORM_CLIENT_SUBJECT_PREFIX}' prefix`,
207
+ ),
208
+ "assign backend fields",
209
+ );
210
+ }
211
+ assignPlatformClientBackendFields(spec);
212
+ return;
213
+ default: {
214
+ const exhaustive: never = provisioning;
215
+ throw internalError(
216
+ new Error(`unknown provisioning ${JSON.stringify(exhaustive)}`),
217
+ "assign backend fields",
218
+ );
219
+ }
220
+ }
166
221
  },
167
222
  };
168
223
  }
@@ -109,6 +109,31 @@ function federatedAccount(overrides: {
109
109
  });
110
110
  }
111
111
 
112
+ /**
113
+ * A platform-client end user as the mint provisions it: the reserved
114
+ * composite subject, mode `platform_client`, the owning organization, and
115
+ * the derived id of its subject (the id every subject lookup reads).
116
+ */
117
+ function platformClientAccount(overrides: {
118
+ idpId: string;
119
+ email: string;
120
+ }): IdentityAccount {
121
+ return create(IdentityAccountSchema, {
122
+ apiVersion: "iam.stigmer.ai/v1",
123
+ kind: "IdentityAccount",
124
+ metadata: {
125
+ id: accountIdFor(overrides.idpId),
126
+ name: overrides.email,
127
+ org: "acme",
128
+ },
129
+ spec: {
130
+ idpId: overrides.idpId,
131
+ email: overrides.email,
132
+ provisioningMode: IdentityAccountProvisioningMode.platform_client,
133
+ },
134
+ });
135
+ }
136
+
112
137
  /** A direct account with no subject — the row no lookup could ever reach. */
113
138
  function subjectlessDirectAccount(): IdentityAccount {
114
139
  return create(IdentityAccountSchema, {
@@ -357,6 +382,32 @@ const CASES: ReadonlyArray<PortContractDeclaration<IdentityAccountStore>> = [
357
382
  );
358
383
  },
359
384
  ],
385
+ [
386
+ "findDirectByIdpId and findDirectByEmail never answer a platform-client account, while findByIdpId (any mode) does",
387
+ async ({ store }) => {
388
+ await store.save(
389
+ platformClientAccount({
390
+ idpId: "stgm_pc|acme|user-7",
391
+ email: "pat@example.com",
392
+ }),
393
+ );
394
+ assert.equal(
395
+ await store.findDirectByIdpId("stgm_pc|acme|user-7"),
396
+ undefined,
397
+ "the direct-subject lookup must not resolve to an account a platform client provisioned",
398
+ );
399
+ assert.equal(
400
+ await store.findDirectByEmail("pat@example.com"),
401
+ undefined,
402
+ "the direct-email lookup must not answer an email a platform asserted",
403
+ );
404
+ assert.equal(
405
+ (await store.findByIdpId("stgm_pc|acme|user-7"))?.spec?.provisioningMode,
406
+ IdentityAccountProvisioningMode.platform_client,
407
+ "the any-mode subject lookup — the mint's — must answer the platform-client account",
408
+ );
409
+ },
410
+ ],
360
411
  [
361
412
  "findByIds answers one row per distinct id, in first-occurrence order; an empty request answers an empty list",
362
413
  async ({ store }) => {
@@ -22,12 +22,14 @@
22
22
  * with an empty `idp_id` is refused, since no lookup could reach it;
23
23
  * - `update` replaces, never creates: an unknown id writes nothing;
24
24
  * - `deleteById` of an unknown id resolves;
25
- * - `findDirectByIdpId` answers only non-federated accounts (the
26
- * platform's own subjects) — a customer's federated IdP may mint the
27
- * same `auth0|…` shape and the platform lane must never resolve to it;
25
+ * - `findDirectByIdpId` answers only the platform's own subjects —
26
+ * never a federated account (a customer's federated IdP may mint the
27
+ * same `auth0|…` shape and the platform lane must never resolve to it)
28
+ * and never a platform-client end user (its subject is the mint's);
28
29
  * - `findDirectByEmail` answers direct accounts only, by exact match:
29
30
  * federation legitimately duplicates emails, so the email axis is
30
- * non-unique;
31
+ * non-unique, and a platform-client account's email is one its
32
+ * platform asserted, so no email lookup may answer it;
31
33
  * - `findByIds` answers one row per distinct id, in first-occurrence
32
34
  * order, and skips unknown ids;
33
35
  * - a typed not-found reads as `undefined`; any other storage failure
@@ -59,7 +61,7 @@ export interface IdentityAccountStore {
59
61
  findById(id: string): Promise<IdentityAccount | undefined>;
60
62
  /** Any provisioning mode — the getByIdpId RPC's lookup. */
61
63
  findByIdpId(idpId: string): Promise<IdentityAccount | undefined>;
62
- /** Non-federated accounts only — the verifiers' and whoAmI's lookup. */
64
+ /** The platform's own subjects only (no federated, no platform-client) — the verifiers' and whoAmI's lookup. */
63
65
  findDirectByIdpId(idpId: string): Promise<IdentityAccount | undefined>;
64
66
  /** Direct accounts only, exact match — the getByEmail RPC's lookup. */
65
67
  findDirectByEmail(email: string): Promise<IdentityAccount | undefined>;
@@ -17,7 +17,8 @@
17
17
  * with the token fields empty (minting a proxy token is cloud-only);
18
18
  * - getServerInfo reports the build stamp by default and the release a
19
19
  * library consumer states through ComposeOptions.version otherwise
20
- * (stigmer#1168: unbundled, the stamp is always "dev");
20
+ * (stigmer#1168: unbundled, the stamp is always "dev"), and a
21
+ * trusted-local server's posture as an explicit false;
21
22
  * - getLicenseStatus answers `absent` with no claims and no key when no
22
23
  * provider is composed, and the composed provider's report verbatim
23
24
  * when one is — both stamped with the ONE instant the controller
@@ -138,6 +139,9 @@ describe("platform domain (composed server)", () => {
138
139
  // Unbundled runs report Go's unstamped default; release bundles stamp
139
140
  // the esbuild define (scripts/bundle-slim.mjs).
140
141
  expect(first.version).toBe("dev");
142
+ // A server that trusts every request says so on the wire: an explicit
143
+ // false, never an absent field a console would read as "unknown".
144
+ expect(first.authenticationRequired).toBe(false);
141
145
 
142
146
  const second = await client.getServerInfo({});
143
147
  expect(second.edition).toBe(first.edition);
@@ -285,6 +289,7 @@ describe("platform domain (keyless runner-token service)", () => {
285
289
  ),
286
290
  edition: ServerEdition.oss,
287
291
  version: "dev",
292
+ authenticationRequired: false,
288
293
  licenseStatus: ABSENT_LICENSE_STATUS,
289
294
  now: () => new Date(),
290
295
  logger: silentLogger,
@@ -320,6 +325,7 @@ describe("platform domain (license status)", () => {
320
325
  ),
321
326
  edition: ServerEdition.enterprise,
322
327
  version: "dev",
328
+ authenticationRequired: false,
323
329
  licenseStatus: provider,
324
330
  now: () => instant,
325
331
  logger: silentLogger,
@@ -106,6 +106,16 @@ export interface PlatformControllerDeps {
106
106
  * states its installed release and answers exactly as a bundle would.
107
107
  */
108
108
  readonly version: string;
109
+ /**
110
+ * The resolved require-authentication posture (compose.ts: the OSS
111
+ * issuer OR a unit's declaration), reported so a console can tell a
112
+ * server that verifies its callers from one that trusts every request —
113
+ * the features that hand out credentials only a verifying server
114
+ * honours (PlatformClient token minting) are unavailable on the latter.
115
+ * Always set on the wire, true or false: the field's absence is how a
116
+ * client recognises a server that predates it.
117
+ */
118
+ readonly authenticationRequired: boolean;
109
119
  /**
110
120
  * What license this server holds (extensions/license-status.ts). The
111
121
  * composition root installs the built-in `absent` provider when no unit
@@ -136,11 +146,12 @@ export function registerPlatformServices(
136
146
  });
137
147
  }
138
148
 
139
- /** Go GetServerInfo: the server edition and release version. */
149
+ /** Go GetServerInfo: the server edition, release version and authentication posture. */
140
150
  function getServerInfo(deps: PlatformControllerDeps): GetServerInfoOutput {
141
151
  return create(GetServerInfoOutputSchema, {
142
152
  edition: deps.edition,
143
153
  version: deps.version,
154
+ authenticationRequired: deps.authenticationRequired,
144
155
  });
145
156
  }
146
157