@x1id/resolve 0.3.0 → 0.6.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/LICENSE +21 -0
- package/README.md +128 -1
- package/dist/accounts.d.ts +7 -0
- package/dist/accounts.js +8 -1
- package/dist/agent.d.ts +186 -0
- package/dist/agent.js +213 -0
- package/dist/attestation.d.ts +20 -8
- package/dist/attestation.js +22 -10
- package/dist/clearRecords.d.ts +60 -0
- package/dist/clearRecords.js +69 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +15 -2
- package/dist/recordWrite.d.ts +136 -0
- package/dist/recordWrite.js +228 -0
- package/dist/records.d.ts +24 -7
- package/dist/records.js +26 -9
- package/dist/register.d.ts +119 -0
- package/dist/register.js +183 -0
- package/dist/subname.d.ts +5 -0
- package/dist/subname.js +6 -1
- package/dist/textRecords.d.ts +18 -7
- package/dist/textRecords.js +20 -9
- package/dist/x402.d.ts +107 -0
- package/dist/x402.js +76 -0
- package/package.json +45 -9
- package/schema/agent-manifest.json +91 -0
- package/wasm/x1_resolve_wasm.wasm +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fortiblox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -257,6 +257,23 @@ const ix = buildCreateSubnameIx({
|
|
|
257
257
|
// add `ix` to a transaction, sign with payer + owner, send.
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
+
**Register a handle** (pays whatever the on-chain ramp charges as of the
|
|
261
|
+
landing slot — there is no client-supplied price):
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { buildRegisterIx, fetchConfigTreasury, HandleType } from "@x1id/resolve";
|
|
265
|
+
|
|
266
|
+
const treasury = await fetchConfigTreasury(rpc, configAccount); // Config.treasury, read fresh
|
|
267
|
+
const ix = buildRegisterIx({
|
|
268
|
+
programId, payer, owner,
|
|
269
|
+
config: configAccount, // the ["config"] PDA
|
|
270
|
+
treasury,
|
|
271
|
+
handle, // the ["handle", name] PDA — must be unregistered
|
|
272
|
+
name: "payday-bot",
|
|
273
|
+
handleType: HandleType.Agent,
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
260
277
|
**Gift a handle** (prepaid voucher; the recipient claims it later):
|
|
261
278
|
|
|
262
279
|
```ts
|
|
@@ -281,13 +298,123 @@ Also shipped, same build → sign → relay shape:
|
|
|
281
298
|
| Name lock / timelocked unlock | `buildLockHandleIx`, `buildInitiateUnlockIx`, `buildCompleteUnlockIx`, `buildCancelUnlockIx` |
|
|
282
299
|
| Record-write delegation | `buildSetRecordDelegateIx`, `buildRevokeRecordDelegateIx` |
|
|
283
300
|
| Typed text records (website / avatar / socials) | `buildCreateTextRecordIx`, `buildUpdateTextRecordIx`, `buildCloseTextRecordIx` |
|
|
301
|
+
| Register a handle | `buildRegisterIx` (+ `fetchConfigTreasury`) |
|
|
302
|
+
| Verify a non-SVM address record | `buildCreateRecordIx`, `buildVerifyRecordEthIx`, `buildVerifyRecordSvmIx`, `buildVerifyRecordBtcIx` (+ `buildRecordChallenge`, `splitEthSignature`, `splitBtcSignature`) — see [Verifying a non-SVM address record](#verifying-a-non-svm-address-record) |
|
|
284
303
|
| Domain / social attestations | `buildCreateAttestationIx`, `buildCloseAttestationIx` (+ `fetchAttestations`, `isHandleVerified`) |
|
|
304
|
+
| Agent records (identity, manifest, x402 hints) | `validateManifest`, `agentVerificationLevel`, `x402AcceptsFromManifest`, `agentTextRecords` — see [Agent records](#agent-records) below |
|
|
285
305
|
| Subname records / revoke | `buildRevokeSubnameIx`, `buildCreateSubnameRecordIx`, `buildUpdateSubnameRecordIx`, `buildCloseSubnameRecordIx` |
|
|
286
306
|
| Voucher claim / refund | `buildClaimVoucherIx`, `buildRefundVoucherIx` |
|
|
287
307
|
|
|
288
308
|
PDA seeds and account layouts are documented per module in the source and typed
|
|
289
309
|
end to end. Reads need only an RPC; writes need a signer.
|
|
290
310
|
|
|
311
|
+
## Agent records
|
|
312
|
+
|
|
313
|
+
An `@handle` can identify, describe, and get paid as an AI agent —
|
|
314
|
+
conventions on top of the existing primitives above (`HandleType.Agent`,
|
|
315
|
+
text records, address records, attestations), **no new on-chain account
|
|
316
|
+
family**. Full design: [`docs/agent-records.md`](../docs/agent-records.md).
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import {
|
|
320
|
+
AGENT_RECORD_KEYS,
|
|
321
|
+
agentTextRecords,
|
|
322
|
+
validateManifest,
|
|
323
|
+
agentVerificationLevel,
|
|
324
|
+
x402AcceptsFromManifest,
|
|
325
|
+
} from "@x1id/resolve";
|
|
326
|
+
|
|
327
|
+
// after fetching + liveTextRecords(...) for the handle:
|
|
328
|
+
const agent = agentTextRecords(textRecords);
|
|
329
|
+
if (agent.manifest) {
|
|
330
|
+
const raw = await fetch(agent.manifest).then((r) => r.json());
|
|
331
|
+
const result = validateManifest(raw, {
|
|
332
|
+
expectedName: "payday-bot",
|
|
333
|
+
expectedHandlePda: handleAccount,
|
|
334
|
+
expectedNetwork: "testnet",
|
|
335
|
+
addressRecords: liveRecords(records), // "the chain wins" cross-check
|
|
336
|
+
});
|
|
337
|
+
if (result.ok) {
|
|
338
|
+
const level = agentVerificationLevel({
|
|
339
|
+
handleType,
|
|
340
|
+
agentRecords: agent,
|
|
341
|
+
liveAttestations: liveAttestations(attestations),
|
|
342
|
+
manifest: result.manifest,
|
|
343
|
+
}); // 0, 1, or null — L2 ("pay-to confirmed") is a live HTTP fact, not implemented here; see the MCP server
|
|
344
|
+
const hints = x402AcceptsFromManifest(result.manifest, { payTo: verifiedAddress });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`validateManifest` never trusts a manifest's own claims over the chain: a
|
|
350
|
+
`payment.addresses[]` entry claiming `verified: true` is rejected outright if
|
|
351
|
+
the supplied on-chain records disagree. `agentVerificationLevel` reports only
|
|
352
|
+
the highest level it actually checked — L1 requires the manifest's claimed
|
|
353
|
+
attestation to match a *live* one, never assumed from presence alone.
|
|
354
|
+
|
|
355
|
+
## Verifying a non-SVM address record
|
|
356
|
+
|
|
357
|
+
Prove control of an ETH, BTC, or another SVM address so its `Record` gets
|
|
358
|
+
`verified: true`. On-chain, the program itself recovers the address from
|
|
359
|
+
the signature — this SDK never derives or checks an address client-side,
|
|
360
|
+
it just builds the challenge and packages the wallet's signature.
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import {
|
|
364
|
+
buildRecordChallenge,
|
|
365
|
+
buildCreateRecordIx,
|
|
366
|
+
buildVerifyRecordEthIx,
|
|
367
|
+
splitEthSignature,
|
|
368
|
+
} from "@x1id/resolve";
|
|
369
|
+
|
|
370
|
+
// 1. create the (unverified) record
|
|
371
|
+
const createIx = buildCreateRecordIx({
|
|
372
|
+
programId, payer, owner, handle, record,
|
|
373
|
+
coinType: 60, // ETH, per SLIP-44
|
|
374
|
+
value: ethAddressBytes,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// 2. build the challenge and have the wallet sign it (personal_sign)
|
|
378
|
+
const challenge = buildRecordChallenge("alice", 60, registeredAt, "someNonce123");
|
|
379
|
+
const sig65 = await wallet.signMessage(challenge); // 65 raw bytes: r || s || v
|
|
380
|
+
|
|
381
|
+
// 3. verify
|
|
382
|
+
const { signature, recoveryId } = splitEthSignature(sig65);
|
|
383
|
+
const verifyIx = buildVerifyRecordEthIx({
|
|
384
|
+
programId, owner, handle, record,
|
|
385
|
+
nonce: "someNonce123", signature, recoveryId,
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
`buildVerifyRecordSvmIx` (the claimed address just co-signs, no challenge
|
|
390
|
+
needed) and `buildVerifyRecordBtcIx` + `splitBtcSignature` (BIP-137, mainnet
|
|
391
|
+
compressed-P2PKH or bech32/P2WPKH only) follow the same shape.
|
|
392
|
+
|
|
393
|
+
## x402 payments
|
|
394
|
+
|
|
395
|
+
An agent gets paid over HTTP via [x402](https://github.com/x402-foundation/x402)'s
|
|
396
|
+
`"exact"` SVM scheme. Full design (including how transactions actually get
|
|
397
|
+
built/verified — that needs real Solana parsing, so it lives in the
|
|
398
|
+
[x1id MCP server](../mcp), not here): [`docs/x402.md`](../docs/x402.md).
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import { buildX402Requirements, X1_TESTNET_NETWORK } from "@x1id/resolve";
|
|
402
|
+
|
|
403
|
+
// server-side: one accepts[] entry for a 402 response
|
|
404
|
+
const requirements = buildX402Requirements({
|
|
405
|
+
payTo: sellerAddress,
|
|
406
|
+
asset: usdcMint,
|
|
407
|
+
amount: "1000000", // smallest units, decimal string
|
|
408
|
+
buyer: buyerAddress, // x1id v1 is self-sponsored: buyer pays their own fee, no facilitator
|
|
409
|
+
memo: "invoice-42",
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
This package only builds the requirements object — it does not build, sign,
|
|
414
|
+
or verify a transaction (that needs `@solana/web3.js`, deliberately kept out
|
|
415
|
+
of this zero-dependency package). Use the MCP server's `prepare_x402_payment`
|
|
416
|
+
/ `verify_x402_payment` tools for that.
|
|
417
|
+
|
|
291
418
|
## Errors
|
|
292
419
|
|
|
293
420
|
`ResolveError` carries a `code` a UI can branch on:
|
|
@@ -412,7 +539,7 @@ attached, and this file is the accurate statement.)
|
|
|
412
539
|
## Links
|
|
413
540
|
|
|
414
541
|
- Home & docs: **[x1id.io](https://x1id.io)**
|
|
415
|
-
- Developer docs: **[docs.
|
|
542
|
+
- Developer docs: **[docs.x1id.io](https://docs.x1id.io)**
|
|
416
543
|
- Questions & issues: [x1id.io](https://x1id.io) — contact links in the footer
|
|
417
544
|
|
|
418
545
|
## License
|
package/dist/accounts.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ export interface ParsedHandle {
|
|
|
27
27
|
* docs/record-trust.md.
|
|
28
28
|
*/
|
|
29
29
|
readonly registeredAt: bigint;
|
|
30
|
+
/**
|
|
31
|
+
* `Handle.records_cleared_at` (0 if never cleared) — set by `clear_records`
|
|
32
|
+
* (#8139). Every per-handle record/text-record/attestation must ALSO be
|
|
33
|
+
* judged stale when `updated_at < recordsClearedAt`, alongside (not instead
|
|
34
|
+
* of) the `registeredAt` epoch rule — see docs/record-trust.md.
|
|
35
|
+
*/
|
|
36
|
+
readonly recordsClearedAt: bigint;
|
|
30
37
|
/** Set when the handle is tokenized (NFT extension present). */
|
|
31
38
|
readonly nftMint: Uint8Array | null;
|
|
32
39
|
}
|
package/dist/accounts.js
CHANGED
|
@@ -26,6 +26,12 @@ const HANDLE_OWNER_OFFSET = 41;
|
|
|
26
26
|
// mirrors `Handle::NFT_EXT_OFFSET` in the program and `HANDLE_BASE_LEN` in
|
|
27
27
|
// tools/api.
|
|
28
28
|
const HANDLE_BASE_LEN = 8 + 149;
|
|
29
|
+
// `records_cleared_at` extension (`Handle::RECORDS_CLEARED_EXT_OFFSET`, #8139):
|
|
30
|
+
// an i64 LE the owner bumps via `clear_records` to bulk-invalidate every
|
|
31
|
+
// record/text-record/attestation without transferring the handle. Read
|
|
32
|
+
// leniently — a short or zeroed account means "never cleared" (0), same
|
|
33
|
+
// convention as every other Handle extension region.
|
|
34
|
+
const RECORDS_CLEARED_EXT_OFFSET = 229;
|
|
29
35
|
// SPL Token account: mint(32) | owner(32) | amount(u64 LE, 8) | ...
|
|
30
36
|
export const TOKEN_ACCOUNT_MIN_LEN = 72;
|
|
31
37
|
// SPL Token mint: mint_authority COption(4+32) | supply(u64 LE, 8) @36 | ...
|
|
@@ -82,7 +88,8 @@ export function parseHandleAccount(data) {
|
|
|
82
88
|
const nftMint = data.length >= HANDLE_BASE_LEN + 33 && data[HANDLE_BASE_LEN] === 1
|
|
83
89
|
? data.slice(HANDLE_BASE_LEN + 1, HANDLE_BASE_LEN + 33)
|
|
84
90
|
: null;
|
|
85
|
-
|
|
91
|
+
const recordsClearedAt = data.length >= RECORDS_CLEARED_EXT_OFFSET + 8 ? readI64(data, RECORDS_CLEARED_EXT_OFFSET) : 0n;
|
|
92
|
+
return { name, owner, registeredAt, recordsClearedAt, nftMint };
|
|
86
93
|
}
|
|
87
94
|
/** Build the RPC plumbing every reader in this package shares. */
|
|
88
95
|
export function makeAccountReader(rpcUrl, fetchImpl) {
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent records — identify, describe, and verify an `@handle` operating as
|
|
3
|
+
* an AI agent. See `docs/agent-records.md` for the full design and the
|
|
4
|
+
* honest account of what each verification level does and does not prove.
|
|
5
|
+
*
|
|
6
|
+
* This is conventions on EXISTING primitives, not a new account family:
|
|
7
|
+
* `HandleType.Agent` (already on-chain), well-known `TextRecord` keys
|
|
8
|
+
* (`textRecords.ts`, #7376), address `Record`s (`records.ts`), and
|
|
9
|
+
* `Attestation` (`attestation.ts`, #7379). Nothing here requires a program
|
|
10
|
+
* change.
|
|
11
|
+
*
|
|
12
|
+
* Hand-rolled like the rest of this package — no schema-interpreter
|
|
13
|
+
* dependency, no `@solana/web3.js` import. `validateManifest` is plain
|
|
14
|
+
* field checks, matching how `records.ts`/`textRecords.ts`/`attestation.ts`
|
|
15
|
+
* hand-decode rather than lean on a generic library.
|
|
16
|
+
*
|
|
17
|
+
* # L2 ("pay-to confirmed") is deliberately NOT implemented here
|
|
18
|
+
*
|
|
19
|
+
* It's a live HTTP fact against an arbitrary third-party host — forcing a
|
|
20
|
+
* network call inside a library used in contexts that may not want one.
|
|
21
|
+
* That check (`probe402`) belongs in a service that's already making
|
|
22
|
+
* network calls on the caller's behalf — tracked as the x1id MCP server,
|
|
23
|
+
* WP #8133. `AgentVerificationLevel` still names level 2 so every surface
|
|
24
|
+
* that reports a level shares one vocabulary.
|
|
25
|
+
*/
|
|
26
|
+
import type { HandleTextRecord } from "./textRecords.js";
|
|
27
|
+
import type { HandleAttestation } from "./attestation.js";
|
|
28
|
+
import type { HandleRecord } from "./records.js";
|
|
29
|
+
/** The six well-known `TextRecord` keys an agent handle may set. Every
|
|
30
|
+
* value must be `https://` EXCEPT `manifest`, which may also be
|
|
31
|
+
* `ipfs://<cid>` — see {@link checkAgentUrl}. */
|
|
32
|
+
export declare const AGENT_RECORD_KEYS: Readonly<{
|
|
33
|
+
readonly manifest: "agent.manifest";
|
|
34
|
+
readonly x402: "agent.x402";
|
|
35
|
+
readonly mcp: "agent.mcp";
|
|
36
|
+
readonly a2a: "agent.a2a";
|
|
37
|
+
readonly attestation: "agent.attestation";
|
|
38
|
+
readonly capabilities: "agent.capabilities";
|
|
39
|
+
}>;
|
|
40
|
+
export type AgentEndpointType = "x402" | "mcp" | "a2a" | "http";
|
|
41
|
+
/** `agent.manifest` v1 (`docs/agent-records.md` §3), as accepted by
|
|
42
|
+
* {@link validateManifest}. */
|
|
43
|
+
export interface AgentManifest {
|
|
44
|
+
readonly x1id: {
|
|
45
|
+
readonly version: 1;
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly network: "testnet" | "mainnet";
|
|
48
|
+
/** Base58 address of the `Handle` PDA — the uniqueness anchor. */
|
|
49
|
+
readonly handle: string;
|
|
50
|
+
};
|
|
51
|
+
readonly identity?: {
|
|
52
|
+
/** Base58 address of a non-stale `Attestation` account on this handle
|
|
53
|
+
* (kind DNS or SOCIAL). See `docs/agent-records.md` §4 for what this
|
|
54
|
+
* does and does not prove — it is NOT an agent-identity-registry
|
|
55
|
+
* ownership check, that concept has no SVM equivalent yet. */
|
|
56
|
+
readonly attestation?: string;
|
|
57
|
+
readonly description?: string;
|
|
58
|
+
};
|
|
59
|
+
readonly endpoints?: readonly {
|
|
60
|
+
readonly type: AgentEndpointType;
|
|
61
|
+
readonly url: string;
|
|
62
|
+
}[];
|
|
63
|
+
readonly capabilities?: {
|
|
64
|
+
readonly schema?: string;
|
|
65
|
+
readonly url: string;
|
|
66
|
+
};
|
|
67
|
+
readonly payment?: {
|
|
68
|
+
readonly addresses?: readonly {
|
|
69
|
+
readonly coinType: number;
|
|
70
|
+
readonly address: string;
|
|
71
|
+
readonly verified?: boolean;
|
|
72
|
+
}[];
|
|
73
|
+
readonly x402?: {
|
|
74
|
+
readonly network: string;
|
|
75
|
+
readonly asset?: string;
|
|
76
|
+
readonly scheme?: string;
|
|
77
|
+
readonly facilitator?: string;
|
|
78
|
+
readonly extra?: Readonly<Record<string, unknown>>;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** URL rule (`docs/agent-records.md` §2): every URL field is `https://`
|
|
83
|
+
* ONLY, except `agent.manifest`'s own record value, which may also be
|
|
84
|
+
* `ipfs://<cid>` (`allowIpfs`). */
|
|
85
|
+
export declare function checkAgentUrl(url: string, allowIpfs?: boolean): boolean;
|
|
86
|
+
export interface ValidateManifestOptions {
|
|
87
|
+
/** The handle name this manifest is being read FOR (no `@`). Required —
|
|
88
|
+
* a manifest that doesn't match is discarded, never partially trusted. */
|
|
89
|
+
readonly expectedName: string;
|
|
90
|
+
/** The `Handle` PDA address (base58) this manifest is being read FOR. */
|
|
91
|
+
readonly expectedHandlePda: string;
|
|
92
|
+
readonly expectedNetwork: "testnet" | "mainnet";
|
|
93
|
+
/** The handle's live, non-stale address records — when supplied, every
|
|
94
|
+
* `payment.addresses[]` entry claiming `verified: true` is cross-checked
|
|
95
|
+
* against them and rejected if the chain disagrees ("the chain wins",
|
|
96
|
+
* `docs/agent-records.md` §3). Omit to skip this cross-check (shape-only
|
|
97
|
+
* validation). */
|
|
98
|
+
readonly addressRecords?: readonly HandleRecord[];
|
|
99
|
+
}
|
|
100
|
+
export type ManifestValidationError = {
|
|
101
|
+
readonly code: "not-an-object";
|
|
102
|
+
} | {
|
|
103
|
+
readonly code: "missing-field";
|
|
104
|
+
readonly field: string;
|
|
105
|
+
} | {
|
|
106
|
+
readonly code: "bad-version";
|
|
107
|
+
} | {
|
|
108
|
+
readonly code: "name-mismatch";
|
|
109
|
+
} | {
|
|
110
|
+
readonly code: "handle-mismatch";
|
|
111
|
+
} | {
|
|
112
|
+
readonly code: "network-mismatch";
|
|
113
|
+
} | {
|
|
114
|
+
readonly code: "bad-url";
|
|
115
|
+
readonly field: string;
|
|
116
|
+
} | {
|
|
117
|
+
readonly code: "bad-attestation-address";
|
|
118
|
+
} | {
|
|
119
|
+
readonly code: "bad-payment-address";
|
|
120
|
+
} | {
|
|
121
|
+
readonly code: "overclaims-verified";
|
|
122
|
+
readonly coinType: number;
|
|
123
|
+
};
|
|
124
|
+
export type ManifestValidationResult = {
|
|
125
|
+
readonly ok: true;
|
|
126
|
+
readonly manifest: AgentManifest;
|
|
127
|
+
} | {
|
|
128
|
+
readonly ok: false;
|
|
129
|
+
readonly errors: readonly ManifestValidationError[];
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Validate a manifest's shape AND, when `addressRecords` is supplied, its
|
|
133
|
+
* agreement with the chain. Never trusts a claim the manifest makes about
|
|
134
|
+
* itself over what the caller already knows to be true on-chain.
|
|
135
|
+
*/
|
|
136
|
+
export declare function validateManifest(raw: unknown, opts: ValidateManifestOptions): ManifestValidationResult;
|
|
137
|
+
/** Pick the agent-related well-known keys out of a handle's live text
|
|
138
|
+
* records (pass the output of `liveTextRecords` — stale entries excluded
|
|
139
|
+
* already, matching every other read path's convention). */
|
|
140
|
+
export declare function agentTextRecords(records: readonly HandleTextRecord[]): Partial<Record<keyof typeof AGENT_RECORD_KEYS, string>>;
|
|
141
|
+
/** L2 is intentionally never returned here — see the module docs. `null`
|
|
142
|
+
* means "not even declared." */
|
|
143
|
+
export type AgentVerificationLevel = 0 | 1 | null;
|
|
144
|
+
export interface AgentVerificationInput {
|
|
145
|
+
readonly handleType: number;
|
|
146
|
+
/** `agentTextRecords(...)`'s output for this handle. */
|
|
147
|
+
readonly agentRecords: Partial<Record<keyof typeof AGENT_RECORD_KEYS, string>>;
|
|
148
|
+
/** This handle's live (non-stale) attestations — `liveAttestations(...)`. */
|
|
149
|
+
readonly liveAttestations: readonly HandleAttestation[];
|
|
150
|
+
/** The parsed manifest, if `agent.manifest` was fetched and validated.
|
|
151
|
+
* Omit if the manifest wasn't fetched — L1 is then reported as
|
|
152
|
+
* "not checked" (null), never "failed", per the reporting rule below. */
|
|
153
|
+
readonly manifest?: AgentManifest;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* L0/L1, computed from data the caller already has (no RPC calls in this
|
|
157
|
+
* function — see the module docs for why L2 requires a live network call
|
|
158
|
+
* and lives elsewhere).
|
|
159
|
+
*
|
|
160
|
+
* A reader reports only the highest level it actually checked. If
|
|
161
|
+
* `manifest` wasn't supplied, L1 is reported `null` ("not checked"), not
|
|
162
|
+
* `0` ("failed") — the same discipline ArcNS's reference design uses.
|
|
163
|
+
*/
|
|
164
|
+
export declare function agentVerificationLevel(input: AgentVerificationInput): AgentVerificationLevel;
|
|
165
|
+
export interface X402AcceptHint {
|
|
166
|
+
readonly scheme: string;
|
|
167
|
+
readonly network: string;
|
|
168
|
+
readonly resource: string;
|
|
169
|
+
readonly asset?: string;
|
|
170
|
+
readonly payTo?: string;
|
|
171
|
+
readonly facilitator?: string;
|
|
172
|
+
readonly extra?: Readonly<Record<string, unknown>>;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Turn a validated manifest into x402-shaped `accepts[]` hints. NEVER
|
|
176
|
+
* authoritative — the resource's own live 402 response always wins; this is
|
|
177
|
+
* a pre-flight convenience so a caller doesn't have to probe every endpoint
|
|
178
|
+
* just to learn the shape it should expect.
|
|
179
|
+
*
|
|
180
|
+
* `opts.payTo`, when supplied, is preferred over anything the manifest
|
|
181
|
+
* claims for itself (chain-verified address beats a self-reported mirror).
|
|
182
|
+
* Returns `[]` for an agent with no `x402` endpoints.
|
|
183
|
+
*/
|
|
184
|
+
export declare function x402AcceptsFromManifest(manifest: AgentManifest, opts?: {
|
|
185
|
+
readonly payTo?: string;
|
|
186
|
+
}): readonly X402AcceptHint[];
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent records — identify, describe, and verify an `@handle` operating as
|
|
3
|
+
* an AI agent. See `docs/agent-records.md` for the full design and the
|
|
4
|
+
* honest account of what each verification level does and does not prove.
|
|
5
|
+
*
|
|
6
|
+
* This is conventions on EXISTING primitives, not a new account family:
|
|
7
|
+
* `HandleType.Agent` (already on-chain), well-known `TextRecord` keys
|
|
8
|
+
* (`textRecords.ts`, #7376), address `Record`s (`records.ts`), and
|
|
9
|
+
* `Attestation` (`attestation.ts`, #7379). Nothing here requires a program
|
|
10
|
+
* change.
|
|
11
|
+
*
|
|
12
|
+
* Hand-rolled like the rest of this package — no schema-interpreter
|
|
13
|
+
* dependency, no `@solana/web3.js` import. `validateManifest` is plain
|
|
14
|
+
* field checks, matching how `records.ts`/`textRecords.ts`/`attestation.ts`
|
|
15
|
+
* hand-decode rather than lean on a generic library.
|
|
16
|
+
*
|
|
17
|
+
* # L2 ("pay-to confirmed") is deliberately NOT implemented here
|
|
18
|
+
*
|
|
19
|
+
* It's a live HTTP fact against an arbitrary third-party host — forcing a
|
|
20
|
+
* network call inside a library used in contexts that may not want one.
|
|
21
|
+
* That check (`probe402`) belongs in a service that's already making
|
|
22
|
+
* network calls on the caller's behalf — tracked as the x1id MCP server,
|
|
23
|
+
* WP #8133. `AgentVerificationLevel` still names level 2 so every surface
|
|
24
|
+
* that reports a level shares one vocabulary.
|
|
25
|
+
*/
|
|
26
|
+
import { X1_TESTNET_NETWORK } from "./x402.js";
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Well-known text-record keys
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/** The six well-known `TextRecord` keys an agent handle may set. Every
|
|
31
|
+
* value must be `https://` EXCEPT `manifest`, which may also be
|
|
32
|
+
* `ipfs://<cid>` — see {@link checkAgentUrl}. */
|
|
33
|
+
export const AGENT_RECORD_KEYS = Object.freeze({
|
|
34
|
+
manifest: "agent.manifest",
|
|
35
|
+
x402: "agent.x402",
|
|
36
|
+
mcp: "agent.mcp",
|
|
37
|
+
a2a: "agent.a2a",
|
|
38
|
+
attestation: "agent.attestation",
|
|
39
|
+
capabilities: "agent.capabilities",
|
|
40
|
+
});
|
|
41
|
+
const HTTPS_RE = /^https:\/\/[^\s@/]+(\/[^\s]*)?$/;
|
|
42
|
+
const IPFS_RE = /^ipfs:\/\/[^\s]+$/;
|
|
43
|
+
const BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
44
|
+
/** URL rule (`docs/agent-records.md` §2): every URL field is `https://`
|
|
45
|
+
* ONLY, except `agent.manifest`'s own record value, which may also be
|
|
46
|
+
* `ipfs://<cid>` (`allowIpfs`). */
|
|
47
|
+
export function checkAgentUrl(url, allowIpfs = false) {
|
|
48
|
+
if (HTTPS_RE.test(url) && url.length <= 2048)
|
|
49
|
+
return true;
|
|
50
|
+
return allowIpfs && IPFS_RE.test(url);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Validate a manifest's shape AND, when `addressRecords` is supplied, its
|
|
54
|
+
* agreement with the chain. Never trusts a claim the manifest makes about
|
|
55
|
+
* itself over what the caller already knows to be true on-chain.
|
|
56
|
+
*/
|
|
57
|
+
export function validateManifest(raw, opts) {
|
|
58
|
+
const errors = [];
|
|
59
|
+
if (typeof raw !== "object" || raw === null) {
|
|
60
|
+
return { ok: false, errors: [{ code: "not-an-object" }] };
|
|
61
|
+
}
|
|
62
|
+
const m = raw;
|
|
63
|
+
const x1id = m.x1id;
|
|
64
|
+
if (typeof x1id !== "object" || x1id === null) {
|
|
65
|
+
return { ok: false, errors: [{ code: "missing-field", field: "x1id" }] };
|
|
66
|
+
}
|
|
67
|
+
if (x1id.version !== 1)
|
|
68
|
+
errors.push({ code: "bad-version" });
|
|
69
|
+
if (x1id.name !== opts.expectedName)
|
|
70
|
+
errors.push({ code: "name-mismatch" });
|
|
71
|
+
if (x1id.handle !== opts.expectedHandlePda)
|
|
72
|
+
errors.push({ code: "handle-mismatch" });
|
|
73
|
+
if (x1id.network !== opts.expectedNetwork)
|
|
74
|
+
errors.push({ code: "network-mismatch" });
|
|
75
|
+
const identity = m.identity;
|
|
76
|
+
if (identity !== undefined) {
|
|
77
|
+
const att = identity.attestation;
|
|
78
|
+
if (att !== undefined && (typeof att !== "string" || !BASE58_RE.test(att))) {
|
|
79
|
+
errors.push({ code: "bad-attestation-address" });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const endpoints = m.endpoints;
|
|
83
|
+
if (endpoints !== undefined) {
|
|
84
|
+
for (const e of endpoints) {
|
|
85
|
+
if (typeof e.url !== "string" || !checkAgentUrl(e.url, false)) {
|
|
86
|
+
errors.push({ code: "bad-url", field: "endpoints[].url" });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const capabilities = m.capabilities;
|
|
91
|
+
if (capabilities !== undefined) {
|
|
92
|
+
if (typeof capabilities.url !== "string" || !checkAgentUrl(capabilities.url, false)) {
|
|
93
|
+
errors.push({ code: "bad-url", field: "capabilities.url" });
|
|
94
|
+
}
|
|
95
|
+
if (capabilities.schema !== undefined &&
|
|
96
|
+
(typeof capabilities.schema !== "string" || !checkAgentUrl(capabilities.schema, false))) {
|
|
97
|
+
errors.push({ code: "bad-url", field: "capabilities.schema" });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const payment = m.payment;
|
|
101
|
+
if (payment !== undefined) {
|
|
102
|
+
const addresses = payment.addresses;
|
|
103
|
+
if (addresses !== undefined) {
|
|
104
|
+
for (const a of addresses) {
|
|
105
|
+
if (typeof a.address !== "string" || a.address.length === 0 || a.address.length > 128) {
|
|
106
|
+
errors.push({ code: "bad-payment-address" });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (a.verified === true && opts.addressRecords) {
|
|
110
|
+
// "The chain wins": a manifest claiming verified:true for a coin
|
|
111
|
+
// type the chain doesn't actually verify (or that resolves to a
|
|
112
|
+
// different address) is rejected outright, never trusted.
|
|
113
|
+
const onChain = opts.addressRecords.find((r) => r.coinType === a.coinType && !r.stale);
|
|
114
|
+
const chainVerified = onChain !== undefined && onChain.address === a.address && onChain.verified === true;
|
|
115
|
+
if (!chainVerified)
|
|
116
|
+
errors.push({ code: "overclaims-verified", coinType: a.coinType });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const x402 = payment.x402;
|
|
121
|
+
if (x402 !== undefined) {
|
|
122
|
+
if (typeof x402.network !== "string" || x402.network.length === 0) {
|
|
123
|
+
errors.push({ code: "missing-field", field: "payment.x402.network" });
|
|
124
|
+
}
|
|
125
|
+
if (x402.facilitator !== undefined &&
|
|
126
|
+
(typeof x402.facilitator !== "string" || !checkAgentUrl(x402.facilitator, false))) {
|
|
127
|
+
errors.push({ code: "bad-url", field: "payment.x402.facilitator" });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (errors.length > 0)
|
|
132
|
+
return { ok: false, errors };
|
|
133
|
+
return { ok: true, manifest: raw };
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Reading agent text records off a handle's already-fetched records
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
/** Pick the agent-related well-known keys out of a handle's live text
|
|
139
|
+
* records (pass the output of `liveTextRecords` — stale entries excluded
|
|
140
|
+
* already, matching every other read path's convention). */
|
|
141
|
+
export function agentTextRecords(records) {
|
|
142
|
+
const byKey = new Map(records.map((r) => [r.key, r.text]));
|
|
143
|
+
const out = {};
|
|
144
|
+
for (const [name, key] of Object.entries(AGENT_RECORD_KEYS)) {
|
|
145
|
+
const v = byKey.get(key);
|
|
146
|
+
if (v)
|
|
147
|
+
out[name] = v;
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
/** `HandleType.Agent`'s numeric value (matches `voucher.ts`'s
|
|
152
|
+
* `HandleType.Agent`). Duplicated here (not imported) to keep this module
|
|
153
|
+
* independent of `voucher.ts`'s import surface. */
|
|
154
|
+
const HANDLE_TYPE_AGENT = 3;
|
|
155
|
+
/**
|
|
156
|
+
* L0/L1, computed from data the caller already has (no RPC calls in this
|
|
157
|
+
* function — see the module docs for why L2 requires a live network call
|
|
158
|
+
* and lives elsewhere).
|
|
159
|
+
*
|
|
160
|
+
* A reader reports only the highest level it actually checked. If
|
|
161
|
+
* `manifest` wasn't supplied, L1 is reported `null` ("not checked"), not
|
|
162
|
+
* `0` ("failed") — the same discipline ArcNS's reference design uses.
|
|
163
|
+
*/
|
|
164
|
+
export function agentVerificationLevel(input) {
|
|
165
|
+
const declared = input.handleType === HANDLE_TYPE_AGENT || input.agentRecords.manifest !== undefined;
|
|
166
|
+
if (!declared)
|
|
167
|
+
return null;
|
|
168
|
+
if (input.manifest === undefined)
|
|
169
|
+
return 0;
|
|
170
|
+
const claimedAttestation = input.manifest.identity?.attestation;
|
|
171
|
+
if (claimedAttestation === undefined)
|
|
172
|
+
return 0;
|
|
173
|
+
const matches = input.liveAttestations.some((a) => a.account === claimedAttestation);
|
|
174
|
+
return matches ? 1 : 0;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Turn a validated manifest into x402-shaped `accepts[]` hints. NEVER
|
|
178
|
+
* authoritative — the resource's own live 402 response always wins; this is
|
|
179
|
+
* a pre-flight convenience so a caller doesn't have to probe every endpoint
|
|
180
|
+
* just to learn the shape it should expect.
|
|
181
|
+
*
|
|
182
|
+
* `opts.payTo`, when supplied, is preferred over anything the manifest
|
|
183
|
+
* claims for itself (chain-verified address beats a self-reported mirror).
|
|
184
|
+
* Returns `[]` for an agent with no `x402` endpoints.
|
|
185
|
+
*/
|
|
186
|
+
export function x402AcceptsFromManifest(manifest, opts) {
|
|
187
|
+
const x402Endpoints = (manifest.endpoints ?? []).filter((e) => e.type === "x402");
|
|
188
|
+
if (x402Endpoints.length === 0)
|
|
189
|
+
return [];
|
|
190
|
+
const x402 = manifest.payment?.x402;
|
|
191
|
+
// X1_TESTNET_NETWORK is the real, verified x402 network id (see x402.ts).
|
|
192
|
+
// Mainnet has no such id yet — X1 mainnet doesn't exist — so a manifest
|
|
193
|
+
// claiming network: "mainnet" without its own explicit x402.network gets
|
|
194
|
+
// an honestly-provisional placeholder rather than a fabricated genesis hash.
|
|
195
|
+
const network = x402?.network ?? (manifest.x1id.network === "testnet" ? X1_TESTNET_NETWORK : "x1:mainnet-unassigned");
|
|
196
|
+
const payTo = opts?.payTo ?? manifest.payment?.addresses?.[0]?.address;
|
|
197
|
+
return x402Endpoints.map((e) => {
|
|
198
|
+
const hint = {
|
|
199
|
+
scheme: x402?.scheme ?? "exact",
|
|
200
|
+
network,
|
|
201
|
+
resource: e.url,
|
|
202
|
+
};
|
|
203
|
+
if (x402?.asset !== undefined)
|
|
204
|
+
hint.asset = x402.asset;
|
|
205
|
+
if (payTo !== undefined)
|
|
206
|
+
hint.payTo = payTo;
|
|
207
|
+
if (x402?.facilitator !== undefined)
|
|
208
|
+
hint.facilitator = x402.facilitator;
|
|
209
|
+
if (x402?.extra !== undefined)
|
|
210
|
+
hint.extra = x402.extra;
|
|
211
|
+
return hint;
|
|
212
|
+
});
|
|
213
|
+
}
|
package/dist/attestation.d.ts
CHANGED
|
@@ -50,6 +50,15 @@
|
|
|
50
50
|
* documented blind spot is shared with `Handle.owner`/`Primary`/
|
|
51
51
|
* `RecordDelegate`: a bearer-NFT marketplace trade bumps no epoch, so the
|
|
52
52
|
* attestation keeps reading live until the attestor re-reviews or revokes.)
|
|
53
|
+
*
|
|
54
|
+
* # `records_cleared_at` (#8139) — the SECOND, independent staleness rule
|
|
55
|
+
*
|
|
56
|
+
* `clear_records` lets an owner bulk-invalidate every record RIGHT NOW,
|
|
57
|
+
* without a transfer — its own doc comment (lib.rs) names attestations
|
|
58
|
+
* explicitly alongside records/text-records as sharing this epoch rule. The
|
|
59
|
+
* same functions therefore also demand the handle's `recordsClearedAt` (0 if
|
|
60
|
+
* never cleared, from `ParsedHandle`), and an attestation is `stale` when
|
|
61
|
+
* EITHER `attested_at < registeredAt` OR `attested_at < recordsClearedAt`.
|
|
53
62
|
*/
|
|
54
63
|
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
55
64
|
import type { RpcFn } from "./accounts.js";
|
|
@@ -113,13 +122,15 @@ export interface HandleAttestation {
|
|
|
113
122
|
* Decode one Attestation account.
|
|
114
123
|
*
|
|
115
124
|
* `registeredAt` is the owning Handle's `registered_at`, decoded by the
|
|
116
|
-
* caller from the Handle account — it decides `stale`.
|
|
117
|
-
*
|
|
125
|
+
* caller from the Handle account — it decides `stale`. `recordsClearedAt` is
|
|
126
|
+
* that same Handle's `recordsClearedAt` (0 if never cleared, #8139), a
|
|
127
|
+
* SECOND independent staleness anchor. There is intentionally no overload
|
|
128
|
+
* without either (see the module docs).
|
|
118
129
|
*
|
|
119
130
|
* Returns null for anything that is not an Attestation: wrong length or
|
|
120
131
|
* wrong discriminator.
|
|
121
132
|
*/
|
|
122
|
-
export declare function decodeAttestation(raw: Uint8Array, account: string, registeredAt: bigint): HandleAttestation | null;
|
|
133
|
+
export declare function decodeAttestation(raw: Uint8Array, account: string, registeredAt: bigint, recordsClearedAt: bigint): HandleAttestation | null;
|
|
123
134
|
/** The attestations that vouch for the CURRENT owner — `stale` ones
|
|
124
135
|
* excluded. This is the list to judge verification from. */
|
|
125
136
|
export declare function liveAttestations(attestations: readonly HandleAttestation[]): HandleAttestation[];
|
|
@@ -139,12 +150,13 @@ export declare function isHandleVerified(attestations: readonly HandleAttestatio
|
|
|
139
150
|
*
|
|
140
151
|
* `registeredAt` is `Handle.registered_at` as decoded from the Handle
|
|
141
152
|
* account the caller already has — the staleness rule needs it, and there
|
|
142
|
-
* is no variant of this function without it.
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
153
|
+
* is no variant of this function without it. `recordsClearedAt` is that same
|
|
154
|
+
* Handle's `recordsClearedAt` (0 if never cleared, #8139) — pass it through.
|
|
155
|
+
* Every attestation is returned, stale ones flagged, so an owner surface can
|
|
156
|
+
* show what a previous registration left behind; anything that renders a
|
|
157
|
+
* verified badge takes {@link isHandleVerified} / {@link liveAttestations}.
|
|
146
158
|
*/
|
|
147
|
-
export declare function fetchAttestations(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint): Promise<HandleAttestation[]>;
|
|
159
|
+
export declare function fetchAttestations(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint): Promise<HandleAttestation[]>;
|
|
148
160
|
export interface SetAttestorParams {
|
|
149
161
|
/** The registry program id. */
|
|
150
162
|
readonly programId: AddressLike;
|