@orbitmem/sdk 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/README.md +104 -0
- package/dist/agent/agent-adapter.d.ts +3 -0
- package/dist/agent/agent-adapter.d.ts.map +1 -0
- package/dist/agent/agent-adapter.js +3 -0
- package/dist/agent/agent-adapter.js.map +1 -0
- package/dist/agent/client.d.ts +5 -0
- package/dist/agent/client.d.ts.map +1 -0
- package/dist/agent/client.js +146 -0
- package/dist/agent/client.js.map +1 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +118 -0
- package/dist/client.js.map +1 -0
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +28 -0
- package/dist/contracts.js.map +1 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +5 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/orbitdb.d.ts +10 -0
- package/dist/data/orbitdb.d.ts.map +1 -0
- package/dist/data/orbitdb.js +39 -0
- package/dist/data/orbitdb.js.map +1 -0
- package/dist/data/pricing.d.ts +7 -0
- package/dist/data/pricing.d.ts.map +1 -0
- package/dist/data/pricing.js +55 -0
- package/dist/data/pricing.js.map +1 -0
- package/dist/data/serialization.d.ts +28 -0
- package/dist/data/serialization.d.ts.map +1 -0
- package/dist/data/serialization.js +76 -0
- package/dist/data/serialization.js.map +1 -0
- package/dist/data/vault.d.ts +21 -0
- package/dist/data/vault.d.ts.map +1 -0
- package/dist/data/vault.js +284 -0
- package/dist/data/vault.js.map +1 -0
- package/dist/discovery/discovery-layer.d.ts +3 -0
- package/dist/discovery/discovery-layer.d.ts.map +1 -0
- package/dist/discovery/discovery-layer.js +205 -0
- package/dist/discovery/discovery-layer.js.map +1 -0
- package/dist/discovery/index.d.ts +4 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +4 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/mock-registry.d.ts +30 -0
- package/dist/discovery/mock-registry.d.ts.map +1 -0
- package/dist/discovery/mock-registry.js +71 -0
- package/dist/discovery/mock-registry.js.map +1 -0
- package/dist/discovery/on-chain-registry.d.ts +35 -0
- package/dist/discovery/on-chain-registry.d.ts.map +1 -0
- package/dist/discovery/on-chain-registry.js +199 -0
- package/dist/discovery/on-chain-registry.js.map +1 -0
- package/dist/encryption/aes.d.ts +15 -0
- package/dist/encryption/aes.d.ts.map +1 -0
- package/dist/encryption/aes.js +63 -0
- package/dist/encryption/aes.js.map +1 -0
- package/dist/encryption/encryption-layer.d.ts +8 -0
- package/dist/encryption/encryption-layer.d.ts.map +1 -0
- package/dist/encryption/encryption-layer.js +82 -0
- package/dist/encryption/encryption-layer.js.map +1 -0
- package/dist/encryption/index.d.ts +6 -0
- package/dist/encryption/index.d.ts.map +1 -0
- package/dist/encryption/index.js +4 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/encryption/lit.d.ts +23 -0
- package/dist/encryption/lit.d.ts.map +1 -0
- package/dist/encryption/lit.js +113 -0
- package/dist/encryption/lit.js.map +1 -0
- package/dist/encryption/vault-key.d.ts +37 -0
- package/dist/encryption/vault-key.d.ts.map +1 -0
- package/dist/encryption/vault-key.js +43 -0
- package/dist/encryption/vault-key.js.map +1 -0
- package/dist/identity/identity-layer.d.ts +3 -0
- package/dist/identity/identity-layer.d.ts.map +1 -0
- package/dist/identity/identity-layer.js +99 -0
- package/dist/identity/identity-layer.js.map +1 -0
- package/dist/identity/index.d.ts +4 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +4 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/identity/ows-adapter.d.ts +15 -0
- package/dist/identity/ows-adapter.d.ts.map +1 -0
- package/dist/identity/ows-adapter.js +67 -0
- package/dist/identity/ows-adapter.js.map +1 -0
- package/dist/identity/session.d.ts +10 -0
- package/dist/identity/session.d.ts.map +1 -0
- package/dist/identity/session.js +36 -0
- package/dist/identity/session.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence/create-agent.d.ts +11 -0
- package/dist/persistence/create-agent.d.ts.map +1 -0
- package/dist/persistence/create-agent.js +47 -0
- package/dist/persistence/create-agent.js.map +1 -0
- package/dist/persistence/index.d.ts +3 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +3 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/persistence-layer.d.ts +12 -0
- package/dist/persistence/persistence-layer.d.ts.map +1 -0
- package/dist/persistence/persistence-layer.js +194 -0
- package/dist/persistence/persistence-layer.js.map +1 -0
- package/dist/transport/index.d.ts +3 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +3 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/relay-session.d.ts +41 -0
- package/dist/transport/relay-session.d.ts.map +1 -0
- package/dist/transport/relay-session.js +86 -0
- package/dist/transport/relay-session.js.map +1 -0
- package/dist/transport/transport-layer.d.ts +32 -0
- package/dist/transport/transport-layer.d.ts.map +1 -0
- package/dist/transport/transport-layer.js +110 -0
- package/dist/transport/transport-layer.js.map +1 -0
- package/dist/types.d.ts +1319 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +91 -0
- package/src/__tests__/client.test.ts +30 -0
- package/src/__tests__/orbitdb-availability.ts +8 -0
- package/src/agent/__tests__/agent-adapter.test.ts +50 -0
- package/src/agent/__tests__/client.test.ts +50 -0
- package/src/agent/agent-adapter.ts +2 -0
- package/src/agent/client.ts +158 -0
- package/src/agent/index.ts +1 -0
- package/src/client.ts +134 -0
- package/src/contracts.ts +44 -0
- package/src/data/__tests__/pricing.test.ts +73 -0
- package/src/data/__tests__/vault-encryption.test.ts +346 -0
- package/src/data/__tests__/vault.test.ts +75 -0
- package/src/data/index.ts +8 -0
- package/src/data/orbitdb.ts +47 -0
- package/src/data/pricing.ts +63 -0
- package/src/data/serialization.ts +108 -0
- package/src/data/vault.ts +382 -0
- package/src/discovery/__tests__/discovery.test.ts +49 -0
- package/src/discovery/__tests__/on-chain-registry.test.ts +176 -0
- package/src/discovery/discovery-layer.ts +244 -0
- package/src/discovery/index.ts +3 -0
- package/src/discovery/mock-registry.ts +96 -0
- package/src/discovery/on-chain-registry.ts +237 -0
- package/src/encryption/__tests__/aes.test.ts +64 -0
- package/src/encryption/__tests__/encryption-layer.test.ts +80 -0
- package/src/encryption/__tests__/lit.test.ts +97 -0
- package/src/encryption/aes.ts +109 -0
- package/src/encryption/encryption-layer.ts +100 -0
- package/src/encryption/index.ts +5 -0
- package/src/encryption/lit.ts +161 -0
- package/src/encryption/vault-key.ts +63 -0
- package/src/identity/__tests__/identity.test.ts +31 -0
- package/src/identity/__tests__/ows-adapter.test.ts +47 -0
- package/src/identity/identity-layer.ts +123 -0
- package/src/identity/index.ts +3 -0
- package/src/identity/ows-adapter.ts +80 -0
- package/src/identity/session.ts +57 -0
- package/src/index.ts +12 -0
- package/src/persistence/__tests__/create-agent.test.ts +9 -0
- package/src/persistence/__tests__/persistence.test.ts +242 -0
- package/src/persistence/create-agent.ts +55 -0
- package/src/persistence/index.ts +2 -0
- package/src/persistence/persistence-layer.ts +236 -0
- package/src/transport/__tests__/solana-transport.test.ts +112 -0
- package/src/transport/__tests__/transport.test.ts +84 -0
- package/src/transport/index.ts +2 -0
- package/src/transport/relay-session.ts +118 -0
- package/src/transport/transport-layer.ts +171 -0
- package/src/types/orbitdb.d.ts +9 -0
- package/src/types.ts +1496 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { IPersistenceLayer, Snapshot, WalletAddress } from "../types.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_GATEWAY = "https://w3s.link";
|
|
4
|
+
|
|
5
|
+
interface PersistenceConfig {
|
|
6
|
+
mock?: boolean;
|
|
7
|
+
relayUrl?: string;
|
|
8
|
+
proof?: string;
|
|
9
|
+
gatewayUrl?: string;
|
|
10
|
+
author?: WalletAddress;
|
|
11
|
+
signer?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ArchiveOptions {
|
|
15
|
+
data?: Uint8Array;
|
|
16
|
+
entryCount?: number;
|
|
17
|
+
label?: string;
|
|
18
|
+
pinToFilecoin?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function generateCID(): string {
|
|
22
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
23
|
+
return (
|
|
24
|
+
"bafy" +
|
|
25
|
+
Array.from(bytes)
|
|
26
|
+
.map((b) => b.toString(36))
|
|
27
|
+
.join("")
|
|
28
|
+
.slice(0, 55)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Mock Persistence ─────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function createMockPersistence(config: PersistenceConfig): IPersistenceLayer {
|
|
35
|
+
const store = new Map<string, { data: Uint8Array; snapshot: Snapshot }>();
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
async archive(opts: ArchiveOptions = {}) {
|
|
39
|
+
const data = opts.data ?? new Uint8Array(0);
|
|
40
|
+
const cid = generateCID();
|
|
41
|
+
const snapshot: Snapshot = {
|
|
42
|
+
cid,
|
|
43
|
+
size: data.length,
|
|
44
|
+
archivedAt: Date.now(),
|
|
45
|
+
author: config.author ?? ("0x0" as WalletAddress),
|
|
46
|
+
entryCount: opts.entryCount ?? 0,
|
|
47
|
+
encrypted: true,
|
|
48
|
+
filecoinStatus: "pending",
|
|
49
|
+
};
|
|
50
|
+
store.set(cid, { data, snapshot });
|
|
51
|
+
return snapshot;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async retrieve(cid) {
|
|
55
|
+
const entry = store.get(cid);
|
|
56
|
+
if (!entry) throw new Error(`Snapshot not found: ${cid}`);
|
|
57
|
+
return entry.data;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async restore(cid) {
|
|
61
|
+
const entry = store.get(cid);
|
|
62
|
+
if (!entry) throw new Error(`Snapshot not found: ${cid}`);
|
|
63
|
+
return { merged: entry.snapshot.entryCount, conflicts: 0 };
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async listSnapshots(opts) {
|
|
67
|
+
const all = Array.from(store.values()).map((e) => e.snapshot);
|
|
68
|
+
const offset = opts?.offset ?? 0;
|
|
69
|
+
const limit = opts?.limit ?? all.length;
|
|
70
|
+
return all.slice(offset, offset + limit);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async deleteSnapshot(cid) {
|
|
74
|
+
store.delete(cid);
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
async getDealStatus(cid) {
|
|
78
|
+
const entry = store.get(cid);
|
|
79
|
+
if (!entry) throw new Error(`Snapshot not found: ${cid}`);
|
|
80
|
+
return { status: entry.snapshot.filecoinStatus };
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Managed Persistence (relay-backed) ───────────────────────
|
|
86
|
+
|
|
87
|
+
function createManagedPersistence(config: PersistenceConfig): IPersistenceLayer {
|
|
88
|
+
const relayUrl = config.relayUrl!;
|
|
89
|
+
const gateway = config.gatewayUrl ?? DEFAULT_GATEWAY;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
async archive(opts: ArchiveOptions = {}) {
|
|
93
|
+
const data = opts.data ?? new Uint8Array(0);
|
|
94
|
+
const body = JSON.stringify({
|
|
95
|
+
data: new TextDecoder().decode(data),
|
|
96
|
+
entryCount: opts.entryCount ?? 0,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const res = await fetch(`${relayUrl}/v1/snapshots/archive`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
body,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const text = await res.text();
|
|
107
|
+
throw new Error(`Archive failed (${res.status}): ${text}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const snap = await res.json();
|
|
111
|
+
return snap as Snapshot;
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async retrieve(cid) {
|
|
115
|
+
const res = await fetch(`${gateway}/ipfs/${cid}`);
|
|
116
|
+
if (!res.ok) throw new Error(`Failed to retrieve ${cid}: ${res.status}`);
|
|
117
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async restore(cid) {
|
|
121
|
+
const res = await fetch(`${gateway}/ipfs/${cid}`);
|
|
122
|
+
if (!res.ok) throw new Error(`Failed to retrieve ${cid}: ${res.status}`);
|
|
123
|
+
return { merged: 0, conflicts: 0 };
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async listSnapshots(_opts) {
|
|
127
|
+
const res = await fetch(`${relayUrl}/v1/snapshots`);
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
const text = await res.text();
|
|
130
|
+
throw new Error(`List snapshots failed (${res.status}): ${text}`);
|
|
131
|
+
}
|
|
132
|
+
const items = await res.json();
|
|
133
|
+
return items as Snapshot[];
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
async deleteSnapshot(cid) {
|
|
137
|
+
const res = await fetch(`${relayUrl}/v1/snapshots/${cid}`, {
|
|
138
|
+
method: "DELETE",
|
|
139
|
+
});
|
|
140
|
+
if (!res.ok && res.status !== 204) {
|
|
141
|
+
const text = await res.text();
|
|
142
|
+
throw new Error(`Delete failed (${res.status}): ${text}`);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
async getDealStatus(_cid) {
|
|
147
|
+
return { status: "pending" as const };
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Direct Persistence (Storacha UCAN) ───────────────────────
|
|
153
|
+
|
|
154
|
+
function createDirectPersistence(config: PersistenceConfig): IPersistenceLayer {
|
|
155
|
+
const gateway = config.gatewayUrl ?? DEFAULT_GATEWAY;
|
|
156
|
+
let clientPromise: Promise<any> | null = null;
|
|
157
|
+
|
|
158
|
+
async function getClient() {
|
|
159
|
+
if (!clientPromise) {
|
|
160
|
+
clientPromise = (async () => {
|
|
161
|
+
const { create } = await import("@storacha/client");
|
|
162
|
+
const { parse } = await import("@storacha/client/proof");
|
|
163
|
+
const client = await create();
|
|
164
|
+
const proof = await parse(config.proof!);
|
|
165
|
+
const space = await client.addSpace(proof);
|
|
166
|
+
await client.setCurrentSpace(space.did());
|
|
167
|
+
return client;
|
|
168
|
+
})();
|
|
169
|
+
}
|
|
170
|
+
return clientPromise;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
async archive(opts: ArchiveOptions = {}) {
|
|
175
|
+
const client = await getClient();
|
|
176
|
+
const data = opts.data ?? new Uint8Array(0);
|
|
177
|
+
const blob = new Blob([data as BlobPart]);
|
|
178
|
+
const cid = await client.uploadFile(blob);
|
|
179
|
+
|
|
180
|
+
const snapshot: Snapshot = {
|
|
181
|
+
cid: cid.toString(),
|
|
182
|
+
size: data.length,
|
|
183
|
+
archivedAt: Date.now(),
|
|
184
|
+
author: config.author ?? ("0x0" as WalletAddress),
|
|
185
|
+
entryCount: opts.entryCount ?? 0,
|
|
186
|
+
encrypted: true,
|
|
187
|
+
filecoinStatus: "pending",
|
|
188
|
+
};
|
|
189
|
+
return snapshot;
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
async retrieve(cid) {
|
|
193
|
+
const res = await fetch(`${gateway}/ipfs/${cid}`);
|
|
194
|
+
if (!res.ok) throw new Error(`Failed to retrieve ${cid}: ${res.status}`);
|
|
195
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async restore(cid) {
|
|
199
|
+
const res = await fetch(`${gateway}/ipfs/${cid}`);
|
|
200
|
+
if (!res.ok) throw new Error(`Failed to retrieve ${cid}: ${res.status}`);
|
|
201
|
+
return { merged: 0, conflicts: 0 };
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
async listSnapshots(_opts) {
|
|
205
|
+
const client = await getClient();
|
|
206
|
+
const result = await client.capability.upload.list();
|
|
207
|
+
return (result.results ?? []).map((entry: any) => ({
|
|
208
|
+
cid: entry.root.toString(),
|
|
209
|
+
size: entry.size ?? 0,
|
|
210
|
+
archivedAt: entry.insertedAt ? new Date(entry.insertedAt).getTime() : Date.now(),
|
|
211
|
+
author: config.author ?? ("0x0" as WalletAddress),
|
|
212
|
+
entryCount: 0,
|
|
213
|
+
encrypted: true,
|
|
214
|
+
filecoinStatus: "pending" as const,
|
|
215
|
+
}));
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
async deleteSnapshot(cid) {
|
|
219
|
+
const client = await getClient();
|
|
220
|
+
const { CID: CIDClass } = await import("multiformats/cid");
|
|
221
|
+
await client.capability.upload.remove(CIDClass.parse(cid));
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
async getDealStatus(_cid) {
|
|
225
|
+
return { status: "pending" as const };
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Factory (mode detection) ─────────────────────────────────
|
|
231
|
+
|
|
232
|
+
export function createPersistenceLayer(config: PersistenceConfig): IPersistenceLayer {
|
|
233
|
+
if (config.proof) return createDirectPersistence(config);
|
|
234
|
+
if (config.relayUrl) return createManagedPersistence(config);
|
|
235
|
+
return createMockPersistence(config);
|
|
236
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
4
|
+
|
|
5
|
+
import { createTransportLayer } from "../transport-layer.js";
|
|
6
|
+
|
|
7
|
+
describe("TransportLayer — Solana Ed25519", () => {
|
|
8
|
+
const privateKey = ed25519.utils.randomSecretKey();
|
|
9
|
+
const publicKey = ed25519.getPublicKey(privateKey);
|
|
10
|
+
const solanaAddress = "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU";
|
|
11
|
+
|
|
12
|
+
test("createSignedRequest adds Solana headers with ed25519", async () => {
|
|
13
|
+
const transport = createTransportLayer({
|
|
14
|
+
signer: async (payload: Uint8Array) => ({
|
|
15
|
+
signature: ed25519.sign(payload, privateKey),
|
|
16
|
+
algorithm: "ed25519" as const,
|
|
17
|
+
}),
|
|
18
|
+
signerAddress: solanaAddress,
|
|
19
|
+
family: "solana",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const signed = await transport.createSignedRequest({
|
|
23
|
+
url: "https://relay.orbitmem.com/v1/vault/read",
|
|
24
|
+
method: "POST",
|
|
25
|
+
body: { key: "preferences" },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(signed.proof.signer).toBe(solanaAddress);
|
|
29
|
+
expect(signed.proof.family).toBe("solana");
|
|
30
|
+
expect(signed.proof.algorithm).toBe("ed25519");
|
|
31
|
+
expect(signed.proof.signature).toBeInstanceOf(Uint8Array);
|
|
32
|
+
expect(signed.proof.nonce).toBeTruthy();
|
|
33
|
+
expect(signed.proof.timestamp).toBeGreaterThan(0);
|
|
34
|
+
expect(signed.headers["X-OrbitMem-Signer"]).toBe(solanaAddress);
|
|
35
|
+
expect(signed.headers["X-OrbitMem-Family"]).toBe("solana");
|
|
36
|
+
expect(signed.headers["X-OrbitMem-Algorithm"]).toBe("ed25519");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("verifyRequest validates Ed25519 signature round-trip", async () => {
|
|
40
|
+
const transport = createTransportLayer({
|
|
41
|
+
signer: async (payload: Uint8Array) => ({
|
|
42
|
+
signature: ed25519.sign(payload, privateKey),
|
|
43
|
+
algorithm: "ed25519" as const,
|
|
44
|
+
}),
|
|
45
|
+
verifier: async (payload: Uint8Array, signature: Uint8Array) => {
|
|
46
|
+
return ed25519.verify(signature, payload, publicKey);
|
|
47
|
+
},
|
|
48
|
+
signerAddress: solanaAddress,
|
|
49
|
+
family: "solana",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const signed = await transport.createSignedRequest({
|
|
53
|
+
url: "https://relay.orbitmem.com/v1/vault/read",
|
|
54
|
+
method: "POST",
|
|
55
|
+
body: { key: "test" },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const result = await transport.verifyRequest(signed);
|
|
59
|
+
expect(result.valid).toBe(true);
|
|
60
|
+
expect(result.signer).toBe(solanaAddress);
|
|
61
|
+
expect(result.family).toBe("solana");
|
|
62
|
+
expect(result.isReplay).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("rejects tampered Ed25519 signature", async () => {
|
|
66
|
+
const transport = createTransportLayer({
|
|
67
|
+
signer: async (payload: Uint8Array) => ({
|
|
68
|
+
signature: ed25519.sign(payload, privateKey),
|
|
69
|
+
algorithm: "ed25519" as const,
|
|
70
|
+
}),
|
|
71
|
+
verifier: async (payload: Uint8Array, signature: Uint8Array) => {
|
|
72
|
+
return ed25519.verify(signature, payload, publicKey);
|
|
73
|
+
},
|
|
74
|
+
signerAddress: solanaAddress,
|
|
75
|
+
family: "solana",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const signed = await transport.createSignedRequest({
|
|
79
|
+
url: "https://relay.orbitmem.com/v1/vault/read",
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: { key: "test" },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
signed.proof.signature = new Uint8Array(64).fill(0xab);
|
|
85
|
+
|
|
86
|
+
const result = await transport.verifyRequest(signed);
|
|
87
|
+
expect(result.valid).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("replay detection works for Solana signer", async () => {
|
|
91
|
+
const transport = createTransportLayer({
|
|
92
|
+
signer: async (_payload) => ({
|
|
93
|
+
signature: new Uint8Array(64).fill(1),
|
|
94
|
+
algorithm: "ed25519" as const,
|
|
95
|
+
}),
|
|
96
|
+
verifier: async () => true,
|
|
97
|
+
signerAddress: solanaAddress,
|
|
98
|
+
family: "solana",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const signed = await transport.createSignedRequest({
|
|
102
|
+
url: "https://relay.orbitmem.com/v1/health",
|
|
103
|
+
method: "GET",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const first = await transport.verifyRequest(signed);
|
|
107
|
+
expect(first.isReplay).toBe(false);
|
|
108
|
+
|
|
109
|
+
const second = await transport.verifyRequest(signed);
|
|
110
|
+
expect(second.isReplay).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { createTransportLayer } from "../transport-layer.js";
|
|
4
|
+
|
|
5
|
+
describe("TransportLayer", () => {
|
|
6
|
+
test("createSignedRequest adds ERC-8128 headers", async () => {
|
|
7
|
+
const transport = createTransportLayer({
|
|
8
|
+
signer: async (_payload: Uint8Array) => ({
|
|
9
|
+
signature: new Uint8Array(65).fill(0xab),
|
|
10
|
+
algorithm: "ecdsa-secp256k1" as const,
|
|
11
|
+
}),
|
|
12
|
+
signerAddress: "0x1234567890abcdef1234567890abcdef12345678",
|
|
13
|
+
family: "evm",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const signed = await transport.createSignedRequest({
|
|
17
|
+
url: "https://orbitmem-relay.fly.dev/v1/vault/read",
|
|
18
|
+
method: "POST",
|
|
19
|
+
body: { key: "preferences" },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(signed.proof.signer).toBe("0x1234567890abcdef1234567890abcdef12345678");
|
|
23
|
+
expect(signed.proof.family).toBe("evm");
|
|
24
|
+
expect(signed.proof.algorithm).toBe("ecdsa-secp256k1");
|
|
25
|
+
expect(signed.proof.signature).toBeInstanceOf(Uint8Array);
|
|
26
|
+
expect(signed.proof.nonce).toBeTruthy();
|
|
27
|
+
expect(signed.proof.timestamp).toBeGreaterThan(0);
|
|
28
|
+
expect(signed.headers["X-OrbitMem-Signer"]).toBe("0x1234567890abcdef1234567890abcdef12345678");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("verifyRequest validates signature round-trip", async () => {
|
|
32
|
+
const transport = createTransportLayer({
|
|
33
|
+
signer: async (payload: Uint8Array) => {
|
|
34
|
+
// Simple "sign" = hash of payload (for testing)
|
|
35
|
+
const hash = await crypto.subtle.digest("SHA-256", payload as BufferSource);
|
|
36
|
+
return {
|
|
37
|
+
signature: new Uint8Array(hash),
|
|
38
|
+
algorithm: "ecdsa-secp256k1" as const,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
verifier: async (payload: Uint8Array, signature: Uint8Array) => {
|
|
42
|
+
const hash = await crypto.subtle.digest("SHA-256", payload as BufferSource);
|
|
43
|
+
const expected = new Uint8Array(hash);
|
|
44
|
+
return signature.length === expected.length && signature.every((b, i) => b === expected[i]);
|
|
45
|
+
},
|
|
46
|
+
signerAddress: "0xAGENT",
|
|
47
|
+
family: "evm",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const signed = await transport.createSignedRequest({
|
|
51
|
+
url: "https://orbitmem-relay.fly.dev/v1/vault/read",
|
|
52
|
+
method: "POST",
|
|
53
|
+
body: { key: "test" },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await transport.verifyRequest(signed);
|
|
57
|
+
expect(result.valid).toBe(true);
|
|
58
|
+
expect(result.signer).toBe("0xAGENT");
|
|
59
|
+
expect(result.isReplay).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("replay detection rejects seen nonce", async () => {
|
|
63
|
+
const transport = createTransportLayer({
|
|
64
|
+
signer: async (_payload) => ({
|
|
65
|
+
signature: new Uint8Array(32).fill(1),
|
|
66
|
+
algorithm: "ecdsa-secp256k1" as const,
|
|
67
|
+
}),
|
|
68
|
+
verifier: async () => true,
|
|
69
|
+
signerAddress: "0xAGENT",
|
|
70
|
+
family: "evm",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const signed = await transport.createSignedRequest({
|
|
74
|
+
url: "https://orbitmem-relay.fly.dev/v1/health",
|
|
75
|
+
method: "GET",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const first = await transport.verifyRequest(signed);
|
|
79
|
+
expect(first.isReplay).toBe(false);
|
|
80
|
+
|
|
81
|
+
const second = await transport.verifyRequest(signed);
|
|
82
|
+
expect(second.isReplay).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay session transport — acquires a bearer token via ERC-8128 auth
|
|
3
|
+
* and caches it for subsequent requests. Designed for browser use with
|
|
4
|
+
* sessionStorage, but works anywhere with a custom storage adapter.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createSignerClient } from "@slicekit/erc8128";
|
|
8
|
+
import type { EthHttpSigner } from "@slicekit/erc8128";
|
|
9
|
+
|
|
10
|
+
export interface RelaySessionConfig {
|
|
11
|
+
/** Relay server URL (e.g. "https://orbitmem-relay.fly.dev") */
|
|
12
|
+
relayUrl: string;
|
|
13
|
+
/** Wallet address (checksummed) */
|
|
14
|
+
address: `0x${string}`;
|
|
15
|
+
/** Chain ID (default: 84532 = Base Sepolia) */
|
|
16
|
+
chainId?: number;
|
|
17
|
+
/** Wallet signMessage function */
|
|
18
|
+
signMessage: (message: Uint8Array) => Promise<`0x${string}`>;
|
|
19
|
+
/** Session TTL in seconds (default: 1800 = 30 min) */
|
|
20
|
+
ttl?: number;
|
|
21
|
+
/** Storage adapter for caching (default: sessionStorage if available) */
|
|
22
|
+
storage?: {
|
|
23
|
+
getItem(key: string): string | null;
|
|
24
|
+
setItem(key: string, value: string): void;
|
|
25
|
+
removeItem(key: string): void;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CachedSession {
|
|
30
|
+
token: string;
|
|
31
|
+
expiresAt: number;
|
|
32
|
+
address: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const CACHE_KEY = "orbitmem:session";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a relay session transport that handles session token
|
|
39
|
+
* acquisition, caching, and authenticated fetch.
|
|
40
|
+
*
|
|
41
|
+
* Usage:
|
|
42
|
+
* ```ts
|
|
43
|
+
* const relay = createRelaySession({ relayUrl, address, signMessage });
|
|
44
|
+
* const res = await relay.fetch("/v1/vault/keys", { method: "POST", body: ... });
|
|
45
|
+
* relay.clear(); // on disconnect
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function createRelaySession(config: RelaySessionConfig) {
|
|
49
|
+
const { relayUrl, address, chainId = 84532, signMessage, ttl = 1800 } = config;
|
|
50
|
+
const storage = config.storage ?? (typeof sessionStorage !== "undefined" ? sessionStorage : null);
|
|
51
|
+
|
|
52
|
+
const signer: EthHttpSigner = {
|
|
53
|
+
address,
|
|
54
|
+
chainId,
|
|
55
|
+
signMessage: async (message: Uint8Array) => signMessage(message),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const client = createSignerClient(signer, {
|
|
59
|
+
preferReplayable: true,
|
|
60
|
+
ttlSeconds: ttl,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function getCached(): CachedSession | null {
|
|
64
|
+
if (!storage) return null;
|
|
65
|
+
const raw = storage.getItem(CACHE_KEY);
|
|
66
|
+
if (!raw) return null;
|
|
67
|
+
try {
|
|
68
|
+
const session: CachedSession = JSON.parse(raw);
|
|
69
|
+
if (session.address !== address) return null;
|
|
70
|
+
if (session.expiresAt <= Date.now() + 30_000) return null;
|
|
71
|
+
return session;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function acquireToken(): Promise<string> {
|
|
78
|
+
const cached = getCached();
|
|
79
|
+
if (cached) return cached.token;
|
|
80
|
+
|
|
81
|
+
const res = await client.fetch(
|
|
82
|
+
`${relayUrl}/v1/auth/session`,
|
|
83
|
+
{
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
body: JSON.stringify({ ttl }),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
binding: "class-bound",
|
|
90
|
+
replay: "replayable",
|
|
91
|
+
components: ["@authority"],
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
if (!res.ok) throw new Error(`Session request failed: ${res.status}`);
|
|
95
|
+
const data: CachedSession = await res.json();
|
|
96
|
+
storage?.setItem(CACHE_KEY, JSON.stringify(data));
|
|
97
|
+
return data.token;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
/** Make an authenticated fetch to the relay using a cached session token. */
|
|
102
|
+
async fetch(path: string, init?: RequestInit): Promise<Response> {
|
|
103
|
+
const token = await acquireToken();
|
|
104
|
+
return globalThis.fetch(`${relayUrl}${path}`, {
|
|
105
|
+
...init,
|
|
106
|
+
headers: {
|
|
107
|
+
...init?.headers,
|
|
108
|
+
Authorization: `Bearer ${token}`,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/** Clear the cached session token. Call on disconnect. */
|
|
114
|
+
clear() {
|
|
115
|
+
storage?.removeItem(CACHE_KEY);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|