630 lines
21 KiB
JavaScript
630 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { createServer } from "node:http";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
export const MOCK_MICROSOFT_ACCESS_TOKEN = "mock-microsoft-access-token";
|
|
export const MOCK_MICROSOFT_REFRESH_TOKEN = "mock-microsoft-refresh-token";
|
|
export const MOCK_WORKER_HOST_TOKEN = "mock-worker-host-token";
|
|
export const MOCK_WORKER_CLIENT_TOKEN = "mock-worker-client-token";
|
|
|
|
const MICROSOFT_SCOPES = [
|
|
"openid",
|
|
"profile",
|
|
"email",
|
|
"offline_access",
|
|
"User.Read",
|
|
"Mail.Read",
|
|
"Calendars.Read",
|
|
"Files.Read",
|
|
];
|
|
|
|
const MICROSOFT_MESSAGES = [
|
|
{
|
|
id: "message-launch-readiness",
|
|
subject: "Launch readiness",
|
|
receivedDateTime: "2026-07-09T16:00:00Z",
|
|
bodyPreview: "The launch checklist is complete. The only remaining item is the final support handoff.",
|
|
body: {
|
|
contentType: "text",
|
|
content: "The launch checklist is complete. The only remaining item is the final support handoff.",
|
|
},
|
|
from: { emailAddress: { name: "Ada Lovelace", address: "ada@example.test" } },
|
|
toRecipients: [{ emailAddress: { name: "OpenWork Tester", address: "tester@example.test" } }],
|
|
webLink: "https://outlook.office.test/mail/message-launch-readiness",
|
|
},
|
|
{
|
|
id: "message-q3-budget",
|
|
subject: "Q3 budget approved",
|
|
receivedDateTime: "2026-07-09T15:00:00Z",
|
|
bodyPreview: "Finance approved the Q3 budget with no changes.",
|
|
body: { contentType: "text", content: "Finance approved the Q3 budget with no changes." },
|
|
from: { emailAddress: { name: "Grace Hopper", address: "grace@example.test" } },
|
|
toRecipients: [{ emailAddress: { name: "OpenWork Tester", address: "tester@example.test" } }],
|
|
webLink: "https://outlook.office.test/mail/message-q3-budget",
|
|
},
|
|
{
|
|
id: "message-customer-feedback",
|
|
subject: "Customer feedback summary",
|
|
receivedDateTime: "2026-07-09T14:00:00Z",
|
|
bodyPreview: "Pilot customers highlighted faster setup and asked for clearer connection health.",
|
|
body: {
|
|
contentType: "text",
|
|
content: "Pilot customers highlighted faster setup and asked for clearer connection health.",
|
|
},
|
|
from: { emailAddress: { name: "Katherine Johnson", address: "katherine@example.test" } },
|
|
toRecipients: [{ emailAddress: { name: "OpenWork Tester", address: "tester@example.test" } }],
|
|
webLink: "https://outlook.office.test/mail/message-customer-feedback",
|
|
},
|
|
];
|
|
|
|
const MICROSOFT_EVENTS = [
|
|
{
|
|
id: "event-launch-review",
|
|
subject: "Launch review",
|
|
bodyPreview: "Review launch status and support handoff.",
|
|
start: { dateTime: "2026-07-10T09:00:00", timeZone: "America/Los_Angeles" },
|
|
end: { dateTime: "2026-07-10T09:30:00", timeZone: "America/Los_Angeles" },
|
|
organizer: { emailAddress: { name: "Ada Lovelace", address: "ada@example.test" } },
|
|
attendees: [],
|
|
location: { displayName: "OpenWork Room" },
|
|
isCancelled: false,
|
|
webLink: "https://outlook.office.test/calendar/event-launch-review",
|
|
},
|
|
{
|
|
id: "event-q3-planning",
|
|
subject: "Q3 planning",
|
|
bodyPreview: "Confirm Q3 milestones and owners.",
|
|
start: { dateTime: "2026-07-10T11:00:00", timeZone: "America/Los_Angeles" },
|
|
end: { dateTime: "2026-07-10T12:00:00", timeZone: "America/Los_Angeles" },
|
|
organizer: { emailAddress: { name: "Grace Hopper", address: "grace@example.test" } },
|
|
attendees: [],
|
|
location: { displayName: "Microsoft Teams" },
|
|
isCancelled: false,
|
|
webLink: "https://outlook.office.test/calendar/event-q3-planning",
|
|
},
|
|
];
|
|
|
|
const MICROSOFT_Q3_FILE = {
|
|
id: "file-q3-plan",
|
|
name: "Q3 Plan.txt",
|
|
size: 128,
|
|
lastModifiedDateTime: "2026-07-09T13:00:00Z",
|
|
webUrl: "https://onedrive.office.test/files/file-q3-plan",
|
|
file: { mimeType: "text/plain", hashes: { quickXorHash: "mock-hash" } },
|
|
parentReference: { driveId: "drive-openwork-test", path: "/drive/root:" },
|
|
};
|
|
|
|
const Q3_FILE_CONTENT = [
|
|
"Q3 Plan",
|
|
"",
|
|
"1. Ship cloud connections.",
|
|
"2. Improve connection health and audit history.",
|
|
"3. Complete the support handoff.",
|
|
].join("\n");
|
|
|
|
function sendJson(response, status, body, headers = {}) {
|
|
response.writeHead(status, {
|
|
"access-control-allow-origin": "*",
|
|
"access-control-allow-methods": "GET,POST,DELETE,OPTIONS",
|
|
"access-control-allow-headers": "authorization,content-type,x-openwork-host-token",
|
|
"content-type": "application/json; charset=utf-8",
|
|
...headers,
|
|
});
|
|
response.end(JSON.stringify(body));
|
|
}
|
|
|
|
function sendText(response, status, body, contentType = "text/plain; charset=utf-8", headers = {}) {
|
|
response.writeHead(status, {
|
|
"access-control-allow-origin": "*",
|
|
"content-type": contentType,
|
|
...headers,
|
|
});
|
|
response.end(body);
|
|
}
|
|
|
|
function sendEmpty(response, status = 204) {
|
|
response.writeHead(status, { "access-control-allow-origin": "*" });
|
|
response.end();
|
|
}
|
|
|
|
function readBody(request) {
|
|
return new Promise((resolve, reject) => {
|
|
let raw = "";
|
|
request.setEncoding("utf8");
|
|
request.on("data", (chunk) => {
|
|
raw += chunk;
|
|
if (raw.length > 1_000_000) request.destroy(new Error("request_too_large"));
|
|
});
|
|
request.on("end", () => resolve(raw));
|
|
request.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function parseJson(raw) {
|
|
if (!raw.trim()) return {};
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
function requestHeaders(request) {
|
|
return Object.fromEntries(
|
|
Object.entries(request.headers).map(([name, value]) => [name, Array.isArray(value) ? value.join(", ") : value ?? ""]),
|
|
);
|
|
}
|
|
|
|
function recordRequest(state, request, url, rawBody) {
|
|
const contentType = request.headers["content-type"] ?? "";
|
|
let body = rawBody;
|
|
if (rawBody && contentType.includes("application/json")) {
|
|
try {
|
|
body = JSON.parse(rawBody);
|
|
} catch {
|
|
body = rawBody;
|
|
}
|
|
} else if (rawBody && contentType.includes("application/x-www-form-urlencoded")) {
|
|
body = Object.fromEntries(new URLSearchParams(rawBody));
|
|
}
|
|
|
|
state.requests.push({
|
|
id: state.requests.length + 1,
|
|
method: request.method ?? "GET",
|
|
path: url.pathname,
|
|
search: url.search,
|
|
headers: requestHeaders(request),
|
|
body,
|
|
});
|
|
}
|
|
|
|
function initialState() {
|
|
return {
|
|
requests: [],
|
|
worker: {
|
|
nextSessionId: 1,
|
|
sessions: new Map(),
|
|
},
|
|
};
|
|
}
|
|
|
|
function resetState(state) {
|
|
const fresh = initialState();
|
|
state.requests = fresh.requests;
|
|
state.worker = fresh.worker;
|
|
}
|
|
|
|
function handleMicrosoftOAuth(request, response, url, rawBody) {
|
|
const match = url.pathname.match(/^\/entra\/([^/]+)\/oauth2\/v2\.0\/(authorize|token)$/);
|
|
if (!match) return false;
|
|
|
|
const operation = match[2];
|
|
if (operation === "authorize" && request.method === "GET") {
|
|
const redirectUri = url.searchParams.get("redirect_uri");
|
|
if (!redirectUri) {
|
|
sendJson(response, 400, { error: "invalid_request", error_description: "redirect_uri is required" });
|
|
return true;
|
|
}
|
|
const callback = new URL(redirectUri);
|
|
const requestedScopes = url.searchParams.get("scope") ?? MICROSOFT_SCOPES.join(" ");
|
|
const encodedScopes = Buffer.from(requestedScopes, "utf8").toString("base64url");
|
|
callback.searchParams.set("code", `mock-microsoft-authorization-code.${encodedScopes}`);
|
|
const state = url.searchParams.get("state");
|
|
if (state) callback.searchParams.set("state", state);
|
|
response.writeHead(302, { location: callback.toString() });
|
|
response.end();
|
|
return true;
|
|
}
|
|
|
|
if (operation === "token" && request.method === "POST") {
|
|
const form = new URLSearchParams(rawBody);
|
|
const grantType = form.get("grant_type");
|
|
if (grantType !== "authorization_code" && grantType !== "refresh_token") {
|
|
sendJson(response, 400, { error: "unsupported_grant_type" });
|
|
return true;
|
|
}
|
|
const authorizationCode = form.get("code") ?? "";
|
|
const encodedScopes = authorizationCode.startsWith("mock-microsoft-authorization-code.")
|
|
? authorizationCode.slice("mock-microsoft-authorization-code.".length)
|
|
: "";
|
|
let authorizedScopes = form.get("scope") ?? MICROSOFT_SCOPES.join(" ");
|
|
if (encodedScopes) {
|
|
try {
|
|
authorizedScopes = Buffer.from(encodedScopes, "base64url").toString("utf8");
|
|
} catch {
|
|
sendJson(response, 400, { error: "invalid_grant", error_description: "The mock authorization code is malformed." });
|
|
return true;
|
|
}
|
|
}
|
|
sendJson(response, 200, {
|
|
token_type: "Bearer",
|
|
scope: authorizedScopes,
|
|
expires_in: 3600,
|
|
access_token: MOCK_MICROSOFT_ACCESS_TOKEN,
|
|
refresh_token: MOCK_MICROSOFT_REFRESH_TOKEN,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
sendJson(response, 405, { error: "method_not_allowed" });
|
|
return true;
|
|
}
|
|
|
|
function hasMicrosoftBearer(request) {
|
|
return request.headers.authorization === `Bearer ${MOCK_MICROSOFT_ACCESS_TOKEN}`;
|
|
}
|
|
|
|
function graphEnvelope(value) {
|
|
return {
|
|
"@odata.context": "https://graph.microsoft.test/v1.0/$metadata#mock",
|
|
value,
|
|
};
|
|
}
|
|
|
|
function handleMicrosoftGraph(request, response, url) {
|
|
if (!url.pathname.startsWith("/graph/v1.0/")) return false;
|
|
if (!hasMicrosoftBearer(request)) {
|
|
sendJson(response, 401, { error: { code: "InvalidAuthenticationToken", message: "Access token is missing or invalid." } });
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/graph/v1.0/me" && request.method === "GET") {
|
|
sendJson(response, 200, {
|
|
id: "microsoft-user-openwork-test",
|
|
displayName: "OpenWork Tester",
|
|
givenName: "OpenWork",
|
|
surname: "Tester",
|
|
mail: "tester@example.test",
|
|
userPrincipalName: "tester@example.test",
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/graph/v1.0/me/messages" && request.method === "GET") {
|
|
const top = Number(url.searchParams.get("$top") ?? MICROSOFT_MESSAGES.length);
|
|
sendJson(response, 200, graphEnvelope(MICROSOFT_MESSAGES.slice(0, Number.isFinite(top) ? top : MICROSOFT_MESSAGES.length)));
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/graph/v1.0/me/messages" && request.method === "POST") {
|
|
sendJson(response, 201, {
|
|
id: "draft-openwork-test",
|
|
subject: "OpenWork permission parity draft",
|
|
bodyPreview: "Drafted by the deterministic Microsoft Graph mock.",
|
|
webLink: "https://outlook.office.test/mail/draft-openwork-test",
|
|
isDraft: true,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
const messageMatch = url.pathname.match(/^\/graph\/v1\.0\/me\/messages\/([^/]+)$/);
|
|
if (messageMatch && request.method === "GET") {
|
|
const message = MICROSOFT_MESSAGES.find((item) => item.id === decodeURIComponent(messageMatch[1]));
|
|
if (!message) {
|
|
sendJson(response, 404, { error: { code: "ErrorItemNotFound", message: "The message was not found." } });
|
|
return true;
|
|
}
|
|
sendJson(response, 200, message);
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
(url.pathname === "/graph/v1.0/me/calendarView" || url.pathname === "/graph/v1.0/me/events")
|
|
&& request.method === "GET"
|
|
) {
|
|
sendJson(response, 200, graphEnvelope(MICROSOFT_EVENTS));
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/graph/v1.0/me/events" && request.method === "POST") {
|
|
sendJson(response, 201, {
|
|
id: "event-openwork-test",
|
|
subject: "OpenWork permission parity review",
|
|
start: { dateTime: "2026-07-13T10:00:00Z", timeZone: "UTC" },
|
|
end: { dateTime: "2026-07-13T10:30:00Z", timeZone: "UTC" },
|
|
webLink: "https://outlook.office.test/calendar/event-openwork-test",
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
(url.pathname === "/graph/v1.0/me/drive/root/children" || url.pathname.startsWith("/graph/v1.0/me/drive/root/search"))
|
|
&& request.method === "GET"
|
|
) {
|
|
sendJson(response, 200, graphEnvelope([MICROSOFT_Q3_FILE]));
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/graph/v1.0/me/drive/items/file-q3-plan" && request.method === "GET") {
|
|
sendJson(response, 200, MICROSOFT_Q3_FILE);
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/graph/v1.0/me/drive/items/file-q3-plan/content" && request.method === "GET") {
|
|
sendText(response, 200, Q3_FILE_CONTENT);
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/graph/v1.0/me/drive/root:/OpenWork/permission-parity.txt:/content" && request.method === "PUT") {
|
|
sendJson(response, 201, {
|
|
id: "file-permission-parity",
|
|
name: "permission-parity.txt",
|
|
size: 37,
|
|
webUrl: "https://onedrive.office.test/files/file-permission-parity",
|
|
file: { mimeType: "text/plain" },
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/graph/v1.0/me/chats" && request.method === "GET") {
|
|
sendJson(response, 200, graphEnvelope([{
|
|
id: "chat-openwork-test",
|
|
topic: "OpenWork launch",
|
|
chatType: "group",
|
|
webUrl: "https://teams.office.test/chats/chat-openwork-test",
|
|
lastUpdatedDateTime: "2026-07-13T09:00:00Z",
|
|
}]));
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/graph/v1.0/chats/chat-openwork-test/messages" && request.method === "GET") {
|
|
sendJson(response, 200, graphEnvelope([{
|
|
id: "teams-message-existing",
|
|
createdDateTime: "2026-07-13T09:05:00Z",
|
|
body: { contentType: "text", content: "Ready for the permission review." },
|
|
from: { user: { id: "microsoft-user-openwork-test", displayName: "OpenWork Tester" } },
|
|
webUrl: "https://teams.office.test/messages/teams-message-existing",
|
|
}]));
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/graph/v1.0/chats/chat-openwork-test/messages" && request.method === "POST") {
|
|
sendJson(response, 201, {
|
|
id: "teams-message-sent",
|
|
createdDateTime: "2026-07-13T09:10:00Z",
|
|
body: { contentType: "text", content: "Permission parity verified." },
|
|
from: { user: { id: "microsoft-user-openwork-test", displayName: "OpenWork Tester" } },
|
|
webUrl: "https://teams.office.test/messages/teams-message-sent",
|
|
});
|
|
return true;
|
|
}
|
|
|
|
sendJson(response, 404, { error: { code: "Request_ResourceNotFound", message: `Unhandled mock Graph route: ${url.pathname}` } });
|
|
return true;
|
|
}
|
|
|
|
function hasWorkerDualAuth(request) {
|
|
return request.headers["x-openwork-host-token"] === MOCK_WORKER_HOST_TOKEN
|
|
&& request.headers.authorization === `Bearer ${MOCK_WORKER_CLIENT_TOKEN}`;
|
|
}
|
|
|
|
function workerUnauthorized(response) {
|
|
sendJson(response, 401, {
|
|
error: "worker_mock_unauthorized",
|
|
message: "Both the OpenWork host token and client bearer token are required.",
|
|
});
|
|
}
|
|
|
|
function workerMessage(session, role, id, text, parentID = null) {
|
|
return {
|
|
info: { id, sessionID: session.id, role, ...(parentID ? { parentID } : {}), time: { created: 1783612800000 } },
|
|
parts: [{ id: `${id}-part`, messageID: id, sessionID: session.id, type: "text", text }],
|
|
};
|
|
}
|
|
|
|
function workerMessages(session, includeReply) {
|
|
const messages = [];
|
|
if (session.prompt) {
|
|
messages.push(workerMessage(session, "user", session.messageId, session.prompt));
|
|
}
|
|
if (includeReply && session.prompt) {
|
|
messages.push(workerMessage(session, "assistant", `${session.messageId}-assistant`, `OpenWork worker reply: ${session.prompt}`, session.messageId));
|
|
}
|
|
return messages;
|
|
}
|
|
|
|
function handleWorker(state, request, response, url, rawBody) {
|
|
if (!url.pathname.startsWith("/worker/")) return false;
|
|
if (!hasWorkerDualAuth(request)) {
|
|
workerUnauthorized(response);
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/worker/workspaces" && request.method === "GET") {
|
|
sendJson(response, 200, {
|
|
activeId: "ws_mock_cloud",
|
|
items: [{ id: "ws_mock_cloud", name: "Mock cloud workspace", path: "/workspace" }],
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/worker/workspace/ws_mock_cloud/opencode/session" && request.method === "POST") {
|
|
let body;
|
|
try {
|
|
body = parseJson(rawBody);
|
|
} catch {
|
|
sendJson(response, 400, { error: "invalid_json" });
|
|
return true;
|
|
}
|
|
const id = `ses_mock_${state.worker.nextSessionId++}`;
|
|
const session = { id, title: typeof body.title === "string" ? body.title : "Cloud connect session", prompt: null, messageId: null, polls: 0 };
|
|
state.worker.sessions.set(id, session);
|
|
sendJson(response, 200, { id, title: session.title, directory: "/workspace" });
|
|
return true;
|
|
}
|
|
|
|
const promptMatch = url.pathname.match(
|
|
/^\/worker\/workspace\/ws_mock_cloud\/opencode\/session\/([^/]+)\/prompt_async$/,
|
|
);
|
|
if (promptMatch && request.method === "POST") {
|
|
const session = state.worker.sessions.get(decodeURIComponent(promptMatch[1]));
|
|
if (!session) {
|
|
sendJson(response, 404, { error: "session_not_found" });
|
|
return true;
|
|
}
|
|
let body;
|
|
try {
|
|
body = parseJson(rawBody);
|
|
} catch {
|
|
sendJson(response, 400, { error: "invalid_json" });
|
|
return true;
|
|
}
|
|
const textPart = Array.isArray(body.parts)
|
|
? body.parts.find((part) => part && part.type === "text" && typeof part.text === "string")
|
|
: null;
|
|
if (!textPart) {
|
|
sendJson(response, 400, { error: "text_part_required" });
|
|
return true;
|
|
}
|
|
session.prompt = textPart.text;
|
|
session.messageId = typeof body.messageID === "string" ? body.messageID : `${session.id}-user`;
|
|
session.polls = 0;
|
|
sendEmpty(response);
|
|
return true;
|
|
}
|
|
|
|
if (url.pathname === "/worker/workspace/ws_mock_cloud/opencode/session/status" && request.method === "GET") {
|
|
const statuses = {};
|
|
for (const session of state.worker.sessions.values()) {
|
|
statuses[session.id] = { type: session.polls++ === 0 ? "busy" : "idle" };
|
|
}
|
|
sendJson(response, 200, statuses);
|
|
return true;
|
|
}
|
|
|
|
const sessionMatch = url.pathname.match(
|
|
/^\/worker\/workspace\/ws_mock_cloud\/opencode\/session\/([^/]+)(?:\/(message|todo))?$/,
|
|
);
|
|
if (sessionMatch && request.method === "GET") {
|
|
const session = state.worker.sessions.get(decodeURIComponent(sessionMatch[1]));
|
|
if (!session) {
|
|
sendJson(response, 404, { error: "session_not_found" });
|
|
return true;
|
|
}
|
|
if (sessionMatch[2] === "message") {
|
|
sendJson(response, 200, workerMessages(session, session.polls > 0));
|
|
} else if (sessionMatch[2] === "todo") {
|
|
sendJson(response, 200, []);
|
|
} else {
|
|
sendJson(response, 200, {
|
|
id: session.id,
|
|
title: session.title,
|
|
directory: "/workspace",
|
|
time: { created: 1783612800000, updated: 1783612801000 },
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
|
|
sendJson(response, 404, { error: "worker_mock_not_found", path: url.pathname });
|
|
return true;
|
|
}
|
|
|
|
function stateView(state, origin) {
|
|
return {
|
|
endpoints: {
|
|
microsoftAuthorizeUrl: `${origin}/entra/{tenantId}/oauth2/v2.0/authorize`,
|
|
microsoftTokenUrl: `${origin}/entra/{tenantId}/oauth2/v2.0/token`,
|
|
microsoftGraphBaseUrl: `${origin}/graph/v1.0`,
|
|
workerBaseUrl: `${origin}/worker`,
|
|
},
|
|
credentials: {
|
|
microsoftAccessToken: MOCK_MICROSOFT_ACCESS_TOKEN,
|
|
workerHostToken: MOCK_WORKER_HOST_TOKEN,
|
|
workerClientToken: MOCK_WORKER_CLIENT_TOKEN,
|
|
},
|
|
worker: {
|
|
sessions: Array.from(state.worker.sessions.values()),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createCloudConnectMockServer(options = {}) {
|
|
const hostname = options.hostname ?? "127.0.0.1";
|
|
const port = options.port ?? 0;
|
|
const state = initialState();
|
|
|
|
const server = createServer(async (request, response) => {
|
|
const origin = `http://${request.headers.host ?? `${hostname}:${port}`}`;
|
|
const url = new URL(request.url ?? "/", origin);
|
|
const method = request.method ?? "GET";
|
|
|
|
if (method === "OPTIONS") {
|
|
sendEmpty(response);
|
|
return;
|
|
}
|
|
|
|
let rawBody = "";
|
|
try {
|
|
rawBody = method === "GET" || method === "HEAD" ? "" : await readBody(request);
|
|
} catch (error) {
|
|
sendJson(response, 400, { error: error instanceof Error ? error.message : "invalid_request" });
|
|
return;
|
|
}
|
|
recordRequest(state, request, url, rawBody);
|
|
|
|
if (url.pathname === "/health" && method === "GET") {
|
|
sendJson(response, 200, { ok: true, service: "cloud-connect-services-mock", ...stateView(state, origin) });
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === "/__mock/state" && method === "GET") {
|
|
sendJson(response, 200, stateView(state, origin));
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === "/__mock/requests" && method === "GET") {
|
|
sendJson(response, 200, { requests: state.requests });
|
|
return;
|
|
}
|
|
|
|
if (url.pathname === "/__mock/reset" && method === "POST") {
|
|
resetState(state);
|
|
sendJson(response, 200, { ok: true });
|
|
return;
|
|
}
|
|
|
|
if (handleMicrosoftOAuth(request, response, url, rawBody)) return;
|
|
if (handleMicrosoftGraph(request, response, url)) return;
|
|
if (handleWorker(state, request, response, url, rawBody)) return;
|
|
|
|
sendJson(response, 404, { error: "not_found", path: url.pathname });
|
|
});
|
|
|
|
return {
|
|
state,
|
|
async start() {
|
|
await new Promise((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(port, hostname, resolve);
|
|
});
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") throw new Error("mock_server_address_unavailable");
|
|
return {
|
|
origin: `http://${hostname}:${address.port}`,
|
|
port: address.port,
|
|
};
|
|
},
|
|
async stop() {
|
|
if (!server.listening) return;
|
|
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
},
|
|
};
|
|
}
|
|
|
|
function cliPort() {
|
|
const index = process.argv.indexOf("--port");
|
|
const raw = index === -1 ? process.env.PORT : process.argv[index + 1];
|
|
const port = Number(raw ?? 3979);
|
|
if (!Number.isInteger(port) || port <= 0) throw new Error(`Invalid port: ${raw}`);
|
|
return port;
|
|
}
|
|
|
|
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
if (isMain) {
|
|
const mock = createCloudConnectMockServer({
|
|
hostname: process.env.HOST ?? "127.0.0.1",
|
|
port: cliPort(),
|
|
});
|
|
const started = await mock.start();
|
|
console.log(`Cloud connection services mock listening at ${started.origin}`);
|
|
console.log(`Inspect endpoints and credentials at ${started.origin}/health`);
|
|
}
|