@ogygie/studio 0.30.0 → 0.32.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
|
@@ -1187,7 +1187,7 @@ ${deps.originOf(path)}`
|
|
|
1187
1187
|
}
|
|
1188
1188
|
|
|
1189
1189
|
// src/server/storage/local-adapter.ts
|
|
1190
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1190
|
+
import { mkdir, stat as stat2, writeFile } from "node:fs/promises";
|
|
1191
1191
|
import { dirname, isAbsolute as isAbsolute2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
1192
1192
|
function resolveInsideRoot2(rootDir, relativePath) {
|
|
1193
1193
|
const root = resolve2(rootDir);
|
|
@@ -1214,6 +1214,14 @@ function createLocalSpecStorage(rootDir) {
|
|
|
1214
1214
|
const absolute = resolveInsideRoot2(rootDir, path);
|
|
1215
1215
|
await mkdir(dirname(absolute), { recursive: true });
|
|
1216
1216
|
await writeFile(absolute, content, "utf8");
|
|
1217
|
+
},
|
|
1218
|
+
async modifiedAt(path) {
|
|
1219
|
+
const absolute = resolveInsideRoot2(rootDir, path);
|
|
1220
|
+
try {
|
|
1221
|
+
return (await stat2(absolute)).mtime.toISOString();
|
|
1222
|
+
} catch {
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1217
1225
|
}
|
|
1218
1226
|
};
|
|
1219
1227
|
}
|
|
@@ -1345,7 +1353,8 @@ function intentOf(body) {
|
|
|
1345
1353
|
const sentence = /^.*?[.!?](?:\s|$)/u.exec(paragraph)?.[0]?.trim() ?? paragraph;
|
|
1346
1354
|
return sentence.length > 160 ? `${sentence.slice(0, 157).trimEnd()}\u2026` : sentence;
|
|
1347
1355
|
}
|
|
1348
|
-
|
|
1356
|
+
var modifiedAtOf = (deps, filePath) => deps.storage.modifiedAt?.(filePath) ?? Promise.resolve(null);
|
|
1357
|
+
function toReadableSummary(filePath, spec, updatedAt) {
|
|
1349
1358
|
return {
|
|
1350
1359
|
id: spec.frontmatter.id,
|
|
1351
1360
|
status: spec.frontmatter.status,
|
|
@@ -1353,7 +1362,8 @@ function toReadableSummary(filePath, spec) {
|
|
|
1353
1362
|
depends_on: spec.frontmatter.depends_on,
|
|
1354
1363
|
filePath,
|
|
1355
1364
|
presentSections: presentSectionsOf(spec.body),
|
|
1356
|
-
intent: intentOf(spec.body)
|
|
1365
|
+
intent: intentOf(spec.body),
|
|
1366
|
+
updatedAt
|
|
1357
1367
|
};
|
|
1358
1368
|
}
|
|
1359
1369
|
async function listSpecFileNames(deps) {
|
|
@@ -1369,7 +1379,7 @@ async function listSpecSummaries(deps) {
|
|
|
1369
1379
|
const parsed = parseSpec(filePath, fileName, content);
|
|
1370
1380
|
const id = fileName.replace(/\.spec\.md$/, "");
|
|
1371
1381
|
summaries.push(
|
|
1372
|
-
parsed.ok ? toReadableSummary(filePath, parsed.spec) : { id, filePath, error: parsed.error }
|
|
1382
|
+
parsed.ok ? toReadableSummary(filePath, parsed.spec, await modifiedAtOf(deps, filePath)) : { id, filePath, error: parsed.error }
|
|
1373
1383
|
);
|
|
1374
1384
|
}
|
|
1375
1385
|
return summaries;
|
|
@@ -1484,7 +1494,7 @@ ${serialized}
|
|
|
1484
1494
|
---${body}`;
|
|
1485
1495
|
const message = isValidation ? `spec: validate ${specId}` : `spec: save ${specId}`;
|
|
1486
1496
|
await deps.storage.write(filePath, content, message);
|
|
1487
|
-
return toReadableSummary(filePath, { frontmatter: input.frontmatter, body });
|
|
1497
|
+
return toReadableSummary(filePath, { frontmatter: input.frontmatter, body }, await modifiedAtOf(deps, filePath));
|
|
1488
1498
|
}
|
|
1489
1499
|
async function createSpec(deps, specId) {
|
|
1490
1500
|
const fileName = `${specId}.spec.md`;
|
|
@@ -1526,7 +1536,7 @@ async function createSpec(deps, specId) {
|
|
|
1526
1536
|
suggestedFix: "Fix the template's frontmatter to match SpecFrontmatterSchema."
|
|
1527
1537
|
});
|
|
1528
1538
|
}
|
|
1529
|
-
return toReadableSummary(filePath, parsed.spec);
|
|
1539
|
+
return toReadableSummary(filePath, parsed.spec, await modifiedAtOf(deps, filePath));
|
|
1530
1540
|
}
|
|
1531
1541
|
async function getManifest(deps) {
|
|
1532
1542
|
const content = await deps.storage.readFile(MANIFEST_PATH);
|
|
@@ -1920,7 +1930,9 @@ var ReadableSpecSummarySchema = z7.object({
|
|
|
1920
1930
|
filePath: z7.string(),
|
|
1921
1931
|
presentSections: z7.array(z7.string()),
|
|
1922
1932
|
/** The first sentence of `# @intent`, for a list that reads (v1.20: the target-spec suggestions of the brief). */
|
|
1923
|
-
intent: z7.string().default("")
|
|
1933
|
+
intent: z7.string().default(""),
|
|
1934
|
+
/** When the spec last changed (ISO 8601) — the board's order, newest first (v3.4); null when the storage does not know. */
|
|
1935
|
+
updatedAt: z7.string().nullable().default(null)
|
|
1924
1936
|
});
|
|
1925
1937
|
var UnreadableSpecSummarySchema = z7.object({
|
|
1926
1938
|
id: z7.string(),
|
|
@@ -2203,7 +2215,7 @@ async function registerStudioRoutes(app, resolver) {
|
|
|
2203
2215
|
}
|
|
2204
2216
|
|
|
2205
2217
|
// src/server/hosted/routes.ts
|
|
2206
|
-
import { z as
|
|
2218
|
+
import { z as z10 } from "zod";
|
|
2207
2219
|
|
|
2208
2220
|
// src/server/hosted/auth.ts
|
|
2209
2221
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
@@ -2258,19 +2270,27 @@ function consumeMagicLink(deps, token) {
|
|
|
2258
2270
|
deps.consumed.set(nonce, exp);
|
|
2259
2271
|
return email;
|
|
2260
2272
|
}
|
|
2261
|
-
function issueSession(deps,
|
|
2262
|
-
return seal(
|
|
2273
|
+
function issueSession(deps, user) {
|
|
2274
|
+
return seal(
|
|
2275
|
+
{ kind: "session", email: user.email.toLowerCase(), displayName: user.displayName, role: user.role, via: user.via, exp: deps.now() + deps.ttlHours * 36e5 },
|
|
2276
|
+
deps.secret
|
|
2277
|
+
);
|
|
2263
2278
|
}
|
|
2279
|
+
var ROLES = /* @__PURE__ */ new Set(["reader", "author", "approver", "operator"]);
|
|
2280
|
+
var VIAS = /* @__PURE__ */ new Set(["magic-link", "entra"]);
|
|
2264
2281
|
function readSession(deps, cookieHeader) {
|
|
2265
2282
|
const token = cookieValue(cookieHeader, SESSION_COOKIE);
|
|
2266
2283
|
if (token === null) return null;
|
|
2267
2284
|
const value = open(token, deps.secret);
|
|
2268
2285
|
const email = value?.["email"];
|
|
2286
|
+
const displayName = value?.["displayName"];
|
|
2287
|
+
const role = value?.["role"];
|
|
2288
|
+
const via = value?.["via"];
|
|
2269
2289
|
const exp = value?.["exp"];
|
|
2270
|
-
if (value?.["kind"] !== "session" || typeof email !== "string" || typeof exp !== "number" || exp <= deps.now()) {
|
|
2290
|
+
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()) {
|
|
2271
2291
|
return null;
|
|
2272
2292
|
}
|
|
2273
|
-
return email;
|
|
2293
|
+
return { email, displayName, role, via };
|
|
2274
2294
|
}
|
|
2275
2295
|
function cookieValue(cookieHeader, name) {
|
|
2276
2296
|
if (cookieHeader === void 0) return null;
|
|
@@ -2319,6 +2339,21 @@ function unauthenticated(cause) {
|
|
|
2319
2339
|
// src/server/hosted/config.ts
|
|
2320
2340
|
import { z as z8 } from "zod";
|
|
2321
2341
|
var HostRoleSchema = z8.enum(["reader", "author", "approver", "operator"]);
|
|
2342
|
+
var HOST_ROLES = HostRoleSchema.options;
|
|
2343
|
+
var HostIdentitySchema = z8.object({
|
|
2344
|
+
kind: z8.literal("entra"),
|
|
2345
|
+
tenantId: z8.string().min(1),
|
|
2346
|
+
clientId: z8.string().min(1),
|
|
2347
|
+
clientSecretPath: z8.string().min(1),
|
|
2348
|
+
/** Where the tenant writes the groups in the id token — a setting of the app registration. */
|
|
2349
|
+
groupsClaim: z8.string().min(1).default("groups"),
|
|
2350
|
+
roles: z8.object({
|
|
2351
|
+
reader: z8.array(z8.string().min(1)).default([]),
|
|
2352
|
+
author: z8.array(z8.string().min(1)).default([]),
|
|
2353
|
+
approver: z8.array(z8.string().min(1)).default([]),
|
|
2354
|
+
operator: z8.array(z8.string().min(1)).default([])
|
|
2355
|
+
})
|
|
2356
|
+
});
|
|
2322
2357
|
var MergePolicySchema = z8.enum(["client", "operator"]);
|
|
2323
2358
|
var HostUserSchema = z8.object({
|
|
2324
2359
|
email: z8.email(),
|
|
@@ -2332,7 +2367,9 @@ var HostConfigSchema = z8.object({
|
|
|
2332
2367
|
installationId: z8.string().min(1),
|
|
2333
2368
|
privateKeyPath: z8.string().min(1)
|
|
2334
2369
|
}),
|
|
2335
|
-
|
|
2370
|
+
/** The addresses the magic link serves; optional once `identity` opens the door (D59). */
|
|
2371
|
+
users: z8.array(HostUserSchema).default([]),
|
|
2372
|
+
identity: HostIdentitySchema.optional(),
|
|
2336
2373
|
/** "operator" at onboarding: the agency keeps the merge until it hands it over (D48). */
|
|
2337
2374
|
mergePolicy: MergePolicySchema.default("operator"),
|
|
2338
2375
|
writeRateLimit: z8.number().int().positive().default(20),
|
|
@@ -2365,6 +2402,9 @@ var HostConfigSchema = z8.object({
|
|
|
2365
2402
|
/** USD per million tokens, by model id — overrides the built-in table of the estimate (B8). */
|
|
2366
2403
|
costPerMillionUsd: z8.record(z8.string(), z8.object({ input: z8.number().nonnegative(), output: z8.number().nonnegative() })).optional()
|
|
2367
2404
|
}).optional()
|
|
2405
|
+
}).refine((config) => config.users.length > 0 || config.identity !== void 0, {
|
|
2406
|
+
path: ["users"],
|
|
2407
|
+
message: "declare at least one user, or an identity block (D59): a host nobody can enter is a configuration error"
|
|
2368
2408
|
});
|
|
2369
2409
|
function parseHostConfig(raw, configPath) {
|
|
2370
2410
|
const parsed = HostConfigSchema.safeParse(raw);
|
|
@@ -2384,20 +2424,190 @@ function findHostUser(config, email) {
|
|
|
2384
2424
|
return config.users.find((user) => user.email.toLowerCase() === wanted) ?? null;
|
|
2385
2425
|
}
|
|
2386
2426
|
|
|
2427
|
+
// src/server/hosted/entra.ts
|
|
2428
|
+
import { createHash, createPublicKey, randomBytes as randomBytes2, verify as verifySignature } from "node:crypto";
|
|
2429
|
+
import { z as z9 } from "zod";
|
|
2430
|
+
var OIDC_SCOPE = "openid profile email";
|
|
2431
|
+
var CLOCK_SKEW_SECONDS = 60;
|
|
2432
|
+
var FILE = "packages/ogygie-studio/src/server/hosted/entra.ts";
|
|
2433
|
+
var baseUrlOf = (config) => `https://login.microsoftonline.com/${config.tenantId}`;
|
|
2434
|
+
var issuerOf = (config) => `${baseUrlOf(config)}/v2.0`;
|
|
2435
|
+
var tokenEndpointOf = (config) => `${baseUrlOf(config)}/oauth2/v2.0/token`;
|
|
2436
|
+
var jwksEndpointOf = (config) => `${baseUrlOf(config)}/discovery/v2.0/keys`;
|
|
2437
|
+
function codeChallengeOf(codeVerifier) {
|
|
2438
|
+
return createHash("sha256").update(codeVerifier).digest("base64url");
|
|
2439
|
+
}
|
|
2440
|
+
function newHandshake() {
|
|
2441
|
+
return { state: randomBytes2(16).toString("hex"), nonce: randomBytes2(16).toString("hex"), codeVerifier: randomBytes2(48).toString("base64url") };
|
|
2442
|
+
}
|
|
2443
|
+
function authorizationUrlOf(config, handshake) {
|
|
2444
|
+
const url = new URL(`${baseUrlOf(config)}/oauth2/v2.0/authorize`);
|
|
2445
|
+
url.searchParams.set("client_id", config.clientId);
|
|
2446
|
+
url.searchParams.set("response_type", "code");
|
|
2447
|
+
url.searchParams.set("redirect_uri", config.callbackUrl);
|
|
2448
|
+
url.searchParams.set("response_mode", "query");
|
|
2449
|
+
url.searchParams.set("scope", OIDC_SCOPE);
|
|
2450
|
+
url.searchParams.set("state", handshake.state);
|
|
2451
|
+
url.searchParams.set("nonce", handshake.nonce);
|
|
2452
|
+
url.searchParams.set("code_challenge", codeChallengeOf(handshake.codeVerifier));
|
|
2453
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
2454
|
+
return url.toString();
|
|
2455
|
+
}
|
|
2456
|
+
function unauthenticated2(cause, suggestedFix) {
|
|
2457
|
+
return throwOgygieError(
|
|
2458
|
+
{ code: "HOST_UNAUTHENTICATED", message: "The Microsoft sign-in did not establish an identity.", file: FILE, cause, suggestedFix },
|
|
2459
|
+
401
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
var IdTokenClaimsSchema = z9.looseObject({
|
|
2463
|
+
sub: z9.string().min(1),
|
|
2464
|
+
iss: z9.string().min(1),
|
|
2465
|
+
aud: z9.union([z9.string(), z9.array(z9.string())]),
|
|
2466
|
+
exp: z9.number(),
|
|
2467
|
+
nonce: z9.string().optional(),
|
|
2468
|
+
email: z9.string().optional(),
|
|
2469
|
+
preferred_username: z9.string().optional(),
|
|
2470
|
+
upn: z9.string().optional(),
|
|
2471
|
+
name: z9.string().optional()
|
|
2472
|
+
});
|
|
2473
|
+
var RsaSigningKeySchema = z9.object({ kid: z9.string().min(1), kty: z9.literal("RSA"), n: z9.string().min(1), e: z9.string().min(1) });
|
|
2474
|
+
var JwksSchema = z9.object({ keys: z9.array(z9.unknown()) });
|
|
2475
|
+
var TokenResponseSchema = z9.looseObject({ id_token: z9.string().min(1) });
|
|
2476
|
+
function decodeJwt(token) {
|
|
2477
|
+
const [encodedHeader, encodedPayload, encodedSignature, ...rest] = token.split(".");
|
|
2478
|
+
if (encodedHeader === void 0 || encodedPayload === void 0 || encodedSignature === void 0 || rest.length > 0) return null;
|
|
2479
|
+
try {
|
|
2480
|
+
const header = JSON.parse(Buffer.from(encodedHeader, "base64url").toString("utf8"));
|
|
2481
|
+
if (typeof header !== "object" || header === null) return null;
|
|
2482
|
+
const { alg, kid } = header;
|
|
2483
|
+
return {
|
|
2484
|
+
header: { ...typeof alg === "string" ? { alg } : {}, ...typeof kid === "string" ? { kid } : {} },
|
|
2485
|
+
payload: JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8")),
|
|
2486
|
+
signingInput: `${encodedHeader}.${encodedPayload}`,
|
|
2487
|
+
signature: Buffer.from(encodedSignature, "base64url")
|
|
2488
|
+
};
|
|
2489
|
+
} catch {
|
|
2490
|
+
return null;
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
function verifyIdToken(input) {
|
|
2494
|
+
const decoded = decodeJwt(input.idToken);
|
|
2495
|
+
if (decoded === null) return unauthenticated2("id token is not a readable JWT", "Retry the sign-in.");
|
|
2496
|
+
if (decoded.header.alg !== "RS256") {
|
|
2497
|
+
return unauthenticated2(`id token signed with alg=${decoded.header.alg ?? "none"}, only RS256 is accepted`, "Check the signing algorithm of the app registration (RS256).");
|
|
2498
|
+
}
|
|
2499
|
+
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);
|
|
2500
|
+
if (candidates.length === 0) {
|
|
2501
|
+
return unauthenticated2(`no published RSA key matches kid=${decoded.header.kid ?? "(absent)"}`, "Check that the id token comes from the configured tenant.");
|
|
2502
|
+
}
|
|
2503
|
+
const signed = candidates.some(
|
|
2504
|
+
(key) => verifySignature("sha256", Buffer.from(decoded.signingInput, "utf8"), createPublicKey({ key: { kty: "RSA", n: key.n, e: key.e }, format: "jwk" }), decoded.signature)
|
|
2505
|
+
);
|
|
2506
|
+
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.");
|
|
2507
|
+
const claims = IdTokenClaimsSchema.safeParse(decoded.payload);
|
|
2508
|
+
if (!claims.success) return unauthenticated2("id token claims do not have the expected shape", "Check the app registration (openid scope, id token).");
|
|
2509
|
+
const expectedIssuer = issuerOf(input.config);
|
|
2510
|
+
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.");
|
|
2511
|
+
const audiences = Array.isArray(claims.data.aud) ? claims.data.aud : [claims.data.aud];
|
|
2512
|
+
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.");
|
|
2513
|
+
if (claims.data.exp + CLOCK_SKEW_SECONDS < Math.floor(input.nowMs / 1e3)) return unauthenticated2("id token has expired", "Retry the sign-in.");
|
|
2514
|
+
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.");
|
|
2515
|
+
return claims.data;
|
|
2516
|
+
}
|
|
2517
|
+
function groupsOfClaim(value) {
|
|
2518
|
+
if (typeof value === "string") return [value];
|
|
2519
|
+
if (!Array.isArray(value)) return [];
|
|
2520
|
+
return value.filter((group) => typeof group === "string" && group.length > 0);
|
|
2521
|
+
}
|
|
2522
|
+
function identityOfClaims(claims, groupsClaim) {
|
|
2523
|
+
const email = claims.email ?? claims.preferred_username ?? claims.upn;
|
|
2524
|
+
if (email === void 0 || email === "") {
|
|
2525
|
+
return unauthenticated2("the id token carries no email, preferred_username nor upn claim", "Add the optional email claim on the app registration.");
|
|
2526
|
+
}
|
|
2527
|
+
return { email: email.toLowerCase(), displayName: claims.name ?? email, groups: groupsOfClaim(claims[groupsClaim]) };
|
|
2528
|
+
}
|
|
2529
|
+
function roleOfGroups(groups, roles) {
|
|
2530
|
+
let found = null;
|
|
2531
|
+
for (const role of HOST_ROLES) if (roles[role].some((group) => group !== "" && groups.includes(group))) found = role;
|
|
2532
|
+
return found;
|
|
2533
|
+
}
|
|
2534
|
+
function hostUserOfIdentity(identity, declared, roles) {
|
|
2535
|
+
if (declared !== null) return { email: declared.email, displayName: declared.displayName, role: declared.role };
|
|
2536
|
+
const role = roleOfGroups(identity.groups, roles);
|
|
2537
|
+
if (role === null) {
|
|
2538
|
+
return throwOgygieError(
|
|
2539
|
+
{
|
|
2540
|
+
code: "HOST_ACCESS_DENIED",
|
|
2541
|
+
message: "This account is authenticated but not invited on this host.",
|
|
2542
|
+
file: null,
|
|
2543
|
+
cause: `${identity.email}: none of its ${identity.groups.length} group(s) is declared under identity.roles, and the address is not in users`,
|
|
2544
|
+
suggestedFix: "Add the account to one of the directory groups the host declares (identity.roles), or declare its address in users."
|
|
2545
|
+
},
|
|
2546
|
+
403
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
return { email: identity.email, displayName: identity.displayName, role };
|
|
2550
|
+
}
|
|
2551
|
+
async function readJson(response, endpoint) {
|
|
2552
|
+
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.");
|
|
2553
|
+
try {
|
|
2554
|
+
return await response.json();
|
|
2555
|
+
} catch {
|
|
2556
|
+
return unauthenticated2(`${endpoint} answered a body that is not JSON`, "Retry the sign-in.");
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
async function exchangeAuthorizationCode(deps, config, input) {
|
|
2560
|
+
const endpoint = tokenEndpointOf(config);
|
|
2561
|
+
const response = await deps.fetchImpl(endpoint, {
|
|
2562
|
+
method: "POST",
|
|
2563
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
2564
|
+
body: new URLSearchParams({
|
|
2565
|
+
grant_type: "authorization_code",
|
|
2566
|
+
client_id: config.clientId,
|
|
2567
|
+
client_secret: config.clientSecret,
|
|
2568
|
+
code: input.code,
|
|
2569
|
+
code_verifier: input.codeVerifier,
|
|
2570
|
+
redirect_uri: config.callbackUrl
|
|
2571
|
+
}).toString()
|
|
2572
|
+
});
|
|
2573
|
+
const parsed = TokenResponseSchema.safeParse(await readJson(response, endpoint));
|
|
2574
|
+
if (!parsed.success) return unauthenticated2(`${endpoint} answered without an id_token`, "Check that the app registration requests the openid scope.");
|
|
2575
|
+
return parsed.data.id_token;
|
|
2576
|
+
}
|
|
2577
|
+
async function fetchSigningKeys(deps, config) {
|
|
2578
|
+
const endpoint = jwksEndpointOf(config);
|
|
2579
|
+
const response = await deps.fetchImpl(endpoint, { method: "GET", headers: {} });
|
|
2580
|
+
const parsed = JwksSchema.safeParse(await readJson(response, endpoint));
|
|
2581
|
+
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.");
|
|
2582
|
+
return parsed.data.keys;
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2387
2585
|
// src/server/hosted/routes.ts
|
|
2586
|
+
var OIDC_COOKIE = "ogygie_host_oidc";
|
|
2587
|
+
var OIDC_TTL_MS = 10 * 60 * 1e3;
|
|
2588
|
+
var oidcCookie = (value) => `${OIDC_COOKIE}=${encodeURIComponent(value)}; Path=/api/auth/entra; Max-Age=${OIDC_TTL_MS / 1e3}; HttpOnly; Secure; SameSite=Lax`;
|
|
2589
|
+
var clearedOidcCookie = () => `${OIDC_COOKIE}=; Path=/api/auth/entra; Max-Age=0; HttpOnly; Secure; SameSite=Lax`;
|
|
2590
|
+
function cookieOf(cookieHeader, name) {
|
|
2591
|
+
if (cookieHeader === void 0) return null;
|
|
2592
|
+
for (const part of cookieHeader.split(";")) {
|
|
2593
|
+
const [key, ...rest] = part.trim().split("=");
|
|
2594
|
+
if (key === name) return decodeURIComponent(rest.join("="));
|
|
2595
|
+
}
|
|
2596
|
+
return null;
|
|
2597
|
+
}
|
|
2388
2598
|
var usersByRequest = /* @__PURE__ */ new WeakMap();
|
|
2389
2599
|
function hostedUserOf(request) {
|
|
2390
2600
|
const user = usersByRequest.get(request);
|
|
2391
2601
|
if (user === void 0) return unauthenticated("no session attached to this request");
|
|
2392
2602
|
return user;
|
|
2393
2603
|
}
|
|
2394
|
-
var MagicLinkInputSchema =
|
|
2395
|
-
var CallbackQuerySchema =
|
|
2396
|
-
var MeOutputSchema =
|
|
2397
|
-
email:
|
|
2398
|
-
displayName:
|
|
2604
|
+
var MagicLinkInputSchema = z10.object({ email: z10.email() });
|
|
2605
|
+
var CallbackQuerySchema = z10.object({ token: z10.string().min(1) });
|
|
2606
|
+
var MeOutputSchema = z10.object({
|
|
2607
|
+
email: z10.email(),
|
|
2608
|
+
displayName: z10.string(),
|
|
2399
2609
|
role: HostRoleSchema,
|
|
2400
|
-
host:
|
|
2610
|
+
host: z10.object({ environments: z10.boolean(), mergePolicy: z10.enum(["client", "operator"]) })
|
|
2401
2611
|
});
|
|
2402
2612
|
async function registerHostedRoutes(app, deps) {
|
|
2403
2613
|
const sessions = {
|
|
@@ -2411,21 +2621,74 @@ async function registerHostedRoutes(app, deps) {
|
|
|
2411
2621
|
consumed: deps.consumedLinks,
|
|
2412
2622
|
...deps.magicLinkTtlMs === void 0 ? {} : { ttlMs: deps.magicLinkTtlMs }
|
|
2413
2623
|
};
|
|
2414
|
-
const PUBLIC_ROUTES = /* @__PURE__ */ new Set(["/api/auth/magic-link", "/api/auth/callback", "/api/auth/logout"]);
|
|
2624
|
+
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"]);
|
|
2415
2625
|
app.addHook("onRequest", async (request) => {
|
|
2416
2626
|
const url = request.url.split("?")[0] ?? request.url;
|
|
2417
2627
|
if (!url.startsWith("/api/") || PUBLIC_ROUTES.has(url)) return;
|
|
2418
|
-
const
|
|
2419
|
-
if (
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2628
|
+
const session = readSession(sessions, request.headers.cookie);
|
|
2629
|
+
if (session === null) return unauthenticated("missing or expired session cookie");
|
|
2630
|
+
if (session.via === "magic-link") {
|
|
2631
|
+
const declared = findHostUser(deps.config, session.email);
|
|
2632
|
+
if (declared === null) return unauthenticated(`session for ${session.email}, no longer a declared user`);
|
|
2633
|
+
usersByRequest.set(request, declared);
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
usersByRequest.set(request, { email: session.email, displayName: session.displayName, role: session.role });
|
|
2423
2637
|
});
|
|
2424
2638
|
const route = app.withTypeProvider();
|
|
2639
|
+
route.route({
|
|
2640
|
+
method: "GET",
|
|
2641
|
+
url: "/api/auth/options",
|
|
2642
|
+
schema: { response: { 200: z10.object({ magicLink: z10.boolean(), entra: z10.boolean() }) } },
|
|
2643
|
+
handler: async (_request, reply) => reply.code(200).send({ magicLink: deps.config.users.length > 0, entra: deps.entra !== void 0 })
|
|
2644
|
+
});
|
|
2645
|
+
if (deps.entra !== void 0) {
|
|
2646
|
+
const entra = deps.entra;
|
|
2647
|
+
route.route({
|
|
2648
|
+
method: "GET",
|
|
2649
|
+
url: "/api/auth/entra/start",
|
|
2650
|
+
handler: async (_request, reply) => {
|
|
2651
|
+
const handshake = newHandshake();
|
|
2652
|
+
const state = seal({ kind: "oidc", ...handshake, exp: deps.now() + OIDC_TTL_MS }, deps.sessionSecret);
|
|
2653
|
+
return reply.header("set-cookie", oidcCookie(state)).redirect(authorizationUrlOf(entra.config, handshake), 302);
|
|
2654
|
+
}
|
|
2655
|
+
});
|
|
2656
|
+
route.route({
|
|
2657
|
+
method: "GET",
|
|
2658
|
+
url: "/api/auth/entra/callback",
|
|
2659
|
+
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() }) },
|
|
2660
|
+
handler: async (request, reply) => {
|
|
2661
|
+
const refused = (code, cause) => {
|
|
2662
|
+
deps.log(JSON.stringify({ action: "sign-in", provider: "entra", outcome: code, cause }));
|
|
2663
|
+
return reply.header("set-cookie", clearedOidcCookie()).redirect(`/login?error=${encodeURIComponent(code)}`, 302);
|
|
2664
|
+
};
|
|
2665
|
+
if (request.query.error !== void 0) return refused("HOST_UNAUTHENTICATED", `tenant answered ${request.query.error}: ${request.query.error_description ?? ""}`);
|
|
2666
|
+
const sealed = cookieOf(request.headers.cookie, OIDC_COOKIE);
|
|
2667
|
+
const handshake = sealed === null ? null : open(sealed, deps.sessionSecret);
|
|
2668
|
+
const expected = { state: handshake?.["state"], nonce: handshake?.["nonce"], codeVerifier: handshake?.["codeVerifier"], exp: handshake?.["exp"] };
|
|
2669
|
+
if (handshake?.["kind"] !== "oidc" || typeof expected.state !== "string" || typeof expected.nonce !== "string" || typeof expected.codeVerifier !== "string" || typeof expected.exp !== "number" || expected.exp <= deps.now()) {
|
|
2670
|
+
return refused("HOST_UNAUTHENTICATED", "no live handshake cookie: the sign-in took more than ten minutes, or did not start here");
|
|
2671
|
+
}
|
|
2672
|
+
if (request.query.state !== expected.state || request.query.code === void 0) return refused("HOST_UNAUTHENTICATED", "state mismatch or no code");
|
|
2673
|
+
try {
|
|
2674
|
+
const idToken = await exchangeAuthorizationCode(entra, entra.config, { code: request.query.code, codeVerifier: expected.codeVerifier });
|
|
2675
|
+
const keys = await fetchSigningKeys(entra, entra.config);
|
|
2676
|
+
const claims = verifyIdToken({ idToken, keys, config: entra.config, nonce: expected.nonce, nowMs: deps.now() });
|
|
2677
|
+
const identity = identityOfClaims(claims, entra.config.groupsClaim);
|
|
2678
|
+
const user = hostUserOfIdentity(identity, findHostUser(deps.config, identity.email), entra.config.roles);
|
|
2679
|
+
deps.log(JSON.stringify({ action: "sign-in", provider: "entra", actor: user.email, role: user.role, outcome: "ok" }));
|
|
2680
|
+
return reply.header("set-cookie", [clearedOidcCookie(), sessionCookie(issueSession(sessions, { ...user, via: "entra" }), deps.config.session.ttlHours)]).redirect("/", 302);
|
|
2681
|
+
} catch (thrown) {
|
|
2682
|
+
if (thrown instanceof OgygieFailure) return refused(thrown.error.code, thrown.error.cause);
|
|
2683
|
+
throw thrown;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2425
2688
|
route.route({
|
|
2426
2689
|
method: "POST",
|
|
2427
2690
|
url: "/api/auth/magic-link",
|
|
2428
|
-
schema: { body: MagicLinkInputSchema, response: { 202:
|
|
2691
|
+
schema: { body: MagicLinkInputSchema, response: { 202: z10.object({ accepted: z10.literal(true) }) } },
|
|
2429
2692
|
handler: async (request, reply) => {
|
|
2430
2693
|
const user = findHostUser(deps.config, request.body.email);
|
|
2431
2694
|
if (user === null) {
|
|
@@ -2455,8 +2718,8 @@ ${link}
|
|
|
2455
2718
|
const email = consumeMagicLink(links, request.query.token);
|
|
2456
2719
|
const user = findHostUser(deps.config, email);
|
|
2457
2720
|
if (user === null) return unauthenticated(`link for ${email}, no longer a declared user`);
|
|
2458
|
-
deps.log(JSON.stringify({ action: "sign-in", actor: user.email, outcome: "ok" }));
|
|
2459
|
-
return reply.header("set-cookie", sessionCookie(issueSession(sessions, user
|
|
2721
|
+
deps.log(JSON.stringify({ action: "sign-in", provider: "magic-link", actor: user.email, outcome: "ok" }));
|
|
2722
|
+
return reply.header("set-cookie", sessionCookie(issueSession(sessions, { ...user, via: "magic-link" }), deps.config.session.ttlHours)).redirect("/", 302);
|
|
2460
2723
|
}
|
|
2461
2724
|
});
|
|
2462
2725
|
route.route({
|
|
@@ -2477,10 +2740,10 @@ ${link}
|
|
|
2477
2740
|
}
|
|
2478
2741
|
|
|
2479
2742
|
// src/server/hosted/brief-routes.ts
|
|
2480
|
-
import { z as
|
|
2743
|
+
import { z as z12 } from "zod";
|
|
2481
2744
|
|
|
2482
2745
|
// src/server/hosted/attachments.ts
|
|
2483
|
-
import { createHash } from "node:crypto";
|
|
2746
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2484
2747
|
import { inflateRawSync } from "node:zlib";
|
|
2485
2748
|
var ATTACHMENT_FORMATS = ["md", "txt", "csv", "json", "html", "xlsx", "pdf"];
|
|
2486
2749
|
var MAX_ATTACHMENTS = 5;
|
|
@@ -2662,7 +2925,7 @@ function prepareBriefAttachments(raw) {
|
|
|
2662
2925
|
if (buffer.length > MAX_ATTACHMENT_BYTES) return tooLarge(piece.name, `${buffer.length} bytes`, MAX_ATTACHMENT_BYTES, "bytes");
|
|
2663
2926
|
totalBytes += buffer.length;
|
|
2664
2927
|
if (totalBytes > MAX_ATTACHMENTS_BYTES) return tooLarge(piece.name, `${totalBytes} bytes in all`, MAX_ATTACHMENTS_BYTES, "bytes");
|
|
2665
|
-
const identity = { name: piece.name, format, bytes: buffer.length, sha256:
|
|
2928
|
+
const identity = { name: piece.name, format, bytes: buffer.length, sha256: createHash2("sha256").update(buffer).digest("hex") };
|
|
2666
2929
|
if (format === "pdf") {
|
|
2667
2930
|
if (!isPdf(buffer)) unsupported(piece.name, "the content does not start like a PDF");
|
|
2668
2931
|
prepared.push({ ...identity, kind: "pdf", data: buffer.toString("base64") });
|
|
@@ -2710,7 +2973,7 @@ ${attachments.map((piece) => `- ${piece.name} (${piece.format}, ${sizeOf(piece.b
|
|
|
2710
2973
|
var attachmentDocumentsOf = (attachments) => attachments.flatMap((piece) => piece.kind === "pdf" ? [{ name: piece.name, data: piece.data }] : []);
|
|
2711
2974
|
|
|
2712
2975
|
// src/server/hosted/brief.ts
|
|
2713
|
-
import { z as
|
|
2976
|
+
import { z as z11 } from "zod";
|
|
2714
2977
|
import { checkSpec, generationGrammarCard, readManifestGeneration } from "@ogygie/generate";
|
|
2715
2978
|
var TEMPLATE_PATH2 = "specs/_TEMPLATE.spec.md";
|
|
2716
2979
|
var PRODUCT_RULES_PATH = "AGENTS.product.md";
|
|
@@ -2735,10 +2998,10 @@ function asksUngenerated(brief) {
|
|
|
2735
2998
|
async function readIfPresent(storage, path) {
|
|
2736
2999
|
return await storage.exists(path) ? storage.readFile(path) : null;
|
|
2737
3000
|
}
|
|
2738
|
-
var ManifestRouteSchema =
|
|
2739
|
-
var ManifestForPromptSchema =
|
|
2740
|
-
product:
|
|
2741
|
-
features:
|
|
3001
|
+
var ManifestRouteSchema = z11.looseObject({ method: z11.string(), path: z11.string(), schemaInput: z11.string().optional(), schemaOutput: z11.string().optional(), invalidates: z11.array(z11.string()).optional() });
|
|
3002
|
+
var ManifestForPromptSchema = z11.looseObject({
|
|
3003
|
+
product: z11.looseObject({ name: z11.string(), description: z11.string().optional() }).optional(),
|
|
3004
|
+
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()
|
|
2742
3005
|
});
|
|
2743
3006
|
async function manifestSummary(storage) {
|
|
2744
3007
|
const raw = await readIfPresent(storage, MANIFEST_PATH2);
|
|
@@ -2805,8 +3068,8 @@ function extractQuestions(text) {
|
|
|
2805
3068
|
} catch {
|
|
2806
3069
|
return { problem: "QUESTIONS: the JSON after the marker does not parse" };
|
|
2807
3070
|
}
|
|
2808
|
-
const shape =
|
|
2809
|
-
questions:
|
|
3071
|
+
const shape = z11.object({
|
|
3072
|
+
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)
|
|
2810
3073
|
});
|
|
2811
3074
|
const checked = shape.safeParse(parsed);
|
|
2812
3075
|
if (!checked.success) return { problem: `QUESTIONS: ${checked.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(" ; ")}` };
|
|
@@ -3265,7 +3528,7 @@ async function registerBriefRoutes(app, deps) {
|
|
|
3265
3528
|
route.route({
|
|
3266
3529
|
method: "GET",
|
|
3267
3530
|
url: "/api/briefs/:jobId",
|
|
3268
|
-
schema: { params:
|
|
3531
|
+
schema: { params: z12.object({ jobId: z12.string().min(1) }), response: { 200: BriefJobSchema } },
|
|
3269
3532
|
handler: async (request, reply) => {
|
|
3270
3533
|
hostedUserOf(request);
|
|
3271
3534
|
return reply.code(200).send(jobs.read(request.params.jobId));
|
|
@@ -3436,6 +3699,12 @@ function createHostedSpecStorage(deps) {
|
|
|
3436
3699
|
async readFile(path) {
|
|
3437
3700
|
return (await readerOf(path)).readFile(path);
|
|
3438
3701
|
},
|
|
3702
|
+
async modifiedAt(path) {
|
|
3703
|
+
if (deps.lastCommitDateOf === void 0) return null;
|
|
3704
|
+
const specId = specIdOfPath2(path);
|
|
3705
|
+
const ref = specId !== null && (await branches()).has(specBranchOf(specId)) ? specBranchOf(specId) : deps.github.defaultBranch;
|
|
3706
|
+
return deps.lastCommitDateOf(ref, path);
|
|
3707
|
+
},
|
|
3439
3708
|
async exists(path) {
|
|
3440
3709
|
return (await readerOf(path)).exists(path);
|
|
3441
3710
|
},
|
|
@@ -3885,17 +4154,17 @@ async function registerPromotionRoutes(app, deps) {
|
|
|
3885
4154
|
}
|
|
3886
4155
|
|
|
3887
4156
|
// src/server/hosted/usage.ts
|
|
3888
|
-
import { z as
|
|
3889
|
-
var UsageEntrySchema =
|
|
3890
|
-
at:
|
|
4157
|
+
import { z as z13 } from "zod";
|
|
4158
|
+
var UsageEntrySchema = z13.object({
|
|
4159
|
+
at: z13.string(),
|
|
3891
4160
|
/** brief → spec from the instance; `speculate` entries arrive with the generator's lot. */
|
|
3892
|
-
action:
|
|
3893
|
-
attempt:
|
|
3894
|
-
model:
|
|
3895
|
-
inputTokens:
|
|
3896
|
-
outputTokens:
|
|
3897
|
-
actor:
|
|
3898
|
-
specId:
|
|
4161
|
+
action: z13.enum(["brief", "speculate"]),
|
|
4162
|
+
attempt: z13.number().int().positive().optional(),
|
|
4163
|
+
model: z13.string(),
|
|
4164
|
+
inputTokens: z13.number().int().nonnegative(),
|
|
4165
|
+
outputTokens: z13.number().int().nonnegative(),
|
|
4166
|
+
actor: z13.string().optional(),
|
|
4167
|
+
specId: z13.string().optional()
|
|
3899
4168
|
});
|
|
3900
4169
|
var USAGE_MARKER = "<!-- ogygie-usage -->";
|
|
3901
4170
|
var DEFAULT_MODEL_COSTS = {
|
|
@@ -3930,7 +4199,7 @@ function parseUsageComment(body) {
|
|
|
3930
4199
|
const block = /```json\n([\s\S]*?)\n```/.exec(body)?.[1];
|
|
3931
4200
|
if (block === void 0) return [];
|
|
3932
4201
|
try {
|
|
3933
|
-
const parsed =
|
|
4202
|
+
const parsed = z13.array(UsageEntrySchema).safeParse(JSON.parse(block));
|
|
3934
4203
|
return parsed.success ? parsed.data : [];
|
|
3935
4204
|
} catch {
|
|
3936
4205
|
return [];
|
|
@@ -4005,9 +4274,9 @@ async function registerConsumptionRoutes(app, deps) {
|
|
|
4005
4274
|
}
|
|
4006
4275
|
|
|
4007
4276
|
// src/server/hosted/preview-routes.ts
|
|
4008
|
-
import { z as
|
|
4009
|
-
var PreviewParamsSchema = SpecParamsSchema.extend({ state:
|
|
4010
|
-
var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition:
|
|
4277
|
+
import { z as z14 } from "zod";
|
|
4278
|
+
var PreviewParamsSchema = SpecParamsSchema.extend({ state: z14.string().min(1).max(80) });
|
|
4279
|
+
var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition: z14.string().min(1).max(80) });
|
|
4011
4280
|
var PREVIEW_CSP = "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:";
|
|
4012
4281
|
async function registerPreviewRoutes(app, deps) {
|
|
4013
4282
|
const route = app.withTypeProvider();
|
|
@@ -4049,7 +4318,7 @@ async function registerPreviewRoutes(app, deps) {
|
|
|
4049
4318
|
}
|
|
4050
4319
|
|
|
4051
4320
|
// src/server/hosted/composition-routes.ts
|
|
4052
|
-
import { z as
|
|
4321
|
+
import { z as z15 } from "zod";
|
|
4053
4322
|
|
|
4054
4323
|
// src/server/hosted/compositions.ts
|
|
4055
4324
|
var COMPOSITION_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
@@ -4093,7 +4362,7 @@ La composition \xAB ${composition} \xBB est retenue parmi celles propos\xE9es ;
|
|
|
4093
4362
|
}
|
|
4094
4363
|
|
|
4095
4364
|
// src/server/hosted/composition-routes.ts
|
|
4096
|
-
var CompositionParamsSchema = SpecParamsSchema.extend({ composition:
|
|
4365
|
+
var CompositionParamsSchema = SpecParamsSchema.extend({ composition: z15.string().min(1).max(80) });
|
|
4097
4366
|
async function registerCompositionRoutes(app, deps) {
|
|
4098
4367
|
const route = app.withTypeProvider();
|
|
4099
4368
|
route.route({
|
|
@@ -4478,6 +4747,15 @@ function createGithubPullsClient(deps) {
|
|
|
4478
4747
|
);
|
|
4479
4748
|
return (json ?? []).map((entry) => ({ sha: entry.sha, message: entry.commit.message }));
|
|
4480
4749
|
},
|
|
4750
|
+
async lastCommitDateOf(branch, path) {
|
|
4751
|
+
const { json } = await request(
|
|
4752
|
+
"GET",
|
|
4753
|
+
`${repoUrl}/commits?sha=${encodeURIComponent(branch)}&path=${encodeURIComponent(path)}&per_page=1`,
|
|
4754
|
+
void 0
|
|
4755
|
+
);
|
|
4756
|
+
const last = json?.[0];
|
|
4757
|
+
return last?.commit.committer?.date ?? last?.commit.author?.date ?? null;
|
|
4758
|
+
},
|
|
4481
4759
|
async getPullRequest(number) {
|
|
4482
4760
|
const { json } = await request(
|
|
4483
4761
|
"GET",
|
|
@@ -4826,6 +5104,7 @@ async function startHostedStudio(options) {
|
|
|
4826
5104
|
const privateKeyPem = readSecret(config.githubApp.privateKeyPath, "the GitHub App private key");
|
|
4827
5105
|
const sessionSecret = readSecret(config.session.secretPath, "the session secret", MIN_SESSION_SECRET_LENGTH);
|
|
4828
5106
|
const mailApiKey = readSecret(config.mail.apiKeyPath, "the mail provider key");
|
|
5107
|
+
const entraClientSecret = config.identity === void 0 ? void 0 : readSecret(config.identity.clientSecretPath, "the Entra client secret");
|
|
4829
5108
|
const modelApiKey = config.model === void 0 ? void 0 : process.env["OGYGIE_MODEL_API_KEY"] ?? "";
|
|
4830
5109
|
if (modelApiKey !== void 0 && modelApiKey.length === 0) {
|
|
4831
5110
|
throwOgygieError({
|
|
@@ -4878,6 +5157,7 @@ async function startHostedStudio(options) {
|
|
|
4878
5157
|
}
|
|
4879
5158
|
}
|
|
4880
5159
|
}
|
|
5160
|
+
const pulls = createGithubPullsClient({ github: config.repo, fetchImpl: githubApp.installationFetch });
|
|
4881
5161
|
const app = await createStudioApp({
|
|
4882
5162
|
uiDistDir,
|
|
4883
5163
|
hosted: {
|
|
@@ -4892,7 +5172,20 @@ async function startHostedStudio(options) {
|
|
|
4892
5172
|
now,
|
|
4893
5173
|
log,
|
|
4894
5174
|
consumedLinks: /* @__PURE__ */ new Map(),
|
|
4895
|
-
|
|
5175
|
+
...config.identity === void 0 || entraClientSecret === void 0 ? {} : {
|
|
5176
|
+
entra: {
|
|
5177
|
+
fetchImpl: fetch,
|
|
5178
|
+
config: {
|
|
5179
|
+
tenantId: config.identity.tenantId,
|
|
5180
|
+
clientId: config.identity.clientId,
|
|
5181
|
+
clientSecret: entraClientSecret,
|
|
5182
|
+
callbackUrl: `${config.publicUrl.replace(/\/$/, "")}/api/auth/entra/callback`,
|
|
5183
|
+
groupsClaim: config.identity.groupsClaim,
|
|
5184
|
+
roles: config.identity.roles
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
},
|
|
5188
|
+
pulls,
|
|
4896
5189
|
readerFor,
|
|
4897
5190
|
usage: createGithubUsageClient({ github: config.repo, fetchImpl: githubApp.installationFetch }),
|
|
4898
5191
|
...deploy === void 0 ? {} : { deploy },
|
|
@@ -4902,6 +5195,7 @@ async function startHostedStudio(options) {
|
|
|
4902
5195
|
github: config.repo,
|
|
4903
5196
|
readerFor,
|
|
4904
5197
|
listSpecBranches,
|
|
5198
|
+
lastCommitDateOf: pulls.lastCommitDateOf,
|
|
4905
5199
|
client,
|
|
4906
5200
|
user,
|
|
4907
5201
|
now,
|