@gethashd/bytecave-browser 1.0.1

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.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * React integration for ByteCave Browser
3
+ *
4
+ * Export all React hooks and components
5
+ */
6
+ export * from './hooks.js';
7
+ export * from './components.js';
8
+ export { useHashdUrl } from './useHashdUrl.js';
@@ -0,0 +1,23 @@
1
+ import {
2
+ HashdAudio,
3
+ HashdContent,
4
+ HashdImage,
5
+ HashdVideo,
6
+ useHashdBatch,
7
+ useHashdContent,
8
+ useHashdImage,
9
+ useHashdMedia,
10
+ useHashdUrl
11
+ } from "../chunk-OJEETLZQ.js";
12
+ import "../chunk-EEZWRIUI.js";
13
+ export {
14
+ HashdAudio,
15
+ HashdContent,
16
+ HashdImage,
17
+ HashdVideo,
18
+ useHashdBatch,
19
+ useHashdContent,
20
+ useHashdImage,
21
+ useHashdMedia,
22
+ useHashdUrl
23
+ };
@@ -0,0 +1,15 @@
1
+ interface UseHashdUrlResult {
2
+ blobUrl: string | null;
3
+ loading: boolean;
4
+ error: string | null;
5
+ }
6
+ /**
7
+ * Hook to convert hashd:// URLs to blob URLs
8
+ * Must be used within ByteCaveProvider
9
+ *
10
+ * @example
11
+ * const { blobUrl, loading, error } = useHashdUrl('hashd://abc123...');
12
+ * return <img src={blobUrl || ''} alt="..." />;
13
+ */
14
+ export declare function useHashdUrl(hashdUrl: string | null | undefined): UseHashdUrlResult;
15
+ export {};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * ByteCave Browser Client Types
3
+ */
4
+ export interface ByteCaveConfig {
5
+ vaultNodeRegistryAddress?: string;
6
+ contentRegistryAddress?: string;
7
+ rpcUrl?: string;
8
+ appId: string;
9
+ directNodeAddrs?: string[];
10
+ relayPeers?: string[];
11
+ maxPeers?: number;
12
+ connectionTimeout?: number;
13
+ }
14
+ export interface PeerInfo {
15
+ peerId: string;
16
+ publicKey: string;
17
+ contentTypes: string[] | 'all';
18
+ connected: boolean;
19
+ latency?: number;
20
+ nodeId?: string;
21
+ isRegistered?: boolean;
22
+ owner?: string;
23
+ }
24
+ export interface StoreResult {
25
+ success: boolean;
26
+ cid?: string;
27
+ peerId?: string;
28
+ error?: string;
29
+ }
30
+ export interface RetrieveResult {
31
+ success: boolean;
32
+ data?: Uint8Array;
33
+ peerId?: string;
34
+ error?: string;
35
+ }
36
+ export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
37
+ export interface SignalingMessage {
38
+ type: 'offer' | 'answer' | 'ice-candidate';
39
+ from: string;
40
+ sdp?: string;
41
+ candidate?: {
42
+ candidate: string;
43
+ sdpMid?: string;
44
+ sdpMLineIndex?: number;
45
+ };
46
+ }
47
+ export interface NodeRegistryEntry {
48
+ nodeId: string;
49
+ owner: string;
50
+ publicKey: string;
51
+ url: string;
52
+ active: boolean;
53
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@gethashd/bytecave-browser",
3
+ "version": "1.0.1",
4
+ "description": "ByteCave browser client for WebRTC P2P connections to storage nodes",
5
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./react": {
16
+ "types": "./dist/react/index.d.ts",
17
+ "import": "./dist/react/index.js",
18
+ "require": "./dist/react/index.cjs"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "build": "tsup && tsc --emitDeclarationOnly --declaration --declarationMap false --outDir dist",
23
+ "prepare": "yarn build",
24
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
25
+ "lint": "eslint src --ext .ts",
26
+ "test": "vitest"
27
+ },
28
+ "keywords": [
29
+ "bytecave",
30
+ "webrtc",
31
+ "p2p",
32
+ "libp2p",
33
+ "browser",
34
+ "storage"
35
+ ],
36
+ "author": "HASHD Team",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@chainsafe/libp2p-noise": "^17.0.0",
40
+ "@chainsafe/libp2p-yamux": "^8.0.1",
41
+ "@libp2p/bootstrap": "^12.0.10",
42
+ "@libp2p/circuit-relay-v2": "^4.1.2",
43
+ "@libp2p/floodsub": "^11.0.10",
44
+ "@libp2p/identify": "^4.0.9",
45
+ "@libp2p/webrtc": "^6.0.10",
46
+ "@libp2p/websockets": "^10.1.2",
47
+ "@multiformats/multiaddr": "^13.0.1",
48
+ "ethers": "^6.0.0",
49
+ "libp2p": "^3.1.2",
50
+ "uint8arrays": "^5.0.0"
51
+ },
52
+ "peerDependencies": {
53
+ "react": "^18.0.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "react": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "devDependencies": {
61
+ "@chainsafe/libp2p-noise": "^17.0.0",
62
+ "@chainsafe/libp2p-yamux": "^8.0.1",
63
+ "@libp2p/bootstrap": "^12.0.10",
64
+ "@libp2p/circuit-relay-v2": "^4.1.2",
65
+ "@libp2p/identify": "^4.0.9",
66
+ "@libp2p/webrtc": "^6.0.10",
67
+ "@libp2p/websockets": "^10.1.2",
68
+ "@multiformats/multiaddr": "^13.0.1",
69
+ "@types/node": "^20.10.0",
70
+ "@types/react": "^19.2.7",
71
+ "libp2p": "^3.1.2",
72
+ "react": "^19.2.3",
73
+ "tsup": "^8.0.0",
74
+ "typescript": "^5.3.3",
75
+ "vitest": "^1.0.0"
76
+ }
77
+ }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Tests for ByteCave Browser P2P Protocol Client (Phase 51)
3
+ *
4
+ * Covers:
5
+ * - P2P protocol client initialization
6
+ * - Store/retrieve via P2P streams
7
+ * - Node info/health retrieval
8
+ * - Base64 encoding/decoding utilities
9
+ */
10
+
11
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
12
+
13
+ describe('P2P Protocol Client', () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ describe('Protocol Constants', () => {
19
+ it('should define correct protocol identifiers', () => {
20
+ const PROTOCOL_BLOB = '/bytecave/blob/1.0.0';
21
+ const PROTOCOL_HEALTH = '/bytecave/health/1.0.0';
22
+ const PROTOCOL_INFO = '/bytecave/info/1.0.0';
23
+
24
+ expect(PROTOCOL_BLOB).toBe('/bytecave/blob/1.0.0');
25
+ expect(PROTOCOL_HEALTH).toBe('/bytecave/health/1.0.0');
26
+ expect(PROTOCOL_INFO).toBe('/bytecave/info/1.0.0');
27
+ });
28
+ });
29
+
30
+ describe('Base64 Encoding', () => {
31
+ it('should encode Uint8Array to base64', () => {
32
+ const bytes = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
33
+
34
+ let binary = '';
35
+ for (let i = 0; i < bytes.length; i++) {
36
+ binary += String.fromCharCode(bytes[i]);
37
+ }
38
+ const base64 = btoa(binary);
39
+
40
+ expect(base64).toBe('SGVsbG8=');
41
+ });
42
+
43
+ it('should decode base64 to Uint8Array', () => {
44
+ const base64 = 'SGVsbG8=';
45
+
46
+ const binary = atob(base64);
47
+ const bytes = new Uint8Array(binary.length);
48
+ for (let i = 0; i < binary.length; i++) {
49
+ bytes[i] = binary.charCodeAt(i);
50
+ }
51
+
52
+ expect(bytes).toEqual(new Uint8Array([72, 101, 108, 108, 111]));
53
+ });
54
+
55
+ it('should handle binary data with special characters', () => {
56
+ const originalBytes = new Uint8Array([0x00, 0x01, 0xff, 0xfe, 0x80]);
57
+
58
+ let binary = '';
59
+ for (let i = 0; i < originalBytes.length; i++) {
60
+ binary += String.fromCharCode(originalBytes[i]);
61
+ }
62
+ const base64 = btoa(binary);
63
+
64
+ const decodedBinary = atob(base64);
65
+ const decodedBytes = new Uint8Array(decodedBinary.length);
66
+ for (let i = 0; i < decodedBinary.length; i++) {
67
+ decodedBytes[i] = decodedBinary.charCodeAt(i);
68
+ }
69
+
70
+ expect(decodedBytes).toEqual(originalBytes);
71
+ });
72
+ });
73
+
74
+ describe('CID Generation', () => {
75
+ it('should generate CID with baf prefix', () => {
76
+ const mockHash = 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef12';
77
+ const cid = 'baf' + mockHash.slice(0, 56);
78
+
79
+ expect(cid).toMatch(/^baf[a-f0-9]{56}$/);
80
+ expect(cid.length).toBe(59);
81
+ });
82
+ });
83
+
84
+ describe('Store Response', () => {
85
+ it('should return success with CID on successful store', () => {
86
+ const response = {
87
+ success: true,
88
+ cid: 'baftest123456'
89
+ };
90
+
91
+ expect(response.success).toBe(true);
92
+ expect(response.cid).toBeTruthy();
93
+ });
94
+
95
+ it('should return error on failed store', () => {
96
+ const response = {
97
+ success: false,
98
+ error: 'Storage full'
99
+ };
100
+
101
+ expect(response.success).toBe(false);
102
+ expect(response.error).toBe('Storage full');
103
+ });
104
+ });
105
+
106
+ describe('Retrieve Response', () => {
107
+ it('should return data on successful retrieve', () => {
108
+ const response = {
109
+ success: true,
110
+ ciphertext: 'SGVsbG8=',
111
+ mimeType: 'application/octet-stream'
112
+ };
113
+
114
+ expect(response.success).toBe(true);
115
+ expect(response.ciphertext).toBeTruthy();
116
+ });
117
+
118
+ it('should return error when blob not found', () => {
119
+ const response = {
120
+ success: false,
121
+ error: 'Blob not found'
122
+ };
123
+
124
+ expect(response.success).toBe(false);
125
+ expect(response.error).toBe('Blob not found');
126
+ });
127
+ });
128
+
129
+ describe('Health Response', () => {
130
+ it('should include required health fields', () => {
131
+ const health = {
132
+ peerId: '12D3KooWTest',
133
+ status: 'healthy',
134
+ blobCount: 42,
135
+ storageUsed: 1024 * 1024,
136
+ storageMax: 1024 * 1024 * 1024,
137
+ uptime: 3600,
138
+ version: '1.0.0',
139
+ contentTypes: 'all',
140
+ multiaddrs: ['/ip4/127.0.0.1/tcp/4001']
141
+ };
142
+
143
+ expect(health.peerId).toBeTruthy();
144
+ expect(health.status).toBe('healthy');
145
+ expect(health.blobCount).toBeGreaterThanOrEqual(0);
146
+ expect(health.storageUsed).toBeGreaterThanOrEqual(0);
147
+ });
148
+ });
149
+
150
+ describe('Info Response', () => {
151
+ it('should include required info fields for registration', () => {
152
+ const info = {
153
+ peerId: '12D3KooWTest',
154
+ publicKey: 'abcdef123456',
155
+ ownerAddress: '0x1234567890abcdef',
156
+ version: '1.0.0',
157
+ contentTypes: 'all'
158
+ };
159
+
160
+ expect(info.peerId).toBeTruthy();
161
+ expect(info.publicKey).toBeTruthy();
162
+ expect(info.ownerAddress).toMatch(/^0x/);
163
+ });
164
+
165
+ it('should allow optional ownerAddress', () => {
166
+ const info = {
167
+ peerId: '12D3KooWTest',
168
+ publicKey: 'abcdef123456',
169
+ version: '1.0.0',
170
+ contentTypes: 'all'
171
+ };
172
+
173
+ expect(info.peerId).toBeTruthy();
174
+ expect(info.publicKey).toBeTruthy();
175
+ });
176
+ });
177
+ });
178
+
179
+ describe('ByteCave Client P2P Integration', () => {
180
+ describe('Store Method', () => {
181
+ it('should try P2P first when peer is connected', () => {
182
+ const peer = { peerId: '12D3KooWTest', connected: true };
183
+ const shouldTryP2P = peer.connected;
184
+
185
+ expect(shouldTryP2P).toBe(true);
186
+ });
187
+
188
+ it('should skip P2P when peer is not connected', () => {
189
+ const peer = { peerId: '12D3KooWTest', connected: false };
190
+ const shouldTryP2P = peer.connected;
191
+
192
+ expect(shouldTryP2P).toBe(false);
193
+ });
194
+
195
+ it('should convert ArrayBuffer to Uint8Array', () => {
196
+ const buffer = new ArrayBuffer(5);
197
+ const view = new Uint8Array(buffer);
198
+ view.set([1, 2, 3, 4, 5]);
199
+
200
+ const data = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer;
201
+ expect(data).toBeInstanceOf(Uint8Array);
202
+ expect(data.length).toBe(5);
203
+ });
204
+ });
205
+
206
+ describe('Retrieve Method', () => {
207
+ it('should try P2P first for connected peers', () => {
208
+ const peers = [
209
+ { peerId: 'peer1', connected: true },
210
+ { peerId: 'peer2', connected: false },
211
+ { peerId: 'peer3', connected: true }
212
+ ];
213
+
214
+ const connectedPeers = peers.filter(p => p.connected);
215
+ expect(connectedPeers.length).toBe(2);
216
+ });
217
+
218
+ it('should fall back to HTTP when P2P fails', () => {
219
+ const p2pResult = null; // P2P failed
220
+ const httpAvailable = true;
221
+
222
+ const shouldTryHttp = p2pResult === null && httpAvailable;
223
+ expect(shouldTryHttp).toBe(true);
224
+ });
225
+ });
226
+
227
+ describe('GetNodeInfo Method', () => {
228
+ it('should return node info for registration', () => {
229
+ const mockInfo = {
230
+ peerId: '12D3KooWTest',
231
+ publicKey: 'abcdef',
232
+ ownerAddress: '0x1234'
233
+ };
234
+
235
+ expect(mockInfo.publicKey).toBeTruthy();
236
+ expect(mockInfo.peerId).toBeTruthy();
237
+ });
238
+ });
239
+ });
240
+
241
+ describe('Connection State', () => {
242
+ it('should track connection states', () => {
243
+ const states = ['disconnected', 'connecting', 'connected', 'error'];
244
+
245
+ expect(states).toContain('disconnected');
246
+ expect(states).toContain('connecting');
247
+ expect(states).toContain('connected');
248
+ expect(states).toContain('error');
249
+ });
250
+
251
+ it('should emit state change events', () => {
252
+ const eventListeners = new Map<string, Set<Function>>();
253
+ eventListeners.set('connectionStateChange', new Set());
254
+
255
+ const callback = vi.fn();
256
+ eventListeners.get('connectionStateChange')?.add(callback);
257
+
258
+ expect(eventListeners.get('connectionStateChange')?.size).toBe(1);
259
+ });
260
+ });
261
+
262
+ describe('Peer Discovery', () => {
263
+ it('should merge known peers with connected peers', () => {
264
+ const knownPeers = new Map([
265
+ ['peer1', { peerId: 'peer1', connected: false }],
266
+ ['peer2', { peerId: 'peer2', connected: false }]
267
+ ]);
268
+
269
+ const connectedPeerIds = new Set(['peer1', 'peer3']);
270
+
271
+ const result: any[] = [];
272
+ for (const [peerIdStr, peer] of knownPeers) {
273
+ result.push({
274
+ ...peer,
275
+ connected: connectedPeerIds.has(peerIdStr)
276
+ });
277
+ connectedPeerIds.delete(peerIdStr);
278
+ }
279
+
280
+ for (const peerIdStr of connectedPeerIds) {
281
+ result.push({
282
+ peerId: peerIdStr,
283
+ connected: true
284
+ });
285
+ }
286
+
287
+ expect(result.length).toBe(3);
288
+ expect(result.find(p => p.peerId === 'peer1')?.connected).toBe(true);
289
+ expect(result.find(p => p.peerId === 'peer2')?.connected).toBe(false);
290
+ expect(result.find(p => p.peerId === 'peer3')?.connected).toBe(true);
291
+ });
292
+ });