@bcts/hubert 1.0.0-alpha.17

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 (104) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +18 -0
  3. package/dist/arid-derivation-1CJuU-kZ.cjs +150 -0
  4. package/dist/arid-derivation-1CJuU-kZ.cjs.map +1 -0
  5. package/dist/arid-derivation-CbqACjdg.mjs +126 -0
  6. package/dist/arid-derivation-CbqACjdg.mjs.map +1 -0
  7. package/dist/bin/hubert.cjs +384 -0
  8. package/dist/bin/hubert.cjs.map +1 -0
  9. package/dist/bin/hubert.d.cts +1 -0
  10. package/dist/bin/hubert.d.mts +1 -0
  11. package/dist/bin/hubert.mjs +383 -0
  12. package/dist/bin/hubert.mjs.map +1 -0
  13. package/dist/chunk-CbDLau6x.cjs +34 -0
  14. package/dist/hybrid/index.cjs +14 -0
  15. package/dist/hybrid/index.d.cts +3 -0
  16. package/dist/hybrid/index.d.mts +3 -0
  17. package/dist/hybrid/index.mjs +6 -0
  18. package/dist/hybrid-BZhumygj.mjs +356 -0
  19. package/dist/hybrid-BZhumygj.mjs.map +1 -0
  20. package/dist/hybrid-dX5JLumO.cjs +410 -0
  21. package/dist/hybrid-dX5JLumO.cjs.map +1 -0
  22. package/dist/index-BEzpUC7r.d.mts +380 -0
  23. package/dist/index-BEzpUC7r.d.mts.map +1 -0
  24. package/dist/index-C2F6ugLL.d.mts +210 -0
  25. package/dist/index-C2F6ugLL.d.mts.map +1 -0
  26. package/dist/index-CUnDouMb.d.mts +215 -0
  27. package/dist/index-CUnDouMb.d.mts.map +1 -0
  28. package/dist/index-CV6lZJqY.d.cts +380 -0
  29. package/dist/index-CV6lZJqY.d.cts.map +1 -0
  30. package/dist/index-CY3TCzIm.d.cts +217 -0
  31. package/dist/index-CY3TCzIm.d.cts.map +1 -0
  32. package/dist/index-DEr4SR1J.d.cts +215 -0
  33. package/dist/index-DEr4SR1J.d.cts.map +1 -0
  34. package/dist/index-T1LHanIb.d.mts +217 -0
  35. package/dist/index-T1LHanIb.d.mts.map +1 -0
  36. package/dist/index-jyzuOhFB.d.cts +210 -0
  37. package/dist/index-jyzuOhFB.d.cts.map +1 -0
  38. package/dist/index.cjs +60 -0
  39. package/dist/index.d.cts +161 -0
  40. package/dist/index.d.cts.map +1 -0
  41. package/dist/index.d.mts +161 -0
  42. package/dist/index.d.mts.map +1 -0
  43. package/dist/index.mjs +10 -0
  44. package/dist/ipfs/index.cjs +13 -0
  45. package/dist/ipfs/index.d.cts +3 -0
  46. package/dist/ipfs/index.d.mts +3 -0
  47. package/dist/ipfs/index.mjs +5 -0
  48. package/dist/ipfs-BRMMCBjv.mjs +1 -0
  49. package/dist/ipfs-CetOVQcO.cjs +0 -0
  50. package/dist/kv-BAmhmMOo.cjs +425 -0
  51. package/dist/kv-BAmhmMOo.cjs.map +1 -0
  52. package/dist/kv-C-emxv0w.mjs +375 -0
  53. package/dist/kv-C-emxv0w.mjs.map +1 -0
  54. package/dist/kv-DJiKvypY.mjs +403 -0
  55. package/dist/kv-DJiKvypY.mjs.map +1 -0
  56. package/dist/kv-store-DmngWWuw.d.mts +183 -0
  57. package/dist/kv-store-DmngWWuw.d.mts.map +1 -0
  58. package/dist/kv-store-ww-AUyLd.d.cts +183 -0
  59. package/dist/kv-store-ww-AUyLd.d.cts.map +1 -0
  60. package/dist/kv-yjvQa_LH.cjs +457 -0
  61. package/dist/kv-yjvQa_LH.cjs.map +1 -0
  62. package/dist/logging-hmzNzifq.mjs +158 -0
  63. package/dist/logging-hmzNzifq.mjs.map +1 -0
  64. package/dist/logging-qc9uMgil.cjs +212 -0
  65. package/dist/logging-qc9uMgil.cjs.map +1 -0
  66. package/dist/mainline/index.cjs +12 -0
  67. package/dist/mainline/index.d.cts +3 -0
  68. package/dist/mainline/index.d.mts +3 -0
  69. package/dist/mainline/index.mjs +5 -0
  70. package/dist/mainline-D_jfeFMh.cjs +0 -0
  71. package/dist/mainline-cFIuXbo-.mjs +1 -0
  72. package/dist/server/index.cjs +14 -0
  73. package/dist/server/index.d.cts +3 -0
  74. package/dist/server/index.d.mts +3 -0
  75. package/dist/server/index.mjs +3 -0
  76. package/dist/server-BBNRZ30D.cjs +912 -0
  77. package/dist/server-BBNRZ30D.cjs.map +1 -0
  78. package/dist/server-DVyk9gqU.mjs +836 -0
  79. package/dist/server-DVyk9gqU.mjs.map +1 -0
  80. package/package.json +125 -0
  81. package/src/arid-derivation.ts +155 -0
  82. package/src/bin/hubert.ts +667 -0
  83. package/src/error.ts +89 -0
  84. package/src/hybrid/error.ts +77 -0
  85. package/src/hybrid/index.ts +24 -0
  86. package/src/hybrid/kv.ts +236 -0
  87. package/src/hybrid/reference.ts +176 -0
  88. package/src/index.ts +145 -0
  89. package/src/ipfs/error.ts +83 -0
  90. package/src/ipfs/index.ts +24 -0
  91. package/src/ipfs/kv.ts +476 -0
  92. package/src/ipfs/value.ts +85 -0
  93. package/src/kv-store.ts +128 -0
  94. package/src/logging.ts +88 -0
  95. package/src/mainline/error.ts +108 -0
  96. package/src/mainline/index.ts +23 -0
  97. package/src/mainline/kv.ts +411 -0
  98. package/src/server/error.ts +83 -0
  99. package/src/server/index.ts +29 -0
  100. package/src/server/kv.ts +211 -0
  101. package/src/server/memory-kv.ts +191 -0
  102. package/src/server/server-kv.ts +92 -0
  103. package/src/server/server.ts +369 -0
  104. package/src/server/sqlite-kv.ts +295 -0
@@ -0,0 +1,667 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hubert: Secure Distributed Substrate for Multiparty Transactions
4
+ *
5
+ * A command-line tool for storing and retrieving Gordian Envelopes using
6
+ * distributed storage backends (BitTorrent Mainline DHT or IPFS).
7
+ *
8
+ * Port of bin/hubert.rs from hubert-rust.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import { Command } from "commander";
14
+ import path from "path";
15
+
16
+ import { ARID } from "@bcts/components";
17
+ import { Envelope } from "@bcts/envelope";
18
+ import { randomData } from "@bcts/rand";
19
+
20
+ import {
21
+ verbosePrintln,
22
+ IpfsKv,
23
+ MainlineDhtKv,
24
+ HybridKv,
25
+ Server,
26
+ type ServerConfig,
27
+ ServerKvClient,
28
+ SqliteKv,
29
+ } from "../index.js";
30
+
31
+ // =============================================================================
32
+ // Types
33
+ // =============================================================================
34
+
35
+ type StorageBackend = "mainline" | "ipfs" | "hybrid" | "server";
36
+
37
+ // =============================================================================
38
+ // Helper Functions
39
+ // =============================================================================
40
+
41
+ /**
42
+ * Parse an ARID from ur:arid string format.
43
+ *
44
+ * Port of `parse_arid()` from bin/hubert.rs lines 150-153.
45
+ */
46
+ function parseArid(s: string): ARID {
47
+ try {
48
+ return ARID.fromUrString(s);
49
+ } catch {
50
+ throw new Error("Invalid ARID format. Expected ur:arid");
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Parse an Envelope from ur:envelope string format.
56
+ *
57
+ * Port of `parse_envelope()` from bin/hubert.rs lines 155-161.
58
+ */
59
+ function parseEnvelope(s: string): Envelope {
60
+ try {
61
+ return Envelope.fromUrString(s);
62
+ } catch {
63
+ throw new Error("Invalid envelope format. Expected ur:envelope");
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Generate a random envelope with the specified size.
69
+ *
70
+ * Port of `generate_random_envelope()` from bin/hubert.rs lines 163-167.
71
+ */
72
+ function generateRandomEnvelope(size: number): Envelope {
73
+ const randomBytes = randomData(size);
74
+ // Pass raw Uint8Array directly - Envelope.new accepts it as EnvelopeEncodableValue
75
+ return Envelope.new(randomBytes);
76
+ }
77
+
78
+ // =============================================================================
79
+ // Check Functions
80
+ // =============================================================================
81
+
82
+ /**
83
+ * Check if Mainline DHT is available.
84
+ *
85
+ * Port of `check_mainline()` from bin/hubert.rs lines 169-182.
86
+ */
87
+ async function checkMainline(): Promise<void> {
88
+ try {
89
+ const dht = await MainlineDhtKv.create();
90
+ await dht.destroy();
91
+ console.log("✓ Mainline DHT is available");
92
+ } catch (e) {
93
+ throw new Error(`✗ Mainline DHT is not available: ${e instanceof Error ? e.message : e}`);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Check if IPFS is available.
99
+ *
100
+ * Port of `check_ipfs()` from bin/hubert.rs lines 184-205.
101
+ */
102
+ async function checkIpfs(port: number): Promise<void> {
103
+ try {
104
+ const url = `http://127.0.0.1:${port}/api/v0/version`;
105
+ const controller = new AbortController();
106
+ const timeout = setTimeout(() => controller.abort(), 2000);
107
+
108
+ const response = await fetch(url, {
109
+ method: "POST",
110
+ signal: controller.signal,
111
+ });
112
+
113
+ clearTimeout(timeout);
114
+
115
+ if (response.ok) {
116
+ console.log(`✓ IPFS is available at 127.0.0.1:${port}`);
117
+ } else {
118
+ throw new Error(`✗ IPFS daemon returned error: ${response.status}`);
119
+ }
120
+ } catch (e) {
121
+ if (e instanceof Error && e.name === "AbortError") {
122
+ throw new Error(`✗ IPFS is not available at 127.0.0.1:${port}: connection timeout`);
123
+ }
124
+ throw new Error(
125
+ `✗ IPFS is not available at 127.0.0.1:${port}: ${e instanceof Error ? e.message : e}`,
126
+ );
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Check if Hubert server is available.
132
+ */
133
+ async function checkServer(host: string, port: number): Promise<void> {
134
+ try {
135
+ const url = `http://${host}:${port}/health`;
136
+ const controller = new AbortController();
137
+ const timeout = setTimeout(() => controller.abort(), 2000);
138
+
139
+ const response = await fetch(url, {
140
+ method: "GET",
141
+ signal: controller.signal,
142
+ });
143
+
144
+ clearTimeout(timeout);
145
+
146
+ if (response.ok) {
147
+ const json = (await response.json()) as { server?: string; version?: string };
148
+ if (json.server === "hubert") {
149
+ const version = json.version ?? "unknown";
150
+ console.log(`✓ Hubert server is available at ${host}:${port} (version ${version})`);
151
+ } else {
152
+ throw new Error(`✗ Server at ${host}:${port} is not a Hubert server`);
153
+ }
154
+ } else {
155
+ throw new Error(`✗ Server at ${host}:${port} is not available (status: ${response.status})`);
156
+ }
157
+ } catch (e) {
158
+ if (e instanceof Error && e.name === "AbortError") {
159
+ throw new Error(`✗ Server is not available at ${host}:${port}: connection timeout`);
160
+ }
161
+ throw new Error(
162
+ `✗ Server is not available at ${host}:${port}: ${e instanceof Error ? e.message : e}`,
163
+ );
164
+ }
165
+ }
166
+
167
+ // =============================================================================
168
+ // Put Functions
169
+ // =============================================================================
170
+
171
+ /**
172
+ * Put envelope to Mainline DHT.
173
+ *
174
+ * Port of `put_mainline()` from bin/hubert.rs lines 207-221.
175
+ */
176
+ async function putMainline(arid: ARID, envelope: Envelope, verbose: boolean): Promise<void> {
177
+ const store = await MainlineDhtKv.create();
178
+ try {
179
+ await store.put(arid, envelope, undefined, verbose);
180
+ if (verbose) {
181
+ verbosePrintln("✓ Stored envelope at ARID");
182
+ }
183
+ } finally {
184
+ await store.destroy();
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Put envelope to IPFS.
190
+ *
191
+ * Port of `put_ipfs()` from bin/hubert.rs lines 223-250.
192
+ */
193
+ async function putIpfs(
194
+ arid: ARID,
195
+ envelope: Envelope,
196
+ port: number,
197
+ pin: boolean,
198
+ verbose: boolean,
199
+ ): Promise<void> {
200
+ const url = `http://127.0.0.1:${port}`;
201
+ const store = new IpfsKv(url).withPinContent(pin);
202
+ const result = await store.put(arid, envelope, undefined, verbose);
203
+
204
+ if (verbose) {
205
+ verbosePrintln("✓ Stored envelope at ARID");
206
+ }
207
+
208
+ // Extract and print CID if pinning was requested
209
+ if (pin) {
210
+ const cidPart = result.split("ipfs://")[1];
211
+ if (cidPart) {
212
+ console.log(`CID: ${cidPart}`);
213
+ }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Put envelope to Hybrid storage.
219
+ *
220
+ * Port of `put_hybrid()` from bin/hubert.rs lines 278-308.
221
+ */
222
+ async function putHybrid(
223
+ arid: ARID,
224
+ envelope: Envelope,
225
+ port: number,
226
+ pin: boolean,
227
+ verbose: boolean,
228
+ ): Promise<void> {
229
+ const url = `http://127.0.0.1:${port}`;
230
+ const store = (await HybridKv.create(url)).withPinContent(pin);
231
+ try {
232
+ const result = await store.put(arid, envelope, undefined, verbose);
233
+
234
+ if (verbose) {
235
+ verbosePrintln("✓ Stored envelope at ARID");
236
+ }
237
+
238
+ // Extract and print CID if pinning was requested and IPFS was used
239
+ if (pin && result.includes("ipfs://")) {
240
+ const cidPart = result.split("ipfs://")[1];
241
+ if (cidPart) {
242
+ console.log(`CID: ${cidPart}`);
243
+ }
244
+ }
245
+ } finally {
246
+ await store.destroy();
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Put envelope to Hubert server.
252
+ *
253
+ * Port of `put_server()` from bin/hubert.rs lines 324-344.
254
+ */
255
+ async function putServer(
256
+ host: string,
257
+ port: number,
258
+ arid: ARID,
259
+ envelope: Envelope,
260
+ ttl: number | undefined,
261
+ verbose: boolean,
262
+ ): Promise<void> {
263
+ const url = `http://${host}:${port}`;
264
+ const store = new ServerKvClient(url);
265
+ await store.put(arid, envelope, ttl, verbose);
266
+ if (verbose) {
267
+ verbosePrintln("✓ Stored envelope at ARID");
268
+ }
269
+ }
270
+
271
+ // =============================================================================
272
+ // Get Functions
273
+ // =============================================================================
274
+
275
+ /**
276
+ * Get envelope from Mainline DHT.
277
+ *
278
+ * Port of `get_mainline()` from bin/hubert.rs lines 252-262.
279
+ */
280
+ async function getMainline(
281
+ arid: ARID,
282
+ timeout: number,
283
+ verbose: boolean,
284
+ ): Promise<Envelope | null> {
285
+ const store = await MainlineDhtKv.create();
286
+ try {
287
+ return await store.get(arid, timeout, verbose);
288
+ } finally {
289
+ await store.destroy();
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Get envelope from IPFS.
295
+ *
296
+ * Port of `get_ipfs()` from bin/hubert.rs lines 264-276.
297
+ */
298
+ async function getIpfs(
299
+ arid: ARID,
300
+ timeout: number,
301
+ port: number,
302
+ verbose: boolean,
303
+ ): Promise<Envelope | null> {
304
+ const url = `http://127.0.0.1:${port}`;
305
+ const store = new IpfsKv(url);
306
+ return await store.get(arid, timeout, verbose);
307
+ }
308
+
309
+ /**
310
+ * Get envelope from Hybrid storage.
311
+ *
312
+ * Port of `get_hybrid()` from bin/hubert.rs lines 310-322.
313
+ */
314
+ async function getHybrid(
315
+ arid: ARID,
316
+ timeout: number,
317
+ port: number,
318
+ verbose: boolean,
319
+ ): Promise<Envelope | null> {
320
+ const url = `http://127.0.0.1:${port}`;
321
+ const store = await HybridKv.create(url);
322
+ try {
323
+ return await store.get(arid, timeout, verbose);
324
+ } finally {
325
+ await store.destroy();
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Get envelope from Hubert server.
331
+ *
332
+ * Port of `get_server()` from bin/hubert.rs lines 346-361.
333
+ */
334
+ async function getServer(
335
+ host: string,
336
+ port: number,
337
+ arid: ARID,
338
+ timeout: number,
339
+ verbose: boolean,
340
+ ): Promise<Envelope | null> {
341
+ const url = `http://${host}:${port}`;
342
+ const store = new ServerKvClient(url);
343
+ return await store.get(arid, timeout, verbose);
344
+ }
345
+
346
+ // =============================================================================
347
+ // CLI Setup
348
+ // =============================================================================
349
+
350
+ const program = new Command();
351
+
352
+ program
353
+ .name("hubert")
354
+ .description("Distributed substrate for multiparty transactions")
355
+ .version("1.0.0-alpha.1")
356
+ .option("-v, --verbose", "Enable verbose logging", false);
357
+
358
+ // Generate command
359
+ const generateCmd = program
360
+ .command("generate")
361
+ .description("Generate a new ARID or example Envelope");
362
+
363
+ generateCmd
364
+ .command("arid")
365
+ .description("Generate a new ARID")
366
+ .action(() => {
367
+ const arid = ARID.new();
368
+ console.log(arid.urString());
369
+ });
370
+
371
+ generateCmd
372
+ .command("envelope <size>")
373
+ .description("Generate a test envelope with random data")
374
+ .action((sizeStr: string) => {
375
+ const size = parseInt(sizeStr, 10);
376
+ if (isNaN(size) || size < 0) {
377
+ console.error("Size must be a positive integer");
378
+ process.exit(1);
379
+ }
380
+ const envelope = generateRandomEnvelope(size);
381
+ console.log(envelope.urString());
382
+ });
383
+
384
+ // Put command
385
+ program
386
+ .command("put <arid> <envelope>")
387
+ .description("Store an envelope at an ARID")
388
+ .option("-s, --storage <backend>", "Storage backend to use", "mainline")
389
+ .option("--host <host>", "Server host (for --storage server)")
390
+ .option("--port <port>", "Port (for --storage server/ipfs/hybrid)")
391
+ .option("--ttl <seconds>", "Time-to-live in seconds (for --storage server)")
392
+ .option("--pin", "Pin content in IPFS (only for --storage ipfs or --storage hybrid)", false)
393
+ .action(
394
+ async (
395
+ aridStr: string,
396
+ envelopeStr: string,
397
+ options: {
398
+ storage: StorageBackend;
399
+ host?: string;
400
+ port?: string;
401
+ ttl?: string;
402
+ pin: boolean;
403
+ },
404
+ ) => {
405
+ const verbose = program.opts()["verbose"] as boolean;
406
+ const storage = options.storage;
407
+ const port = options.port ? parseInt(options.port, 10) : undefined;
408
+ const ttl = options.ttl ? parseInt(options.ttl, 10) : undefined;
409
+
410
+ try {
411
+ // Validate options based on storage backend
412
+ if (storage === "mainline") {
413
+ if (port !== undefined) {
414
+ throw new Error("--port option is not supported for --storage mainline");
415
+ }
416
+ if (options.host !== undefined) {
417
+ throw new Error("--host option is not supported for --storage mainline");
418
+ }
419
+ if (ttl !== undefined) {
420
+ throw new Error("--ttl option is only supported for --storage server");
421
+ }
422
+ if (options.pin) {
423
+ throw new Error(
424
+ "--pin option is only supported for --storage ipfs or --storage hybrid",
425
+ );
426
+ }
427
+ } else if (storage === "ipfs") {
428
+ if (options.host !== undefined) {
429
+ throw new Error(
430
+ "--host option is not supported for --storage ipfs (always uses 127.0.0.1)",
431
+ );
432
+ }
433
+ if (ttl !== undefined) {
434
+ throw new Error("--ttl option is only supported for --storage server");
435
+ }
436
+ } else if (storage === "hybrid") {
437
+ if (options.host !== undefined) {
438
+ throw new Error(
439
+ "--host option is not supported for --storage hybrid (always uses 127.0.0.1)",
440
+ );
441
+ }
442
+ if (ttl !== undefined) {
443
+ throw new Error("--ttl option is only supported for --storage server");
444
+ }
445
+ } else if (storage === "server") {
446
+ if (options.pin) {
447
+ throw new Error(
448
+ "--pin option is only supported for --storage ipfs or --storage hybrid",
449
+ );
450
+ }
451
+ }
452
+
453
+ const arid = parseArid(aridStr);
454
+ const envelope = parseEnvelope(envelopeStr);
455
+
456
+ switch (storage) {
457
+ case "mainline":
458
+ await putMainline(arid, envelope, verbose);
459
+ console.log("Stored in Mainline DHT");
460
+ break;
461
+ case "ipfs":
462
+ await putIpfs(arid, envelope, port ?? 5001, options.pin, verbose);
463
+ console.log("Stored in IPFS");
464
+ break;
465
+ case "hybrid":
466
+ await putHybrid(arid, envelope, port ?? 5001, options.pin, verbose);
467
+ console.log("Stored in Hybrid storage");
468
+ break;
469
+ case "server": {
470
+ const host = options.host ?? "127.0.0.1";
471
+ await putServer(host, port ?? 45678, arid, envelope, ttl, verbose);
472
+ console.log("Stored in server");
473
+ break;
474
+ }
475
+ }
476
+ } catch (e) {
477
+ console.error(e instanceof Error ? e.message : e);
478
+ process.exit(1);
479
+ }
480
+ },
481
+ );
482
+
483
+ // Get command
484
+ program
485
+ .command("get <arid>")
486
+ .description("Retrieve an envelope by ARID")
487
+ .option("-s, --storage <backend>", "Storage backend to use", "mainline")
488
+ .option("--host <host>", "Server host (for --storage server)")
489
+ .option("--port <port>", "Port (for --storage server/ipfs/hybrid)")
490
+ .option("-t, --timeout <seconds>", "Maximum time to wait in seconds", "30")
491
+ .action(
492
+ async (
493
+ aridStr: string,
494
+ options: {
495
+ storage: StorageBackend;
496
+ host?: string;
497
+ port?: string;
498
+ timeout: string;
499
+ },
500
+ ) => {
501
+ const verbose = program.opts()["verbose"] as boolean;
502
+ const storage = options.storage;
503
+ const port = options.port ? parseInt(options.port, 10) : undefined;
504
+ const timeout = parseInt(options.timeout, 10);
505
+
506
+ try {
507
+ // Validate options based on storage backend
508
+ if (storage === "mainline") {
509
+ if (port !== undefined) {
510
+ throw new Error("--port option is not supported for --storage mainline");
511
+ }
512
+ if (options.host !== undefined) {
513
+ throw new Error("--host option is not supported for --storage mainline");
514
+ }
515
+ } else if (storage === "ipfs") {
516
+ if (options.host !== undefined) {
517
+ throw new Error(
518
+ "--host option is not supported for --storage ipfs (always uses 127.0.0.1)",
519
+ );
520
+ }
521
+ } else if (storage === "hybrid") {
522
+ if (options.host !== undefined) {
523
+ throw new Error(
524
+ "--host option is not supported for --storage hybrid (always uses 127.0.0.1)",
525
+ );
526
+ }
527
+ }
528
+
529
+ const arid = parseArid(aridStr);
530
+
531
+ let envelope: Envelope | null;
532
+ switch (storage) {
533
+ case "mainline":
534
+ envelope = await getMainline(arid, timeout, verbose);
535
+ break;
536
+ case "ipfs":
537
+ envelope = await getIpfs(arid, timeout, port ?? 5001, verbose);
538
+ break;
539
+ case "hybrid":
540
+ envelope = await getHybrid(arid, timeout, port ?? 5001, verbose);
541
+ break;
542
+ case "server": {
543
+ const host = options.host ?? "127.0.0.1";
544
+ envelope = await getServer(host, port ?? 45678, arid, timeout, verbose);
545
+ break;
546
+ }
547
+ }
548
+
549
+ if (envelope) {
550
+ console.log(envelope.urString());
551
+ } else {
552
+ throw new Error(`Value not found within ${timeout} seconds`);
553
+ }
554
+ } catch (e) {
555
+ console.error(e instanceof Error ? e.message : e);
556
+ process.exit(1);
557
+ }
558
+ },
559
+ );
560
+
561
+ // Check command
562
+ program
563
+ .command("check")
564
+ .description("Check if storage backend is available")
565
+ .option("-s, --storage <backend>", "Storage backend to use", "mainline")
566
+ .option("--host <host>", "Server host (for --storage server)")
567
+ .option("--port <port>", "Port (for --storage server/ipfs/hybrid)")
568
+ .action(async (options: { storage: StorageBackend; host?: string; port?: string }) => {
569
+ const storage = options.storage;
570
+ const port = options.port ? parseInt(options.port, 10) : undefined;
571
+
572
+ try {
573
+ // Validate options based on storage backend
574
+ if (storage === "mainline") {
575
+ if (port !== undefined) {
576
+ throw new Error("--port option is not supported for --storage mainline");
577
+ }
578
+ if (options.host !== undefined) {
579
+ throw new Error("--host option is not supported for --storage mainline");
580
+ }
581
+ } else if (storage === "ipfs") {
582
+ if (options.host !== undefined) {
583
+ throw new Error(
584
+ "--host option is not supported for --storage ipfs (always uses 127.0.0.1)",
585
+ );
586
+ }
587
+ } else if (storage === "hybrid") {
588
+ if (options.host !== undefined) {
589
+ throw new Error(
590
+ "--host option is not supported for --storage hybrid (always uses 127.0.0.1)",
591
+ );
592
+ }
593
+ }
594
+
595
+ switch (storage) {
596
+ case "mainline":
597
+ await checkMainline();
598
+ break;
599
+ case "ipfs":
600
+ await checkIpfs(port ?? 5001);
601
+ break;
602
+ case "hybrid":
603
+ await checkMainline();
604
+ await checkIpfs(port ?? 5001);
605
+ console.log("✓ Hybrid storage is available (DHT + IPFS)");
606
+ break;
607
+ case "server": {
608
+ const host = options.host ?? "127.0.0.1";
609
+ await checkServer(host, port ?? 45678);
610
+ break;
611
+ }
612
+ }
613
+ } catch (e) {
614
+ console.error(e instanceof Error ? e.message : e);
615
+ process.exit(1);
616
+ }
617
+ });
618
+
619
+ // Server command
620
+ program
621
+ .command("server")
622
+ .description("Start the Hubert HTTP server")
623
+ .option("--port <port>", "Port for the server to listen on", "45678")
624
+ .option("--sqlite <path>", "SQLite database file path for persistent storage")
625
+ .action(async (options: { port: string; sqlite?: string }) => {
626
+ const verbose = program.opts()["verbose"] as boolean;
627
+ const port = parseInt(options.port, 10);
628
+
629
+ try {
630
+ const config: ServerConfig = {
631
+ port,
632
+ maxTtl: 86400, // 24 hours
633
+ verbose,
634
+ };
635
+
636
+ if (options.sqlite) {
637
+ // Use SQLite storage
638
+ let sqlitePath = options.sqlite;
639
+ // Check if it's a directory
640
+ const fs = await import("fs");
641
+ try {
642
+ const stats = fs.statSync(sqlitePath);
643
+ if (stats.isDirectory()) {
644
+ sqlitePath = path.join(sqlitePath, "hubert.sqlite");
645
+ }
646
+ } catch {
647
+ // Path doesn't exist, treat as file path
648
+ }
649
+
650
+ const store = new SqliteKv(sqlitePath);
651
+ const server = Server.newSqlite(config, store);
652
+ console.log(`Starting Hubert server on port ${port} with SQLite storage: ${sqlitePath}`);
653
+ await server.run();
654
+ } else {
655
+ // Use in-memory storage
656
+ const server = Server.newMemory(config);
657
+ console.log(`Starting Hubert server on port ${port} with in-memory storage`);
658
+ await server.run();
659
+ }
660
+ } catch (e) {
661
+ console.error(e instanceof Error ? e.message : e);
662
+ process.exit(1);
663
+ }
664
+ });
665
+
666
+ // Parse and run
667
+ program.parse();