@x1scroll/agent-sdk 1.0.3 → 1.1.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/README.md +38 -0
- package/package.json +1 -1
- package/src/index.js +96 -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,17 @@ 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
|
+
// ── Validator Storage Registry Program (LIVE on X1 Mainnet) ──────────────────
|
|
34
|
+
const STORAGE_REGISTRY_PROGRAM_ID = new PublicKey('GqzvCjz8nzxWxH39twk4oPfFaHXeyVDty9oJ6F4UcfF5');
|
|
35
|
+
|
|
36
|
+
// ── Fallback validators — used when registry is empty or unreachable ───────────
|
|
37
|
+
const FALLBACK_VALIDATORS = [
|
|
38
|
+
{ endpoint: 'https://x1scroll.io/api/ipfs/upload', active: true, fallback: true },
|
|
39
|
+
];
|
|
40
|
+
|
|
30
41
|
// ── Protocol constants — hardcoded, do not change ─────────────────────────────
|
|
31
42
|
/**
|
|
32
43
|
* On-chain program address. Immutable — this SDK only talks to this program.
|
|
@@ -312,8 +323,10 @@ class AgentClient {
|
|
|
312
323
|
);
|
|
313
324
|
}
|
|
314
325
|
|
|
315
|
-
this.rpcUrl
|
|
316
|
-
this._connection
|
|
326
|
+
this.rpcUrl = rpcUrl;
|
|
327
|
+
this._connection = null; // lazy
|
|
328
|
+
this._registryCache = null;
|
|
329
|
+
this._registryCacheExpiry = 0;
|
|
317
330
|
}
|
|
318
331
|
|
|
319
332
|
// ── Internal ────────────────────────────────────────────────────────────────
|
|
@@ -351,6 +364,64 @@ class AgentClient {
|
|
|
351
364
|
return sig;
|
|
352
365
|
}
|
|
353
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Get active validators from on-chain registry (simulated — falls back to hardcoded list).
|
|
369
|
+
* Results are cached for REGISTRY_CACHE_TTL (5 minutes).
|
|
370
|
+
* @returns {Promise<Array<{endpoint: string, active: boolean, fallback?: boolean}>>}
|
|
371
|
+
*/
|
|
372
|
+
async _getActiveValidators() {
|
|
373
|
+
const now = Date.now();
|
|
374
|
+
if (this._registryCache && now < this._registryCacheExpiry) {
|
|
375
|
+
return this._registryCache;
|
|
376
|
+
}
|
|
377
|
+
// Registry program is live: GqzvCjz8nzxWxH39twk4oPfFaHXeyVDty9oJ6F4UcfF5
|
|
378
|
+
// TODO: query on-chain StorageNode accounts via getProgramAccounts
|
|
379
|
+
// For now: return fallback while validator onboarding ramps up
|
|
380
|
+
this._registryCache = FALLBACK_VALIDATORS;
|
|
381
|
+
this._registryCacheExpiry = now + REGISTRY_CACHE_TTL;
|
|
382
|
+
return this._registryCache;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Verify that a pinned CID is reachable on the public IPFS gateway.
|
|
387
|
+
* Non-fatal — returns false on failure (content may still propagate).
|
|
388
|
+
* @param {string} cid
|
|
389
|
+
* @returns {Promise<boolean>}
|
|
390
|
+
*/
|
|
391
|
+
async _verifyPin(cid) {
|
|
392
|
+
try {
|
|
393
|
+
const res = await fetch(`https://ipfs.io/ipfs/${cid}`, {
|
|
394
|
+
method: 'HEAD',
|
|
395
|
+
signal: AbortSignal.timeout(8000),
|
|
396
|
+
});
|
|
397
|
+
return res.ok;
|
|
398
|
+
} catch {
|
|
399
|
+
return false; // non-fatal — log warning but don't throw
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Pin content to a single validator endpoint.
|
|
405
|
+
* @param {string} endpoint
|
|
406
|
+
* @param {string} body Serialized content
|
|
407
|
+
* @param {string} topic
|
|
408
|
+
* @param {string} agentPubkey
|
|
409
|
+
* @returns {Promise<string|null>} CID string on success, null on failure
|
|
410
|
+
*/
|
|
411
|
+
async _pinToEndpoint(endpoint, body, topic, agentPubkey) {
|
|
412
|
+
const res = await fetch(endpoint, {
|
|
413
|
+
method: 'POST',
|
|
414
|
+
headers: { 'Content-Type': 'application/json' },
|
|
415
|
+
body: JSON.stringify({ content: body, topic, agentPubkey }),
|
|
416
|
+
});
|
|
417
|
+
if (!res.ok) {
|
|
418
|
+
const err = await res.text();
|
|
419
|
+
throw new AgentSDKError(`Validator pin failed at ${endpoint} (${res.status}): ${err}`, 'PIN_ENDPOINT_ERROR');
|
|
420
|
+
}
|
|
421
|
+
const json = await res.json();
|
|
422
|
+
return json.cid || null;
|
|
423
|
+
}
|
|
424
|
+
|
|
354
425
|
// ── Static PDA Helpers ──────────────────────────────────────────────────────
|
|
355
426
|
|
|
356
427
|
/**
|
|
@@ -563,22 +634,23 @@ class AgentClient {
|
|
|
563
634
|
let cid;
|
|
564
635
|
|
|
565
636
|
if (provider === 'x1scroll' || !provider) {
|
|
566
|
-
//
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
637
|
+
// Multi-pin to up to 5 active validators simultaneously for resilience
|
|
638
|
+
const validators = await this._getActiveValidators();
|
|
639
|
+
const selected = validators
|
|
640
|
+
.slice()
|
|
641
|
+
.sort(() => Math.random() - 0.5)
|
|
642
|
+
.slice(0, Math.min(5, validators.length));
|
|
643
|
+
|
|
644
|
+
const agentPubkeyStr = agentKeypair.publicKey.toBase58();
|
|
645
|
+
const results = await Promise.allSettled(
|
|
646
|
+
selected.map(v => this._pinToEndpoint(v.endpoint, body, topic, agentPubkeyStr))
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
const success = results.find(r => r.status === 'fulfilled' && r.value);
|
|
650
|
+
if (!success) {
|
|
651
|
+
throw new AgentSDKError('All validator pins failed', 'PIN_FAILED');
|
|
579
652
|
}
|
|
580
|
-
|
|
581
|
-
cid = json.cid;
|
|
653
|
+
cid = success.value;
|
|
582
654
|
|
|
583
655
|
} else if (provider === 'pinata') {
|
|
584
656
|
if (!pinataJwt) {
|
|
@@ -613,10 +685,16 @@ class AgentClient {
|
|
|
613
685
|
throw new AgentSDKError('IPFS upload returned no CID', 'NO_CID');
|
|
614
686
|
}
|
|
615
687
|
|
|
688
|
+
// Verify pin is reachable on public IPFS gateway (non-fatal)
|
|
689
|
+
const verified = await this._verifyPin(cid);
|
|
690
|
+
if (!verified) {
|
|
691
|
+
console.warn(`[x1scroll] Warning: CID ${cid} could not be verified on public IPFS gateway. Content may take time to propagate.`);
|
|
692
|
+
}
|
|
693
|
+
|
|
616
694
|
// Store CID on-chain
|
|
617
695
|
const result = await this.storeMemory(agentKeypair, agentRecordHuman, topic, cid, tags, encrypted);
|
|
618
696
|
|
|
619
|
-
return { ...result, cid };
|
|
697
|
+
return { ...result, cid, verified };
|
|
620
698
|
}
|
|
621
699
|
|
|
622
700
|
/**
|