@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.
Files changed (176) hide show
  1. package/README.md +104 -0
  2. package/dist/agent/agent-adapter.d.ts +3 -0
  3. package/dist/agent/agent-adapter.d.ts.map +1 -0
  4. package/dist/agent/agent-adapter.js +3 -0
  5. package/dist/agent/agent-adapter.js.map +1 -0
  6. package/dist/agent/client.d.ts +5 -0
  7. package/dist/agent/client.d.ts.map +1 -0
  8. package/dist/agent/client.js +146 -0
  9. package/dist/agent/client.js.map +1 -0
  10. package/dist/agent/index.d.ts +2 -0
  11. package/dist/agent/index.d.ts.map +1 -0
  12. package/dist/agent/index.js +2 -0
  13. package/dist/agent/index.js.map +1 -0
  14. package/dist/client.d.ts +3 -0
  15. package/dist/client.d.ts.map +1 -0
  16. package/dist/client.js +118 -0
  17. package/dist/client.js.map +1 -0
  18. package/dist/contracts.d.ts +19 -0
  19. package/dist/contracts.d.ts.map +1 -0
  20. package/dist/contracts.js +28 -0
  21. package/dist/contracts.js.map +1 -0
  22. package/dist/data/index.d.ts +5 -0
  23. package/dist/data/index.d.ts.map +1 -0
  24. package/dist/data/index.js +5 -0
  25. package/dist/data/index.js.map +1 -0
  26. package/dist/data/orbitdb.d.ts +10 -0
  27. package/dist/data/orbitdb.d.ts.map +1 -0
  28. package/dist/data/orbitdb.js +39 -0
  29. package/dist/data/orbitdb.js.map +1 -0
  30. package/dist/data/pricing.d.ts +7 -0
  31. package/dist/data/pricing.d.ts.map +1 -0
  32. package/dist/data/pricing.js +55 -0
  33. package/dist/data/pricing.js.map +1 -0
  34. package/dist/data/serialization.d.ts +28 -0
  35. package/dist/data/serialization.d.ts.map +1 -0
  36. package/dist/data/serialization.js +76 -0
  37. package/dist/data/serialization.js.map +1 -0
  38. package/dist/data/vault.d.ts +21 -0
  39. package/dist/data/vault.d.ts.map +1 -0
  40. package/dist/data/vault.js +284 -0
  41. package/dist/data/vault.js.map +1 -0
  42. package/dist/discovery/discovery-layer.d.ts +3 -0
  43. package/dist/discovery/discovery-layer.d.ts.map +1 -0
  44. package/dist/discovery/discovery-layer.js +205 -0
  45. package/dist/discovery/discovery-layer.js.map +1 -0
  46. package/dist/discovery/index.d.ts +4 -0
  47. package/dist/discovery/index.d.ts.map +1 -0
  48. package/dist/discovery/index.js +4 -0
  49. package/dist/discovery/index.js.map +1 -0
  50. package/dist/discovery/mock-registry.d.ts +30 -0
  51. package/dist/discovery/mock-registry.d.ts.map +1 -0
  52. package/dist/discovery/mock-registry.js +71 -0
  53. package/dist/discovery/mock-registry.js.map +1 -0
  54. package/dist/discovery/on-chain-registry.d.ts +35 -0
  55. package/dist/discovery/on-chain-registry.d.ts.map +1 -0
  56. package/dist/discovery/on-chain-registry.js +199 -0
  57. package/dist/discovery/on-chain-registry.js.map +1 -0
  58. package/dist/encryption/aes.d.ts +15 -0
  59. package/dist/encryption/aes.d.ts.map +1 -0
  60. package/dist/encryption/aes.js +63 -0
  61. package/dist/encryption/aes.js.map +1 -0
  62. package/dist/encryption/encryption-layer.d.ts +8 -0
  63. package/dist/encryption/encryption-layer.d.ts.map +1 -0
  64. package/dist/encryption/encryption-layer.js +82 -0
  65. package/dist/encryption/encryption-layer.js.map +1 -0
  66. package/dist/encryption/index.d.ts +6 -0
  67. package/dist/encryption/index.d.ts.map +1 -0
  68. package/dist/encryption/index.js +4 -0
  69. package/dist/encryption/index.js.map +1 -0
  70. package/dist/encryption/lit.d.ts +23 -0
  71. package/dist/encryption/lit.d.ts.map +1 -0
  72. package/dist/encryption/lit.js +113 -0
  73. package/dist/encryption/lit.js.map +1 -0
  74. package/dist/encryption/vault-key.d.ts +37 -0
  75. package/dist/encryption/vault-key.d.ts.map +1 -0
  76. package/dist/encryption/vault-key.js +43 -0
  77. package/dist/encryption/vault-key.js.map +1 -0
  78. package/dist/identity/identity-layer.d.ts +3 -0
  79. package/dist/identity/identity-layer.d.ts.map +1 -0
  80. package/dist/identity/identity-layer.js +99 -0
  81. package/dist/identity/identity-layer.js.map +1 -0
  82. package/dist/identity/index.d.ts +4 -0
  83. package/dist/identity/index.d.ts.map +1 -0
  84. package/dist/identity/index.js +4 -0
  85. package/dist/identity/index.js.map +1 -0
  86. package/dist/identity/ows-adapter.d.ts +15 -0
  87. package/dist/identity/ows-adapter.d.ts.map +1 -0
  88. package/dist/identity/ows-adapter.js +67 -0
  89. package/dist/identity/ows-adapter.js.map +1 -0
  90. package/dist/identity/session.d.ts +10 -0
  91. package/dist/identity/session.d.ts.map +1 -0
  92. package/dist/identity/session.js +36 -0
  93. package/dist/identity/session.js.map +1 -0
  94. package/dist/index.d.ts +12 -0
  95. package/dist/index.d.ts.map +1 -0
  96. package/dist/index.js +12 -0
  97. package/dist/index.js.map +1 -0
  98. package/dist/persistence/create-agent.d.ts +11 -0
  99. package/dist/persistence/create-agent.d.ts.map +1 -0
  100. package/dist/persistence/create-agent.js +47 -0
  101. package/dist/persistence/create-agent.js.map +1 -0
  102. package/dist/persistence/index.d.ts +3 -0
  103. package/dist/persistence/index.d.ts.map +1 -0
  104. package/dist/persistence/index.js +3 -0
  105. package/dist/persistence/index.js.map +1 -0
  106. package/dist/persistence/persistence-layer.d.ts +12 -0
  107. package/dist/persistence/persistence-layer.d.ts.map +1 -0
  108. package/dist/persistence/persistence-layer.js +194 -0
  109. package/dist/persistence/persistence-layer.js.map +1 -0
  110. package/dist/transport/index.d.ts +3 -0
  111. package/dist/transport/index.d.ts.map +1 -0
  112. package/dist/transport/index.js +3 -0
  113. package/dist/transport/index.js.map +1 -0
  114. package/dist/transport/relay-session.d.ts +41 -0
  115. package/dist/transport/relay-session.d.ts.map +1 -0
  116. package/dist/transport/relay-session.js +86 -0
  117. package/dist/transport/relay-session.js.map +1 -0
  118. package/dist/transport/transport-layer.d.ts +32 -0
  119. package/dist/transport/transport-layer.d.ts.map +1 -0
  120. package/dist/transport/transport-layer.js +110 -0
  121. package/dist/transport/transport-layer.js.map +1 -0
  122. package/dist/types.d.ts +1319 -0
  123. package/dist/types.d.ts.map +1 -0
  124. package/dist/types.js +7 -0
  125. package/dist/types.js.map +1 -0
  126. package/package.json +91 -0
  127. package/src/__tests__/client.test.ts +30 -0
  128. package/src/__tests__/orbitdb-availability.ts +8 -0
  129. package/src/agent/__tests__/agent-adapter.test.ts +50 -0
  130. package/src/agent/__tests__/client.test.ts +50 -0
  131. package/src/agent/agent-adapter.ts +2 -0
  132. package/src/agent/client.ts +158 -0
  133. package/src/agent/index.ts +1 -0
  134. package/src/client.ts +134 -0
  135. package/src/contracts.ts +44 -0
  136. package/src/data/__tests__/pricing.test.ts +73 -0
  137. package/src/data/__tests__/vault-encryption.test.ts +346 -0
  138. package/src/data/__tests__/vault.test.ts +75 -0
  139. package/src/data/index.ts +8 -0
  140. package/src/data/orbitdb.ts +47 -0
  141. package/src/data/pricing.ts +63 -0
  142. package/src/data/serialization.ts +108 -0
  143. package/src/data/vault.ts +382 -0
  144. package/src/discovery/__tests__/discovery.test.ts +49 -0
  145. package/src/discovery/__tests__/on-chain-registry.test.ts +176 -0
  146. package/src/discovery/discovery-layer.ts +244 -0
  147. package/src/discovery/index.ts +3 -0
  148. package/src/discovery/mock-registry.ts +96 -0
  149. package/src/discovery/on-chain-registry.ts +237 -0
  150. package/src/encryption/__tests__/aes.test.ts +64 -0
  151. package/src/encryption/__tests__/encryption-layer.test.ts +80 -0
  152. package/src/encryption/__tests__/lit.test.ts +97 -0
  153. package/src/encryption/aes.ts +109 -0
  154. package/src/encryption/encryption-layer.ts +100 -0
  155. package/src/encryption/index.ts +5 -0
  156. package/src/encryption/lit.ts +161 -0
  157. package/src/encryption/vault-key.ts +63 -0
  158. package/src/identity/__tests__/identity.test.ts +31 -0
  159. package/src/identity/__tests__/ows-adapter.test.ts +47 -0
  160. package/src/identity/identity-layer.ts +123 -0
  161. package/src/identity/index.ts +3 -0
  162. package/src/identity/ows-adapter.ts +80 -0
  163. package/src/identity/session.ts +57 -0
  164. package/src/index.ts +12 -0
  165. package/src/persistence/__tests__/create-agent.test.ts +9 -0
  166. package/src/persistence/__tests__/persistence.test.ts +242 -0
  167. package/src/persistence/create-agent.ts +55 -0
  168. package/src/persistence/index.ts +2 -0
  169. package/src/persistence/persistence-layer.ts +236 -0
  170. package/src/transport/__tests__/solana-transport.test.ts +112 -0
  171. package/src/transport/__tests__/transport.test.ts +84 -0
  172. package/src/transport/index.ts +2 -0
  173. package/src/transport/relay-session.ts +118 -0
  174. package/src/transport/transport-layer.ts +171 -0
  175. package/src/types/orbitdb.d.ts +9 -0
  176. package/src/types.ts +1496 -0
@@ -0,0 +1,123 @@
1
+ import type {
2
+ ChainFamily,
3
+ IdentityConfig,
4
+ IIdentityLayer,
5
+ SessionKey,
6
+ SignatureAlgorithm,
7
+ WalletConnection,
8
+ } from "../types.js";
9
+ import { deriveSessionKey } from "./session.js";
10
+
11
+ export function createIdentityLayer(config: IdentityConfig): IIdentityLayer {
12
+ let connection: WalletConnection | null = null;
13
+ let activeSession: SessionKey | null = null;
14
+ const sessions = new Map<string, SessionKey>();
15
+ const listeners: Set<(conn: WalletConnection | null) => void> = new Set();
16
+
17
+ // Store signer function set by external wallet adapters
18
+ let signFn:
19
+ | ((message: string) => Promise<{ signature: Uint8Array; algorithm: SignatureAlgorithm }>)
20
+ | null = null;
21
+
22
+ return {
23
+ async connect(opts) {
24
+ // If an OWS wallet was provided (CLI / server usage), auto-connect via OWS adapter
25
+ if (config.owsWallet) {
26
+ const { createOwsAdapter } = await import("./ows-adapter.js");
27
+ const owsChain = config.owsChain ?? "eip155:84532";
28
+ const adapter = createOwsAdapter(config.owsWallet, owsChain);
29
+ const address = await adapter.getAddress();
30
+
31
+ const family: ChainFamily = owsChain.startsWith("solana:") ? "solana" : "evm";
32
+ const algorithm: SignatureAlgorithm = family === "solana" ? "ed25519" : "ecdsa-secp256k1";
33
+
34
+ connection = {
35
+ address,
36
+ family,
37
+ signatureAlgorithm: algorithm,
38
+ connectedAt: Date.now(),
39
+ };
40
+
41
+ signFn = async (message: string) => {
42
+ return adapter.signMessage(message);
43
+ };
44
+
45
+ for (const cb of listeners) cb(connection);
46
+ return connection;
47
+ }
48
+
49
+ throw new Error(
50
+ `connect(${opts.method}) requires a wallet adapter or owsWallet config. ` +
51
+ "Use setConnection() for testing or integrate a wallet provider.",
52
+ );
53
+ },
54
+
55
+ async createPasskey() {
56
+ throw new Error("Passkey creation requires browser WebAuthn API");
57
+ },
58
+
59
+ async disconnect() {
60
+ connection = null;
61
+ activeSession = null;
62
+ signFn = null;
63
+ for (const cb of listeners) cb(null);
64
+ },
65
+
66
+ async signChallenge(message) {
67
+ if (!signFn) throw new Error("No signer available — connect a wallet first");
68
+ return signFn(message);
69
+ },
70
+
71
+ async createSessionKey(permissions, opts) {
72
+ if (!connection) throw new Error("No wallet connected");
73
+ if (!signFn) throw new Error("No signer available");
74
+
75
+ const challenge = `OrbitMem Authentication\nTimestamp: ${Date.now()}\nNonce: ${crypto.randomUUID()}`;
76
+ const { signature } = await signFn(challenge);
77
+
78
+ const session = await deriveSessionKey({
79
+ family: connection.family,
80
+ signature,
81
+ parentAddress: connection.address,
82
+ permissions,
83
+ ttl: opts?.ttl ?? config.sessionTTL ?? 3600,
84
+ });
85
+
86
+ sessions.set(session.id, session);
87
+ activeSession = session;
88
+ return session;
89
+ },
90
+
91
+ async resumeSession(sessionId) {
92
+ const session = sessions.get(sessionId);
93
+ if (!session) return null;
94
+ if (session.expiresAt <= Date.now()) {
95
+ sessions.delete(sessionId);
96
+ return null;
97
+ }
98
+ activeSession = session;
99
+ return session;
100
+ },
101
+
102
+ async revokeSession(sessionId) {
103
+ sessions.delete(sessionId);
104
+ if (activeSession?.id === sessionId) activeSession = null;
105
+ },
106
+
107
+ getConnection() {
108
+ return connection;
109
+ },
110
+
111
+ getActiveSession() {
112
+ if (activeSession && activeSession.expiresAt <= Date.now()) {
113
+ activeSession = null;
114
+ }
115
+ return activeSession;
116
+ },
117
+
118
+ onConnectionChange(callback) {
119
+ listeners.add(callback);
120
+ return () => listeners.delete(callback);
121
+ },
122
+ };
123
+ }
@@ -0,0 +1,3 @@
1
+ export { createIdentityLayer } from "./identity-layer.js";
2
+ export { deriveSessionKey } from "./session.js";
3
+ export { createOwsAdapter, type OwsAdapter } from "./ows-adapter.js";
@@ -0,0 +1,80 @@
1
+ import type { SignatureAlgorithm, WalletAddress } from "../types.js";
2
+
3
+ export interface OwsAdapter {
4
+ getAddress(): Promise<WalletAddress>;
5
+ signMessage(message: string): Promise<{ signature: Uint8Array; algorithm: SignatureAlgorithm }>;
6
+ toViemAccount(): Promise<import("viem").Account>;
7
+ }
8
+
9
+ /**
10
+ * @param walletName — OWS wallet name (e.g., "orbitmem")
11
+ * @param chain — CAIP-2 chain ID (e.g., "eip155:84532" for Base Sepolia)
12
+ */
13
+ export function createOwsAdapter(walletName: string, chain: string): OwsAdapter {
14
+ // OWS NAPI-RS bindings are synchronous — wrap in lazy import for ESM compat.
15
+ const ows = () => import("@open-wallet-standard/core");
16
+
17
+ function hexToBytes(hex: string): Uint8Array {
18
+ const clean = hex.replace(/^0x/, "");
19
+ return new Uint8Array(clean.match(/.{2}/g)!.map((b) => Number.parseInt(b, 16)));
20
+ }
21
+
22
+ /**
23
+ * Find the EVM address for the given CAIP-2 chain from WalletInfo.accounts.
24
+ * Falls back to the first eip155 account if exact chain not found.
25
+ */
26
+ function resolveAddress(accounts: Array<{ chainId: string; address: string }>): string {
27
+ const exact = accounts.find((a) => a.chainId === chain);
28
+ if (exact) return exact.address;
29
+ const evm = accounts.find((a) => a.chainId.startsWith("eip155:"));
30
+ if (evm) return evm.address;
31
+ throw new Error(`No EVM account found for chain ${chain} in wallet "${walletName}"`);
32
+ }
33
+
34
+ return {
35
+ async getAddress(): Promise<WalletAddress> {
36
+ const { getWallet } = await ows();
37
+ const wallet = getWallet(walletName);
38
+ return resolveAddress(wallet.accounts) as WalletAddress;
39
+ },
40
+
41
+ async signMessage(
42
+ message: string,
43
+ ): Promise<{ signature: Uint8Array; algorithm: SignatureAlgorithm }> {
44
+ const { signMessage: owsSign } = await ows();
45
+ const result = owsSign(walletName, chain, message);
46
+ return { signature: hexToBytes(result.signature), algorithm: "ecdsa-secp256k1" };
47
+ },
48
+
49
+ async toViemAccount(): Promise<import("viem").Account> {
50
+ const address = await this.getAddress();
51
+ const { toAccount } = await import("viem/accounts");
52
+ return toAccount({
53
+ address: address as `0x${string}`,
54
+ async signMessage({ message }) {
55
+ const { signMessage: owsSign } = await ows();
56
+ const msg =
57
+ typeof message === "string"
58
+ ? message
59
+ : typeof message === "object" && "raw" in message
60
+ ? typeof message.raw === "string"
61
+ ? message.raw
62
+ : new TextDecoder().decode(message.raw)
63
+ : String(message);
64
+ const result = owsSign(walletName, chain, msg);
65
+ return result.signature as `0x${string}`;
66
+ },
67
+ async signTransaction(tx) {
68
+ const { signTransaction: owsSignTx } = await ows();
69
+ const result = owsSignTx(walletName, chain, JSON.stringify(tx));
70
+ return result.signature as `0x${string}`;
71
+ },
72
+ async signTypedData(typedData) {
73
+ const { signTypedData: owsSignTyped } = await ows();
74
+ const result = owsSignTyped(walletName, chain, JSON.stringify(typedData));
75
+ return result.signature as `0x${string}`;
76
+ },
77
+ });
78
+ },
79
+ };
80
+ }
@@ -0,0 +1,57 @@
1
+ import type {
2
+ ChainFamily,
3
+ SessionKey,
4
+ SessionPermission,
5
+ SignatureAlgorithm,
6
+ WalletAddress,
7
+ } from "../types.js";
8
+
9
+ export async function deriveSessionKey(opts: {
10
+ family: ChainFamily;
11
+ signature: Uint8Array;
12
+ parentAddress: WalletAddress;
13
+ permissions: SessionPermission[];
14
+ ttl: number; // seconds
15
+ nonce?: Uint8Array;
16
+ }): Promise<SessionKey> {
17
+ const nonce = opts.nonce ?? crypto.getRandomValues(new Uint8Array(32));
18
+
19
+ // Derive session address from signature + nonce
20
+ const combined = new Uint8Array(opts.signature.length + nonce.length);
21
+ combined.set(opts.signature, 0);
22
+ combined.set(nonce, opts.signature.length);
23
+
24
+ const hashBuffer = await crypto.subtle.digest("SHA-256", combined);
25
+ const hashArray = new Uint8Array(hashBuffer);
26
+
27
+ // Use first 20 bytes as session address (EVM-style)
28
+ const sessionAddrBytes = hashArray.slice(0, 20);
29
+ const sessionAddress = ("0x" +
30
+ Array.from(sessionAddrBytes)
31
+ .map((b) => b.toString(16).padStart(2, "0"))
32
+ .join("")) as WalletAddress;
33
+
34
+ // Session ID from hash
35
+ const id = Array.from(hashArray.slice(20, 32))
36
+ .map((b) => b.toString(16).padStart(2, "0"))
37
+ .join("");
38
+
39
+ const expiresAt = Date.now() + opts.ttl * 1000;
40
+
41
+ const algorithmMap: Record<ChainFamily, SignatureAlgorithm> = {
42
+ passkey: "p256",
43
+ evm: "ecdsa-secp256k1",
44
+ solana: "ed25519",
45
+ };
46
+
47
+ return {
48
+ id,
49
+ parentAddress: opts.parentAddress,
50
+ family: opts.family,
51
+ sessionAddress,
52
+ permissions: opts.permissions,
53
+ expiresAt,
54
+ isActive: expiresAt > Date.now(),
55
+ algorithm: algorithmMap[opts.family],
56
+ };
57
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { createOrbitMem } from "./client.js";
2
+ export type { NetworkConfig, NetworkId } from "./contracts.js";
3
+ export { DEFAULT_NETWORK, getNetwork, NETWORKS } from "./contracts.js";
4
+ export { createOrbitDBInstance, createVault } from "./data/index.js";
5
+ export { createDiscoveryLayer, MockRegistry, OnChainRegistry } from "./discovery/index.js";
6
+ // Layer-level exports for advanced usage
7
+ export { AESEngine, createEncryptionLayer } from "./encryption/index.js";
8
+ export { createIdentityLayer } from "./identity/index.js";
9
+ export { createPersistenceLayer } from "./persistence/index.js";
10
+ export { createRelaySession, createTransportLayer } from "./transport/index.js";
11
+ export { deriveVaultKeyWithCache } from "./encryption/vault-key.js";
12
+ export * from "./types.js";
@@ -0,0 +1,9 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ describe("createStorachaAgent", () => {
4
+ test("returns agentDID, proof, and instructions", async () => {
5
+ const { createStorachaAgent } = await import("../create-agent.js");
6
+ expect(createStorachaAgent).toBeDefined();
7
+ expect(typeof createStorachaAgent).toBe("function");
8
+ });
9
+ });
@@ -0,0 +1,242 @@
1
+ import { afterEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { createPersistenceLayer } from "../persistence-layer.js";
4
+
5
+ // ── Mode detection ───────────────────────────────────────────
6
+
7
+ describe("createPersistenceLayer mode detection", () => {
8
+ test("mock: true → creates mock persistence (archive returns bafy CID)", async () => {
9
+ const layer = createPersistenceLayer({ mock: true });
10
+ const snap = await layer.archive({});
11
+ expect(snap.cid).toMatch(/^bafy/);
12
+ });
13
+
14
+ test("relayUrl → creates managed persistence (methods exist)", () => {
15
+ const layer = createPersistenceLayer({ relayUrl: "http://localhost:3000" });
16
+ expect(layer.archive).toBeFunction();
17
+ expect(layer.retrieve).toBeFunction();
18
+ expect(layer.listSnapshots).toBeFunction();
19
+ expect(layer.deleteSnapshot).toBeFunction();
20
+ expect(layer.getDealStatus).toBeFunction();
21
+ });
22
+
23
+ test("proof → creates direct persistence (methods exist)", () => {
24
+ const layer = createPersistenceLayer({ proof: "ucan-proof-string" });
25
+ expect(layer.archive).toBeFunction();
26
+ expect(layer.retrieve).toBeFunction();
27
+ expect(layer.listSnapshots).toBeFunction();
28
+ expect(layer.deleteSnapshot).toBeFunction();
29
+ expect(layer.getDealStatus).toBeFunction();
30
+ });
31
+
32
+ test("empty config {} → defaults to mock", async () => {
33
+ const layer = createPersistenceLayer({});
34
+ const snap = await layer.archive({});
35
+ expect(snap.cid).toMatch(/^bafy/);
36
+ });
37
+ });
38
+
39
+ // ── Mock persistence ─────────────────────────────────────────
40
+
41
+ describe("PersistenceLayer (mock)", () => {
42
+ const layer = createPersistenceLayer({ mock: true });
43
+
44
+ test("archive creates a snapshot", async () => {
45
+ const snapshot = await (layer.archive as any)({
46
+ data: new TextEncoder().encode('{"test": true}'),
47
+ entryCount: 1,
48
+ });
49
+ expect(snapshot.cid).toBeTruthy();
50
+ expect(snapshot.size).toBeGreaterThan(0);
51
+ expect(snapshot.encrypted).toBe(true);
52
+ });
53
+
54
+ test("listSnapshots returns archived snapshots", async () => {
55
+ const list = await layer.listSnapshots();
56
+ expect(list.length).toBeGreaterThan(0);
57
+ });
58
+
59
+ test("retrieve returns snapshot data", async () => {
60
+ const snapshots = await layer.listSnapshots();
61
+ const data = await layer.retrieve(snapshots[0].cid);
62
+ expect(data).toBeInstanceOf(Uint8Array);
63
+ });
64
+
65
+ test("restore returns merged count", async () => {
66
+ const snapshots = await layer.listSnapshots();
67
+ const result = await layer.restore(snapshots[0].cid);
68
+ expect(result.merged).toBeDefined();
69
+ expect(result.conflicts).toBe(0);
70
+ });
71
+
72
+ test("deleteSnapshot removes a snapshot", async () => {
73
+ const snap = await (layer.archive as any)({
74
+ data: new TextEncoder().encode("delete me"),
75
+ entryCount: 1,
76
+ });
77
+ const beforeCount = (await layer.listSnapshots()).length;
78
+ await layer.deleteSnapshot(snap.cid);
79
+ const afterCount = (await layer.listSnapshots()).length;
80
+ expect(afterCount).toBe(beforeCount - 1);
81
+ });
82
+
83
+ test("getDealStatus returns status for existing snapshot", async () => {
84
+ const snap = await (layer.archive as any)({
85
+ data: new TextEncoder().encode("status check"),
86
+ entryCount: 1,
87
+ });
88
+ const deal = await layer.getDealStatus(snap.cid);
89
+ expect(deal.status).toBe("pending");
90
+ });
91
+ });
92
+
93
+ // ── Managed persistence ──────────────────────────────────────
94
+
95
+ describe("PersistenceLayer (managed)", () => {
96
+ const originalFetch = globalThis.fetch;
97
+
98
+ afterEach(() => {
99
+ globalThis.fetch = originalFetch;
100
+ });
101
+
102
+ test("archive POSTs to relay and returns mapped snapshot", async () => {
103
+ const mockResponse = {
104
+ cid: "bafyrelay123",
105
+ size: 42,
106
+ archivedAt: Date.now(),
107
+ author: "0xabc",
108
+ entryCount: 5,
109
+ encrypted: true,
110
+ filecoinStatus: "pending",
111
+ };
112
+
113
+ globalThis.fetch = mock((url: string, init?: RequestInit) => {
114
+ expect(url).toBe("http://localhost:3000/v1/snapshots/archive");
115
+ expect(init?.method).toBe("POST");
116
+ const body = JSON.parse(init?.body as string);
117
+ expect(body.data).toBeDefined();
118
+ expect(body.entryCount).toBe(5);
119
+ return Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 }));
120
+ }) as unknown as typeof fetch;
121
+
122
+ const layer = createPersistenceLayer({
123
+ relayUrl: "http://localhost:3000",
124
+ author: "0xabc" as `0x${string}`,
125
+ });
126
+
127
+ const snap = await (layer.archive as any)({
128
+ data: new TextEncoder().encode("hello relay"),
129
+ entryCount: 5,
130
+ });
131
+
132
+ expect(snap.cid).toBe("bafyrelay123");
133
+ expect(snap.entryCount).toBe(5);
134
+ });
135
+
136
+ test("listSnapshots GETs from relay and returns mapped array", async () => {
137
+ const mockSnapshots = [
138
+ {
139
+ cid: "bafy1",
140
+ size: 10,
141
+ archivedAt: Date.now(),
142
+ author: "0xabc",
143
+ entryCount: 2,
144
+ encrypted: true,
145
+ filecoinStatus: "pending",
146
+ },
147
+ {
148
+ cid: "bafy2",
149
+ size: 20,
150
+ archivedAt: Date.now(),
151
+ author: "0xabc",
152
+ entryCount: 3,
153
+ encrypted: true,
154
+ filecoinStatus: "active",
155
+ },
156
+ ];
157
+
158
+ globalThis.fetch = mock((url: string) => {
159
+ expect(url).toBe("http://localhost:3000/v1/snapshots");
160
+ return Promise.resolve(new Response(JSON.stringify(mockSnapshots), { status: 200 }));
161
+ }) as unknown as typeof fetch;
162
+
163
+ const layer = createPersistenceLayer({ relayUrl: "http://localhost:3000" });
164
+ const list = await layer.listSnapshots();
165
+
166
+ expect(list).toHaveLength(2);
167
+ expect(list[0].cid).toBe("bafy1");
168
+ expect(list[1].cid).toBe("bafy2");
169
+ });
170
+
171
+ test("retrieve fetches from IPFS gateway", async () => {
172
+ const payload = new TextEncoder().encode("snapshot data");
173
+
174
+ globalThis.fetch = mock((url: string) => {
175
+ expect(url).toBe("https://w3s.link/ipfs/bafytest");
176
+ return Promise.resolve(new Response(payload, { status: 200 }));
177
+ }) as unknown as typeof fetch;
178
+
179
+ const layer = createPersistenceLayer({ relayUrl: "http://localhost:3000" });
180
+ const data = await layer.retrieve("bafytest");
181
+
182
+ expect(data).toBeInstanceOf(Uint8Array);
183
+ expect(new TextDecoder().decode(data)).toBe("snapshot data");
184
+ });
185
+
186
+ test("retrieve uses custom gatewayUrl when provided", async () => {
187
+ const payload = new TextEncoder().encode("custom gw");
188
+
189
+ globalThis.fetch = mock((url: string) => {
190
+ expect(url).toBe("https://custom.gw/ipfs/bafycustom");
191
+ return Promise.resolve(new Response(payload, { status: 200 }));
192
+ }) as unknown as typeof fetch;
193
+
194
+ const layer = createPersistenceLayer({
195
+ relayUrl: "http://localhost:3000",
196
+ gatewayUrl: "https://custom.gw",
197
+ });
198
+ const data = await layer.retrieve("bafycustom");
199
+ expect(new TextDecoder().decode(data)).toBe("custom gw");
200
+ });
201
+
202
+ test("archive throws on non-ok response", async () => {
203
+ globalThis.fetch = mock(() => {
204
+ return Promise.resolve(new Response("Internal Server Error", { status: 500 }));
205
+ }) as unknown as typeof fetch;
206
+
207
+ const layer = createPersistenceLayer({ relayUrl: "http://localhost:3000" });
208
+ expect((layer.archive as any)({ data: new Uint8Array(0), entryCount: 0 })).rejects.toThrow(
209
+ "500",
210
+ );
211
+ });
212
+
213
+ test("deleteSnapshot sends DELETE to relay", async () => {
214
+ globalThis.fetch = mock((url: string, init?: RequestInit) => {
215
+ expect(url).toBe("http://localhost:3000/v1/snapshots/bafydel");
216
+ expect(init?.method).toBe("DELETE");
217
+ return Promise.resolve(new Response(null, { status: 204 }));
218
+ }) as unknown as typeof fetch;
219
+
220
+ const layer = createPersistenceLayer({ relayUrl: "http://localhost:3000" });
221
+ await layer.deleteSnapshot("bafydel");
222
+ });
223
+
224
+ test("restore retrieves then returns merge info", async () => {
225
+ const payload = new TextEncoder().encode("data");
226
+
227
+ globalThis.fetch = mock(() => {
228
+ return Promise.resolve(new Response(payload, { status: 200 }));
229
+ }) as unknown as typeof fetch;
230
+
231
+ const layer = createPersistenceLayer({ relayUrl: "http://localhost:3000" });
232
+ const result = await layer.restore("bafyrestore");
233
+ expect(result.merged).toBe(0);
234
+ expect(result.conflicts).toBe(0);
235
+ });
236
+
237
+ test("getDealStatus returns pending", async () => {
238
+ const layer = createPersistenceLayer({ relayUrl: "http://localhost:3000" });
239
+ const deal = await layer.getDealStatus("bafydeal");
240
+ expect(deal.status).toBe("pending");
241
+ });
242
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * One-time setup helper for BYOS (Bring Your Own Storacha) users.
3
+ * Creates a Storacha agent, provisions a space, and returns the
4
+ * serialized proof to store in config.
5
+ */
6
+ export async function createStorachaAgent(name = "orbitmem"): Promise<{
7
+ agentDID: string;
8
+ proof: string;
9
+ instructions: string;
10
+ }> {
11
+ const { Client } = await import("@storacha/client");
12
+
13
+ const client = await (Client as any).create();
14
+ const space = await client.createSpace(name);
15
+ await client.setCurrentSpace(space.did());
16
+
17
+ const delegation = await client.createDelegation(client.agent, ["*"], {
18
+ expiration: Infinity,
19
+ });
20
+
21
+ const chunks: Uint8Array[] = [];
22
+ for await (const chunk of delegation.archive()) {
23
+ chunks.push(chunk);
24
+ }
25
+ const totalLength = chunks.reduce((acc, c) => acc + c.length, 0);
26
+ const car = new Uint8Array(totalLength);
27
+ let offset = 0;
28
+ for (const chunk of chunks) {
29
+ car.set(chunk, offset);
30
+ offset += chunk.length;
31
+ }
32
+
33
+ const proof = btoa(String.fromCharCode(...car));
34
+
35
+ return {
36
+ agentDID: client.agent.did(),
37
+ proof,
38
+ instructions: [
39
+ "Storacha agent created successfully.",
40
+ `Agent DID: ${client.agent.did()}`,
41
+ `Space DID: ${space.did()}`,
42
+ "",
43
+ "Save the 'proof' string in your OrbitMem config:",
44
+ "",
45
+ " createOrbitMem({ persistence: { proof: '<proof string>' } })",
46
+ "",
47
+ "Or set it as an environment variable:",
48
+ "",
49
+ " ORBITMEM_STORACHA_PROOF=<proof string>",
50
+ "",
51
+ "Note: You must register this agent with Storacha before uploading.",
52
+ "Run: npx @storacha/cli login <email>",
53
+ ].join("\n"),
54
+ };
55
+ }
@@ -0,0 +1,2 @@
1
+ export { createStorachaAgent } from "./create-agent.js";
2
+ export { createPersistenceLayer } from "./persistence-layer.js";