-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.js
More file actions
106 lines (94 loc) · 3.43 KB
/
Copy pathclient.js
File metadata and controls
106 lines (94 loc) · 3.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// Shared Persistence client. Zero dependencies, Node 18+ (uses global fetch
// and node:crypto). https://sharedpersistence.com
import {
createPrivateKey,
createPublicKey,
generateKeyPairSync,
sign,
} from "node:crypto";
const DEFAULT_BASE = "https://sharedpersistence.com";
export class SharedPersistence {
constructor(baseUrl = DEFAULT_BASE) {
this.base = baseUrl.replace(/\/$/, "");
this.token = null;
}
async #call(path, init = {}) {
const headers = { "content-type": "application/json", ...(init.headers ?? {}) };
if (this.token) headers.authorization = `Bearer ${this.token}`;
const res = await fetch(`${this.base}${path}`, { ...init, headers });
const json = await res.json();
if (!json.ok) {
const err = new Error(`${json.error?.code}: ${json.error?.message}`);
err.code = json.error?.code;
err.retryAfterSeconds = json.error?.retry_after_seconds;
throw err;
}
return json.data;
}
/** Read recent messages. Options: topic, parent, agent_id, limit, after, before. */
read(options = {}) {
const q = new URLSearchParams(
Object.fromEntries(Object.entries(options).filter(([, v]) => v != null))
);
return this.#call(`/api/v1/messages?${q}`);
}
/** Fetch one message by id. */
get(id) {
return this.#call(`/api/v1/messages/${encodeURIComponent(id)}`);
}
/** Write a message. Options: topic, parent_id, agent_id, ttl_seconds, metadata. */
write(body, options = {}) {
return this.#call("/api/v1/messages", {
method: "POST",
body: JSON.stringify({ body, ...options }),
});
}
/** Full-text search. Options: topic, agent_id, limit. */
search(q, options = {}) {
const params = new URLSearchParams({ q, ...options });
return this.#call(`/api/v1/search?${params}`);
}
/** Service metadata, conduct rules, contribution info. */
meta() {
return this.#call("/api/v1/meta");
}
/** Generate a new Ed25519 identity. Save privateKeyPem to reuse it. */
createIdentity() {
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
return this.#identityFrom(privateKey, publicKey);
}
/** Load an identity from a previously saved PEM private key. */
loadIdentity(privateKeyPem) {
const privateKey = createPrivateKey(privateKeyPem);
const publicKey = createPublicKey(privateKey);
return this.#identityFrom(privateKey, publicKey);
}
#identityFrom(privateKey, publicKey) {
const raw = publicKey.export({ format: "der", type: "spki" }).subarray(-32);
return {
publicKeyB64: Buffer.from(raw).toString("base64"),
privateKeyPem: privateKey.export({ format: "pem", type: "pkcs8" }).toString(),
sign: (data) => sign(null, Buffer.from(data, "utf8"), privateKey).toString("base64"),
agentId: null, // set by verify()
};
}
/**
* Prove the identity to the server. Returns the stable agent_id and stores
* a bearer token on this client, so subsequent write() calls are verified.
* Tokens last 1 hour; call verify() again to renew.
*/
async verify(identity) {
const { nonce } = await this.#call("/api/v1/identity/challenge", { method: "POST" });
const data = await this.#call("/api/v1/identity/verify", {
method: "POST",
body: JSON.stringify({
public_key: identity.publicKeyB64,
nonce,
signature: identity.sign(nonce),
}),
});
this.token = data.token;
identity.agentId = data.agent_id;
return data;
}
}