@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.
- package/LICENSE +48 -0
- package/README.md +18 -0
- package/dist/arid-derivation-1CJuU-kZ.cjs +150 -0
- package/dist/arid-derivation-1CJuU-kZ.cjs.map +1 -0
- package/dist/arid-derivation-CbqACjdg.mjs +126 -0
- package/dist/arid-derivation-CbqACjdg.mjs.map +1 -0
- package/dist/bin/hubert.cjs +384 -0
- package/dist/bin/hubert.cjs.map +1 -0
- package/dist/bin/hubert.d.cts +1 -0
- package/dist/bin/hubert.d.mts +1 -0
- package/dist/bin/hubert.mjs +383 -0
- package/dist/bin/hubert.mjs.map +1 -0
- package/dist/chunk-CbDLau6x.cjs +34 -0
- package/dist/hybrid/index.cjs +14 -0
- package/dist/hybrid/index.d.cts +3 -0
- package/dist/hybrid/index.d.mts +3 -0
- package/dist/hybrid/index.mjs +6 -0
- package/dist/hybrid-BZhumygj.mjs +356 -0
- package/dist/hybrid-BZhumygj.mjs.map +1 -0
- package/dist/hybrid-dX5JLumO.cjs +410 -0
- package/dist/hybrid-dX5JLumO.cjs.map +1 -0
- package/dist/index-BEzpUC7r.d.mts +380 -0
- package/dist/index-BEzpUC7r.d.mts.map +1 -0
- package/dist/index-C2F6ugLL.d.mts +210 -0
- package/dist/index-C2F6ugLL.d.mts.map +1 -0
- package/dist/index-CUnDouMb.d.mts +215 -0
- package/dist/index-CUnDouMb.d.mts.map +1 -0
- package/dist/index-CV6lZJqY.d.cts +380 -0
- package/dist/index-CV6lZJqY.d.cts.map +1 -0
- package/dist/index-CY3TCzIm.d.cts +217 -0
- package/dist/index-CY3TCzIm.d.cts.map +1 -0
- package/dist/index-DEr4SR1J.d.cts +215 -0
- package/dist/index-DEr4SR1J.d.cts.map +1 -0
- package/dist/index-T1LHanIb.d.mts +217 -0
- package/dist/index-T1LHanIb.d.mts.map +1 -0
- package/dist/index-jyzuOhFB.d.cts +210 -0
- package/dist/index-jyzuOhFB.d.cts.map +1 -0
- package/dist/index.cjs +60 -0
- package/dist/index.d.cts +161 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +161 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +10 -0
- package/dist/ipfs/index.cjs +13 -0
- package/dist/ipfs/index.d.cts +3 -0
- package/dist/ipfs/index.d.mts +3 -0
- package/dist/ipfs/index.mjs +5 -0
- package/dist/ipfs-BRMMCBjv.mjs +1 -0
- package/dist/ipfs-CetOVQcO.cjs +0 -0
- package/dist/kv-BAmhmMOo.cjs +425 -0
- package/dist/kv-BAmhmMOo.cjs.map +1 -0
- package/dist/kv-C-emxv0w.mjs +375 -0
- package/dist/kv-C-emxv0w.mjs.map +1 -0
- package/dist/kv-DJiKvypY.mjs +403 -0
- package/dist/kv-DJiKvypY.mjs.map +1 -0
- package/dist/kv-store-DmngWWuw.d.mts +183 -0
- package/dist/kv-store-DmngWWuw.d.mts.map +1 -0
- package/dist/kv-store-ww-AUyLd.d.cts +183 -0
- package/dist/kv-store-ww-AUyLd.d.cts.map +1 -0
- package/dist/kv-yjvQa_LH.cjs +457 -0
- package/dist/kv-yjvQa_LH.cjs.map +1 -0
- package/dist/logging-hmzNzifq.mjs +158 -0
- package/dist/logging-hmzNzifq.mjs.map +1 -0
- package/dist/logging-qc9uMgil.cjs +212 -0
- package/dist/logging-qc9uMgil.cjs.map +1 -0
- package/dist/mainline/index.cjs +12 -0
- package/dist/mainline/index.d.cts +3 -0
- package/dist/mainline/index.d.mts +3 -0
- package/dist/mainline/index.mjs +5 -0
- package/dist/mainline-D_jfeFMh.cjs +0 -0
- package/dist/mainline-cFIuXbo-.mjs +1 -0
- package/dist/server/index.cjs +14 -0
- package/dist/server/index.d.cts +3 -0
- package/dist/server/index.d.mts +3 -0
- package/dist/server/index.mjs +3 -0
- package/dist/server-BBNRZ30D.cjs +912 -0
- package/dist/server-BBNRZ30D.cjs.map +1 -0
- package/dist/server-DVyk9gqU.mjs +836 -0
- package/dist/server-DVyk9gqU.mjs.map +1 -0
- package/package.json +125 -0
- package/src/arid-derivation.ts +155 -0
- package/src/bin/hubert.ts +667 -0
- package/src/error.ts +89 -0
- package/src/hybrid/error.ts +77 -0
- package/src/hybrid/index.ts +24 -0
- package/src/hybrid/kv.ts +236 -0
- package/src/hybrid/reference.ts +176 -0
- package/src/index.ts +145 -0
- package/src/ipfs/error.ts +83 -0
- package/src/ipfs/index.ts +24 -0
- package/src/ipfs/kv.ts +476 -0
- package/src/ipfs/value.ts +85 -0
- package/src/kv-store.ts +128 -0
- package/src/logging.ts +88 -0
- package/src/mainline/error.ts +108 -0
- package/src/mainline/index.ts +23 -0
- package/src/mainline/kv.ts +411 -0
- package/src/server/error.ts +83 -0
- package/src/server/index.ts +29 -0
- package/src/server/kv.ts +211 -0
- package/src/server/memory-kv.ts +191 -0
- package/src/server/server-kv.ts +92 -0
- package/src/server/server.ts +369 -0
- 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
|
+
}
|