import assert from "node:assert/strict"; import { createHash, generateKeyPairSync, verify } from "node:crypto"; import { chmod, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { createFoundryReaderGrantProvisioner } from "./foundry-reader-grant-provisioner.mjs"; const signatureSchema = "nodedc.external-data-plane.managed-provisioner-request/v1"; const audience = "nodedc-external-data-plane.managed-provisioning.v1"; const serviceId = "nodedc-module-foundry"; const keyId = "foundry-edp-managed-provisioner-v1"; const target = { application: { id: "11111111-1111-4111-8111-111111111111" }, page: { id: "map" }, binding: { id: "fleet-current", dataProductId: "fleet.positions.current.v1" }, }; test("managed Foundry grant provisioning signs digest-only requests and persists the token privately", async () => { const root = await mkdtemp(join(tmpdir(), "foundry-reader-grant-")); const privateKeyFile = join(root, "private-key.pem"); const grantsDir = join(root, "grants"); const { privateKey, publicKey } = generateKeyPairSync("ed25519"); await writeFile(privateKeyFile, privateKey.export({ type: "pkcs8", format: "pem" }), { mode: 0o400 }); await chmod(privateKeyFile, 0o400); const requestBodies = []; let capabilityDigest = null; const fetchImpl = async (input, options) => { const url = new URL(input); const body = String(options.body); const parsed = JSON.parse(body); requestBodies.push(body); const bodySha256 = createHash("sha256").update(body, "utf8").digest("hex"); assert.equal(options.headers["x-nodedc-content-sha256"], bodySha256); const signedPayload = JSON.stringify({ schemaVersion: signatureSchema, audience, serviceId, keyId, method: options.method, path: url.pathname, timestamp: options.headers["x-nodedc-request-timestamp"], nonce: options.headers["x-nodedc-request-nonce"], bodySha256, }); assert.equal(verify( null, Buffer.from(signedPayload, "utf8"), publicKey, Buffer.from(options.headers["x-nodedc-request-signature"], "base64url"), ), true); assert.equal(options.headers["x-nodedc-engine-service-id"], serviceId); assert.equal(options.headers["x-nodedc-engine-key-id"], keyId); assert.equal(options.headers["x-nodedc-request-audience"], audience); assert.deepEqual(parsed.allowedDataProductIds, ["fleet.positions.current.v1"]); for (const forbidden of ["provider", "providerId", "tenant", "tenantId", "connection", "connectionId", "token", "capability"]) { assert.equal(Object.hasOwn(parsed, forbidden), false); } if (url.pathname.endsWith("/plan")) { assert.deepEqual(Object.keys(parsed), ["allowedDataProductIds"]); return Response.json({ ok: true, sourceScope: "resolved-server-side", dataProducts: [{ id: "fleet.positions.current.v1", version: "1.0.0", deliveryMode: "snapshot+patch", semanticTypes: ["map.moving_object"], active: true, }], }); } assert.match(url.pathname, /\/consumer-reader-bindings\/by-key\/fndrc-[0-9a-f]{64}$/); assert.deepEqual(Object.keys(parsed).sort(), ["allowedDataProductIds", "capabilityDigest", "expiresAt", "generation"].sort()); assert.match(parsed.capabilityDigest, /^[0-9a-f]{64}$/); capabilityDigest ||= parsed.capabilityDigest; assert.equal(parsed.capabilityDigest, capabilityDigest); const bindingKey = decodeURIComponent(url.pathname.split("/").at(-1)); return Response.json({ ok: true, idempotent: capabilityDigest === parsed.capabilityDigest, readerBinding: { bindingKey, generation: 1, active: true, expiresAt: null, sourceScope: "resolved-server-side", allowedDataProductIds: parsed.allowedDataProductIds, }, }); }; const provisioner = createFoundryReaderGrantProvisioner({ dataPlaneUrl: "http://edp.test", privateKeyFile, grantsDir, serviceId, keyId, audience, fetchImpl, now: () => new Date("2026-07-19T14:00:00.000Z"), randomBytesImpl: () => Buffer.alloc(32, 7), randomUUIDImpl: () => "22222222-2222-4222-8222-222222222222", production: false, }); try { assert.equal(provisioner.configured, true); const planned = await provisioner.plan(target); assert.equal(planned.product.id, "fleet.positions.current.v1"); const first = await provisioner.ensure(target); const second = await provisioner.ensure(target); assert.equal(first.ensured, true); assert.equal(second.ensured, true); const files = await readdir(grantsDir); assert.equal(files.length, 1); const tokenPath = join(grantsDir, files[0]); const token = (await readFile(tokenPath, "utf8")).trim(); assert.match(token, /^ndc_edprb_/); assert.equal((await stat(tokenPath)).mode & 0o777, 0o400); assert.equal(createHash("sha256").update(token, "utf8").digest("hex"), capabilityDigest); assert.equal(await provisioner.readToken(target), token); assert.equal(requestBodies.some((body) => body.includes(token)), false); assert.equal(JSON.stringify([planned, first, second]).includes(token), false); assert.equal(requestBodies.some((body) => /provider|tenant|connection/i.test(body)), false); } finally { await rm(root, { recursive: true, force: true }); } }); test("managed Foundry grant provisioning keeps successor capabilities separate and revokes an exact predecessor generation", async () => { const root = await mkdtemp(join(tmpdir(), "foundry-reader-grant-generation-")); const privateKeyFile = join(root, "private-key.pem"); const grantsDir = join(root, "grants"); const { privateKey } = generateKeyPairSync("ed25519"); await writeFile(privateKeyFile, privateKey.export({ type: "pkcs8", format: "pem" }), { mode: 0o400 }); await chmod(privateKeyFile, 0o400); const requests = []; const fetchImpl = async (input, options) => { const url = new URL(input); const parsed = JSON.parse(String(options.body)); requests.push({ method: options.method, path: url.pathname, body: parsed }); const bindingKey = url.pathname.match(/by-key\/([^/]+)/)?.[1]; if (url.pathname.endsWith("/plan")) { const id = parsed.allowedDataProductIds[0]; return Response.json({ ok: true, sourceScope: "resolved-server-side", dataProducts: [{ id, version: id.endsWith(".v2") ? "2.0.0" : "1.0.0", deliveryMode: "snapshot+patch", semanticTypes: ["map.moving_object"], active: true, }], }); } if (url.pathname.endsWith("/revoke")) { const generation = Number(url.pathname.split("/").at(-2)); return Response.json({ ok: true, idempotent: false, readerBinding: { bindingKey: decodeURIComponent(bindingKey), generation, active: false, expiresAt: null, sourceScope: "resolved-server-side", allowedDataProductIds: ["fleet.positions.current.v1"], }, }); } return Response.json({ ok: true, idempotent: false, readerBinding: { bindingKey: decodeURIComponent(bindingKey), generation: parsed.generation, active: true, expiresAt: null, sourceScope: "resolved-server-side", allowedDataProductIds: parsed.allowedDataProductIds, }, }); }; const provisioner = createFoundryReaderGrantProvisioner({ dataPlaneUrl: "http://edp.test", privateKeyFile, grantsDir, serviceId, keyId, audience, fetchImpl, randomBytesImpl: (size) => Buffer.alloc(size, requests.length + 1), randomUUIDImpl: () => "33333333-3333-4333-8333-333333333333", production: false, }); const successorTarget = structuredClone(target); successorTarget.binding.dataProductId = "fleet.positions.current.v2"; try { const first = await provisioner.ensure(target, { generation: 1 }); const second = await provisioner.ensure(successorTarget, { generation: 2 }); assert.equal(first.generation, 1); assert.equal(second.generation, 2); const firstToken = await provisioner.readToken(target, { generation: 1 }); const secondToken = await provisioner.readToken(successorTarget, { generation: 2 }); assert.notEqual(firstToken, secondToken); const files = (await readdir(grantsDir)).sort(); const baseFile = files.find((entry) => !entry.includes(".g")); assert.ok(baseFile); assert.deepEqual(files, [baseFile, `${baseFile}.g2`]); const revoked = await provisioner.revoke(target, { generation: 1 }); assert.equal(revoked.generation, 1); assert.ok(requests.some((request) => request.method === "POST" && request.path.endsWith("/generations/1/revoke"))); assert.equal(requests.some((request) => JSON.stringify(request.body).includes(firstToken)), false); assert.equal(requests.some((request) => JSON.stringify(request.body).includes(secondToken)), false); } finally { await rm(root, { recursive: true, force: true }); } });