@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
package/src/logging.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Logging utilities for verbose output with timestamps.
3
+ *
4
+ * Port of logging.rs from hubert-rust.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ /**
10
+ * Format a timestamp in ISO-8601 Zulu format with fractional seconds.
11
+ *
12
+ * Port of `timestamp()` from logging.rs lines 6-71.
13
+ *
14
+ * @returns Timestamp string in format "YYYY-MM-DDTHH:MM:SS.mmmZ"
15
+ * @category Logging
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * timestamp() // => "2024-01-15T14:30:45.123Z"
20
+ * ```
21
+ */
22
+ export function timestamp(): string {
23
+ const now = new Date();
24
+
25
+ const year = now.getUTCFullYear();
26
+ const month = String(now.getUTCMonth() + 1).padStart(2, "0");
27
+ const day = String(now.getUTCDate()).padStart(2, "0");
28
+ const hours = String(now.getUTCHours()).padStart(2, "0");
29
+ const minutes = String(now.getUTCMinutes()).padStart(2, "0");
30
+ const seconds = String(now.getUTCSeconds()).padStart(2, "0");
31
+ const millis = String(now.getUTCMilliseconds()).padStart(3, "0");
32
+
33
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${millis}Z`;
34
+ }
35
+
36
+ /**
37
+ * Print a verbose message with timestamp prefix.
38
+ *
39
+ * Port of `verbose_println()` from logging.rs lines 74-78.
40
+ *
41
+ * @param message - The message to print
42
+ * @category Logging
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * verbosePrintln("Starting operation...");
47
+ * // Output: [2024-01-15T14:30:45.123Z] Starting operation...
48
+ * ```
49
+ */
50
+ export function verbosePrintln(message: string): void {
51
+ if (message.length > 0) {
52
+ console.log(`[${timestamp()}] ${message}`);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Print a polling dot on the same line (no newline).
58
+ *
59
+ * Port of `verbose_print_dot()` from logging.rs lines 81-84.
60
+ *
61
+ * @category Logging
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * verbosePrintDot(); // Prints "." without newline
66
+ * ```
67
+ */
68
+ export function verbosePrintDot(): void {
69
+ process.stdout.write(".");
70
+ }
71
+
72
+ /**
73
+ * Print a newline after dots.
74
+ *
75
+ * Port of `verbose_newline()` from logging.rs lines 87-89.
76
+ *
77
+ * @category Logging
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * verbosePrintDot();
82
+ * verbosePrintDot();
83
+ * verboseNewline(); // Completes the line of dots
84
+ * ```
85
+ */
86
+ export function verboseNewline(): void {
87
+ console.log();
88
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Mainline DHT-specific errors.
3
+ *
4
+ * Port of mainline/error.rs from hubert-rust.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import { HubertError } from "../error.js";
10
+
11
+ /**
12
+ * Base class for Mainline DHT-specific errors.
13
+ *
14
+ * @category Mainline
15
+ */
16
+ export class MainlineError extends HubertError {
17
+ constructor(message: string) {
18
+ super(message);
19
+ this.name = "MainlineError";
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Value size exceeds DHT limit.
25
+ *
26
+ * Port of `Error::ValueTooLarge { size }` from mainline/error.rs line 4-5.
27
+ *
28
+ * @category Mainline
29
+ */
30
+ export class ValueTooLargeError extends MainlineError {
31
+ readonly size: number;
32
+
33
+ constructor(size: number) {
34
+ super(`Value size ${size} exceeds DHT limit of 1000 bytes`);
35
+ this.name = "ValueTooLargeError";
36
+ this.size = size;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * DHT operation error.
42
+ *
43
+ * Port of `Error::DhtError` from mainline/error.rs line 7-8.
44
+ *
45
+ * @category Mainline
46
+ */
47
+ export class DhtError extends MainlineError {
48
+ constructor(message: string) {
49
+ super(`DHT operation error: ${message}`);
50
+ this.name = "DhtError";
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Put query error.
56
+ *
57
+ * Port of `Error::PutQueryError` from mainline/error.rs line 10-11.
58
+ *
59
+ * @category Mainline
60
+ */
61
+ export class PutQueryError extends MainlineError {
62
+ constructor(message: string) {
63
+ super(`Put query error: ${message}`);
64
+ this.name = "PutQueryError";
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Decode ID error.
70
+ *
71
+ * Port of `Error::DecodeIdError` from mainline/error.rs line 13-14.
72
+ *
73
+ * @category Mainline
74
+ */
75
+ export class DecodeIdError extends MainlineError {
76
+ constructor(message: string) {
77
+ super(`Decode ID error: ${message}`);
78
+ this.name = "DecodeIdError";
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Put mutable error.
84
+ *
85
+ * Port of `Error::PutMutableError` from mainline/error.rs line 16-17.
86
+ *
87
+ * @category Mainline
88
+ */
89
+ export class PutMutableError extends MainlineError {
90
+ constructor(message: string) {
91
+ super(`Put mutable error: ${message}`);
92
+ this.name = "PutMutableError";
93
+ }
94
+ }
95
+
96
+ /**
97
+ * I/O error.
98
+ *
99
+ * Port of `Error::Io` from mainline/error.rs line 19-20.
100
+ *
101
+ * @category Mainline
102
+ */
103
+ export class MainlineIoError extends MainlineError {
104
+ constructor(message: string) {
105
+ super(`IO error: ${message}`);
106
+ this.name = "MainlineIoError";
107
+ }
108
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Mainline DHT module for Hubert distributed storage.
3
+ *
4
+ * This module provides Mainline DHT-backed storage using BEP-44 mutable items.
5
+ *
6
+ * Port of mainline/mod.rs from hubert-rust.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ // Error types
12
+ export {
13
+ MainlineError,
14
+ ValueTooLargeError,
15
+ DhtError,
16
+ PutQueryError,
17
+ DecodeIdError,
18
+ PutMutableError,
19
+ MainlineIoError,
20
+ } from "./error.js";
21
+
22
+ // Mainline DHT KvStore implementation
23
+ export { MainlineDhtKv } from "./kv.js";
@@ -0,0 +1,411 @@
1
+ /**
2
+ * Mainline DHT-backed key-value store using ARID-based addressing.
3
+ *
4
+ * Port of mainline/kv.rs from hubert-rust.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import crypto from "node:crypto";
10
+ import { type ARID } from "@bcts/components";
11
+ import { type Envelope, EnvelopeDecoder } from "@bcts/envelope";
12
+ // @ts-expect-error - bittorrent-dht has no type declarations
13
+ import DHT from "bittorrent-dht";
14
+ import { ed25519 } from "@noble/curves/ed25519.js";
15
+
16
+ import { AlreadyExistsError } from "../error.js";
17
+ import { type KvStore } from "../kv-store.js";
18
+ import { deriveMainlineKey, obfuscateWithArid } from "../arid-derivation.js";
19
+ import { verboseNewline, verbosePrintDot, verbosePrintln } from "../logging.js";
20
+ import { DhtError, PutMutableError, ValueTooLargeError } from "./error.js";
21
+
22
+ /**
23
+ * Mainline DHT-backed key-value store using ARID-based addressing.
24
+ *
25
+ * This implementation uses:
26
+ * - ARID → ed25519 signing key derivation (deterministic)
27
+ * - BEP-44 mutable storage (fixed location based on pubkey)
28
+ * - Mainline DHT (BitTorrent DHT) for decentralized storage
29
+ * - Write-once semantics (seq=1, put fails if already exists)
30
+ * - Maximum value size: 1000 bytes (DHT protocol limit)
31
+ *
32
+ * Port of `struct MainlineDhtKv` from mainline/kv.rs lines 60-64.
33
+ *
34
+ * # Storage Model
35
+ *
36
+ * Uses BEP-44 mutable items where:
37
+ * - Public key derived from ARID (deterministic ed25519)
38
+ * - Sequence number starts at 1 (write-once)
39
+ * - Optional salt for namespace separation
40
+ * - Location fixed by pubkey (not content hash)
41
+ *
42
+ * # Requirements
43
+ *
44
+ * No external daemon required - the DHT client runs embedded.
45
+ *
46
+ * # Size Limits
47
+ *
48
+ * The Mainline DHT has a practical limit of ~1KB per value. For larger
49
+ * envelopes, use `IpfsKv` or `HybridKv` instead.
50
+ *
51
+ * @category Mainline Backend
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * const store = await MainlineDhtKv.create();
56
+ * const arid = ARID.new();
57
+ * const envelope = Envelope.new("Small message");
58
+ *
59
+ * // Put envelope (write-once)
60
+ * await store.put(arid, envelope);
61
+ *
62
+ * // Get envelope with verbose logging
63
+ * const retrieved = await store.get(arid, undefined, true);
64
+ * ```
65
+ */
66
+ export class MainlineDhtKv implements KvStore {
67
+ private readonly dht: DHT;
68
+ private maxValueSize: number;
69
+ private salt: Uint8Array | undefined;
70
+ private _isBootstrapped: boolean;
71
+
72
+ /**
73
+ * Private constructor - use `create()` factory method.
74
+ */
75
+ private constructor(dht: DHT) {
76
+ this.dht = dht;
77
+ this.maxValueSize = 1000; // DHT protocol limit
78
+ this.salt = undefined;
79
+ this._isBootstrapped = false;
80
+ }
81
+
82
+ /**
83
+ * Check if the DHT is bootstrapped.
84
+ */
85
+ get isBootstrapped(): boolean {
86
+ return this._isBootstrapped;
87
+ }
88
+
89
+ /**
90
+ * Create a new Mainline DHT KV store with default settings.
91
+ *
92
+ * Port of `MainlineDhtKv::new()` from mainline/kv.rs lines 68-79.
93
+ */
94
+ static async create(): Promise<MainlineDhtKv> {
95
+ const dht = new DHT();
96
+
97
+ const instance = new MainlineDhtKv(dht);
98
+
99
+ // Wait for bootstrap
100
+ await new Promise<void>((resolve, reject) => {
101
+ const timeout = setTimeout(() => {
102
+ reject(new DhtError("Bootstrap timeout"));
103
+ }, 30000);
104
+
105
+ dht.on("ready", () => {
106
+ clearTimeout(timeout);
107
+ instance._isBootstrapped = true;
108
+ resolve();
109
+ });
110
+
111
+ dht.on("error", (err: Error) => {
112
+ clearTimeout(timeout);
113
+ reject(new DhtError(err.message));
114
+ });
115
+ });
116
+
117
+ return instance;
118
+ }
119
+
120
+ /**
121
+ * Set the maximum value size (default: 1000 bytes).
122
+ *
123
+ * Note: Values larger than ~1KB may not be reliably stored in the DHT.
124
+ *
125
+ * Port of `MainlineDhtKv::with_max_size()` from mainline/kv.rs lines 84-87.
126
+ */
127
+ withMaxSize(size: number): this {
128
+ this.maxValueSize = size;
129
+ return this;
130
+ }
131
+
132
+ /**
133
+ * Set a salt for namespace separation.
134
+ *
135
+ * Different salts will create separate namespaces for the same ARID.
136
+ *
137
+ * Port of `MainlineDhtKv::with_salt()` from mainline/kv.rs lines 92-95.
138
+ */
139
+ withSalt(salt: Uint8Array): this {
140
+ this.salt = salt;
141
+ return this;
142
+ }
143
+
144
+ /**
145
+ * Derive an ed25519 signing key from an ARID.
146
+ *
147
+ * Uses the ARID-derived key material extended to 32 bytes for ed25519.
148
+ *
149
+ * Port of `MainlineDhtKv::derive_signing_key()` from mainline/kv.rs lines 100-112.
150
+ *
151
+ * @internal
152
+ */
153
+ private static deriveSigningKey(arid: ARID): { privateKey: Uint8Array; publicKey: Uint8Array } {
154
+ const keyBytes = deriveMainlineKey(arid);
155
+
156
+ // Extend to 32 bytes if needed (ARID gives us 20, we need 32)
157
+ const seed = new Uint8Array(32);
158
+ seed.set(keyBytes.slice(0, 20));
159
+ // Use simple derivation for remaining 12 bytes
160
+ for (let i = 20; i < 32; i++) {
161
+ seed[i] = (keyBytes[i % 20] * i) & 0xff;
162
+ }
163
+
164
+ // Get public key from private key seed
165
+ const publicKey = ed25519.getPublicKey(seed);
166
+
167
+ return { privateKey: seed, publicKey };
168
+ }
169
+
170
+ /**
171
+ * Get mutable item from DHT.
172
+ *
173
+ * @internal
174
+ */
175
+ private getMutable(publicKey: Uint8Array, salt?: Uint8Array): Promise<Buffer | null> {
176
+ return new Promise((resolve) => {
177
+ const target = this.computeTarget(publicKey, salt);
178
+
179
+ this.dht.get(target, (err: Error | null, res: { v?: Buffer } | null) => {
180
+ if (err || !res?.v) {
181
+ resolve(null);
182
+ } else {
183
+ resolve(res.v);
184
+ }
185
+ });
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Put mutable item to DHT.
191
+ *
192
+ * @internal
193
+ */
194
+ private putMutable(
195
+ privateKey: Uint8Array,
196
+ publicKey: Uint8Array,
197
+ value: Uint8Array,
198
+ seq: number,
199
+ salt?: Uint8Array,
200
+ ): Promise<void> {
201
+ return new Promise((resolve, reject) => {
202
+ const opts: {
203
+ k: Buffer;
204
+ v: Buffer;
205
+ seq: number;
206
+ sign: (buf: Buffer) => Buffer;
207
+ salt?: Buffer;
208
+ } = {
209
+ k: Buffer.from(publicKey),
210
+ v: Buffer.from(value),
211
+ seq,
212
+ sign: (buf: Buffer) => {
213
+ return Buffer.from(ed25519.sign(buf, privateKey));
214
+ },
215
+ };
216
+
217
+ if (salt) {
218
+ opts.salt = Buffer.from(salt);
219
+ }
220
+
221
+ this.dht.put(opts, (err: Error | null) => {
222
+ if (err) {
223
+ reject(new PutMutableError(err.message));
224
+ } else {
225
+ resolve();
226
+ }
227
+ });
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Compute DHT target hash from public key and optional salt.
233
+ *
234
+ * @internal
235
+ */
236
+ private computeTarget(publicKey: Uint8Array, salt?: Uint8Array): Buffer {
237
+ // For BEP-44 mutable items, the target is sha1(publicKey + salt)
238
+ // The bittorrent-dht library handles this internally when using get/put with k/salt
239
+ // But for direct get() calls we need to compute it ourselves
240
+ const hash = crypto.createHash("sha1");
241
+ hash.update(publicKey);
242
+ if (salt) {
243
+ hash.update(salt);
244
+ }
245
+ return hash.digest();
246
+ }
247
+
248
+ /**
249
+ * Store an envelope at the given ARID.
250
+ *
251
+ * Port of `KvStore::put()` implementation from mainline/kv.rs lines 144-220.
252
+ */
253
+ async put(
254
+ arid: ARID,
255
+ envelope: Envelope,
256
+ _ttlSeconds?: number, // Ignored - DHT has no TTL support
257
+ verbose?: boolean,
258
+ ): Promise<string> {
259
+ if (verbose) {
260
+ verbosePrintln("Starting Mainline DHT put operation");
261
+ }
262
+
263
+ // Serialize envelope
264
+ const bytes = envelope.taggedCborData();
265
+
266
+ if (verbose) {
267
+ verbosePrintln(`Envelope size: ${bytes.length} bytes`);
268
+ }
269
+
270
+ // Obfuscate with ARID-derived key so it appears as random data
271
+ const obfuscated = obfuscateWithArid(arid, bytes);
272
+
273
+ // Check size after obfuscation (same size, but check anyway)
274
+ if (obfuscated.length > this.maxValueSize) {
275
+ throw new ValueTooLargeError(obfuscated.length);
276
+ }
277
+
278
+ if (verbose) {
279
+ verbosePrintln("Obfuscated envelope data");
280
+ }
281
+
282
+ // Derive signing key from ARID
283
+ if (verbose) {
284
+ verbosePrintln("Deriving DHT signing key from ARID");
285
+ }
286
+ const { privateKey, publicKey } = MainlineDhtKv.deriveSigningKey(arid);
287
+
288
+ // Check if already exists (write-once semantics)
289
+ if (verbose) {
290
+ verbosePrintln("Checking for existing value (write-once check)");
291
+ }
292
+ const existing = await this.getMutable(publicKey, this.salt);
293
+ if (existing !== null) {
294
+ throw new AlreadyExistsError(arid.urString());
295
+ }
296
+
297
+ // Create mutable item with seq=1 (first write) using obfuscated data
298
+ if (verbose) {
299
+ verbosePrintln("Creating mutable DHT item");
300
+ }
301
+
302
+ // Put to DHT
303
+ if (verbose) {
304
+ verbosePrintln("Putting value to DHT");
305
+ }
306
+ await this.putMutable(privateKey, publicKey, obfuscated, 1, this.salt);
307
+
308
+ if (verbose) {
309
+ verbosePrintln("Mainline DHT put operation completed");
310
+ }
311
+
312
+ return `dht://${Buffer.from(publicKey).toString("hex")}`;
313
+ }
314
+
315
+ /**
316
+ * Retrieve an envelope for the given ARID.
317
+ *
318
+ * Port of `KvStore::get()` implementation from mainline/kv.rs lines 223-303.
319
+ */
320
+ async get(arid: ARID, timeoutSeconds?: number, verbose?: boolean): Promise<Envelope | null> {
321
+ if (verbose) {
322
+ verbosePrintln("Starting Mainline DHT get operation");
323
+ }
324
+
325
+ // Derive public key from ARID
326
+ if (verbose) {
327
+ verbosePrintln("Deriving DHT public key from ARID");
328
+ }
329
+ const { publicKey } = MainlineDhtKv.deriveSigningKey(arid);
330
+
331
+ const timeout = (timeoutSeconds ?? 30) * 1000; // Default 30 seconds
332
+ const deadline = Date.now() + timeout;
333
+ // Changed to 1000ms for verbose mode polling
334
+ const pollInterval = 1000;
335
+
336
+ if (verbose) {
337
+ verbosePrintln("Polling DHT for value");
338
+ }
339
+
340
+ while (true) {
341
+ // Get mutable item
342
+ const item = await this.getMutable(publicKey, this.salt);
343
+
344
+ if (item !== null) {
345
+ if (verbose) {
346
+ verboseNewline();
347
+ verbosePrintln("Value found in DHT");
348
+ }
349
+
350
+ // Deobfuscate the data using ARID-derived key
351
+ const obfuscatedBytes = new Uint8Array(item);
352
+ const deobfuscated = obfuscateWithArid(arid, obfuscatedBytes);
353
+
354
+ if (verbose) {
355
+ verbosePrintln("Deobfuscated envelope data");
356
+ }
357
+
358
+ // Deserialize envelope from deobfuscated data
359
+ const envelope = EnvelopeDecoder.tryFromCborData(deobfuscated);
360
+
361
+ if (verbose) {
362
+ verbosePrintln("Mainline DHT get operation completed");
363
+ }
364
+
365
+ return envelope;
366
+ }
367
+
368
+ // Not found yet - check if we should keep polling
369
+ if (Date.now() >= deadline) {
370
+ // Timeout reached
371
+ if (verbose) {
372
+ verboseNewline();
373
+ verbosePrintln("Timeout reached, value not found");
374
+ }
375
+ return null;
376
+ }
377
+
378
+ // Print polling dot if verbose
379
+ if (verbose) {
380
+ verbosePrintDot();
381
+ }
382
+
383
+ // Wait before retrying (now 1000ms)
384
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Check if an envelope exists at the given ARID.
390
+ *
391
+ * Port of `KvStore::exists()` implementation from mainline/kv.rs lines 306-314.
392
+ */
393
+ async exists(arid: ARID): Promise<boolean> {
394
+ const { publicKey } = MainlineDhtKv.deriveSigningKey(arid);
395
+
396
+ // Check if mutable item exists
397
+ const item = await this.getMutable(publicKey, this.salt);
398
+ return item !== null;
399
+ }
400
+
401
+ /**
402
+ * Destroy the DHT client and release resources.
403
+ */
404
+ destroy(): Promise<void> {
405
+ return new Promise((resolve) => {
406
+ this.dht.destroy(() => {
407
+ resolve();
408
+ });
409
+ });
410
+ }
411
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Server-specific error types.
3
+ *
4
+ * Port of server/error.rs from hubert-rust.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import { HubertError } from "../error.js";
10
+
11
+ /**
12
+ * Base error class for server errors.
13
+ *
14
+ * @category Server Errors
15
+ */
16
+ export class ServerError extends HubertError {
17
+ constructor(message: string) {
18
+ super(message);
19
+ this.name = "ServerError";
20
+ }
21
+ }
22
+
23
+ /**
24
+ * General server error.
25
+ *
26
+ * Port of `Error::General(String)` from server/error.rs line 4.
27
+ *
28
+ * @category Server Errors
29
+ */
30
+ export class ServerGeneralError extends ServerError {
31
+ constructor(message: string) {
32
+ super(`Server error: ${message}`);
33
+ this.name = "ServerGeneralError";
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Network error during server communication.
39
+ *
40
+ * Port of `Error::NetworkError(String)` from server/error.rs line 7.
41
+ *
42
+ * @category Server Errors
43
+ */
44
+ export class ServerNetworkError extends ServerError {
45
+ constructor(message: string) {
46
+ super(`Network error: ${message}`);
47
+ this.name = "ServerNetworkError";
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Parse error during data handling.
53
+ *
54
+ * Port of `Error::ParseError(String)` from server/error.rs line 10.
55
+ *
56
+ * @category Server Errors
57
+ */
58
+ export class ServerParseError extends ServerError {
59
+ constructor(message: string) {
60
+ super(`Parse error: ${message}`);
61
+ this.name = "ServerParseError";
62
+ }
63
+ }
64
+
65
+ /**
66
+ * SQLite database error.
67
+ *
68
+ * Port of `Error::Sqlite(e)` from server/error.rs line 19.
69
+ *
70
+ * @category Server Errors
71
+ */
72
+ export class SqliteError extends ServerError {
73
+ /** The underlying error */
74
+ override readonly cause?: Error;
75
+
76
+ constructor(message: string, cause?: Error) {
77
+ super(`SQLite error: ${message}`);
78
+ this.name = "SqliteError";
79
+ if (cause !== undefined) {
80
+ this.cause = cause;
81
+ }
82
+ }
83
+ }