// Connected-services core (docs/connected-services.md): the capability-grant // crypto (sealForPub/openSealed), the OAuth PKCE helper, and the DNSimple client // (token exchange + read-only checks). import http from "node:http"; import crypto from "node:crypto"; import { generateUserKeypair, sealForPub, openSealed, pkcePair } from "../web/static/crypto.js"; function assert(cond, msg) { if (!cond) { console.error("FAIL:", msg); process.exit(1); } } // A grant round-trips WITH its metadata, and only the bot it was sealed to can // open it — the whole basis for handing a bot a time-boxed capability. async function testGrant() { const bot = await generateUserKeypair(); const other = await generateUserKeypair(); const grant = { service: "dnsimple", token: "dnsimple_scoped_abc", exp: 1893456000000 }; const sealed = await sealForPub(bot.publicKey, grant); const opened = await openSealed(sealed, bot.privateKey); assert( opened.token === grant.token && opened.service === "dnsimple" && opened.exp === grant.exp, "grant seals/opens with its metadata intact", ); let threw = false; try { await openSealed(sealed, other.privateKey); } catch { threw = true; } assert(threw, "a grant sealed to one bot can't be opened by another key"); } // A fake DNSimple so the bot's read-only availability check is exercised without // a real account (LCHAT_DNSIMPLE_BASE is read at import time, so import late). // A PKCE verifier's challenge is exactly base64url(S256(verifier)), a fresh pair // each call — the basis of authorizing with no client secret. async function testPkce() { const { verifier, challenge } = await pkcePair(); const expected = crypto.createHash("sha256").update(verifier).digest("base64url"); assert(challenge === expected, "pkce challenge is base64url(S256(verifier))"); assert(verifier.length >= 43 && !/[+/=]/.test(verifier), "verifier is url-safe and long enough (RFC 7636)"); assert((await pkcePair()).verifier !== verifier, "each pkcePair is fresh"); } function startMock() { const server = http.createServer((req, res) => { const send = (obj, code = 200) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); }; // Connect broker exchange endpoint: bearer-authed, returns an access token. if (req.method === "POST" && req.url === "/api/exchange") { if (!/^Bearer /.test(req.headers.authorization || "")) return send({ message: "unauthorized" }, 401); let body = ""; req.on("data", (ch) => (body += ch)); req.on("end", () => { const p = JSON.parse(body || "{}"); if (p.code) return send({ access_token: "scoped_from_exchange" }); send({ message: "bad exchange request" }, 400); }); return; } if (!/^Bearer scoped_/.test(req.headers.authorization || "")) return send({ message: "Authentication failed" }, 401); if (req.url === "/v2/whoami") return send({ data: { account: { id: 4242 } } }); if (req.url.startsWith("/v2/4242/domains")) return send({ data: [{ name: "one.example", expires_on: "2027-01-01", auto_renew: true, state: "registered" }, { name: "two.example" }] }); const m = req.url.match(/^\/v2\/4242\/registrar\/domains\/([^/]+)\/check$/); if (m) { const d = decodeURIComponent(m[1]); return send({ data: { domain: d, available: d === "free.example", premium: false } }); } const pm = req.url.match(/^\/v2\/4242\/registrar\/domains\/([^/]+)\/prices$/); if (pm) { const d = decodeURIComponent(pm[1]); return send({ data: { domain: d, premium: false, registration_price: 14.0, renewal_price: 14.0, transfer_price: 14.0 } }); } send({ message: "not found" }, 404); }); return new Promise((resolve) => server.listen(0, () => resolve(server))); } async function testClient() { const server = await startMock(); process.env.LCHAT_DNSIMPLE_BASE = `http://127.0.0.1:${server.address().port}`; const base = process.env.LCHAT_DNSIMPLE_BASE; const { exchangeViaBroker, checkAvailability, listDomains } = await import("../agent/dnsimple.mjs"); // The sidecar swaps a code for a token via the broker (which holds the secret). const token = await exchangeViaBroker({ code: "abc", state: "s", brokerUrl: base, brokerToken: "brk" }); assert(token === "scoped_from_exchange", "exchangeViaBroker returns the access token from the broker"); let noBroker = false; try { await exchangeViaBroker({ code: "abc", state: "s" }); } catch { noBroker = true; } assert(noBroker, "exchangeViaBroker refuses without broker config"); // /list — the wired, read-only command, using the exchanged token. const domains = await listDomains(token); assert(domains.length === 2 && domains.some((d) => d.name === "one.example"), "listDomains returns structured domain rows"); const one = domains.find((d) => d.name === "one.example"); assert(one.expires_on === "2027-01-01" && one.auto_renew === true, "rows carry expiry + auto-renew for the table"); assert(one.account_id === 4242, "rows carry the account id so the app can deep-link to dnsimple.com"); assert(one.price === 14, "rows carry the renewal price (best-effort per-domain lookup) for the cost column"); const free = await checkAvailability("free.example", token); assert(free.available === true, "an unregistered domain reports available"); const taken = await checkAvailability("taken.example", token); assert(taken.available === false, "a registered domain reports taken"); let authThrew = false; try { await checkAvailability("free.example", "rejected"); } catch { authThrew = true; } assert(authThrew, "a rejected token surfaces an error"); let badDomain = false; try { await checkAvailability("not a domain", token); } catch { badDomain = true; } assert(badDomain, "a malformed domain is refused before any network call"); server.close(); } await testGrant(); await testPkce(); await testClient(); console.log("connect.test.mjs OK");