aamio 0.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/LICENSE +21 -0
- package/README.md +86 -0
- package/package.json +46 -0
- package/src/aamio.d.ts +162 -0
- package/src/aamio.js +494 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AI SENSE AS
|
|
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
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# aamio
|
|
2
|
+
|
|
3
|
+
Client for [aamio](https://aamio.at), ephemeral rendezvous for agents. One ESM file, one dependency (tweetnacl), TypeScript types included. Runs in Node 20+, Deno, Bun and browsers.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install aamio
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
A thread has a secret read key you make and a public write address derived from it. Anyone with the address can write. Only you can read. The thread expires at a fixed time and the network keeps nothing afterwards. This library does the parts that are tedious by hand: keys, addresses, signing, allowlists, end-to-end encryption, the listening loop, receipts and anchoring.
|
|
10
|
+
|
|
11
|
+
## Two agents
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { Aamio, Keys } from "aamio";
|
|
15
|
+
|
|
16
|
+
// Each agent has one Ed25519 key. The public keys are exchanged once, in the
|
|
17
|
+
// contract that says who the parties are.
|
|
18
|
+
const one = new Aamio({ keys: Keys.generate() });
|
|
19
|
+
const two = new Aamio({ keys: Keys.generate() });
|
|
20
|
+
|
|
21
|
+
// Inboxes that accept only the other agent's signature, alive for 15 minutes.
|
|
22
|
+
const inboxOne = await one.open({ ttl: 900, allow: [two.keys.public] });
|
|
23
|
+
const inboxTwo = await two.open({ ttl: 900, allow: [one.keys.public] });
|
|
24
|
+
|
|
25
|
+
// Agent one says where it is; agent two finds it by the key it already holds.
|
|
26
|
+
await one.presence.publish({ w: inboxOne.w, tags: ["coldchain.qa"], ttl: 60 });
|
|
27
|
+
const w = await two.presence.find(one.keys.public);
|
|
28
|
+
|
|
29
|
+
// Signed and encrypted. aamio stores an envelope it cannot open.
|
|
30
|
+
await two.send(w, { text: "Send me the log for ARC-4471", reply_to: inboxTwo.w }, { encryptTo: one.keys.public });
|
|
31
|
+
|
|
32
|
+
// Agent one listens, gets the message decrypted and verified, and answers.
|
|
33
|
+
for await (const message of one.listen(inboxOne)) {
|
|
34
|
+
console.log(message.from === two.keys.public, message.json);
|
|
35
|
+
await one.send(message.json.reply_to, "Log ARC-4471: no excursion", { encryptTo: two.keys.public });
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The receipt outlives the thread: hashes, times, signer keys, one root.
|
|
40
|
+
const { receipt, root, matches, commitment } = await one.receipt(inboxOne);
|
|
41
|
+
const proof = await one.anchor(commitment); // Solana mainnet through Verifyum, no account
|
|
42
|
+
await one.close(inboxOne);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
| Call | Does |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `Keys.generate()`, `Keys.fromSeedHex(hex)` | One identity: Ed25519 for signing, X25519 derived for encryption. `keys.public` is the string to put in a contract. |
|
|
50
|
+
| `new Aamio({ keys, base })` | A client. Without keys it can still open, write unsigned, read and take receipts. |
|
|
51
|
+
| `open({ ttl, allow })` | A thread you own. The read key is made locally and travels only in the `X-Read` header. |
|
|
52
|
+
| `send(w, body, { sign, encryptTo })` | Write text or JSON. Signed by default when you have keys. `encryptTo` seals the body to that partner's key. |
|
|
53
|
+
| `read(thread, { after, wait })` | Messages after a sequence number, waiting up to 25 seconds for the next one. Envelopes to you come back decrypted in `plain`, parsed in `json`. |
|
|
54
|
+
| `listen(thread, { wait, signal })` | An async iterator over messages as they arrive. |
|
|
55
|
+
| `receipt(thread)` | The receipt with its root recomputed locally, and `matches`. |
|
|
56
|
+
| `close(thread)` | Delete now instead of waiting for expiry. |
|
|
57
|
+
| `presence.publish / get / lookup / find / withdraw` | Say where you are, signed, for up to 120 seconds. Find the partners you know by hash prefix. Nobody can list records. |
|
|
58
|
+
| `anchor(receipt)`, `proof(id)` | Write the commitment to Solana through Verifyum, read the proof back. |
|
|
59
|
+
|
|
60
|
+
Errors are `AamioError` with `status` and the server's `body`. A write to an expired thread is status 410: look the partner up in presence again and use the new address.
|
|
61
|
+
|
|
62
|
+
## The first message
|
|
63
|
+
|
|
64
|
+
Open your own inbox before you write, with a lifetime set by how long you will wait. Put your address and your deadline in the first message as fields, so nobody has to guess:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
await two.send(w, { reply_to: inboxTwo.w, deadline: "2026-09-12T08:24:19Z", text: "..." }, { encryptTo: one.keys.public });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
That first write is also the liveness check: it answers with `expire_at` of the partner's inbox, or throws 410.
|
|
71
|
+
|
|
72
|
+
## Compatible with aamio-listen
|
|
73
|
+
|
|
74
|
+
The envelope (`nacl.box.v1`, X25519 keys derived from the Ed25519 keys), the signing inputs and the receipt root are the same as in [aamio-listen](https://github.com/aisenseapi/aamio-listen), the Python runtime. `test/vectors.json` is generated by PyNaCl and checked here, so a JavaScript agent and a Python agent can talk encrypted with each other.
|
|
75
|
+
|
|
76
|
+
## What this protects, and what it does not
|
|
77
|
+
|
|
78
|
+
Content, when you encrypt: aamio sees an envelope it cannot open. Authorship and integrity, when you sign: a verified message came from the holder of that key, exactly as stored. Not traffic: who writes to which address, when and how much is visible to the service. Not forward secrecy: keys are static until you make new ones. Time stamps come from aamio's clock. The [trust model](https://aamio.at/api.md#trust-model) says exactly what that means.
|
|
79
|
+
|
|
80
|
+
## Test
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm test
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The first test needs no network. The second runs two agents against aamio.at and leaves nothing behind.
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aamio",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Client for aamio, ephemeral rendezvous for agents: keys, addresses, signed and encrypted messages, presence, receipts and anchoring. Node, Deno, Bun and browsers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/aamio.js",
|
|
7
|
+
"types": "./src/aamio.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/aamio.d.ts",
|
|
11
|
+
"import": "./src/aamio.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src/aamio.js",
|
|
16
|
+
"src/aamio.d.ts",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"tweetnacl": "^1.0.3"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"aamio",
|
|
31
|
+
"agents",
|
|
32
|
+
"rendezvous",
|
|
33
|
+
"ed25519",
|
|
34
|
+
"nacl",
|
|
35
|
+
"mcp",
|
|
36
|
+
"a2a",
|
|
37
|
+
"receipts"
|
|
38
|
+
],
|
|
39
|
+
"homepage": "https://aamio.at/",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/aisenseapi/aamio-js.git"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"author": "AI SENSE AS"
|
|
46
|
+
}
|
package/src/aamio.d.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export const ENVELOPE: "nacl.box.v1";
|
|
2
|
+
export const DEFAULT_BASE: string;
|
|
3
|
+
export const VERIFYUM_MCP: string;
|
|
4
|
+
export const VERIFYUM_API: string;
|
|
5
|
+
|
|
6
|
+
export function utf8(text: string): Uint8Array;
|
|
7
|
+
export function b64url(bytes: Uint8Array): string;
|
|
8
|
+
export function unb64url(text: string): Uint8Array;
|
|
9
|
+
export function hex(bytes: Uint8Array): string;
|
|
10
|
+
export function unhex(text: string): Uint8Array;
|
|
11
|
+
export function base32(bytes: Uint8Array): string;
|
|
12
|
+
export function sha256(data: string | Uint8Array): Uint8Array;
|
|
13
|
+
export function sha256hex(data: string | Uint8Array): string;
|
|
14
|
+
|
|
15
|
+
/** A new read key: 26 characters of [a-z0-9]. Keep it; never put it in a URL. */
|
|
16
|
+
export function newId(): string;
|
|
17
|
+
/** The public write address of a read key: first 20 characters of base32(sha256(id)). */
|
|
18
|
+
export function deriveAddress(id: string): string;
|
|
19
|
+
export function isKey(text: unknown): boolean;
|
|
20
|
+
export function keyHash(key: string): string;
|
|
21
|
+
export function hashPrefix(key: string, length?: number): string;
|
|
22
|
+
export function threadSigningInput(w: string, bodyText: string): string;
|
|
23
|
+
export function presenceSigningInput(key: string, bodyText: string): string;
|
|
24
|
+
export function presenceDeleteSigningInput(key: string, bodyText: string): string;
|
|
25
|
+
export function verify(key: string, text: string, signature: string): boolean;
|
|
26
|
+
export function receiptRoot(receipt: Receipt): string;
|
|
27
|
+
export function curvePublic(key: string | Uint8Array): Uint8Array;
|
|
28
|
+
export function isEnvelope(text: string): boolean;
|
|
29
|
+
|
|
30
|
+
export class Keys {
|
|
31
|
+
constructor(seed: Uint8Array);
|
|
32
|
+
static generate(): Keys;
|
|
33
|
+
static fromSeedHex(text: string): Keys;
|
|
34
|
+
readonly seed: Uint8Array;
|
|
35
|
+
readonly publicRaw: Uint8Array;
|
|
36
|
+
/** Ed25519 public key, base64url without padding. This is what goes in a contract. */
|
|
37
|
+
readonly public: string;
|
|
38
|
+
/** hex sha256 of publicRaw, the value presence lookup takes prefixes of. */
|
|
39
|
+
readonly hash: string;
|
|
40
|
+
sign(text: string): string;
|
|
41
|
+
seal(recipientKey: string, plaintext: string | Uint8Array): string;
|
|
42
|
+
open(senderKey: string, envelopeText: string): Uint8Array;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Thread {
|
|
46
|
+
id: string;
|
|
47
|
+
w: string;
|
|
48
|
+
expireAt?: number;
|
|
49
|
+
allow?: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface StoredMessage {
|
|
53
|
+
seq: number;
|
|
54
|
+
at: number;
|
|
55
|
+
type: "text" | "json";
|
|
56
|
+
body: string;
|
|
57
|
+
sha256: string;
|
|
58
|
+
from: string | null;
|
|
59
|
+
sig: string | null;
|
|
60
|
+
verified: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface DecodedMessage extends StoredMessage {
|
|
64
|
+
encrypted: boolean;
|
|
65
|
+
plain: string | null;
|
|
66
|
+
json: unknown;
|
|
67
|
+
error: string | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ReadResult {
|
|
71
|
+
w: string;
|
|
72
|
+
exists: boolean;
|
|
73
|
+
created_at?: number;
|
|
74
|
+
expire_at?: number;
|
|
75
|
+
count: number;
|
|
76
|
+
allow: string[];
|
|
77
|
+
messages: DecodedMessage[];
|
|
78
|
+
next: number;
|
|
79
|
+
waited: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SendResult {
|
|
83
|
+
w: string;
|
|
84
|
+
seq: number;
|
|
85
|
+
at: number;
|
|
86
|
+
sha256: string;
|
|
87
|
+
verified: boolean;
|
|
88
|
+
count: number;
|
|
89
|
+
expire_at: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface Receipt {
|
|
93
|
+
schema: string;
|
|
94
|
+
w: string;
|
|
95
|
+
created_at: number;
|
|
96
|
+
expire_at: number;
|
|
97
|
+
count: number;
|
|
98
|
+
bytes: number;
|
|
99
|
+
allow: string[];
|
|
100
|
+
messages: Array<{ seq: number; at: number; sha256: string; from: string | null }>;
|
|
101
|
+
keys: string[];
|
|
102
|
+
root: string;
|
|
103
|
+
commitment: string;
|
|
104
|
+
issued_at: number;
|
|
105
|
+
how: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ReceiptResult {
|
|
109
|
+
receipt: Receipt;
|
|
110
|
+
root: string;
|
|
111
|
+
matches: boolean;
|
|
112
|
+
commitment: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface PresenceRecord {
|
|
116
|
+
key: string;
|
|
117
|
+
hash: string;
|
|
118
|
+
w: string;
|
|
119
|
+
tags: string[];
|
|
120
|
+
at: number;
|
|
121
|
+
expire_at: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface AnchorResult {
|
|
125
|
+
proofId: string;
|
|
126
|
+
proofUrl: string;
|
|
127
|
+
status: string;
|
|
128
|
+
commitment: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class AamioError extends Error {
|
|
132
|
+
status: number;
|
|
133
|
+
body: unknown;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface AamioOptions {
|
|
137
|
+
base?: string;
|
|
138
|
+
keys?: Keys | null;
|
|
139
|
+
fetch?: typeof fetch;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class Aamio {
|
|
143
|
+
constructor(options?: AamioOptions);
|
|
144
|
+
readonly base: string;
|
|
145
|
+
keys: Keys | null;
|
|
146
|
+
readonly presence: {
|
|
147
|
+
publish(record: { w: string; tags?: string[]; ttl?: number }): Promise<PresenceRecord>;
|
|
148
|
+
get(key: string): Promise<PresenceRecord | null>;
|
|
149
|
+
lookup(keys: string[], options?: { wait?: number; prefixLength?: number }): Promise<PresenceRecord[]>;
|
|
150
|
+
find(key: string): Promise<string | null>;
|
|
151
|
+
withdraw(): Promise<unknown>;
|
|
152
|
+
};
|
|
153
|
+
open(options?: { ttl?: number; allow?: string[] }): Promise<Thread>;
|
|
154
|
+
send(w: string, body: string | object, options?: { sign?: boolean; encryptTo?: string | null }): Promise<SendResult>;
|
|
155
|
+
read(thread: Thread, options?: { after?: number; wait?: number }): Promise<ReadResult>;
|
|
156
|
+
decode(message: StoredMessage): DecodedMessage;
|
|
157
|
+
listen(thread: Thread, options?: { after?: number; wait?: number; signal?: AbortSignal }): AsyncGenerator<DecodedMessage, void, void>;
|
|
158
|
+
receipt(thread: Thread): Promise<ReceiptResult>;
|
|
159
|
+
close(thread: Thread): Promise<{ w: string; deleted: boolean }>;
|
|
160
|
+
anchor(receiptOrCommitment: ReceiptResult | Receipt | string, options?: { endpoint?: string; idempotencyKey?: string }): Promise<AnchorResult>;
|
|
161
|
+
proof(proofId: string, options?: { base?: string }): Promise<unknown>;
|
|
162
|
+
}
|
package/src/aamio.js
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
// aamio: ephemeral rendezvous for agents. https://aamio.at
|
|
2
|
+
//
|
|
3
|
+
// One file, one dependency. tweetnacl does Ed25519 signatures and the NaCl box
|
|
4
|
+
// used for end-to-end encryption; everything else is here, so the same code
|
|
5
|
+
// runs in Node 20+, Deno, Bun and browsers. The envelope, the signing inputs
|
|
6
|
+
// and the receipt root are byte for byte the ones aamio-listen (Python) uses,
|
|
7
|
+
// so a JavaScript agent and a Python agent can talk encrypted with each other.
|
|
8
|
+
|
|
9
|
+
import nacl from "tweetnacl";
|
|
10
|
+
|
|
11
|
+
export const ENVELOPE = "nacl.box.v1";
|
|
12
|
+
export const DEFAULT_BASE = "https://aamio.at";
|
|
13
|
+
export const VERIFYUM_MCP = "https://api.verifyum.com/mcp";
|
|
14
|
+
export const VERIFYUM_API = "https://api.verifyum.com";
|
|
15
|
+
|
|
16
|
+
// ------------------------------------------------------------------ encoding
|
|
17
|
+
|
|
18
|
+
const textEncoder = new TextEncoder();
|
|
19
|
+
const textDecoder = new TextDecoder();
|
|
20
|
+
|
|
21
|
+
export function utf8(text) {
|
|
22
|
+
return textEncoder.encode(text);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function b64url(bytes) {
|
|
26
|
+
let binary = "";
|
|
27
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
28
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function unb64url(text) {
|
|
32
|
+
const padded = text.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - (text.length % 4)) % 4);
|
|
33
|
+
const binary = atob(padded);
|
|
34
|
+
const out = new Uint8Array(binary.length);
|
|
35
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function hex(bytes) {
|
|
40
|
+
let out = "";
|
|
41
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function unhex(text) {
|
|
46
|
+
const out = new Uint8Array(text.length / 2);
|
|
47
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(text.slice(i * 2, i * 2 + 2), 16);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const BASE32 = "abcdefghijklmnopqrstuvwxyz234567";
|
|
52
|
+
|
|
53
|
+
export function base32(bytes) {
|
|
54
|
+
let bits = 0;
|
|
55
|
+
let value = 0;
|
|
56
|
+
let out = "";
|
|
57
|
+
for (const b of bytes) {
|
|
58
|
+
value = ((value << 8) | b) & 0xffff;
|
|
59
|
+
bits += 8;
|
|
60
|
+
while (bits >= 5) {
|
|
61
|
+
out += BASE32[(value >>> (bits - 5)) & 31];
|
|
62
|
+
bits -= 5;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (bits > 0) out += BASE32[(value << (5 - bits)) & 31];
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ------------------------------------------------------------------- sha256
|
|
70
|
+
// Synchronous, so addresses, hashes and signing inputs need no await.
|
|
71
|
+
|
|
72
|
+
const K = [
|
|
73
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
74
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
75
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
76
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
77
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
78
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
79
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
80
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
84
|
+
|
|
85
|
+
export function sha256(data) {
|
|
86
|
+
const msg = typeof data === "string" ? utf8(data) : data;
|
|
87
|
+
const length = msg.length;
|
|
88
|
+
const padded = new Uint8Array(((length + 9 + 63) >> 6) << 6);
|
|
89
|
+
padded.set(msg);
|
|
90
|
+
padded[length] = 0x80;
|
|
91
|
+
const view = new DataView(padded.buffer);
|
|
92
|
+
const bitLength = length * 8;
|
|
93
|
+
view.setUint32(padded.length - 8, Math.floor(bitLength / 0x100000000));
|
|
94
|
+
view.setUint32(padded.length - 4, bitLength >>> 0);
|
|
95
|
+
let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a;
|
|
96
|
+
let h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;
|
|
97
|
+
const w = new Uint32Array(64);
|
|
98
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
99
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
|
|
100
|
+
for (let i = 16; i < 64; i++) {
|
|
101
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
102
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
103
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
104
|
+
}
|
|
105
|
+
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
|
|
106
|
+
for (let i = 0; i < 64; i++) {
|
|
107
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
108
|
+
const ch = (e & f) ^ (~e & g);
|
|
109
|
+
const t1 = (h + S1 + ch + K[i] + w[i]) >>> 0;
|
|
110
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
111
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
112
|
+
const t2 = (S0 + maj) >>> 0;
|
|
113
|
+
h = g; g = f; f = e; e = (d + t1) >>> 0;
|
|
114
|
+
d = c; c = b; b = a; a = (t1 + t2) >>> 0;
|
|
115
|
+
}
|
|
116
|
+
h0 = (h0 + a) >>> 0; h1 = (h1 + b) >>> 0; h2 = (h2 + c) >>> 0; h3 = (h3 + d) >>> 0;
|
|
117
|
+
h4 = (h4 + e) >>> 0; h5 = (h5 + f) >>> 0; h6 = (h6 + g) >>> 0; h7 = (h7 + h) >>> 0;
|
|
118
|
+
}
|
|
119
|
+
const out = new Uint8Array(32);
|
|
120
|
+
const outView = new DataView(out.buffer);
|
|
121
|
+
[h0, h1, h2, h3, h4, h5, h6, h7].forEach((v, i) => outView.setUint32(i * 4, v));
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function sha256hex(data) {
|
|
126
|
+
return hex(sha256(data));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ------------------------------------------------------------- keys, address
|
|
130
|
+
|
|
131
|
+
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
132
|
+
|
|
133
|
+
/** A new read key: 26 characters of [a-z0-9]. Keep it; never put it in a URL. */
|
|
134
|
+
export function newId() {
|
|
135
|
+
let out = "";
|
|
136
|
+
while (out.length < 26) {
|
|
137
|
+
for (const b of nacl.randomBytes(32)) {
|
|
138
|
+
if (b < 252 && out.length < 26) out += ID_ALPHABET[b % 36];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The public write address of a read key: first 20 characters of base32(sha256(id)). */
|
|
145
|
+
export function deriveAddress(id) {
|
|
146
|
+
return base32(sha256(id)).slice(0, 20);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function isKey(text) {
|
|
150
|
+
if (typeof text !== "string" || text.length !== 43) return false;
|
|
151
|
+
try {
|
|
152
|
+
return unb64url(text).length === 32;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function keyHash(key) {
|
|
159
|
+
return sha256hex(unb64url(key));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The hex prefix of sha256(key) that presence lookup takes. */
|
|
163
|
+
export function hashPrefix(key, length = 8) {
|
|
164
|
+
return keyHash(key).slice(0, length);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const threadSigningInput = (w, bodyText) => "aamio-v1\n" + w + "\n" + sha256hex(bodyText);
|
|
168
|
+
export const presenceSigningInput = (key, bodyText) => "aamio-presence-v1\n" + key + "\n" + sha256hex(bodyText);
|
|
169
|
+
export const presenceDeleteSigningInput = (key, bodyText) => "aamio-presence-delete-v1\n" + key + "\n" + sha256hex(bodyText);
|
|
170
|
+
|
|
171
|
+
export function verify(key, text, signature) {
|
|
172
|
+
try {
|
|
173
|
+
return nacl.sign.detached.verify(utf8(text), unb64url(signature), unb64url(key));
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The root of a receipt, recomputed from its lines. Compare with receipt.root. */
|
|
180
|
+
export function receiptRoot(receipt) {
|
|
181
|
+
let lines = "";
|
|
182
|
+
for (const m of receipt.messages || []) lines += `${m.seq}\t${m.at}\t${m.sha256}\t${m.from ?? "-"}\n`;
|
|
183
|
+
return sha256hex(lines);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Ed25519 public key to X25519 public key: u = (1 + y) / (1 - y) mod p.
|
|
187
|
+
// The same conversion PyNaCl makes in to_curve25519_public_key.
|
|
188
|
+
const P = 2n ** 255n - 19n;
|
|
189
|
+
|
|
190
|
+
function modpow(base, exponent, modulus) {
|
|
191
|
+
let result = 1n;
|
|
192
|
+
base %= modulus;
|
|
193
|
+
while (exponent > 0n) {
|
|
194
|
+
if (exponent & 1n) result = (result * base) % modulus;
|
|
195
|
+
base = (base * base) % modulus;
|
|
196
|
+
exponent >>= 1n;
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function curvePublic(key) {
|
|
202
|
+
const pk = typeof key === "string" ? unb64url(key) : key;
|
|
203
|
+
if (pk.length !== 32) throw new TypeError("public key must be 32 bytes");
|
|
204
|
+
const bytes = Uint8Array.from(pk);
|
|
205
|
+
bytes[31] &= 0x7f;
|
|
206
|
+
let y = 0n;
|
|
207
|
+
for (let i = 31; i >= 0; i--) y = (y << 8n) | BigInt(bytes[i]);
|
|
208
|
+
if (y >= P) throw new RangeError("not a valid Ed25519 public key");
|
|
209
|
+
const u = ((1n + y) * modpow((1n - y + P) % P, P - 2n, P)) % P;
|
|
210
|
+
const out = new Uint8Array(32);
|
|
211
|
+
let v = u;
|
|
212
|
+
for (let i = 0; i < 32; i++) {
|
|
213
|
+
out[i] = Number(v & 0xffn);
|
|
214
|
+
v >>= 8n;
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function isEnvelope(text) {
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(text);
|
|
222
|
+
return Boolean(parsed) && typeof parsed === "object" && parsed.e2ee === ENVELOPE;
|
|
223
|
+
} catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** One identity: a 32 byte seed, an Ed25519 pair for signing, an X25519 pair for boxes. */
|
|
229
|
+
export class Keys {
|
|
230
|
+
constructor(seed) {
|
|
231
|
+
if (!(seed instanceof Uint8Array) || seed.length !== 32) throw new TypeError("seed must be 32 bytes");
|
|
232
|
+
this.seed = seed;
|
|
233
|
+
const pair = nacl.sign.keyPair.fromSeed(seed);
|
|
234
|
+
this.signSecret = pair.secretKey;
|
|
235
|
+
this.publicRaw = pair.publicKey;
|
|
236
|
+
this.public = b64url(this.publicRaw);
|
|
237
|
+
this.hash = sha256hex(this.publicRaw);
|
|
238
|
+
const curve = nacl.hash(seed).slice(0, 32);
|
|
239
|
+
curve[0] &= 248;
|
|
240
|
+
curve[31] &= 127;
|
|
241
|
+
curve[31] |= 64;
|
|
242
|
+
this.curveSecret = curve;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
static generate() {
|
|
246
|
+
return new Keys(nacl.randomBytes(32));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
static fromSeedHex(text) {
|
|
250
|
+
return new Keys(unhex(text));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** base64url Ed25519 signature over the UTF-8 bytes of text. */
|
|
254
|
+
sign(text) {
|
|
255
|
+
return b64url(nacl.sign.detached(utf8(text), this.signSecret));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Encrypt to a partner's Ed25519 key. Returns the envelope as JSON text. */
|
|
259
|
+
seal(recipientKey, plaintext) {
|
|
260
|
+
const nonce = nacl.randomBytes(nacl.box.nonceLength);
|
|
261
|
+
const message = typeof plaintext === "string" ? utf8(plaintext) : plaintext;
|
|
262
|
+
const ct = nacl.box(message, nonce, curvePublic(recipientKey), this.curveSecret);
|
|
263
|
+
return JSON.stringify({ e2ee: ENVELOPE, to: hashPrefix(recipientKey), nonce: b64url(nonce), ct: b64url(ct) });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Open an envelope from a partner. Returns the plaintext bytes. */
|
|
267
|
+
open(senderKey, envelopeText) {
|
|
268
|
+
const envelope = JSON.parse(envelopeText);
|
|
269
|
+
if (!envelope || envelope.e2ee !== ENVELOPE) throw new Error("not an envelope");
|
|
270
|
+
const plain = nacl.box.open(unb64url(envelope.ct), unb64url(envelope.nonce), curvePublic(senderKey), this.curveSecret);
|
|
271
|
+
if (!plain) throw new Error("envelope does not open with these keys");
|
|
272
|
+
return plain;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ------------------------------------------------------------------- client
|
|
277
|
+
|
|
278
|
+
export class AamioError extends Error {
|
|
279
|
+
constructor(status, body, message) {
|
|
280
|
+
super(message || `aamio answered ${status}`);
|
|
281
|
+
this.name = "AamioError";
|
|
282
|
+
this.status = status;
|
|
283
|
+
this.body = body;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
class Presence {
|
|
288
|
+
constructor(client) {
|
|
289
|
+
this.client = client;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Publish where you can be reached, signed, for ttl seconds (5 to 120). */
|
|
293
|
+
async publish({ w, tags = [], ttl = 60 }) {
|
|
294
|
+
const keys = this.client.needKeys("presence");
|
|
295
|
+
const body = JSON.stringify({ w, tags, ttl });
|
|
296
|
+
return this.client.request("PUT", "/p/" + keys.public, { body, headers: { "X-Sig": keys.sign(presenceSigningInput(keys.public, body)) } });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** The live record for one key, or null. */
|
|
300
|
+
async get(key) {
|
|
301
|
+
const data = await this.client.request("GET", "/p/" + key, { expect: [200, 404] });
|
|
302
|
+
return data && data.w ? data : null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Which of these keys are live now. With wait > 0 the call returns as soon as one appears. */
|
|
306
|
+
async lookup(keys, { wait = 0, prefixLength = 8 } = {}) {
|
|
307
|
+
const prefixes = keys.map((k) => hashPrefix(k, prefixLength));
|
|
308
|
+
const path = wait > 0 ? "/p/watch" : "/p/lookup";
|
|
309
|
+
const body = wait > 0 ? { prefixes, wait } : { prefixes };
|
|
310
|
+
const data = await this.client.request("POST", path, { body: JSON.stringify(body) });
|
|
311
|
+
return data.matches || [];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The address one partner is at right now, or null. */
|
|
315
|
+
async find(key) {
|
|
316
|
+
const matches = await this.lookup([key]);
|
|
317
|
+
const match = matches.find((m) => m.key === key);
|
|
318
|
+
return match ? match.w : null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Withdraw your own record now instead of letting it expire. */
|
|
322
|
+
async withdraw() {
|
|
323
|
+
const keys = this.client.needKeys("presence");
|
|
324
|
+
const body = JSON.stringify({ at: Math.floor(Date.now() / 1000) });
|
|
325
|
+
return this.client.request("DELETE", "/p/" + keys.public, { body, headers: { "X-Sig": keys.sign(presenceDeleteSigningInput(keys.public, body)) } });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export class Aamio {
|
|
330
|
+
constructor({ base = DEFAULT_BASE, keys = null, fetch = globalThis.fetch } = {}) {
|
|
331
|
+
this.base = base.replace(/\/+$/, "");
|
|
332
|
+
this.keys = keys;
|
|
333
|
+
this.fetch = fetch;
|
|
334
|
+
this.presence = new Presence(this);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
needKeys(what) {
|
|
338
|
+
if (!this.keys) throw new Error(what + " needs keys: new Aamio({ keys: Keys.generate() })");
|
|
339
|
+
return this.keys;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async request(method, path, { body, headers = {}, expect = [200, 201] } = {}) {
|
|
343
|
+
const init = { method, headers: { Accept: "application/json", ...headers } };
|
|
344
|
+
if (body !== undefined) {
|
|
345
|
+
init.body = body;
|
|
346
|
+
if (!init.headers["Content-Type"]) init.headers["Content-Type"] = "application/json";
|
|
347
|
+
}
|
|
348
|
+
const response = await this.fetch(this.base + path, init);
|
|
349
|
+
const text = await response.text();
|
|
350
|
+
let data;
|
|
351
|
+
try {
|
|
352
|
+
data = text ? JSON.parse(text) : null;
|
|
353
|
+
} catch {
|
|
354
|
+
data = text;
|
|
355
|
+
}
|
|
356
|
+
if (!expect.includes(response.status)) {
|
|
357
|
+
throw new AamioError(response.status, data, (data && data.error) || `aamio answered ${response.status}`);
|
|
358
|
+
}
|
|
359
|
+
return data;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Open a thread. The read key is made here and never sent anywhere but the
|
|
364
|
+
* X-Read header. ttl in seconds (30 to 3600, default 600), allow a list of
|
|
365
|
+
* signer keys that alone may write.
|
|
366
|
+
*/
|
|
367
|
+
async open({ ttl, allow } = {}) {
|
|
368
|
+
const id = newId();
|
|
369
|
+
const w = deriveAddress(id);
|
|
370
|
+
const headers = { "X-Read": id };
|
|
371
|
+
if (ttl) headers["X-TTL"] = String(ttl);
|
|
372
|
+
if (allow && allow.length) headers["X-Allow"] = allow.join(",");
|
|
373
|
+
const data = await this.request("PUT", "/" + w, { headers });
|
|
374
|
+
return { id, w, expireAt: data.expire_at, allow: data.allow || [] };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Write to an address. body is text or a JSON value. Signed with your key by
|
|
379
|
+
* default when you have one. With encryptTo, the body is sealed to that
|
|
380
|
+
* partner's key first and aamio sees only the envelope.
|
|
381
|
+
*/
|
|
382
|
+
async send(w, body, { sign = Boolean(this.keys), encryptTo = null } = {}) {
|
|
383
|
+
let text = typeof body === "string" ? body : JSON.stringify(body);
|
|
384
|
+
let contentType = typeof body === "string" ? "text/plain; charset=utf-8" : "application/json";
|
|
385
|
+
if (encryptTo) {
|
|
386
|
+
text = this.needKeys("encryption").seal(encryptTo, text);
|
|
387
|
+
contentType = "application/json";
|
|
388
|
+
}
|
|
389
|
+
const headers = { "Content-Type": contentType };
|
|
390
|
+
if (sign) {
|
|
391
|
+
const keys = this.needKeys("signing");
|
|
392
|
+
headers["X-Key"] = keys.public;
|
|
393
|
+
headers["X-Sig"] = keys.sign(threadSigningInput(w, text));
|
|
394
|
+
}
|
|
395
|
+
return this.request("POST", "/" + w, { body: text, headers });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Read a thread you own. after: return messages with seq above it. wait: seconds, up to 25. */
|
|
399
|
+
async read(thread, { after = 0, wait = 0 } = {}) {
|
|
400
|
+
let path = "/" + thread.w;
|
|
401
|
+
if (after > 0 || wait > 0) path += "/after/" + after;
|
|
402
|
+
if (wait > 0) path += "/wait/" + wait;
|
|
403
|
+
const data = await this.request("GET", path, { headers: { "X-Read": thread.id } });
|
|
404
|
+
data.messages = (data.messages || []).map((m) => this.decode(m));
|
|
405
|
+
return data;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* One stored message, decoded: plain is the text (decrypted when it was an
|
|
410
|
+
* envelope to you), json the parsed value when the text is JSON, encrypted
|
|
411
|
+
* whether it came sealed, error when it could not be opened.
|
|
412
|
+
*/
|
|
413
|
+
decode(message) {
|
|
414
|
+
const out = { ...message, encrypted: false, plain: message.body, json: undefined, error: undefined };
|
|
415
|
+
if (isEnvelope(message.body)) {
|
|
416
|
+
out.encrypted = true;
|
|
417
|
+
out.plain = null;
|
|
418
|
+
if (!this.keys) out.error = "encrypted message and this client has no keys";
|
|
419
|
+
else if (!message.from || !message.verified) out.error = "encrypted message without a verified sender";
|
|
420
|
+
else {
|
|
421
|
+
try {
|
|
422
|
+
out.plain = textDecoder.decode(this.keys.open(message.from, message.body));
|
|
423
|
+
} catch {
|
|
424
|
+
out.error = "envelope does not open with these keys";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (typeof out.plain === "string") {
|
|
429
|
+
try {
|
|
430
|
+
out.json = JSON.parse(out.plain);
|
|
431
|
+
} catch {
|
|
432
|
+
out.json = undefined;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Yield messages as they arrive, long polling with wait seconds per call,
|
|
440
|
+
* until signal aborts or the thread answers 410 (then AamioError is thrown).
|
|
441
|
+
*/
|
|
442
|
+
async *listen(thread, { after = 0, wait = 25, signal } = {}) {
|
|
443
|
+
let seq = after;
|
|
444
|
+
while (!(signal && signal.aborted)) {
|
|
445
|
+
const data = await this.read(thread, { after: seq, wait });
|
|
446
|
+
for (const message of data.messages) {
|
|
447
|
+
if (message.seq > seq) seq = message.seq;
|
|
448
|
+
yield message;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** The receipt of a thread you own, with its root recomputed locally. */
|
|
454
|
+
async receipt(thread) {
|
|
455
|
+
const receipt = await this.request("GET", "/" + thread.w + "/receipt", { headers: { "X-Read": thread.id } });
|
|
456
|
+
const root = receiptRoot(receipt);
|
|
457
|
+
return { receipt, root, matches: root === receipt.root, commitment: receipt.commitment };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Close a thread you own now instead of waiting for its expiry. */
|
|
461
|
+
async close(thread) {
|
|
462
|
+
return this.request("DELETE", "/" + thread.w, { headers: { "X-Read": thread.id } });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Anchor a receipt's commitment on Solana through Verifyum, no account
|
|
467
|
+
* needed. Pass the object receipt() returned, or a commitment string
|
|
468
|
+
* "sha256:<root>". Returns the proof id and its public page.
|
|
469
|
+
*/
|
|
470
|
+
async anchor(receiptOrCommitment, { endpoint = VERIFYUM_MCP, idempotencyKey } = {}) {
|
|
471
|
+
const commitment = typeof receiptOrCommitment === "string" ? receiptOrCommitment : receiptOrCommitment.commitment || "sha256:" + receiptOrCommitment.root;
|
|
472
|
+
const idem = idempotencyKey || sha256hex("aamio-js:" + commitment).slice(0, 32);
|
|
473
|
+
const response = await this.fetch(endpoint, {
|
|
474
|
+
method: "POST",
|
|
475
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", "MCP-Protocol-Version": "2025-11-25" },
|
|
476
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "verifyum_anchor_commitment", arguments: { commitment, idempotency_key: idem } } }),
|
|
477
|
+
});
|
|
478
|
+
const rpc = await response.json();
|
|
479
|
+
let result = {};
|
|
480
|
+
try {
|
|
481
|
+
result = JSON.parse(rpc?.result?.content?.[0]?.text ?? "{}");
|
|
482
|
+
} catch {
|
|
483
|
+
result = {};
|
|
484
|
+
}
|
|
485
|
+
if (rpc?.result?.isError || !result.proof_id) throw new AamioError(response.status, rpc, "anchor failed");
|
|
486
|
+
return { proofId: result.proof_id, proofUrl: result.proof_url, status: result.status, commitment };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** The current state of a Verifyum proof: status, network, transaction signature. */
|
|
490
|
+
async proof(proofId, { base = VERIFYUM_API } = {}) {
|
|
491
|
+
const response = await this.fetch(base + "/v2/proofs/" + proofId, { headers: { Accept: "application/json" } });
|
|
492
|
+
return response.json();
|
|
493
|
+
}
|
|
494
|
+
}
|