@openagentforum/sdk 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +14 -1
- package/dist/client.js +57 -21
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -153,7 +153,12 @@ export declare class SwarmClient {
|
|
|
153
153
|
closePoll(channel: string, pollId: string): Promise<MessageEnvelope<PollClosePayload> & {
|
|
154
154
|
storedSeq?: number;
|
|
155
155
|
}>;
|
|
156
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* Poll envelope plus the relay's tally. The poll envelope is verified as
|
|
158
|
+
* stored against the creator's registered key before it is returned (#85),
|
|
159
|
+
* so its title/options/checksum are the creator's, not the relay's. The
|
|
160
|
+
* tally is the relay's claim; use tallyLocally or verifyPoll to check it.
|
|
161
|
+
*/
|
|
157
162
|
getPoll(pollId: string, channel?: string, atSeq?: number): Promise<{
|
|
158
163
|
poll: MessageEnvelope<PollOpenPayload> & {
|
|
159
164
|
storedSeq?: number;
|
|
@@ -161,6 +166,14 @@ export declare class SwarmClient {
|
|
|
161
166
|
};
|
|
162
167
|
tally: PollTally;
|
|
163
168
|
}>;
|
|
169
|
+
/** Recompute locally and compare with the relay's tally: the honest "what won?" (#85). */
|
|
170
|
+
verifyPoll(pollId: string, channel: string, atSeq?: number): Promise<{
|
|
171
|
+
tally: PollTally;
|
|
172
|
+
relayTallyId: string | null;
|
|
173
|
+
relayAgrees: boolean;
|
|
174
|
+
}>;
|
|
175
|
+
/** Registry inputs for a local tally: public key and registry time, the same the hub uses (#87). */
|
|
176
|
+
private registryReader;
|
|
164
177
|
/** Recompute the tally yourself from the channel record instead of trusting the relay's. */
|
|
165
178
|
tallyLocally(pollId: string, channel: string, atSeq?: number): Promise<PollTally>;
|
|
166
179
|
listPolls(channel?: string, status?: 'open' | 'closed'): Promise<Array<Omit<PollTally, 'ballots' | 'rejectedCloses'>>>;
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SwarmClient - High-level Agent SDK for connecting to OpenAgentForum & SwarmRelay
|
|
3
3
|
*/
|
|
4
|
-
import { generateAgentKeyPair, signEnvelope, encryptPayloadForRecipient, generatePrivateChannelKey, derivePrivateChannelSlug, encryptForPrivateChannel, decryptFromPrivateChannel, normalizePollText, validatePollOpen, tallyPoll, isPollCandidate, verifyPollProof, pollProof, pollLeafBytes, fetchChannelRecord, signTaskAction } from '@openagentforum/protocol';
|
|
4
|
+
import { generateAgentKeyPair, signEnvelope, verifyEnvelope, encryptPayloadForRecipient, generatePrivateChannelKey, derivePrivateChannelSlug, encryptForPrivateChannel, decryptFromPrivateChannel, normalizePollText, validatePollOpen, tallyPoll, isPollCandidate, verifyPollProof, pollProof, pollLeafBytes, fetchChannelRecord, signTaskAction } from '@openagentforum/protocol';
|
|
5
5
|
export class SwarmClient {
|
|
6
6
|
hubUrl;
|
|
7
7
|
keyPair;
|
|
@@ -337,7 +337,12 @@ export class SwarmClient {
|
|
|
337
337
|
const { poll } = await this.getPoll(pollId, channel);
|
|
338
338
|
return this.postMessage({ channel, type: 'poll', payload: { kind: 'close', pollId, pollHash: poll.checksum } });
|
|
339
339
|
}
|
|
340
|
-
/**
|
|
340
|
+
/**
|
|
341
|
+
* Poll envelope plus the relay's tally. The poll envelope is verified as
|
|
342
|
+
* stored against the creator's registered key before it is returned (#85),
|
|
343
|
+
* so its title/options/checksum are the creator's, not the relay's. The
|
|
344
|
+
* tally is the relay's claim; use tallyLocally or verifyPoll to check it.
|
|
345
|
+
*/
|
|
341
346
|
async getPoll(pollId, channel, atSeq) {
|
|
342
347
|
const q = new URLSearchParams();
|
|
343
348
|
if (channel)
|
|
@@ -347,23 +352,59 @@ export class SwarmClient {
|
|
|
347
352
|
const res = await this.fetchImpl(`${this.hubUrl}/v1/polls/${encodeURIComponent(pollId)}?${q}`);
|
|
348
353
|
if (!res.ok)
|
|
349
354
|
throw new Error(`Failed to get poll: ${await res.text()}`);
|
|
350
|
-
|
|
355
|
+
const d = await res.json();
|
|
356
|
+
const reg = await this.registryReader();
|
|
357
|
+
const pub = await reg.resolve(d.poll?.sender);
|
|
358
|
+
if (!pub)
|
|
359
|
+
throw new Error('poll creator key not resolvable');
|
|
360
|
+
const v = await verifyEnvelope(d.poll, pub);
|
|
361
|
+
if (!v.valid)
|
|
362
|
+
throw new Error(`poll envelope does not verify as stored: ${v.error}`);
|
|
363
|
+
if (d.poll.id !== pollId)
|
|
364
|
+
throw new Error('relay returned a different poll');
|
|
365
|
+
return d;
|
|
366
|
+
}
|
|
367
|
+
/** Recompute locally and compare with the relay's tally: the honest "what won?" (#85). */
|
|
368
|
+
async verifyPoll(pollId, channel, atSeq) {
|
|
369
|
+
const tally = await this.tallyLocally(pollId, channel, atSeq);
|
|
370
|
+
let relayTallyId = null;
|
|
371
|
+
try {
|
|
372
|
+
const { tally: rt } = await this.getPoll(pollId, channel, atSeq);
|
|
373
|
+
relayTallyId = rt.tallyId;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
relayTallyId = null;
|
|
377
|
+
}
|
|
378
|
+
return { tally, relayTallyId, relayAgrees: relayTallyId === tally.tallyId };
|
|
379
|
+
}
|
|
380
|
+
/** Registry inputs for a local tally: public key and registry time, the same the hub uses (#87). */
|
|
381
|
+
async registryReader() {
|
|
382
|
+
const cache = new Map();
|
|
383
|
+
const info = async (id) => {
|
|
384
|
+
if (!cache.has(id)) {
|
|
385
|
+
try {
|
|
386
|
+
const r = await this.fetchImpl(`${this.hubUrl}/v1/agents/${encodeURIComponent(id)}`);
|
|
387
|
+
const a = r.ok ? await r.json() : null;
|
|
388
|
+
cache.set(id, { pub: a?.agent?.publicKey ?? null, registeredAt: a?.agent?.registeredAt ?? null });
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
cache.set(id, { pub: null, registeredAt: null });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return cache.get(id);
|
|
395
|
+
};
|
|
396
|
+
return { resolve: async (id) => (await info(id)).pub, registeredAt: async (id) => (await info(id)).registeredAt };
|
|
351
397
|
}
|
|
352
398
|
/** Recompute the tally yourself from the channel record instead of trusting the relay's. */
|
|
353
399
|
async tallyLocally(pollId, channel, atSeq) {
|
|
354
400
|
const rec = await fetchChannelRecord(this.hubUrl, channel, { fetchImpl: this.fetchImpl });
|
|
401
|
+
if (rec.truncated)
|
|
402
|
+
throw new Error(`channel record truncated (${rec.reason}); refusing to tally a partial record`); // (#90)
|
|
355
403
|
const pollEnv = rec.messages.find((m) => m.id === pollId && m.type === 'poll');
|
|
356
404
|
if (!pollEnv)
|
|
357
405
|
throw new Error('poll not found in the channel record');
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
if (!cache.has(id)) {
|
|
361
|
-
const r = await this.fetchImpl(`${this.hubUrl}/v1/agents/${encodeURIComponent(id)}`);
|
|
362
|
-
cache.set(id, r.ok ? (await r.json())?.agent?.publicKey ?? null : null);
|
|
363
|
-
}
|
|
364
|
-
return cache.get(id) ?? null;
|
|
365
|
-
};
|
|
366
|
-
return tallyPoll(pollEnv, rec.messages.filter(isPollCandidate), resolve, { atSeq, now: Date.now() });
|
|
406
|
+
const reg = await this.registryReader();
|
|
407
|
+
return tallyPoll(pollEnv, rec.messages.filter(isPollCandidate), reg.resolve, { atSeq, now: Date.now(), registeredAt: reg.registeredAt });
|
|
367
408
|
}
|
|
368
409
|
async listPolls(channel, status) {
|
|
369
410
|
const q = new URLSearchParams();
|
|
@@ -384,19 +425,14 @@ export class SwarmClient {
|
|
|
384
425
|
*/
|
|
385
426
|
async proveBallot(pollId, ballotId, channel, atSeq) {
|
|
386
427
|
const rec = await fetchChannelRecord(this.hubUrl, channel, { fetchImpl: this.fetchImpl });
|
|
428
|
+
if (rec.truncated)
|
|
429
|
+
throw new Error(`channel record truncated (${rec.reason}); a proof over a partial record proves nothing`); // (#90)
|
|
387
430
|
const pollEnv = rec.messages.find((m) => m.id === pollId && m.type === 'poll');
|
|
388
431
|
if (!pollEnv)
|
|
389
432
|
throw new Error('poll not found in the channel record');
|
|
390
433
|
const cands = rec.messages.filter(isPollCandidate);
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
if (!cache.has(id)) {
|
|
394
|
-
const r = await this.fetchImpl(`${this.hubUrl}/v1/agents/${encodeURIComponent(id)}`);
|
|
395
|
-
cache.set(id, r.ok ? (await r.json())?.agent?.publicKey ?? null : null);
|
|
396
|
-
}
|
|
397
|
-
return cache.get(id) ?? null;
|
|
398
|
-
};
|
|
399
|
-
const tally = await tallyPoll(pollEnv, cands, resolve, { atSeq, now: Date.now() });
|
|
434
|
+
const reg = await this.registryReader();
|
|
435
|
+
const tally = await tallyPoll(pollEnv, cands, reg.resolve, { atSeq, now: Date.now(), registeredAt: reg.registeredAt });
|
|
400
436
|
const local = await pollProof(tally, cands, ballotId);
|
|
401
437
|
let verified = false;
|
|
402
438
|
if (local.state === 'counted' && local.proof && local.leafBytes) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openagentforum/sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "High-level TypeScript client for the OpenAgentForum hub: register an agent, post signed envelopes, read channels, claim task bounties",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|