Files

289 lines
11 KiB
JavaScript

import assert from "node:assert/strict";
import { Buffer } from "node:buffer";
import { createPrivateKey, generateKeyPairSync, sign } from "node:crypto";
import { mkdtemp, readFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import {
createConnectLinkReplayGuard,
desktopBootstrapFromConnectClaims,
extractConnectExchange,
extractConnectLinkToken,
resolveConnectExchangeUrl,
verifyConnectLinkToken,
verifyConnectLinkUrl,
} from "./connect-link.mjs";
import {
applyDesktopBootstrapBrandIcon,
persistConnectLinkBranding,
} from "./connect-link-branding.mjs";
const NOW = 1_783_000_000;
const KID = "owc-test";
const { publicKey: publicKeyPem, privateKey: privateKeyPem } = generateKeyPairSync("ed25519", {
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs8", format: "pem" },
});
const publicKeys = { [KID]: publicKeyPem };
/**
* @param {Record<string, unknown>} [overrides]
* @returns {import("@openwork/types/connect-link").ConnectLinkClaims}
*/
function claims(overrides = {}) {
return {
iss: "https://api.openwork.acme.example.com",
aud: "openwork-desktop-connect",
iat: NOW,
exp: NOW + 72 * 3600,
jti: "test-jti-0001",
v: 1,
org: { name: "Acme Robotics" },
brand: { appName: "Acme Work", logoUrl: null, iconUrl: null },
den: {
baseUrl: "https://openwork.acme.example.com",
apiBaseUrl: "https://api.openwork.acme.example.com",
},
requireSignin: true,
...overrides,
};
}
/**
* @param {Record<string, unknown>} payload
* @param {Record<string, unknown>} [header]
*/
function mint(payload, header = { alg: "EdDSA", typ: "JWT", kid: KID }) {
const encode = (value) => Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
const signingInput = `${encode(header)}.${encode(payload)}`;
const signature = sign(null, Buffer.from(signingInput), createPrivateKey(privateKeyPem));
return `${signingInput}.${signature.toString("base64url")}`;
}
function verifyAt(token, nowEpochSeconds = NOW, extra = {}) {
return verifyConnectLinkToken({ token, publicKeys, nowEpochSeconds, ...extra });
}
/** @param {import("@openwork/types/connect-link").ConnectLinkVerifyResult} result */
function failureOf(result) {
assert.equal(result.ok, false);
if (result.ok !== false) throw new Error("expected a failure result");
return result;
}
test("extracts only dedicated desktop connect links", () => {
assert.equal(extractConnectLinkToken("openwork://connect?token=a.b.c"), "a.b.c");
assert.equal(extractConnectLinkToken("openwork-dev://connect?token=a.b.c"), "a.b.c");
assert.equal(extractConnectLinkToken("openwork:///connect?token=a.b.c"), "a.b.c");
assert.equal(extractConnectLinkToken("openwork://den-auth?grant=x"), null);
assert.equal(extractConnectLinkToken("https://connect?token=a.b.c"), null);
});
test("verifies an exact signed organization target end to end", () => {
const expected = claims();
const token = mint(expected);
const result = verifyConnectLinkUrl(
`openwork://connect?token=${encodeURIComponent(token)}`,
{ publicKeys, nowEpochSeconds: NOW },
);
assert.equal(result.ok, true);
assert.deepEqual(result.claims, expected);
assert.equal(result.kid, KID);
assert.equal(result.transport, "signed");
});
test("resolves a keyless exchange through the exact HTTPS Den endpoint", async () => {
const code = "abcdefghijklmnopqrstuvwxyz123456";
const apiBaseUrl = "https://api.openwork.acme.example.com/api/den";
const expected = claims({
iss: apiBaseUrl,
brand: {
appName: "Acme Work",
logoUrl: "https://assets.acme.example.com/wordmark.svg",
iconUrl: "https://assets.acme.example.com/icon.png",
},
den: {
baseUrl: "https://openwork.acme.example.com",
apiBaseUrl,
},
});
const rawUrl = `openwork://connect?code=${code}&apiBaseUrl=${encodeURIComponent(apiBaseUrl)}`;
assert.deepEqual(extractConnectExchange(rawUrl), { code, apiBaseUrl });
const calls = [];
const result = await resolveConnectExchangeUrl(rawUrl, {
mode: "preview",
nowEpochSeconds: NOW,
fetcher: (url, init) => {
calls.push({ url, init });
return Promise.resolve(Response.json({ claims: expected }));
},
});
assert.equal(result.ok, true);
if (!result.ok) throw new Error("expected exchange success");
assert.equal(result.transport, "exchange");
assert.equal(result.kid, null);
assert.deepEqual(result.claims, expected);
assert.equal(calls[0].url, `${apiBaseUrl}/v1/install-connect/preview`);
assert.deepEqual(JSON.parse(calls[0].init.body), { code });
assert.equal(calls[0].init.redirect, "error");
assert.deepEqual(desktopBootstrapFromConnectClaims(result.claims), {
baseUrl: "https://openwork.acme.example.com",
apiBaseUrl,
requireSignin: true,
brandAppName: "Acme Work",
brandLogoUrl: "https://assets.acme.example.com/wordmark.svg",
brandIconUrl: "https://assets.acme.example.com/icon.png",
});
});
test("persists accepted Enterprise activation and branding before applying its icon", async () => {
const expectedClaims = claims({
brand: {
appName: "Acme Work",
logoUrl: null,
iconUrl: "https://assets.acme.example.com/icon.png",
},
});
const enterpriseActivation = {
activatedAt: "2026-07-27T14:00:00.000Z",
denBaseUrl: expectedClaims.den.baseUrl,
};
const calls = [];
const config = await persistConnectLinkBranding(expectedClaims, {
persistBootstrap: async (nextConfig) => {
calls.push({ type: "persist", config: nextConfig });
return {
...nextConfig,
baseUrl: expectedClaims.den.baseUrl,
requireSignin: expectedClaims.requireSignin,
writtenAt: "2026-07-15T12:00:00.000Z",
};
},
applyBrandIconUrl: async (iconUrl) => {
calls.push({ type: "apply", iconUrl });
return { ok: true };
},
enterpriseActivation,
});
assert.deepEqual(calls.map((call) => call.type), ["persist", "apply"]);
assert.deepEqual(calls[0].config.enterpriseActivation, enterpriseActivation);
assert.equal(calls[0].config.brandAppName, "Acme Work");
assert.equal(calls[0].config.brandIconUrl, "https://assets.acme.example.com/icon.png");
assert.equal(calls[1].iconUrl, expectedClaims.brand.iconUrl);
assert.equal(config.writtenAt, "2026-07-15T12:00:00.000Z");
});
test("startup branding applies a saved icon and skips an absent one", async () => {
const applied = [];
const applyBrandIconUrl = async (iconUrl) => {
applied.push(iconUrl);
return { ok: true };
};
await applyDesktopBootstrapBrandIcon({ brandIconUrl: "https://assets.acme.example.com/icon.png" }, applyBrandIconUrl);
await applyDesktopBootstrapBrandIcon({}, applyBrandIconUrl);
assert.deepEqual(applied, ["https://assets.acme.example.com/icon.png"]);
});
test("fails closed for ambiguous, insecure, mismatched, expired, and replayed exchanges", async () => {
const code = "abcdefghijklmnopqrstuvwxyz123456";
const apiBaseUrl = "https://api.openwork.acme.example.com";
const rawUrl = `openwork://connect?code=${code}&apiBaseUrl=${encodeURIComponent(apiBaseUrl)}`;
assert.equal(extractConnectExchange(`${rawUrl}&token=a.b.c`), null);
const insecure = `openwork://connect?code=${code}&apiBaseUrl=${encodeURIComponent("http://den.example.com")}`;
assert.equal(failureOf(await resolveConnectExchangeUrl(insecure, {
mode: "preview",
fetcher: () => Promise.reject(new Error("must not fetch")),
})).code, "insecure_url");
const mismatched = await resolveConnectExchangeUrl(rawUrl, {
mode: "preview",
nowEpochSeconds: NOW,
fetcher: () => Promise.resolve(Response.json({ claims: claims({
iss: "https://other.example.com",
den: {
baseUrl: "https://openwork.acme.example.com",
apiBaseUrl: "https://other.example.com",
},
}) })),
});
assert.equal(failureOf(mismatched).code, "malformed_claims");
for (const failureCase of [
{ status: 409, expectedCode: "replayed" },
{ status: 410, expectedCode: "expired" },
{ status: 404, expectedCode: "invalid_token" },
]) {
const result = await resolveConnectExchangeUrl(rawUrl, {
mode: "exchange",
fetcher: () => Promise.resolve(Response.json({}, { status: failureCase.status })),
});
assert.equal(failureOf(result).code, failureCase.expectedCode);
}
const unavailable = await resolveConnectExchangeUrl(rawUrl, {
mode: "preview",
fetcher: () => Promise.reject(new Error("offline")),
});
assert.equal(failureOf(unavailable).code, "unavailable");
});
test("rejects tampering, algorithm confusion, and unknown keys", () => {
const token = mint(claims());
const [header, , signature] = token.split(".");
const forged = Buffer.from(JSON.stringify(claims({
den: { baseUrl: "https://evil.example.com" },
}))).toString("base64url");
assert.equal(failureOf(verifyAt(`${header}.${forged}.${signature}`)).code, "bad_signature");
for (const invalidHeader of [
{ alg: "none", kid: KID },
{ alg: "HS256", kid: KID },
{ alg: "EdDSA", kid: KID, crit: ["exp"] },
]) {
assert.equal(verifyAt(mint(claims(), invalidHeader)).ok, false);
}
assert.equal(failureOf(verifyAt(mint(claims(), { alg: "EdDSA", kid: "other" }))).code, "unknown_kid");
});
test("enforces time and HTTPS policies", () => {
const token = mint(claims());
assert.equal(verifyAt(token, NOW + 72 * 3600 + 30).ok, true);
assert.equal(failureOf(verifyAt(token, NOW + 72 * 3600 + 61)).code, "expired");
assert.equal(failureOf(verifyAt(token, NOW - 61)).code, "not_yet_valid");
const intranet = mint(claims({ den: { baseUrl: "http://intranet.example.com" } }));
assert.equal(failureOf(verifyAt(intranet, NOW, { allowInsecureLoopback: true })).code, "insecure_url");
const loopback = mint(claims({ den: { baseUrl: "http://127.0.0.1:8790" } }));
assert.equal(verifyAt(loopback).ok, false);
assert.equal(verifyAt(loopback, NOW, { allowInsecureLoopback: true }).ok, true);
});
test("replay guard is persistent, atomic, and bounded", async () => {
const dir = await mkdtemp(path.join(os.tmpdir(), "connect-link-test-"));
const filePath = path.join(dir, "seen.json");
const guard = createConnectLinkReplayGuard({ filePath });
assert.equal(await guard.remember("jti-1"), true);
assert.equal(await guard.remember("jti-1"), false);
const reloaded = createConnectLinkReplayGuard({ filePath });
assert.equal(await reloaded.has("jti-1"), true);
const concurrent = await Promise.all([
reloaded.remember("jti-concurrent"),
reloaded.remember("jti-concurrent"),
]);
assert.deepEqual(concurrent.sort(), [false, true]);
for (let index = 0; index < 520; index += 1) {
await reloaded.remember(`jti-fill-${index}`);
}
const persisted = JSON.parse(await readFile(filePath, "utf8"));
assert.ok(persisted.length <= 512);
});