源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)

This commit is contained in:
agent-radar
2026-09-02 01:05:41 +08:00
commit 04bac1f6e2
2200 changed files with 489415 additions and 0 deletions
@@ -0,0 +1,57 @@
import { callOpenWorkCloudUploadAction } from "../../../../apps/server/src/extensions/cloud-uploads.ts";
import { listExperimentalExtensionActions } from "../../../../apps/server/src/extensions/index.ts";
const input = JSON.parse(await Bun.stdin.text());
if (input.mode === "inspect") {
process.stdout.write(JSON.stringify({ ok: true, result: listExperimentalExtensionActions("") }));
} else {
let networkCalls = 0;
const config = {
host: "127.0.0.1",
port: 8787,
token: "test-client-token",
hostToken: "test-host-token",
configPath: `${input.root}/server.json`,
approval: { mode: "auto", timeoutMs: 30_000 },
corsOrigins: ["*"],
workspaces: [],
authorizedRoots: [input.root],
readOnly: false,
startedAt: Date.now(),
tokenSource: "generated",
hostTokenSource: "generated",
logFormat: "pretty",
logRequests: false,
};
try {
const result = await callOpenWorkCloudUploadAction(
config,
input.action,
input.args,
input.context,
{
readCloudMcp: async () => ({
type: "remote",
enabled: true,
url: input.cloudUrl,
headers: { Authorization: `Bearer ${input.mcpToken}` },
oauth: false,
}),
fetchImpl: async (url, init) => {
networkCalls += 1;
return fetch(url, init);
},
},
);
process.stdout.write(JSON.stringify({ ok: true, result, networkCalls }));
} catch (error) {
process.stdout.write(JSON.stringify({
ok: false,
status: error && typeof error === "object" && "status" in error ? error.status : null,
code: error && typeof error === "object" && "code" in error ? error.code : null,
message: error instanceof Error ? error.message : String(error),
networkCalls,
}));
}
}
+33
View File
@@ -0,0 +1,33 @@
{
"name": "@openwork/labs",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./src/index.ts"
},
"./egress": {
"types": "./src/egress.ts",
"development": "./src/egress.ts",
"default": "./src/egress.ts"
},
"./idp": {
"types": "./src/idp.ts",
"development": "./src/idp.ts",
"default": "./src/idp.ts"
},
"./mock-mcp": {
"types": "./src/mock-mcp.ts",
"development": "./src/mock-mcp.ts",
"default": "./src/mock-mcp.ts"
},
"./release-feed": {
"types": "./src/release-feed.ts",
"development": "./src/release-feed.ts",
"default": "./src/release-feed.ts"
}
}
}
File diff suppressed because it is too large Load Diff
+901
View File
@@ -0,0 +1,901 @@
import { createPublicKey, createSign, generateKeyPairSync, randomBytes, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
import type { IncomingMessage, Server, ServerResponse } from "node:http";
import type { AddressInfo } from "node:net";
import { trimTrailingSlashes } from "./strings.ts";
const DEFAULT_DOMAIN = "acme.test";
const DEFAULT_CLIENT_ID = "openwork-eval-oidc-client";
const DEFAULT_CLIENT_SECRET = "openwork-eval-oidc-secret";
const DEFAULT_ISSUER = "http://127.0.0.1/mock-openwork-idp";
const DEFAULT_EVAL_ISSUER = "http://127.0.0.1:19190";
const DEFAULT_GROUPS = ["Engineering", "OpenWork Lab"];
const DEFAULT_ROLE = "member";
export const MOCK_IDP_BLOCKED_USER_PHRASE = "administrator has configured the application to block users";
export type MockGroupClaims =
| "present"
| "absent"
| "unexpected-shape"
| readonly string[]
| {
claim: string;
values: readonly string[];
};
export interface MockIdpKnobs {
certTrailingNewline?: boolean;
wrongDomain?: boolean;
blockedUser?: string;
groupClaims?: MockGroupClaims;
emailMismatch?: boolean | string;
guestUser?: boolean;
}
export interface MockIdpSubjectInput {
sub?: string;
email?: string;
name?: string;
}
export interface MockIdpSubject {
sub: string;
email: string;
name: string;
}
export interface MockIdpConfig {
issuer?: string;
domain?: string;
clientId?: string;
clientSecret?: string;
defaultSubject?: MockIdpSubjectInput;
knobs?: MockIdpKnobs;
}
export interface NormalizedMockIdpKnobs {
certTrailingNewline: boolean;
wrongDomain: boolean;
blockedUser: string | null;
groupClaims: MockGroupClaims;
emailMismatch: boolean | string;
guestUser: boolean;
}
export interface NormalizedMockIdpConfig {
issuer: string;
domain: string;
clientId: string;
clientSecret: string;
defaultSubject: MockIdpSubject;
knobs: NormalizedMockIdpKnobs;
}
export interface MockOidcRegistration {
kind: "oidc";
issuer: string;
domain: string;
clientId: string;
clientSecret: string;
scopes: string[];
skipDiscovery: boolean;
authorizationEndpoint: string;
tokenEndpoint: string;
jwksEndpoint: string;
userInfoEndpoint: string;
tokenEndpointAuthentication: "client_secret_post";
}
export interface MockSamlEndpoints {
metadataUrl: string;
ssoPostUrl: string;
certificate: string;
}
export interface StartedMockIdpLab {
protocol: "oidc";
config: NormalizedMockIdpConfig;
issuer: string;
authorizationEndpoint: string;
tokenEndpoint: string;
jwksEndpoint: string;
userInfoEndpoint: string;
saml: MockSamlEndpoints;
keyId: string;
certificate: string;
publicKeyPem: string;
registration(overrides?: Partial<MockOidcRegistration>): MockOidcRegistration;
claimsFor(subject?: MockIdpSubjectInput): Record<string, unknown>;
stop(): Promise<void>;
[Symbol.asyncDispose](): Promise<void>;
}
interface KeyMaterial {
keyId: string;
privateKeyPem: string;
publicKeyPem: string;
jwk: Record<string, unknown>;
}
interface AuthorizationCodeRecord {
clientId: string;
nonce: string | null;
redirectUri: string;
subject: MockIdpSubject;
}
interface MockIdpState {
config: NormalizedMockIdpConfig;
keys: KeyMaterial;
codes: Map<string, AuthorizationCodeRecord>;
accessTokens: Map<string, Record<string, unknown>>;
}
export type SsoConfigErrorCode = "sso_cert_trailing_newline" | "sso_domain_mismatch";
export interface NormalizedCertificateMaterial {
value: string;
hadTrailingNewline: boolean;
}
export interface SsoConfigurationValidationInput {
configuredDomain: string;
cert?: string | null;
subjectEmail?: string | null;
}
export interface SsoConfigurationError {
code: SsoConfigErrorCode;
message: string;
detail: string;
}
export interface SsoConfigurationValidation {
ok: boolean;
errors: SsoConfigurationError[];
normalizedDomain: string;
normalizedCert?: string;
}
export interface SsoExpectation {
code: SsoConfigErrorCode;
includes?: readonly string[];
}
export interface SsoExpectationMatch {
passed: boolean;
expectedCode: SsoConfigErrorCode;
actualCodes: string[];
detail: string;
}
export interface Rs256SigningArgvInput {
keyId: string;
payload: string;
}
export interface BlockedUserResponseShape {
status: 403;
error: "access_denied";
errorDescription: string;
message: string;
html: string;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isAddressInfo(value: ReturnType<Server["address"]>): value is AddressInfo {
return typeof value === "object"
&& value !== null
&& typeof value.address === "string"
&& typeof value.family === "string"
&& typeof value.port === "number";
}
function isStringArray(value: unknown): value is readonly string[] {
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
}
function isNamedGroupClaim(value: MockGroupClaims): value is { claim: string; values: readonly string[] } {
return isRecord(value) && typeof value.claim === "string" && isStringArray(value.values);
}
function cleanIssuer(value: string | undefined): string {
const trimmed = trimTrailingSlashes((value ?? DEFAULT_ISSUER).trim());
return trimmed || DEFAULT_ISSUER;
}
function requestedIssuer(value: string | undefined): URL | null {
const trimmed = value?.trim();
if (!trimmed) return null;
try {
const url = new URL(cleanIssuer(trimmed));
return url.protocol === "http:" ? url : null;
} catch {
return null;
}
}
function configuredMockIdpIssuer(input: MockIdpConfig): URL | null {
const evalIssuer = process.env.OPENWORK_EVAL_DEN_API_URL?.trim() ? DEFAULT_EVAL_ISSUER : undefined;
return requestedIssuer(input.issuer) ?? requestedIssuer(process.env.OPENWORK_EVAL_MOCK_IDP_ISSUER) ?? requestedIssuer(evalIssuer);
}
export function normalizeDomain(value: string | undefined): string {
const input = (value ?? DEFAULT_DOMAIN).trim().toLowerCase();
let start = 0;
let end = input.length;
while (start < end && input[start] === "@") start += 1;
while (end > start && input[end - 1] === ".") end -= 1;
const normalized = input.slice(start, end);
return normalized || DEFAULT_DOMAIN;
}
export function emailDomain(email: string | null | undefined): string | null {
const value = email?.trim().toLowerCase() ?? "";
const at = value.lastIndexOf("@");
if (at < 0 || at === value.length - 1) {
return null;
}
return normalizeDomain(value.slice(at + 1));
}
function localPart(email: string): string {
const at = email.indexOf("@");
return at > 0 ? email.slice(0, at) : "sso.user";
}
export function buildWrongDomainEmail(email: string, configuredDomain: string): string {
return `${localPart(email)}@wrong-${normalizeDomain(configuredDomain)}`;
}
function defaultSubject(domain: string): MockIdpSubject {
const email = `sso.user@${domain}`;
return {
sub: email,
email,
name: "OpenWork SSO User",
};
}
function normalizeSubject(input: MockIdpSubjectInput | undefined, domain: string): MockIdpSubject {
const fallback = defaultSubject(domain);
const email = input?.email?.trim().toLowerCase() || fallback.email;
return {
sub: input?.sub?.trim() || email,
email,
name: input?.name?.trim() || fallback.name,
};
}
function normalizeKnobs(knobs: MockIdpKnobs | undefined): NormalizedMockIdpKnobs {
return {
certTrailingNewline: knobs?.certTrailingNewline === true,
wrongDomain: knobs?.wrongDomain === true,
blockedUser: knobs?.blockedUser?.trim() || null,
groupClaims: knobs?.groupClaims ?? "present",
emailMismatch: knobs?.emailMismatch ?? false,
guestUser: knobs?.guestUser === true,
};
}
export function normalizeMockIdpConfig(input: MockIdpConfig = {}): NormalizedMockIdpConfig {
const domain = normalizeDomain(input.domain);
return {
issuer: cleanIssuer(input.issuer),
domain,
clientId: input.clientId?.trim() || DEFAULT_CLIENT_ID,
clientSecret: input.clientSecret?.trim() || DEFAULT_CLIENT_SECRET,
defaultSubject: normalizeSubject(input.defaultSubject, domain),
knobs: normalizeKnobs(input.knobs),
};
}
export function normalizeCertificateMaterial(value: string | null | undefined): NormalizedCertificateMaterial {
const raw = value ?? "";
let end = raw.length;
while (end > 0 && raw[end - 1] === "\n") {
end -= 1;
if (end > 0 && raw[end - 1] === "\r") end -= 1;
}
return {
value: raw.slice(0, end),
hadTrailingNewline: end !== raw.length,
};
}
export function validateSsoConfiguration(input: SsoConfigurationValidationInput): SsoConfigurationValidation {
const normalizedDomain = normalizeDomain(input.configuredDomain);
const errors: SsoConfigurationError[] = [];
const cert = normalizeCertificateMaterial(input.cert);
if (input.cert !== undefined && input.cert !== null && cert.hadTrailingNewline) {
errors.push({
code: "sso_cert_trailing_newline",
message: "SSO certificate has trailing newline characters. Remove the blank line at the end of the pasted certificate and save again.",
detail: "The field POC failure was a certificate paste with a trailing newline; this matcher names it instead of surfacing a generic SSO failure.",
});
}
const subjectDomain = emailDomain(input.subjectEmail);
if (subjectDomain && subjectDomain !== normalizedDomain) {
errors.push({
code: "sso_domain_mismatch",
message: `SSO domain mismatch: IdP email domain ${subjectDomain} does not match configured organization domain ${normalizedDomain}.`,
detail: "The IdP subject email domain must match the organization SSO domain before users are sent through the provider.",
});
}
return {
ok: errors.length === 0,
errors,
normalizedDomain,
normalizedCert: input.cert === undefined || input.cert === null ? undefined : cert.value,
};
}
export function matchSsoExpectation(validation: SsoConfigurationValidation, expectation: SsoExpectation): SsoExpectationMatch {
const match = validation.errors.find((error) => error.code === expectation.code) ?? null;
const includes = expectation.includes ?? [];
const haystack = match ? `${match.message}\n${match.detail}` : "";
const missing = includes.filter((needle) => !haystack.toLowerCase().includes(needle.toLowerCase()));
const passed = Boolean(match) && missing.length === 0;
return {
passed,
expectedCode: expectation.code,
actualCodes: validation.errors.map((error) => error.code),
detail: passed
? match?.message ?? expectation.code
: `Expected ${expectation.code}${missing.length ? ` with text ${missing.join(", ")}` : ""}; got ${validation.errors.map((error) => error.code).join(", ") || "no errors"}.`,
};
}
export function buildRs256SigningArgv(input: Rs256SigningArgvInput): readonly string[] {
return [
"node:crypto",
"createSign",
"RSA-SHA256",
"--kid",
input.keyId,
"--payload-bytes",
String(Buffer.byteLength(input.payload, "utf8")),
];
}
export function subjectWithKnobs(config: NormalizedMockIdpConfig, input?: MockIdpSubjectInput): MockIdpSubject {
const requested = normalizeSubject(input, config.domain);
let email = requested.email;
if (config.knobs.emailMismatch) {
email = typeof config.knobs.emailMismatch === "string" && config.knobs.emailMismatch.trim()
? config.knobs.emailMismatch.trim().toLowerCase()
: `mismatch.${localPart(requested.email)}@${config.domain}`;
}
if (config.knobs.wrongDomain) {
email = buildWrongDomainEmail(email, config.domain);
}
if (config.knobs.guestUser) {
email = input?.email?.trim().toLowerCase() || `guest.user@external.${config.domain}`;
}
return {
sub: input?.sub?.trim() || (config.knobs.guestUser ? `guest:${email}` : email),
email,
name: input?.name?.trim() || requested.name,
};
}
function secondsSinceEpoch(date: Date): number {
return Math.floor(date.getTime() / 1000);
}
function applyGroupClaims(claims: Record<string, unknown>, groupClaims: MockGroupClaims): void {
if (groupClaims === "absent") {
return;
}
if (groupClaims === "unexpected-shape") {
claims.groups = { primary: DEFAULT_GROUPS[0], all: DEFAULT_GROUPS };
claims.roles = { default: DEFAULT_ROLE };
return;
}
if (isStringArray(groupClaims)) {
claims.groups = [...groupClaims];
claims.roles = [DEFAULT_ROLE];
return;
}
if (isNamedGroupClaim(groupClaims)) {
claims[groupClaims.claim] = [...groupClaims.values];
claims.roles = [DEFAULT_ROLE];
return;
}
claims.groups = [...DEFAULT_GROUPS];
claims.roles = [DEFAULT_ROLE];
}
export function buildOidcClaims(input: {
issuer: string;
clientId: string;
subject: MockIdpSubject;
nonce?: string | null;
now?: Date;
expiresInSeconds?: number;
knobs?: NormalizedMockIdpKnobs;
}): Record<string, unknown> {
const now = input.now ?? new Date();
const issuedAt = secondsSinceEpoch(now);
const knobs = input.knobs ?? normalizeKnobs(undefined);
const claims: Record<string, unknown> = {
iss: cleanIssuer(input.issuer),
sub: input.subject.sub,
aud: input.clientId,
iat: issuedAt,
exp: issuedAt + (input.expiresInSeconds ?? 300),
email: input.subject.email,
email_verified: true,
name: input.subject.name,
preferred_username: input.subject.email,
picture: `https://avatar.openwork.test/${encodeURIComponent(input.subject.email)}`,
department: "Enterprise Lab",
};
if (input.nonce) {
claims.nonce = input.nonce;
}
if (knobs.guestUser) {
claims.userType = "Guest";
claims.tenant_hint = "external";
}
applyGroupClaims(claims, knobs.groupClaims);
return claims;
}
function base64UrlJson(value: unknown): string {
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
}
export function signOidcJwt(input: {
keyId: string;
privateKeyPem: string;
claims: Record<string, unknown>;
}): string {
const header = base64UrlJson({ alg: "RS256", typ: "JWT", kid: input.keyId });
const payload = base64UrlJson(input.claims);
const signingInput = `${header}.${payload}`;
const signer = createSign("RSA-SHA256");
signer.update(signingInput);
signer.end();
const signature = signer.sign(input.privateKeyPem).toString("base64url");
return `${signingInput}.${signature}`;
}
function publicJwk(publicKeyPem: string, keyId: string): Record<string, unknown> {
const exported = createPublicKey(publicKeyPem).export({ format: "jwk" });
const jwk: Record<string, unknown> = {};
for (const [name, value] of Object.entries(exported)) {
jwk[name] = value;
}
jwk.kid = keyId;
jwk.use = "sig";
jwk.alg = "RS256";
return jwk;
}
function createKeyMaterial(): KeyMaterial {
const keyPair = generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs8", format: "pem" },
});
const keyId = `mock-idp-${randomBytes(6).toString("hex")}`;
return {
keyId,
privateKeyPem: keyPair.privateKey,
publicKeyPem: keyPair.publicKey,
jwk: publicJwk(keyPair.publicKey, keyId),
};
}
function certificateFromPublicKey(publicKeyPem: string, trailingNewline: boolean): string {
const body = publicKeyPem
.split(/\r?\n/g)
.filter((line) => line && !line.startsWith("-----"))
.join("\n");
const cert = `-----BEGIN CERTIFICATE-----\n${body}\n-----END CERTIFICATE-----`;
return trailingNewline ? `${cert}\n` : cert;
}
function safeEqual(left: string, right: string): boolean {
const leftBuffer = Buffer.from(left);
const rightBuffer = Buffer.from(right);
if (leftBuffer.length !== rightBuffer.length) {
return false;
}
return timingSafeEqual(leftBuffer, rightBuffer);
}
const HTML_ENTITIES: Record<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};
// A single global character-class replace: linear (no backtracking) and the
// shape static analysis recognises as HTML escaping, unlike a chain of
// string-pattern replaceAll calls.
function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, (character) => HTML_ENTITIES[character] ?? character);
}
export function buildBlockedUserResponse(subject: MockIdpSubjectInput, policyName = "OpenWork Mock IdP policy"): BlockedUserResponseShape {
const label = subject.email?.trim() || subject.sub?.trim() || "this user";
const errorDescription = `The ${MOCK_IDP_BLOCKED_USER_PHRASE} unless they are assigned to the enterprise application.`;
const message = `${policyName} blocked ${label}. ${errorDescription} Ask an IdP administrator to assign the user or update the app assignment policy.`;
return {
status: 403,
error: "access_denied",
errorDescription,
message,
html: `<!doctype html><html><head><title>OpenWork Mock IdP policy block</title></head><body><main style="font-family: sans-serif; max-width: 720px; margin: 48px auto;"><p>OpenWork Mock IdP policy</p><h1>Sign-in blocked by identity provider policy</h1><p>${escapeHtml(message)}</p><p>error=access_denied</p></main></body></html>`,
};
}
function matchesBlockedUser(subject: MockIdpSubject, blockedUser: string | null): boolean {
if (!blockedUser) {
return false;
}
const blocked = blockedUser.trim().toLowerCase();
return subject.email.toLowerCase() === blocked || subject.sub.toLowerCase() === blocked;
}
function sendJson(response: ServerResponse, status: number, body: unknown): void {
const payload = JSON.stringify(body);
response.writeHead(status, {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
});
response.end(payload);
}
interface HtmlDocument {
title: string;
heading: string;
paragraphs: string[];
}
function sendHtml(response: ServerResponse, status: number, document: HtmlDocument): void {
// Escape at the write boundary so request-derived fields cannot reach the
// HTML response as markup.
const title = escapeHtml(document.title);
const heading = escapeHtml(document.heading);
const paragraphs = document.paragraphs.map((text) => `<p>${escapeHtml(text)}</p>`).join("");
const body = `<!doctype html><html><head><title>${title}</title></head><body><main style="font-family: sans-serif; max-width: 720px; margin: 48px auto;"><h1>${heading}</h1>${paragraphs}</main></body></html>`;
response.writeHead(status, {
"content-type": "text/html; charset=utf-8",
"cache-control": "no-store",
});
response.end(body);
}
function sendRedirect(response: ServerResponse, location: string): void {
response.writeHead(302, {
location,
"cache-control": "no-store",
});
response.end();
}
function method(request: IncomingMessage): string {
return (request.method ?? "GET").toUpperCase();
}
function requestUrl(request: IncomingMessage, issuer: string): URL {
return new URL(request.url ?? "/", issuer);
}
function headerValue(request: IncomingMessage, name: string): string {
const raw = request.headers[name.toLowerCase()];
if (Array.isArray(raw)) {
return raw[0] ?? "";
}
return typeof raw === "string" ? raw : "";
}
async function requestBody(request: IncomingMessage): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of request) {
if (typeof chunk === "string") {
chunks.push(Buffer.from(chunk));
} else if (Buffer.isBuffer(chunk)) {
chunks.push(chunk);
} else {
chunks.push(Buffer.from(String(chunk)));
}
}
return Buffer.concat(chunks).toString("utf8");
}
async function formBody(request: IncomingMessage): Promise<URLSearchParams> {
return new URLSearchParams(await requestBody(request));
}
function basicClientCredentials(authorization: string): { clientId: string; clientSecret: string } | null {
if (!authorization.startsWith("Basic ")) {
return null;
}
const decoded = Buffer.from(authorization.slice("Basic ".length), "base64").toString("utf8");
const separator = decoded.indexOf(":");
if (separator < 0) {
return null;
}
return {
clientId: decodeURIComponent(decoded.slice(0, separator)),
clientSecret: decodeURIComponent(decoded.slice(separator + 1)),
};
}
function clientAuthenticated(params: URLSearchParams, authorization: string, config: NormalizedMockIdpConfig): boolean {
const basic = basicClientCredentials(authorization);
const clientId = basic?.clientId ?? params.get("client_id") ?? "";
const clientSecret = basic?.clientSecret ?? params.get("client_secret") ?? "";
return safeEqual(clientId, config.clientId) && safeEqual(clientSecret, config.clientSecret);
}
function oidcRegistration(config: NormalizedMockIdpConfig, overrides: Partial<MockOidcRegistration> = {}): MockOidcRegistration {
const issuer = overrides.issuer ?? config.issuer;
return {
kind: "oidc",
issuer,
domain: overrides.domain ?? config.domain,
clientId: overrides.clientId ?? config.clientId,
clientSecret: overrides.clientSecret ?? config.clientSecret,
scopes: overrides.scopes ?? ["openid", "email", "profile"],
skipDiscovery: overrides.skipDiscovery ?? false,
authorizationEndpoint: overrides.authorizationEndpoint ?? `${issuer}/authorize`,
tokenEndpoint: overrides.tokenEndpoint ?? `${issuer}/token`,
jwksEndpoint: overrides.jwksEndpoint ?? `${issuer}/jwks`,
userInfoEndpoint: overrides.userInfoEndpoint ?? `${issuer}/userinfo`,
tokenEndpointAuthentication: "client_secret_post",
};
}
function discovery(config: NormalizedMockIdpConfig): Record<string, unknown> {
return {
issuer: config.issuer,
authorization_endpoint: `${config.issuer}/authorize`,
token_endpoint: `${config.issuer}/token`,
jwks_uri: `${config.issuer}/jwks`,
userinfo_endpoint: `${config.issuer}/userinfo`,
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
subject_types_supported: ["public"],
id_token_signing_alg_values_supported: ["RS256"],
token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
scopes_supported: ["openid", "email", "profile"],
claims_supported: ["sub", "email", "email_verified", "name", "preferred_username", "groups", "roles", "department"],
};
}
function samlMetadata(config: NormalizedMockIdpConfig, certificate: string): string {
const certBody = certificate
.replace(/-----BEGIN CERTIFICATE-----/g, "")
.replace(/-----END CERTIFICATE-----/g, "")
.replace(/\s+/g, "");
const entityId = escapeHtml(`${config.issuer}/saml/metadata`);
const ssoUrl = escapeHtml(`${config.issuer}/saml/sso`);
return `<?xml version="1.0" encoding="UTF-8"?>
<EntityDescriptor entityID="${entityId}" xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing"><KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><X509Data><X509Certificate>${certBody}</X509Certificate></X509Data></KeyInfo></KeyDescriptor>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${ssoUrl}"/>
</IDPSSODescriptor>
</EntityDescriptor>`;
}
function samlPostBindingPage(config: NormalizedMockIdpConfig): HtmlDocument {
return {
title: "OpenWork Mock IdP SAML endpoint",
heading: "SAML POST binding endpoint",
paragraphs: [
"OpenWork Mock IdP lab",
"This fixture exposes SAML metadata and a POST binding endpoint, but the OpenWork eval lab drives OIDC because this checkout implements OIDC and SAML SSO and OIDC keeps the signed-token path dependency-free.",
`Issuer: ${config.issuer}`,
],
};
}
function authorize(state: MockIdpState, url: URL, response: ServerResponse): void {
const redirectUri = url.searchParams.get("redirect_uri") ?? "";
const clientId = url.searchParams.get("client_id") ?? "";
if (!redirectUri || !safeEqual(clientId, state.config.clientId)) {
sendJson(response, 400, { error: "invalid_request", error_description: "redirect_uri and a registered client_id are required." });
return;
}
const loginHint = url.searchParams.get("login_hint") ?? undefined;
const requestedSubject = loginHint ? { email: loginHint } : undefined;
const subject = subjectWithKnobs(state.config, requestedSubject);
if (matchesBlockedUser(subject, state.config.knobs.blockedUser)) {
const blocked = buildBlockedUserResponse(subject);
sendHtml(response, blocked.status, {
title: "OpenWork Mock IdP policy block",
heading: "Sign-in blocked by identity provider policy",
paragraphs: ["OpenWork Mock IdP policy", blocked.message, "error=access_denied"],
});
return;
}
const code = randomBytes(18).toString("base64url");
state.codes.set(code, {
clientId,
nonce: url.searchParams.get("nonce"),
redirectUri,
subject,
});
const callback = new URL(redirectUri);
callback.searchParams.set("code", code);
const relayState = url.searchParams.get("state");
if (relayState) {
callback.searchParams.set("state", relayState);
}
sendRedirect(response, callback.toString());
}
async function token(state: MockIdpState, request: IncomingMessage, response: ServerResponse): Promise<void> {
const params = await formBody(request);
if (!clientAuthenticated(params, headerValue(request, "authorization"), state.config)) {
sendJson(response, 401, { error: "invalid_client", error_description: "The mock IdP did not recognize the OIDC client credentials." });
return;
}
if (params.get("grant_type") !== "authorization_code") {
sendJson(response, 400, { error: "unsupported_grant_type" });
return;
}
const code = params.get("code") ?? "";
const record = state.codes.get(code) ?? null;
if (!record) {
sendJson(response, 400, { error: "invalid_grant", error_description: "The authorization code was not issued by this mock IdP." });
return;
}
state.codes.delete(code);
const claims = buildOidcClaims({
issuer: state.config.issuer,
clientId: record.clientId,
subject: record.subject,
nonce: record.nonce,
knobs: state.config.knobs,
});
const accessToken = randomBytes(24).toString("base64url");
state.accessTokens.set(accessToken, claims);
sendJson(response, 200, {
access_token: accessToken,
token_type: "Bearer",
expires_in: 300,
id_token: signOidcJwt({ keyId: state.keys.keyId, privateKeyPem: state.keys.privateKeyPem, claims }),
});
}
function userinfo(state: MockIdpState, request: IncomingMessage, response: ServerResponse): void {
const authorization = headerValue(request, "authorization");
const tokenValue = authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length).trim() : "";
const claims = state.accessTokens.get(tokenValue) ?? null;
if (!claims) {
sendJson(response, 401, { error: "invalid_token" });
return;
}
sendJson(response, 200, claims);
}
async function handleRequest(state: MockIdpState, certificate: string, request: IncomingMessage, response: ServerResponse): Promise<void> {
const url = requestUrl(request, state.config.issuer);
if (method(request) === "GET" && url.pathname === "/.well-known/openid-configuration") {
sendJson(response, 200, discovery(state.config));
return;
}
if (method(request) === "GET" && url.pathname === "/jwks") {
sendJson(response, 200, { keys: [state.keys.jwk] });
return;
}
if (method(request) === "GET" && url.pathname === "/authorize") {
authorize(state, url, response);
return;
}
if (method(request) === "POST" && url.pathname === "/token") {
await token(state, request, response);
return;
}
if (method(request) === "GET" && url.pathname === "/userinfo") {
userinfo(state, request, response);
return;
}
if (method(request) === "GET" && url.pathname === "/saml/metadata") {
response.writeHead(200, { "content-type": "application/samlmetadata+xml; charset=utf-8", "cache-control": "no-store" });
response.end(samlMetadata(state.config, certificate));
return;
}
if ((method(request) === "GET" || method(request) === "POST") && url.pathname === "/saml/sso") {
sendHtml(response, 200, samlPostBindingPage(state.config));
return;
}
sendJson(response, 404, { error: "not_found" });
}
function listen(server: Server, host: string, port: number): Promise<AddressInfo> {
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(port, host, () => {
server.off("error", reject);
const address = server.address();
if (isAddressInfo(address)) {
resolve(address);
return;
}
reject(new Error("Mock IdP server did not expose a TCP port."));
});
});
}
function close(server: Server): Promise<void> {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
export async function startMockIdpLab(input: MockIdpConfig = {}): Promise<StartedMockIdpLab> {
const keys = createKeyMaterial();
const certificate = certificateFromPublicKey(keys.publicKeyPem, input.knobs?.certTrailingNewline === true);
const fixedIssuer = configuredMockIdpIssuer(input);
const state: MockIdpState = {
config: normalizeMockIdpConfig(input),
keys,
codes: new Map(),
accessTokens: new Map(),
};
const server = createServer((request, response) => {
void handleRequest(state, certificate, request, response).catch((error) => {
sendJson(response, 500, { error: "mock_idp_error", message: error instanceof Error ? error.message : String(error) });
});
});
const fixedPort = fixedIssuer?.port ? Number.parseInt(fixedIssuer.port, 10) : 0;
const address = await listen(server, fixedIssuer?.hostname ?? "127.0.0.1", fixedPort);
state.config = normalizeMockIdpConfig({
...input,
issuer: fixedIssuer ? fixedIssuer.origin : `http://127.0.0.1:${address.port}`,
});
const stop = () => close(server);
return {
protocol: "oidc",
config: state.config,
issuer: state.config.issuer,
authorizationEndpoint: `${state.config.issuer}/authorize`,
tokenEndpoint: `${state.config.issuer}/token`,
jwksEndpoint: `${state.config.issuer}/jwks`,
userInfoEndpoint: `${state.config.issuer}/userinfo`,
saml: {
metadataUrl: `${state.config.issuer}/saml/metadata`,
ssoPostUrl: `${state.config.issuer}/saml/sso`,
certificate,
},
keyId: keys.keyId,
certificate,
publicKeyPem: keys.publicKeyPem,
registration: (overrides = {}) => oidcRegistration(state.config, overrides),
claimsFor: (subject) => buildOidcClaims({
issuer: state.config.issuer,
clientId: state.config.clientId,
subject: subjectWithKnobs(state.config, subject),
knobs: state.config.knobs,
}),
stop,
[Symbol.asyncDispose]: stop,
};
}
+7
View File
@@ -0,0 +1,7 @@
export * from "./egress.ts";
export * from "./idp.ts";
export * from "./mock-google.ts";
export * from "./mock-github.ts";
export * from "./mock-mcp.ts";
export * from "./not-implemented.ts";
export * from "./release-feed.ts";
+395
View File
@@ -0,0 +1,395 @@
import { createHash } from "node:crypto";
import { createServer } from "node:http";
import type { IncomingMessage, Server, ServerResponse } from "node:http";
import type { AddressInfo } from "node:net";
export interface MockGithubRepository {
id: number;
fullName: string;
defaultBranch: string;
private: boolean;
}
export interface MockGithubRequest {
method: string;
path: string;
url: string;
at: string;
authorization: string | null;
}
export interface MockGithubHandle {
apiUrl: string;
/** Replaces the repository's complete file map and returns its deterministic new head SHA. */
advanceHead(fullName: string, files: Record<string, string>): { headSha: string };
injectFaults(fault: MockGithubFault): void;
renameRepository(oldFullName: string, newFullName: string): void;
requests(): Promise<MockGithubRequest[]>;
stop(): Promise<void>;
[Symbol.asyncDispose](): Promise<void>;
}
export interface MockGithubFault {
count: number;
status: number;
pathIncludes?: string;
retryAfterSeconds?: number;
delayMs?: number;
}
export interface StartMockGithubOptions {
installationId: number;
accountLogin: string;
repositories: MockGithubRepository[];
/** files by repo fullName -> { branch, files: Record<repoRelativePath, string> } */
repoFiles: Record<string, { branch: string; files: Record<string, string> }>;
port?: number;
}
interface RepositorySnapshot {
headSha: string;
treeSha: string;
tree: Array<{
path: string;
type: "blob" | "tree";
sha: string;
size?: number;
}>;
}
interface PendingFault extends MockGithubFault {
remaining: number;
}
function isAddressInfo(value: ReturnType<Server["address"]>): value is AddressInfo {
return typeof value === "object"
&& value !== null
&& typeof value.address === "string"
&& typeof value.family === "string"
&& typeof value.port === "number";
}
function sendJson(
response: ServerResponse,
status: number,
body: unknown,
headers: Record<string, string> = {},
): void {
response.writeHead(status, {
"cache-control": "no-store",
"content-type": "application/json; charset=utf-8",
...headers,
});
response.end(JSON.stringify(body));
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function requestMethod(request: IncomingMessage): string {
return (request.method ?? "GET").toUpperCase();
}
function authorizationHeader(request: IncomingMessage): string | null {
const value = request.headers.authorization;
if (typeof value === "string") return value;
if (Array.isArray(value)) return value[0] ?? null;
return null;
}
function contentDigest(files: Record<string, string>): string {
const entries = Object.entries(files).sort(([left], [right]) => left.localeCompare(right));
return JSON.stringify(entries);
}
function repositorySnapshot(files: Record<string, string>): RepositorySnapshot {
const digest = contentDigest(files);
const treeSha = createHash("sha256").update(`tree\0${digest}`).digest("hex");
const headSha = createHash("sha1").update(`commit\0${treeSha}`).digest("hex");
const directories = new Set<string>();
for (const path of Object.keys(files)) {
const segments = path.split("/").filter(Boolean);
for (let index = 1; index < segments.length; index += 1) {
directories.add(segments.slice(0, index).join("/"));
}
}
const tree: RepositorySnapshot["tree"] = [];
for (const path of [...directories].sort()) {
tree.push({
path,
type: "tree",
sha: createHash("sha1").update(`directory\0${path}\0${digest}`).digest("hex"),
});
}
for (const [path, content] of Object.entries(files).sort(([left], [right]) => left.localeCompare(right))) {
tree.push({
path,
type: "blob",
sha: createHash("sha1").update(content).digest("hex"),
size: Buffer.byteLength(content),
});
}
tree.sort((left, right) => left.path.localeCompare(right.path));
return { headSha, treeSha, tree };
}
function repositoryJson(repository: MockGithubRepository): Record<string, unknown> {
return {
id: repository.id,
full_name: repository.fullName,
default_branch: repository.defaultBranch,
private: repository.private,
};
}
function positiveInteger(value: string | null, fallback: number): number {
if (!value) return fallback;
const parsed = Number(value);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
function listen(server: Server, port: number): Promise<AddressInfo> {
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(port, "127.0.0.1", () => {
server.off("error", reject);
const address = server.address();
if (isAddressInfo(address)) {
resolve(address);
return;
}
reject(new Error("Mock GitHub server did not expose a TCP port."));
});
});
}
function close(server: Server): Promise<void> {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
server.closeIdleConnections();
});
}
export async function startMockGithub(options: StartMockGithubOptions): Promise<MockGithubHandle> {
const requests: MockGithubRequest[] = [];
const faults: PendingFault[] = [];
const repositories = options.repositories.map((repository) => ({ ...repository }));
const repositoriesByFullName = new Map<string, MockGithubRepository>();
const fixturesByFullName = new Map<string, { branch: string; files: Record<string, string> }>();
const snapshotsByFullName = new Map<string, RepositorySnapshot>();
for (const repository of repositories) {
repositoriesByFullName.set(repository.fullName, repository);
const fixture = options.repoFiles[repository.fullName];
if (fixture) {
const files = { ...fixture.files };
fixturesByFullName.set(repository.fullName, { branch: fixture.branch, files });
snapshotsByFullName.set(repository.fullName, repositorySnapshot(files));
}
}
let apiUrl = "http://127.0.0.1";
const handleRequest = async (request: IncomingMessage, response: ServerResponse): Promise<void> => {
try {
const url = new URL(request.url ?? "/", apiUrl);
const method = requestMethod(request);
if (method === "GET" && url.pathname === "/health") {
sendJson(response, 200, { ok: true });
return;
}
if (method === "GET" && url.pathname === "/requests") {
sendJson(response, 200, { requests });
return;
}
requests.push({
method,
path: url.pathname,
url: `${url.pathname}${url.search}`,
at: new Date().toISOString(),
authorization: authorizationHeader(request),
});
const faultIndex = faults.findIndex((fault) => (
fault.remaining > 0 && (!fault.pathIncludes || url.pathname.includes(fault.pathIncludes))
));
if (faultIndex >= 0) {
const fault = faults[faultIndex];
if (fault) {
fault.remaining -= 1;
if (fault.remaining === 0) faults.splice(faultIndex, 1);
if (fault.delayMs !== undefined) {
await sleep(fault.delayMs);
} else {
const headers: Record<string, string> = {};
if (fault.retryAfterSeconds !== undefined) {
headers["retry-after"] = String(fault.retryAfterSeconds);
}
sendJson(response, fault.status, { message: "mock fault" }, headers);
return;
}
}
}
const segments = url.pathname.split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
if (method === "POST"
&& segments.length === 4
&& segments[0] === "app"
&& segments[1] === "installations"
&& segments[2] === String(options.installationId)
&& segments[3] === "access_tokens") {
sendJson(response, 201, { token: `mock-installation-token-${options.installationId}` });
return;
}
if (method === "GET"
&& segments.length === 3
&& segments[0] === "app"
&& segments[1] === "installations"
&& segments[2] === String(options.installationId)) {
sendJson(response, 200, {
id: options.installationId,
account: { login: options.accountLogin, type: "Organization" },
repository_selection: "all",
html_url: `${apiUrl}/settings`,
});
return;
}
if (method === "GET" && url.pathname === "/installation/repositories") {
const perPage = positiveInteger(url.searchParams.get("per_page"), 30);
const page = positiveInteger(url.searchParams.get("page"), 1);
const start = (page - 1) * perPage;
sendJson(response, 200, {
total_count: repositories.length,
repositories: repositories.slice(start, start + perPage).map(repositoryJson),
});
return;
}
if (segments[0] === "repos" && segments.length >= 3) {
const fullName = `${segments[1]}/${segments[2]}`;
const repository = repositoriesByFullName.get(fullName);
const fixture = fixturesByFullName.get(fullName);
const snapshot = snapshotsByFullName.get(fullName);
if (!repository) {
sendJson(response, 404, { message: "not found" });
return;
}
if (method === "GET" && segments.length === 3) {
sendJson(response, 200, repositoryJson(repository));
return;
}
if (method === "GET" && segments.length === 5 && segments[3] === "branches") {
const branch = segments[4];
if (branch === repository.defaultBranch && (!fixture || fixture.branch === branch)) {
sendJson(response, 200, { name: branch });
} else {
sendJson(response, 404, { message: "not found" });
}
return;
}
if (method === "GET" && segments.length === 5 && segments[3] === "commits") {
if (fixture && snapshot && segments[4] === fixture.branch) {
sendJson(response, 200, {
sha: snapshot.headSha,
commit: { tree: { sha: snapshot.treeSha } },
});
} else {
sendJson(response, 404, { message: "not found" });
}
return;
}
if (method === "GET"
&& segments.length === 6
&& segments[3] === "git"
&& segments[4] === "trees") {
if (snapshot && segments[5] === snapshot.treeSha && url.searchParams.get("recursive") === "1") {
sendJson(response, 200, { truncated: false, tree: snapshot.tree });
} else {
sendJson(response, 404, { message: "not found" });
}
return;
}
if (method === "GET" && segments.length >= 5 && segments[3] === "contents") {
const path = segments.slice(4).join("/");
const ref = url.searchParams.get("ref") ?? repository.defaultBranch;
const content = fixture?.branch === ref ? fixture.files[path] : undefined;
if (typeof content === "string") {
sendJson(response, 200, { content: Buffer.from(content).toString("base64"), encoding: "base64" });
} else {
sendJson(response, 404, { message: "not found" });
}
return;
}
}
sendJson(response, 404, { message: "not found" });
} catch (error) {
// Log the detail server-side; never echo error text into the HTTP body
// (CodeQL js/stack-trace-exposure), even in a local test witness.
console.error(`[mock-github] request handler failed: ${error instanceof Error ? error.message : String(error)}`);
sendJson(response, 500, { message: "mock github internal error" });
}
};
const server = createServer((request, response) => {
void handleRequest(request, response);
});
const address = await listen(server, options.port ?? 0);
apiUrl = `http://127.0.0.1:${address.port}`;
let stopped = false;
const stop = async (): Promise<void> => {
if (stopped) return;
stopped = true;
await close(server);
};
return {
apiUrl,
advanceHead(fullName, files) {
const repository = repositoriesByFullName.get(fullName);
if (!repository) throw new Error(`Mock GitHub repository not found: ${fullName}`);
const fixture = fixturesByFullName.get(fullName);
const nextFiles = { ...files };
const snapshot = repositorySnapshot(nextFiles);
fixturesByFullName.set(fullName, {
branch: fixture?.branch ?? repository.defaultBranch,
files: nextFiles,
});
snapshotsByFullName.set(fullName, snapshot);
return { headSha: snapshot.headSha };
},
injectFaults(fault) {
if (fault.count <= 0) return;
faults.push({ ...fault, remaining: fault.count });
},
renameRepository(oldFullName, newFullName) {
const repository = repositoriesByFullName.get(oldFullName);
if (!repository) throw new Error(`Mock GitHub repository not found: ${oldFullName}`);
if (repositoriesByFullName.has(newFullName)) {
throw new Error(`Mock GitHub repository already exists: ${newFullName}`);
}
const renamed = { ...repository, fullName: newFullName };
const index = repositories.findIndex((candidate) => candidate.fullName === oldFullName);
if (index >= 0) repositories[index] = renamed;
repositoriesByFullName.delete(oldFullName);
repositoriesByFullName.set(newFullName, renamed);
const fixture = fixturesByFullName.get(oldFullName);
fixturesByFullName.delete(oldFullName);
if (fixture) fixturesByFullName.set(newFullName, fixture);
const snapshot = snapshotsByFullName.get(oldFullName);
snapshotsByFullName.delete(oldFullName);
if (snapshot) snapshotsByFullName.set(newFullName, snapshot);
},
async requests() {
return requests.map((request) => ({ ...request }));
},
stop,
[Symbol.asyncDispose]: stop,
};
}
@@ -0,0 +1,732 @@
import { createHash, generateKeyPairSync, randomUUID } from "node:crypto";
import { createServer } from "node:http";
import type { IncomingMessage, Server, ServerResponse } from "node:http";
import type { AddressInfo } from "node:net";
import { buildOidcClaims, signOidcJwt } from "./idp.ts";
interface MockGoogleServerOptions {
accounts: string[];
port: number;
autoApprove: boolean;
baseUrl?: string;
}
interface StartedMockGoogleServer {
baseUrl: string;
stop(): Promise<void>;
}
interface Account {
sub: string;
email: string;
name: string;
}
interface RequestEntry {
method: string;
path: string;
url: string;
at: string;
}
interface PendingAuthorization {
id: string;
params: URLSearchParams;
}
interface AuthorizationCode {
account: Account;
clientId: string;
codeChallenge: string | null;
codeChallengeMethod: string;
scope: string;
}
interface RefreshCredential {
account: Account;
clientId: string;
scope: string;
}
interface RecordedDraft {
to: string;
body: string;
threadId?: string;
attachments?: RecordedAttachment[];
tokenId: string;
at: string;
}
interface RecordedAttachment {
filename: string;
mimeType: string;
size: number;
dataBase64: string;
}
interface RecordedDriveUpload extends RecordedAttachment {
tokenId: string;
at: string;
}
interface SigningKeys {
keyId: string;
privateKeyPem: string;
}
interface MockGoogleState {
accounts: Account[];
autoApprove: boolean;
baseUrl: string;
requests: RequestEntry[];
pending: Map<string, PendingAuthorization>;
codes: Map<string, AuthorizationCode>;
accessTokens: Map<string, Account>;
refreshTokens: Map<string, RefreshCredential>;
drafts: Map<string, RecordedDraft[]>;
driveUploads: Map<string, RecordedDriveUpload[]>;
keys: SigningKeys;
}
const HTML_ENTITIES: Record<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isAddressInfo(value: ReturnType<Server["address"]>): value is AddressInfo {
return typeof value === "object"
&& value !== null
&& typeof value.address === "string"
&& typeof value.family === "string"
&& typeof value.port === "number";
}
function accountName(email: string): string {
const localPart = email.split("@")[0] ?? email;
const words = localPart.split(/[._+-]+/).filter(Boolean);
return words.map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join(" ") || email;
}
function normalizeAccounts(values: string[]): Account[] {
const emails = Array.from(new Set(values.map((value) => value.trim().toLowerCase()).filter(Boolean)));
if (emails.length === 0) {
throw new Error("startMockGoogle requires at least one account.");
}
return emails.map((email) => ({ sub: email, email, name: accountName(email) }));
}
function createSigningKeys(): SigningKeys {
const pair = generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs8", format: "pem" },
});
return {
keyId: `mock-google-${randomUUID()}`,
privateKeyPem: pair.privateKey,
};
}
function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, (character) => HTML_ENTITIES[character] ?? character);
}
function sendJson(response: ServerResponse, status: number, body: unknown): void {
response.writeHead(status, {
"access-control-allow-origin": "*",
"access-control-allow-headers": "authorization, content-type",
"access-control-allow-methods": "GET, POST, OPTIONS",
"cache-control": "no-store",
"content-type": "application/json; charset=utf-8",
});
response.end(JSON.stringify(body));
}
function sendHtml(response: ServerResponse, status: number, body: string): void {
response.writeHead(status, {
"access-control-allow-origin": "*",
"cache-control": "no-store",
"content-type": "text/html; charset=utf-8",
});
response.end(body);
}
function sendRedirect(response: ServerResponse, location: string): void {
response.writeHead(302, { "cache-control": "no-store", location });
response.end();
}
function method(request: IncomingMessage): string {
return (request.method ?? "GET").toUpperCase();
}
function requestUrl(request: IncomingMessage, baseUrl: string): URL {
return new URL(request.url ?? "/", baseUrl);
}
async function requestBuffer(request: IncomingMessage): Promise<Buffer> {
const chunks: Buffer[] = [];
for await (const chunk of request) {
if (typeof chunk === "string") {
chunks.push(Buffer.from(chunk));
} else if (Buffer.isBuffer(chunk)) {
chunks.push(chunk);
} else {
chunks.push(Buffer.from(String(chunk)));
}
}
return Buffer.concat(chunks);
}
async function requestBody(request: IncomingMessage): Promise<string> {
return (await requestBuffer(request)).toString("utf8");
}
async function formBody(request: IncomingMessage): Promise<URLSearchParams> {
return new URLSearchParams(await requestBody(request));
}
async function jsonBody(request: IncomingMessage): Promise<unknown> {
const body = await requestBody(request);
return body.trim() ? JSON.parse(body) : null;
}
function bearerToken(request: IncomingMessage): string | null {
const raw = request.headers.authorization;
const header = Array.isArray(raw) ? raw[0] ?? "" : raw ?? "";
if (!/^bearer /i.test(header)) return null;
return header.slice("bearer ".length).trim() || null;
}
function tokenId(token: string): string {
return createHash("sha256").update(token).digest("hex").slice(0, 12);
}
function accountForRequest(state: MockGoogleState, request: IncomingMessage): Account | null {
const token = bearerToken(request);
return token ? state.accessTokens.get(token) ?? null : null;
}
function recordRequest(state: MockGoogleState, request: IncomingMessage, url: URL): void {
state.requests.push({
method: method(request),
path: url.pathname,
url: `${url.pathname}${url.search}`,
at: new Date().toISOString(),
});
}
function callbackForCode(params: URLSearchParams, code: string): string | null {
const redirectUri = params.get("redirect_uri");
if (!redirectUri) return null;
const callback = new URL(redirectUri);
callback.searchParams.set("code", code);
const relayState = params.get("state");
if (relayState) callback.searchParams.set("state", relayState);
return callback.toString();
}
function authorizeAccount(
state: MockGoogleState,
pending: PendingAuthorization,
account: Account,
response: ServerResponse,
): void {
const code = `mock-google-code-${randomUUID()}`;
const callback = callbackForCode(pending.params, code);
if (!callback) {
sendJson(response, 400, { error: "invalid_request", error_description: "redirect_uri is required" });
return;
}
state.codes.set(code, {
account,
clientId: pending.params.get("client_id") ?? "mock-google-client",
codeChallenge: pending.params.get("code_challenge"),
codeChallengeMethod: pending.params.get("code_challenge_method") ?? "plain",
scope: pending.params.get("scope") ?? "openid email profile",
});
state.pending.delete(pending.id);
sendRedirect(response, callback);
}
function chooserPage(state: MockGoogleState, pending: PendingAuthorization): string {
const accounts = state.accounts.map((account) => {
const chooseUrl = new URL("/choose-account", state.baseUrl);
chooseUrl.searchParams.set("request_id", pending.id);
chooseUrl.searchParams.set("email", account.email);
return `<li><a data-account-email="${escapeHtml(account.email)}" href="${escapeHtml(`${chooseUrl.pathname}${chooseUrl.search}`)}">${escapeHtml(account.name)} <span>${escapeHtml(account.email)}</span></a></li>`;
}).join("");
return `<!doctype html><html><head><title>Choose an account</title></head><body><main style="font-family: system-ui, sans-serif; max-width: 560px; margin: 48px auto;"><h1>Choose an account</h1><p>Continue to OpenWork</p><ul>${accounts}</ul></main></body></html>`;
}
function consentPage(pending: PendingAuthorization): string {
const approveUrl = new URL("/approve", "http://mock.invalid");
approveUrl.searchParams.set("request_id", pending.id);
return `<!doctype html><html><head><title>Mock Google OAuth</title></head><body><main style="font-family: system-ui, sans-serif; max-width: 560px; margin: 48px auto;"><h1>Mock Google OAuth</h1><p>This fake Google provider is for OpenWork end-to-end tests.</p><form method="post" action="${escapeHtml(`${approveUrl.pathname}${approveUrl.search}`)}"><button>Approve OpenWork</button></form></main></body></html>`;
}
function authorize(state: MockGoogleState, url: URL, response: ServerResponse): void {
if (!url.searchParams.get("redirect_uri")) {
sendJson(response, 400, { error: "invalid_request", error_description: "redirect_uri is required" });
return;
}
const pending: PendingAuthorization = {
id: randomUUID(),
params: new URLSearchParams(url.searchParams),
};
const selectAccount = (url.searchParams.get("prompt") ?? "").split(/\s+/).includes("select_account");
if (selectAccount) {
state.pending.set(pending.id, pending);
sendHtml(response, 200, chooserPage(state, pending));
return;
}
if (state.autoApprove) {
authorizeAccount(state, pending, state.accounts[0], response);
return;
}
state.pending.set(pending.id, pending);
sendHtml(response, 200, consentPage(pending));
}
function chooseAccount(state: MockGoogleState, url: URL, response: ServerResponse): void {
const pending = state.pending.get(url.searchParams.get("request_id") ?? "") ?? null;
const account = state.accounts.find((candidate) => candidate.email === (url.searchParams.get("email") ?? "").toLowerCase()) ?? null;
if (!pending || !account) {
sendJson(response, 404, { error: "authorization_not_found" });
return;
}
authorizeAccount(state, pending, account, response);
}
function approve(state: MockGoogleState, url: URL, response: ServerResponse): void {
const pending = state.pending.get(url.searchParams.get("request_id") ?? "") ?? null;
if (!pending) {
sendJson(response, 404, { error: "authorization_not_found" });
return;
}
authorizeAccount(state, pending, state.accounts[0], response);
}
function claimsFor(state: MockGoogleState, credential: RefreshCredential): Record<string, unknown> {
return buildOidcClaims({
issuer: state.baseUrl,
clientId: credential.clientId,
subject: credential.account,
});
}
function issueCredential(state: MockGoogleState, credential: RefreshCredential): {
accessToken: string;
refreshToken: string;
idToken: string;
} {
const accessToken = `mock-google-access-${randomUUID()}`;
const refreshToken = `mock-google-refresh-${randomUUID()}`;
const claims = claimsFor(state, credential);
state.accessTokens.set(accessToken, credential.account);
state.refreshTokens.set(refreshToken, credential);
return {
accessToken,
refreshToken,
idToken: signOidcJwt({ keyId: state.keys.keyId, privateKeyPem: state.keys.privateKeyPem, claims }),
};
}
async function token(state: MockGoogleState, request: IncomingMessage, response: ServerResponse): Promise<void> {
const params = await formBody(request);
const grantType = params.get("grant_type") ?? "authorization_code";
let credential: RefreshCredential;
if (grantType === "authorization_code") {
const codeValue = params.get("code") ?? "";
const code = state.codes.get(codeValue) ?? null;
if (!code) {
sendJson(response, 400, { error: "invalid_grant" });
return;
}
if (code.codeChallenge) {
const verifier = params.get("code_verifier") ?? "";
const expected = code.codeChallengeMethod === "S256"
? createHash("sha256").update(verifier).digest("base64url")
: verifier;
if (expected !== code.codeChallenge) {
sendJson(response, 400, { error: "invalid_grant", error_description: "PKCE verification failed" });
return;
}
}
state.codes.delete(codeValue);
credential = { account: code.account, clientId: code.clientId, scope: code.scope };
} else if (grantType === "refresh_token") {
const refresh = state.refreshTokens.get(params.get("refresh_token") ?? "") ?? null;
if (!refresh) {
sendJson(response, 400, { error: "invalid_grant" });
return;
}
credential = refresh;
} else {
sendJson(response, 400, { error: "unsupported_grant_type" });
return;
}
const issued = issueCredential(state, credential);
sendJson(response, 200, {
access_token: issued.accessToken,
refresh_token: issued.refreshToken,
token_type: "Bearer",
expires_in: 3600,
scope: credential.scope,
id_token: issued.idToken,
});
}
function userinfo(state: MockGoogleState, request: IncomingMessage, response: ServerResponse): void {
const account = accountForRequest(state, request);
if (!account) {
sendJson(response, 401, { error: "invalid_token" });
return;
}
sendJson(response, 200, { sub: account.sub, email: account.email, email_verified: true, name: account.name });
}
function decodedRawMessage(raw: string): string {
try {
return Buffer.from(raw, "base64url").toString("utf8");
} catch {
return "";
}
}
function splitMessage(message: string): { headers: string; body: string } {
const separator = /\r?\n\r?\n/.exec(message);
if (!separator || separator.index === undefined) return { headers: message, body: "" };
return {
headers: message.slice(0, separator.index),
body: message.slice(separator.index + separator[0].length),
};
}
function headerValue(headers: string, name: string): string {
const unfolded = headers.replace(/\r?\n[ \t]+/g, " ");
const prefix = `${name.toLowerCase()}:`;
const line = unfolded.split(/\r?\n/).find((candidate) => candidate.toLowerCase().startsWith(prefix));
return line ? line.slice(line.indexOf(":") + 1).trim() : "";
}
function decodeBase64Body(body: string): string {
try {
return Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf8");
} catch {
return body;
}
}
function mimeTextBody(headers: string, body: string): string | null {
const contentType = headerValue(headers, "content-type");
const boundary = /boundary="?([^";]+)"?/i.exec(contentType)?.[1] ?? null;
if (boundary) {
for (const part of body.split(`--${boundary}`).slice(1)) {
if (!part.trim() || part.trim().startsWith("--")) continue;
const split = splitMessage(part.replace(/^\r?\n/, ""));
const decoded = mimeTextBody(split.headers, split.body);
if (decoded !== null) return decoded;
}
return null;
}
if (contentType && !contentType.toLowerCase().startsWith("text/plain")) return null;
return headerValue(headers, "content-transfer-encoding").toLowerCase() === "base64"
? decodeBase64Body(body)
: body.trimEnd();
}
function draftBody(headers: string, body: string): string {
return mimeTextBody(headers, body) ?? "";
}
function unquoteMimeParameter(value: string): string {
return value.replace(/\\([\\"])/g, "$1");
}
function mimeParameter(value: string, name: string): string {
const quoted = new RegExp(`${name}="((?:\\\\.|[^"\\\\])*)"`, "i").exec(value)?.[1];
if (quoted !== undefined) return unquoteMimeParameter(quoted);
return new RegExp(`${name}=([^;\\s]+)`, "i").exec(value)?.[1] ?? "";
}
function mimeAttachments(headers: string, body: string): RecordedAttachment[] {
const contentType = headerValue(headers, "content-type");
const boundary = mimeParameter(contentType, "boundary");
if (boundary) {
return body.split(`--${boundary}`).slice(1).flatMap((part) => {
if (!part.trim() || part.trim().startsWith("--")) return [];
const split = splitMessage(part.replace(/^\r?\n/, ""));
return mimeAttachments(split.headers, split.body);
});
}
const disposition = headerValue(headers, "content-disposition");
if (!/^attachment(?:;|$)/i.test(disposition)) return [];
const filename = mimeParameter(disposition, "filename") || mimeParameter(contentType, "name");
const mimeType = contentType.split(";", 1)[0]?.trim() || "application/octet-stream";
const content = headerValue(headers, "content-transfer-encoding").toLowerCase() === "base64"
? Buffer.from(body.replace(/\s+/g, ""), "base64")
: Buffer.from(body.replace(/\r?\n$/, ""), "utf8");
return [{ filename, mimeType, size: content.byteLength, dataBase64: content.toString("base64") }];
}
function draftInput(value: unknown): { raw: string; threadId?: string } {
if (!isRecord(value) || !isRecord(value.message)) return { raw: "" };
const raw = typeof value.message.raw === "string" ? value.message.raw : "";
const threadId = typeof value.message.threadId === "string" ? value.message.threadId : undefined;
return threadId ? { raw, threadId } : { raw };
}
async function createDraft(state: MockGoogleState, request: IncomingMessage, response: ServerResponse): Promise<void> {
const account = accountForRequest(state, request);
const accessToken = bearerToken(request);
if (!account || !accessToken) {
sendJson(response, 401, { error: { code: 401, message: "Invalid Credentials" } });
return;
}
const input = draftInput(await jsonBody(request));
const message = decodedRawMessage(input.raw);
const split = splitMessage(message);
const draft: RecordedDraft = {
to: headerValue(split.headers, "to"),
body: draftBody(split.headers, split.body),
tokenId: tokenId(accessToken),
at: new Date().toISOString(),
};
if (input.threadId) draft.threadId = input.threadId;
const attachments = mimeAttachments(split.headers, split.body);
if (attachments.length > 0) draft.attachments = attachments;
const mailbox = state.drafts.get(account.email) ?? [];
mailbox.push(draft);
state.drafts.set(account.email, mailbox);
const threadId = input.threadId ?? `thread-${randomUUID()}`;
sendJson(response, 200, {
id: `draft-${randomUUID()}`,
message: { id: `msg-${randomUUID()}`, threadId },
});
}
function requestHeader(request: IncomingMessage, name: string): string {
const value = request.headers[name.toLowerCase()];
return Array.isArray(value) ? value[0] ?? "" : value ?? "";
}
function parseDriveMultipart(body: Buffer, contentType: string): RecordedAttachment | null {
const boundary = mimeParameter(contentType, "boundary");
if (!boundary) return null;
const partBoundary = Buffer.from(`\r\n--${boundary}\r\n`, "utf8");
const firstHeaderEnd = body.indexOf(Buffer.from("\r\n\r\n", "utf8"));
const secondPart = body.indexOf(partBoundary);
if (firstHeaderEnd < 0 || secondPart < 0 || secondPart <= firstHeaderEnd) return null;
const metadataText = body.subarray(firstHeaderEnd + 4, secondPart).toString("utf8");
let metadata: unknown;
try {
metadata = JSON.parse(metadataText);
} catch {
return null;
}
const filename = isRecord(metadata) && typeof metadata.name === "string" ? metadata.name : "";
if (!filename) return null;
const secondHeaderStart = secondPart + partBoundary.byteLength;
const secondHeaderEnd = body.indexOf(Buffer.from("\r\n\r\n", "utf8"), secondHeaderStart);
const footer = Buffer.from(`\r\n--${boundary}--`, "utf8");
const contentEnd = body.lastIndexOf(footer);
if (secondHeaderEnd < 0 || contentEnd < 0 || contentEnd < secondHeaderEnd) return null;
const headers = body.subarray(secondHeaderStart, secondHeaderEnd).toString("utf8");
const mimeType = headerValue(headers, "content-type") || "application/octet-stream";
const content = body.subarray(secondHeaderEnd + 4, contentEnd);
return { filename, mimeType, size: content.byteLength, dataBase64: content.toString("base64") };
}
async function createDriveUpload(state: MockGoogleState, request: IncomingMessage, response: ServerResponse): Promise<void> {
const account = accountForRequest(state, request);
const accessToken = bearerToken(request);
if (!account || !accessToken) {
sendJson(response, 401, { error: { code: 401, message: "Invalid Credentials" } });
return;
}
const upload = parseDriveMultipart(await requestBuffer(request), requestHeader(request, "content-type"));
if (!upload) {
sendJson(response, 400, { error: { code: 400, message: "Invalid multipart upload" } });
return;
}
const recorded: RecordedDriveUpload = {
...upload,
tokenId: tokenId(accessToken),
at: new Date().toISOString(),
};
const uploads = state.driveUploads.get(account.email) ?? [];
uploads.push(recorded);
state.driveUploads.set(account.email, uploads);
const id = `drive-${randomUUID()}`;
sendJson(response, 200, {
id,
name: upload.filename,
mimeType: upload.mimeType,
modifiedTime: recorded.at,
webViewLink: `https://drive.google.test/file/d/${id}/view`,
size: String(upload.size),
});
}
function pendingAuthorizations(state: MockGoogleState): unknown {
return {
pending: Array.from(state.pending.values()).map((entry) => ({
id: entry.id,
accounts: state.accounts.map((account) => {
const url = new URL("/choose-account", state.baseUrl);
url.searchParams.set("request_id", entry.id);
url.searchParams.set("email", account.email);
return { email: account.email, chooseUrl: url.toString() };
}),
})),
};
}
async function handleRequest(state: MockGoogleState, request: IncomingMessage, response: ServerResponse): Promise<void> {
const url = requestUrl(request, state.baseUrl);
const requestMethod = method(request);
recordRequest(state, request, url);
if (requestMethod === "OPTIONS") {
sendJson(response, 204, {});
return;
}
if (requestMethod === "GET" && url.pathname === "/health") {
sendJson(response, 200, { ok: true, accounts: state.accounts.map((account) => account.email), autoApprove: state.autoApprove });
return;
}
if (requestMethod === "GET" && url.pathname === "/requests") {
sendJson(response, 200, { requests: state.requests });
return;
}
if (requestMethod === "GET" && url.pathname === "/__mock-google/pending-authorizations") {
sendJson(response, 200, pendingAuthorizations(state));
return;
}
if (requestMethod === "GET" && url.pathname === "/__mock-google/drafts") {
sendJson(response, 200, { drafts: state.drafts.get((url.searchParams.get("email") ?? "").toLowerCase()) ?? [] });
return;
}
if (requestMethod === "GET" && url.pathname === "/__mock-google/drive-uploads") {
sendJson(response, 200, { uploads: state.driveUploads.get((url.searchParams.get("email") ?? "").toLowerCase()) ?? [] });
return;
}
if (requestMethod === "GET" && url.pathname === "/authorize") {
authorize(state, url, response);
return;
}
if (requestMethod === "GET" && url.pathname === "/choose-account") {
chooseAccount(state, url, response);
return;
}
if (requestMethod === "POST" && url.pathname === "/approve") {
approve(state, url, response);
return;
}
if (requestMethod === "POST" && url.pathname === "/token") {
await token(state, request, response);
return;
}
if (requestMethod === "GET" && url.pathname === "/userinfo") {
userinfo(state, request, response);
return;
}
if (requestMethod === "GET" && url.pathname === "/gmail/v1/users/me/profile") {
const account = accountForRequest(state, request);
if (!account) {
sendJson(response, 401, { error: { code: 401, message: "Invalid Credentials" } });
return;
}
sendJson(response, 200, { emailAddress: account.email, messagesTotal: 0, threadsTotal: 0, historyId: "1" });
return;
}
if (requestMethod === "GET" && url.pathname === "/gmail/v1/users/me/messages") {
if (!accountForRequest(state, request)) {
sendJson(response, 401, { error: { code: 401, message: "Invalid Credentials" } });
return;
}
sendJson(response, 200, { messages: [], resultSizeEstimate: 0 });
return;
}
if (requestMethod === "POST" && url.pathname === "/gmail/v1/users/me/drafts") {
await createDraft(state, request, response);
return;
}
if (requestMethod === "POST" && url.pathname === "/upload/drive/v3/files") {
await createDriveUpload(state, request, response);
return;
}
sendJson(response, 404, { error: "not_found" });
}
function listen(server: Server, port: number): Promise<AddressInfo> {
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(port, "127.0.0.1", () => {
server.off("error", reject);
const address = server.address();
if (isAddressInfo(address)) {
resolve(address);
return;
}
reject(new Error("Mock Google server did not expose a TCP port."));
});
});
}
function close(server: Server): Promise<void> {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
server.closeIdleConnections();
});
}
export async function startMockGoogleServer(options: MockGoogleServerOptions): Promise<StartedMockGoogleServer> {
const state: MockGoogleState = {
accounts: normalizeAccounts(options.accounts),
autoApprove: options.autoApprove,
baseUrl: options.baseUrl ?? "http://127.0.0.1",
requests: [],
pending: new Map(),
codes: new Map(),
accessTokens: new Map(),
refreshTokens: new Map(),
drafts: new Map(),
driveUploads: new Map(),
keys: createSigningKeys(),
};
const server = createServer((request, response) => {
void handleRequest(state, request, response).catch((error) => {
sendJson(response, 500, { error: "mock_google_error", message: error instanceof Error ? error.message : String(error) });
});
});
const address = await listen(server, options.port);
state.baseUrl = options.baseUrl ?? `http://127.0.0.1:${address.port}`;
let stopped = false;
return {
baseUrl: state.baseUrl,
async stop() {
if (stopped) return;
stopped = true;
await close(server);
},
};
}
+265
View File
@@ -0,0 +1,265 @@
import { startMockGoogleServer } from "./mock-google-server.ts";
import { trimTrailingSlashes } from "./strings.ts";
export interface MockGoogleDraft {
to: string;
body: string;
threadId?: string;
attachments?: MockGoogleAttachment[];
/** Which credential created it — the isolation witness. */
tokenId: string;
at: string;
}
export interface MockGoogleAttachment {
filename: string;
mimeType: string;
size: number;
content: Buffer;
}
export interface MockGoogleDriveUpload extends MockGoogleAttachment {
/** Which credential created it — the isolation witness. */
tokenId: string;
at: string;
}
export interface MockGoogleHandle {
/** Base URL for DEN_GOOGLE_API_BASE_URL. */
apiUrl: string;
/** For DEN_GOOGLE_OAUTH_AUTHORIZE_URL / _TOKEN_URL / _USERINFO_URL. */
authorizeUrl: string;
tokenUrl: string;
userinfoUrl: string;
/** Drive the real chooser page shown when prompt=select_account. Resolves once the callback has been served. */
chooseAccount(email: string, opts?: { timeoutMs?: number }): Promise<void>;
/** Drafts attributed to ONE mailbox. Must be able to return [] as a real negative assertion. */
draftsFor(email: string, opts?: { since?: string; timeoutMs?: number; atLeast?: number }): Promise<MockGoogleDraft[]>;
/** Drive uploads attributed to ONE account, including provider-observed bytes. */
driveUploadsFor(email: string, opts?: { since?: string; timeoutMs?: number; atLeast?: number }): Promise<MockGoogleDriveUpload[]>;
authorizeRequestSince(iso: string, opts?: { timeoutMs?: number }): Promise<{ params: URLSearchParams }>;
stop(): Promise<void>;
[Symbol.asyncDispose](): Promise<void>;
}
export interface StartMockGoogleOptions {
accounts: string[];
port?: number;
publicUrl?: string;
autoApprove?: boolean;
}
export async function startMockGoogle(options: StartMockGoogleOptions): Promise<MockGoogleHandle> {
const accounts = Array.from(new Set(options.accounts.map((email) => email.trim().toLowerCase()).filter(Boolean)));
if (accounts.length === 0) throw new Error("startMockGoogle requires at least one account.");
const externalUrl = options.publicUrl ? trimTrailingSlashes(options.publicUrl.trim()) || undefined : undefined;
const local = externalUrl
? null
: await startMockGoogleServer({ accounts, port: options.port ?? 3980, autoApprove: options.autoApprove ?? true });
const url = externalUrl ?? local?.baseUrl;
if (!url) throw new Error("Mock Google did not expose a URL.");
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function stringArray(value: unknown): string[] | null {
return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : null;
}
async function json(urlValue: string, timeoutMs = 30_000): Promise<unknown> {
const response = await fetch(urlValue, { signal: AbortSignal.timeout(timeoutMs) });
const body: unknown = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Mock Google request failed: HTTP ${response.status} ${JSON.stringify(body).slice(0, 500)}`);
return body;
}
const healthDeadline = Date.now() + 30_000;
let healthy = false;
let healthDetail = "unreachable";
while (Date.now() < healthDeadline) {
try {
const body = await json(`${url}/health`, 2_000);
if (isRecord(body) && body.ok === true) {
const servedAccounts = stringArray(body.accounts);
if (servedAccounts && (servedAccounts.length !== accounts.length || servedAccounts.some((email, index) => email !== accounts[index]))) {
throw new Error(`Mock Google account mismatch: expected ${accounts.join(", ")}; got ${servedAccounts.join(", ")}.`);
}
if (typeof body.autoApprove === "boolean" && body.autoApprove !== (options.autoApprove ?? true)) {
throw new Error(`Mock Google autoApprove mismatch: expected ${options.autoApprove ?? true}; got ${body.autoApprove}.`);
}
healthy = true;
break;
}
healthDetail = JSON.stringify(body);
} catch (error) {
healthDetail = error instanceof Error ? error.message : String(error);
}
await sleep(250);
}
if (!healthy) {
await local?.stop();
throw new Error(`Mock Google not reachable at ${url}. Last: ${healthDetail}`);
}
async function requests(): Promise<Record<string, unknown>[]> {
const body = await json(`${url}/requests`);
return isRecord(body) && Array.isArray(body.requests) ? body.requests.filter(isRecord) : [];
}
function parseDraft(value: unknown): MockGoogleDraft | null {
if (!isRecord(value)
|| typeof value.to !== "string"
|| typeof value.body !== "string"
|| typeof value.tokenId !== "string"
|| typeof value.at !== "string") return null;
const draft: MockGoogleDraft = {
to: value.to,
body: value.body,
tokenId: value.tokenId,
at: value.at,
};
if (typeof value.threadId === "string") draft.threadId = value.threadId;
if (Array.isArray(value.attachments)) {
const attachments = value.attachments.flatMap((entry) => {
const attachment = parseAttachment(entry);
return attachment ? [attachment] : [];
});
if (attachments.length > 0) draft.attachments = attachments;
}
return draft;
}
function parseAttachment(value: unknown): MockGoogleAttachment | null {
if (!isRecord(value)
|| typeof value.filename !== "string"
|| typeof value.mimeType !== "string"
|| typeof value.size !== "number"
|| typeof value.dataBase64 !== "string") return null;
const content = Buffer.from(value.dataBase64, "base64");
if (content.byteLength !== value.size) return null;
return {
filename: value.filename,
mimeType: value.mimeType,
size: value.size,
content,
};
}
function parseDriveUpload(value: unknown): MockGoogleDriveUpload | null {
const attachment = parseAttachment(value);
if (!attachment || !isRecord(value) || typeof value.tokenId !== "string" || typeof value.at !== "string") return null;
return { ...attachment, tokenId: value.tokenId, at: value.at };
}
async function readDrafts(email: string, since: string | undefined, timeoutMs: number): Promise<MockGoogleDraft[]> {
const endpoint = new URL("/__mock-google/drafts", url);
endpoint.searchParams.set("email", email.trim().toLowerCase());
const body = await json(endpoint.toString(), timeoutMs);
if (!isRecord(body) || !Array.isArray(body.drafts)) return [];
return body.drafts.flatMap((value) => {
const draft = parseDraft(value);
return draft && (!since || draft.at >= since) ? [draft] : [];
});
}
async function readDriveUploads(email: string, since: string | undefined, timeoutMs: number): Promise<MockGoogleDriveUpload[]> {
const endpoint = new URL("/__mock-google/drive-uploads", url);
endpoint.searchParams.set("email", email.trim().toLowerCase());
const body = await json(endpoint.toString(), timeoutMs);
if (!isRecord(body) || !Array.isArray(body.uploads)) return [];
return body.uploads.flatMap((value) => {
const upload = parseDriveUpload(value);
return upload && (!since || upload.at >= since) ? [upload] : [];
});
}
async function pollForAtLeast<T>(
read: (timeoutMs: number) => Promise<T[]>,
atLeast: number | undefined,
timeoutMs: number,
): Promise<T[]> {
if (atLeast === undefined || atLeast <= 0) return read(timeoutMs);
const deadline = Date.now() + timeoutMs;
let values = await read(Math.max(1, deadline - Date.now()));
while (values.length < atLeast && Date.now() < deadline) {
await sleep(250);
values = await read(Math.max(1, deadline - Date.now()));
}
return values;
}
const stop = async (): Promise<void> => {
await local?.stop();
};
return {
apiUrl: url,
authorizeUrl: `${url}/authorize`,
tokenUrl: `${url}/token`,
userinfoUrl: `${url}/userinfo`,
async chooseAccount(email, opts = {}) {
const normalizedEmail = email.trim().toLowerCase();
if (!accounts.includes(normalizedEmail)) throw new Error(`Unknown mock Google account: ${email}`);
const deadline = Date.now() + (opts.timeoutMs ?? 60_000);
let chooseUrl: string | null = null;
while (!chooseUrl && Date.now() < deadline) {
const body = await json(`${url}/__mock-google/pending-authorizations`, Math.max(1, deadline - Date.now()));
const pending = isRecord(body) && Array.isArray(body.pending) ? body.pending.filter(isRecord) : [];
for (const authorization of [...pending].reverse()) {
if (!Array.isArray(authorization.accounts)) continue;
const account = authorization.accounts.find((value) => isRecord(value) && value.email === normalizedEmail);
if (isRecord(account) && typeof account.chooseUrl === "string") {
chooseUrl = account.chooseUrl;
break;
}
}
if (!chooseUrl) await sleep(100);
}
if (!chooseUrl) throw new Error(`No pending mock Google account chooser offered ${normalizedEmail}.`);
const remaining = Math.max(1, deadline - Date.now());
const choice = await fetch(chooseUrl, { redirect: "manual", signal: AbortSignal.timeout(remaining) });
const callbackUrl = choice.headers.get("location");
if (choice.status !== 302 || !callbackUrl) {
throw new Error(`Mock Google chooser failed: HTTP ${choice.status}`);
}
await fetch(callbackUrl, {
redirect: "manual",
signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())),
});
},
async draftsFor(email, opts = {}) {
const timeoutMs = opts.timeoutMs ?? 120_000;
return pollForAtLeast(
(remainingMs) => readDrafts(email, opts.since, remainingMs),
opts.atLeast,
timeoutMs,
);
},
async driveUploadsFor(email, opts = {}) {
const timeoutMs = opts.timeoutMs ?? 120_000;
return pollForAtLeast(
(remainingMs) => readDriveUploads(email, opts.since, remainingMs),
opts.atLeast,
timeoutMs,
);
},
async authorizeRequestSince(iso, opts = {}) {
const deadline = Date.now() + (opts.timeoutMs ?? 60_000);
while (Date.now() < deadline) {
const request = (await requests()).find((entry) => entry.method === "GET"
&& entry.path === "/authorize"
&& typeof entry.at === "string"
&& entry.at >= iso);
if (request && typeof request.url === "string") {
return { params: new URL(request.url, url).searchParams };
}
await sleep(100);
}
throw new Error(`No GET /authorize reached the mock Google provider after ${iso}.`);
},
stop,
[Symbol.asyncDispose]: stop,
};
}
+472
View File
@@ -0,0 +1,472 @@
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { join } from "node:path";
import type { ChildProcess } from "node:child_process";
import { trimTrailingSlashes } from "./strings.ts";
export interface MockAuthorizeRequest {
method: string;
path: string;
url: string;
at: string;
}
/** A tool invocation the connector actually served, and which credential served it. */
export interface MockToolCall {
name: string;
args: Record<string, unknown>;
/** sha256 prefix of the caller's bearer token — distinct per member credential. */
tokenId: string | null;
/** When the connector served it (the mock's clock). */
at: string;
}
export interface MockAgentToolStep {
tool: string;
arguments: Record<string, unknown>;
}
export interface MockAgentWorkload {
promptMarker: string;
finalReply: string;
steps: MockAgentToolStep[];
}
export interface MockAgentRequest {
model: string;
promptMarker: string | null;
matchedMarkers: string[];
completedTools: number;
kind: "utility" | "tool" | "final" | "error";
toolName: string | null;
arguments: Record<string, unknown>;
at: string;
}
export interface MockMcpHandle {
url: string;
mcpUrl: string;
authorizeRequestSince(iso: string, opts?: { timeoutMs?: number }): Promise<MockAuthorizeRequest & { params: URLSearchParams }>;
requests(): Promise<MockAuthorizeRequest[]>;
/**
* Tool calls the connector served. This is the AUTHORITY for "did a person
* really use it" — the app's own UI can look connected while nothing was
* invoked, and per-member isolation is only provable by distinct tokenIds.
*
* Pass sinceIso when the mock is long-lived (publicUrl): its request log
* spans runs, so an unfiltered atLeast is satisfied by a PREVIOUS run's
* calls and returns before this run's calls ever arrive.
*/
toolCalls(opts?: { name?: string; timeoutMs?: number; atLeast?: number; sinceIso?: string }): Promise<MockToolCall[]>;
agentRequests(opts?: { promptMarker?: string; timeoutMs?: number; atLeast?: number; sinceIso?: string }): Promise<MockAgentRequest[]>;
handshakes(opts?: { timeoutMs?: number; atLeast?: number; sinceIso?: string }): Promise<MockAuthorizeRequest[]>;
configureOAuthRedirectUris(redirectUris: readonly string[]): Promise<void>;
resetOAuth(): Promise<void>;
stop(): Promise<void>;
[Symbol.asyncDispose](): Promise<void>;
}
export interface StartMockMcpOptions {
port?: number;
scriptPath?: string;
publicUrl?: string;
/** Advertised OAuth/resource origin when the mock sits behind a proxy; defaults to the mock's own URL. */
issuer?: string;
profileId?: EnterpriseMcpProfileId;
fault?: string;
oauthClientSecret?: string;
allowUnauthenticatedMcp?: boolean;
/** Serve this many additional synthetic mock_tool_<i> tools for scale specs. */
extraToolCount?: number;
/** Script deterministic OpenAI-compatible agent turns through this mock. */
agentWorkloads?: MockAgentWorkload[];
}
export type EnterpriseMcpProfileId =
| "synthetic-enterprise-oauth-mcp"
| "servicenow-inbound-quickstart"
| "microsoft-work-iq"
| "microsoft-enterprise"
| "agent-365-mail-v1-2026-07"
| "slack-user-mcp";
const REPO_ROOT = fileURLToPath(new URL("../../../..", import.meta.url));
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function parseRequest(value: unknown): MockAuthorizeRequest | null {
if (!isRecord(value)) return null;
if (
typeof value.method !== "string"
|| typeof value.path !== "string"
|| typeof value.url !== "string"
|| typeof value.at !== "string"
) return null;
return { method: value.method, path: value.path, url: value.url, at: value.at };
}
function parseRequests(value: unknown): MockAuthorizeRequest[] {
if (!isRecord(value) || !Array.isArray(value.requests)) return [];
return value.requests.flatMap((entry) => {
const request = parseRequest(entry);
return request ? [request] : [];
});
}
async function waitForHealth(url: string, output: () => string, child: ChildProcess | null): Promise<void> {
let last = "unreachable";
const deadline = Date.now() + 30_000;
while (Date.now() < deadline) {
// With an externally-managed mock there is no child at all; only a spawned
// child that has ALREADY exited (exitCode set) means startup failed.
if (child && child.exitCode !== null) {
throw new Error(`Mock OAuth+MCP server exited before becoming healthy. Output: ${output().slice(-1_000)}`);
}
try {
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(10_000) });
const body: unknown = await response.json().catch(() => null);
if (response.ok && isRecord(body) && body.ok === true) {
if (Object.hasOwn(body, "autoApprove") && body.autoApprove === false) {
throw new Error("Mock OAuth+MCP server must report autoApprove=true.");
}
return;
}
last = response.ok ? JSON.stringify(body) : `HTTP ${response.status}`;
} catch (error) {
last = error instanceof Error ? error.message : String(error);
}
await sleep(500);
}
throw new Error(`Mock OAuth+MCP server not reachable at ${url}. Last: ${last}. Output: ${output().slice(-1_000)}`);
}
async function waitForEnterpriseHealth(url: string, output: () => string, child: ChildProcess): Promise<string> {
let last = "unreachable";
const deadline = Date.now() + 30_000;
while (Date.now() < deadline) {
if (child.exitCode !== null) {
throw new Error(`Enterprise MCP mock exited before becoming healthy. Output: ${output().slice(-1_000)}`);
}
try {
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(10_000) });
const body: unknown = await response.json().catch(() => null);
if (response.ok && isRecord(body) && body.status === "ok" && typeof body.mcpUrl === "string") {
return body.mcpUrl;
}
last = response.ok ? JSON.stringify(body) : `HTTP ${response.status}`;
} catch (error) {
last = error instanceof Error ? error.message : String(error);
}
await sleep(500);
}
throw new Error(`Enterprise MCP mock not reachable at ${url}. Last: ${last}. Output: ${output().slice(-1_000)}`);
}
async function stopChild(child: ChildProcess | null): Promise<void> {
if (!child || child.exitCode !== null) return;
await new Promise<void>((resolve) => {
const timer = setTimeout(() => {
child.kill("SIGKILL");
resolve();
}, 5_000);
child.once("exit", () => {
clearTimeout(timer);
resolve();
});
child.kill("SIGTERM");
});
}
async function stopProcessGroup(child: ChildProcess | null): Promise<void> {
if (!child || child.exitCode !== null || !child.pid) return;
const pid = child.pid;
await new Promise<void>((resolve) => {
const timer = setTimeout(() => {
try {
process.kill(-pid, "SIGKILL");
} catch {
// The process group already exited.
}
resolve();
}, 5_000);
child.once("exit", () => {
clearTimeout(timer);
resolve();
});
try {
process.kill(-pid, "SIGTERM");
} catch {
clearTimeout(timer);
resolve();
}
});
}
async function startEnterpriseProfileMock(options: StartMockMcpOptions): Promise<MockMcpHandle> {
if (options.publicUrl) throw new Error("Enterprise MCP provider profiles must be started in the local eval process.");
if (!options.profileId) throw new Error("An enterprise MCP provider profile id is required.");
const profileId = options.profileId;
const port = options.port ?? 3979;
const url = `http://127.0.0.1:${port}`;
const runner = join(REPO_ROOT, "packages", "enterprise-mcp-mock-server", "src", "cli.ts");
let child: ChildProcess | null = null;
let output = "";
let redirectUris: readonly string[] = [];
let mcpUrl = `${url}/mcp`;
const boot = async (): Promise<void> => {
output = "";
const active = spawn("pnpm", ["--filter", "@openwork/enterprise-mcp-mock-server", "exec", "tsx", runner], {
cwd: REPO_ROOT,
detached: true,
env: {
...process.env,
PORT: String(port),
PROFILE_ID: profileId,
...(options.fault !== undefined ? { ACTIVE_FAULT_ID: options.fault } : {}),
OAUTH_CLIENT_SECRET: options.oauthClientSecret ?? "enterprise-mcp-eval-client-secret",
OAUTH_REDIRECT_URIS: redirectUris.join(","),
},
stdio: ["ignore", "pipe", "pipe"],
});
child = active;
active.stdout?.on("data", (chunk: Buffer) => {
output += String(chunk);
});
active.stderr?.on("data", (chunk: Buffer) => {
output += String(chunk);
});
mcpUrl = await waitForEnterpriseHealth(url, () => output, active);
};
await boot();
const stop = async (): Promise<void> => {
const active = child;
child = null;
await stopProcessGroup(active);
};
return {
url,
mcpUrl,
async requests() {
return [];
},
async toolCalls(opts = {}) {
if ((opts.atLeast ?? 0) > 0) await sleep(opts.timeoutMs ?? 120_000);
return [];
},
async agentRequests(opts = {}) {
if ((opts.atLeast ?? 0) > 0) await sleep(opts.timeoutMs ?? 120_000);
return [];
},
async handshakes(opts = {}) {
if ((opts.atLeast ?? 0) > 0) await sleep(opts.timeoutMs ?? 120_000);
return [];
},
async authorizeRequestSince(iso) {
throw new Error(`Enterprise profile request logs do not retain OAuth query parameters after ${iso}.`);
},
async configureOAuthRedirectUris(nextRedirectUris) {
await stop();
redirectUris = [...nextRedirectUris];
await boot();
},
async resetOAuth() {
await stop();
await boot();
},
stop,
[Symbol.asyncDispose]: stop,
};
}
export async function startMockMcp(options: StartMockMcpOptions = {}): Promise<MockMcpHandle> {
if (options.profileId) return startEnterpriseProfileMock(options);
const port = options.port ?? 3979;
const externalUrl = options.publicUrl ? trimTrailingSlashes(options.publicUrl.trim()) : undefined;
const localUrl = `http://127.0.0.1:${port}`;
const url = externalUrl || localUrl;
let child: ChildProcess | null = null;
let output = "";
if (!externalUrl) {
child = spawn(process.execPath, [options.scriptPath ?? join(REPO_ROOT, "scripts", "mock-oauth-mcp-server.mjs")], {
cwd: REPO_ROOT,
env: {
...process.env,
HOST: "0.0.0.0",
PORT: String(port),
ISSUER: options.issuer ?? url,
AUTO_APPROVE: "1",
...(options.allowUnauthenticatedMcp ? { MOCK_ALLOW_UNAUTHENTICATED_MCP: "1" } : {}),
...(options.extraToolCount ? { MOCK_EXTRA_TOOL_COUNT: String(options.extraToolCount) } : {}),
},
stdio: ["ignore", "pipe", "pipe"],
});
child.stdout?.on("data", (chunk: Buffer) => {
output += String(chunk);
});
child.stderr?.on("data", (chunk: Buffer) => {
output += String(chunk);
});
}
await waitForHealth(url, () => output, child);
if (options.agentWorkloads) {
const response = await fetch(`${url}/admin/agent-workloads`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ workloads: options.agentWorkloads }),
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) {
throw new Error(`Mock agent workload configuration failed: HTTP ${response.status} ${(await response.text()).slice(0, 500)}`);
}
}
const requests = async (): Promise<MockAuthorizeRequest[]> => {
const response = await fetch(`${url}/requests`, { signal: AbortSignal.timeout(15_000) });
const body: unknown = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Mock request log failed: HTTP ${response.status} ${JSON.stringify(body).slice(0, 500)}`);
return parseRequests(body);
};
const stop = async (): Promise<void> => {
const active = child;
child = null;
await stopChild(active);
};
const rawEntries = async (): Promise<Record<string, unknown>[]> => {
const response = await fetch(`${url}/requests`, { signal: AbortSignal.timeout(15_000) });
const body: unknown = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Mock request log failed: HTTP ${response.status}`);
return isRecord(body) && Array.isArray(body.requests) ? body.requests.filter(isRecord) : [];
};
const readToolCalls = async (name?: string, sinceIso?: string): Promise<MockToolCall[]> => {
const calls: MockToolCall[] = [];
for (const entry of await rawEntries()) {
if (!Array.isArray(entry.toolCalls)) continue;
const at = typeof entry.at === "string" ? entry.at : "";
if (sinceIso && at < sinceIso) continue;
for (const call of entry.toolCalls) {
if (!isRecord(call) || typeof call.name !== "string") continue;
if (name && call.name !== name) continue;
calls.push({
name: call.name,
args: isRecord(call.args) ? call.args : {},
tokenId: typeof call.tokenId === "string" ? call.tokenId : null,
at,
});
}
}
return calls;
};
const readAgentRequests = async (promptMarker?: string, sinceIso?: string): Promise<MockAgentRequest[]> => {
const completions: MockAgentRequest[] = [];
for (const entry of await rawEntries()) {
if (!isRecord(entry.agentCompletion)) continue;
const completion = entry.agentCompletion;
const at = typeof entry.at === "string" ? entry.at : "";
if (sinceIso && at < sinceIso) continue;
const kind = completion.kind;
if (kind !== "utility" && kind !== "tool" && kind !== "final" && kind !== "error") continue;
const marker = typeof completion.promptMarker === "string" ? completion.promptMarker : null;
if (promptMarker && marker !== promptMarker) continue;
if (typeof completion.model !== "string"
|| !Array.isArray(completion.matchedMarkers)
|| typeof completion.completedTools !== "number") continue;
completions.push({
model: completion.model,
promptMarker: marker,
matchedMarkers: completion.matchedMarkers.filter((value): value is string => typeof value === "string"),
completedTools: completion.completedTools,
kind,
toolName: typeof completion.toolName === "string" ? completion.toolName : null,
arguments: isRecord(completion.arguments) ? completion.arguments : {},
at,
});
}
return completions;
};
const readHandshakes = async (sinceIso?: string): Promise<MockAuthorizeRequest[]> => {
const handshakes: MockAuthorizeRequest[] = [];
for (const entry of await rawEntries()) {
if (!Array.isArray(entry.rpcMethods) || !entry.rpcMethods.includes("initialize")) continue;
const request = parseRequest(entry);
if (!request || (sinceIso && request.at < sinceIso)) continue;
handshakes.push(request);
}
return handshakes;
};
return {
url,
mcpUrl: `${url}/mcp`,
requests,
async toolCalls(opts = {}) {
const wanted = opts.atLeast ?? 0;
if (wanted <= 0) return readToolCalls(opts.name, opts.sinceIso);
// The engine invokes tools asynchronously after the model decides to, so
// poll rather than read once.
const deadline = Date.now() + (opts.timeoutMs ?? 120_000);
let calls = await readToolCalls(opts.name, opts.sinceIso);
while (calls.length < wanted && Date.now() < deadline) {
await sleep(1_000);
// A slow or aborted log read is a retryable poll attempt, not the
// verdict; only the deadline decides.
calls = await readToolCalls(opts.name, opts.sinceIso).catch(() => calls);
}
return calls;
},
async agentRequests(opts = {}) {
const wanted = opts.atLeast ?? 0;
if (wanted <= 0) return readAgentRequests(opts.promptMarker, opts.sinceIso);
const deadline = Date.now() + (opts.timeoutMs ?? 120_000);
let completions = await readAgentRequests(opts.promptMarker, opts.sinceIso);
while (completions.length < wanted && Date.now() < deadline) {
await sleep(500);
completions = await readAgentRequests(opts.promptMarker, opts.sinceIso).catch(() => completions);
}
return completions;
},
async handshakes(opts = {}) {
const wanted = opts.atLeast ?? 0;
if (wanted <= 0) return readHandshakes(opts.sinceIso);
const deadline = Date.now() + (opts.timeoutMs ?? 120_000);
let handshakes: MockAuthorizeRequest[] = [];
while (handshakes.length < wanted && Date.now() < deadline) {
try {
handshakes = await readHandshakes(opts.sinceIso);
} catch {
// A bounded request-log read can fail transiently while a remote mock recovers.
}
if (handshakes.length < wanted) await sleep(1_000);
}
return handshakes;
},
async authorizeRequestSince(iso, opts = {}) {
const deadline = Date.now() + (opts.timeoutMs ?? 60_000);
while (Date.now() < deadline) {
const request = (await requests()).find((entry) => entry.method === "GET" && entry.path === "/authorize" && entry.at >= iso);
if (request) return { ...request, params: new URL(request.url, url).searchParams };
await sleep(500);
}
throw new Error(`No GET /authorize reached the mock IdP after ${iso}. Output: ${output.slice(-1_000)}`);
},
async configureOAuthRedirectUris() {
throw new Error("The legacy mock must receive preregistered redirect URIs before startup.");
},
async resetOAuth() {
const response = await fetch(`${url}/admin/expire-oauth-tokens`, { method: "POST" });
if (!response.ok) throw new Error(`Mock OAuth reset failed: HTTP ${response.status}`);
},
stop,
[Symbol.asyncDispose]: stop,
};
}
@@ -0,0 +1,3 @@
export function notImplemented(name: string, how: string): never {
throw new Error(`${name} not implemented: ${how}`);
}
+806
View File
@@ -0,0 +1,806 @@
import { createServer } from "node:http";
import type { IncomingMessage, Server, ServerResponse } from "node:http";
import { setTimeout as sleep } from "node:timers/promises";
import { trimTrailingSlashes } from "./strings.ts";
export type ReleasePlatform = "mac-arm64" | "mac-x64" | "win-x64" | "linux-x64" | "linux-arm64";
export type ReleaseDistribution = "public" | "enterprise" | "cloud";
export type ReleaseLabErrorCode =
| "release_asset_missing"
| "release_asset_not_installer_capable"
| "release_asset_propagation_lag"
| "release_asset_unavailable"
| "release_feed_cache_stale"
| "release_host_denied"
| "release_probe_unexpected_success";
export type ReleaseAssetFault =
| { kind: "status"; status: number; message?: string }
| { kind: "slow-chunked"; chunkSize: number; delayMs: number };
export interface ReleaseAssetInput {
platform: ReleasePlatform;
distribution?: ReleaseDistribution;
fileName?: string;
body?: string | Uint8Array;
contentType?: string;
installerCapable?: boolean;
rewrittenFileName?: string;
fault?: ReleaseAssetFault;
}
export interface ReleaseVersionInput {
version: string;
publishedAt?: string;
prerelease?: boolean;
installerCapable?: boolean;
assets?: ReleaseAssetInput[];
}
export interface ReleaseAsset {
platform: ReleasePlatform;
distribution: ReleaseDistribution;
fileName: string;
body: Uint8Array;
size: number;
contentType: string;
installerCapable: boolean;
rewrittenFileName: string | null;
fault: ReleaseAssetFault | null;
}
export interface ReleaseVersion {
version: string;
tagName: string;
publishedAt: string;
prerelease: boolean;
installerCapable: boolean;
assets: ReleaseAsset[];
}
export interface DenAppVersionMetadataFixture {
minAppVersion: string;
latestAppVersion: string;
publishedDesktopVersions: string[];
}
export interface CatalogOptions {
platforms?: ReleasePlatform[];
distribution?: ReleaseDistribution;
preInstallerVersions?: string[];
}
export interface ResolveAllowedUpdateInput {
catalog: ReleaseVersion[];
currentVersion: string;
allowedVersions?: string[] | null;
}
export type ReleaseFeedProbe =
| { kind: "asset"; version: string; platform: ReleasePlatform; allowedHosts?: string[] }
| { kind: "cache"; expectedVersion: string }
| { kind: "host"; url?: string; version?: string; platform?: ReleasePlatform; allowedHosts: string[] };
export interface ReleaseFeedLabConfig {
catalog: ReleaseVersionInput[];
initialCatalog?: ReleaseVersionInput[];
allowedVersions?: string[] | null;
cacheTtlMs?: number;
staleUntil?: number | Date;
repo?: string;
actionableProbe?: ReleaseFeedProbe;
}
export interface ReleaseSnapshot {
catalog: ReleaseVersion[];
metadata: DenAppVersionMetadataFixture;
stale: boolean;
cachedUntil: number | null;
}
export interface ReleaseAssetUrlInput {
baseUrl: string;
version: string;
fileName: string;
repo?: string;
}
type ComparableVersion = {
release: number[];
prerelease: string[];
};
const DEFAULT_REPO = "different-ai/openwork";
const DEFAULT_PLATFORMS: ReleasePlatform[] = ["mac-arm64", "mac-x64", "win-x64", "linux-x64", "linux-arm64"];
const DEFAULT_DISTRIBUTION: ReleaseDistribution = "enterprise";
const textEncoder = new TextEncoder();
export class ReleaseLabError extends Error {
code: ReleaseLabErrorCode;
action: string;
status: number | null;
constructor(code: ReleaseLabErrorCode, message: string, action: string, status: number | null = null) {
super(message);
this.name = code;
this.code = code;
this.action = action;
this.status = status;
}
}
export function isReleaseLabError(value: unknown): value is ReleaseLabError {
return value instanceof ReleaseLabError;
}
function parseComparableVersion(value: string): ComparableVersion | null {
const normalized = value.trim().replace(/^v/i, "");
if (!normalized) return null;
const [withoutBuild] = normalized.split("+", 1);
if (!withoutBuild) return null;
const [releasePart, prereleasePart = ""] = withoutBuild.split("-", 2);
const release = releasePart.split(".").map((part) => Number(part));
if (release.length !== 3 || release.some((part) => !Number.isInteger(part) || part < 0)) {
return null;
}
const prerelease = prereleasePart
.split(".")
.flatMap((part) => {
const trimmed = part.trim();
return trimmed ? [trimmed] : [];
});
return { release, prerelease };
}
function comparePrerelease(left: string[], right: string[]): number {
if (!left.length && !right.length) return 0;
if (!left.length) return 1;
if (!right.length) return -1;
const count = Math.max(left.length, right.length);
for (let index = 0; index < count; index += 1) {
const leftPart = left[index];
const rightPart = right[index];
if (leftPart === undefined) return -1;
if (rightPart === undefined) return 1;
const leftNumeric = /^\d+$/.test(leftPart) ? Number(leftPart) : null;
const rightNumeric = /^\d+$/.test(rightPart) ? Number(rightPart) : null;
if (leftNumeric !== null && rightNumeric !== null) {
if (leftNumeric !== rightNumeric) return leftNumeric < rightNumeric ? -1 : 1;
continue;
}
if (leftNumeric !== null) return -1;
if (rightNumeric !== null) return 1;
const comparison = leftPart.localeCompare(rightPart);
if (comparison !== 0) return comparison < 0 ? -1 : 1;
}
return 0;
}
export function normalizeReleaseVersion(value: string): string | null {
const normalized = value.trim().replace(/^v/i, "");
return /^\d+\.\d+\.\d+$/.test(normalized) ? normalized : null;
}
export function compareReleaseVersions(left: string, right: string): number | null {
const parsedLeft = parseComparableVersion(left);
const parsedRight = parseComparableVersion(right);
if (!parsedLeft || !parsedRight) return null;
for (let index = 0; index < 3; index += 1) {
const leftPart = parsedLeft.release[index];
const rightPart = parsedRight.release[index];
if (leftPart === undefined || rightPart === undefined) return null;
if (leftPart !== rightPart) return leftPart < rightPart ? -1 : 1;
}
return comparePrerelease(parsedLeft.prerelease, parsedRight.prerelease);
}
function requireVersion(value: string): string {
const normalized = normalizeReleaseVersion(value);
if (!normalized) {
throw new ReleaseLabError(
"release_asset_missing",
`Release version ${JSON.stringify(value)} is not a stable x.y.z version.`,
"Use a stable desktop version tag like 0.17.40 or v0.17.40.",
);
}
return normalized;
}
function versionTag(value: string): string {
return `v${requireVersion(value)}`;
}
function platformFilePart(platform: ReleasePlatform): string {
if (platform === "linux-x64") return "linux-x86_64";
return platform;
}
export function desktopReleaseAssetFileName(
version: string,
platform: ReleasePlatform,
distribution: ReleaseDistribution = DEFAULT_DISTRIBUTION,
): string {
const normalized = requireVersion(version);
const prefix = distribution === "public" ? "openwork" : `openwork-${distribution}`;
if (platform === "win-x64") return `${prefix}-win-x64-${normalized}.exe`;
if (platform === "mac-arm64" || platform === "mac-x64") return `${prefix}-${platform}-${normalized}.dmg`;
return `${prefix}-${platformFilePart(platform)}-${normalized}.AppImage`;
}
function contentTypeForPlatform(platform: ReleasePlatform): string {
if (platform.startsWith("mac-")) return "application/x-apple-diskimage";
if (platform === "win-x64") return "application/vnd.microsoft.portable-executable";
return "application/vnd.appimage";
}
function assetBody(input: ReleaseAssetInput, version: string): Uint8Array {
if (typeof input.body === "string") return textEncoder.encode(input.body);
if (input.body) return input.body;
return textEncoder.encode(`openwork release ${version} ${input.platform}\n`);
}
function normalizeAsset(input: ReleaseAssetInput, version: string, installerCapable: boolean): ReleaseAsset {
const distribution = input.distribution ?? DEFAULT_DISTRIBUTION;
const fileName = input.fileName ?? desktopReleaseAssetFileName(version, input.platform, distribution);
const body = assetBody(input, version);
return {
platform: input.platform,
distribution,
fileName,
body,
size: body.byteLength,
contentType: input.contentType ?? contentTypeForPlatform(input.platform),
installerCapable: input.installerCapable ?? installerCapable,
rewrittenFileName: input.rewrittenFileName ?? null,
fault: input.fault ?? null,
};
}
export function normalizeReleaseCatalog(input: ReleaseVersionInput[]): ReleaseVersion[] {
const catalog = input.map((entry, index) => {
const version = requireVersion(entry.version);
const installerCapable = entry.installerCapable ?? true;
const assetsInput = entry.assets ?? DEFAULT_PLATFORMS.map((platform) => ({ platform }));
return {
version,
tagName: versionTag(version),
publishedAt: entry.publishedAt ?? new Date(Date.UTC(2026, 0, 1, 12, 0, index)).toISOString(),
prerelease: entry.prerelease ?? false,
installerCapable,
assets: assetsInput.map((asset) => normalizeAsset(asset, version, installerCapable)),
};
});
return sortReleaseCatalog(catalog);
}
export function createReleaseCatalog(versions: string[], options: CatalogOptions = {}): ReleaseVersionInput[] {
const preInstallerVersions = options.preInstallerVersions ?? [];
const platforms = options.platforms ?? DEFAULT_PLATFORMS;
const distribution = options.distribution ?? DEFAULT_DISTRIBUTION;
return versions.map((version) => {
const normalized = requireVersion(version);
const installerCapable = !preInstallerVersions.some((entry) => compareReleaseVersions(entry, normalized) === 0);
return {
version: normalized,
installerCapable,
assets: platforms.map((platform) => ({ platform, distribution, installerCapable })),
};
});
}
export function sortReleaseCatalog(catalog: ReleaseVersion[]): ReleaseVersion[] {
return [...catalog].sort((left, right) => compareReleaseVersions(left.version, right.version) ?? 0);
}
export function releaseCatalogMetadata(catalog: ReleaseVersion[]): DenAppVersionMetadataFixture {
const sorted = sortReleaseCatalog(catalog);
const versions = sorted.map((entry) => entry.version);
const minAppVersion = versions[0] ?? "0.0.0";
const latestAppVersion = versions.at(-1) ?? "0.0.0";
return { minAppVersion, latestAppVersion, publishedDesktopVersions: versions };
}
export function allowedReleaseCatalog(catalog: ReleaseVersion[], allowedVersions?: string[] | null): ReleaseVersion[] {
const sorted = sortReleaseCatalog(catalog);
if (!allowedVersions || allowedVersions.length === 0) return sorted;
return sorted.filter((release) => allowedVersions.some((allowed) => compareReleaseVersions(release.version, allowed) === 0));
}
export function highestAllowedVersion(catalog: ReleaseVersion[], allowedVersions?: string[] | null): string | null {
return allowedReleaseCatalog(catalog, allowedVersions).at(-1)?.version ?? null;
}
export function resolveAllowedUpdate(input: ResolveAllowedUpdateInput): string | null {
const currentVersion = requireVersion(input.currentVersion);
return allowedReleaseCatalog(input.catalog, input.allowedVersions)
.filter((release) => compareReleaseVersions(release.version, currentVersion) === 1)
.at(-1)?.version ?? null;
}
function cleanBaseUrl(value: string): string {
return trimTrailingSlashes(value.trim());
}
export function buildReleaseAssetUrl(input: ReleaseAssetUrlInput): string {
const repo = input.repo ?? DEFAULT_REPO;
return `${cleanBaseUrl(input.baseUrl)}/${repo}/releases/download/${versionTag(input.version)}/${encodeURIComponent(input.fileName)}`;
}
export function simulateBrowserRenamedFileName(fileName: string, suffix = " (1)"): string {
const extensionIndex = fileName.lastIndexOf(".");
if (extensionIndex <= 0) return `${fileName}${suffix}`;
return `${fileName.slice(0, extensionIndex)}${suffix}${fileName.slice(extensionIndex)}`;
}
function stripBrowserRenameSuffix(fileName: string): string {
return fileName.replace(/ \(\d+\)(?=\.[^.]+$)/, "");
}
export function clientFileNameMatchesAsset(asset: ReleaseAsset, clientFileName: string): boolean {
const normalized = clientFileName.trim();
return normalized === asset.fileName
|| normalized === asset.rewrittenFileName
|| stripBrowserRenameSuffix(normalized) === asset.fileName;
}
function hostFromUrl(value: string): string {
try {
return new URL(value).host;
} catch {
return "";
}
}
export function assertReleaseHostAllowed(url: string, allowedHosts: string[]): void {
const host = hostFromUrl(url);
if (allowedHosts.includes(host)) return;
throw new ReleaseLabError(
"release_host_denied",
`Release download host ${host || "<invalid>"} is not in the allowed outbound host list.`,
"Add the release host to enterprise outbound allow rules or configure an internal artifact mirror.",
);
}
export async function fetchAssetOrThrow(url: string, allowedHosts?: string[]): Promise<Uint8Array> {
if (allowedHosts) assertReleaseHostAllowed(url, allowedHosts);
const response = await fetch(url);
if (!response.ok) throw actionableHttpError(response.status, url);
return new Uint8Array(await response.arrayBuffer());
}
function actionableHttpError(status: number, url: string): ReleaseLabError {
if (status === 504) {
return new ReleaseLabError(
"release_asset_propagation_lag",
`Release asset ${url} returned HTTP 504 while the release was still propagating.`,
"Retry against the mirrored artifact host or wait for release propagation before offering the update.",
status,
);
}
return new ReleaseLabError(
"release_asset_unavailable",
`Release asset ${url} returned HTTP ${status}.`,
"Surface the failed artifact URL and status to the user instead of silently reporting no update.",
status,
);
}
function cacheStaleError(expectedVersion: string): ReleaseLabError {
return new ReleaseLabError(
"release_feed_cache_stale",
`The release feed cache has not exposed ${versionTag(expectedVersion)} yet.`,
"Bypass or expire the release metadata cache before forcing an update during an enterprise rollout.",
);
}
function sendJson(res: ServerResponse, status: number, body: unknown): void {
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
res.end(`${JSON.stringify(body, null, 2)}\n`);
}
function sendText(res: ServerResponse, status: number, body: string, headers: Record<string, string> = {}): void {
res.writeHead(status, { "content-type": "text/plain; charset=utf-8", ...headers });
res.end(body);
}
function serverBaseUrl(server: Server): string {
const address = server.address();
if (typeof address === "object" && address !== null && typeof address.port === "number") {
return `http://127.0.0.1:${address.port}`;
}
throw new ReleaseLabError(
"release_asset_unavailable",
"Release lab server did not expose a TCP port.",
"Restart the hermetic release lab fixture before running update assertions.",
);
}
function decodePathSegments(url: URL): string[] {
return url.pathname.split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
}
function repoFromSegments(segments: string[]): string | null {
const releaseIndex = segments.indexOf("releases");
if (releaseIndex >= 2) return `${segments[releaseIndex - 2]}/${segments[releaseIndex - 1]}`;
return null;
}
function fileNameFromSegments(segments: string[]): string | null {
const downloadIndex = segments.indexOf("download");
if (downloadIndex < 0) return null;
const fileName = segments.slice(downloadIndex + 2).join("/");
return fileName || null;
}
function tagFromSegments(segments: string[]): string | null {
const downloadIndex = segments.indexOf("download");
if (downloadIndex < 0) return null;
const tag = segments[downloadIndex + 1];
return tag || null;
}
function latestDownloadFileNameFromSegments(segments: string[]): string | null {
const releasesIndex = segments.indexOf("releases");
if (releasesIndex < 0) return null;
if (segments[releasesIndex + 1] !== "latest" || segments[releasesIndex + 2] !== "download") return null;
const fileName = segments.slice(releasesIndex + 3).join("/");
return fileName || null;
}
function isGitHubReleasesApi(segments: string[]): boolean {
if (segments.length === 4 && segments[0] === "repos" && segments[3] === "releases") return true;
return segments.length === 5 && segments[0] === "api" && segments[1] === "repos" && segments[4] === "releases";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export class ReleaseFeedLab {
private catalog: ReleaseVersion[];
private initialCatalog: ReleaseVersion[];
private cachedSnapshot: ReleaseSnapshot | null;
private server: Server | null;
private baseUrlValue: string | null;
private readonly allowedVersions: string[] | null;
private readonly cacheTtlMs: number;
private readonly staleUntilMs: number | null;
private readonly repo: string;
private readonly actionableProbe: ReleaseFeedProbe | null;
constructor(config: ReleaseFeedLabConfig) {
this.catalog = normalizeReleaseCatalog(config.catalog);
this.initialCatalog = normalizeReleaseCatalog(config.initialCatalog ?? config.catalog);
this.cachedSnapshot = null;
this.server = null;
this.baseUrlValue = null;
this.allowedVersions = config.allowedVersions ? [...config.allowedVersions] : null;
this.cacheTtlMs = Math.max(0, config.cacheTtlMs ?? 0);
this.staleUntilMs = config.staleUntil instanceof Date ? config.staleUntil.getTime() : config.staleUntil ?? null;
this.repo = config.repo ?? DEFAULT_REPO;
this.actionableProbe = config.actionableProbe ?? null;
}
get baseUrl(): string {
if (!this.baseUrlValue) {
throw new ReleaseLabError(
"release_asset_unavailable",
"Release lab server has not been started.",
"Call feed.start() before constructing hermetic artifact URLs.",
);
}
return this.baseUrlValue;
}
updateCatalog(catalog: ReleaseVersionInput[]): void {
this.catalog = normalizeReleaseCatalog(catalog);
}
clearCache(): void {
this.cachedSnapshot = null;
}
snapshot(now = Date.now()): ReleaseSnapshot {
const staleByKnob = this.staleUntilMs !== null && now < this.staleUntilMs;
if (staleByKnob) return this.makeSnapshot(this.initialCatalog, true, this.staleUntilMs);
if (this.cachedSnapshot && this.cachedSnapshot.cachedUntil !== null && now < this.cachedSnapshot.cachedUntil) {
return this.cachedSnapshot;
}
const cachedUntil = this.cacheTtlMs > 0 ? now + this.cacheTtlMs : null;
const snapshot = this.makeSnapshot(this.catalog, false, cachedUntil);
if (cachedUntil !== null) this.cachedSnapshot = snapshot;
return snapshot;
}
metadata(now = Date.now()): DenAppVersionMetadataFixture {
return this.snapshot(now).metadata;
}
highestAllowedVersion(now = Date.now()): string | null {
return highestAllowedVersion(this.snapshot(now).catalog, this.allowedVersions);
}
resolveAllowedUpdate(currentVersion: string, now = Date.now()): string | null {
return resolveAllowedUpdate({ catalog: this.snapshot(now).catalog, currentVersion, allowedVersions: this.allowedVersions });
}
assetUrl(version: string, platform: ReleasePlatform, distribution: ReleaseDistribution = DEFAULT_DISTRIBUTION): string {
const asset = this.resolveAsset(version, platform, distribution);
return buildReleaseAssetUrl({ baseUrl: this.baseUrl, repo: this.repo, version, fileName: asset.fileName });
}
githubAssetUrl(version: string, platform: ReleasePlatform, distribution: ReleaseDistribution = DEFAULT_DISTRIBUTION): string {
const asset = this.resolveAsset(version, platform, distribution);
return buildReleaseAssetUrl({ baseUrl: "https://github.com", repo: this.repo, version, fileName: asset.fileName });
}
localizeGitHubUrl(url: string): string {
const parsed = new URL(url);
return `${this.baseUrl}${parsed.pathname}`;
}
resolveAsset(version: string, platform: ReleasePlatform, distribution: ReleaseDistribution = DEFAULT_DISTRIBUTION): ReleaseAsset {
const normalized = requireVersion(version);
const release = this.snapshot().catalog.find((entry) => compareReleaseVersions(entry.version, normalized) === 0);
if (!release) {
throw new ReleaseLabError(
"release_asset_missing",
`Release ${versionTag(normalized)} is not present in the hermetic catalog.`,
"Publish the requested version in the release feed before allowing clients to target it.",
);
}
const asset = release.assets.find((entry) => entry.platform === platform && entry.distribution === distribution)
?? release.assets.find((entry) => entry.platform === platform);
if (!asset) {
throw new ReleaseLabError(
"release_asset_missing",
`Release ${versionTag(normalized)} has no ${platform} artifact.`,
"Keep the update hidden for this platform until the matching installer asset exists.",
);
}
return asset;
}
assertInstallableAsset(version: string, platform: ReleasePlatform, distribution: ReleaseDistribution = DEFAULT_DISTRIBUTION): ReleaseAsset {
const asset = this.resolveAsset(version, platform, distribution);
if (asset.installerCapable) return asset;
throw new ReleaseLabError(
"release_asset_not_installer_capable",
`Release ${versionTag(version)} has ${platform} assets that predate installer-capable desktop releases.`,
"Approve a newer desktop version with signed installer assets, or mount a compatible artifact before exposing the install path.",
);
}
resolveClientDownloadedAsset(version: string, platform: ReleasePlatform, clientFileName: string): ReleaseAsset {
const release = this.snapshot().catalog.find((entry) => compareReleaseVersions(entry.version, version) === 0);
const asset = release?.assets.find((entry) => entry.platform === platform && clientFileNameMatchesAsset(entry, clientFileName));
if (!asset) {
throw new ReleaseLabError(
"release_asset_missing",
`Downloaded filename ${JSON.stringify(clientFileName)} did not resolve to a ${platform} release asset.`,
"Resolve install metadata from the URL or bundle manifest, not solely from a browser-controlled filename.",
);
}
return asset;
}
async start(): Promise<this> {
if (this.server) return this;
const server = createServer((req, res) => {
void this.handleRequest(req, res);
});
await new Promise<void>((resolve, reject) => {
const onError = (error: Error) => reject(error);
server.once("error", onError);
server.listen(0, "127.0.0.1", () => {
server.off("error", onError);
resolve();
});
});
this.server = server;
this.baseUrlValue = serverBaseUrl(server);
return this;
}
async stop(): Promise<void> {
const server = this.server;
if (!server) return;
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) reject(error);
else resolve();
});
});
this.server = null;
this.baseUrlValue = null;
}
async [Symbol.asyncDispose](): Promise<void> {
await this.stop();
}
async runActionableProbe(): Promise<never> {
const probe = this.actionableProbe;
if (!probe) {
throw new ReleaseLabError(
"release_probe_unexpected_success",
"No degraded release-feed probe was configured.",
"Configure the lab with a 504 asset, stale cache, or denied-host probe before asserting actionable errors.",
);
}
if (probe.kind === "cache") {
const metadata = this.metadata();
if (!metadata.publishedDesktopVersions.some((version) => compareReleaseVersions(version, probe.expectedVersion) === 0)) {
throw cacheStaleError(probe.expectedVersion);
}
throw new ReleaseLabError(
"release_probe_unexpected_success",
`Expected ${versionTag(probe.expectedVersion)} to be hidden by cache lag, but it was visible.`,
"Tighten the cache-lag fixture before asserting stale-feed behavior.",
);
}
if (probe.kind === "host") {
const url = probe.url
?? this.githubAssetUrl(probe.version ?? this.metadata().latestAppVersion, probe.platform ?? "win-x64");
assertReleaseHostAllowed(url, probe.allowedHosts);
throw new ReleaseLabError(
"release_probe_unexpected_success",
`Expected ${url} to be denied by host policy, but it was allowed.`,
"Use an allowed-host list that excludes the release host for this degraded-flow frame.",
);
}
const url = this.assetUrl(probe.version, probe.platform);
await fetchAssetOrThrow(url, probe.allowedHosts);
throw new ReleaseLabError(
"release_probe_unexpected_success",
`Expected ${url} to fail, but it downloaded successfully.`,
"Attach a fault knob to the release asset before asserting propagation failures.",
);
}
private makeSnapshot(catalog: ReleaseVersion[], stale: boolean, cachedUntil: number | null): ReleaseSnapshot {
const allowedCatalog = allowedReleaseCatalog(catalog, this.allowedVersions);
return {
catalog: allowedCatalog,
metadata: releaseCatalogMetadata(allowedCatalog.length > 0 ? allowedCatalog : catalog),
stale,
cachedUntil,
};
}
private githubReleasesJson(): unknown[] {
return this.snapshot().catalog.slice().reverse().map((release) => ({
url: `${this.baseUrl}/repos/${this.repo}/releases/tags/${release.tagName}`,
html_url: `${this.baseUrl}/${this.repo}/releases/tag/${release.tagName}`,
tag_name: release.tagName,
name: release.tagName,
draft: false,
prerelease: release.prerelease,
published_at: release.publishedAt,
assets: release.assets.map((asset, index) => ({
id: `${release.version}-${index}`,
name: asset.fileName,
label: asset.fileName,
state: "uploaded",
size: asset.size,
content_type: asset.contentType,
browser_download_url: buildReleaseAssetUrl({ baseUrl: this.baseUrl, repo: this.repo, version: release.version, fileName: asset.fileName }),
})),
}));
}
private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
try {
const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
const segments = decodePathSegments(requestUrl);
if (requestUrl.pathname === "/v1/app-version") {
sendJson(res, 200, this.metadata());
return;
}
if (isGitHubReleasesApi(segments)) {
sendJson(res, 200, this.githubReleasesJson());
return;
}
if (requestUrl.pathname.endsWith(".yml")) {
this.sendElectronUpdaterManifest(res, segments);
return;
}
const latestFileName = latestDownloadFileNameFromSegments(segments);
if (latestFileName) {
const latestVersion = this.metadata().latestAppVersion;
await this.sendAsset(res, latestVersion, latestFileName);
return;
}
const tag = tagFromSegments(segments);
const fileName = fileNameFromSegments(segments);
if (tag && fileName) {
await this.sendAsset(res, tag, fileName);
return;
}
sendJson(res, 404, { error: "not_found", path: requestUrl.pathname, repo: repoFromSegments(segments) });
} catch (error) {
const body = isReleaseLabError(error)
? { error: error.code, message: error.message, action: error.action }
: { error: "release_lab_internal_error", message: error instanceof Error ? error.message : String(error) };
sendJson(res, 500, body);
}
}
private sendElectronUpdaterManifest(res: ServerResponse, segments: string[]): void {
const tag = tagFromSegments(segments) ?? this.metadata().latestAppVersion;
const version = requireVersion(tag);
const asset = this.resolveAsset(version, "mac-arm64", "enterprise");
const url = buildReleaseAssetUrl({ baseUrl: this.baseUrl, repo: this.repo, version, fileName: asset.fileName });
sendText(res, 200, `version: ${version}\nfiles:\n - url: ${asset.fileName}\n sha512: release-lab-fixture\n size: ${asset.size}\npath: ${asset.fileName}\nsha512: release-lab-fixture\nreleaseDate: ${this.snapshot().catalog.find((entry) => entry.version === version)?.publishedAt ?? new Date(0).toISOString()}\n`, {
"x-release-lab-asset-url": url,
});
}
private async sendAsset(res: ServerResponse, version: string, fileName: string): Promise<void> {
const normalizedVersion = requireVersion(version);
const release = this.snapshot().catalog.find((entry) => compareReleaseVersions(entry.version, normalizedVersion) === 0);
const asset = release?.assets.find((entry) => entry.fileName === fileName);
if (!asset) {
sendJson(res, 404, { error: "release_asset_missing", version: normalizedVersion, fileName });
return;
}
if (asset.fault?.kind === "status") {
sendJson(res, asset.fault.status, { error: "release_asset_fault", message: asset.fault.message ?? `HTTP ${asset.fault.status}` });
return;
}
const headers = {
"content-type": asset.contentType,
"content-length": String(asset.size),
"connection": "close",
"content-disposition": `attachment; filename="${(asset.rewrittenFileName ?? asset.fileName).replace(/["\\]/g, "-")}"`,
"cache-control": "private, max-age=300",
"x-release-lab-version": normalizedVersion,
"x-release-lab-installer-capable": String(asset.installerCapable),
};
res.writeHead(200, headers);
if (asset.fault?.kind === "slow-chunked") {
for (let index = 0; index < asset.body.byteLength; index += asset.fault.chunkSize) {
res.write(asset.body.slice(index, index + asset.fault.chunkSize));
await sleep(asset.fault.delayMs);
}
res.end();
return;
}
res.end(asset.body);
}
}
export function denMetadataFromUnknown(value: unknown): DenAppVersionMetadataFixture | null {
if (!isRecord(value)) return null;
const minAppVersion = typeof value.minAppVersion === "string" ? value.minAppVersion : "";
const latestAppVersion = typeof value.latestAppVersion === "string" ? value.latestAppVersion : "";
const publishedDesktopVersions = Array.isArray(value.publishedDesktopVersions)
? value.publishedDesktopVersions.filter((entry): entry is string => typeof entry === "string")
: [];
if (!minAppVersion || !latestAppVersion) return null;
return { minAppVersion, latestAppVersion, publishedDesktopVersions };
}
+5
View File
@@ -0,0 +1,5 @@
export function trimTrailingSlashes(value: string): string {
let end = value.length;
while (end > 0 && value[end - 1] === "/") end -= 1;
return value.slice(0, end);
}
+184
View File
@@ -0,0 +1,184 @@
import { readFileSync } from "node:fs";
import { X509Certificate } from "node:crypto";
import assert from "node:assert/strict";
import { test } from "node:test";
import {
createBlipSchedule,
denyHostsFromOutboundManifest,
opensslCertificateCommands,
opensslFlavor,
outboundManifestFromUnknown,
parseClientHelloVersions,
resolveEgressProfileConfig,
startEgressLab,
} from "../src/egress.ts";
import { productDiagnosticsPrecondition } from "@openwork/behaviors";
import { matchVerdictExpectations } from "@openwork/matchers";
function hi(value: number): number {
return (value >> 8) & 0xff;
}
function lo(value: number): number {
return value & 0xff;
}
function clientHelloFixture(versions: number[]): Buffer {
const supportedVersions = versions.flatMap((version) => [hi(version), lo(version)]);
const extensions = supportedVersions.length > 0
? [0x00, 0x2b, 0x00, supportedVersions.length + 1, supportedVersions.length, ...supportedVersions]
: [];
const body = [
0x03,
0x03,
...Array.from({ length: 32 }, () => 0),
0x00,
0x00,
0x02,
0x00,
0x2f,
0x01,
0x00,
hi(extensions.length),
lo(extensions.length),
...extensions,
];
const handshakeLength = body.length;
const recordLength = handshakeLength + 4;
return Buffer.from([
0x16,
0x03,
0x01,
hi(recordLength),
lo(recordLength),
0x01,
0x00,
hi(handshakeLength),
lo(handshakeLength),
...body,
]);
}
test("ClientHello parser distinguishes TLS 1.2-only from TLS 1.3-capable clients", () => {
const tls12 = parseClientHelloVersions(clientHelloFixture([0x0303]));
if (tls12.kind !== "parsed") throw new Error(`unexpected parse result: ${tls12.kind}`);
assert.equal(tls12.offersTls13, false);
assert.deepEqual(tls12.supportedVersionLabels, ["TLSv1.2"]);
const tls13 = parseClientHelloVersions(clientHelloFixture([0x0304, 0x0303]));
if (tls13.kind !== "parsed") throw new Error(`unexpected parse result: ${tls13.kind}`);
assert.equal(tls13.offersTls13, true);
assert.deepEqual(tls13.supportedVersionLabels, ["TLSv1.3", "TLSv1.2"]);
});
test("profile config resolution normalizes defaults", () => {
const config = resolveEgressProfileConfig({
profile: "deny",
port: 0,
denyHosts: ["GitHub.com", "github.com.", ""],
denyMode: "blackhole",
slow: { totalBytes: 10, chunkBytes: 4, delayMs: 0, latencyMs: 1 },
blip: { route: "api/den", count: 2, fault: "reset" },
});
assert.equal(config.port, null);
assert.deepEqual(config.denyHosts, ["github.com"]);
assert.equal(config.denyMode, "blackhole");
assert.equal(config.slow.totalBytes, 10);
assert.equal(config.blip.route, "/api/den");
assert.equal(config.blip.count, 2);
assert.equal(config.blip.fault, "reset");
});
test("openssl argv construction includes CA, AIA, and fullchain prerequisites", () => {
const commands = opensslCertificateCommands({
dir: "/tmp/openwork-egress-test",
hostname: "localhost",
aiaUrl: "http://127.0.0.1:9000/__egress-lab/intermediate.der",
corporateIssuer: true,
});
const labels = commands.map((command) => command.label);
assert.deepEqual(labels, [
"root-key",
"root-csr",
"root-cert",
"intermediate-key",
"intermediate-csr",
"intermediate-cert",
"intermediate-der",
"leaf-key",
"leaf-csr",
"leaf-cert",
]);
assert.ok(commands.some((command) => command.args.includes("/CN=OpenWork Egress Lab Corporate Interception CA")));
assert.ok(commands.some((command) => command.args.includes("-extfile")));
const rootCert = commands.find((command) => command.label === "root-cert");
assert.ok(rootCert, "root-cert command must exist");
assert.ok(
rootCert.args.includes("-extfile"),
"root CA extensions must come from -extfile; openssl 1.1.1 ignores `req -x509 -addext`",
);
assert.ok(!rootCert.args.includes("-addext"), "root-cert must not depend on -addext");
});
test("openssl flavor probe returns a known value", async () => {
const flavor = await opensslFlavor();
assert.ok(["openssl", "libressl", "unknown"].includes(flavor));
});
test("deny-list seeding reads installer-critical hosts from the outbound manifest", () => {
const manifest = outboundManifestFromUnknown({
hosts: [
{ host: "github.com", kind: "fetched", components: ["installer"], requirement: "required-in-practice", blockedEffect: "installer dead" },
{ host: "release-assets.githubusercontent.com", kind: "redirect-target", components: ["installer"], requirement: "required-in-practice", blockedEffect: "download fails" },
{ host: "cdn.simpleicons.org", kind: "fetched", components: ["renderer"], requirement: "optional", blockedEffect: "icons missing" },
],
});
if (!manifest) throw new Error("manifest fixture did not parse");
assert.deepEqual(denyHostsFromOutboundManifest(manifest), ["github.com", "release-assets.githubusercontent.com"]);
});
test("blip schedule fires once, then stops", () => {
const schedule = createBlipSchedule({ route: "/v1/me", count: 1, fault: "401", status: 401, body: "proxy" });
const first = schedule.next("/v1/me");
const second = schedule.next("/v1/me");
assert.deepEqual(first, { kind: "status", status: 401, body: "proxy" });
assert.deepEqual(second, { kind: "pass" });
assert.equal(schedule.snapshot()[0]?.remaining, 0);
});
test("verdict matcher names injected faults and rejects vague output", () => {
assert.equal(matchVerdictExpectations("LIKELY TLS VERSION/HANDSHAKE FAULT: TLS 1.3 timed out while TLS 1.2 completed", "tls12-only").ok, true);
assert.equal(matchVerdictExpectations("endpoint_tls_handshake_timeout_tls12_comparison_failed: TLS 1.3 timed out and the runtime TLS 1.2 comparison also timed out", "tls12-only").ok, true);
assert.equal(matchVerdictExpectations("LIKELY MISSING INTERMEDIATE OR UNTRUSTED ROOT", "broken-chain").ok, true);
assert.equal(matchVerdictExpectations("LIKELY TLS INTERCEPTION: endpoint leaf is re-signed by Corporate CA", "intercept").ok, true);
assert.equal(matchVerdictExpectations("BLOCKED HOST / PROXY DENY: github.com is blocked", "deny").ok, true);
assert.deepEqual(matchVerdictExpectations("TypeError: fetch failed", ["tls12-only", "broken-chain"]), {
ok: false,
missing: ["tls12-only", "broken-chain"],
});
});
test("product diagnostics precondition skips clearly when Bun is unavailable", () => {
const reason = productDiagnosticsPrecondition({ PATH: "/definitely-no-bun-here" });
if (!reason) throw new Error("Expected a missing-Bun skip reason");
assert.ok(reason.includes("Bun is required"));
assert.ok(reason.includes("product-verdict egress proofs"));
});
test("generated egress lab CA certificates carry basicConstraints on every openssl build", async () => {
await using lab = await startEgressLab({ profile: "tls12-only" });
assert.ok(lab.rootPem, "lab must expose its root PEM");
const root = new X509Certificate(lab.rootPem);
// openssl 1.1.1 silently produced a non-CA root, which broke chain building
// only on runners shipping 1.1.1 (macos-14).
assert.equal(root.ca, true, `root must be a CA (subject: ${root.subject})`);
assert.ok(lab.intermediatePemPath, "lab must expose its intermediate path");
const intermediate = new X509Certificate(readFileSync(lab.intermediatePemPath, "utf8"));
assert.equal(intermediate.ca, true, `intermediate must be a CA (subject: ${intermediate.subject})`);
});
@@ -0,0 +1,198 @@
import assert from "node:assert/strict";
import { X509Certificate } from "node:crypto";
import { access, chmod, mkdtemp, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
import http from "node:http";
import https from "node:https";
import { tmpdir } from "node:os";
import path from "node:path";
import tls from "node:tls";
import { test } from "node:test";
import { stageTrustedEnterpriseTlsRoot, startEnterpriseTlsReverseEdge } from "../src/egress.ts";
function listen(server: http.Server): Promise<number> {
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") return reject(new Error("server did not bind"));
resolve(address.port);
});
});
}
function close(server: http.Server): Promise<void> {
return new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
function request(url: string, options: { ca?: string; method?: string; path?: string; body?: string } = {}): Promise<string> {
return new Promise((resolve, reject) => {
const target = new URL(url);
const outgoing = https.request({
hostname: target.hostname,
port: target.port,
ca: options.ca,
method: options.method,
path: options.path ?? `${target.pathname}${target.search}`,
}, (response) => {
const chunks: Buffer[] = [];
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
response.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
});
outgoing.on("error", reject);
outgoing.end(options.body);
});
}
function peerCertificatePem(url: string, ca: string): Promise<string> {
const target = new URL(url);
return new Promise((resolve, reject) => {
const socket = tls.connect({ host: target.hostname, port: Number(target.port), ca, servername: "localhost" }, () => {
const raw = socket.getPeerCertificate().raw;
socket.end();
if (!raw) reject(new Error("peer did not provide a certificate"));
else resolve(new X509Certificate(raw).toString());
});
socket.on("error", reject);
});
}
function manifest(rootPemPath: string): string {
return `${JSON.stringify({
pid: process.pid,
candidateUrl: "https://localhost:8443",
negativeUrl: "https://localhost:9443",
adminUrl: "http://127.0.0.1:8445",
rootPemPath,
})}\n`;
}
test("enterprise TLS privileged trust material is validated and staged", async (t) => {
const edge = await startEnterpriseTlsReverseEdge({ upstream: "http://127.0.0.1:1" });
const dir = await mkdtemp(path.join(tmpdir(), "openwork-enterprise-tls-validation-test-"));
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
try {
await t.test("accepts an owned regular manifest and CA PEM", async () => {
const rootPemPath = path.join(dir, "happy-root.pem");
const manifestPath = path.join(dir, "happy-manifest.json");
await writeFile(rootPemPath, edge.rootPem, { mode: 0o600 });
await writeFile(manifestPath, manifest(rootPemPath), { mode: 0o600 });
const staged = await stageTrustedEnterpriseTlsRoot(manifestPath, [uid]);
try {
assert.deepEqual(staged.rootPem, Buffer.from(edge.rootPem));
assert.deepEqual(await readFile(staged.stagedRootPemPath), staged.rootPem);
assert.notEqual(staged.stagedRootPemPath, rootPemPath);
assert.equal((await stat(staged.stagedRootPemPath)).mode & 0o777, 0o600);
} finally {
await staged.cleanup();
}
});
await t.test("rejects a symlinked manifest", async () => {
const rootPemPath = path.join(dir, "manifest-link-root.pem");
const targetPath = path.join(dir, "manifest-target.json");
const manifestPath = path.join(dir, "manifest-link.json");
await writeFile(rootPemPath, edge.rootPem, { mode: 0o600 });
await writeFile(targetPath, manifest(rootPemPath), { mode: 0o600 });
await symlink(targetPath, manifestPath);
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid]), { name: "ENTERPRISE_TLS_MANIFEST_UNTRUSTED" });
});
await t.test("rejects a symlinked PEM", async () => {
const targetPath = path.join(dir, "pem-target.pem");
const rootPemPath = path.join(dir, "pem-link.pem");
const manifestPath = path.join(dir, "pem-link-manifest.json");
await writeFile(targetPath, edge.rootPem, { mode: 0o600 });
await symlink(targetPath, rootPemPath);
await writeFile(manifestPath, manifest(rootPemPath), { mode: 0o600 });
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid]), { name: "ENTERPRISE_TLS_ROOT_PEM_UNTRUSTED" });
});
await t.test("rejects a world-writable PEM", async () => {
const rootPemPath = path.join(dir, "writable-root.pem");
const manifestPath = path.join(dir, "writable-manifest.json");
await writeFile(rootPemPath, edge.rootPem, { mode: 0o600 });
await chmod(rootPemPath, 0o666);
await writeFile(manifestPath, manifest(rootPemPath), { mode: 0o600 });
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid]), { name: "ENTERPRISE_TLS_ROOT_PEM_UNTRUSTED" });
});
await t.test("rejects non-CA and unparseable PEM files", async () => {
const manifestPath = path.join(dir, "invalid-manifest.json");
const rootPemPath = path.join(dir, "invalid-root.pem");
await writeFile(manifestPath, manifest(rootPemPath), { mode: 0o600 });
await writeFile(rootPemPath, await peerCertificatePem(edge.candidateUrl, edge.rootPem), { mode: 0o600 });
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid]), /certificate is not a CA/u);
await writeFile(rootPemPath, "not a certificate\n", { mode: 0o600 });
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid]), { name: "ENTERPRISE_TLS_ROOT_PEM_UNTRUSTED" });
});
await t.test("rejects a relative rootPemPath", async () => {
const manifestPath = path.join(dir, "relative-manifest.json");
await writeFile(manifestPath, manifest("relative-root.pem"), { mode: 0o600 });
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid]), /rootPemPath must be absolute/u);
});
await t.test("rejects an owner outside the explicit allowed uid set", async () => {
const rootPemPath = path.join(dir, "owner-root.pem");
const manifestPath = path.join(dir, "owner-manifest.json");
await writeFile(rootPemPath, edge.rootPem, { mode: 0o600 });
await writeFile(manifestPath, manifest(rootPemPath), { mode: 0o600 });
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid + 1]), /owner uid .* is not allowed/u);
});
} finally {
await edge.stop();
await rm(dir, { recursive: true, force: true });
}
});
test("enterprise TLS edge pins its upstream and exposes selective trust", async () => {
const upstreamRequests: { method: string; url: string; body: string }[] = [];
const upstream = http.createServer(async (incoming, response) => {
const chunks: Buffer[] = [];
for await (const chunk of incoming) chunks.push(Buffer.from(chunk));
upstreamRequests.push({ method: incoming.method ?? "GET", url: incoming.url ?? "/", body: Buffer.concat(chunks).toString("utf8") });
response.end("from-pinned-upstream");
});
const attackerHits: string[] = [];
const attacker = http.createServer((incoming, response) => {
attackerHits.push(incoming.url ?? "/");
response.end("attacker");
});
const upstreamPort = await listen(upstream);
const attackerPort = await listen(attacker);
const edge = await startEnterpriseTlsReverseEdge({ upstream: `http://127.0.0.1:${upstreamPort}` });
const rootPath = edge.rootPemPath;
try {
await assert.rejects(request(`${edge.candidateUrl}/default-trust`));
assert.equal(await request(`${edge.candidateUrl}/v1/me?full=1`, { ca: edge.rootPem, method: "POST", body: "payload" }), "from-pinned-upstream");
await assert.rejects(request(`${edge.negativeUrl}/negative`, { ca: edge.rootPem }));
const absoluteTarget = `http://127.0.0.1:${attackerPort}/stolen?token=yes`;
assert.equal(await request(edge.candidateUrl, { ca: edge.rootPem, path: absoluteTarget }), "from-pinned-upstream");
assert.deepEqual(attackerHits, []);
assert.deepEqual(upstreamRequests, [
{ method: "POST", url: "/v1/me?full=1", body: "payload" },
{ method: "GET", url: "/stolen?token=yes", body: "" },
]);
assert.deepEqual(edge.requests.map(({ endpoint, method, path, body }) => ({ endpoint, method, path, body })), [
{ endpoint: "trusted-candidate", method: "POST", path: "/v1/me?full=1", body: "payload" },
{ endpoint: "trusted-candidate", method: "GET", path: "/stolen?token=yes", body: "" },
]);
assert.equal(edge.linuxTrust.restartApplication, true);
assert.equal(edge.linuxTrust.install()[1]?.file, "/usr/sbin/update-ca-certificates");
assert.equal(edge.linuxTrust.prerequisiteFailures.root, "ENTERPRISE_TLS_LINUX_ROOT_REQUIRED");
const prerequisites = await edge.linuxTrust.checkPrerequisites();
if (!prerequisites.ok) {
assert.ok([
"ENTERPRISE_TLS_LINUX_ROOT_REQUIRED",
"ENTERPRISE_TLS_UPDATE_CA_CERTIFICATES_REQUIRED",
].includes(prerequisites.failure));
}
} finally {
await edge.stop();
await Promise.all([close(upstream), close(attacker)]);
}
await assert.rejects(access(rootPath));
await assert.rejects(request(edge.candidateUrl, { ca: edge.rootPem }));
});
+124
View File
@@ -0,0 +1,124 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
buildBlockedUserResponse,
buildOidcClaims,
buildRs256SigningArgv,
buildWrongDomainEmail,
matchSsoExpectation,
normalizeCertificateMaterial,
normalizeDomain,
normalizeMockIdpConfig,
startMockIdpLab,
subjectWithKnobs,
validateSsoConfiguration,
} from "../src/idp.ts";
test("mock IdP config normalization trims issuer, domain, and credentials", () => {
const config = normalizeMockIdpConfig({
issuer: " https://idp.example.test/ ",
domain: " @Acme.TEST. ",
clientId: " client-one ",
clientSecret: " secret-one ",
});
assert.equal(config.issuer, "https://idp.example.test");
assert.equal(config.domain, "acme.test");
assert.equal(config.clientId, "client-one");
assert.equal(config.clientSecret, "secret-one");
assert.equal(config.defaultSubject.email, "sso.user@acme.test");
});
test("certificate normalizer names the trailing-newline paste bug", () => {
const cert = "-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----\n";
const normalized = normalizeCertificateMaterial(cert);
const validation = validateSsoConfiguration({ configuredDomain: "acme.test", cert, subjectEmail: "maya@acme.test" });
const match = matchSsoExpectation(validation, { code: "sso_cert_trailing_newline", includes: ["trailing newline", "certificate"] });
assert.equal(normalized.hadTrailingNewline, true);
assert.equal(normalized.value.endsWith("\n"), false);
assert.equal(validation.ok, false);
assert.equal(match.passed, true);
});
test("domain mismatch normalizer names IdP email domains that do not match the org", () => {
const wrongEmail = buildWrongDomainEmail("maya@acme.test", "acme.test");
const validation = validateSsoConfiguration({ configuredDomain: "ACME.test", subjectEmail: wrongEmail });
const match = matchSsoExpectation(validation, { code: "sso_domain_mismatch", includes: ["wrong-acme.test", "acme.test"] });
assert.equal(wrongEmail, "maya@wrong-acme.test");
assert.equal(normalizeDomain(" @ACME.test. "), "acme.test");
assert.equal(validation.normalizedDomain, "acme.test");
assert.equal(match.passed, true);
});
test("claim builder supports present, absent, and unexpected group claim shapes", () => {
const baseConfig = normalizeMockIdpConfig({ domain: "acme.test" });
const subject = subjectWithKnobs(baseConfig, { email: "dev@acme.test", name: "Dev User" });
const present = buildOidcClaims({ issuer: baseConfig.issuer, clientId: baseConfig.clientId, subject, knobs: baseConfig.knobs });
const absentConfig = normalizeMockIdpConfig({ knobs: { groupClaims: "absent" } });
const absent = buildOidcClaims({ issuer: absentConfig.issuer, clientId: absentConfig.clientId, subject, knobs: absentConfig.knobs });
const oddConfig = normalizeMockIdpConfig({ knobs: { groupClaims: "unexpected-shape" } });
const odd = buildOidcClaims({ issuer: oddConfig.issuer, clientId: oddConfig.clientId, subject, knobs: oddConfig.knobs });
assert.deepEqual(present.groups, ["Engineering", "OpenWork Lab"]);
assert.equal("groups" in absent, false);
assert.deepEqual(odd.groups, { primary: "Engineering", all: ["Engineering", "OpenWork Lab"] });
});
test("email mismatch and guest-user knobs shape SSO subjects", () => {
const mismatchConfig = normalizeMockIdpConfig({ knobs: { emailMismatch: "other@acme.test" } });
const guestConfig = normalizeMockIdpConfig({ knobs: { guestUser: true } });
assert.equal(subjectWithKnobs(mismatchConfig, { email: "invited@acme.test" }).email, "other@acme.test");
assert.equal(subjectWithKnobs(guestConfig, { email: "guest@external.test" }).sub, "guest:guest@external.test");
});
test("RS256 signing argv documents the dependency-free node:crypto path", () => {
assert.deepEqual(buildRs256SigningArgv({ keyId: "kid-one", payload: "abc" }), [
"node:crypto",
"createSign",
"RSA-SHA256",
"--kid",
"kid-one",
"--payload-bytes",
"3",
]);
});
test("blocked-user response uses the Entra-style policy wording", () => {
const response = buildBlockedUserResponse({ email: "blocked@acme.test" });
assert.equal(response.status, 403);
assert.equal(response.error, "access_denied");
assert.match(response.errorDescription, /administrator has configured the application to block users/);
assert.match(response.message, /IdP administrator/);
assert.match(response.html, /OpenWork Mock IdP policy/);
});
test("mock IdP escapes request-derived blocked-user HTML", async () => {
const loginHint = "blocked@acme.test<script>alert(1)</script>";
await using lab = await startMockIdpLab({ knobs: { blockedUser: loginHint } });
const url = new URL(lab.authorizationEndpoint);
url.searchParams.set("client_id", lab.config.clientId);
url.searchParams.set("redirect_uri", "http://127.0.0.1/callback");
url.searchParams.set("login_hint", loginHint);
const response = await fetch(url);
const body = await response.text();
assert.equal(response.status, 403);
// Plain substring checks: stricter than a tag regex (`<script` also catches
// `<script src=...`) and they cannot be mistaken for a tag-filtering sanitizer.
assert.ok(!body.includes("<script"), `escaped body must not contain raw markup: ${body}`);
assert.ok(body.includes("&lt;script&gt;"), `escaped body must contain the escaped payload: ${body}`);
});
test("expectation matcher fails with actionable detail when the named error is missing", () => {
const validation = validateSsoConfiguration({ configuredDomain: "acme.test", subjectEmail: "maya@acme.test" });
const match = matchSsoExpectation(validation, { code: "sso_domain_mismatch" });
assert.equal(match.passed, false);
assert.deepEqual(match.actualCodes, []);
assert.match(match.detail, /Expected sso_domain_mismatch/);
});
@@ -0,0 +1,338 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { createServer } from "node:http";
import type { Server } from "node:http";
import type { AddressInfo } from "node:net";
import test from "node:test";
import { startMockGoogle } from "../src/mock-google.ts";
import type { MockGoogleHandle } from "../src/mock-google.ts";
interface CallbackLab {
url: string;
callbacks: URL[];
stop(): Promise<void>;
[Symbol.asyncDispose](): Promise<void>;
}
interface OAuthTokens {
accessToken: string;
idToken: string;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isAddressInfo(value: ReturnType<Server["address"]>): value is AddressInfo {
return typeof value === "object"
&& value !== null
&& typeof value.address === "string"
&& typeof value.family === "string"
&& typeof value.port === "number";
}
function close(server: Server): Promise<void> {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
server.closeIdleConnections();
});
}
async function startCallbackLab(): Promise<CallbackLab> {
const callbacks: URL[] = [];
const server = createServer((request, response) => {
callbacks.push(new URL(request.url ?? "/", "http://127.0.0.1"));
response.writeHead(200, { "content-type": "text/plain" });
response.end("OAuth callback served");
});
const address = await new Promise<AddressInfo>((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
server.off("error", reject);
const value = server.address();
if (isAddressInfo(value)) {
resolve(value);
return;
}
reject(new Error("Callback lab did not expose a TCP port."));
});
});
let stopped = false;
const stop = async (): Promise<void> => {
if (stopped) return;
stopped = true;
await close(server);
};
return {
url: `http://127.0.0.1:${address.port}/callback`,
callbacks,
stop,
[Symbol.asyncDispose]: stop,
};
}
function tokenFingerprint(accessToken: string): string {
return createHash("sha256").update(accessToken).digest("hex").slice(0, 12);
}
async function authorize(
google: MockGoogleHandle,
callback: CallbackLab,
email: string,
clientId: string,
): Promise<OAuthTokens> {
const verifier = `verifier-${clientId}`;
const challenge = createHash("sha256").update(verifier).digest("base64url");
const authorizeUrl = new URL(google.authorizeUrl);
authorizeUrl.searchParams.set("client_id", clientId);
authorizeUrl.searchParams.set("redirect_uri", callback.url);
authorizeUrl.searchParams.set("response_type", "code");
authorizeUrl.searchParams.set("scope", "openid email profile https://www.googleapis.com/auth/gmail.compose");
authorizeUrl.searchParams.set("state", `state-${clientId}`);
authorizeUrl.searchParams.set("prompt", "consent select_account");
authorizeUrl.searchParams.set("code_challenge", challenge);
authorizeUrl.searchParams.set("code_challenge_method", "S256");
const since = new Date().toISOString();
const callbackCount = callback.callbacks.length;
const chooser = await fetch(authorizeUrl, { redirect: "manual" });
const chooserHtml = await chooser.text();
assert.equal(chooser.status, 200);
assert.match(chooserHtml, /Choose an account/);
assert.match(chooserHtml, new RegExp(`data-account-email="${email.replaceAll(".", "\\.")}"`));
const observed = await google.authorizeRequestSince(since, { timeoutMs: 2_000 });
assert.equal(observed.params.get("prompt"), "consent select_account");
await google.chooseAccount(email, { timeoutMs: 2_000 });
assert.equal(callback.callbacks.length, callbackCount + 1, "chooseAccount must wait until the callback server receives the request");
const callbackUrl = callback.callbacks.at(-1);
const code = callbackUrl?.searchParams.get("code");
assert.ok(code, "callback must carry an authorization code");
const response = await fetch(google.tokenUrl, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
code_verifier: verifier,
client_id: clientId,
client_secret: `secret-${clientId}`,
redirect_uri: callback.url,
}),
});
const body: unknown = await response.json();
assert.equal(response.status, 200);
assert.ok(isRecord(body));
if (typeof body.access_token !== "string" || typeof body.id_token !== "string") {
throw new Error(`Token response omitted access_token or id_token: ${JSON.stringify(body)}`);
}
return { accessToken: body.access_token, idToken: body.id_token };
}
function idTokenPayload(idToken: string): Record<string, unknown> {
const payload = idToken.split(".")[1];
assert.ok(payload, "id_token must have a JWT payload");
const parsed: unknown = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
assert.ok(isRecord(parsed));
return parsed;
}
async function userinfo(google: MockGoogleHandle, accessToken: string): Promise<Record<string, unknown>> {
const response = await fetch(google.userinfoUrl, { headers: { authorization: `Bearer ${accessToken}` } });
const body: unknown = await response.json();
assert.equal(response.status, 200);
assert.ok(isRecord(body));
return body;
}
async function submitDraft(
google: MockGoogleHandle,
accessToken: string,
message: string,
threadId?: string,
): Promise<void> {
const requestMessage: { raw: string; threadId?: string } = {
raw: Buffer.from(message, "utf8").toString("base64url"),
};
if (threadId) requestMessage.threadId = threadId;
const response = await fetch(`${google.apiUrl}/gmail/v1/users/me/drafts`, {
method: "POST",
headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json" },
body: JSON.stringify({ message: requestMessage }),
});
assert.equal(response.status, 200);
}
async function createDraft(
google: MockGoogleHandle,
accessToken: string,
to: string,
body: string,
threadId?: string,
): Promise<void> {
const message = [
`To: ${to}`,
"Subject: Fixture witness",
"MIME-Version: 1.0",
'Content-Type: text/plain; charset="UTF-8"',
"Content-Transfer-Encoding: base64",
"",
Buffer.from(body, "utf8").toString("base64"),
].join("\r\n");
await submitDraft(google, accessToken, message, threadId);
}
test("mock Google issues independent account identities and credential fingerprints", async () => {
const accountA = "jordan@acme.test";
const accountB = "jordan@acmelabs.test";
await using google = await startMockGoogle({ accounts: [accountA, accountB], port: 0, autoApprove: false });
await using callback = await startCallbackLab();
const tokensA = await authorize(google, callback, accountA, "client-a");
const tokensB = await authorize(google, callback, accountB, "client-b");
assert.notEqual(tokenFingerprint(tokensA.accessToken), tokenFingerprint(tokensB.accessToken));
assert.equal(idTokenPayload(tokensA.idToken).email, accountA);
assert.equal(idTokenPayload(tokensB.idToken).email, accountB);
assert.equal((await userinfo(google, tokensA.accessToken)).email, accountA);
assert.equal((await userinfo(google, tokensB.accessToken)).email, accountB);
await createDraft(google, tokensA.accessToken, "archive@acme.test", "Account A", "thread-a");
await createDraft(google, tokensB.accessToken, "archive@acmelabs.test", "Account B", "thread-b");
const draftsA = await google.draftsFor(accountA);
const draftsB = await google.draftsFor(accountB);
assert.equal(draftsA[0]?.tokenId, tokenFingerprint(tokensA.accessToken));
assert.equal(draftsB[0]?.tokenId, tokenFingerprint(tokensB.accessToken));
assert.notEqual(draftsA[0]?.tokenId, draftsB[0]?.tokenId);
});
test("mock Google attributes drafts to one mailbox and returns an empty mailbox promptly", async () => {
const accountA = "jordan@acme.test";
const accountB = "jordan@acmelabs.test";
await using google = await startMockGoogle({ accounts: [accountA, accountB], port: 0, autoApprove: false });
await using callback = await startCallbackLab();
const tokensA = await authorize(google, callback, accountA, "client-a");
const since = new Date().toISOString();
await createDraft(google, tokensA.accessToken, "supplier@parts.test", "Mailbox isolation witness", "thread-one");
const draftsA = await google.draftsFor(accountA, { since, atLeast: 1, timeoutMs: 2_000 });
const startedAt = Date.now();
const draftsB = await google.draftsFor(accountB, { since, timeoutMs: 5_000 });
const elapsedMs = Date.now() - startedAt;
assert.deepEqual(draftsB, []);
assert.ok(elapsedMs < 1_000, `empty mailbox read blocked for ${elapsedMs}ms`);
assert.equal(draftsA.length, 1);
assert.deepEqual(draftsA[0], {
to: "supplier@parts.test",
body: "Mailbox isolation witness",
threadId: "thread-one",
tokenId: tokenFingerprint(tokensA.accessToken),
at: draftsA[0]?.at,
});
});
test("mock Google decodes text/plain nested inside mixed and alternative MIME", async () => {
const account = "jordan@acme.test";
await using google = await startMockGoogle({ accounts: [account], port: 0, autoApprove: false });
await using callback = await startCallbackLab();
const tokens = await authorize(google, callback, account, "client-nested-mime");
const plainBody = "Nested multipart body marker";
const mixedBoundary = "fixture-mixed";
const alternativeBoundary = "fixture-alternative";
const message = [
"To: archive@acme.test",
"Subject: Nested MIME witness",
"MIME-Version: 1.0",
`Content-Type: multipart/mixed; boundary="${mixedBoundary}"`,
"",
`--${mixedBoundary}`,
`Content-Type: multipart/alternative; boundary="${alternativeBoundary}"`,
"",
`--${alternativeBoundary}`,
'Content-Type: text/plain; charset="UTF-8"',
"Content-Transfer-Encoding: base64",
"",
Buffer.from(plainBody, "utf8").toString("base64"),
`--${alternativeBoundary}`,
'Content-Type: text/html; charset="UTF-8"',
"Content-Transfer-Encoding: base64",
"",
Buffer.from(`<div>${plainBody}</div>`, "utf8").toString("base64"),
`--${alternativeBoundary}--`,
`--${mixedBoundary}`,
'Content-Type: application/octet-stream; name="witness.bin"',
'Content-Disposition: attachment; filename="witness.bin"',
"Content-Transfer-Encoding: base64",
"",
Buffer.from("attachment bytes", "utf8").toString("base64"),
`--${mixedBoundary}--`,
"",
].join("\r\n");
await submitDraft(google, tokens.accessToken, message);
const drafts = await google.draftsFor(account);
assert.equal(drafts[0]?.body, plainBody);
});
test("mock Google witnesses exact Gmail attachment and Drive upload bytes", async () => {
const account = "jordan@acme.test";
const fixture = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff, 0x80, 0x13, 0x0a]);
await using google = await startMockGoogle({ accounts: [account], port: 0, autoApprove: false });
await using callback = await startCallbackLab();
const tokens = await authorize(google, callback, account, "client-binary-witness");
const mixedBoundary = "fixture-mixed-binary";
const message = [
"To: archive@acme.test",
"Subject: Binary witness",
"MIME-Version: 1.0",
`Content-Type: multipart/mixed; boundary="${mixedBoundary}"`,
"",
`--${mixedBoundary}`,
'Content-Type: text/plain; charset="UTF-8"',
"Content-Transfer-Encoding: base64",
"",
Buffer.from("Binary body", "utf8").toString("base64"),
`--${mixedBoundary}`,
'Content-Type: image/png; name="witness.png"',
'Content-Disposition: attachment; filename="witness.png"',
"Content-Transfer-Encoding: base64",
"",
fixture.toString("base64"),
`--${mixedBoundary}--`,
"",
].join("\r\n");
await submitDraft(google, tokens.accessToken, message);
const drafts = await google.draftsFor(account, { atLeast: 1, timeoutMs: 2_000 });
assert.equal(drafts[0]?.attachments?.[0]?.filename, "witness.png");
assert.equal(drafts[0]?.attachments?.[0]?.mimeType, "image/png");
assert.deepEqual(drafts[0]?.attachments?.[0]?.content, fixture);
const driveBoundary = "fixture-drive-binary";
const metadata = Buffer.from(`--${driveBoundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n{"name":"witness.png"}\r\n`, "utf8");
const contentHeader = Buffer.from(`--${driveBoundary}\r\nContent-Type: image/png\r\n\r\n`, "utf8");
const footer = Buffer.from(`\r\n--${driveBoundary}--\r\n`, "utf8");
const uploadBody = Buffer.concat([metadata, contentHeader, fixture, footer]);
const uploadBytes = new Uint8Array(uploadBody.byteLength);
uploadBytes.set(uploadBody);
const response = await fetch(`${google.apiUrl}/upload/drive/v3/files?uploadType=multipart`, {
method: "POST",
headers: {
authorization: `Bearer ${tokens.accessToken}`,
"content-type": `multipart/related; boundary=${driveBoundary}`,
},
body: uploadBytes,
});
assert.equal(response.status, 200);
const uploads = await google.driveUploadsFor(account, { atLeast: 1, timeoutMs: 2_000 });
assert.equal(uploads[0]?.filename, "witness.png");
assert.equal(uploads[0]?.mimeType, "image/png");
assert.deepEqual(uploads[0]?.content, fixture);
});
+95
View File
@@ -0,0 +1,95 @@
import assert from "node:assert/strict";
import test from "node:test";
import { allocateFreePort } from "@openwork/cdp";
import { startMockMcp } from "../src/mock-mcp.ts";
test("records an unauthenticated initialize attempt as a handshake", async () => {
await using mock = await startMockMcp({ port: await allocateFreePort() });
const startedAt = new Date().toISOString();
const response = await fetch(mock.mcpUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-06-18",
capabilities: {},
clientInfo: { name: "mock-handshake-test", version: "1.0.0" },
},
}),
});
assert.equal(response.status, 401);
const handshakes = await mock.handshakes({ sinceIso: startedAt });
assert.equal(handshakes.length, 1);
assert.equal(handshakes[0]?.method, "POST");
assert.equal(handshakes[0]?.path, "/mcp");
});
function completionBody(marker: string, completedTools: number): Record<string, unknown> {
return {
model: "mock-agent-workload-model",
stream: true,
tools: [
{ type: "function", function: { name: "write", parameters: { type: "object" } } },
{ type: "function", function: { name: "read", parameters: { type: "object" } } },
],
messages: [
{ role: "user", content: `run ${marker}` },
...Array.from({ length: completedTools }, (_, index) => ({
role: "tool",
tool_call_id: `call-${index}`,
content: `tool result ${index}`,
})),
],
};
}
test("scripts and records deterministic OpenAI-compatible agent tool rounds", async () => {
const marker = "agent-workload-unit-marker";
await using mock = await startMockMcp({
port: await allocateFreePort(),
agentWorkloads: [{
promptMarker: marker,
finalReply: "unit workload complete",
steps: [
{ tool: "write", arguments: { filePath: "/tmp/unit.txt", content: marker } },
{ tool: "read", arguments: { filePath: "/tmp/unit.txt" } },
],
}],
});
const startedAt = new Date().toISOString();
const first = await fetch(`${mock.url}/v1/chat/completions`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(completionBody(marker, 0)),
});
const firstText = await first.text();
assert.equal(first.status, 200);
assert.match(firstText, /"name":"write"/);
assert.match(firstText, /unit\.txt/);
const second = await fetch(`${mock.url}/v1/chat/completions`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(completionBody(marker, 1)),
});
assert.match(await second.text(), /"name":"read"/);
const final = await fetch(`${mock.url}/v1/chat/completions`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(completionBody(marker, 2)),
});
assert.match(await final.text(), /unit workload complete/);
const requests = await mock.agentRequests({ promptMarker: marker, sinceIso: startedAt, atLeast: 3, timeoutMs: 5_000 });
assert.deepEqual(requests.map((request) => request.kind), ["tool", "tool", "final"]);
assert.deepEqual(requests.map((request) => request.completedTools), [0, 1, 2]);
assert.deepEqual(requests.map((request) => request.matchedMarkers), [[marker], [marker], [marker]]);
});