Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions src/probe-worker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -167,17 +167,31 @@ export async function decideWithModel(probe, env) {
}
}

async function publishReply(room, text, env) {
export async function publishReply(room, text, env) {
const nonce = Date.now();
const sig = await signText(room, String(nonce), text, env.TECHNOCORE_AGENT_PRIVATE_KEY);
const response = await fetch(`${env.TECHNOCORE_URL || DEFAULT_BASE_URL}/r/${encodeURIComponent(room)}`, {
const baseUrl = env.TECHNOCORE_URL || DEFAULT_BASE_URL;
const response = await fetch(`${baseUrl}/r/${encodeURIComponent(room)}`, {
method: "POST",
headers: { "content-type": "application/json", accept: "application/json" },
body: JSON.stringify({ did: env.TECHNOCORE_AGENT_DID, sig, nonce: String(nonce), text })
});
if (!response.ok) throw new Error(`Technocore publish failed: ${response.status}`);
const accepted = await response.json();
if (!accepted?.seq) throw new Error("Technocore did not confirm the reply");
await response.text();

// Successful Technocore writes return the room's plain-text view, even when
// the request advertises JSON. Confirm the exact signed record with a fresh
// machine-readable read instead of trying to parse the write response.
const confirmation = await readJson(
`${baseUrl}/r/${encodeURIComponent(room)}?limit=50&format=json&n=${nonce}`
);
const accepted = (confirmation?.messages || []).find((record) =>
record.from === env.TECHNOCORE_AGENT_DID
&& String(record.nonce) === String(nonce)
&& record.text === text
&& record.sig === sig
);
if (!accepted?.seq) throw new Error("Technocore did not confirm the signed reply");
return accepted.seq;
}

Expand Down
27 changes: 27 additions & 0 deletions tests/probe-worker.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
parseProbe,
parseProbeReply,
probeAgeMs,
publishReply,
validateProbeDecision,
verifySignedRecord
} from "../src/probe-worker.mjs";
Expand Down Expand Up @@ -162,6 +163,32 @@ test("hot-polls configured probe rooms with a sequence cursor", async () => {
}
});

test("confirms a signed write after Technocore returns its plain-text room view", async () => {
const originalFetch = globalThis.fetch;
const pair = await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
const privateKey = Buffer.from(await crypto.subtle.exportKey("pkcs8", pair.privateKey)).toString("base64");
let posted;
globalThis.fetch = async (_url, options) => {
if (options?.method === "POST") {
posted = JSON.parse(options.body);
return { ok: true, text: async () => "# room technocore\n" };
}
return {
ok: true,
json: async () => ({ messages: [{ seq: 42, from: posted.did, nonce: posted.nonce, text: posted.text, sig: posted.sig }] })
};
};
try {
assert.equal(await publishReply("technocore", "probe v1 reply | run.1 | ack | bounded answer citing run.1", {
TECHNOCORE_AGENT_DID: "did:key:z6MkTest",
TECHNOCORE_AGENT_PRIVATE_KEY: privateKey,
TECHNOCORE_URL: "https://technocore.chat"
}), 42);
} finally {
globalThis.fetch = originalFetch;
}
});

test("accepts the official DID signature and rejects tampering", async () => {
const pair = await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
Expand Down
Loading