@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 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
- /** Poll envelope plus the relay's recomputed tally (optionally at an explicit cutoff). */
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
- /** Poll envelope plus the relay's recomputed tally (optionally at an explicit cutoff). */
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
- return (await res.json());
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 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() });
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 cache = new Map();
392
- const resolve = async (id) => {
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.1",
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",