@ogygie/studio 0.29.2 → 0.31.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.
package/dist/server.js
CHANGED
|
@@ -1397,12 +1397,22 @@ async function readParsedSpec(deps, specId) {
|
|
|
1397
1397
|
}
|
|
1398
1398
|
return parsed.spec;
|
|
1399
1399
|
}
|
|
1400
|
+
async function generatedProductOf(deps) {
|
|
1401
|
+
if (!await deps.storage.exists("ogygie.manifest.json")) return false;
|
|
1402
|
+
try {
|
|
1403
|
+
const raw = JSON.parse(await deps.storage.readFile("ogygie.manifest.json"));
|
|
1404
|
+
return typeof raw === "object" && raw !== null && "generation" in raw && raw.generation !== null;
|
|
1405
|
+
} catch {
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1400
1409
|
async function getSpec(deps, specId) {
|
|
1401
1410
|
const spec = await readParsedSpec(deps, specId);
|
|
1402
1411
|
return {
|
|
1403
1412
|
frontmatter: spec.frontmatter,
|
|
1404
1413
|
body: spec.body,
|
|
1405
|
-
presentSections: presentSectionsOf(spec.body)
|
|
1414
|
+
presentSections: presentSectionsOf(spec.body),
|
|
1415
|
+
generatedProduct: await generatedProductOf(deps)
|
|
1406
1416
|
};
|
|
1407
1417
|
}
|
|
1408
1418
|
async function saveSpec(deps, specId, input) {
|
|
@@ -1931,7 +1941,9 @@ var GetSpecOutputSchema = z7.object({
|
|
|
1931
1941
|
frontmatter: SpecFrontmatterSchema,
|
|
1932
1942
|
/** Markdown body after the frontmatter block, byte-exact. */
|
|
1933
1943
|
body: z7.string(),
|
|
1934
|
-
presentSections: z7.array(z7.string())
|
|
1944
|
+
presentSections: z7.array(z7.string()),
|
|
1945
|
+
/** D58: the product's map declares a `generation` block — a spec without `# Génération` is then hand-written, nothing derives from it. */
|
|
1946
|
+
generatedProduct: z7.boolean().default(false)
|
|
1935
1947
|
});
|
|
1936
1948
|
var UpdateSpecInputSchema = z7.object({
|
|
1937
1949
|
frontmatter: SpecFrontmatterSchema,
|
|
@@ -2191,7 +2203,7 @@ async function registerStudioRoutes(app, resolver) {
|
|
|
2191
2203
|
}
|
|
2192
2204
|
|
|
2193
2205
|
// src/server/hosted/routes.ts
|
|
2194
|
-
import { z as
|
|
2206
|
+
import { z as z10 } from "zod";
|
|
2195
2207
|
|
|
2196
2208
|
// src/server/hosted/auth.ts
|
|
2197
2209
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
@@ -2246,19 +2258,27 @@ function consumeMagicLink(deps, token) {
|
|
|
2246
2258
|
deps.consumed.set(nonce, exp);
|
|
2247
2259
|
return email;
|
|
2248
2260
|
}
|
|
2249
|
-
function issueSession(deps,
|
|
2250
|
-
return seal(
|
|
2261
|
+
function issueSession(deps, user) {
|
|
2262
|
+
return seal(
|
|
2263
|
+
{ kind: "session", email: user.email.toLowerCase(), displayName: user.displayName, role: user.role, via: user.via, exp: deps.now() + deps.ttlHours * 36e5 },
|
|
2264
|
+
deps.secret
|
|
2265
|
+
);
|
|
2251
2266
|
}
|
|
2267
|
+
var ROLES = /* @__PURE__ */ new Set(["reader", "author", "approver", "operator"]);
|
|
2268
|
+
var VIAS = /* @__PURE__ */ new Set(["magic-link", "entra"]);
|
|
2252
2269
|
function readSession(deps, cookieHeader) {
|
|
2253
2270
|
const token = cookieValue(cookieHeader, SESSION_COOKIE);
|
|
2254
2271
|
if (token === null) return null;
|
|
2255
2272
|
const value = open(token, deps.secret);
|
|
2256
2273
|
const email = value?.["email"];
|
|
2274
|
+
const displayName = value?.["displayName"];
|
|
2275
|
+
const role = value?.["role"];
|
|
2276
|
+
const via = value?.["via"];
|
|
2257
2277
|
const exp = value?.["exp"];
|
|
2258
|
-
if (value?.["kind"] !== "session" || typeof email !== "string" || typeof exp !== "number" || exp <= deps.now()) {
|
|
2278
|
+
if (value?.["kind"] !== "session" || typeof email !== "string" || typeof displayName !== "string" || typeof role !== "string" || !ROLES.has(role) || typeof via !== "string" || !VIAS.has(via) || typeof exp !== "number" || exp <= deps.now()) {
|
|
2259
2279
|
return null;
|
|
2260
2280
|
}
|
|
2261
|
-
return email;
|
|
2281
|
+
return { email, displayName, role, via };
|
|
2262
2282
|
}
|
|
2263
2283
|
function cookieValue(cookieHeader, name) {
|
|
2264
2284
|
if (cookieHeader === void 0) return null;
|
|
@@ -2307,6 +2327,21 @@ function unauthenticated(cause) {
|
|
|
2307
2327
|
// src/server/hosted/config.ts
|
|
2308
2328
|
import { z as z8 } from "zod";
|
|
2309
2329
|
var HostRoleSchema = z8.enum(["reader", "author", "approver", "operator"]);
|
|
2330
|
+
var HOST_ROLES = HostRoleSchema.options;
|
|
2331
|
+
var HostIdentitySchema = z8.object({
|
|
2332
|
+
kind: z8.literal("entra"),
|
|
2333
|
+
tenantId: z8.string().min(1),
|
|
2334
|
+
clientId: z8.string().min(1),
|
|
2335
|
+
clientSecretPath: z8.string().min(1),
|
|
2336
|
+
/** Where the tenant writes the groups in the id token — a setting of the app registration. */
|
|
2337
|
+
groupsClaim: z8.string().min(1).default("groups"),
|
|
2338
|
+
roles: z8.object({
|
|
2339
|
+
reader: z8.array(z8.string().min(1)).default([]),
|
|
2340
|
+
author: z8.array(z8.string().min(1)).default([]),
|
|
2341
|
+
approver: z8.array(z8.string().min(1)).default([]),
|
|
2342
|
+
operator: z8.array(z8.string().min(1)).default([])
|
|
2343
|
+
})
|
|
2344
|
+
});
|
|
2310
2345
|
var MergePolicySchema = z8.enum(["client", "operator"]);
|
|
2311
2346
|
var HostUserSchema = z8.object({
|
|
2312
2347
|
email: z8.email(),
|
|
@@ -2320,7 +2355,9 @@ var HostConfigSchema = z8.object({
|
|
|
2320
2355
|
installationId: z8.string().min(1),
|
|
2321
2356
|
privateKeyPath: z8.string().min(1)
|
|
2322
2357
|
}),
|
|
2323
|
-
|
|
2358
|
+
/** The addresses the magic link serves; optional once `identity` opens the door (D59). */
|
|
2359
|
+
users: z8.array(HostUserSchema).default([]),
|
|
2360
|
+
identity: HostIdentitySchema.optional(),
|
|
2324
2361
|
/** "operator" at onboarding: the agency keeps the merge until it hands it over (D48). */
|
|
2325
2362
|
mergePolicy: MergePolicySchema.default("operator"),
|
|
2326
2363
|
writeRateLimit: z8.number().int().positive().default(20),
|
|
@@ -2353,6 +2390,9 @@ var HostConfigSchema = z8.object({
|
|
|
2353
2390
|
/** USD per million tokens, by model id — overrides the built-in table of the estimate (B8). */
|
|
2354
2391
|
costPerMillionUsd: z8.record(z8.string(), z8.object({ input: z8.number().nonnegative(), output: z8.number().nonnegative() })).optional()
|
|
2355
2392
|
}).optional()
|
|
2393
|
+
}).refine((config) => config.users.length > 0 || config.identity !== void 0, {
|
|
2394
|
+
path: ["users"],
|
|
2395
|
+
message: "declare at least one user, or an identity block (D59): a host nobody can enter is a configuration error"
|
|
2356
2396
|
});
|
|
2357
2397
|
function parseHostConfig(raw, configPath) {
|
|
2358
2398
|
const parsed = HostConfigSchema.safeParse(raw);
|
|
@@ -2372,20 +2412,190 @@ function findHostUser(config, email) {
|
|
|
2372
2412
|
return config.users.find((user) => user.email.toLowerCase() === wanted) ?? null;
|
|
2373
2413
|
}
|
|
2374
2414
|
|
|
2415
|
+
// src/server/hosted/entra.ts
|
|
2416
|
+
import { createHash, createPublicKey, randomBytes as randomBytes2, verify as verifySignature } from "node:crypto";
|
|
2417
|
+
import { z as z9 } from "zod";
|
|
2418
|
+
var OIDC_SCOPE = "openid profile email";
|
|
2419
|
+
var CLOCK_SKEW_SECONDS = 60;
|
|
2420
|
+
var FILE = "packages/ogygie-studio/src/server/hosted/entra.ts";
|
|
2421
|
+
var baseUrlOf = (config) => `https://login.microsoftonline.com/${config.tenantId}`;
|
|
2422
|
+
var issuerOf = (config) => `${baseUrlOf(config)}/v2.0`;
|
|
2423
|
+
var tokenEndpointOf = (config) => `${baseUrlOf(config)}/oauth2/v2.0/token`;
|
|
2424
|
+
var jwksEndpointOf = (config) => `${baseUrlOf(config)}/discovery/v2.0/keys`;
|
|
2425
|
+
function codeChallengeOf(codeVerifier) {
|
|
2426
|
+
return createHash("sha256").update(codeVerifier).digest("base64url");
|
|
2427
|
+
}
|
|
2428
|
+
function newHandshake() {
|
|
2429
|
+
return { state: randomBytes2(16).toString("hex"), nonce: randomBytes2(16).toString("hex"), codeVerifier: randomBytes2(48).toString("base64url") };
|
|
2430
|
+
}
|
|
2431
|
+
function authorizationUrlOf(config, handshake) {
|
|
2432
|
+
const url = new URL(`${baseUrlOf(config)}/oauth2/v2.0/authorize`);
|
|
2433
|
+
url.searchParams.set("client_id", config.clientId);
|
|
2434
|
+
url.searchParams.set("response_type", "code");
|
|
2435
|
+
url.searchParams.set("redirect_uri", config.callbackUrl);
|
|
2436
|
+
url.searchParams.set("response_mode", "query");
|
|
2437
|
+
url.searchParams.set("scope", OIDC_SCOPE);
|
|
2438
|
+
url.searchParams.set("state", handshake.state);
|
|
2439
|
+
url.searchParams.set("nonce", handshake.nonce);
|
|
2440
|
+
url.searchParams.set("code_challenge", codeChallengeOf(handshake.codeVerifier));
|
|
2441
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
2442
|
+
return url.toString();
|
|
2443
|
+
}
|
|
2444
|
+
function unauthenticated2(cause, suggestedFix) {
|
|
2445
|
+
return throwOgygieError(
|
|
2446
|
+
{ code: "HOST_UNAUTHENTICATED", message: "The Microsoft sign-in did not establish an identity.", file: FILE, cause, suggestedFix },
|
|
2447
|
+
401
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
var IdTokenClaimsSchema = z9.looseObject({
|
|
2451
|
+
sub: z9.string().min(1),
|
|
2452
|
+
iss: z9.string().min(1),
|
|
2453
|
+
aud: z9.union([z9.string(), z9.array(z9.string())]),
|
|
2454
|
+
exp: z9.number(),
|
|
2455
|
+
nonce: z9.string().optional(),
|
|
2456
|
+
email: z9.string().optional(),
|
|
2457
|
+
preferred_username: z9.string().optional(),
|
|
2458
|
+
upn: z9.string().optional(),
|
|
2459
|
+
name: z9.string().optional()
|
|
2460
|
+
});
|
|
2461
|
+
var RsaSigningKeySchema = z9.object({ kid: z9.string().min(1), kty: z9.literal("RSA"), n: z9.string().min(1), e: z9.string().min(1) });
|
|
2462
|
+
var JwksSchema = z9.object({ keys: z9.array(z9.unknown()) });
|
|
2463
|
+
var TokenResponseSchema = z9.looseObject({ id_token: z9.string().min(1) });
|
|
2464
|
+
function decodeJwt(token) {
|
|
2465
|
+
const [encodedHeader, encodedPayload, encodedSignature, ...rest] = token.split(".");
|
|
2466
|
+
if (encodedHeader === void 0 || encodedPayload === void 0 || encodedSignature === void 0 || rest.length > 0) return null;
|
|
2467
|
+
try {
|
|
2468
|
+
const header = JSON.parse(Buffer.from(encodedHeader, "base64url").toString("utf8"));
|
|
2469
|
+
if (typeof header !== "object" || header === null) return null;
|
|
2470
|
+
const { alg, kid } = header;
|
|
2471
|
+
return {
|
|
2472
|
+
header: { ...typeof alg === "string" ? { alg } : {}, ...typeof kid === "string" ? { kid } : {} },
|
|
2473
|
+
payload: JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8")),
|
|
2474
|
+
signingInput: `${encodedHeader}.${encodedPayload}`,
|
|
2475
|
+
signature: Buffer.from(encodedSignature, "base64url")
|
|
2476
|
+
};
|
|
2477
|
+
} catch {
|
|
2478
|
+
return null;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
function verifyIdToken(input) {
|
|
2482
|
+
const decoded = decodeJwt(input.idToken);
|
|
2483
|
+
if (decoded === null) return unauthenticated2("id token is not a readable JWT", "Retry the sign-in.");
|
|
2484
|
+
if (decoded.header.alg !== "RS256") {
|
|
2485
|
+
return unauthenticated2(`id token signed with alg=${decoded.header.alg ?? "none"}, only RS256 is accepted`, "Check the signing algorithm of the app registration (RS256).");
|
|
2486
|
+
}
|
|
2487
|
+
const candidates = input.keys.map((key) => RsaSigningKeySchema.safeParse(key)).flatMap((parsed) => parsed.success ? [parsed.data] : []).filter((key) => decoded.header.kid === void 0 || key.kid === decoded.header.kid);
|
|
2488
|
+
if (candidates.length === 0) {
|
|
2489
|
+
return unauthenticated2(`no published RSA key matches kid=${decoded.header.kid ?? "(absent)"}`, "Check that the id token comes from the configured tenant.");
|
|
2490
|
+
}
|
|
2491
|
+
const signed = candidates.some(
|
|
2492
|
+
(key) => verifySignature("sha256", Buffer.from(decoded.signingInput, "utf8"), createPublicKey({ key: { kty: "RSA", n: key.n, e: key.e }, format: "jwk" }), decoded.signature)
|
|
2493
|
+
);
|
|
2494
|
+
if (!signed) return unauthenticated2("id token signature does not match the keys published by the tenant", "Retry the sign-in; if it persists, check the tenant of this host.");
|
|
2495
|
+
const claims = IdTokenClaimsSchema.safeParse(decoded.payload);
|
|
2496
|
+
if (!claims.success) return unauthenticated2("id token claims do not have the expected shape", "Check the app registration (openid scope, id token).");
|
|
2497
|
+
const expectedIssuer = issuerOf(input.config);
|
|
2498
|
+
if (claims.data.iss !== expectedIssuer) return unauthenticated2(`id token issued by ${claims.data.iss}, expected ${expectedIssuer}`, "Point identity.tenantId at the tenant that issues the token.");
|
|
2499
|
+
const audiences = Array.isArray(claims.data.aud) ? claims.data.aud : [claims.data.aud];
|
|
2500
|
+
if (!audiences.includes(input.config.clientId)) return unauthenticated2("id token was issued for another application", "Check identity.clientId against the app registration the sign-in used.");
|
|
2501
|
+
if (claims.data.exp + CLOCK_SKEW_SECONDS < Math.floor(input.nowMs / 1e3)) return unauthenticated2("id token has expired", "Retry the sign-in.");
|
|
2502
|
+
if (claims.data.nonce !== input.nonce) return unauthenticated2("id token nonce does not match the one this handshake sent", "Retry the sign-in from the start; a token cannot be replayed.");
|
|
2503
|
+
return claims.data;
|
|
2504
|
+
}
|
|
2505
|
+
function groupsOfClaim(value) {
|
|
2506
|
+
if (typeof value === "string") return [value];
|
|
2507
|
+
if (!Array.isArray(value)) return [];
|
|
2508
|
+
return value.filter((group) => typeof group === "string" && group.length > 0);
|
|
2509
|
+
}
|
|
2510
|
+
function identityOfClaims(claims, groupsClaim) {
|
|
2511
|
+
const email = claims.email ?? claims.preferred_username ?? claims.upn;
|
|
2512
|
+
if (email === void 0 || email === "") {
|
|
2513
|
+
return unauthenticated2("the id token carries no email, preferred_username nor upn claim", "Add the optional email claim on the app registration.");
|
|
2514
|
+
}
|
|
2515
|
+
return { email: email.toLowerCase(), displayName: claims.name ?? email, groups: groupsOfClaim(claims[groupsClaim]) };
|
|
2516
|
+
}
|
|
2517
|
+
function roleOfGroups(groups, roles) {
|
|
2518
|
+
let found = null;
|
|
2519
|
+
for (const role of HOST_ROLES) if (roles[role].some((group) => group !== "" && groups.includes(group))) found = role;
|
|
2520
|
+
return found;
|
|
2521
|
+
}
|
|
2522
|
+
function hostUserOfIdentity(identity, declared, roles) {
|
|
2523
|
+
if (declared !== null) return { email: declared.email, displayName: declared.displayName, role: declared.role };
|
|
2524
|
+
const role = roleOfGroups(identity.groups, roles);
|
|
2525
|
+
if (role === null) {
|
|
2526
|
+
return throwOgygieError(
|
|
2527
|
+
{
|
|
2528
|
+
code: "HOST_ACCESS_DENIED",
|
|
2529
|
+
message: "This account is authenticated but not invited on this host.",
|
|
2530
|
+
file: null,
|
|
2531
|
+
cause: `${identity.email}: none of its ${identity.groups.length} group(s) is declared under identity.roles, and the address is not in users`,
|
|
2532
|
+
suggestedFix: "Add the account to one of the directory groups the host declares (identity.roles), or declare its address in users."
|
|
2533
|
+
},
|
|
2534
|
+
403
|
|
2535
|
+
);
|
|
2536
|
+
}
|
|
2537
|
+
return { email: identity.email, displayName: identity.displayName, role };
|
|
2538
|
+
}
|
|
2539
|
+
async function readJson(response, endpoint) {
|
|
2540
|
+
if (!response.ok) return unauthenticated2(`${endpoint} answered ${response.status}`, "Retry the sign-in; if it persists, check the tenant, the client id and the client secret.");
|
|
2541
|
+
try {
|
|
2542
|
+
return await response.json();
|
|
2543
|
+
} catch {
|
|
2544
|
+
return unauthenticated2(`${endpoint} answered a body that is not JSON`, "Retry the sign-in.");
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
async function exchangeAuthorizationCode(deps, config, input) {
|
|
2548
|
+
const endpoint = tokenEndpointOf(config);
|
|
2549
|
+
const response = await deps.fetchImpl(endpoint, {
|
|
2550
|
+
method: "POST",
|
|
2551
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
2552
|
+
body: new URLSearchParams({
|
|
2553
|
+
grant_type: "authorization_code",
|
|
2554
|
+
client_id: config.clientId,
|
|
2555
|
+
client_secret: config.clientSecret,
|
|
2556
|
+
code: input.code,
|
|
2557
|
+
code_verifier: input.codeVerifier,
|
|
2558
|
+
redirect_uri: config.callbackUrl
|
|
2559
|
+
}).toString()
|
|
2560
|
+
});
|
|
2561
|
+
const parsed = TokenResponseSchema.safeParse(await readJson(response, endpoint));
|
|
2562
|
+
if (!parsed.success) return unauthenticated2(`${endpoint} answered without an id_token`, "Check that the app registration requests the openid scope.");
|
|
2563
|
+
return parsed.data.id_token;
|
|
2564
|
+
}
|
|
2565
|
+
async function fetchSigningKeys(deps, config) {
|
|
2566
|
+
const endpoint = jwksEndpointOf(config);
|
|
2567
|
+
const response = await deps.fetchImpl(endpoint, { method: "GET", headers: {} });
|
|
2568
|
+
const parsed = JwksSchema.safeParse(await readJson(response, endpoint));
|
|
2569
|
+
if (!parsed.success) return unauthenticated2(`${endpoint} answered a document that is not a JWKS`, "Check identity.tenantId \u2014 the JWKS of the tenant is what verifies the id token.");
|
|
2570
|
+
return parsed.data.keys;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2375
2573
|
// src/server/hosted/routes.ts
|
|
2574
|
+
var OIDC_COOKIE = "ogygie_host_oidc";
|
|
2575
|
+
var OIDC_TTL_MS = 10 * 60 * 1e3;
|
|
2576
|
+
var oidcCookie = (value) => `${OIDC_COOKIE}=${encodeURIComponent(value)}; Path=/api/auth/entra; Max-Age=${OIDC_TTL_MS / 1e3}; HttpOnly; Secure; SameSite=Lax`;
|
|
2577
|
+
var clearedOidcCookie = () => `${OIDC_COOKIE}=; Path=/api/auth/entra; Max-Age=0; HttpOnly; Secure; SameSite=Lax`;
|
|
2578
|
+
function cookieOf(cookieHeader, name) {
|
|
2579
|
+
if (cookieHeader === void 0) return null;
|
|
2580
|
+
for (const part of cookieHeader.split(";")) {
|
|
2581
|
+
const [key, ...rest] = part.trim().split("=");
|
|
2582
|
+
if (key === name) return decodeURIComponent(rest.join("="));
|
|
2583
|
+
}
|
|
2584
|
+
return null;
|
|
2585
|
+
}
|
|
2376
2586
|
var usersByRequest = /* @__PURE__ */ new WeakMap();
|
|
2377
2587
|
function hostedUserOf(request) {
|
|
2378
2588
|
const user = usersByRequest.get(request);
|
|
2379
2589
|
if (user === void 0) return unauthenticated("no session attached to this request");
|
|
2380
2590
|
return user;
|
|
2381
2591
|
}
|
|
2382
|
-
var MagicLinkInputSchema =
|
|
2383
|
-
var CallbackQuerySchema =
|
|
2384
|
-
var MeOutputSchema =
|
|
2385
|
-
email:
|
|
2386
|
-
displayName:
|
|
2592
|
+
var MagicLinkInputSchema = z10.object({ email: z10.email() });
|
|
2593
|
+
var CallbackQuerySchema = z10.object({ token: z10.string().min(1) });
|
|
2594
|
+
var MeOutputSchema = z10.object({
|
|
2595
|
+
email: z10.email(),
|
|
2596
|
+
displayName: z10.string(),
|
|
2387
2597
|
role: HostRoleSchema,
|
|
2388
|
-
host:
|
|
2598
|
+
host: z10.object({ environments: z10.boolean(), mergePolicy: z10.enum(["client", "operator"]) })
|
|
2389
2599
|
});
|
|
2390
2600
|
async function registerHostedRoutes(app, deps) {
|
|
2391
2601
|
const sessions = {
|
|
@@ -2399,21 +2609,74 @@ async function registerHostedRoutes(app, deps) {
|
|
|
2399
2609
|
consumed: deps.consumedLinks,
|
|
2400
2610
|
...deps.magicLinkTtlMs === void 0 ? {} : { ttlMs: deps.magicLinkTtlMs }
|
|
2401
2611
|
};
|
|
2402
|
-
const PUBLIC_ROUTES = /* @__PURE__ */ new Set(["/api/auth/magic-link", "/api/auth/callback", "/api/auth/logout"]);
|
|
2612
|
+
const PUBLIC_ROUTES = /* @__PURE__ */ new Set(["/api/auth/options", "/api/auth/magic-link", "/api/auth/callback", "/api/auth/logout", "/api/auth/entra/start", "/api/auth/entra/callback"]);
|
|
2403
2613
|
app.addHook("onRequest", async (request) => {
|
|
2404
2614
|
const url = request.url.split("?")[0] ?? request.url;
|
|
2405
2615
|
if (!url.startsWith("/api/") || PUBLIC_ROUTES.has(url)) return;
|
|
2406
|
-
const
|
|
2407
|
-
if (
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2616
|
+
const session = readSession(sessions, request.headers.cookie);
|
|
2617
|
+
if (session === null) return unauthenticated("missing or expired session cookie");
|
|
2618
|
+
if (session.via === "magic-link") {
|
|
2619
|
+
const declared = findHostUser(deps.config, session.email);
|
|
2620
|
+
if (declared === null) return unauthenticated(`session for ${session.email}, no longer a declared user`);
|
|
2621
|
+
usersByRequest.set(request, declared);
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
usersByRequest.set(request, { email: session.email, displayName: session.displayName, role: session.role });
|
|
2411
2625
|
});
|
|
2412
2626
|
const route = app.withTypeProvider();
|
|
2627
|
+
route.route({
|
|
2628
|
+
method: "GET",
|
|
2629
|
+
url: "/api/auth/options",
|
|
2630
|
+
schema: { response: { 200: z10.object({ magicLink: z10.boolean(), entra: z10.boolean() }) } },
|
|
2631
|
+
handler: async (_request, reply) => reply.code(200).send({ magicLink: deps.config.users.length > 0, entra: deps.entra !== void 0 })
|
|
2632
|
+
});
|
|
2633
|
+
if (deps.entra !== void 0) {
|
|
2634
|
+
const entra = deps.entra;
|
|
2635
|
+
route.route({
|
|
2636
|
+
method: "GET",
|
|
2637
|
+
url: "/api/auth/entra/start",
|
|
2638
|
+
handler: async (_request, reply) => {
|
|
2639
|
+
const handshake = newHandshake();
|
|
2640
|
+
const state = seal({ kind: "oidc", ...handshake, exp: deps.now() + OIDC_TTL_MS }, deps.sessionSecret);
|
|
2641
|
+
return reply.header("set-cookie", oidcCookie(state)).redirect(authorizationUrlOf(entra.config, handshake), 302);
|
|
2642
|
+
}
|
|
2643
|
+
});
|
|
2644
|
+
route.route({
|
|
2645
|
+
method: "GET",
|
|
2646
|
+
url: "/api/auth/entra/callback",
|
|
2647
|
+
schema: { querystring: z10.object({ code: z10.string().min(1).optional(), state: z10.string().min(1).optional(), error: z10.string().optional(), error_description: z10.string().optional() }) },
|
|
2648
|
+
handler: async (request, reply) => {
|
|
2649
|
+
const refused = (code, cause) => {
|
|
2650
|
+
deps.log(JSON.stringify({ action: "sign-in", provider: "entra", outcome: code, cause }));
|
|
2651
|
+
return reply.header("set-cookie", clearedOidcCookie()).redirect(`/login?error=${encodeURIComponent(code)}`, 302);
|
|
2652
|
+
};
|
|
2653
|
+
if (request.query.error !== void 0) return refused("HOST_UNAUTHENTICATED", `tenant answered ${request.query.error}: ${request.query.error_description ?? ""}`);
|
|
2654
|
+
const sealed = cookieOf(request.headers.cookie, OIDC_COOKIE);
|
|
2655
|
+
const handshake = sealed === null ? null : open(sealed, deps.sessionSecret);
|
|
2656
|
+
const expected = { state: handshake?.["state"], nonce: handshake?.["nonce"], codeVerifier: handshake?.["codeVerifier"], exp: handshake?.["exp"] };
|
|
2657
|
+
if (handshake?.["kind"] !== "oidc" || typeof expected.state !== "string" || typeof expected.nonce !== "string" || typeof expected.codeVerifier !== "string" || typeof expected.exp !== "number" || expected.exp <= deps.now()) {
|
|
2658
|
+
return refused("HOST_UNAUTHENTICATED", "no live handshake cookie: the sign-in took more than ten minutes, or did not start here");
|
|
2659
|
+
}
|
|
2660
|
+
if (request.query.state !== expected.state || request.query.code === void 0) return refused("HOST_UNAUTHENTICATED", "state mismatch or no code");
|
|
2661
|
+
try {
|
|
2662
|
+
const idToken = await exchangeAuthorizationCode(entra, entra.config, { code: request.query.code, codeVerifier: expected.codeVerifier });
|
|
2663
|
+
const keys = await fetchSigningKeys(entra, entra.config);
|
|
2664
|
+
const claims = verifyIdToken({ idToken, keys, config: entra.config, nonce: expected.nonce, nowMs: deps.now() });
|
|
2665
|
+
const identity = identityOfClaims(claims, entra.config.groupsClaim);
|
|
2666
|
+
const user = hostUserOfIdentity(identity, findHostUser(deps.config, identity.email), entra.config.roles);
|
|
2667
|
+
deps.log(JSON.stringify({ action: "sign-in", provider: "entra", actor: user.email, role: user.role, outcome: "ok" }));
|
|
2668
|
+
return reply.header("set-cookie", [clearedOidcCookie(), sessionCookie(issueSession(sessions, { ...user, via: "entra" }), deps.config.session.ttlHours)]).redirect("/", 302);
|
|
2669
|
+
} catch (thrown) {
|
|
2670
|
+
if (thrown instanceof OgygieFailure) return refused(thrown.error.code, thrown.error.cause);
|
|
2671
|
+
throw thrown;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2413
2676
|
route.route({
|
|
2414
2677
|
method: "POST",
|
|
2415
2678
|
url: "/api/auth/magic-link",
|
|
2416
|
-
schema: { body: MagicLinkInputSchema, response: { 202:
|
|
2679
|
+
schema: { body: MagicLinkInputSchema, response: { 202: z10.object({ accepted: z10.literal(true) }) } },
|
|
2417
2680
|
handler: async (request, reply) => {
|
|
2418
2681
|
const user = findHostUser(deps.config, request.body.email);
|
|
2419
2682
|
if (user === null) {
|
|
@@ -2443,8 +2706,8 @@ ${link}
|
|
|
2443
2706
|
const email = consumeMagicLink(links, request.query.token);
|
|
2444
2707
|
const user = findHostUser(deps.config, email);
|
|
2445
2708
|
if (user === null) return unauthenticated(`link for ${email}, no longer a declared user`);
|
|
2446
|
-
deps.log(JSON.stringify({ action: "sign-in", actor: user.email, outcome: "ok" }));
|
|
2447
|
-
return reply.header("set-cookie", sessionCookie(issueSession(sessions, user
|
|
2709
|
+
deps.log(JSON.stringify({ action: "sign-in", provider: "magic-link", actor: user.email, outcome: "ok" }));
|
|
2710
|
+
return reply.header("set-cookie", sessionCookie(issueSession(sessions, { ...user, via: "magic-link" }), deps.config.session.ttlHours)).redirect("/", 302);
|
|
2448
2711
|
}
|
|
2449
2712
|
});
|
|
2450
2713
|
route.route({
|
|
@@ -2465,10 +2728,10 @@ ${link}
|
|
|
2465
2728
|
}
|
|
2466
2729
|
|
|
2467
2730
|
// src/server/hosted/brief-routes.ts
|
|
2468
|
-
import { z as
|
|
2731
|
+
import { z as z12 } from "zod";
|
|
2469
2732
|
|
|
2470
2733
|
// src/server/hosted/attachments.ts
|
|
2471
|
-
import { createHash } from "node:crypto";
|
|
2734
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2472
2735
|
import { inflateRawSync } from "node:zlib";
|
|
2473
2736
|
var ATTACHMENT_FORMATS = ["md", "txt", "csv", "json", "html", "xlsx", "pdf"];
|
|
2474
2737
|
var MAX_ATTACHMENTS = 5;
|
|
@@ -2650,7 +2913,7 @@ function prepareBriefAttachments(raw) {
|
|
|
2650
2913
|
if (buffer.length > MAX_ATTACHMENT_BYTES) return tooLarge(piece.name, `${buffer.length} bytes`, MAX_ATTACHMENT_BYTES, "bytes");
|
|
2651
2914
|
totalBytes += buffer.length;
|
|
2652
2915
|
if (totalBytes > MAX_ATTACHMENTS_BYTES) return tooLarge(piece.name, `${totalBytes} bytes in all`, MAX_ATTACHMENTS_BYTES, "bytes");
|
|
2653
|
-
const identity = { name: piece.name, format, bytes: buffer.length, sha256:
|
|
2916
|
+
const identity = { name: piece.name, format, bytes: buffer.length, sha256: createHash2("sha256").update(buffer).digest("hex") };
|
|
2654
2917
|
if (format === "pdf") {
|
|
2655
2918
|
if (!isPdf(buffer)) unsupported(piece.name, "the content does not start like a PDF");
|
|
2656
2919
|
prepared.push({ ...identity, kind: "pdf", data: buffer.toString("base64") });
|
|
@@ -2698,7 +2961,7 @@ ${attachments.map((piece) => `- ${piece.name} (${piece.format}, ${sizeOf(piece.b
|
|
|
2698
2961
|
var attachmentDocumentsOf = (attachments) => attachments.flatMap((piece) => piece.kind === "pdf" ? [{ name: piece.name, data: piece.data }] : []);
|
|
2699
2962
|
|
|
2700
2963
|
// src/server/hosted/brief.ts
|
|
2701
|
-
import { z as
|
|
2964
|
+
import { z as z11 } from "zod";
|
|
2702
2965
|
import { checkSpec, generationGrammarCard, readManifestGeneration } from "@ogygie/generate";
|
|
2703
2966
|
var TEMPLATE_PATH2 = "specs/_TEMPLATE.spec.md";
|
|
2704
2967
|
var PRODUCT_RULES_PATH = "AGENTS.product.md";
|
|
@@ -2715,13 +2978,18 @@ var BRIEF_IN_MESSAGE_MAX = 2e3;
|
|
|
2715
2978
|
var MAX_QUESTION_ROUNDS = 3;
|
|
2716
2979
|
var QUESTIONS_MARKER = /^\s*QUESTIONS\s*$/mu;
|
|
2717
2980
|
var NO_MORE_QUESTIONS = "no more questions: write the draft now, and put what you do not know under # Hypoth\xE8ses";
|
|
2981
|
+
var UNGENERATED_ASKED = /sans\s+g[ée]n[ée]ration/iu;
|
|
2982
|
+
var BLOCK_REQUIRED = "this product is generated (D58): write the `# G\xE9n\xE9ration` block in the closed grammar of the card \u2014 a spec without it would be implemented by nobody; only a brief saying \xAB sans g\xE9n\xE9ration \xBB may omit it";
|
|
2983
|
+
function asksUngenerated(brief) {
|
|
2984
|
+
return UNGENERATED_ASKED.test(brief);
|
|
2985
|
+
}
|
|
2718
2986
|
async function readIfPresent(storage, path) {
|
|
2719
2987
|
return await storage.exists(path) ? storage.readFile(path) : null;
|
|
2720
2988
|
}
|
|
2721
|
-
var ManifestRouteSchema =
|
|
2722
|
-
var ManifestForPromptSchema =
|
|
2723
|
-
product:
|
|
2724
|
-
features:
|
|
2989
|
+
var ManifestRouteSchema = z11.looseObject({ method: z11.string(), path: z11.string(), schemaInput: z11.string().optional(), schemaOutput: z11.string().optional(), invalidates: z11.array(z11.string()).optional() });
|
|
2990
|
+
var ManifestForPromptSchema = z11.looseObject({
|
|
2991
|
+
product: z11.looseObject({ name: z11.string(), description: z11.string().optional() }).optional(),
|
|
2992
|
+
features: z11.array(z11.looseObject({ name: z11.string(), intent: z11.string().optional(), status: z11.string().optional(), spec: z11.string().nullable().optional(), routes: z11.array(ManifestRouteSchema).optional() })).optional()
|
|
2725
2993
|
});
|
|
2726
2994
|
async function manifestSummary(storage) {
|
|
2727
2995
|
const raw = await readIfPresent(storage, MANIFEST_PATH2);
|
|
@@ -2788,8 +3056,8 @@ function extractQuestions(text) {
|
|
|
2788
3056
|
} catch {
|
|
2789
3057
|
return { problem: "QUESTIONS: the JSON after the marker does not parse" };
|
|
2790
3058
|
}
|
|
2791
|
-
const shape =
|
|
2792
|
-
questions:
|
|
3059
|
+
const shape = z11.object({
|
|
3060
|
+
questions: z11.array(z11.object({ id: FeatureNameSchema, text: z11.string().trim().min(1), options: z11.array(z11.string().trim().min(1)).min(2).max(4) })).min(3).max(5)
|
|
2793
3061
|
});
|
|
2794
3062
|
const checked = shape.safeParse(parsed);
|
|
2795
3063
|
if (!checked.success) return { problem: `QUESTIONS: ${checked.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(" ; ")}` };
|
|
@@ -2814,7 +3082,9 @@ function buildBriefPrompt(context) {
|
|
|
2814
3082
|
"- Tu ne cites une d\xE9cision `D<n>` QUE si elle figure dans la liste des d\xE9cisions connues ci-dessous ; sinon n'en cite aucune.",
|
|
2815
3083
|
"- Tu n'inventes ni table, ni route, ni feature existante : tu t'appuies sur la carte du produit ci-dessous ; une rubrique nouvelle nomme ses propres contrats.",
|
|
2816
3084
|
"- Tu n'\xE9cris jamais de code ni de design (couleurs, composants) : la spec dit QUOI, jamais COMMENT. Le bloc YAML de `# G\xE9n\xE9ration` n'est pas du code : c'est la d\xE9claration que le g\xE9n\xE9rateur lit.",
|
|
2817
|
-
...context.grammarCard === null || context.grammarCard === void 0 ? [] : [
|
|
3085
|
+
...context.grammarCard === null || context.grammarCard === void 0 ? [] : [
|
|
3086
|
+
context.ungenerated === true ? "- Ce produit est g\xE9n\xE9r\xE9, mais le brief demande une rubrique SANS g\xE9n\xE9ration : n'\xE9cris pas la section `# G\xE9n\xE9ration`, cette spec sera impl\xE9ment\xE9e \xE0 la main." : "- Ce produit est G\xC9N\xC9R\xC9 (D58) : \xE9cris TOUJOURS la section `# G\xE9n\xE9ration` apr\xE8s `# @intent`, et ses `# Comportements` dans la grammaire ferm\xE9e (carte ci-dessous) ; le brouillon est jug\xE9 par le v\xE9rificateur du g\xE9n\xE9rateur avant d'\xEAtre montr\xE9, et un brouillon sans cette section est refus\xE9. Routes, base, tests et \xE9crans se d\xE9rivent du bloc ; ce qui se pense devient un trou typ\xE9."
|
|
3087
|
+
],
|
|
2818
3088
|
"- Tu ne proposes jamais `status: validated` ni `implemented` : valider est le geste de l'humain.",
|
|
2819
3089
|
"- HYPOTH\xC8SES : quand tu tranches un point que le brief ne dit pas (un nombre, un ordre, un p\xE9rim\xE8tre, un mot), ajoute apr\xE8s `# Budget` (et avant `# Design` si elle existe) une section `# Hypoth\xE8ses`, une puce par choix, en fran\xE7ais, courte ; sans hypoth\xE8se, pas de section.",
|
|
2820
3090
|
'- QUESTIONS : quand la demande ci-dessous t\'y autorise et qu\'une r\xE9ponse du client CHANGERAIT la spec, tu peux, AU LIEU du fichier, r\xE9pondre par une ligne `QUESTIONS` seule puis un objet JSON `{"questions":[{"id":"kebab-case","text":"la question, en fran\xE7ais","options":["r\xE9ponse","r\xE9ponse"]}]}` \u2014 trois \xE0 cinq questions ferm\xE9es, deux \xE0 quatre options chacune, rien d\'autre avant ni apr\xE8s. Une question dont la r\xE9ponse ne changerait pas la spec ne se pose pas : elle devient une hypoth\xE8se.',
|
|
@@ -2966,6 +3236,8 @@ async function runBrief(deps, input) {
|
|
|
2966
3236
|
const existingSpec = input.specId !== void 0 ? await readIfPresent(deps.storage, `specs/${input.specId}.spec.md`) : null;
|
|
2967
3237
|
const isAmendment = existingSpec !== null;
|
|
2968
3238
|
const round = Math.min(Math.max(input.round ?? 1, 1), MAX_QUESTION_ROUNDS + 1);
|
|
3239
|
+
const ungeneratedAsked = asksUngenerated(input.brief);
|
|
3240
|
+
const blockRequired = manifest.generation !== null && !ungeneratedAsked;
|
|
2969
3241
|
const questionsAllowed = input.skipQuestions !== true && round <= MAX_QUESTION_ROUNDS;
|
|
2970
3242
|
const prompt = buildBriefPrompt({
|
|
2971
3243
|
template,
|
|
@@ -2979,7 +3251,8 @@ async function runBrief(deps, input) {
|
|
|
2979
3251
|
brief: input.brief,
|
|
2980
3252
|
attachments: input.attachments,
|
|
2981
3253
|
answers: input.answers,
|
|
2982
|
-
questions: { allowed: questionsAllowed, round }
|
|
3254
|
+
questions: { allowed: questionsAllowed, round },
|
|
3255
|
+
ungenerated: ungeneratedAsked
|
|
2983
3256
|
});
|
|
2984
3257
|
const usage = [];
|
|
2985
3258
|
let refusedQuestions = false;
|
|
@@ -3021,7 +3294,8 @@ async function runBrief(deps, input) {
|
|
|
3021
3294
|
});
|
|
3022
3295
|
const generationJudgement = judged.problems.length === 0 && judged.specId !== null ? await judgeGeneration({ content, specPath: `specs/${judged.specId}.spec.md`, generation: manifest.generation, storage: deps.storage, features: manifest.features }) : { problems: [], tally: null };
|
|
3023
3296
|
judged.problems.push(...generationJudgement.problems);
|
|
3024
|
-
if (generationJudgement.tally
|
|
3297
|
+
if (blockRequired && judged.problems.length === 0 && generationJudgement.tally === null) judged.problems.push(BLOCK_REQUIRED);
|
|
3298
|
+
if (generationJudgement.tally !== null || blockRequired) maxAttempts = MAX_ATTEMPTS_GENERATED;
|
|
3025
3299
|
deps.log(JSON.stringify({ action: "brief", attempt, specId: judged.specId, problems: judged.problems.length, generated: generationJudgement.tally !== null, ...answer.usage }));
|
|
3026
3300
|
if (judged.problems.length === 0 && judged.specId !== null) {
|
|
3027
3301
|
const specId = judged.specId;
|
|
@@ -3242,7 +3516,7 @@ async function registerBriefRoutes(app, deps) {
|
|
|
3242
3516
|
route.route({
|
|
3243
3517
|
method: "GET",
|
|
3244
3518
|
url: "/api/briefs/:jobId",
|
|
3245
|
-
schema: { params:
|
|
3519
|
+
schema: { params: z12.object({ jobId: z12.string().min(1) }), response: { 200: BriefJobSchema } },
|
|
3246
3520
|
handler: async (request, reply) => {
|
|
3247
3521
|
hostedUserOf(request);
|
|
3248
3522
|
return reply.code(200).send(jobs.read(request.params.jobId));
|
|
@@ -3862,17 +4136,17 @@ async function registerPromotionRoutes(app, deps) {
|
|
|
3862
4136
|
}
|
|
3863
4137
|
|
|
3864
4138
|
// src/server/hosted/usage.ts
|
|
3865
|
-
import { z as
|
|
3866
|
-
var UsageEntrySchema =
|
|
3867
|
-
at:
|
|
4139
|
+
import { z as z13 } from "zod";
|
|
4140
|
+
var UsageEntrySchema = z13.object({
|
|
4141
|
+
at: z13.string(),
|
|
3868
4142
|
/** brief → spec from the instance; `speculate` entries arrive with the generator's lot. */
|
|
3869
|
-
action:
|
|
3870
|
-
attempt:
|
|
3871
|
-
model:
|
|
3872
|
-
inputTokens:
|
|
3873
|
-
outputTokens:
|
|
3874
|
-
actor:
|
|
3875
|
-
specId:
|
|
4143
|
+
action: z13.enum(["brief", "speculate"]),
|
|
4144
|
+
attempt: z13.number().int().positive().optional(),
|
|
4145
|
+
model: z13.string(),
|
|
4146
|
+
inputTokens: z13.number().int().nonnegative(),
|
|
4147
|
+
outputTokens: z13.number().int().nonnegative(),
|
|
4148
|
+
actor: z13.string().optional(),
|
|
4149
|
+
specId: z13.string().optional()
|
|
3876
4150
|
});
|
|
3877
4151
|
var USAGE_MARKER = "<!-- ogygie-usage -->";
|
|
3878
4152
|
var DEFAULT_MODEL_COSTS = {
|
|
@@ -3907,7 +4181,7 @@ function parseUsageComment(body) {
|
|
|
3907
4181
|
const block = /```json\n([\s\S]*?)\n```/.exec(body)?.[1];
|
|
3908
4182
|
if (block === void 0) return [];
|
|
3909
4183
|
try {
|
|
3910
|
-
const parsed =
|
|
4184
|
+
const parsed = z13.array(UsageEntrySchema).safeParse(JSON.parse(block));
|
|
3911
4185
|
return parsed.success ? parsed.data : [];
|
|
3912
4186
|
} catch {
|
|
3913
4187
|
return [];
|
|
@@ -3982,9 +4256,9 @@ async function registerConsumptionRoutes(app, deps) {
|
|
|
3982
4256
|
}
|
|
3983
4257
|
|
|
3984
4258
|
// src/server/hosted/preview-routes.ts
|
|
3985
|
-
import { z as
|
|
3986
|
-
var PreviewParamsSchema = SpecParamsSchema.extend({ state:
|
|
3987
|
-
var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition:
|
|
4259
|
+
import { z as z14 } from "zod";
|
|
4260
|
+
var PreviewParamsSchema = SpecParamsSchema.extend({ state: z14.string().min(1).max(80) });
|
|
4261
|
+
var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition: z14.string().min(1).max(80) });
|
|
3988
4262
|
var PREVIEW_CSP = "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:";
|
|
3989
4263
|
async function registerPreviewRoutes(app, deps) {
|
|
3990
4264
|
const route = app.withTypeProvider();
|
|
@@ -4026,7 +4300,7 @@ async function registerPreviewRoutes(app, deps) {
|
|
|
4026
4300
|
}
|
|
4027
4301
|
|
|
4028
4302
|
// src/server/hosted/composition-routes.ts
|
|
4029
|
-
import { z as
|
|
4303
|
+
import { z as z15 } from "zod";
|
|
4030
4304
|
|
|
4031
4305
|
// src/server/hosted/compositions.ts
|
|
4032
4306
|
var COMPOSITION_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
@@ -4070,7 +4344,7 @@ La composition \xAB ${composition} \xBB est retenue parmi celles propos\xE9es ;
|
|
|
4070
4344
|
}
|
|
4071
4345
|
|
|
4072
4346
|
// src/server/hosted/composition-routes.ts
|
|
4073
|
-
var CompositionParamsSchema = SpecParamsSchema.extend({ composition:
|
|
4347
|
+
var CompositionParamsSchema = SpecParamsSchema.extend({ composition: z15.string().min(1).max(80) });
|
|
4074
4348
|
async function registerCompositionRoutes(app, deps) {
|
|
4075
4349
|
const route = app.withTypeProvider();
|
|
4076
4350
|
route.route({
|
|
@@ -4803,6 +5077,7 @@ async function startHostedStudio(options) {
|
|
|
4803
5077
|
const privateKeyPem = readSecret(config.githubApp.privateKeyPath, "the GitHub App private key");
|
|
4804
5078
|
const sessionSecret = readSecret(config.session.secretPath, "the session secret", MIN_SESSION_SECRET_LENGTH);
|
|
4805
5079
|
const mailApiKey = readSecret(config.mail.apiKeyPath, "the mail provider key");
|
|
5080
|
+
const entraClientSecret = config.identity === void 0 ? void 0 : readSecret(config.identity.clientSecretPath, "the Entra client secret");
|
|
4806
5081
|
const modelApiKey = config.model === void 0 ? void 0 : process.env["OGYGIE_MODEL_API_KEY"] ?? "";
|
|
4807
5082
|
if (modelApiKey !== void 0 && modelApiKey.length === 0) {
|
|
4808
5083
|
throwOgygieError({
|
|
@@ -4869,6 +5144,19 @@ async function startHostedStudio(options) {
|
|
|
4869
5144
|
now,
|
|
4870
5145
|
log,
|
|
4871
5146
|
consumedLinks: /* @__PURE__ */ new Map(),
|
|
5147
|
+
...config.identity === void 0 || entraClientSecret === void 0 ? {} : {
|
|
5148
|
+
entra: {
|
|
5149
|
+
fetchImpl: fetch,
|
|
5150
|
+
config: {
|
|
5151
|
+
tenantId: config.identity.tenantId,
|
|
5152
|
+
clientId: config.identity.clientId,
|
|
5153
|
+
clientSecret: entraClientSecret,
|
|
5154
|
+
callbackUrl: `${config.publicUrl.replace(/\/$/, "")}/api/auth/entra/callback`,
|
|
5155
|
+
groupsClaim: config.identity.groupsClaim,
|
|
5156
|
+
roles: config.identity.roles
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
},
|
|
4872
5160
|
pulls: createGithubPullsClient({ github: config.repo, fetchImpl: githubApp.installationFetch }),
|
|
4873
5161
|
readerFor,
|
|
4874
5162
|
usage: createGithubUsageClient({ github: config.repo, fetchImpl: githubApp.installationFetch }),
|