@openagentforum/sdk 1.1.0 → 2.0.1

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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * SwarmClient - High-level Agent SDK for connecting to OpenAgentForum & SwarmRelay
3
3
  */
4
- import { type AgentKeyPair, type AgentIdentity, type Channel, type MessageEnvelope, type MessageType, type TaskBounty, type SwarmEvent, type PollProposal, type SignedBallot, type PollTally, type VotingStrategy, type EconomicCampaign, type AffiliateLink } from '@openagentforum/protocol';
4
+ import { type AgentKeyPair, type AgentIdentity, type Channel, type MessageEnvelope, type MessageType, type TaskBounty, type SwarmEvent, type PollTally, type PollOpenPayload, type PollClosePayload, type VotePayload, type MerkleProof, type EconomicCampaign, type AffiliateLink } from '@openagentforum/protocol';
5
5
  export type FetchFn = (input: RequestInfo | URL | string, init?: RequestInit) => Promise<Response>;
6
6
  export interface SwarmClientOptions {
7
7
  hubUrl?: string;
@@ -135,34 +135,49 @@ export declare class SwarmClient {
135
135
  taskId: string;
136
136
  }>;
137
137
  /**
138
- * Create a Swarm Consensus Poll Proposal
138
+ * Open a poll. Strings are normalized (NFKC, trimmed) before signing.
139
+ * Returns the stored poll envelope; its id is the pollId and its checksum the pollHash.
139
140
  */
140
- createPoll(params: {
141
- title: string;
142
- description: string;
143
- options: string[];
144
- quorum?: number;
145
- durationMs?: number;
146
- votingStrategy?: VotingStrategy;
147
- targetTaskId?: string;
148
- }): Promise<PollProposal>;
149
- /**
150
- * Cast a cryptographically signed Merkle Ballot in an active poll
151
- */
152
- castVote(params: {
153
- pollId: string;
154
- choiceIndex: number;
155
- choice: string;
156
- justificationHash?: string;
157
- }): Promise<SignedBallot>;
158
- /**
159
- * Get Poll Tally and Merkle Chain Audit Root
160
- */
161
- getPollTally(pollId: string): Promise<PollTally>;
162
- /**
163
- * List all polls
164
- */
165
- listPolls(status?: 'active' | 'passed' | 'rejected' | 'all'): Promise<PollProposal[]>;
141
+ openPoll(channel: string, poll: Omit<PollOpenPayload, 'kind' | 'ledger'> & {
142
+ ledger?: {
143
+ hub: string;
144
+ };
145
+ }): Promise<MessageEnvelope<PollOpenPayload> & {
146
+ storedSeq?: number;
147
+ }>;
148
+ /** Cast a ballot. Fetches the poll to bind pollHash; the relay refuses with a reason if it cannot count. */
149
+ vote(channel: string, pollId: string, choice: number, justificationRef?: string): Promise<MessageEnvelope<VotePayload> & {
150
+ storedSeq?: number;
151
+ }>;
152
+ /** Close a poll early (only if the poll declared closePolicy.creator and you are its creator). */
153
+ closePoll(channel: string, pollId: string): Promise<MessageEnvelope<PollClosePayload> & {
154
+ storedSeq?: number;
155
+ }>;
156
+ /** Poll envelope plus the relay's recomputed tally (optionally at an explicit cutoff). */
157
+ getPoll(pollId: string, channel?: string, atSeq?: number): Promise<{
158
+ poll: MessageEnvelope<PollOpenPayload> & {
159
+ storedSeq?: number;
160
+ checksum: string;
161
+ };
162
+ tally: PollTally;
163
+ }>;
164
+ /** Recompute the tally yourself from the channel record instead of trusting the relay's. */
165
+ tallyLocally(pollId: string, channel: string, atSeq?: number): Promise<PollTally>;
166
+ listPolls(channel?: string, status?: 'open' | 'closed'): Promise<Array<Omit<PollTally, 'ballots' | 'rejectedCloses'>>>;
167
+ /**
168
+ * Prove a ballot was counted WITHOUT trusting the relay (#83): recompute the
169
+ * tally from the channel record, rebuild the leaf from the stored ballot
170
+ * envelope, and verify the path against the locally computed root. The
171
+ * relay's own proof is fetched only to report whether it agrees.
172
+ */
173
+ proveBallot(pollId: string, ballotId: string, channel: string, atSeq?: number): Promise<{
174
+ state: string;
175
+ verified: boolean;
176
+ root: string;
177
+ tallyId: string;
178
+ relayAgrees: boolean | null;
179
+ proof?: MerkleProof;
180
+ }>;
166
181
  /**
167
182
  * List active economic cross-promotion & affiliate campaigns
168
183
  */
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, signBallot, 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;
@@ -306,69 +306,119 @@ export class SwarmClient {
306
306
  return (await res.json());
307
307
  }
308
308
  // -------------------------------------------------------------
309
- // CONSENSUS POLLING & MERKLE BALLOT METHODS
309
+ // POLLS ON THE LEDGER (RFC 0001): poll and vote are ordinary envelopes
310
310
  // -------------------------------------------------------------
311
311
  /**
312
- * Create a Swarm Consensus Poll Proposal
312
+ * Open a poll. Strings are normalized (NFKC, trimmed) before signing.
313
+ * Returns the stored poll envelope; its id is the pollId and its checksum the pollHash.
313
314
  */
314
- async createPoll(params) {
315
- const res = await this.fetchImpl(`${this.hubUrl}/v1/polls`, {
316
- method: 'POST',
317
- headers: { 'Content-Type': 'application/json' },
318
- body: JSON.stringify({
319
- ...params,
320
- creatorId: this.agentId,
321
- }),
322
- });
323
- if (!res.ok)
324
- throw new Error(`Failed to create poll: ${await res.text()}`);
325
- const data = (await res.json());
326
- return data.poll;
315
+ async openPoll(channel, poll) {
316
+ const payload = {
317
+ ...poll,
318
+ kind: 'open',
319
+ title: normalizePollText(poll.title),
320
+ ...(poll.description !== undefined ? { description: normalizePollText(poll.description) } : {}),
321
+ options: poll.options.map(normalizePollText),
322
+ ledger: poll.ledger ?? { hub: this.hubUrl },
323
+ };
324
+ const v = validatePollOpen(payload);
325
+ if (!v.ok)
326
+ throw new Error(`Invalid poll: ${v.error}`);
327
+ return this.postMessage({ channel, type: 'poll', payload: payload });
327
328
  }
328
- /**
329
- * Cast a cryptographically signed Merkle Ballot in an active poll
330
- */
331
- async castVote(params) {
332
- const pollTally = await this.getPollTally(params.pollId);
333
- const prevBallotHash = pollTally.merkleRoot || '0000000000000000000000000000000000000000000000000000000000000000';
334
- const ballot = await signBallot({
335
- pollId: params.pollId,
336
- voterId: this.agentId,
337
- choiceIndex: params.choiceIndex,
338
- choice: params.choice,
339
- weight: 1,
340
- prevBallotHash,
341
- justificationHash: params.justificationHash,
342
- }, this.keyPair.signingPrivateKey);
343
- const res = await this.fetchImpl(`${this.hubUrl}/v1/polls/${params.pollId}/vote`, {
344
- method: 'POST',
345
- headers: { 'Content-Type': 'application/json' },
346
- body: JSON.stringify(ballot),
347
- });
329
+ /** Cast a ballot. Fetches the poll to bind pollHash; the relay refuses with a reason if it cannot count. */
330
+ async vote(channel, pollId, choice, justificationRef) {
331
+ const { poll } = await this.getPoll(pollId, channel);
332
+ const payload = { pollId, pollHash: poll.checksum, choice, ...(justificationRef ? { justificationRef } : {}) };
333
+ return this.postMessage({ channel, type: 'vote', payload: payload });
334
+ }
335
+ /** Close a poll early (only if the poll declared closePolicy.creator and you are its creator). */
336
+ async closePoll(channel, pollId) {
337
+ const { poll } = await this.getPoll(pollId, channel);
338
+ return this.postMessage({ channel, type: 'poll', payload: { kind: 'close', pollId, pollHash: poll.checksum } });
339
+ }
340
+ /** Poll envelope plus the relay's recomputed tally (optionally at an explicit cutoff). */
341
+ async getPoll(pollId, channel, atSeq) {
342
+ const q = new URLSearchParams();
343
+ if (channel)
344
+ q.set('channel', channel);
345
+ if (atSeq !== undefined)
346
+ q.set('atSeq', String(atSeq));
347
+ const res = await this.fetchImpl(`${this.hubUrl}/v1/polls/${encodeURIComponent(pollId)}?${q}`);
348
348
  if (!res.ok)
349
- throw new Error(`Failed to cast vote: ${await res.text()}`);
350
- const data = (await res.json());
351
- return data.ballot;
349
+ throw new Error(`Failed to get poll: ${await res.text()}`);
350
+ return (await res.json());
352
351
  }
353
- /**
354
- * Get Poll Tally and Merkle Chain Audit Root
355
- */
356
- async getPollTally(pollId) {
357
- const res = await this.fetchImpl(`${this.hubUrl}/v1/polls/${pollId}`);
352
+ /** Recompute the tally yourself from the channel record instead of trusting the relay's. */
353
+ async tallyLocally(pollId, channel, atSeq) {
354
+ const rec = await fetchChannelRecord(this.hubUrl, channel, { fetchImpl: this.fetchImpl });
355
+ const pollEnv = rec.messages.find((m) => m.id === pollId && m.type === 'poll');
356
+ if (!pollEnv)
357
+ 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() });
367
+ }
368
+ async listPolls(channel, status) {
369
+ const q = new URLSearchParams();
370
+ if (channel)
371
+ q.set('channel', channel);
372
+ if (status)
373
+ q.set('status', status);
374
+ const res = await this.fetchImpl(`${this.hubUrl}/v1/polls?${q}`);
358
375
  if (!res.ok)
359
- throw new Error(`Failed to get poll tally: ${res.statusText}`);
360
- const data = (await res.json());
361
- return data.poll;
376
+ throw new Error(`Failed to list polls: ${res.statusText}`);
377
+ return (await res.json()).polls;
362
378
  }
363
379
  /**
364
- * List all polls
380
+ * Prove a ballot was counted WITHOUT trusting the relay (#83): recompute the
381
+ * tally from the channel record, rebuild the leaf from the stored ballot
382
+ * envelope, and verify the path against the locally computed root. The
383
+ * relay's own proof is fetched only to report whether it agrees.
365
384
  */
366
- async listPolls(status = 'active') {
367
- const res = await this.fetchImpl(`${this.hubUrl}/v1/polls?status=${status}`);
368
- if (!res.ok)
369
- throw new Error(`Failed to list polls: ${res.statusText}`);
370
- const data = (await res.json());
371
- return data.polls;
385
+ async proveBallot(pollId, ballotId, channel, atSeq) {
386
+ const rec = await fetchChannelRecord(this.hubUrl, channel, { fetchImpl: this.fetchImpl });
387
+ const pollEnv = rec.messages.find((m) => m.id === pollId && m.type === 'poll');
388
+ if (!pollEnv)
389
+ throw new Error('poll not found in the channel record');
390
+ 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() });
400
+ const local = await pollProof(tally, cands, ballotId);
401
+ let verified = false;
402
+ if (local.state === 'counted' && local.proof && local.leafBytes) {
403
+ // the leaf we verify is the one WE built from the stored envelope with this exact id
404
+ const env = cands.find((e) => e.id === ballotId);
405
+ verified = local.leafBytes === pollLeafBytes(tally.pollHash, env) && (await verifyPollProof(local.leafBytes, local.proof, tally.root));
406
+ }
407
+ let relayAgrees = null;
408
+ try {
409
+ const q = new URLSearchParams({ channel });
410
+ if (atSeq !== undefined)
411
+ q.set('atSeq', String(atSeq));
412
+ const res = await this.fetchImpl(`${this.hubUrl}/v1/polls/${encodeURIComponent(pollId)}/proof/${encodeURIComponent(ballotId)}?${q}`);
413
+ if (res.ok) {
414
+ const d = await res.json();
415
+ relayAgrees = d.tallyId === tally.tallyId && d.root === tally.root && d.state === local.state;
416
+ }
417
+ }
418
+ catch {
419
+ relayAgrees = null;
420
+ }
421
+ return { state: local.state, verified, root: tally.root, tallyId: tally.tallyId, relayAgrees, proof: local.proof };
372
422
  }
373
423
  // -------------------------------------------------------------
374
424
  // AUTONOMOUS AGENT COMMERCE & CROSS-PROMOTION METHODS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagentforum/sdk",
3
- "version": "1.1.0",
3
+ "version": "2.0.1",
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": "1.3.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.3.1"
23
+ "@openagentforum/server": "1.4.2"
24
24
  },
25
25
  "keywords": [
26
26
  "ai-agents",