@stigmer/server 3.14.1 → 3.15.1
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.
- package/dist/boot/compose.d.ts +8 -1
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +121 -14
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/config.d.ts +14 -0
- package/dist/boot/config.d.ts.map +1 -1
- package/dist/boot/config.js +1 -0
- package/dist/boot/config.js.map +1 -1
- package/dist/domain/apikey/verifier.d.ts +8 -1
- package/dist/domain/apikey/verifier.d.ts.map +1 -1
- package/dist/domain/apikey/verifier.js +14 -2
- package/dist/domain/apikey/verifier.js.map +1 -1
- package/dist/domain/identityaccount/constants.d.ts +66 -0
- package/dist/domain/identityaccount/constants.d.ts.map +1 -0
- package/dist/domain/identityaccount/constants.js +153 -0
- package/dist/domain/identityaccount/constants.js.map +1 -0
- package/dist/domain/identityaccount/controller.d.ts +34 -0
- package/dist/domain/identityaccount/controller.d.ts.map +1 -0
- package/dist/domain/identityaccount/controller.js +342 -0
- package/dist/domain/identityaccount/controller.js.map +1 -0
- package/dist/domain/identityaccount/operator.d.ts +14 -0
- package/dist/domain/identityaccount/operator.d.ts.map +1 -0
- package/dist/domain/identityaccount/operator.js +67 -0
- package/dist/domain/identityaccount/operator.js.map +1 -0
- package/dist/domain/identityaccount/provisioning.d.ts +52 -0
- package/dist/domain/identityaccount/provisioning.d.ts.map +1 -0
- package/dist/domain/identityaccount/provisioning.js +114 -0
- package/dist/domain/identityaccount/provisioning.js.map +1 -0
- package/dist/domain/identityaccount/resolve.d.ts +36 -0
- package/dist/domain/identityaccount/resolve.d.ts.map +1 -0
- package/dist/domain/identityaccount/resolve.js +12 -0
- package/dist/domain/identityaccount/resolve.js.map +1 -0
- package/dist/domain/identityaccount/resource-store.d.ts +4 -0
- package/dist/domain/identityaccount/resource-store.d.ts.map +1 -0
- package/dist/domain/identityaccount/resource-store.js +131 -0
- package/dist/domain/identityaccount/resource-store.js.map +1 -0
- package/dist/domain/identityaccount/steps.d.ts +79 -0
- package/dist/domain/identityaccount/steps.d.ts.map +1 -0
- package/dist/domain/identityaccount/steps.js +291 -0
- package/dist/domain/identityaccount/steps.js.map +1 -0
- package/dist/domain/identityaccount/store-contract.d.ts +22 -0
- package/dist/domain/identityaccount/store-contract.d.ts.map +1 -0
- package/dist/domain/identityaccount/store-contract.js +307 -0
- package/dist/domain/identityaccount/store-contract.js.map +1 -0
- package/dist/domain/identityaccount/store.d.ts +66 -0
- package/dist/domain/identityaccount/store.d.ts.map +1 -0
- package/dist/domain/identityaccount/store.js +13 -0
- package/dist/domain/identityaccount/store.js.map +1 -0
- package/dist/extensions/drivers.d.ts +26 -0
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/gate-slots.d.ts +16 -1
- package/dist/extensions/gate-slots.d.ts.map +1 -1
- package/dist/extensions/gate-slots.js +1 -0
- package/dist/extensions/gate-slots.js.map +1 -1
- package/dist/extensions/identity-federation.d.ts +36 -0
- package/dist/extensions/identity-federation.d.ts.map +1 -0
- package/dist/extensions/identity-federation.js +2 -0
- package/dist/extensions/identity-federation.js.map +1 -0
- package/dist/extensions/registry.d.ts +18 -1
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +20 -0
- package/dist/extensions/registry.js.map +1 -1
- package/dist/identity/oidc-discovery.d.ts +36 -0
- package/dist/identity/oidc-discovery.d.ts.map +1 -0
- package/dist/identity/oidc-discovery.js +60 -0
- package/dist/identity/oidc-discovery.js.map +1 -0
- package/dist/identity/oidc-userinfo.d.ts +30 -0
- package/dist/identity/oidc-userinfo.d.ts.map +1 -0
- package/dist/identity/oidc-userinfo.js +28 -0
- package/dist/identity/oidc-userinfo.js.map +1 -0
- package/dist/identity/oidc-verifier.d.ts +6 -0
- package/dist/identity/oidc-verifier.d.ts.map +1 -1
- package/dist/identity/oidc-verifier.js +35 -34
- package/dist/identity/oidc-verifier.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/interceptors/auth.d.ts +10 -0
- package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
- package/dist/pipeline/interceptors/auth.js +9 -1
- package/dist/pipeline/interceptors/auth.js.map +1 -1
- package/dist/transport/console/handler.d.ts +22 -5
- package/dist/transport/console/handler.d.ts.map +1 -1
- package/dist/transport/console/handler.js +53 -24
- package/dist/transport/console/handler.js.map +1 -1
- package/package.json +4 -4
- package/src/boot/__tests__/auth-enabled-composition.test.ts +274 -4
- package/src/boot/__tests__/config.test.ts +41 -3
- package/src/boot/compose.ts +148 -15
- package/src/boot/config.ts +15 -0
- package/src/domain/apikey/__tests__/verifier.test.ts +146 -22
- package/src/domain/apikey/verifier.ts +24 -2
- package/src/domain/identityaccount/__tests__/constants.test.ts +155 -0
- package/src/domain/identityaccount/__tests__/identityaccount.test.ts +484 -0
- package/src/domain/identityaccount/__tests__/operator.test.ts +180 -0
- package/src/domain/identityaccount/__tests__/provisioning.test.ts +281 -0
- package/src/domain/identityaccount/__tests__/resolve.test.ts +114 -0
- package/src/domain/identityaccount/__tests__/resource-store.test.ts +216 -0
- package/src/domain/identityaccount/__tests__/support.ts +58 -0
- package/src/domain/identityaccount/constants.ts +178 -0
- package/src/domain/identityaccount/controller.ts +667 -0
- package/src/domain/identityaccount/operator.ts +94 -0
- package/src/domain/identityaccount/provisioning.ts +182 -0
- package/src/domain/identityaccount/resolve.ts +52 -0
- package/src/domain/identityaccount/resource-store.ts +157 -0
- package/src/domain/identityaccount/steps.ts +377 -0
- package/src/domain/identityaccount/store-contract.ts +471 -0
- package/src/domain/identityaccount/store.ts +70 -0
- package/src/extensions/__tests__/identity-account-composed.test.ts +803 -0
- package/src/extensions/__tests__/identity-account-points.test.ts +129 -0
- package/src/extensions/__tests__/registry.test.ts +4 -0
- package/src/extensions/__tests__/tier-truthfulness.test.ts +299 -0
- package/src/extensions/drivers.ts +26 -0
- package/src/extensions/gate-slots.ts +16 -0
- package/src/extensions/identity-federation.ts +58 -0
- package/src/extensions/registry.ts +44 -1
- package/src/identity/__tests__/oidc-discovery.test.ts +122 -0
- package/src/identity/__tests__/oidc-userinfo.test.ts +103 -0
- package/src/identity/__tests__/oidc-verifier.test.ts +173 -3
- package/src/identity/oidc-discovery.ts +84 -0
- package/src/identity/oidc-userinfo.ts +64 -0
- package/src/identity/oidc-verifier.ts +42 -45
- package/src/index.ts +19 -0
- package/src/pipeline/interceptors/auth.ts +13 -1
- package/src/pipeline/steps/__tests__/authorize-annotation-completeness.test.ts +184 -0
- package/src/transport/console/__tests__/handler.test.ts +93 -23
- package/src/transport/console/__tests__/resolver.test.ts +6 -0
- package/src/transport/console/handler.ts +84 -30
|
@@ -14,9 +14,24 @@
|
|
|
14
14
|
* owning user (the runner's credential lane, ruling Q3 — the runner
|
|
15
15
|
* presents exactly such a key via STIGMER_TOKEN);
|
|
16
16
|
* 4. deleting the key revokes it on the very next request;
|
|
17
|
-
* 5. garbage credentials keep the Q6 unclaimed-token rejection
|
|
17
|
+
* 5. garbage credentials keep the Q6 unclaimed-token rejection;
|
|
18
|
+
* 6. (20260911.11) the identity-account arms: an unprovisioned subject is
|
|
19
|
+
* idp-shaped (whoAmI NOT_FOUND, writes stamped with the raw sub);
|
|
20
|
+
* provisionMyAccount creates the account under the derived id with
|
|
21
|
+
* the issuer's userinfo profile; from then on the verifier resolves
|
|
22
|
+
* the subject to the account id and API keys minted before OR after
|
|
23
|
+
* provisioning answer whoAmI with the owner's account; no operator
|
|
24
|
+
* account exists under the posture;
|
|
25
|
+
* 7. (20260913.02, sp.console-login) the console the server serves is
|
|
26
|
+
* told the posture through /config.json — the issuer and audience
|
|
27
|
+
* this composition runs under, with the console client id the
|
|
28
|
+
* operator did NOT register here left empty — and the composition
|
|
29
|
+
* root WARNs at wiring time that the served console cannot sign in
|
|
30
|
+
* (Q-CL-2), the STIGMER_OAUTH_REDIRECT_URI precedent. Proven through
|
|
31
|
+
* compose.ts with STIGMER_CONSOLE_DIR at a fixture export, not
|
|
32
|
+
* through the lane alone (handler.test.ts covers that).
|
|
18
33
|
*/
|
|
19
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
34
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
20
35
|
import { tmpdir } from "node:os";
|
|
21
36
|
import path from "node:path";
|
|
22
37
|
import { createServer } from "node:http";
|
|
@@ -30,8 +45,12 @@ import { SignJWT, exportJWK, generateKeyPair } from "jose";
|
|
|
30
45
|
import type { GenerateKeyPairResult } from "jose";
|
|
31
46
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
32
47
|
|
|
48
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
33
49
|
import { ApiKeyCommandController } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/command_pb";
|
|
34
50
|
import { ApiKeyQueryController } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/query_pb";
|
|
51
|
+
import { IdentityAccountCommandController } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/command_pb";
|
|
52
|
+
import { IdentityAccountProvisioningMode } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/enum_pb";
|
|
53
|
+
import { IdentityAccountQueryController } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/query_pb";
|
|
35
54
|
import { PlatformQueryController } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
|
|
36
55
|
import {
|
|
37
56
|
Health,
|
|
@@ -42,6 +61,11 @@ import { loadConfig } from "../config.js";
|
|
|
42
61
|
import { composeServer } from "../compose.js";
|
|
43
62
|
import type { ComposedServer } from "../compose.js";
|
|
44
63
|
import { createLogger } from "../logger.js";
|
|
64
|
+
import {
|
|
65
|
+
ACCOUNT_NOT_FOUND_FOR_CALLER_MESSAGE,
|
|
66
|
+
USERINFO_UNAVAILABLE_PREFIX,
|
|
67
|
+
accountIdFor,
|
|
68
|
+
} from "../../domain/identityaccount/constants.js";
|
|
45
69
|
import { AUTHENTICATION_TOKEN_MISSING_MESSAGE } from "../../pipeline/interceptors/auth.js";
|
|
46
70
|
|
|
47
71
|
const AUDIENCE = "https://api.stigmer.test/";
|
|
@@ -50,8 +74,18 @@ let dir: string;
|
|
|
50
74
|
let issuerServer: Server;
|
|
51
75
|
let issuer: string;
|
|
52
76
|
let privateKey: GenerateKeyPairResult["privateKey"];
|
|
77
|
+
// A lever for the UNAVAILABLE arm: the issuer's /userinfo answers 503 for
|
|
78
|
+
// this subject while set. Undefined = every subject is served.
|
|
79
|
+
let userinfoRefusesSub: string | undefined;
|
|
53
80
|
let server: ComposedServer;
|
|
54
81
|
let port: number;
|
|
82
|
+
// A key minted while its owner was still idp-shaped: its creator stamp is
|
|
83
|
+
// the raw sub. The identity-account arms prove that stamp resolves to the
|
|
84
|
+
// owner's account once one exists (20260911.11 A1's derived id).
|
|
85
|
+
let keyMintedBeforeProvisioning: string;
|
|
86
|
+
// Every WARN the composition root logged while wiring — the console
|
|
87
|
+
// sign-in arm (7) reads it.
|
|
88
|
+
const warnings: string[] = [];
|
|
55
89
|
|
|
56
90
|
function bearer(token: string): Interceptor {
|
|
57
91
|
return (next) => (request) => {
|
|
@@ -88,8 +122,16 @@ beforeAll(async () => {
|
|
|
88
122
|
};
|
|
89
123
|
issuerServer = createServer((req, res) => {
|
|
90
124
|
if (req.url === "/.well-known/openid-configuration") {
|
|
125
|
+
// The provisioning lane reads userinfo_endpoint from here (A8) —
|
|
126
|
+
// never a guessed `<issuer>/userinfo`.
|
|
91
127
|
res.setHeader("content-type", "application/json");
|
|
92
|
-
res.end(
|
|
128
|
+
res.end(
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
issuer,
|
|
131
|
+
jwks_uri: `${issuer}/jwks`,
|
|
132
|
+
userinfo_endpoint: `${issuer}/userinfo`,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
93
135
|
return;
|
|
94
136
|
}
|
|
95
137
|
if (req.url === "/jwks") {
|
|
@@ -97,6 +139,43 @@ beforeAll(async () => {
|
|
|
97
139
|
res.end(JSON.stringify({ keys: [jwk] }));
|
|
98
140
|
return;
|
|
99
141
|
}
|
|
142
|
+
if (req.url === "/userinfo") {
|
|
143
|
+
// The provisioning lane's profile source (20260911.11 A1/Q-IA-2):
|
|
144
|
+
// answers the bearer's own claims as an IdP would, given_name and
|
|
145
|
+
// family_name included — the signature was already proven upstream.
|
|
146
|
+
const header = req.headers.authorization ?? "";
|
|
147
|
+
const token = header.startsWith("Bearer ")
|
|
148
|
+
? header.slice("Bearer ".length)
|
|
149
|
+
: "";
|
|
150
|
+
const payloadSegment = token.split(".")[1];
|
|
151
|
+
if (payloadSegment === undefined) {
|
|
152
|
+
res.statusCode = 401;
|
|
153
|
+
res.end();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const claims = JSON.parse(
|
|
157
|
+
Buffer.from(payloadSegment, "base64url").toString("utf8"),
|
|
158
|
+
) as { sub?: string; email?: string };
|
|
159
|
+
if (
|
|
160
|
+
userinfoRefusesSub !== undefined &&
|
|
161
|
+
claims.sub === userinfoRefusesSub
|
|
162
|
+
) {
|
|
163
|
+
res.statusCode = 503;
|
|
164
|
+
res.end();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
res.setHeader("content-type", "application/json");
|
|
168
|
+
res.end(
|
|
169
|
+
JSON.stringify({
|
|
170
|
+
sub: claims.sub ?? "",
|
|
171
|
+
email: claims.email ?? "",
|
|
172
|
+
given_name: "Composed",
|
|
173
|
+
family_name: "User",
|
|
174
|
+
picture: `${issuer}/pictures/${encodeURIComponent(claims.sub ?? "")}`,
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
100
179
|
res.statusCode = 404;
|
|
101
180
|
res.end();
|
|
102
181
|
});
|
|
@@ -109,6 +188,10 @@ beforeAll(async () => {
|
|
|
109
188
|
issuer = `http://127.0.0.1:${address.port}`;
|
|
110
189
|
|
|
111
190
|
dir = mkdtempSync(path.join(tmpdir(), "auth-enabled-test-"));
|
|
191
|
+
// The smallest export the console lane discovers: the app shell alone.
|
|
192
|
+
const consoleDir = path.join(dir, "console");
|
|
193
|
+
mkdirSync(consoleDir, { recursive: true });
|
|
194
|
+
writeFileSync(path.join(consoleDir, "index.html"), "<html>app shell</html>");
|
|
112
195
|
server = await composeServer({
|
|
113
196
|
config: loadConfig({
|
|
114
197
|
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
@@ -118,8 +201,18 @@ beforeAll(async () => {
|
|
|
118
201
|
ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
|
|
119
202
|
STIGMER_OIDC_ISSUER: issuer,
|
|
120
203
|
STIGMER_OIDC_AUDIENCE: AUDIENCE,
|
|
204
|
+
STIGMER_CONSOLE_DIR: consoleDir,
|
|
205
|
+
}),
|
|
206
|
+
logger: createLogger({
|
|
207
|
+
level: "warn",
|
|
208
|
+
pretty: false,
|
|
209
|
+
write: (line) => {
|
|
210
|
+
const entry = JSON.parse(line) as { level?: string; message?: string };
|
|
211
|
+
if (entry.level === "warn" && entry.message !== undefined) {
|
|
212
|
+
warnings.push(entry.message);
|
|
213
|
+
}
|
|
214
|
+
},
|
|
121
215
|
}),
|
|
122
|
-
logger: createLogger({ level: "error", pretty: false, write: () => {} }),
|
|
123
216
|
portOverride: 0,
|
|
124
217
|
host: "127.0.0.1",
|
|
125
218
|
});
|
|
@@ -168,6 +261,27 @@ describe("the require-authentication posture on the wire", () => {
|
|
|
168
261
|
});
|
|
169
262
|
});
|
|
170
263
|
|
|
264
|
+
describe("the console the server serves is told the posture (20260913.02, sp.console-login)", () => {
|
|
265
|
+
it("/config.json carries the issuer and audience this composition verifies against, with the unregistered console client id empty", async () => {
|
|
266
|
+
const response = await fetch(`http://127.0.0.1:${port}/config.json`);
|
|
267
|
+
expect(response.status).toBe(200);
|
|
268
|
+
expect(await response.json()).toEqual({
|
|
269
|
+
apiUrl: "",
|
|
270
|
+
appUrl: "",
|
|
271
|
+
authMode: "oidc",
|
|
272
|
+
oidcIssuer: issuer,
|
|
273
|
+
oidcClientId: "",
|
|
274
|
+
oidcAudience: AUDIENCE,
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("the composition root WARNs at wiring time that the served console cannot sign in without STIGMER_OIDC_CONSOLE_CLIENT_ID (Q-CL-2)", () => {
|
|
279
|
+
expect(warnings).toContainEqual(
|
|
280
|
+
expect.stringContaining("STIGMER_OIDC_CONSOLE_CLIENT_ID"),
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
171
285
|
describe("the full credential loop (OIDC login → API key → revocation)", () => {
|
|
172
286
|
let apiKeyPlaintext: string;
|
|
173
287
|
let apiKeyId: string;
|
|
@@ -221,6 +335,7 @@ describe("the full credential loop (OIDC login → API key → revocation)", ()
|
|
|
221
335
|
expect(second.status?.audit?.specAudit?.createdBy?.id).toBe(
|
|
222
336
|
"auth0|loop-user",
|
|
223
337
|
);
|
|
338
|
+
keyMintedBeforeProvisioning = second.spec?.keyHash ?? "";
|
|
224
339
|
});
|
|
225
340
|
|
|
226
341
|
it("deleting the key revokes it on the very next request", async () => {
|
|
@@ -243,3 +358,158 @@ describe("the full credential loop (OIDC login → API key → revocation)", ()
|
|
|
243
358
|
expect(ConnectError.from(error).rawMessage).toBe("invalid token");
|
|
244
359
|
});
|
|
245
360
|
});
|
|
361
|
+
|
|
362
|
+
describe("identity accounts under the OIDC posture (20260911.11; Q-IA-2, A1, A2)", () => {
|
|
363
|
+
const SUB = "auth0|loop-user";
|
|
364
|
+
const accountId = accountIdFor(SUB);
|
|
365
|
+
|
|
366
|
+
it("no operator account exists under an authentication posture — the boot-time ensure is trusted-local only", async () => {
|
|
367
|
+
const rows = await server.store.listResources(
|
|
368
|
+
ApiResourceKind.identity_account,
|
|
369
|
+
);
|
|
370
|
+
expect(rows).toHaveLength(0);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("whoAmI for an unprovisioned subject is NOT_FOUND with the cloud's copy, and its writes stay stamped with the raw sub", async () => {
|
|
374
|
+
const token = await mintOidcToken(SUB, "loop@example.com");
|
|
375
|
+
const query = createClient(
|
|
376
|
+
IdentityAccountQueryController,
|
|
377
|
+
transportWith(token),
|
|
378
|
+
);
|
|
379
|
+
const error = await query.whoAmI({}).catch((e: unknown) => e);
|
|
380
|
+
expect(ConnectError.from(error).code).toBe(Code.NotFound);
|
|
381
|
+
expect(ConnectError.from(error).rawMessage).toBe(
|
|
382
|
+
ACCOUNT_NOT_FOUND_FOR_CALLER_MESSAGE,
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("provisionMyAccount creates the account under the derived id with the userinfo profile", async () => {
|
|
387
|
+
const token = await mintOidcToken(SUB, "loop@example.com");
|
|
388
|
+
const command = createClient(
|
|
389
|
+
IdentityAccountCommandController,
|
|
390
|
+
transportWith(token),
|
|
391
|
+
);
|
|
392
|
+
const account = await command.provisionMyAccount({});
|
|
393
|
+
expect(account.metadata?.id).toBe(accountId);
|
|
394
|
+
expect(account.metadata?.name).toBe("loop@example.com");
|
|
395
|
+
expect(account.spec).toMatchObject({
|
|
396
|
+
idpId: SUB,
|
|
397
|
+
email: "loop@example.com",
|
|
398
|
+
firstName: "Composed",
|
|
399
|
+
lastName: "User",
|
|
400
|
+
provisioningMode: IdentityAccountProvisioningMode.direct,
|
|
401
|
+
isMachineAccount: false,
|
|
402
|
+
});
|
|
403
|
+
// The account is created BY the subject it is for: the stamp is the
|
|
404
|
+
// sub the verifier admitted idp-shaped — no account existed yet.
|
|
405
|
+
expect(account.status?.audit?.specAudit?.createdBy?.id).toBe(SUB);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("from the next request on, the verifier resolves the subject to the account id — whoAmI answers and writes are stamped with it", async () => {
|
|
409
|
+
const token = await mintOidcToken(SUB, "loop@example.com");
|
|
410
|
+
const query = createClient(
|
|
411
|
+
IdentityAccountQueryController,
|
|
412
|
+
transportWith(token),
|
|
413
|
+
);
|
|
414
|
+
const me = await query.whoAmI({});
|
|
415
|
+
expect(me.metadata?.id).toBe(accountId);
|
|
416
|
+
|
|
417
|
+
const keys = createClient(ApiKeyCommandController, transportWith(token));
|
|
418
|
+
const key = await keys.create({
|
|
419
|
+
apiVersion: "iam.stigmer.ai/v1",
|
|
420
|
+
kind: "ApiKey",
|
|
421
|
+
metadata: { name: "minted after provisioning", org: "local" },
|
|
422
|
+
spec: {},
|
|
423
|
+
});
|
|
424
|
+
expect(key.status?.audit?.specAudit?.createdBy?.id).toBe(accountId);
|
|
425
|
+
expect(key.status?.audit?.specAudit?.createdBy?.email).toBe(
|
|
426
|
+
"loop@example.com",
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// Over that key, whoAmI is the owner's account — the key is the user.
|
|
430
|
+
const overKey = createClient(
|
|
431
|
+
IdentityAccountQueryController,
|
|
432
|
+
transportWith(key.spec?.keyHash ?? ""),
|
|
433
|
+
);
|
|
434
|
+
expect((await overKey.whoAmI({})).metadata?.id).toBe(accountId);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("a key minted BEFORE provisioning still resolves to the owner's account — the raw-sub stamp derives to the same id", async () => {
|
|
438
|
+
const overLegacyKey = createClient(
|
|
439
|
+
IdentityAccountQueryController,
|
|
440
|
+
transportWith(keyMintedBeforeProvisioning),
|
|
441
|
+
);
|
|
442
|
+
expect((await overLegacyKey.whoAmI({})).metadata?.id).toBe(accountId);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("provisionMyAccount is idempotent for a provisioned subject", async () => {
|
|
446
|
+
const token = await mintOidcToken(SUB, "loop@example.com");
|
|
447
|
+
const command = createClient(
|
|
448
|
+
IdentityAccountCommandController,
|
|
449
|
+
transportWith(token),
|
|
450
|
+
);
|
|
451
|
+
const before = (
|
|
452
|
+
await server.store.listResources(ApiResourceKind.identity_account)
|
|
453
|
+
).length;
|
|
454
|
+
const again = await command.provisionMyAccount({});
|
|
455
|
+
expect(again.metadata?.id).toBe(accountId);
|
|
456
|
+
expect(
|
|
457
|
+
(await server.store.listResources(ApiResourceKind.identity_account))
|
|
458
|
+
.length,
|
|
459
|
+
).toBe(before);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("two concurrent first logins for one subject end in exactly one account", async () => {
|
|
463
|
+
const token = await mintOidcToken("auth0|racer", "racer@example.com");
|
|
464
|
+
const command = createClient(
|
|
465
|
+
IdentityAccountCommandController,
|
|
466
|
+
transportWith(token),
|
|
467
|
+
);
|
|
468
|
+
const before = (
|
|
469
|
+
await server.store.listResources(ApiResourceKind.identity_account)
|
|
470
|
+
).length;
|
|
471
|
+
const [a, b] = await Promise.all([
|
|
472
|
+
command.provisionMyAccount({}),
|
|
473
|
+
command.provisionMyAccount({}),
|
|
474
|
+
]);
|
|
475
|
+
expect(a.metadata?.id).toBe(accountIdFor("auth0|racer"));
|
|
476
|
+
expect(b.metadata?.id).toBe(a.metadata?.id);
|
|
477
|
+
expect(
|
|
478
|
+
(await server.store.listResources(ApiResourceKind.identity_account))
|
|
479
|
+
.length,
|
|
480
|
+
).toBe(before + 1);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("a userinfo failure is UNAVAILABLE with the cloud's copy, and no account is created", async () => {
|
|
484
|
+
// A real token (the verifier must admit it) whose subject the issuer's
|
|
485
|
+
// /userinfo refuses while the lever is set.
|
|
486
|
+
const token = await mintOidcToken(
|
|
487
|
+
"auth0|userinfo-down",
|
|
488
|
+
"down@example.com",
|
|
489
|
+
);
|
|
490
|
+
userinfoRefusesSub = "auth0|userinfo-down";
|
|
491
|
+
try {
|
|
492
|
+
const command = createClient(
|
|
493
|
+
IdentityAccountCommandController,
|
|
494
|
+
transportWith(token),
|
|
495
|
+
);
|
|
496
|
+
const error = await command
|
|
497
|
+
.provisionMyAccount({})
|
|
498
|
+
.catch((e: unknown) => e);
|
|
499
|
+
expect(ConnectError.from(error).code).toBe(Code.Unavailable);
|
|
500
|
+
expect(
|
|
501
|
+
ConnectError.from(error).rawMessage.startsWith(
|
|
502
|
+
USERINFO_UNAVAILABLE_PREFIX,
|
|
503
|
+
),
|
|
504
|
+
).toBe(true);
|
|
505
|
+
const query = createClient(
|
|
506
|
+
IdentityAccountQueryController,
|
|
507
|
+
transportWith(token),
|
|
508
|
+
);
|
|
509
|
+
const missing = await query.whoAmI({}).catch((e: unknown) => e);
|
|
510
|
+
expect(ConnectError.from(missing).code).toBe(Code.NotFound);
|
|
511
|
+
} finally {
|
|
512
|
+
userinfoRefusesSub = undefined;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
});
|
|
@@ -59,15 +59,53 @@ describe("loadConfig", () => {
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it("reads an explicit ARTIFACT_HTTP_HOST (the container override)", () => {
|
|
62
|
-
expect(
|
|
63
|
-
|
|
64
|
-
)
|
|
62
|
+
expect(loadConfig({ ARTIFACT_HTTP_HOST: "0.0.0.0" }).artifactHttpHost).toBe(
|
|
63
|
+
"0.0.0.0",
|
|
64
|
+
);
|
|
65
65
|
// Empty string is absent, per the Go getEnvString leniency.
|
|
66
66
|
expect(loadConfig({ ARTIFACT_HTTP_HOST: "" }).artifactHttpHost).toBe(
|
|
67
67
|
"127.0.0.1",
|
|
68
68
|
);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
// 20260913.02 (sp.console-login, Q-CL-1): the console's public PKCE client
|
|
72
|
+
// id is lenient on purpose — a CLI-only self-host that set the issuer
|
|
73
|
+
// before this knob existed must keep booting on upgrade, and the served
|
|
74
|
+
// console reports the gap itself (Q-CL-2).
|
|
75
|
+
describe("STIGMER_OIDC_CONSOLE_CLIENT_ID (the console's sign-in client)", () => {
|
|
76
|
+
const OIDC = {
|
|
77
|
+
STIGMER_OIDC_ISSUER: "https://auth.example.com/realms/main",
|
|
78
|
+
STIGMER_OIDC_AUDIENCE: "https://stigmer.example.com/",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
it("is empty when absent, and the issuer alone still boots", () => {
|
|
82
|
+
expect(loadConfig(OIDC).oidcConsoleClientId).toBe("");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("reads the explicit value", () => {
|
|
86
|
+
expect(
|
|
87
|
+
loadConfig({
|
|
88
|
+
...OIDC,
|
|
89
|
+
STIGMER_OIDC_CONSOLE_CLIENT_ID: "stigmer-console",
|
|
90
|
+
}).oidcConsoleClientId,
|
|
91
|
+
).toBe("stigmer-console");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("treats an empty string as absent (Go's getEnvString)", () => {
|
|
95
|
+
expect(
|
|
96
|
+
loadConfig({ ...OIDC, STIGMER_OIDC_CONSOLE_CLIENT_ID: "" })
|
|
97
|
+
.oidcConsoleClientId,
|
|
98
|
+
).toBe("");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("is carried even without an issuer — the posture, not this knob, decides whether it is used", () => {
|
|
102
|
+
expect(
|
|
103
|
+
loadConfig({ STIGMER_OIDC_CONSOLE_CLIENT_ID: "stigmer-console" })
|
|
104
|
+
.oidcConsoleClientId,
|
|
105
|
+
).toBe("stigmer-console");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
71
109
|
it("disables the model-registry refresh only for the literal 'off'", () => {
|
|
72
110
|
expect(
|
|
73
111
|
loadConfig({ STIGMER_MODEL_REGISTRY_REFRESH: "off" })
|
package/src/boot/compose.ts
CHANGED
|
@@ -28,6 +28,7 @@ import path from "node:path";
|
|
|
28
28
|
|
|
29
29
|
import type { ConnectRouter, Transport } from "@connectrpc/connect";
|
|
30
30
|
|
|
31
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
31
32
|
import { HealthCheckResponse_ServingStatus as ServingStatus } from "@stigmer/protos/grpc/health/v1/health_pb";
|
|
32
33
|
|
|
33
34
|
import { registerAgentServices } from "../domain/agent/controller.js";
|
|
@@ -64,6 +65,15 @@ import { registerChannelConversationServices } from "../domain/agentchannel/conv
|
|
|
64
65
|
import { registerChannelMessageServices } from "../domain/agentchannel/message.js";
|
|
65
66
|
import { registerAgentShareServices } from "../domain/agentshare/controller.js";
|
|
66
67
|
import { registerApiKeyServices } from "../domain/apikey/controller.js";
|
|
68
|
+
import {
|
|
69
|
+
newCreateAccountPath,
|
|
70
|
+
registerIdentityAccountServices,
|
|
71
|
+
} from "../domain/identityaccount/controller.js";
|
|
72
|
+
import { ensureOperatorAccount } from "../domain/identityaccount/operator.js";
|
|
73
|
+
import { newDirectAccountProvisioner } from "../domain/identityaccount/provisioning.js";
|
|
74
|
+
import { newResourceIdentityAccountStore } from "../domain/identityaccount/resource-store.js";
|
|
75
|
+
import { newIssuerDiscovery } from "../identity/oidc-discovery.js";
|
|
76
|
+
import { newOidcUserInfoClient } from "../identity/oidc-userinfo.js";
|
|
67
77
|
import { newApiKeyIdentityVerifier } from "../domain/apikey/verifier.js";
|
|
68
78
|
import type { IdentityVerifier } from "../extensions/identity.js";
|
|
69
79
|
import { registerArtifactServices } from "../domain/artifact/controller.js";
|
|
@@ -117,6 +127,7 @@ import {
|
|
|
117
127
|
} from "../observability/rpc-metrics.js";
|
|
118
128
|
import { buildInterceptorChain } from "../pipeline/chain.js";
|
|
119
129
|
import { createVerifierChainInterceptor } from "../pipeline/interceptors/auth.js";
|
|
130
|
+
import { operatorIdentitySnapshot } from "../pipeline/steps/defaults.js";
|
|
120
131
|
import { createErrorBoundaryInterceptor } from "../pipeline/interceptors/error-boundary.js";
|
|
121
132
|
import { createRequestMetricsInterceptor } from "../pipeline/interceptors/request-metrics.js";
|
|
122
133
|
import { newPermissiveSingleTeamAuthorizer } from "../pipeline/steps/authorize.js";
|
|
@@ -150,7 +161,10 @@ import { newWorkflowExecutionEngineStateProvider } from "../temporal/workflowexe
|
|
|
150
161
|
import { newWorkflowExecutionWorkerFactory } from "../temporal/workflowexecution/worker.js";
|
|
151
162
|
import { HealthState, registerHealthService } from "../transport/health.js";
|
|
152
163
|
import { resolveConsoleAssets } from "../transport/console/assets.js";
|
|
153
|
-
import {
|
|
164
|
+
import {
|
|
165
|
+
createConsoleLane,
|
|
166
|
+
type ConsoleSignInPosture,
|
|
167
|
+
} from "../transport/console/handler.js";
|
|
154
168
|
import { createRegistryLanes } from "../transport/registry/lanes.js";
|
|
155
169
|
import { createUnifiedPortServer } from "../transport/server.js";
|
|
156
170
|
import type { ServerConfig } from "./config.js";
|
|
@@ -221,6 +235,13 @@ export interface ComposedServer {
|
|
|
221
235
|
* them or reorder them, and both are the drift DD-007 rules out.
|
|
222
236
|
*/
|
|
223
237
|
identityVerifiers: ReadonlyArray<IdentityVerifier>;
|
|
238
|
+
/**
|
|
239
|
+
* The one routes function both routers are built from (exposed for
|
|
240
|
+
* tests, the `store` posture — the tier-truthfulness invariant
|
|
241
|
+
* enumerates the services a composition registers through it; the
|
|
242
|
+
* server registers no reflection service, deliberately).
|
|
243
|
+
*/
|
|
244
|
+
routes: (router: ConnectRouter) => void;
|
|
224
245
|
/** Completes wiring, flips SERVING, binds the port; returns the bound port. */
|
|
225
246
|
start(): Promise<number>;
|
|
226
247
|
/** NOT_SERVING first, stop background work, drain connections. */
|
|
@@ -294,6 +315,75 @@ export async function composeServer(
|
|
|
294
315
|
});
|
|
295
316
|
}
|
|
296
317
|
|
|
318
|
+
// The require-authentication posture has two sources OR'd here: the
|
|
319
|
+
// OSS OIDC issuer (O3 rulings Q1+Q2) and a composed unit's declaration
|
|
320
|
+
// (registry point, entry 20260904.02). Neither source = the OSS
|
|
321
|
+
// trusted-local posture, byte-identical wire behavior. Resolved in the
|
|
322
|
+
// storage stage because the posture decides a WRITE below (the operator
|
|
323
|
+
// account); the verifier chain it also governs is built at the end.
|
|
324
|
+
const authEnabled = config.oidcIssuer !== "";
|
|
325
|
+
const requireAuthentication =
|
|
326
|
+
authEnabled || extensions.requireAuthentication !== undefined;
|
|
327
|
+
|
|
328
|
+
// Stage: identity accounts (20260911.11) — the first domain whose
|
|
329
|
+
// persistence is a PORT rather than the generic Store. The composed
|
|
330
|
+
// driver (`drivers.identityAccountStore`, Q-IA-9) serves when a unit
|
|
331
|
+
// registers one; otherwise open source's adapter over `store` installs
|
|
332
|
+
// HERE (the default lives with the consumer, never in the registry).
|
|
333
|
+
// This ONE binding is what the create path, the provisioner, the
|
|
334
|
+
// operator ensure and both OSS verifiers read, so a composed driver is
|
|
335
|
+
// followed everywhere by construction. The domain's one create path is
|
|
336
|
+
// built once and shared by the create RPC, provisioning and the operator
|
|
337
|
+
// ensure below.
|
|
338
|
+
const identityAccounts =
|
|
339
|
+
extensions.drivers.identityAccountStore ??
|
|
340
|
+
newResourceIdentityAccountStore(store);
|
|
341
|
+
const identityAccountPath = {
|
|
342
|
+
accounts: identityAccounts,
|
|
343
|
+
logger,
|
|
344
|
+
authorizer,
|
|
345
|
+
authorizationLifecycle,
|
|
346
|
+
};
|
|
347
|
+
const createIdentityAccount = newCreateAccountPath(
|
|
348
|
+
identityAccountPath,
|
|
349
|
+
ApiResourceKind.identity_account,
|
|
350
|
+
);
|
|
351
|
+
// ONE issuer discovery for every lane that reads a well-known document
|
|
352
|
+
// (A8): provisioning's userinfo client here and the OIDC verifier's
|
|
353
|
+
// JWKS location below share it, so an issuer is discovered once per
|
|
354
|
+
// process and validated the same way wherever it is consumed. Built in
|
|
355
|
+
// this stage, not inside routes() (which runs twice) — the same reason
|
|
356
|
+
// the searchRegistry lives here.
|
|
357
|
+
const issuerDiscovery = newIssuerDiscovery();
|
|
358
|
+
// Provisioning reads the caller's profile from the userinfo endpoint of
|
|
359
|
+
// the issuer that vouched for the token, discovered per issuer and
|
|
360
|
+
// cached (A8) — so a composition whose own verifiers name their issuer
|
|
361
|
+
// on the identity needs no knob here.
|
|
362
|
+
const identityAccountProvisioner = newDirectAccountProvisioner({
|
|
363
|
+
accounts: identityAccounts,
|
|
364
|
+
createAccount: createIdentityAccount,
|
|
365
|
+
userInfo: newOidcUserInfoClient(issuerDiscovery),
|
|
366
|
+
});
|
|
367
|
+
// Under the trusted-local posture the server knows its one principal
|
|
368
|
+
// from the operator seam (the SAME seam the interceptor stamps callers
|
|
369
|
+
// from — never config directly, so the row and the stamp cannot
|
|
370
|
+
// disagree; T01_1_review.md A2, A5) and states the fact at boot:
|
|
371
|
+
// create-if-absent, so preferences set later survive every reboot. A
|
|
372
|
+
// composition with an authentication posture never creates one — its
|
|
373
|
+
// users provision themselves on first sign-in. A stage that writes a
|
|
374
|
+
// resource is new here; it earns its place because it is the truth of
|
|
375
|
+
// that posture, and a failure is a loud boot throw like the storage
|
|
376
|
+
// stage's own.
|
|
377
|
+
if (!requireAuthentication) {
|
|
378
|
+
const operator = await ensureOperatorAccount(
|
|
379
|
+
{ accounts: identityAccounts, createAccount: createIdentityAccount },
|
|
380
|
+
operatorIdentitySnapshot(),
|
|
381
|
+
);
|
|
382
|
+
logger.info("trusted-local operator account ensured", {
|
|
383
|
+
accountId: operator.metadata?.id ?? "",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
297
387
|
// Stage: keys — the ratified fail-loud boot ASYMMETRY (D2 cross-domain
|
|
298
388
|
// invariants; Go server.go:277-293):
|
|
299
389
|
//
|
|
@@ -675,11 +765,36 @@ export async function composeServer(
|
|
|
675
765
|
} else {
|
|
676
766
|
logger.debug("no web console export bundled; console lane disabled");
|
|
677
767
|
}
|
|
768
|
+
// The console is told how to sign in (20260913.02 sp.console-login): the
|
|
769
|
+
// OSS issuer arm of the posture, in the console's vocabulary. A composed
|
|
770
|
+
// unit's own `requireAuthentication` is NOT expressed here — a
|
|
771
|
+
// composition whose verifiers resolve its users serves its console its
|
|
772
|
+
// own way (the cloud's container does); the Enterprise console story is
|
|
773
|
+
// P4's. Issuer set but no console client registered is WARN-degrade, the
|
|
774
|
+
// STIGMER_OAUTH_REDIRECT_URI posture: the CLI and SDKs keep working with
|
|
775
|
+
// API keys, and the served console refuses with copy naming the knob.
|
|
776
|
+
const consoleSignIn: ConsoleSignInPosture = authEnabled
|
|
777
|
+
? {
|
|
778
|
+
posture: "oidc",
|
|
779
|
+
issuer: config.oidcIssuer,
|
|
780
|
+
audience: config.oidcAudience,
|
|
781
|
+
consoleClientId: config.oidcConsoleClientId,
|
|
782
|
+
}
|
|
783
|
+
: { posture: "trusted-local" };
|
|
784
|
+
if (
|
|
785
|
+
consoleAssets !== undefined &&
|
|
786
|
+
consoleSignIn.posture === "oidc" &&
|
|
787
|
+
consoleSignIn.consoleClientId === ""
|
|
788
|
+
) {
|
|
789
|
+
logger.warn(
|
|
790
|
+
"STIGMER_OIDC_ISSUER is set but STIGMER_OIDC_CONSOLE_CLIENT_ID is not — the web console this server serves cannot sign in; register a public PKCE client for the console at the issuer and set it",
|
|
791
|
+
);
|
|
792
|
+
}
|
|
678
793
|
const consoleLane =
|
|
679
794
|
consoleAssets !== undefined
|
|
680
795
|
? createConsoleLane({
|
|
681
796
|
assets: consoleAssets,
|
|
682
|
-
|
|
797
|
+
signIn: consoleSignIn,
|
|
683
798
|
logger,
|
|
684
799
|
})
|
|
685
800
|
: undefined;
|
|
@@ -799,6 +914,17 @@ export async function composeServer(
|
|
|
799
914
|
authorizationLifecycle,
|
|
800
915
|
listReadScope: extensions.drivers.listReadScope,
|
|
801
916
|
});
|
|
917
|
+
// IdentityAccount (20260911.11): served once by open source in every
|
|
918
|
+
// edition over the store PORT bound in the identity-accounts stage; a
|
|
919
|
+
// composition adds what differs per edition through the registry —
|
|
920
|
+
// the federation capability (the four federated RPC arms) and the
|
|
921
|
+
// provision slot's gate steps (the cloud's personal organization).
|
|
922
|
+
registerIdentityAccountServices(router, {
|
|
923
|
+
...identityAccountPath,
|
|
924
|
+
provisioner: identityAccountProvisioner,
|
|
925
|
+
federation: extensions.drivers.identityFederation,
|
|
926
|
+
gateSteps: extensions.gateSteps,
|
|
927
|
+
});
|
|
802
928
|
registerEnvironmentServices(router, {
|
|
803
929
|
store,
|
|
804
930
|
logger,
|
|
@@ -1092,14 +1218,8 @@ export async function composeServer(
|
|
|
1092
1218
|
const inProcessWiring = createInProcessClients(routes, logger);
|
|
1093
1219
|
inProcess = inProcessWiring.clients;
|
|
1094
1220
|
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
// (registry point, entry 20260904.02). Neither source = the OSS
|
|
1098
|
-
// trusted-local posture, byte-identical wire behavior.
|
|
1099
|
-
const authEnabled = config.oidcIssuer !== "";
|
|
1100
|
-
const requireAuthentication =
|
|
1101
|
-
authEnabled || extensions.requireAuthentication !== undefined;
|
|
1102
|
-
// What the posture registers, in Java's ProviderManager order (opaque
|
|
1221
|
+
// What the posture (resolved in the storage stage) registers, in Java's
|
|
1222
|
+
// ProviderManager order (opaque
|
|
1103
1223
|
// keys first — the cheapest claim — then the JWT lanes), ahead of the
|
|
1104
1224
|
// extension entries ("OSS entries first", the registry contract):
|
|
1105
1225
|
// - the API-key verifier rides the POSTURE, not the issuer
|
|
@@ -1108,19 +1228,31 @@ export async function composeServer(
|
|
|
1108
1228
|
// authentication posture must honor the keys it issues. Gating it on
|
|
1109
1229
|
// the issuer left a posture-declaring composition minting keys
|
|
1110
1230
|
// nothing verified.
|
|
1111
|
-
// - the OIDC verifier rides the ISSUER alone: it
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
1114
|
-
//
|
|
1231
|
+
// - the OIDC verifier rides the ISSUER alone: it is open source's
|
|
1232
|
+
// single-issuer lane, and a composition with verifiers of its own
|
|
1233
|
+
// names their issuers and audiences itself and declares the posture
|
|
1234
|
+
// instead — so the knob and the extension entries never both claim
|
|
1235
|
+
// one JWT.
|
|
1236
|
+
// - both OSS verifiers resolve their subject through the identity-
|
|
1237
|
+
// account domain (20260911.11 Q-IA-2, A6): a provisioned user is
|
|
1238
|
+
// stamped with the account id whichever credential they present,
|
|
1239
|
+
// an unprovisioned one is admitted idp-shaped. They read the SAME
|
|
1240
|
+
// port the domain writes through (`identityAccounts`, the
|
|
1241
|
+
// identity-accounts stage — a composed driver included), so a row
|
|
1242
|
+
// provisioning creates resolves on the very next request.
|
|
1115
1243
|
// The runner's credential in either posture is an operator-minted API
|
|
1116
1244
|
// token via STIGMER_TOKEN (O3 ruling Q3) — no runner-specific verifier.
|
|
1117
1245
|
const identityVerifiers = [
|
|
1118
|
-
...(requireAuthentication
|
|
1246
|
+
...(requireAuthentication
|
|
1247
|
+
? [newApiKeyIdentityVerifier({ store, accounts: identityAccounts })]
|
|
1248
|
+
: []),
|
|
1119
1249
|
...(authEnabled
|
|
1120
1250
|
? [
|
|
1121
1251
|
newOidcIdentityVerifier({
|
|
1122
1252
|
issuer: config.oidcIssuer,
|
|
1123
1253
|
audience: config.oidcAudience,
|
|
1254
|
+
accounts: identityAccounts,
|
|
1255
|
+
discovery: issuerDiscovery,
|
|
1124
1256
|
}),
|
|
1125
1257
|
]
|
|
1126
1258
|
: []),
|
|
@@ -1203,6 +1335,7 @@ export async function composeServer(
|
|
|
1203
1335
|
workflowExecutionStreamBroker,
|
|
1204
1336
|
inProcessTransport: inProcessWiring.transport,
|
|
1205
1337
|
identityVerifiers,
|
|
1338
|
+
routes,
|
|
1206
1339
|
|
|
1207
1340
|
async start(): Promise<number> {
|
|
1208
1341
|
// Temporal boot is NON-fatal end to end (Go server.go): a failed
|