feat(runtime): 即梦本机运行时 - 接入安全生成闭环与桌面本机渠道
Refs #225 Co-authored-by: zkhyww <127009345+zkhyww@users.noreply.github.com>
This commit is contained in:
646
canvas-agent/test/local-runtime-http.test.ts
Normal file
646
canvas-agent/test/local-runtime-http.test.ts
Normal file
@@ -0,0 +1,646 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto, { type KeyObject } from "node:crypto";
|
||||
import http, { type Server } from "node:http";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "node:test";
|
||||
|
||||
import {
|
||||
canonicalRuntimeJson,
|
||||
createRuntimeRequestPayload,
|
||||
sha256Base64Url,
|
||||
type LocalRuntimeModuleDescriptor,
|
||||
} from "../src/local-runtime-contract.js";
|
||||
import { createLocalRuntimeApp, type LocalRuntimeModule } from "../src/local-runtime.js";
|
||||
import { createDreaminaHttpModule } from "../src/modules/dreamina-http.js";
|
||||
import {
|
||||
LocalRuntimeSessionManager,
|
||||
type RuntimeBrowserRegistration,
|
||||
} from "../src/local-runtime-session.js";
|
||||
|
||||
const origin = "http://127.0.0.1:3001";
|
||||
const hostileOrigin = "https://hostile.example";
|
||||
const authority = "127.0.0.1:41742";
|
||||
const endpoint = `http://${authority}`;
|
||||
const runtimeInstanceId = "runtime-http-fixture";
|
||||
const now = Date.parse("2026-08-10T00:00:00.000Z");
|
||||
|
||||
test("runtime info has exact Host and route-scoped CORS/PNA without exposing modules", async () => {
|
||||
await withRuntime(async ({ server }) => {
|
||||
const wrongHost = await request(server, {
|
||||
path: "/runtime/info",
|
||||
headers: { Host: "localhost:41742", Origin: hostileOrigin },
|
||||
});
|
||||
assert.equal(wrongHost.status, 421);
|
||||
assert.equal(wrongHost.headers["access-control-allow-origin"], undefined);
|
||||
|
||||
const info = await request(server, {
|
||||
path: "/runtime/info",
|
||||
headers: { Host: authority, Origin: hostileOrigin },
|
||||
});
|
||||
assert.equal(info.status, 200);
|
||||
assert.equal(info.headers["access-control-allow-origin"], hostileOrigin);
|
||||
assert.equal(info.headers["access-control-allow-credentials"], undefined);
|
||||
assert.equal(info.headers["cache-control"], "no-store, max-age=0");
|
||||
assert.deepEqual(JSON.parse(info.body), {
|
||||
runtime: "framefield-local-runtime",
|
||||
apiVersion: 2,
|
||||
protocolVersion: "framefield-runtime-session-v1",
|
||||
runtimeInstanceId,
|
||||
originTrusted: false,
|
||||
});
|
||||
assert.equal(info.body.includes("module"), false);
|
||||
assert.equal(info.body.includes("token"), false);
|
||||
|
||||
const preflight = await request(server, {
|
||||
method: "OPTIONS",
|
||||
path: "/runtime/info",
|
||||
headers: {
|
||||
Host: authority,
|
||||
Origin: hostileOrigin,
|
||||
"Access-Control-Request-Method": "GET",
|
||||
"Access-Control-Request-Private-Network": "true",
|
||||
},
|
||||
});
|
||||
assert.equal(preflight.status, 204);
|
||||
assert.equal(preflight.headers["access-control-allow-private-network"], "true");
|
||||
|
||||
const forbidden = await request(server, {
|
||||
method: "POST",
|
||||
path: "/runtime/session/challenge",
|
||||
headers: jsonHeaders(hostileOrigin),
|
||||
body: "{}",
|
||||
});
|
||||
assert.equal(forbidden.status, 403);
|
||||
assert.equal(forbidden.headers["access-control-allow-origin"], undefined);
|
||||
});
|
||||
});
|
||||
|
||||
test("only an exact trusted Origin can silently establish a signed browser session", async () => {
|
||||
await withRuntime(async ({ server }) => {
|
||||
const key = browserKey();
|
||||
const challenge = await challengeRequest(server, key.publicJwk);
|
||||
assert.equal(challenge.state, "challenge");
|
||||
assert.equal(JSON.stringify(challenge).includes("pair"), false);
|
||||
const session = await exchangeRequest(server, key.privateKey, challenge);
|
||||
assert.equal(session.keyId, challenge.keyId);
|
||||
|
||||
for (const rejectedOrigin of [undefined, "null", hostileOrigin]) {
|
||||
const rejected = await request(server, {
|
||||
method: "POST",
|
||||
path: "/runtime/session/challenge",
|
||||
headers: {
|
||||
Host: authority,
|
||||
...(rejectedOrigin ? { Origin: rejectedOrigin } : {}),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ publicKeyJwk: browserKey().publicJwk }),
|
||||
});
|
||||
assert.equal(rejected.status, 403);
|
||||
assert.equal(rejected.headers.location, undefined);
|
||||
}
|
||||
|
||||
for (const method of ["GET", "POST"]) {
|
||||
const removed = await request(server, { method, path: `/runtime/pair/${challenge.challengeId}`, headers: { Host: authority } });
|
||||
assert.equal(removed.status, 404);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("signed protected route maps its own scope and rejects replay or substituted paths before module invocation", async () => {
|
||||
let calls = 0;
|
||||
await withRuntime(async ({ server, manager }) => {
|
||||
const key = browserKey();
|
||||
const challenge = await challengeRequest(server, key.publicJwk);
|
||||
const session = await exchangeRequest(server, key.privateKey, challenge);
|
||||
const signed = signedHeaders(key.privateKey, session, "GET", "/dreamina/status", Buffer.alloc(0));
|
||||
|
||||
const accepted = await request(server, {
|
||||
path: "/dreamina/status",
|
||||
headers: { Host: authority, Origin: origin, ...signed },
|
||||
});
|
||||
assert.equal(accepted.status, 200);
|
||||
assert.equal(calls, 1);
|
||||
|
||||
const replay = await request(server, {
|
||||
path: "/dreamina/status",
|
||||
headers: { Host: authority, Origin: origin, ...signed },
|
||||
});
|
||||
assert.equal(replay.status, 409);
|
||||
assert.equal(calls, 1);
|
||||
|
||||
const freshDreaminaProof = signedHeaders(
|
||||
key.privateKey,
|
||||
session,
|
||||
"GET",
|
||||
"/dreamina/status",
|
||||
Buffer.alloc(0),
|
||||
);
|
||||
const substituted = await request(server, {
|
||||
path: "/runtime/status",
|
||||
headers: { Host: authority, Origin: origin, ...freshDreaminaProof },
|
||||
});
|
||||
assert.equal(substituted.status, 401);
|
||||
assert.equal(calls, 1);
|
||||
}, () => {
|
||||
calls += 1;
|
||||
});
|
||||
});
|
||||
|
||||
test("Dreamina effect routes require a signed dreamina:generate request before invocation", async () => {
|
||||
let calls = 0;
|
||||
await withRuntime(async ({ server }) => {
|
||||
const key = browserKey();
|
||||
const challenge = await challengeRequest(server, key.publicJwk);
|
||||
const session = await exchangeRequest(server, key.privateKey, challenge);
|
||||
const path = "/dreamina/generate/effects/claim";
|
||||
const body = Buffer.from(JSON.stringify({
|
||||
consumerId: "web-generation-materializer",
|
||||
taskId: "dreamina:signed-effect-task-0001",
|
||||
effectKey: "materialize:dreamina:signed-effect-task-0001:0",
|
||||
}));
|
||||
|
||||
const unsigned = await request(server, {
|
||||
method: "POST",
|
||||
path,
|
||||
headers: jsonHeaders(origin),
|
||||
body: body.toString("utf8"),
|
||||
});
|
||||
assert.equal(unsigned.status, 401);
|
||||
assert.equal(calls, 0);
|
||||
|
||||
const substitutedBody = Buffer.from(JSON.stringify({ consumerId: "substituted" }));
|
||||
const substituted = await request(server, {
|
||||
method: "POST",
|
||||
path,
|
||||
headers: {
|
||||
...jsonHeaders(origin),
|
||||
...signedHeaders(key.privateKey, session, "POST", path, body),
|
||||
},
|
||||
body: substitutedBody.toString("utf8"),
|
||||
});
|
||||
assert.equal(substituted.status, 401);
|
||||
assert.equal(calls, 0);
|
||||
|
||||
const accepted = await request(server, {
|
||||
method: "POST",
|
||||
path,
|
||||
headers: {
|
||||
...jsonHeaders(origin),
|
||||
...signedHeaders(key.privateKey, session, "POST", path, body),
|
||||
},
|
||||
body: body.toString("utf8"),
|
||||
});
|
||||
assert.equal(accepted.status, 200);
|
||||
assert.equal(calls, 1);
|
||||
}, undefined, () => {
|
||||
calls += 1;
|
||||
});
|
||||
});
|
||||
|
||||
test("production Dreamina effect claim composes signing scope and strict JSON in the real runtime app", async () => {
|
||||
await withProductionDreaminaRuntime(async ({ server }) => {
|
||||
const key = browserKey();
|
||||
const challenge = await challengeRequest(server, key.publicJwk);
|
||||
const session = await exchangeRequest(server, key.privateKey, challenge);
|
||||
const route = "/dreamina/generate/effects/claim";
|
||||
const body = Buffer.from(JSON.stringify({
|
||||
consumerId: "web-generation-materializer",
|
||||
taskId: "dreamina:dreamina-http-effect-task-0001",
|
||||
effectKey: "materialize:dreamina:dreamina-http-effect-task-0001:0",
|
||||
}));
|
||||
|
||||
const substitutedBody = Buffer.from(JSON.stringify({
|
||||
consumerId: "web-generation-materializer",
|
||||
taskId: "dreamina:dreamina-http-effect-task-0001",
|
||||
effectKey: "materialize:dreamina:dreamina-http-effect-task-0001:0",
|
||||
unexpected: true,
|
||||
}));
|
||||
const substituted = await request(server, {
|
||||
method: "POST",
|
||||
path: route,
|
||||
headers: {
|
||||
...jsonHeaders(origin),
|
||||
...signedHeaders(key.privateKey, session, "POST", route, body),
|
||||
},
|
||||
body: substitutedBody.toString("utf8"),
|
||||
});
|
||||
assert.equal(substituted.status, 401);
|
||||
|
||||
const unexpected = await request(server, {
|
||||
method: "POST",
|
||||
path: route,
|
||||
headers: {
|
||||
...jsonHeaders(origin),
|
||||
...signedHeaders(key.privateKey, session, "POST", route, substitutedBody),
|
||||
},
|
||||
body: substitutedBody.toString("utf8"),
|
||||
});
|
||||
assert.equal(unexpected.status, 400);
|
||||
assert.equal((JSON.parse(unexpected.body) as { code?: string }).code, "dreamina_request_invalid");
|
||||
|
||||
const accepted = await request(server, {
|
||||
method: "POST",
|
||||
path: route,
|
||||
headers: {
|
||||
...jsonHeaders(origin),
|
||||
...signedHeaders(key.privateKey, session, "POST", route, body),
|
||||
},
|
||||
body: body.toString("utf8"),
|
||||
});
|
||||
assert.equal(accepted.status, 200);
|
||||
assert.equal((JSON.parse(accepted.body) as { result?: { status?: string } }).result?.status, "claimed");
|
||||
});
|
||||
|
||||
await withProductionDreaminaRuntime(async ({ server }) => {
|
||||
const key = browserKey();
|
||||
const challenge = await challengeRequest(server, key.publicJwk);
|
||||
const session = await exchangeRequest(server, key.privateKey, challenge);
|
||||
assert.equal(session.scopes.includes("dreamina:generate"), false);
|
||||
const route = "/dreamina/generate/effects/claim";
|
||||
const body = Buffer.from(JSON.stringify({
|
||||
consumerId: "web-generation-materializer",
|
||||
taskId: "dreamina:dreamina-http-effect-task-0001",
|
||||
effectKey: "materialize:dreamina:dreamina-http-effect-task-0001:0",
|
||||
}));
|
||||
const denied = await request(server, {
|
||||
method: "POST",
|
||||
path: route,
|
||||
headers: {
|
||||
...jsonHeaders(origin),
|
||||
...signedHeaders(key.privateKey, session, "POST", route, body),
|
||||
},
|
||||
body: body.toString("utf8"),
|
||||
});
|
||||
assert.equal(denied.status, 403);
|
||||
assert.equal((JSON.parse(denied.body) as { code?: string }).code, "scope_denied");
|
||||
}, ["dreamina:status"]);
|
||||
});
|
||||
|
||||
test("production Dreamina task list accepts the signed canonical pagination query", async () => {
|
||||
await withProductionDreaminaRuntime(async ({ server }) => {
|
||||
const key = browserKey();
|
||||
const challenge = await challengeRequest(server, key.publicJwk);
|
||||
const session = await exchangeRequest(server, key.privateKey, challenge);
|
||||
const route = "/dreamina/generate/tasks?activeOnly=true&limit=100";
|
||||
const response = await request(server, {
|
||||
path: route,
|
||||
headers: {
|
||||
Host: authority,
|
||||
Origin: origin,
|
||||
...signedHeaders(key.privateKey, session, "GET", route, Buffer.alloc(0)),
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const body = JSON.parse(response.body) as { ok?: boolean; result?: { tasks?: unknown[] } };
|
||||
assert.equal(body.ok, true);
|
||||
assert.equal(Array.isArray(body.result?.tasks), true);
|
||||
});
|
||||
});
|
||||
|
||||
test("signed session and registration revoke invalidate the intended authorization", async () => {
|
||||
await withRuntime(async ({ server, manager }) => {
|
||||
const key = browserKey();
|
||||
const challenge = await challengeRequest(server, key.publicJwk);
|
||||
const firstSession = await exchangeRequest(server, key.privateKey, challenge);
|
||||
const emptyBody = Buffer.from("{}");
|
||||
|
||||
const revokeSession = await request(server, {
|
||||
method: "POST",
|
||||
path: "/runtime/session/revoke",
|
||||
headers: {
|
||||
...jsonHeaders(origin),
|
||||
...signedHeaders(key.privateKey, firstSession, "POST", "/runtime/session/revoke", emptyBody),
|
||||
},
|
||||
body: emptyBody.toString("utf8"),
|
||||
});
|
||||
assert.equal(revokeSession.status, 200);
|
||||
assert.deepEqual(JSON.parse(revokeSession.body), { ok: true });
|
||||
assert.equal(revokeSession.headers["access-control-allow-origin"], origin);
|
||||
|
||||
const rejectedSession = await request(server, {
|
||||
path: "/runtime/status",
|
||||
headers: {
|
||||
Host: authority,
|
||||
Origin: origin,
|
||||
...signedHeaders(key.privateKey, firstSession, "GET", "/runtime/status", Buffer.alloc(0)),
|
||||
},
|
||||
});
|
||||
assert.equal(rejectedSession.status, 401);
|
||||
|
||||
const secondChallenge = await challengeRequest(server, undefined, firstSession.keyId);
|
||||
const secondSession = await exchangeRequest(server, key.privateKey, secondChallenge);
|
||||
const thirdChallenge = await challengeRequest(server, undefined, firstSession.keyId);
|
||||
const thirdSession = await exchangeRequest(server, key.privateKey, thirdChallenge);
|
||||
const revokeRegistration = await request(server, {
|
||||
method: "POST",
|
||||
path: "/runtime/session/registration/revoke",
|
||||
headers: {
|
||||
...jsonHeaders(origin),
|
||||
...signedHeaders(key.privateKey, secondSession, "POST", "/runtime/session/registration/revoke", emptyBody),
|
||||
},
|
||||
body: emptyBody.toString("utf8"),
|
||||
});
|
||||
assert.equal(revokeRegistration.status, 200);
|
||||
assert.deepEqual(JSON.parse(revokeRegistration.body), { ok: true });
|
||||
|
||||
const rejectedSiblingSession = await request(server, {
|
||||
path: "/runtime/status",
|
||||
headers: {
|
||||
Host: authority,
|
||||
Origin: origin,
|
||||
...signedHeaders(key.privateKey, thirdSession, "GET", "/runtime/status", Buffer.alloc(0)),
|
||||
},
|
||||
});
|
||||
assert.equal(rejectedSiblingSession.status, 401);
|
||||
|
||||
const rejectedRegistration = await request(server, {
|
||||
method: "POST",
|
||||
path: "/runtime/session/challenge",
|
||||
headers: jsonHeaders(origin),
|
||||
body: JSON.stringify({ keyId: firstSession.keyId }),
|
||||
});
|
||||
assert.equal(rejectedRegistration.status, 404);
|
||||
assert.equal(JSON.parse(rejectedRegistration.body).code, "registration_not_found");
|
||||
});
|
||||
});
|
||||
|
||||
async function withProductionDreaminaRuntime(
|
||||
run: (fixture: { server: Server; manager: LocalRuntimeSessionManager }) => Promise<void>,
|
||||
scopes?: ConstructorParameters<typeof LocalRuntimeSessionManager>[0]["scopes"],
|
||||
) {
|
||||
const configDir = await fs.mkdtemp(path.join(os.tmpdir(), "dreamina-production-http-"));
|
||||
const idempotencyKey = "dreamina-http-effect-task-0001";
|
||||
await fs.writeFile(path.join(configDir, "dreamina-runtime-state.json"), JSON.stringify({
|
||||
version: 1,
|
||||
records: [{
|
||||
ownerId: "owner-production-http-0001",
|
||||
idempotencyKey,
|
||||
requestHash: "a".repeat(64),
|
||||
state: "accepted",
|
||||
updatedAt: "2026-08-13T00:00:00.000Z",
|
||||
submitId: "opaque-fixture-id",
|
||||
taskVersion: 1,
|
||||
operation: "text2video",
|
||||
mode: "video",
|
||||
model: "seedance2.0mini",
|
||||
createdAt: "2026-08-13T00:00:00.000Z",
|
||||
}],
|
||||
}));
|
||||
const registrations: RuntimeBrowserRegistration[] = [];
|
||||
const manager = new LocalRuntimeSessionManager({
|
||||
endpoint,
|
||||
runtimeInstanceId,
|
||||
trustedOrigins: [origin],
|
||||
registrations,
|
||||
now: () => now,
|
||||
scopes,
|
||||
});
|
||||
const task = (id: string) => ({
|
||||
id,
|
||||
provider: "dreamina-cli" as const,
|
||||
mode: "video" as const,
|
||||
operation: "text2video" as const,
|
||||
model: "seedance2.0mini",
|
||||
status: "running" as const,
|
||||
stage: "submitted" as const,
|
||||
progress: 10,
|
||||
receiptRecorded: true,
|
||||
createdAt: "2026-08-13T00:00:00.000Z",
|
||||
updatedAt: "2026-08-13T00:00:00.000Z",
|
||||
});
|
||||
const module = createDreaminaHttpModule({
|
||||
ownerId: "owner-production-http-0001",
|
||||
configDir,
|
||||
dreaminaRuntime: {
|
||||
run: async () => ({ state: "accepted" as const, submitId: "opaque-fixture-id" }),
|
||||
generateToResult: async () => ({ mode: "video" as const, video: { dataUrl: "data:video/mp4;base64,AA==", mimeType: "video/mp4", bytes: 1 } }),
|
||||
resumeToResult: async () => ({ mode: "video" as const, video: { dataUrl: "data:video/mp4;base64,AA==", mimeType: "video/mp4", bytes: 1 } }),
|
||||
getTask: async (id: string) => task(id),
|
||||
waitForTask: async () => ({ mode: "video" as const, video: { dataUrl: "data:video/mp4;base64,AA==", mimeType: "video/mp4", bytes: 1 } }),
|
||||
refreshTask: async (id: string) => task(id),
|
||||
listTasks: async () => [task(idempotencyKey)],
|
||||
cancelTask: async (id: string) => ({ ...task(id), status: "cancelled" as const, stage: "cancelled" as const }),
|
||||
deleteTask: async () => ({ deleted: true as const }),
|
||||
enqueue: async () => task(idempotencyKey),
|
||||
},
|
||||
});
|
||||
const app = createLocalRuntimeApp({
|
||||
authority,
|
||||
endpoint,
|
||||
version: "0.1.0",
|
||||
sessionManager: manager,
|
||||
modules: [module],
|
||||
});
|
||||
const server = app.listen(0, "127.0.0.1");
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("listening", resolve);
|
||||
server.once("error", reject);
|
||||
});
|
||||
try {
|
||||
await run({ server, manager });
|
||||
} finally {
|
||||
manager.dispose();
|
||||
await module.dispose?.();
|
||||
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
||||
await fs.rm(configDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function withRuntime(
|
||||
run: (fixture: { server: Server; manager: LocalRuntimeSessionManager }) => Promise<void>,
|
||||
onDreaminaStatus: (() => void) | undefined = () => undefined,
|
||||
onDreaminaEffect: () => void = () => undefined,
|
||||
) {
|
||||
const registrations: RuntimeBrowserRegistration[] = [];
|
||||
const manager = new LocalRuntimeSessionManager({
|
||||
endpoint,
|
||||
runtimeInstanceId,
|
||||
trustedOrigins: [origin],
|
||||
registrations,
|
||||
now: () => now,
|
||||
});
|
||||
const descriptor: LocalRuntimeModuleDescriptor = {
|
||||
id: "dreamina",
|
||||
displayName: "Dreamina CLI",
|
||||
apiVersion: 1,
|
||||
scopes: ["dreamina:status", "dreamina:generate"],
|
||||
};
|
||||
const module: LocalRuntimeModule = {
|
||||
descriptor,
|
||||
routes: [
|
||||
{
|
||||
method: "GET",
|
||||
path: "/dreamina/status",
|
||||
scope: "dreamina:status",
|
||||
handler: (_req, res) => {
|
||||
onDreaminaStatus?.();
|
||||
res.json({ ok: true });
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/dreamina/generate/effects/claim",
|
||||
scope: "dreamina:generate",
|
||||
handler: (_req, res) => {
|
||||
onDreaminaEffect();
|
||||
res.json({ ok: true });
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const app = createLocalRuntimeApp({
|
||||
authority,
|
||||
endpoint,
|
||||
version: "0.1.0",
|
||||
sessionManager: manager,
|
||||
modules: [module],
|
||||
});
|
||||
const server = app.listen(0, "127.0.0.1");
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("listening", resolve);
|
||||
server.once("error", reject);
|
||||
});
|
||||
try {
|
||||
await run({ server, manager });
|
||||
} finally {
|
||||
manager.dispose();
|
||||
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
||||
}
|
||||
}
|
||||
|
||||
async function challengeRequest(server: Server, publicKeyJwk?: JsonWebKey, keyId?: string) {
|
||||
const response = await request(server, {
|
||||
method: "POST",
|
||||
path: "/runtime/session/challenge",
|
||||
headers: jsonHeaders(origin),
|
||||
body: JSON.stringify(publicKeyJwk ? { publicKeyJwk } : { keyId }),
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
return JSON.parse(response.body) as {
|
||||
state: "challenge";
|
||||
challengeId: string;
|
||||
nonce: string;
|
||||
runtimeInstanceId: string;
|
||||
expiresAt: string;
|
||||
keyId: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function exchangeRequest(
|
||||
server: Server,
|
||||
privateKey: KeyObject,
|
||||
challenge: Awaited<ReturnType<typeof challengeRequest>>,
|
||||
) {
|
||||
const signature = crypto.sign("sha256", Buffer.from(canonicalRuntimeJson({
|
||||
protocol: "framefield-runtime-session-v1",
|
||||
challengeId: challenge.challengeId,
|
||||
nonce: challenge.nonce,
|
||||
origin,
|
||||
endpoint,
|
||||
runtimeInstanceId,
|
||||
expiresAt: challenge.expiresAt,
|
||||
})), { key: privateKey, dsaEncoding: "ieee-p1363" }).toString("base64url");
|
||||
const response = await request(server, {
|
||||
method: "POST",
|
||||
path: "/runtime/session/exchange",
|
||||
headers: jsonHeaders(origin),
|
||||
body: JSON.stringify({ challengeId: challenge.challengeId, signature }),
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
return JSON.parse(response.body) as {
|
||||
sessionId: string;
|
||||
keyId: string;
|
||||
scopes: string[];
|
||||
expiresAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
function signedHeaders(
|
||||
privateKey: KeyObject,
|
||||
session: { sessionId: string; keyId: string; expiresAt: string },
|
||||
method: string,
|
||||
pathAndQuery: string,
|
||||
body: Buffer,
|
||||
) {
|
||||
const requestNonce = crypto.randomBytes(16).toString("base64url");
|
||||
const payload = createRuntimeRequestPayload({
|
||||
sessionId: session.sessionId,
|
||||
keyId: session.keyId,
|
||||
method,
|
||||
pathAndQuery,
|
||||
bodySha256: sha256Base64Url(body),
|
||||
lastEventId: null,
|
||||
origin,
|
||||
endpoint,
|
||||
runtimeInstanceId,
|
||||
requestNonce,
|
||||
timestamp: now,
|
||||
sessionExpiresAt: session.expiresAt,
|
||||
});
|
||||
return {
|
||||
"X-Framefield-Runtime-Session": session.sessionId,
|
||||
"X-Framefield-Runtime-Timestamp": String(now),
|
||||
"X-Framefield-Runtime-Nonce": requestNonce,
|
||||
"X-Framefield-Runtime-Proof": crypto.sign(
|
||||
"sha256",
|
||||
Buffer.from(canonicalRuntimeJson(payload)),
|
||||
{ key: privateKey, dsaEncoding: "ieee-p1363" },
|
||||
).toString("base64url"),
|
||||
};
|
||||
}
|
||||
|
||||
function browserKey() {
|
||||
const { publicKey, privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
||||
const exported = publicKey.export({ format: "jwk" });
|
||||
return {
|
||||
privateKey,
|
||||
publicJwk: {
|
||||
kty: exported.kty,
|
||||
crv: exported.crv,
|
||||
x: exported.x,
|
||||
y: exported.y,
|
||||
ext: true,
|
||||
key_ops: ["verify"],
|
||||
} satisfies JsonWebKey,
|
||||
};
|
||||
}
|
||||
|
||||
function jsonHeaders(requestOrigin: string) {
|
||||
return { Host: authority, Origin: requestOrigin, "Content-Type": "application/json" };
|
||||
}
|
||||
|
||||
function request(
|
||||
server: Server,
|
||||
options: {
|
||||
method?: string;
|
||||
path: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
},
|
||||
) {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("fixture server is not listening");
|
||||
return new Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }>((resolve, reject) => {
|
||||
const req = http.request({
|
||||
hostname: "127.0.0.1",
|
||||
port: address.port,
|
||||
method: options.method ?? "GET",
|
||||
path: options.path,
|
||||
headers: options.headers,
|
||||
}, (res) => {
|
||||
const chunks: Buffer[] = [];
|
||||
res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
res.on("end", () => resolve({
|
||||
status: res.statusCode ?? 0,
|
||||
headers: res.headers,
|
||||
body: Buffer.concat(chunks).toString("utf8"),
|
||||
}));
|
||||
});
|
||||
req.once("error", reject);
|
||||
if (options.body) req.write(options.body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user