@x1scroll/agent-sdk 1.0.3 → 1.1.0
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/README.md +38 -0
- package/package.json +1 -1
- package/src/index.js +92 -18
package/README.md
CHANGED
|
@@ -405,6 +405,44 @@ Fees are **automatic** — built into the on-chain instructions. Developers don'
|
|
|
405
405
|
|
|
406
406
|
---
|
|
407
407
|
|
|
408
|
+
## Security
|
|
409
|
+
|
|
410
|
+
The SDK implements several hardened security features to ensure reliable, tamper-resistant memory storage:
|
|
411
|
+
|
|
412
|
+
### Multi-Validator Pinning (5 Simultaneous)
|
|
413
|
+
|
|
414
|
+
When using the `x1scroll` provider, content is pinned to **up to 5 validators simultaneously** using `Promise.allSettled`. The first successful CID is used — if any validator succeeds, the upload proceeds. This eliminates single points of failure and ensures resilience against validator downtime.
|
|
415
|
+
|
|
416
|
+
```
|
|
417
|
+
Upload → [Validator 1] ✓ CID returned ← used
|
|
418
|
+
[Validator 2] ✓ CID returned
|
|
419
|
+
[Validator 3] ✗ Failed ← ignored (others succeeded)
|
|
420
|
+
...
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
If **all** validators fail, an `AgentSDKError` with code `PIN_FAILED` is thrown.
|
|
424
|
+
|
|
425
|
+
### Automatic Fallback to x1scroll.io
|
|
426
|
+
|
|
427
|
+
If the on-chain validator registry is unreachable or empty, the SDK automatically falls back to `https://x1scroll.io/api/ipfs/upload` — no configuration needed. Uploads will succeed even if the registry program hasn't been deployed yet.
|
|
428
|
+
|
|
429
|
+
### CID Verification After Upload
|
|
430
|
+
|
|
431
|
+
After a successful pin, the SDK verifies the CID is reachable on the public IPFS gateway (`https://ipfs.io/ipfs/<cid>`) using a HEAD request with an 8-second timeout. This is **non-fatal** — if verification fails, a warning is logged and the `verified: false` flag is returned in the response. Content may still propagate to the gateway within minutes.
|
|
432
|
+
|
|
433
|
+
```js
|
|
434
|
+
const { cid, verified } = await client.uploadMemory(...);
|
|
435
|
+
if (!verified) {
|
|
436
|
+
// Content pinned, but not yet visible on public gateway — normal for new pins
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Registry Cache (5-Minute TTL)
|
|
441
|
+
|
|
442
|
+
The active validator list is cached in memory for **5 minutes** to avoid hammering the on-chain registry on every upload. The cache is per-client-instance and invalidates automatically after TTL expiry.
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
408
446
|
## Protocol Info
|
|
409
447
|
|
|
410
448
|
| Field | Value |
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -27,6 +27,14 @@ const bs58 = require('bs58');
|
|
|
27
27
|
const bs58encode = (typeof bs58.encode === 'function') ? bs58.encode : bs58.default.encode;
|
|
28
28
|
const bs58decode = (typeof bs58.decode === 'function') ? bs58.decode : bs58.default.decode;
|
|
29
29
|
|
|
30
|
+
// ── Registry cache TTL ────────────────────────────────────────────────────────
|
|
31
|
+
const REGISTRY_CACHE_TTL = 5 * 60 * 1000; // 5 minutes in ms
|
|
32
|
+
|
|
33
|
+
// ── Fallback validators — used when registry is empty or unreachable ───────────
|
|
34
|
+
const FALLBACK_VALIDATORS = [
|
|
35
|
+
{ endpoint: 'https://x1scroll.io/api/ipfs/upload', active: true, fallback: true },
|
|
36
|
+
];
|
|
37
|
+
|
|
30
38
|
// ── Protocol constants — hardcoded, do not change ─────────────────────────────
|
|
31
39
|
/**
|
|
32
40
|
* On-chain program address. Immutable — this SDK only talks to this program.
|
|
@@ -312,8 +320,10 @@ class AgentClient {
|
|
|
312
320
|
);
|
|
313
321
|
}
|
|
314
322
|
|
|
315
|
-
this.rpcUrl
|
|
316
|
-
this._connection
|
|
323
|
+
this.rpcUrl = rpcUrl;
|
|
324
|
+
this._connection = null; // lazy
|
|
325
|
+
this._registryCache = null;
|
|
326
|
+
this._registryCacheExpiry = 0;
|
|
317
327
|
}
|
|
318
328
|
|
|
319
329
|
// ── Internal ────────────────────────────────────────────────────────────────
|
|
@@ -351,6 +361,63 @@ class AgentClient {
|
|
|
351
361
|
return sig;
|
|
352
362
|
}
|
|
353
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Get active validators from on-chain registry (simulated — falls back to hardcoded list).
|
|
366
|
+
* Results are cached for REGISTRY_CACHE_TTL (5 minutes).
|
|
367
|
+
* @returns {Promise<Array<{endpoint: string, active: boolean, fallback?: boolean}>>}
|
|
368
|
+
*/
|
|
369
|
+
async _getActiveValidators() {
|
|
370
|
+
const now = Date.now();
|
|
371
|
+
if (this._registryCache && now < this._registryCacheExpiry) {
|
|
372
|
+
return this._registryCache;
|
|
373
|
+
}
|
|
374
|
+
// For now: return fallback (registry program not deployed yet)
|
|
375
|
+
// When program is live, this queries on-chain StorageNode accounts
|
|
376
|
+
this._registryCache = FALLBACK_VALIDATORS;
|
|
377
|
+
this._registryCacheExpiry = now + REGISTRY_CACHE_TTL;
|
|
378
|
+
return this._registryCache;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Verify that a pinned CID is reachable on the public IPFS gateway.
|
|
383
|
+
* Non-fatal — returns false on failure (content may still propagate).
|
|
384
|
+
* @param {string} cid
|
|
385
|
+
* @returns {Promise<boolean>}
|
|
386
|
+
*/
|
|
387
|
+
async _verifyPin(cid) {
|
|
388
|
+
try {
|
|
389
|
+
const res = await fetch(`https://ipfs.io/ipfs/${cid}`, {
|
|
390
|
+
method: 'HEAD',
|
|
391
|
+
signal: AbortSignal.timeout(8000),
|
|
392
|
+
});
|
|
393
|
+
return res.ok;
|
|
394
|
+
} catch {
|
|
395
|
+
return false; // non-fatal — log warning but don't throw
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Pin content to a single validator endpoint.
|
|
401
|
+
* @param {string} endpoint
|
|
402
|
+
* @param {string} body Serialized content
|
|
403
|
+
* @param {string} topic
|
|
404
|
+
* @param {string} agentPubkey
|
|
405
|
+
* @returns {Promise<string|null>} CID string on success, null on failure
|
|
406
|
+
*/
|
|
407
|
+
async _pinToEndpoint(endpoint, body, topic, agentPubkey) {
|
|
408
|
+
const res = await fetch(endpoint, {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: { 'Content-Type': 'application/json' },
|
|
411
|
+
body: JSON.stringify({ content: body, topic, agentPubkey }),
|
|
412
|
+
});
|
|
413
|
+
if (!res.ok) {
|
|
414
|
+
const err = await res.text();
|
|
415
|
+
throw new AgentSDKError(`Validator pin failed at ${endpoint} (${res.status}): ${err}`, 'PIN_ENDPOINT_ERROR');
|
|
416
|
+
}
|
|
417
|
+
const json = await res.json();
|
|
418
|
+
return json.cid || null;
|
|
419
|
+
}
|
|
420
|
+
|
|
354
421
|
// ── Static PDA Helpers ──────────────────────────────────────────────────────
|
|
355
422
|
|
|
356
423
|
/**
|
|
@@ -563,22 +630,23 @@ class AgentClient {
|
|
|
563
630
|
let cid;
|
|
564
631
|
|
|
565
632
|
if (provider === 'x1scroll' || !provider) {
|
|
566
|
-
//
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
633
|
+
// Multi-pin to up to 5 active validators simultaneously for resilience
|
|
634
|
+
const validators = await this._getActiveValidators();
|
|
635
|
+
const selected = validators
|
|
636
|
+
.slice()
|
|
637
|
+
.sort(() => Math.random() - 0.5)
|
|
638
|
+
.slice(0, Math.min(5, validators.length));
|
|
639
|
+
|
|
640
|
+
const agentPubkeyStr = agentKeypair.publicKey.toBase58();
|
|
641
|
+
const results = await Promise.allSettled(
|
|
642
|
+
selected.map(v => this._pinToEndpoint(v.endpoint, body, topic, agentPubkeyStr))
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
const success = results.find(r => r.status === 'fulfilled' && r.value);
|
|
646
|
+
if (!success) {
|
|
647
|
+
throw new AgentSDKError('All validator pins failed', 'PIN_FAILED');
|
|
579
648
|
}
|
|
580
|
-
|
|
581
|
-
cid = json.cid;
|
|
649
|
+
cid = success.value;
|
|
582
650
|
|
|
583
651
|
} else if (provider === 'pinata') {
|
|
584
652
|
if (!pinataJwt) {
|
|
@@ -613,10 +681,16 @@ class AgentClient {
|
|
|
613
681
|
throw new AgentSDKError('IPFS upload returned no CID', 'NO_CID');
|
|
614
682
|
}
|
|
615
683
|
|
|
684
|
+
// Verify pin is reachable on public IPFS gateway (non-fatal)
|
|
685
|
+
const verified = await this._verifyPin(cid);
|
|
686
|
+
if (!verified) {
|
|
687
|
+
console.warn(`[x1scroll] Warning: CID ${cid} could not be verified on public IPFS gateway. Content may take time to propagate.`);
|
|
688
|
+
}
|
|
689
|
+
|
|
616
690
|
// Store CID on-chain
|
|
617
691
|
const result = await this.storeMemory(agentKeypair, agentRecordHuman, topic, cid, tags, encrypted);
|
|
618
692
|
|
|
619
|
-
return { ...result, cid };
|
|
693
|
+
return { ...result, cid, verified };
|
|
620
694
|
}
|
|
621
695
|
|
|
622
696
|
/**
|