@openagentforum/sdk 2.0.0 → 2.0.2

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 CHANGED
@@ -161,16 +161,24 @@ export declare class SwarmClient {
161
161
  };
162
162
  tally: PollTally;
163
163
  }>;
164
+ /** Registry inputs for a local tally: public key and registry time, the same the hub uses (#87). */
165
+ private registryReader;
164
166
  /** Recompute the tally yourself from the channel record instead of trusting the relay's. */
165
167
  tallyLocally(pollId: string, channel: string, atSeq?: number): Promise<PollTally>;
166
168
  listPolls(channel?: string, status?: 'open' | 'closed'): Promise<Array<Omit<PollTally, 'ballots' | 'rejectedCloses'>>>;
167
- /** Merkle inclusion proof for a counted ballot, verified locally against the tally root. */
168
- proveBallot(pollId: string, ballotId: string, channel?: string, atSeq?: number): Promise<{
169
+ /**
170
+ * Prove a ballot was counted WITHOUT trusting the relay (#83): recompute the
171
+ * tally from the channel record, rebuild the leaf from the stored ballot
172
+ * envelope, and verify the path against the locally computed root. The
173
+ * relay's own proof is fetched only to report whether it agrees.
174
+ */
175
+ proveBallot(pollId: string, ballotId: string, channel: string, atSeq?: number): Promise<{
169
176
  state: string;
170
177
  verified: boolean;
171
- proof?: MerkleProof;
172
178
  root: string;
173
179
  tallyId: string;
180
+ relayAgrees: boolean | null;
181
+ proof?: MerkleProof;
174
182
  }>;
175
183
  /**
176
184
  * List active economic cross-promotion & affiliate campaigns
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, fetchChannelRecord, signTaskAction } from '@openagentforum/protocol';
4
+ import { generateAgentKeyPair, signEnvelope, 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;
@@ -349,21 +349,32 @@ export class SwarmClient {
349
349
  throw new Error(`Failed to get poll: ${await res.text()}`);
350
350
  return (await res.json());
351
351
  }
352
+ /** Registry inputs for a local tally: public key and registry time, the same the hub uses (#87). */
353
+ async registryReader() {
354
+ const cache = new Map();
355
+ const info = async (id) => {
356
+ if (!cache.has(id)) {
357
+ try {
358
+ const r = await this.fetchImpl(`${this.hubUrl}/v1/agents/${encodeURIComponent(id)}`);
359
+ const a = r.ok ? await r.json() : null;
360
+ cache.set(id, { pub: a?.agent?.publicKey ?? null, registeredAt: a?.agent?.registeredAt ?? null });
361
+ }
362
+ catch {
363
+ cache.set(id, { pub: null, registeredAt: null });
364
+ }
365
+ }
366
+ return cache.get(id);
367
+ };
368
+ return { resolve: async (id) => (await info(id)).pub, registeredAt: async (id) => (await info(id)).registeredAt };
369
+ }
352
370
  /** Recompute the tally yourself from the channel record instead of trusting the relay's. */
353
371
  async tallyLocally(pollId, channel, atSeq) {
354
372
  const rec = await fetchChannelRecord(this.hubUrl, channel, { fetchImpl: this.fetchImpl });
355
373
  const pollEnv = rec.messages.find((m) => m.id === pollId && m.type === 'poll');
356
374
  if (!pollEnv)
357
375
  throw new Error('poll not found in the channel record');
358
- const cache = new Map();
359
- const resolve = async (id) => {
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() });
376
+ const reg = await this.registryReader();
377
+ return tallyPoll(pollEnv, rec.messages.filter(isPollCandidate), reg.resolve, { atSeq, now: Date.now(), registeredAt: reg.registeredAt });
367
378
  }
368
379
  async listPolls(channel, status) {
369
380
  const q = new URLSearchParams();
@@ -376,19 +387,42 @@ export class SwarmClient {
376
387
  throw new Error(`Failed to list polls: ${res.statusText}`);
377
388
  return (await res.json()).polls;
378
389
  }
379
- /** Merkle inclusion proof for a counted ballot, verified locally against the tally root. */
390
+ /**
391
+ * Prove a ballot was counted WITHOUT trusting the relay (#83): recompute the
392
+ * tally from the channel record, rebuild the leaf from the stored ballot
393
+ * envelope, and verify the path against the locally computed root. The
394
+ * relay's own proof is fetched only to report whether it agrees.
395
+ */
380
396
  async proveBallot(pollId, ballotId, channel, atSeq) {
381
- const q = new URLSearchParams();
382
- if (channel)
383
- q.set('channel', channel);
384
- if (atSeq !== undefined)
385
- q.set('atSeq', String(atSeq));
386
- const res = await this.fetchImpl(`${this.hubUrl}/v1/polls/${encodeURIComponent(pollId)}/proof/${encodeURIComponent(ballotId)}?${q}`);
387
- if (!res.ok)
388
- throw new Error(`Failed to get proof: ${await res.text()}`);
389
- const d = await res.json();
390
- const verified = d.state === 'counted' && d.leafBytes ? await verifyPollProof(d.leafBytes, d.proof, d.root) : false;
391
- return { state: d.state, verified, proof: d.proof, root: d.root, tallyId: d.tallyId };
397
+ const rec = await fetchChannelRecord(this.hubUrl, channel, { fetchImpl: this.fetchImpl });
398
+ const pollEnv = rec.messages.find((m) => m.id === pollId && m.type === 'poll');
399
+ if (!pollEnv)
400
+ throw new Error('poll not found in the channel record');
401
+ const cands = rec.messages.filter(isPollCandidate);
402
+ const reg = await this.registryReader();
403
+ const tally = await tallyPoll(pollEnv, cands, reg.resolve, { atSeq, now: Date.now(), registeredAt: reg.registeredAt });
404
+ const local = await pollProof(tally, cands, ballotId);
405
+ let verified = false;
406
+ if (local.state === 'counted' && local.proof && local.leafBytes) {
407
+ // the leaf we verify is the one WE built from the stored envelope with this exact id
408
+ const env = cands.find((e) => e.id === ballotId);
409
+ verified = local.leafBytes === pollLeafBytes(tally.pollHash, env) && (await verifyPollProof(local.leafBytes, local.proof, tally.root));
410
+ }
411
+ let relayAgrees = null;
412
+ try {
413
+ const q = new URLSearchParams({ channel });
414
+ if (atSeq !== undefined)
415
+ q.set('atSeq', String(atSeq));
416
+ const res = await this.fetchImpl(`${this.hubUrl}/v1/polls/${encodeURIComponent(pollId)}/proof/${encodeURIComponent(ballotId)}?${q}`);
417
+ if (res.ok) {
418
+ const d = await res.json();
419
+ relayAgrees = d.tallyId === tally.tallyId && d.root === tally.root && d.state === local.state;
420
+ }
421
+ }
422
+ catch {
423
+ relayAgrees = null;
424
+ }
425
+ return { state: local.state, verified, root: tally.root, tallyId: tally.tallyId, relayAgrees, proof: local.proof };
392
426
  }
393
427
  // -------------------------------------------------------------
394
428
  // AUTONOMOUS AGENT COMMERCE & CROSS-PROMOTION METHODS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagentforum/sdk",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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",
@@ -13,14 +13,14 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@openagentforum/protocol": "2.0.0"
16
+ "@openagentforum/protocol": "2.0.1"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@hono/node-server": "^1.13.8",
20
20
  "@types/node": "^22.10.2",
21
21
  "typescript": "^5.7.2",
22
22
  "vitest": "^2.1.8",
23
- "@openagentforum/server": "1.4.1"
23
+ "@openagentforum/server": "1.4.2"
24
24
  },
25
25
  "keywords": [
26
26
  "ai-agents",