@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-DVyk9gqU.mjs","names":["path"],"sources":["../src/server/error.ts","../src/server/memory-kv.ts","../src/server/sqlite-kv.ts","../src/server/server-kv.ts","../src/server/server.ts","../src/server/kv.ts"],"sourcesContent":["/**\n * Server-specific error types.\n *\n * Port of server/error.rs from hubert-rust.\n *\n * @module\n */\n\nimport { HubertError } from \"../error.js\";\n\n/**\n * Base error class for server errors.\n *\n * @category Server Errors\n */\nexport class ServerError extends HubertError {\n constructor(message: string) {\n super(message);\n this.name = \"ServerError\";\n }\n}\n\n/**\n * General server error.\n *\n * Port of `Error::General(String)` from server/error.rs line 4.\n *\n * @category Server Errors\n */\nexport class ServerGeneralError extends ServerError {\n constructor(message: string) {\n super(`Server error: ${message}`);\n this.name = \"ServerGeneralError\";\n }\n}\n\n/**\n * Network error during server communication.\n *\n * Port of `Error::NetworkError(String)` from server/error.rs line 7.\n *\n * @category Server Errors\n */\nexport class ServerNetworkError extends ServerError {\n constructor(message: string) {\n super(`Network error: ${message}`);\n this.name = \"ServerNetworkError\";\n }\n}\n\n/**\n * Parse error during data handling.\n *\n * Port of `Error::ParseError(String)` from server/error.rs line 10.\n *\n * @category Server Errors\n */\nexport class ServerParseError extends ServerError {\n constructor(message: string) {\n super(`Parse error: ${message}`);\n this.name = \"ServerParseError\";\n }\n}\n\n/**\n * SQLite database error.\n *\n * Port of `Error::Sqlite(e)` from server/error.rs line 19.\n *\n * @category Server Errors\n */\nexport class SqliteError extends ServerError {\n /** The underlying error */\n override readonly cause?: Error;\n\n constructor(message: string, cause?: Error) {\n super(`SQLite error: ${message}`);\n this.name = \"SqliteError\";\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n","/**\n * In-memory key-value store for Gordian Envelopes.\n *\n * Port of server/memory_kv.rs from hubert-rust.\n *\n * @module\n */\n\nimport { type ARID } from \"@bcts/components\";\nimport { type Envelope } from \"@bcts/envelope\";\n\nimport { AlreadyExistsError } from \"../error.js\";\nimport { type KvStore } from \"../kv-store.js\";\nimport { verbosePrintln } from \"../logging.js\";\n\n/**\n * Storage entry with envelope data and optional expiration.\n * @internal\n */\ninterface StorageEntry {\n /** CBOR-encoded envelope data */\n envelopeCbor: Uint8Array;\n /** Expiration timestamp in milliseconds, or undefined for no expiration */\n expiresAt?: number;\n}\n\n/**\n * In-memory key-value store for Gordian Envelopes.\n *\n * Provides volatile storage with TTL support and automatic cleanup of\n * expired entries.\n *\n * Port of `struct MemoryKv` from server/memory_kv.rs lines 14-21.\n *\n * @category Server Backend\n *\n * @example\n * ```typescript\n * const store = new MemoryKv();\n * const arid = ARID.new();\n * const envelope = Envelope.new(\"Hello, Memory!\");\n *\n * await store.put(arid, envelope, 3600); // 1 hour TTL\n * const result = await store.get(arid);\n * ```\n */\nexport class MemoryKv implements KvStore {\n private readonly storage: Map<string, StorageEntry>;\n\n /**\n * Create a new in-memory key-value store.\n *\n * Port of `MemoryKv::new()` from server/memory_kv.rs lines 29-33.\n */\n constructor() {\n this.storage = new Map();\n }\n\n /**\n * Check if an ARID exists and is not expired.\n *\n * Port of `check_exists()` from server/memory_kv.rs lines 36-53.\n *\n * @internal\n */\n private checkExists(arid: ARID): boolean {\n const key = arid.urString();\n const entry = this.storage.get(key);\n\n if (entry) {\n if (entry.expiresAt !== undefined && Date.now() >= entry.expiresAt) {\n // Entry is expired, remove it\n this.storage.delete(key);\n return false;\n }\n return true;\n }\n return false;\n }\n\n /**\n * Store an envelope at the given ARID.\n *\n * Port of `KvStore::put()` implementation from server/memory_kv.rs lines 62-102.\n */\n // eslint-disable-next-line @typescript-eslint/require-await\n async put(\n arid: ARID,\n envelope: Envelope,\n ttlSeconds?: number,\n verbose?: boolean,\n ): Promise<string> {\n const key = arid.urString();\n\n // Check if already exists\n if (this.storage.has(key)) {\n if (verbose) {\n verbosePrintln(`PUT ${key} ALREADY_EXISTS`);\n }\n throw new AlreadyExistsError(key);\n }\n\n const expiresAt = ttlSeconds !== undefined ? Date.now() + ttlSeconds * 1000 : undefined;\n const envelopeCbor = envelope.taggedCborData();\n\n const entry: StorageEntry = { envelopeCbor };\n if (expiresAt !== undefined) {\n entry.expiresAt = expiresAt;\n }\n this.storage.set(key, entry);\n\n if (verbose) {\n const ttlMsg = ttlSeconds !== undefined ? ` (TTL ${ttlSeconds}s)` : \"\";\n verbosePrintln(`PUT ${key}${ttlMsg} OK (Memory)`);\n }\n\n return \"Stored in memory\";\n }\n\n /**\n * Retrieve an envelope for the given ARID.\n *\n * Port of `KvStore::get()` implementation from server/memory_kv.rs lines 104-181.\n */\n async get(arid: ARID, timeoutSeconds?: number, verbose?: boolean): Promise<Envelope | null> {\n const timeout = timeoutSeconds ?? 30;\n const start = Date.now();\n let firstAttempt = true;\n const key = arid.urString();\n\n // Dynamic import to avoid circular dependencies\n const { EnvelopeDecoder } = await import(\"@bcts/envelope\");\n\n while (true) {\n const entry = this.storage.get(key);\n\n if (entry) {\n // Check if expired\n if (entry.expiresAt !== undefined && Date.now() >= entry.expiresAt) {\n // Entry is expired, remove it\n this.storage.delete(key);\n if (verbose) {\n verbosePrintln(`GET ${key} EXPIRED`);\n }\n return null;\n }\n\n // Parse CBOR bytes back to Envelope\n try {\n const envelope = EnvelopeDecoder.tryFromCborData(entry.envelopeCbor);\n if (verbose) {\n verbosePrintln(`GET ${key} OK (Memory)`);\n }\n return envelope;\n } catch {\n // If parsing fails, treat as not found\n return null;\n }\n }\n\n // Not found yet\n const elapsed = (Date.now() - start) / 1000;\n if (elapsed >= timeout) {\n if (verbose) {\n verbosePrintln(`GET ${key} NOT_FOUND (timeout after ${timeout}s)`);\n }\n return null;\n }\n\n if (firstAttempt && verbose) {\n verbosePrintln(`Polling for ${key} (timeout: ${timeout}s)`);\n firstAttempt = false;\n } else if (verbose) {\n process.stdout.write(\".\");\n }\n\n // Wait 500ms before polling again\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n }\n\n /**\n * Check if an envelope exists at the given ARID.\n *\n * Port of `KvStore::exists()` implementation from server/memory_kv.rs lines 183-186.\n */\n // eslint-disable-next-line @typescript-eslint/require-await\n async exists(arid: ARID): Promise<boolean> {\n return this.checkExists(arid);\n }\n}\n","/**\n * SQLite-backed key-value store for Gordian Envelopes.\n *\n * Port of server/sqlite_kv.rs from hubert-rust.\n *\n * @module\n */\n\nimport Database from \"better-sqlite3\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\nimport { type ARID } from \"@bcts/components\";\nimport { type Envelope } from \"@bcts/envelope\";\n\nimport { AlreadyExistsError } from \"../error.js\";\nimport { type KvStore } from \"../kv-store.js\";\nimport { verbosePrintln } from \"../logging.js\";\nimport { SqliteError } from \"./error.js\";\n\n/**\n * SQLite-backed key-value store for Gordian Envelopes.\n *\n * Provides persistent storage with TTL support and automatic cleanup of\n * expired entries.\n *\n * Port of `struct SqliteKv` from server/sqlite_kv.rs lines 16-24.\n *\n * @category Server Backend\n *\n * @example\n * ```typescript\n * const store = new SqliteKv(\"./hubert.db\");\n * const arid = ARID.new();\n * const envelope = Envelope.new(\"Hello, SQLite!\");\n *\n * await store.put(arid, envelope, 3600); // 1 hour TTL\n * const result = await store.get(arid);\n *\n * // Cleanup when done\n * store.close();\n * ```\n */\nexport class SqliteKv implements KvStore {\n private readonly db: Database.Database;\n private readonly dbPath: string;\n private cleanupInterval: ReturnType<typeof setInterval> | null = null;\n\n /**\n * Create a new SQLite-backed key-value store.\n *\n * Port of `SqliteKv::new()` from server/sqlite_kv.rs lines 26-67.\n *\n * @param dbPath - Path to the SQLite database file. Will be created if it doesn't exist.\n * @throws {SqliteError} If database initialization fails\n */\n constructor(dbPath: string) {\n this.dbPath = dbPath;\n\n // Create parent directory if it doesn't exist\n const parentDir = path.dirname(dbPath);\n if (parentDir && parentDir !== \".\" && !fs.existsSync(parentDir)) {\n fs.mkdirSync(parentDir, { recursive: true });\n }\n\n try {\n this.db = new Database(dbPath);\n\n // Create table if it doesn't exist\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS hubert_store (\n arid TEXT PRIMARY KEY,\n envelope TEXT NOT NULL,\n expires_at INTEGER\n );\n CREATE INDEX IF NOT EXISTS idx_expires_at ON hubert_store(expires_at);\n `);\n } catch (error) {\n throw new SqliteError(\n `Failed to initialize database: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined,\n );\n }\n\n // Start background cleanup task\n this.startCleanupTask();\n }\n\n /**\n * Start a background task that prunes expired entries every minute.\n *\n * Port of `start_cleanup_task()` from server/sqlite_kv.rs lines 70-126.\n *\n * @internal\n */\n private startCleanupTask(): void {\n this.cleanupInterval = setInterval(() => {\n const now = Math.floor(Date.now() / 1000);\n\n try {\n // First collect the ARIDs that will be deleted\n const selectStmt = this.db.prepare(\n \"SELECT arid FROM hubert_store WHERE expires_at IS NOT NULL AND expires_at <= ?\",\n );\n const rows = selectStmt.all(now) as { arid: string }[];\n const arids = rows.map((row) => row.arid);\n\n if (arids.length > 0) {\n // Now delete them\n const deleteStmt = this.db.prepare(\n \"DELETE FROM hubert_store WHERE expires_at IS NOT NULL AND expires_at <= ?\",\n );\n deleteStmt.run(now);\n\n const count = arids.length;\n const aridList = arids.join(\" \");\n verbosePrintln(\n `Pruned ${count} expired ${count === 1 ? \"entry\" : \"entries\"}: ${aridList}`,\n );\n }\n } catch {\n // Silently ignore cleanup errors\n }\n }, 60 * 1000); // Every 60 seconds\n }\n\n /**\n * Check if an ARID exists and is not expired.\n *\n * Port of `check_exists()` from server/sqlite_kv.rs lines 129-170.\n *\n * @internal\n */\n private checkExists(arid: ARID): boolean {\n const aridStr = arid.urString();\n const now = Math.floor(Date.now() / 1000);\n\n try {\n const stmt = this.db.prepare(\"SELECT expires_at FROM hubert_store WHERE arid = ?\");\n const row = stmt.get(aridStr) as { expires_at: number | null } | undefined;\n\n if (row) {\n // Check if expired\n if (row.expires_at !== null && now >= row.expires_at) {\n // Entry is expired, remove it\n const deleteStmt = this.db.prepare(\"DELETE FROM hubert_store WHERE arid = ?\");\n deleteStmt.run(aridStr);\n return false;\n }\n return true;\n }\n return false;\n } catch {\n return false;\n }\n }\n\n /**\n * Store an envelope at the given ARID.\n *\n * Port of `KvStore::put()` implementation from server/sqlite_kv.rs lines 175-236.\n */\n // eslint-disable-next-line @typescript-eslint/require-await\n async put(\n arid: ARID,\n envelope: Envelope,\n ttlSeconds?: number,\n verbose?: boolean,\n ): Promise<string> {\n // Check if already exists\n if (this.checkExists(arid)) {\n if (verbose) {\n verbosePrintln(`PUT ${arid.urString()} ALREADY_EXISTS`);\n }\n throw new AlreadyExistsError(arid.urString());\n }\n\n const aridStr = arid.urString();\n const envelopeStr = envelope.urString();\n\n const expiresAt = ttlSeconds !== undefined ? Math.floor(Date.now() / 1000) + ttlSeconds : null;\n\n try {\n const stmt = this.db.prepare(\n \"INSERT INTO hubert_store (arid, envelope, expires_at) VALUES (?, ?, ?)\",\n );\n stmt.run(aridStr, envelopeStr, expiresAt);\n\n if (verbose) {\n const ttlMsg = ttlSeconds !== undefined ? ` (TTL ${ttlSeconds}s)` : \"\";\n verbosePrintln(`PUT ${aridStr}${ttlMsg} OK (SQLite: ${this.dbPath})`);\n }\n\n return `Stored in SQLite: ${this.dbPath}`;\n } catch (error) {\n throw new SqliteError(\n `Failed to insert: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined,\n );\n }\n }\n\n /**\n * Retrieve an envelope for the given ARID.\n *\n * Port of `KvStore::get()` implementation from server/sqlite_kv.rs lines 238-354.\n */\n async get(arid: ARID, timeoutSeconds?: number, verbose?: boolean): Promise<Envelope | null> {\n const timeout = timeoutSeconds ?? 30;\n const start = Date.now();\n let firstAttempt = true;\n const aridStr = arid.urString();\n\n // Dynamic import to avoid circular dependencies\n const { Envelope } = await import(\"@bcts/envelope\");\n\n while (true) {\n const now = Math.floor(Date.now() / 1000);\n\n try {\n const stmt = this.db.prepare(\n \"SELECT envelope, expires_at FROM hubert_store WHERE arid = ?\",\n );\n const row = stmt.get(aridStr) as\n | { envelope: string; expires_at: number | null }\n | undefined;\n\n if (row) {\n // Check if expired\n if (row.expires_at !== null && now >= row.expires_at) {\n // Entry is expired, remove it\n const deleteStmt = this.db.prepare(\"DELETE FROM hubert_store WHERE arid = ?\");\n deleteStmt.run(aridStr);\n\n if (verbose) {\n verbosePrintln(`GET ${aridStr} EXPIRED`);\n }\n return null;\n }\n\n // Entry found and not expired\n const envelope = Envelope.fromUrString(row.envelope);\n\n if (verbose) {\n verbosePrintln(`GET ${aridStr} OK (SQLite: ${this.dbPath})`);\n }\n\n return envelope;\n }\n } catch {\n // Query failed, treat as not found\n }\n\n // Not found yet\n const elapsed = (Date.now() - start) / 1000;\n if (elapsed >= timeout) {\n if (verbose) {\n verbosePrintln(`GET ${aridStr} NOT_FOUND (timeout after ${timeout}s)`);\n }\n return null;\n }\n\n if (firstAttempt && verbose) {\n verbosePrintln(`Polling for ${aridStr} (timeout: ${timeout}s)`);\n firstAttempt = false;\n } else if (verbose) {\n process.stdout.write(\".\");\n }\n\n // Wait 500ms before polling again\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n }\n\n /**\n * Check if an envelope exists at the given ARID.\n *\n * Port of `KvStore::exists()` implementation from server/sqlite_kv.rs lines 356-359.\n */\n // eslint-disable-next-line @typescript-eslint/require-await\n async exists(arid: ARID): Promise<boolean> {\n return this.checkExists(arid);\n }\n\n /**\n * Close the database connection and stop the cleanup task.\n */\n close(): void {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = null;\n }\n this.db.close();\n }\n}\n","/**\n * Server-side key-value storage backend union type.\n *\n * Port of server/server_kv.rs from hubert-rust.\n *\n * @module\n */\n\nimport { type ARID } from \"@bcts/components\";\nimport { type Envelope } from \"@bcts/envelope\";\n\nimport { type KvStore } from \"../kv-store.js\";\nimport { MemoryKv } from \"./memory-kv.js\";\nimport { type SqliteKv } from \"./sqlite-kv.js\";\n\n/**\n * Server-side key-value storage backend.\n *\n * This type allows selecting between in-memory and SQLite storage\n * at server setup time.\n *\n * Port of `enum ServerKv` from server/server_kv.rs lines 7-15.\n *\n * @category Server Backend\n */\nexport type ServerKv = MemoryKv | SqliteKv;\n\n/**\n * Create a new in-memory server KV store.\n *\n * Port of `ServerKv::memory()` from server/server_kv.rs line 19.\n *\n * @returns A new MemoryKv instance\n * @category Server Backend\n */\nexport function createMemoryKv(): MemoryKv {\n return new MemoryKv();\n}\n\n/**\n * Create a new SQLite-backed server KV store.\n *\n * Port of `ServerKv::sqlite()` from server/server_kv.rs line 22.\n *\n * @param store - The SqliteKv instance to use\n * @returns The same SqliteKv instance\n * @category Server Backend\n */\nexport function createSqliteKv(store: SqliteKv): SqliteKv {\n return store;\n}\n\n/**\n * Synchronously put an envelope into the store.\n *\n * This function wraps the async KvStore trait implementation for use\n * in synchronous HTTP handlers.\n *\n * Port of `put_sync()` from server/server_kv.rs lines 27-52.\n *\n * @param store - The KvStore to use\n * @param arid - The ARID to store at\n * @param envelope - The envelope to store\n * @param ttlSeconds - TTL in seconds\n * @returns Promise that resolves when storage is complete\n * @internal\n */\nexport async function putSync(\n store: KvStore,\n arid: ARID,\n envelope: Envelope,\n ttlSeconds: number,\n): Promise<void> {\n await store.put(arid, envelope, ttlSeconds, false);\n}\n\n/**\n * Synchronously get an envelope from the store.\n *\n * This function wraps the async KvStore trait implementation for use\n * in synchronous HTTP handlers.\n *\n * Port of `get_sync()` from server/server_kv.rs lines 58-71.\n *\n * @param store - The KvStore to use\n * @param arid - The ARID to retrieve\n * @returns The envelope if found, or null\n * @internal\n */\nexport async function getSync(store: KvStore, arid: ARID): Promise<Envelope | null> {\n return await store.get(arid, 0, false);\n}\n","/**\n * Hubert HTTP server implementation.\n *\n * Port of server/server.rs from hubert-rust.\n *\n * @module\n */\n\nimport Fastify, { type FastifyInstance, type FastifyRequest, type FastifyReply } from \"fastify\";\n\nimport { ARID } from \"@bcts/components\";\nimport { Envelope } from \"@bcts/envelope\";\n\nimport { verbosePrintln } from \"../logging.js\";\nimport { MemoryKv } from \"./memory-kv.js\";\nimport { type ServerKv, getSync, putSync } from \"./server-kv.js\";\nimport { type SqliteKv } from \"./sqlite-kv.js\";\n\n/**\n * Package version for health endpoint.\n */\nconst VERSION = \"1.0.0-alpha.1\";\n\n/**\n * Configuration for the Hubert server.\n *\n * Port of `struct ServerConfig` from server/server.rs lines 19-30.\n *\n * @category Server\n */\nexport interface ServerConfig {\n /** Port to listen on */\n port: number;\n /**\n * Maximum TTL in seconds allowed.\n * If a put() specifies a TTL higher than this, it will be clamped.\n * If put() specifies None, this value will be used.\n * Hubert is intended for coordination, not long-term storage.\n */\n maxTtl: number;\n /** Enable verbose logging with timestamps */\n verbose: boolean;\n}\n\n/**\n * Default server configuration.\n *\n * Port of `impl Default for ServerConfig` from server/server.rs lines 32-40.\n */\nexport const defaultServerConfig: ServerConfig = {\n port: 45678,\n maxTtl: 86400, // 24 hours max (and default)\n verbose: false,\n};\n\n/**\n * Shared server state.\n *\n * Port of `struct ServerState` from server/server.rs lines 43-47.\n *\n * @internal\n */\nclass ServerState {\n storage: ServerKv;\n config: ServerConfig;\n\n constructor(config: ServerConfig, storage: ServerKv) {\n this.storage = storage;\n this.config = config;\n }\n\n /**\n * Put an envelope into storage.\n *\n * Port of `ServerState::put()` from server/server.rs lines 54-101.\n */\n async put(\n arid: ARID,\n envelope: Envelope,\n requestedTtl: number | undefined,\n clientIp: string | undefined,\n ): Promise<void> {\n // Determine effective TTL:\n // - If requested, use it (clamped to maxTtl)\n // - If undefined requested, use maxTtl\n // All entries expire (hubert is for coordination, not long-term storage)\n const maxTtl = this.config.maxTtl;\n let ttl: number;\n if (requestedTtl !== undefined) {\n ttl = requestedTtl > maxTtl ? maxTtl : requestedTtl;\n } else {\n ttl = maxTtl;\n }\n\n try {\n await putSync(this.storage, arid, envelope, ttl);\n\n if (this.config.verbose) {\n const ipStr = clientIp ? `${clientIp}: ` : \"\";\n verbosePrintln(`${ipStr}PUT ${arid.urString()} (TTL ${ttl}s) OK`);\n }\n } catch (error) {\n if (this.config.verbose) {\n const ipStr = clientIp ? `${clientIp}: ` : \"\";\n const errorMsg = error instanceof Error ? error.message : String(error);\n verbosePrintln(`${ipStr}PUT ${arid.urString()} (TTL ${ttl}s) ERROR: ${errorMsg}`);\n }\n throw error;\n }\n }\n\n /**\n * Get an envelope from storage.\n *\n * Port of `ServerState::get()` from server/server.rs lines 103-126.\n */\n async get(arid: ARID, clientIp: string | undefined): Promise<Envelope | null> {\n const result = await getSync(this.storage, arid);\n\n if (this.config.verbose) {\n const ipStr = clientIp ? `${clientIp}: ` : \"\";\n const status = result ? \"OK\" : \"NOT_FOUND\";\n verbosePrintln(`${ipStr}GET ${arid.urString()} ${status}`);\n }\n\n return result;\n }\n}\n\n/**\n * Hubert HTTP server.\n *\n * Port of `struct Server` from server/server.rs lines 128-133.\n *\n * @category Server\n *\n * @example\n * ```typescript\n * const server = Server.newMemory({ port: 8080, maxTtl: 3600, verbose: true });\n * await server.run();\n * ```\n */\nexport class Server {\n private readonly config: ServerConfig;\n private readonly state: ServerState;\n private readonly fastify: FastifyInstance;\n\n /**\n * Create a new server with the given configuration and storage backend.\n *\n * Port of `Server::new()` from server/server.rs lines 135-139.\n */\n constructor(config: ServerConfig, storage: ServerKv) {\n this.config = config;\n this.state = new ServerState(config, storage);\n this.fastify = Fastify({ logger: false });\n this.setupRoutes();\n }\n\n /**\n * Create a new server with in-memory storage.\n *\n * Port of `Server::new_memory()` from server/server.rs lines 142-144.\n */\n static newMemory(config: Partial<ServerConfig> = {}): Server {\n const fullConfig = { ...defaultServerConfig, ...config };\n return new Server(fullConfig, new MemoryKv());\n }\n\n /**\n * Create a new server with SQLite storage.\n *\n * Port of `Server::new_sqlite()` from server/server.rs lines 147-149.\n */\n static newSqlite(config: Partial<ServerConfig> = {}, storage: SqliteKv): Server {\n const fullConfig = { ...defaultServerConfig, ...config };\n return new Server(fullConfig, storage);\n }\n\n /**\n * Setup HTTP routes.\n * @internal\n */\n private setupRoutes(): void {\n // Health check endpoint\n this.fastify.get(\"/health\", this.handleHealth.bind(this));\n\n // PUT endpoint\n this.fastify.post(\"/put\", this.handlePut.bind(this));\n\n // GET endpoint\n this.fastify.post(\"/get\", this.handleGet.bind(this));\n }\n\n /**\n * Handle health check requests.\n *\n * Port of `handle_health()` from server/server.rs lines 179-187.\n */\n // eslint-disable-next-line @typescript-eslint/require-await\n private async handleHealth(_request: FastifyRequest, reply: FastifyReply): Promise<void> {\n reply.send({\n server: \"hubert\",\n version: VERSION,\n status: \"ok\",\n });\n }\n\n /**\n * Handle PUT requests.\n *\n * Port of `handle_put()` from server/server.rs lines 195-238.\n */\n private async handlePut(\n request: FastifyRequest<{ Body: string }>,\n reply: FastifyReply,\n ): Promise<void> {\n try {\n const bodyStr = request.body;\n if (typeof bodyStr !== \"string\") {\n reply.status(400).send(\"Expected text body\");\n return;\n }\n\n const lines = bodyStr.split(\"\\n\");\n if (lines.length < 2) {\n reply.status(400).send(\"Expected at least 2 lines: ur:arid and ur:envelope\");\n return;\n }\n\n // Parse ARID\n let arid: ARID;\n try {\n arid = ARID.fromUrString(lines[0]);\n } catch {\n reply.status(400).send(\"Invalid ur:arid\");\n return;\n }\n\n // Parse Envelope\n let envelope: Envelope;\n try {\n envelope = Envelope.fromUrString(lines[1]);\n } catch {\n reply.status(400).send(\"Invalid ur:envelope\");\n return;\n }\n\n // Parse optional TTL\n let ttl: number | undefined;\n if (lines.length > 2 && lines[2].trim() !== \"\") {\n const parsed = parseInt(lines[2], 10);\n if (isNaN(parsed)) {\n reply.status(400).send(\"Invalid TTL\");\n return;\n }\n ttl = parsed;\n }\n\n // Get client IP\n const clientIp = request.ip;\n\n // Store the envelope\n await this.state.put(arid, envelope, ttl, clientIp);\n\n reply.status(200).send(\"OK\");\n } catch (error) {\n // Check if it's an AlreadyExists error\n if (error instanceof Error && error.name === \"AlreadyExistsError\") {\n reply.status(409).send(error.message);\n } else {\n reply.status(500).send(error instanceof Error ? error.message : \"Internal server error\");\n }\n }\n }\n\n /**\n * Handle GET requests.\n *\n * Port of `handle_get()` from server/server.rs lines 244-269.\n */\n private async handleGet(\n request: FastifyRequest<{ Body: string }>,\n reply: FastifyReply,\n ): Promise<void> {\n try {\n const bodyStr = request.body;\n if (typeof bodyStr !== \"string\") {\n reply.status(400).send(\"Expected text body\");\n return;\n }\n\n const aridStr = bodyStr.trim();\n if (aridStr === \"\") {\n reply.status(400).send(\"Expected ur:arid\");\n return;\n }\n\n // Parse ARID\n let arid: ARID;\n try {\n arid = ARID.fromUrString(aridStr);\n } catch {\n reply.status(400).send(\"Invalid ur:arid\");\n return;\n }\n\n // Get client IP\n const clientIp = request.ip;\n\n // Retrieve the envelope\n const envelope = await this.state.get(arid, clientIp);\n\n if (envelope) {\n reply.status(200).send(envelope.urString());\n } else {\n reply.status(404).send(\"Not found\");\n }\n } catch (error) {\n reply.status(500).send(error instanceof Error ? error.message : \"Internal server error\");\n }\n }\n\n /**\n * Run the server.\n *\n * Port of `Server::run()` from server/server.rs lines 152-170.\n */\n async run(): Promise<void> {\n const addr = `127.0.0.1:${this.config.port}`;\n\n // Configure Fastify to parse plain text bodies\n this.fastify.addContentTypeParser(\n \"text/plain\",\n { parseAs: \"string\" },\n (_request, payload, done) => {\n done(null, payload);\n },\n );\n\n // Also handle application/octet-stream and no content-type\n this.fastify.addContentTypeParser(\n \"application/octet-stream\",\n { parseAs: \"string\" },\n (_request, payload, done) => {\n done(null, payload);\n },\n );\n\n await this.fastify.listen({ port: this.config.port, host: \"127.0.0.1\" });\n console.log(`✓ Hubert server listening on ${addr}`);\n }\n\n /**\n * Get the port the server is configured to listen on.\n *\n * Port of `Server::port()` from server/server.rs line 173.\n */\n port(): number {\n return this.config.port;\n }\n\n /**\n * Stop the server.\n */\n async close(): Promise<void> {\n await this.fastify.close();\n }\n}\n","/**\n * Server-backed key-value store using HTTP API.\n *\n * Port of server/kv.rs from hubert-rust.\n *\n * @module\n */\n\nimport { type ARID } from \"@bcts/components\";\nimport { Envelope } from \"@bcts/envelope\";\n\nimport { AlreadyExistsError } from \"../error.js\";\nimport { type KvStore } from \"../kv-store.js\";\nimport { verboseNewline, verbosePrintDot, verbosePrintln } from \"../logging.js\";\nimport { ServerGeneralError, ServerNetworkError, ServerParseError } from \"./error.js\";\n\n/**\n * Server-backed key-value store using HTTP API.\n *\n * This implementation communicates with a Hubert server via HTTP POST requests.\n *\n * Port of `struct ServerKvClient` from server/kv.rs lines 6-37.\n *\n * @category Server Backend\n *\n * @example\n * ```typescript\n * const store = new ServerKvClient(\"http://127.0.0.1:45678\");\n * const arid = ARID.new();\n * const envelope = Envelope.new(\"Hello, Server!\");\n *\n * // Put envelope (write-once)\n * await store.put(arid, envelope);\n *\n * // Get envelope with verbose logging\n * const retrieved = await store.get(arid, undefined, true);\n * ```\n */\nexport class ServerKvClient implements KvStore {\n private readonly baseUrl: string;\n\n /**\n * Create a new server KV store client.\n *\n * Port of `ServerKvClient::new()` from server/kv.rs lines 39-46.\n *\n * @param baseUrl - Base URL of the Hubert server (e.g., \"http://127.0.0.1:45678\")\n */\n constructor(baseUrl: string) {\n this.baseUrl = baseUrl;\n }\n\n /**\n * Store an envelope at the given ARID.\n *\n * Port of `KvStore::put()` implementation from server/kv.rs lines 67-122.\n */\n async put(\n arid: ARID,\n envelope: Envelope,\n ttlSeconds?: number,\n verbose?: boolean,\n ): Promise<string> {\n if (verbose) {\n verbosePrintln(\"Starting server put operation\");\n }\n\n // Format body with optional TTL on third line\n let body: string;\n if (ttlSeconds !== undefined) {\n body = `${arid.urString()}\\n${envelope.urString()}\\n${ttlSeconds}`;\n } else {\n body = `${arid.urString()}\\n${envelope.urString()}`;\n }\n\n if (verbose) {\n verbosePrintln(\"Sending PUT request to server\");\n }\n\n try {\n const response = await fetch(`${this.baseUrl}/put`, {\n method: \"POST\",\n body,\n headers: {\n \"Content-Type\": \"text/plain\",\n },\n });\n\n if (response.status === 200) {\n if (verbose) {\n verbosePrintln(\"Server put operation completed\");\n }\n return \"Stored successfully\";\n } else if (response.status === 409) {\n if (verbose) {\n verbosePrintln(\"Server put operation failed\");\n }\n throw new AlreadyExistsError(arid.urString());\n } else {\n if (verbose) {\n verbosePrintln(\"Server put operation failed\");\n }\n const errorMsg = await response.text();\n throw new ServerGeneralError(errorMsg);\n }\n } catch (error) {\n if (error instanceof AlreadyExistsError || error instanceof ServerGeneralError) {\n throw error;\n }\n throw new ServerNetworkError(error instanceof Error ? error.message : String(error));\n }\n }\n\n /**\n * Retrieve an envelope for the given ARID.\n *\n * Port of `KvStore::get()` implementation from server/kv.rs lines 124-212.\n */\n async get(arid: ARID, timeoutSeconds?: number, verbose?: boolean): Promise<Envelope | null> {\n let printedDot = false;\n\n if (verbose) {\n verbosePrintln(\"Starting server get operation\");\n }\n\n const timeout = timeoutSeconds ?? 30; // Default 30 seconds\n const deadline = Date.now() + timeout * 1000;\n // Changed to 1000ms for verbose mode polling\n const pollInterval = 1000;\n\n if (verbose) {\n verbosePrintln(\"Polling server for value\");\n }\n\n while (true) {\n const body = arid.urString();\n\n try {\n const response = await fetch(`${this.baseUrl}/get`, {\n method: \"POST\",\n body,\n headers: {\n \"Content-Type\": \"text/plain\",\n },\n });\n\n if (response.status === 200) {\n if (verbose && printedDot) {\n verboseNewline();\n }\n if (verbose) {\n verbosePrintln(\"Value found on server\");\n }\n\n const envelopeStr = await response.text();\n try {\n const envelope = Envelope.fromUrString(envelopeStr);\n\n if (verbose) {\n verbosePrintln(\"Server get operation completed\");\n }\n\n return envelope;\n } catch (error) {\n throw new ServerParseError(error instanceof Error ? error.message : String(error));\n }\n } else if (response.status === 404) {\n // Not found yet - check if we should keep polling\n if (Date.now() >= deadline) {\n // Timeout reached\n if (verbose && printedDot) {\n verboseNewline();\n }\n if (verbose) {\n verbosePrintln(\"Timeout reached, value not found\");\n }\n return null;\n }\n\n // Print polling dot if verbose\n if (verbose) {\n verbosePrintDot();\n printedDot = true;\n }\n\n // Wait before retrying (now 1000ms)\n await new Promise((resolve) => setTimeout(resolve, pollInterval));\n } else {\n const errorMsg = await response.text();\n throw new ServerGeneralError(errorMsg);\n }\n } catch (error) {\n if (error instanceof ServerGeneralError || error instanceof ServerParseError) {\n throw error;\n }\n throw new ServerNetworkError(error instanceof Error ? error.message : String(error));\n }\n }\n }\n\n /**\n * Check if an envelope exists at the given ARID.\n *\n * Port of `KvStore::exists()` implementation from server/kv.rs lines 214-218.\n */\n async exists(arid: ARID): Promise<boolean> {\n // Use a short timeout for exists check (1 second), no verbose\n const result = await this.get(arid, 1, false);\n return result !== null;\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bcts/hubert",
|
|
3
|
+
"version": "1.0.0-alpha.17",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Hubert - Distributed infrastructure for secure multiparty transactions using Gordian Envelope",
|
|
6
|
+
"license": "BSD-2-Clause-Patent",
|
|
7
|
+
"contributors": [
|
|
8
|
+
{
|
|
9
|
+
"name": "Leonardo Custodio",
|
|
10
|
+
"email": "leonardo.custodio@parity.io",
|
|
11
|
+
"url": "https://github.com/leonardocustodio"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "Karim Jedda",
|
|
15
|
+
"email": "karim@parity.io",
|
|
16
|
+
"url": "https://github.com/KarimJedda"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://bcts.dev",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/leonardocustodio/bcts",
|
|
23
|
+
"directory": "packages/hubert"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/leonardocustodio/bcts/issues"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"hubert": "./dist/bin/hubert.mjs"
|
|
30
|
+
},
|
|
31
|
+
"main": "dist/index.cjs",
|
|
32
|
+
"module": "dist/index.mjs",
|
|
33
|
+
"types": "dist/index.d.mts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.mts",
|
|
37
|
+
"import": "./dist/index.mjs",
|
|
38
|
+
"require": "./dist/index.cjs",
|
|
39
|
+
"default": "./dist/index.mjs"
|
|
40
|
+
},
|
|
41
|
+
"./ipfs": {
|
|
42
|
+
"types": "./dist/ipfs/index.d.mts",
|
|
43
|
+
"import": "./dist/ipfs/index.mjs",
|
|
44
|
+
"require": "./dist/ipfs/index.cjs"
|
|
45
|
+
},
|
|
46
|
+
"./mainline": {
|
|
47
|
+
"types": "./dist/mainline/index.d.mts",
|
|
48
|
+
"import": "./dist/mainline/index.mjs",
|
|
49
|
+
"require": "./dist/mainline/index.cjs"
|
|
50
|
+
},
|
|
51
|
+
"./server": {
|
|
52
|
+
"types": "./dist/server/index.d.mts",
|
|
53
|
+
"import": "./dist/server/index.mjs",
|
|
54
|
+
"require": "./dist/server/index.cjs"
|
|
55
|
+
},
|
|
56
|
+
"./hybrid": {
|
|
57
|
+
"types": "./dist/hybrid/index.d.mts",
|
|
58
|
+
"import": "./dist/hybrid/index.mjs",
|
|
59
|
+
"require": "./dist/hybrid/index.cjs"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"files": [
|
|
63
|
+
"dist",
|
|
64
|
+
"src",
|
|
65
|
+
"README.md"
|
|
66
|
+
],
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "tsdown",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"test:watch": "vitest",
|
|
71
|
+
"lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'",
|
|
72
|
+
"lint:fix": "eslint 'src/**/*.ts' 'tests/**/*.ts' --fix",
|
|
73
|
+
"typecheck": "tsc --noEmit",
|
|
74
|
+
"clean": "rm -rf dist",
|
|
75
|
+
"docs": "typedoc",
|
|
76
|
+
"prepublishOnly": "npm run clean && npm run build && npm test"
|
|
77
|
+
},
|
|
78
|
+
"keywords": [
|
|
79
|
+
"hubert",
|
|
80
|
+
"blockchain-commons",
|
|
81
|
+
"gordian-envelope",
|
|
82
|
+
"distributed-storage",
|
|
83
|
+
"dht",
|
|
84
|
+
"mainline",
|
|
85
|
+
"ipfs",
|
|
86
|
+
"frost",
|
|
87
|
+
"threshold-signatures",
|
|
88
|
+
"multiparty",
|
|
89
|
+
"arid",
|
|
90
|
+
"key-value-store"
|
|
91
|
+
],
|
|
92
|
+
"engines": {
|
|
93
|
+
"node": ">=18.0.0"
|
|
94
|
+
},
|
|
95
|
+
"dependencies": {
|
|
96
|
+
"@bcts/components": "workspace:*",
|
|
97
|
+
"@bcts/crypto": "workspace:*",
|
|
98
|
+
"@bcts/dcbor": "workspace:*",
|
|
99
|
+
"@bcts/envelope": "workspace:*",
|
|
100
|
+
"@bcts/known-values": "workspace:*",
|
|
101
|
+
"@bcts/rand": "workspace:*",
|
|
102
|
+
"@noble/ciphers": "^2.1.1",
|
|
103
|
+
"@noble/curves": "^2.0.1",
|
|
104
|
+
"better-sqlite3": "^12.6.2",
|
|
105
|
+
"bittorrent-dht": "^11.0.7",
|
|
106
|
+
"commander": "^13.1.0",
|
|
107
|
+
"fastify": "^5.7.1",
|
|
108
|
+
"kubo-rpc-client": "^6.0.2"
|
|
109
|
+
},
|
|
110
|
+
"devDependencies": {
|
|
111
|
+
"@bcts/eslint": "workspace:*",
|
|
112
|
+
"@bcts/tsconfig": "workspace:*",
|
|
113
|
+
"@eslint/js": "^9.39.2",
|
|
114
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
115
|
+
"@types/node": "^25.0.10",
|
|
116
|
+
"eslint": "^9.39.2",
|
|
117
|
+
"globals": "^17.1.0",
|
|
118
|
+
"prettier": "^3.8.1",
|
|
119
|
+
"ts-node": "^10.9.2",
|
|
120
|
+
"tsdown": "^0.20.1",
|
|
121
|
+
"typedoc": "^0.28.16",
|
|
122
|
+
"typescript": "^5.9.3",
|
|
123
|
+
"vitest": "^4.0.18"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARID key derivation and obfuscation utilities.
|
|
3
|
+
*
|
|
4
|
+
* Port of arid_derivation.rs from hubert-rust.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type ARID } from "@bcts/components";
|
|
10
|
+
import { hkdfHmacSha256 } from "@bcts/crypto";
|
|
11
|
+
import { chacha20 } from "@noble/ciphers/chacha.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Derive a deterministic key from an ARID using a specific salt.
|
|
15
|
+
*
|
|
16
|
+
* Uses HKDF to derive key material from the ARID, ensuring that:
|
|
17
|
+
* - Same ARID always produces same key for a given salt
|
|
18
|
+
* - Keys are cryptographically derived (not guessable)
|
|
19
|
+
* - Collision resistance inherited from ARID
|
|
20
|
+
* - No identifying information in the key (fully anonymized)
|
|
21
|
+
*
|
|
22
|
+
* Port of `derive_key()` from arid_derivation.rs lines 8-29.
|
|
23
|
+
*
|
|
24
|
+
* @param salt - Domain-specific salt to ensure different backends derive different keys
|
|
25
|
+
* @param arid - The ARID to derive from
|
|
26
|
+
* @param outputLen - Length of output in bytes (typically 20 or 32)
|
|
27
|
+
* @returns Derived key bytes
|
|
28
|
+
* @category ARID Derivation
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const key = deriveKey(new TextEncoder().encode("my-salt"), arid, 32);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function deriveKey(salt: Uint8Array, arid: ARID, outputLen: number): Uint8Array {
|
|
36
|
+
const aridBytes = arid.data();
|
|
37
|
+
// Note: @bcts/crypto hkdfHmacSha256 takes (keyMaterial, salt, keyLen)
|
|
38
|
+
// Rust bc-crypto takes (salt, ikm, keyLen)
|
|
39
|
+
return hkdfHmacSha256(aridBytes, salt, outputLen);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Salt for IPFS IPNS key derivation.
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
const IPFS_KEY_NAME_SALT = new TextEncoder().encode("hubert-ipfs-ipns-v1");
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Salt for Mainline DHT key derivation.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
const MAINLINE_KEY_SALT = new TextEncoder().encode("hubert-mainline-dht-v1");
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Salt for obfuscation key derivation.
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
const OBFUSCATION_SALT = new TextEncoder().encode("hubert-obfuscation-v1");
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Derive an IPNS key name from an ARID.
|
|
62
|
+
*
|
|
63
|
+
* Returns a 64-character hex string suitable for use as an IPFS key name.
|
|
64
|
+
*
|
|
65
|
+
* Port of `derive_ipfs_key_name()` from arid_derivation.rs lines 31-37.
|
|
66
|
+
*
|
|
67
|
+
* @param arid - The ARID to derive from
|
|
68
|
+
* @returns 64-character hex string
|
|
69
|
+
* @category ARID Derivation
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const keyName = deriveIpfsKeyName(arid);
|
|
74
|
+
* // => "a1b2c3d4e5f6..." (64 hex characters)
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function deriveIpfsKeyName(arid: ARID): string {
|
|
78
|
+
const keyBytes = deriveKey(IPFS_KEY_NAME_SALT, arid, 32);
|
|
79
|
+
return bytesToHex(keyBytes);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Derive Mainline DHT key material from an ARID.
|
|
84
|
+
*
|
|
85
|
+
* Returns 20 bytes of key material (SHA-1 compatible length).
|
|
86
|
+
*
|
|
87
|
+
* Port of `derive_mainline_key()` from arid_derivation.rs lines 39-45.
|
|
88
|
+
*
|
|
89
|
+
* @param arid - The ARID to derive from
|
|
90
|
+
* @returns 20 bytes of key material
|
|
91
|
+
* @category ARID Derivation
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const key = deriveMainlineKey(arid);
|
|
96
|
+
* // => Uint8Array(20) [...]
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export function deriveMainlineKey(arid: ARID): Uint8Array {
|
|
100
|
+
return deriveKey(MAINLINE_KEY_SALT, arid, 20);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Obfuscate or deobfuscate data using ChaCha20 with an ARID-derived key.
|
|
105
|
+
*
|
|
106
|
+
* This function uses ChaCha20 as a stream cipher to XOR the data with a
|
|
107
|
+
* keystream derived from the ARID. Since XOR is symmetric, the same function
|
|
108
|
+
* is used for both obfuscation and deobfuscation.
|
|
109
|
+
*
|
|
110
|
+
* The result appears as uniform random data to anyone who doesn't have the
|
|
111
|
+
* ARID, hiding both the structure and content of the reference envelope.
|
|
112
|
+
*
|
|
113
|
+
* Port of `obfuscate_with_arid()` from arid_derivation.rs lines 47-91.
|
|
114
|
+
*
|
|
115
|
+
* @param arid - The ARID used to derive the obfuscation key
|
|
116
|
+
* @param data - The data to obfuscate or deobfuscate
|
|
117
|
+
* @returns The obfuscated (or deobfuscated) data
|
|
118
|
+
* @category ARID Derivation
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* const obfuscated = obfuscateWithArid(arid, plaintext);
|
|
123
|
+
* const deobfuscated = obfuscateWithArid(arid, obfuscated);
|
|
124
|
+
* // deobfuscated === plaintext
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export function obfuscateWithArid(arid: ARID, data: Uint8Array): Uint8Array {
|
|
128
|
+
if (data.length === 0) {
|
|
129
|
+
return new Uint8Array(0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Derive a 32-byte key from the ARID using HKDF with domain-specific salt
|
|
133
|
+
const key = deriveKey(OBFUSCATION_SALT, arid, 32);
|
|
134
|
+
|
|
135
|
+
// Derive IV from the key (last 12 bytes, reversed)
|
|
136
|
+
// Match Rust: key.iter().rev().take(12).copied().collect()
|
|
137
|
+
const iv = new Uint8Array(12);
|
|
138
|
+
for (let i = 0; i < 12; i++) {
|
|
139
|
+
iv[i] = key[31 - i];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Use ChaCha20 to XOR the data
|
|
143
|
+
// @noble/ciphers chacha20 takes (key, nonce, data) and returns encrypted data
|
|
144
|
+
return chacha20(key, iv, data);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Convert bytes to lowercase hex string.
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
function bytesToHex(bytes: Uint8Array): string {
|
|
152
|
+
return Array.from(bytes)
|
|
153
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
154
|
+
.join("");
|
|
155
|
+
}
|