@zerox1/sdk 0.1.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.
@@ -0,0 +1,117 @@
1
+ export interface Zerox1AgentConfig {
2
+ /**
3
+ * 32-byte Ed25519 secret key as Uint8Array, OR a path to an existing
4
+ * key file (raw 32 bytes). If the path does not exist, the node
5
+ * generates a new key and writes it there.
6
+ */
7
+ keypair: Uint8Array | string;
8
+ /** Display name broadcast in BEACON/ADVERTISE. Default: 'zerox1-agent'. */
9
+ name?: string;
10
+ /**
11
+ * SATI mint address as hex (32 bytes). Required for mainnet.
12
+ * Omit to run in dev mode (SATI checks are advisory only).
13
+ */
14
+ satiMint?: string;
15
+ /** Solana RPC URL. Default: mainnet-beta. */
16
+ rpcUrl?: string;
17
+ /** Directory for per-epoch envelope logs. Default: current dir. */
18
+ logDir?: string;
19
+ /** Additional bootstrap peer multiaddrs. */
20
+ bootstrap?: string[];
21
+ }
22
+ export type MsgType = 'ADVERTISE' | 'DISCOVER' | 'PROPOSE' | 'COUNTER' | 'ACCEPT' | 'REJECT' | 'DELIVER' | 'NOTARIZE_BID' | 'NOTARIZE_ASSIGN' | 'VERDICT' | 'FEEDBACK' | 'DISPUTE';
23
+ export interface SendParams {
24
+ msgType: MsgType;
25
+ /** Hex-encoded 32-byte agent ID. Omit for broadcast types. */
26
+ recipient?: string;
27
+ /** Hex-encoded 16-byte conversation ID. */
28
+ conversationId: string;
29
+ payload: Buffer | Uint8Array;
30
+ }
31
+ export interface SentConfirmation {
32
+ nonce: number;
33
+ payloadHash: string;
34
+ }
35
+ export interface FeedbackPayload {
36
+ conversationId: string;
37
+ targetAgent: string;
38
+ score: number;
39
+ outcome: number;
40
+ isDispute: boolean;
41
+ role: number;
42
+ }
43
+ export interface NotarizeBidPayload {
44
+ bidType: number;
45
+ conversationId: string;
46
+ opaqueB64: string;
47
+ }
48
+ export interface InboundEnvelope {
49
+ msgType: MsgType;
50
+ sender: string;
51
+ recipient: string;
52
+ conversationId: string;
53
+ slot: number;
54
+ nonce: number;
55
+ payloadB64: string;
56
+ feedback?: FeedbackPayload;
57
+ notarizeBid?: NotarizeBidPayload;
58
+ }
59
+ export interface SendFeedbackParams {
60
+ conversationId: string;
61
+ targetAgent: string;
62
+ /** -100 to +100 */
63
+ score: number;
64
+ outcome: 'negative' | 'neutral' | 'positive';
65
+ role: 'participant' | 'notary';
66
+ }
67
+ type Handler = (env: InboundEnvelope) => void | Promise<void>;
68
+ export declare class Zerox1Agent {
69
+ private proc;
70
+ private ws;
71
+ private handlers;
72
+ private port;
73
+ private nodeUrl;
74
+ private constructor();
75
+ /**
76
+ * Create an Zerox1Agent instance.
77
+ * Call `agent.on(...)` to register handlers, then `agent.start()` to join
78
+ * the mesh. The node binary is bundled — no separate install required.
79
+ */
80
+ static create(config: Zerox1AgentConfig): Zerox1Agent;
81
+ private _config;
82
+ /**
83
+ * Start the node, wait for it to be ready, connect the inbox stream.
84
+ * Safe to await — resolves once the agent is live on the mesh.
85
+ */
86
+ start(): Promise<void>;
87
+ /**
88
+ * Disconnect from the mesh and stop the node process.
89
+ */
90
+ disconnect(): void;
91
+ /**
92
+ * Register a handler for a message type.
93
+ * Use `'*'` to catch all inbound message types.
94
+ * Chain multiple `.on()` calls — all handlers for a type are called in order.
95
+ */
96
+ on(msgType: MsgType | '*', handler: Handler): this;
97
+ private _dispatch;
98
+ /**
99
+ * Send an envelope. The node signs it and routes via libp2p.
100
+ * Returns the assigned nonce and payload hash for tracking.
101
+ */
102
+ send(params: SendParams): Promise<SentConfirmation>;
103
+ /**
104
+ * Send a FEEDBACK envelope with CBOR-encoded payload.
105
+ * Protocol rule 9 requires CBOR — this method handles the encoding.
106
+ */
107
+ sendFeedback(params: SendFeedbackParams): Promise<SentConfirmation>;
108
+ /** Generate a random 16-byte conversation ID as hex. */
109
+ newConversationId(): string;
110
+ /**
111
+ * Encode a bid value (i128 LE) into the first 16 bytes of a payload,
112
+ * followed by optional extra bytes (your terms).
113
+ */
114
+ encodeBidValue(value: bigint, rest?: Buffer): Buffer;
115
+ private _connectInbox;
116
+ }
117
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,330 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.Zerox1Agent = void 0;
40
+ const fs = __importStar(require("fs"));
41
+ const net = __importStar(require("net"));
42
+ const os = __importStar(require("os"));
43
+ const path = __importStar(require("path"));
44
+ const child_process_1 = require("child_process");
45
+ const ws_1 = __importDefault(require("ws"));
46
+ // ============================================================================
47
+ // CBOR encoding for FEEDBACK payload
48
+ //
49
+ // FEEDBACK payloads must be CBOR-encoded. Receiving nodes run
50
+ // FeedbackPayload::decode() which is a strict CBOR parser — any other
51
+ // encoding fails validation rule 9 and the message is silently dropped.
52
+ //
53
+ // Structure: CBOR array of 6 items:
54
+ // [0] bstr(16) conversation_id
55
+ // [1] bstr(32) target_agent
56
+ // [2] int score (-100..100)
57
+ // [3] uint outcome (0..2)
58
+ // [4] bool is_dispute
59
+ // [5] uint role (0..1)
60
+ // ============================================================================
61
+ function cborInt(n) {
62
+ if (n >= 0 && n <= 23)
63
+ return Buffer.from([n]);
64
+ if (n >= 24 && n <= 255)
65
+ return Buffer.from([0x18, n]);
66
+ if (n >= -24 && n < 0)
67
+ return Buffer.from([0x20 + (-n - 1)]);
68
+ if (n >= -256 && n < -24)
69
+ return Buffer.from([0x38, -n - 1]);
70
+ throw new RangeError(`CBOR int out of range: ${n}`);
71
+ }
72
+ function encodeFeedbackCbor(conversationIdHex, targetAgentHex, score, outcome, isDispute, role) {
73
+ const convId = Buffer.from(conversationIdHex, 'hex'); // 16 bytes
74
+ const targetAgent = Buffer.from(targetAgentHex, 'hex'); // 32 bytes
75
+ return Buffer.concat([
76
+ Buffer.from([0x86]), // array(6)
77
+ Buffer.from([0x50]), convId, // bytes(16)
78
+ Buffer.from([0x58, 0x20]), targetAgent, // bytes(32)
79
+ cborInt(score),
80
+ cborInt(outcome),
81
+ Buffer.from([isDispute ? 0xF5 : 0xF4]),
82
+ cborInt(role),
83
+ ]);
84
+ }
85
+ // ============================================================================
86
+ // Binary resolution
87
+ // ============================================================================
88
+ function getBinaryPath() {
89
+ const platform = process.platform; // 'win32' | 'darwin' | 'linux'
90
+ const arch = process.arch; // 'x64' | 'arm64'
91
+ const binName = platform === 'win32' ? 'zerox1-node.exe' : 'zerox1-node';
92
+ const pkgName = `@zerox1/sdk-${platform}-${arch}`;
93
+ try {
94
+ const pkgJson = require.resolve(`${pkgName}/package.json`);
95
+ return path.join(path.dirname(pkgJson), 'bin', binName);
96
+ }
97
+ catch {
98
+ // Optional platform package not installed — fall back to PATH.
99
+ // This allows developers to use a locally built binary during development.
100
+ return binName;
101
+ }
102
+ }
103
+ // ============================================================================
104
+ // Port + process utilities
105
+ // ============================================================================
106
+ function getFreePort() {
107
+ return new Promise((resolve, reject) => {
108
+ const srv = net.createServer();
109
+ srv.listen(0, '127.0.0.1', () => {
110
+ const port = srv.address().port;
111
+ srv.close(() => resolve(port));
112
+ });
113
+ srv.on('error', reject);
114
+ });
115
+ }
116
+ function resolveKeypairPath(keypair) {
117
+ if (typeof keypair === 'string') {
118
+ // Caller passed a file path — use it directly.
119
+ return keypair;
120
+ }
121
+ // Caller passed raw bytes — write to a temp file with restrictive permissions.
122
+ // mode 0o600: owner read/write only — prevents other users from reading the key.
123
+ const tmpPath = path.join(os.tmpdir(), `zerox1-identity-${Date.now()}.key`);
124
+ fs.writeFileSync(tmpPath, Buffer.from(keypair), { mode: 0o600 });
125
+ return tmpPath;
126
+ }
127
+ async function waitForReady(port, timeoutMs = 15000) {
128
+ const deadline = Date.now() + timeoutMs;
129
+ while (Date.now() < deadline) {
130
+ try {
131
+ const res = await fetch(`http://127.0.0.1:${port}/peers`);
132
+ if (res.ok)
133
+ return;
134
+ }
135
+ catch {
136
+ // not ready yet
137
+ }
138
+ await new Promise(r => setTimeout(r, 200));
139
+ }
140
+ throw new Error(`zerox1-node did not become ready within ${timeoutMs}ms`);
141
+ }
142
+ class Zerox1Agent {
143
+ constructor() {
144
+ this.proc = null;
145
+ this.ws = null;
146
+ this.handlers = new Map();
147
+ this.port = 0;
148
+ this.nodeUrl = '';
149
+ }
150
+ // ── Factory ───────────────────────────────────────────────────────────────
151
+ /**
152
+ * Create an Zerox1Agent instance.
153
+ * Call `agent.on(...)` to register handlers, then `agent.start()` to join
154
+ * the mesh. The node binary is bundled — no separate install required.
155
+ */
156
+ static create(config) {
157
+ const agent = new Zerox1Agent();
158
+ agent._config = config;
159
+ return agent;
160
+ }
161
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
162
+ /**
163
+ * Start the node, wait for it to be ready, connect the inbox stream.
164
+ * Safe to await — resolves once the agent is live on the mesh.
165
+ */
166
+ async start() {
167
+ this.port = await getFreePort();
168
+ this.nodeUrl = `http://127.0.0.1:${this.port}`;
169
+ const keypairPath = resolveKeypairPath(this._config.keypair);
170
+ const binaryPath = getBinaryPath();
171
+ const args = [
172
+ '--keypair-path', keypairPath,
173
+ '--api-addr', `127.0.0.1:${this.port}`,
174
+ '--agent-name', this._config.name ?? 'zerox1-agent',
175
+ ];
176
+ if (this._config.satiMint)
177
+ args.push('--sati-mint', this._config.satiMint);
178
+ if (this._config.rpcUrl)
179
+ args.push('--rpc-url', this._config.rpcUrl);
180
+ if (this._config.logDir)
181
+ args.push('--log-dir', this._config.logDir);
182
+ for (const b of this._config.bootstrap ?? []) {
183
+ args.push('--bootstrap', b);
184
+ }
185
+ this.proc = (0, child_process_1.spawn)(binaryPath, args, { stdio: ['ignore', 'pipe', 'pipe'] });
186
+ this.proc.on('error', (err) => {
187
+ throw new Error(`Failed to start zerox1-node (${binaryPath}): ${err.message}\n` +
188
+ `Make sure the binary is installed or in your PATH.`);
189
+ });
190
+ // Surface node logs prefixed so they're distinguishable in agent output.
191
+ this.proc.stderr?.on('data', (d) => {
192
+ process.stderr.write(`[zerox1-node] ${d}`);
193
+ });
194
+ // Wait until the HTTP server is accepting connections.
195
+ await waitForReady(this.port);
196
+ // Open the inbox WebSocket.
197
+ this._connectInbox();
198
+ }
199
+ /**
200
+ * Disconnect from the mesh and stop the node process.
201
+ */
202
+ disconnect() {
203
+ this.ws?.close();
204
+ this.ws = null;
205
+ this.proc?.kill();
206
+ this.proc = null;
207
+ }
208
+ // ── Handlers ──────────────────────────────────────────────────────────────
209
+ /**
210
+ * Register a handler for a message type.
211
+ * Use `'*'` to catch all inbound message types.
212
+ * Chain multiple `.on()` calls — all handlers for a type are called in order.
213
+ */
214
+ on(msgType, handler) {
215
+ const key = msgType === '*' ? '__all__' : msgType;
216
+ const list = this.handlers.get(key) ?? [];
217
+ list.push(handler);
218
+ this.handlers.set(key, list);
219
+ return this;
220
+ }
221
+ _dispatch(env) {
222
+ const specific = this.handlers.get(env.msgType) ?? [];
223
+ const wildcard = this.handlers.get('__all__') ?? [];
224
+ for (const h of [...specific, ...wildcard]) {
225
+ try {
226
+ void h(env);
227
+ }
228
+ catch { /* handler errors are isolated */ }
229
+ }
230
+ }
231
+ // ── Sending ───────────────────────────────────────────────────────────────
232
+ /**
233
+ * Send an envelope. The node signs it and routes via libp2p.
234
+ * Returns the assigned nonce and payload hash for tracking.
235
+ */
236
+ async send(params) {
237
+ const body = {
238
+ msg_type: params.msgType,
239
+ recipient: params.recipient ?? null,
240
+ conversation_id: params.conversationId,
241
+ payload_b64: Buffer.from(params.payload).toString('base64'),
242
+ };
243
+ const res = await fetch(`${this.nodeUrl}/envelopes/send`, {
244
+ method: 'POST',
245
+ headers: { 'Content-Type': 'application/json' },
246
+ body: JSON.stringify(body),
247
+ });
248
+ const json = await res.json();
249
+ if (!res.ok) {
250
+ throw new Error(json['error'] ?? `HTTP ${res.status}`);
251
+ }
252
+ return {
253
+ nonce: json['nonce'],
254
+ payloadHash: json['payload_hash'],
255
+ };
256
+ }
257
+ /**
258
+ * Send a FEEDBACK envelope with CBOR-encoded payload.
259
+ * Protocol rule 9 requires CBOR — this method handles the encoding.
260
+ */
261
+ async sendFeedback(params) {
262
+ const outcomeMap = { negative: 0, neutral: 1, positive: 2 };
263
+ const roleMap = { participant: 0, notary: 1 };
264
+ const payload = encodeFeedbackCbor(params.conversationId, params.targetAgent, params.score, outcomeMap[params.outcome], false, roleMap[params.role]);
265
+ return this.send({
266
+ msgType: 'FEEDBACK',
267
+ conversationId: params.conversationId,
268
+ payload,
269
+ });
270
+ }
271
+ // ── Utilities ─────────────────────────────────────────────────────────────
272
+ /** Generate a random 16-byte conversation ID as hex. */
273
+ newConversationId() {
274
+ const bytes = new Uint8Array(16);
275
+ crypto.getRandomValues(bytes);
276
+ return Buffer.from(bytes).toString('hex');
277
+ }
278
+ /**
279
+ * Encode a bid value (i128 LE) into the first 16 bytes of a payload,
280
+ * followed by optional extra bytes (your terms).
281
+ */
282
+ encodeBidValue(value, rest = Buffer.alloc(0)) {
283
+ const buf = Buffer.alloc(16);
284
+ buf.writeBigInt64LE(value & 0xffffffffffffffffn, 0);
285
+ buf.writeBigInt64LE(value >> 64n, 8);
286
+ return Buffer.concat([buf, rest]);
287
+ }
288
+ // ── Internal ──────────────────────────────────────────────────────────────
289
+ _connectInbox() {
290
+ const wsUrl = `ws://127.0.0.1:${this.port}/ws/inbox`;
291
+ const ws = new ws_1.default(wsUrl);
292
+ this.ws = ws;
293
+ ws.on('message', (data) => {
294
+ try {
295
+ const raw = JSON.parse(data.toString());
296
+ const env = {
297
+ msgType: raw.msg_type,
298
+ sender: raw.sender,
299
+ recipient: raw.recipient,
300
+ conversationId: raw.conversation_id,
301
+ slot: raw.slot,
302
+ nonce: raw.nonce,
303
+ payloadB64: raw.payload_b64,
304
+ feedback: raw.feedback ? {
305
+ conversationId: raw.feedback.conversation_id,
306
+ targetAgent: raw.feedback.target_agent,
307
+ score: raw.feedback.score,
308
+ outcome: raw.feedback.outcome,
309
+ isDispute: raw.feedback.is_dispute,
310
+ role: raw.feedback.role,
311
+ } : undefined,
312
+ notarizeBid: raw.notarize_bid ? {
313
+ bidType: raw.notarize_bid.bid_type,
314
+ conversationId: raw.notarize_bid.conversation_id,
315
+ opaqueB64: raw.notarize_bid.opaque_b64,
316
+ } : undefined,
317
+ };
318
+ this._dispatch(env);
319
+ }
320
+ catch { /* malformed — ignore */ }
321
+ });
322
+ ws.on('close', () => {
323
+ // Reconnect only if the process is still running.
324
+ if (this.proc)
325
+ setTimeout(() => this._connectInbox(), 1000);
326
+ });
327
+ ws.on('error', () => { });
328
+ }
329
+ }
330
+ exports.Zerox1Agent = Zerox1Agent;
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@zerox1/sdk",
3
+ "version": "0.1.3",
4
+ "description": "0x01 mesh agent SDK — zero-config, binary bundled, works on every platform",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch"
10
+ },
11
+ "dependencies": {
12
+ "ws": "^8.18.0"
13
+ },
14
+ "optionalDependencies": {
15
+ "@zerox1/sdk-darwin-arm64": "0.1.3",
16
+ "@zerox1/sdk-darwin-x64": "0.1.3",
17
+ "@zerox1/sdk-linux-x64": "0.1.3"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.0.0",
21
+ "@types/ws": "^8.5.13",
22
+ "typescript": "^5.7.0"
23
+ }
24
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@zerox1/sdk-darwin-arm64",
3
+ "version": "0.1.3",
4
+ "description": "zerox1-node binary for macOS ARM64 (Apple Silicon)",
5
+ "os": [
6
+ "darwin"
7
+ ],
8
+ "cpu": [
9
+ "arm64"
10
+ ],
11
+ "files": [
12
+ "bin/"
13
+ ]
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@zerox1/sdk-darwin-x64",
3
+ "version": "0.1.3",
4
+ "description": "zerox1-node binary for macOS x64 (Intel)",
5
+ "os": [
6
+ "darwin"
7
+ ],
8
+ "cpu": [
9
+ "x64"
10
+ ],
11
+ "files": [
12
+ "bin/"
13
+ ]
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@zerox1/sdk-linux-x64",
3
+ "version": "0.1.3",
4
+ "description": "zerox1-node binary for Linux x64",
5
+ "os": [
6
+ "linux"
7
+ ],
8
+ "cpu": [
9
+ "x64"
10
+ ],
11
+ "files": [
12
+ "bin/"
13
+ ]
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@zerox1/sdk-win32-x64",
3
+ "version": "0.1.3",
4
+ "description": "zerox1-node binary for Windows x64",
5
+ "os": [
6
+ "win32"
7
+ ],
8
+ "cpu": [
9
+ "x64"
10
+ ],
11
+ "files": [
12
+ "bin/"
13
+ ]
14
+ }
package/src/index.ts ADDED
@@ -0,0 +1,427 @@
1
+ import * as fs from 'fs'
2
+ import * as net from 'net'
3
+ import * as os from 'os'
4
+ import * as path from 'path'
5
+ import { spawn, ChildProcess } from 'child_process'
6
+ import WebSocket from 'ws'
7
+
8
+ // ============================================================================
9
+ // Public config / types
10
+ // ============================================================================
11
+
12
+ export interface Zerox1AgentConfig {
13
+ /**
14
+ * 32-byte Ed25519 secret key as Uint8Array, OR a path to an existing
15
+ * key file (raw 32 bytes). If the path does not exist, the node
16
+ * generates a new key and writes it there.
17
+ */
18
+ keypair: Uint8Array | string
19
+ /** Display name broadcast in BEACON/ADVERTISE. Default: 'zerox1-agent'. */
20
+ name?: string
21
+ /**
22
+ * SATI mint address as hex (32 bytes). Required for mainnet.
23
+ * Omit to run in dev mode (SATI checks are advisory only).
24
+ */
25
+ satiMint?: string
26
+ /** Solana RPC URL. Default: mainnet-beta. */
27
+ rpcUrl?: string
28
+ /** Directory for per-epoch envelope logs. Default: current dir. */
29
+ logDir?: string
30
+ /** Additional bootstrap peer multiaddrs. */
31
+ bootstrap?: string[]
32
+ }
33
+
34
+ export type MsgType =
35
+ | 'ADVERTISE' | 'DISCOVER'
36
+ | 'PROPOSE' | 'COUNTER' | 'ACCEPT' | 'REJECT'
37
+ | 'DELIVER'
38
+ | 'NOTARIZE_BID' | 'NOTARIZE_ASSIGN'
39
+ | 'VERDICT'
40
+ | 'FEEDBACK'
41
+ | 'DISPUTE'
42
+
43
+ export interface SendParams {
44
+ msgType: MsgType
45
+ /** Hex-encoded 32-byte agent ID. Omit for broadcast types. */
46
+ recipient?: string
47
+ /** Hex-encoded 16-byte conversation ID. */
48
+ conversationId: string
49
+ payload: Buffer | Uint8Array
50
+ }
51
+
52
+ export interface SentConfirmation {
53
+ nonce: number
54
+ payloadHash: string
55
+ }
56
+
57
+ export interface FeedbackPayload {
58
+ conversationId: string
59
+ targetAgent: string
60
+ score: number
61
+ outcome: number
62
+ isDispute: boolean
63
+ role: number
64
+ }
65
+
66
+ export interface NotarizeBidPayload {
67
+ bidType: number
68
+ conversationId: string
69
+ opaqueB64: string
70
+ }
71
+
72
+ export interface InboundEnvelope {
73
+ msgType: MsgType
74
+ sender: string
75
+ recipient: string
76
+ conversationId: string
77
+ slot: number
78
+ nonce: number
79
+ payloadB64: string
80
+ feedback?: FeedbackPayload
81
+ notarizeBid?: NotarizeBidPayload
82
+ }
83
+
84
+ export interface SendFeedbackParams {
85
+ conversationId: string
86
+ targetAgent: string
87
+ /** -100 to +100 */
88
+ score: number
89
+ outcome: 'negative' | 'neutral' | 'positive'
90
+ role: 'participant' | 'notary'
91
+ }
92
+
93
+ // ============================================================================
94
+ // CBOR encoding for FEEDBACK payload
95
+ //
96
+ // FEEDBACK payloads must be CBOR-encoded. Receiving nodes run
97
+ // FeedbackPayload::decode() which is a strict CBOR parser — any other
98
+ // encoding fails validation rule 9 and the message is silently dropped.
99
+ //
100
+ // Structure: CBOR array of 6 items:
101
+ // [0] bstr(16) conversation_id
102
+ // [1] bstr(32) target_agent
103
+ // [2] int score (-100..100)
104
+ // [3] uint outcome (0..2)
105
+ // [4] bool is_dispute
106
+ // [5] uint role (0..1)
107
+ // ============================================================================
108
+
109
+ function cborInt(n: number): Buffer {
110
+ if (n >= 0 && n <= 23) return Buffer.from([n])
111
+ if (n >= 24 && n <= 255) return Buffer.from([0x18, n])
112
+ if (n >= -24 && n < 0) return Buffer.from([0x20 + (-n - 1)])
113
+ if (n >= -256 && n < -24) return Buffer.from([0x38, -n - 1])
114
+ throw new RangeError(`CBOR int out of range: ${n}`)
115
+ }
116
+
117
+ function encodeFeedbackCbor(
118
+ conversationIdHex: string,
119
+ targetAgentHex: string,
120
+ score: number,
121
+ outcome: number,
122
+ isDispute: boolean,
123
+ role: number,
124
+ ): Buffer {
125
+ const convId = Buffer.from(conversationIdHex, 'hex') // 16 bytes
126
+ const targetAgent = Buffer.from(targetAgentHex, 'hex') // 32 bytes
127
+ return Buffer.concat([
128
+ Buffer.from([0x86]), // array(6)
129
+ Buffer.from([0x50]), convId, // bytes(16)
130
+ Buffer.from([0x58, 0x20]), targetAgent, // bytes(32)
131
+ cborInt(score),
132
+ cborInt(outcome),
133
+ Buffer.from([isDispute ? 0xF5 : 0xF4]),
134
+ cborInt(role),
135
+ ])
136
+ }
137
+
138
+ // ============================================================================
139
+ // Binary resolution
140
+ // ============================================================================
141
+
142
+ function getBinaryPath(): string {
143
+ const platform = process.platform // 'win32' | 'darwin' | 'linux'
144
+ const arch = process.arch // 'x64' | 'arm64'
145
+ const binName = platform === 'win32' ? 'zerox1-node.exe' : 'zerox1-node'
146
+ const pkgName = `@zerox1/sdk-${platform}-${arch}`
147
+
148
+ try {
149
+ const pkgJson = require.resolve(`${pkgName}/package.json`)
150
+ return path.join(path.dirname(pkgJson), 'bin', binName)
151
+ } catch {
152
+ // Optional platform package not installed — fall back to PATH.
153
+ // This allows developers to use a locally built binary during development.
154
+ return binName
155
+ }
156
+ }
157
+
158
+ // ============================================================================
159
+ // Port + process utilities
160
+ // ============================================================================
161
+
162
+ function getFreePort(): Promise<number> {
163
+ return new Promise((resolve, reject) => {
164
+ const srv = net.createServer()
165
+ srv.listen(0, '127.0.0.1', () => {
166
+ const port = (srv.address() as net.AddressInfo).port
167
+ srv.close(() => resolve(port))
168
+ })
169
+ srv.on('error', reject)
170
+ })
171
+ }
172
+
173
+ function resolveKeypairPath(keypair: Uint8Array | string): string {
174
+ if (typeof keypair === 'string') {
175
+ // Caller passed a file path — use it directly.
176
+ return keypair
177
+ }
178
+ // Caller passed raw bytes — write to a temp file with restrictive permissions.
179
+ // mode 0o600: owner read/write only — prevents other users from reading the key.
180
+ const tmpPath = path.join(os.tmpdir(), `zerox1-identity-${Date.now()}.key`)
181
+ fs.writeFileSync(tmpPath, Buffer.from(keypair), { mode: 0o600 })
182
+ return tmpPath
183
+ }
184
+
185
+ async function waitForReady(port: number, timeoutMs = 15_000): Promise<void> {
186
+ const deadline = Date.now() + timeoutMs
187
+ while (Date.now() < deadline) {
188
+ try {
189
+ const res = await fetch(`http://127.0.0.1:${port}/peers`)
190
+ if (res.ok) return
191
+ } catch {
192
+ // not ready yet
193
+ }
194
+ await new Promise(r => setTimeout(r, 200))
195
+ }
196
+ throw new Error(`zerox1-node did not become ready within ${timeoutMs}ms`)
197
+ }
198
+
199
+ // ============================================================================
200
+ // Zerox1Agent
201
+ // ============================================================================
202
+
203
+ type Handler = (env: InboundEnvelope) => void | Promise<void>
204
+
205
+ export class Zerox1Agent {
206
+ private proc: ChildProcess | null = null
207
+ private ws: WebSocket | null = null
208
+ private handlers: Map<string, Handler[]> = new Map()
209
+ private port: number = 0
210
+ private nodeUrl: string = ''
211
+
212
+ private constructor() {}
213
+
214
+ // ── Factory ───────────────────────────────────────────────────────────────
215
+
216
+ /**
217
+ * Create an Zerox1Agent instance.
218
+ * Call `agent.on(...)` to register handlers, then `agent.start()` to join
219
+ * the mesh. The node binary is bundled — no separate install required.
220
+ */
221
+ static create(config: Zerox1AgentConfig): Zerox1Agent {
222
+ const agent = new Zerox1Agent()
223
+ agent._config = config
224
+ return agent
225
+ }
226
+
227
+ private _config!: Zerox1AgentConfig
228
+
229
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
230
+
231
+ /**
232
+ * Start the node, wait for it to be ready, connect the inbox stream.
233
+ * Safe to await — resolves once the agent is live on the mesh.
234
+ */
235
+ async start(): Promise<void> {
236
+ this.port = await getFreePort()
237
+ this.nodeUrl = `http://127.0.0.1:${this.port}`
238
+
239
+ const keypairPath = resolveKeypairPath(this._config.keypair)
240
+ const binaryPath = getBinaryPath()
241
+
242
+ const args: string[] = [
243
+ '--keypair-path', keypairPath,
244
+ '--api-addr', `127.0.0.1:${this.port}`,
245
+ '--agent-name', this._config.name ?? 'zerox1-agent',
246
+ ]
247
+
248
+ if (this._config.satiMint) args.push('--sati-mint', this._config.satiMint)
249
+ if (this._config.rpcUrl) args.push('--rpc-url', this._config.rpcUrl)
250
+ if (this._config.logDir) args.push('--log-dir', this._config.logDir)
251
+ for (const b of this._config.bootstrap ?? []) {
252
+ args.push('--bootstrap', b)
253
+ }
254
+
255
+ this.proc = spawn(binaryPath, args, { stdio: ['ignore', 'pipe', 'pipe'] })
256
+
257
+ this.proc.on('error', (err) => {
258
+ throw new Error(
259
+ `Failed to start zerox1-node (${binaryPath}): ${err.message}\n` +
260
+ `Make sure the binary is installed or in your PATH.`
261
+ )
262
+ })
263
+
264
+ // Surface node logs prefixed so they're distinguishable in agent output.
265
+ this.proc.stderr?.on('data', (d: Buffer) => {
266
+ process.stderr.write(`[zerox1-node] ${d}`)
267
+ })
268
+
269
+ // Wait until the HTTP server is accepting connections.
270
+ await waitForReady(this.port)
271
+
272
+ // Open the inbox WebSocket.
273
+ this._connectInbox()
274
+ }
275
+
276
+ /**
277
+ * Disconnect from the mesh and stop the node process.
278
+ */
279
+ disconnect(): void {
280
+ this.ws?.close()
281
+ this.ws = null
282
+ this.proc?.kill()
283
+ this.proc = null
284
+ }
285
+
286
+ // ── Handlers ──────────────────────────────────────────────────────────────
287
+
288
+ /**
289
+ * Register a handler for a message type.
290
+ * Use `'*'` to catch all inbound message types.
291
+ * Chain multiple `.on()` calls — all handlers for a type are called in order.
292
+ */
293
+ on(msgType: MsgType | '*', handler: Handler): this {
294
+ const key = msgType === '*' ? '__all__' : msgType
295
+ const list = this.handlers.get(key) ?? []
296
+ list.push(handler)
297
+ this.handlers.set(key, list)
298
+ return this
299
+ }
300
+
301
+ private _dispatch(env: InboundEnvelope): void {
302
+ const specific = this.handlers.get(env.msgType) ?? []
303
+ const wildcard = this.handlers.get('__all__') ?? []
304
+ for (const h of [...specific, ...wildcard]) {
305
+ try { void h(env) } catch { /* handler errors are isolated */ }
306
+ }
307
+ }
308
+
309
+ // ── Sending ───────────────────────────────────────────────────────────────
310
+
311
+ /**
312
+ * Send an envelope. The node signs it and routes via libp2p.
313
+ * Returns the assigned nonce and payload hash for tracking.
314
+ */
315
+ async send(params: SendParams): Promise<SentConfirmation> {
316
+ const body = {
317
+ msg_type: params.msgType,
318
+ recipient: params.recipient ?? null,
319
+ conversation_id: params.conversationId,
320
+ payload_b64: Buffer.from(params.payload).toString('base64'),
321
+ }
322
+
323
+ const res = await fetch(`${this.nodeUrl}/envelopes/send`, {
324
+ method: 'POST',
325
+ headers: { 'Content-Type': 'application/json' },
326
+ body: JSON.stringify(body),
327
+ })
328
+ const json = await res.json() as Record<string, unknown>
329
+
330
+ if (!res.ok) {
331
+ throw new Error((json['error'] as string) ?? `HTTP ${res.status}`)
332
+ }
333
+
334
+ return {
335
+ nonce: json['nonce'] as number,
336
+ payloadHash: json['payload_hash'] as string,
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Send a FEEDBACK envelope with CBOR-encoded payload.
342
+ * Protocol rule 9 requires CBOR — this method handles the encoding.
343
+ */
344
+ async sendFeedback(params: SendFeedbackParams): Promise<SentConfirmation> {
345
+ const outcomeMap = { negative: 0, neutral: 1, positive: 2 } as const
346
+ const roleMap = { participant: 0, notary: 1 } as const
347
+
348
+ const payload = encodeFeedbackCbor(
349
+ params.conversationId,
350
+ params.targetAgent,
351
+ params.score,
352
+ outcomeMap[params.outcome],
353
+ false,
354
+ roleMap[params.role],
355
+ )
356
+
357
+ return this.send({
358
+ msgType: 'FEEDBACK',
359
+ conversationId: params.conversationId,
360
+ payload,
361
+ })
362
+ }
363
+
364
+ // ── Utilities ─────────────────────────────────────────────────────────────
365
+
366
+ /** Generate a random 16-byte conversation ID as hex. */
367
+ newConversationId(): string {
368
+ const bytes = new Uint8Array(16)
369
+ crypto.getRandomValues(bytes)
370
+ return Buffer.from(bytes).toString('hex')
371
+ }
372
+
373
+ /**
374
+ * Encode a bid value (i128 LE) into the first 16 bytes of a payload,
375
+ * followed by optional extra bytes (your terms).
376
+ */
377
+ encodeBidValue(value: bigint, rest: Buffer = Buffer.alloc(0)): Buffer {
378
+ const buf = Buffer.alloc(16)
379
+ buf.writeBigInt64LE(value & 0xFFFFFFFFFFFFFFFFn, 0)
380
+ buf.writeBigInt64LE(value >> 64n, 8)
381
+ return Buffer.concat([buf, rest])
382
+ }
383
+
384
+ // ── Internal ──────────────────────────────────────────────────────────────
385
+
386
+ private _connectInbox(): void {
387
+ const wsUrl = `ws://127.0.0.1:${this.port}/ws/inbox`
388
+ const ws = new WebSocket(wsUrl)
389
+ this.ws = ws
390
+
391
+ ws.on('message', (data) => {
392
+ try {
393
+ const raw = JSON.parse(data.toString())
394
+ const env: InboundEnvelope = {
395
+ msgType: raw.msg_type,
396
+ sender: raw.sender,
397
+ recipient: raw.recipient,
398
+ conversationId: raw.conversation_id,
399
+ slot: raw.slot,
400
+ nonce: raw.nonce,
401
+ payloadB64: raw.payload_b64,
402
+ feedback: raw.feedback ? {
403
+ conversationId: raw.feedback.conversation_id,
404
+ targetAgent: raw.feedback.target_agent,
405
+ score: raw.feedback.score,
406
+ outcome: raw.feedback.outcome,
407
+ isDispute: raw.feedback.is_dispute,
408
+ role: raw.feedback.role,
409
+ } : undefined,
410
+ notarizeBid: raw.notarize_bid ? {
411
+ bidType: raw.notarize_bid.bid_type,
412
+ conversationId: raw.notarize_bid.conversation_id,
413
+ opaqueB64: raw.notarize_bid.opaque_b64,
414
+ } : undefined,
415
+ }
416
+ this._dispatch(env)
417
+ } catch { /* malformed — ignore */ }
418
+ })
419
+
420
+ ws.on('close', () => {
421
+ // Reconnect only if the process is still running.
422
+ if (this.proc) setTimeout(() => this._connectInbox(), 1000)
423
+ })
424
+
425
+ ws.on('error', () => { /* close event handles reconnect */ })
426
+ }
427
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "dist",
7
+ "declaration": true,
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src"]
13
+ }