From 05815be9929c34bb149885d13117b5ba810ad777 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Thu, 16 Apr 2026 02:10:46 -0400
Subject: [PATCH 01/27] Add missing C sources
---
.gitignore | 8 +
src/crypto.c | 8 +
src/p2p.c | 7 +
src/protocol.c | 6 +
src/security.c | 591 +++++++++++++++++++++++++++++++++++++++++++++++++
src/sha2.c | 359 ++++++++++++++++++++++++++++++
src/sha2.h | 17 ++
7 files changed, 996 insertions(+)
create mode 100644 src/crypto.c
create mode 100644 src/p2p.c
create mode 100644 src/protocol.c
create mode 100644 src/security.c
create mode 100644 src/sha2.c
create mode 100644 src/sha2.h
diff --git a/.gitignore b/.gitignore
index 7e1ac32f3..3467785fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,21 +17,29 @@ src/*
!src/cli.h
!src/cli_main.c
!src/constants.h
+!src/crypto.c
!src/datadir.h
!src/errors.c
!src/errors.h
!src/message.c
!src/message.h
+!src/memory.h
!src/noise.h
+!src/p2p.c
!src/peer.c
!src/peer.h
+!src/protocol.c
!src/protocol.h
+!src/random.h
!src/scoring.c
!src/scoring.h
!src/secure_memory.c
!src/secure_memory.h
!src/secure_random.c
!src/secure_random.h
+!src/security.c
+!src/sha2.c
+!src/sha2.h
!src/taproot.c
!src/taproot.h
!src/threads.c
diff --git a/src/crypto.c b/src/crypto.c
new file mode 100644
index 000000000..7cbc59e53
--- /dev/null
+++ b/src/crypto.c
@@ -0,0 +1,8 @@
+/**
+ * Native addon — crypto and Taproot stack (single translation unit).
+ * Order: SHA-256/512 → secure memory/random → Bech32/segwit_addr → BIP340/Taproot.
+ */
+#include "sha2.c"
+#include "security.c"
+#include "../native/sipa/segwit_addr.c"
+#include "taproot.c"
diff --git a/src/p2p.c b/src/p2p.c
new file mode 100644
index 000000000..3838e1d77
--- /dev/null
+++ b/src/p2p.c
@@ -0,0 +1,7 @@
+/**
+ * Native addon — P2P stack: threading primitives, scoring, Noise peer (single translation unit).
+ * Named p2p.c (not peer.c) so this TU can #include peer.c without self-inclusion.
+ */
+#include "threads.c"
+#include "scoring.c"
+#include "peer.c"
diff --git a/src/protocol.c b/src/protocol.c
new file mode 100644
index 000000000..90eb7e221
--- /dev/null
+++ b/src/protocol.c
@@ -0,0 +1,6 @@
+/**
+ * Native addon — protocol layer: errors, validation, Message (single translation unit).
+ */
+#include "errors.c"
+#include "validation.c"
+#include "message.c"
diff --git a/src/security.c b/src/security.c
new file mode 100644
index 000000000..307d7f925
--- /dev/null
+++ b/src/security.c
@@ -0,0 +1,591 @@
+#include "memory.h"
+#include "random.h"
+
+#include
+#include
+#include
+#include
+#include
+
+// Platform-specific includes
+#ifdef _WIN32
+#include
+#include
+#else
+#include
+#include
+#include
+#endif
+
+#ifdef __linux__
+#include
+#endif
+
+#ifdef __APPLE__
+#include
+#endif
+
+// Internal allocation tracking for secure free operations
+typedef struct allocation_record
+{
+ void *ptr;
+ size_t size;
+ struct allocation_record *next;
+} allocation_record_t;
+
+static allocation_record_t *allocations = NULL;
+static int memory_initialized = 0;
+
+// Platform-specific secure zero implementation
+#ifdef _WIN32
+#define SECURE_ZERO_IMPL(ptr, len) SecureZeroMemory((ptr), (len))
+#else
+// Use volatile to prevent compiler optimization
+static void secure_zero_impl(volatile void *ptr, size_t len)
+{
+ volatile uint8_t *p = (volatile uint8_t *)ptr;
+ for (size_t i = 0; i < len; i++)
+ {
+ p[i] = 0;
+ }
+ // Memory barrier to prevent reordering
+ __asm__ __volatile__("" : : "r"(ptr) : "memory");
+}
+#define SECURE_ZERO_IMPL(ptr, len) secure_zero_impl((ptr), (len))
+#endif
+
+FabricError fabric_secure_memory_init(void)
+{
+ if (memory_initialized)
+ {
+ return FABRIC_SUCCESS;
+ }
+
+ allocations = NULL;
+ memory_initialized = 1;
+ return FABRIC_SUCCESS;
+}
+
+void fabric_secure_memory_cleanup(void)
+{
+ if (!memory_initialized)
+ {
+ return;
+ }
+
+ // Clean up any remaining allocations
+ allocation_record_t *current = allocations;
+ while (current)
+ {
+ allocation_record_t *next = current->next;
+
+ // Securely zero the tracked memory
+ SECURE_ZERO_IMPL(current->ptr, current->size);
+ free(current->ptr);
+ free(current);
+
+ current = next;
+ }
+
+ allocations = NULL;
+ memory_initialized = 0;
+}
+
+static void track_allocation(void *ptr, size_t size)
+{
+ if (!ptr || size == 0)
+ return;
+
+ allocation_record_t *record = malloc(sizeof(allocation_record_t));
+ if (!record)
+ return; // Unable to track, but don't fail the allocation
+
+ record->ptr = ptr;
+ record->size = size;
+ record->next = allocations;
+ allocations = record;
+}
+
+static size_t untrack_allocation(void *ptr)
+{
+ if (!ptr)
+ return 0;
+
+ allocation_record_t **current = &allocations;
+ while (*current)
+ {
+ if ((*current)->ptr == ptr)
+ {
+ allocation_record_t *to_remove = *current;
+ size_t size = to_remove->size;
+ *current = to_remove->next;
+ free(to_remove);
+ return size;
+ }
+ current = &(*current)->next;
+ }
+
+ return 0; // Not found
+}
+
+void fabric_secure_zero(volatile void *ptr, size_t len)
+{
+ if (!ptr || len == 0)
+ return;
+
+ SECURE_ZERO_IMPL(ptr, len);
+}
+
+void *fabric_secure_malloc(size_t size)
+{
+ if (size == 0)
+ return NULL;
+
+ if (!memory_initialized)
+ {
+ fabric_secure_memory_init();
+ }
+
+ void *ptr = malloc(size);
+ if (!ptr)
+ return NULL;
+
+ // Zero initialize
+ memset(ptr, 0, size);
+
+ // Track the allocation
+ track_allocation(ptr, size);
+
+ return ptr;
+}
+
+void fabric_secure_free(void *ptr, size_t size)
+{
+ if (!ptr)
+ return;
+
+ // If size is 0, try to get it from tracking
+ if (size == 0)
+ {
+ size = untrack_allocation(ptr);
+ }
+ else
+ {
+ untrack_allocation(ptr);
+ }
+
+ // Securely zero the memory
+ if (size > 0)
+ {
+ SECURE_ZERO_IMPL(ptr, size);
+ }
+
+ free(ptr);
+}
+
+void *fabric_secure_realloc(void *ptr, size_t old_size, size_t new_size)
+{
+ if (new_size == 0)
+ {
+ fabric_secure_free(ptr, old_size);
+ return NULL;
+ }
+
+ if (!ptr)
+ {
+ return fabric_secure_malloc(new_size);
+ }
+
+ void *new_ptr = fabric_secure_malloc(new_size);
+ if (!new_ptr)
+ {
+ return NULL; // Original memory is unchanged
+ }
+
+ // Copy the minimum of old and new sizes
+ size_t copy_size = (old_size < new_size) ? old_size : new_size;
+ memcpy(new_ptr, ptr, copy_size);
+
+ // Securely free the old memory
+ fabric_secure_free(ptr, old_size);
+
+ return new_ptr;
+}
+
+int fabric_secure_memcmp(const void *a, const void *b, size_t len)
+{
+ if (!a || !b)
+ {
+ return (a == b) ? 0 : 1;
+ }
+
+ const uint8_t *pa = (const uint8_t *)a;
+ const uint8_t *pb = (const uint8_t *)b;
+ uint8_t result = 0;
+
+ // Constant-time comparison
+ for (size_t i = 0; i < len; i++)
+ {
+ result |= pa[i] ^ pb[i];
+ }
+
+ return result;
+}
+
+FabricError fabric_secure_memcpy(void *dest, size_t dest_size,
+ const void *src, size_t src_size)
+{
+ FABRIC_CHECK_NULL(dest);
+ FABRIC_CHECK_NULL(src);
+ FABRIC_CHECK_CONDITION(dest_size > 0, FABRIC_ERROR_INVALID_SIZE);
+ FABRIC_CHECK_CONDITION(src_size > 0, FABRIC_ERROR_INVALID_SIZE);
+ FABRIC_CHECK_CONDITION(src_size <= dest_size, FABRIC_ERROR_BUFFER_TOO_SMALL);
+
+ memcpy(dest, src, src_size);
+
+ // Zero remaining bytes if dest is larger
+ if (dest_size > src_size)
+ {
+ memset((uint8_t *)dest + src_size, 0, dest_size - src_size);
+ }
+
+ return FABRIC_SUCCESS;
+}
+
+FabricError fabric_secure_mlock(void *ptr, size_t len)
+{
+ FABRIC_CHECK_NULL(ptr);
+ FABRIC_CHECK_CONDITION(len > 0, FABRIC_ERROR_INVALID_SIZE);
+
+#ifdef _WIN32
+ if (!VirtualLock(ptr, len))
+ {
+ return FABRIC_ERROR_SYSTEM_CALL_FAILED;
+ }
+#else
+ if (mlock(ptr, len) != 0)
+ {
+ return FABRIC_ERROR_SYSTEM_CALL_FAILED;
+ }
+#endif
+
+ return FABRIC_SUCCESS;
+}
+
+FabricError fabric_secure_munlock(void *ptr, size_t len)
+{
+ FABRIC_CHECK_NULL(ptr);
+ FABRIC_CHECK_CONDITION(len > 0, FABRIC_ERROR_INVALID_SIZE);
+
+#ifdef _WIN32
+ if (!VirtualUnlock(ptr, len))
+ {
+ return FABRIC_ERROR_SYSTEM_CALL_FAILED;
+ }
+#else
+ if (munlock(ptr, len) != 0)
+ {
+ return FABRIC_ERROR_SYSTEM_CALL_FAILED;
+ }
+#endif
+
+ return FABRIC_SUCCESS;
+}
+
+char *fabric_secure_strdup(const char *str)
+{
+ if (!str)
+ return NULL;
+
+ size_t len = strlen(str) + 1;
+ char *dup = fabric_secure_malloc(len);
+ if (!dup)
+ return NULL;
+
+ memcpy(dup, str, len);
+ return dup;
+}
+
+size_t fabric_secure_malloc_size(void *ptr)
+{
+ if (!ptr)
+ return 0;
+
+ allocation_record_t *current = allocations;
+ while (current)
+ {
+ if (current->ptr == ptr)
+ {
+ return current->size;
+ }
+ current = current->next;
+ }
+
+ return 0; // Not found
+}
+
+// Secure random internal state
+static int random_initialized = 0;
+static int urandom_fd = -1;
+
+#ifdef _WIN32
+static HCRYPTPROV hCryptProv = 0;
+#endif
+
+FabricError fabric_secure_random_init(void)
+{
+ if (random_initialized)
+ {
+ return FABRIC_SUCCESS;
+ }
+
+#ifdef _WIN32
+ // Windows: Use CryptGenRandom
+ if (!CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
+ {
+ return FABRIC_ERROR_CRYPTO_INIT_FAILED;
+ }
+#else
+ // Unix-like systems: Open /dev/urandom as fallback
+ urandom_fd = open("/dev/urandom", O_RDONLY);
+ if (urandom_fd < 0)
+ {
+ return FABRIC_ERROR_SYSTEM_CALL_FAILED;
+ }
+
+ // Set FD_CLOEXEC to prevent descriptor leaking to child processes
+ int flags = fcntl(urandom_fd, F_GETFD);
+ if (flags >= 0)
+ {
+ fcntl(urandom_fd, F_SETFD, flags | FD_CLOEXEC);
+ }
+#endif
+
+ random_initialized = 1;
+ return FABRIC_SUCCESS;
+}
+
+FabricError fabric_secure_random_bytes(uint8_t *buffer, size_t length)
+{
+ FABRIC_CHECK_NULL(buffer);
+ FABRIC_CHECK_CONDITION(length > 0, FABRIC_ERROR_INVALID_SIZE);
+
+ if (!random_initialized)
+ {
+ FabricError init_result = fabric_secure_random_init();
+ if (init_result != FABRIC_SUCCESS)
+ {
+ return init_result;
+ }
+ }
+
+#ifdef __linux__
+ // Linux: Try getrandom() first (available since kernel 3.17)
+ ssize_t result = getrandom(buffer, length, 0);
+ if (result == (ssize_t)length)
+ {
+ return FABRIC_SUCCESS;
+ }
+
+ // If getrandom() fails, fall back to /dev/urandom
+ if (result < 0 && errno != ENOSYS)
+ {
+ return FABRIC_ERROR_SYSTEM_CALL_FAILED;
+ }
+#endif
+
+#ifdef __APPLE__
+ // macOS: Use Security framework
+ OSStatus status = SecRandomCopyBytes(kSecRandomDefault, length, buffer);
+ if (status == errSecSuccess)
+ {
+ return FABRIC_SUCCESS;
+ }
+
+ // Fall back to /dev/urandom on failure
+#endif
+
+#ifdef _WIN32
+ // Windows: Use CryptGenRandom
+ if (CryptGenRandom(hCryptProv, (DWORD)length, buffer))
+ {
+ return FABRIC_SUCCESS;
+ }
+ return FABRIC_ERROR_CRYPTO_INIT_FAILED;
+#else
+ // Unix fallback: Read from /dev/urandom
+ if (urandom_fd < 0)
+ {
+ return FABRIC_ERROR_SYSTEM_CALL_FAILED;
+ }
+
+ size_t bytes_read = 0;
+ while (bytes_read < length)
+ {
+ ssize_t result = read(urandom_fd, buffer + bytes_read, length - bytes_read);
+ if (result < 0)
+ {
+ if (errno == EINTR)
+ {
+ continue; // Interrupted by signal, retry
+ }
+ return FABRIC_ERROR_SYSTEM_CALL_FAILED;
+ }
+ if (result == 0)
+ {
+ return FABRIC_ERROR_SYSTEM_CALL_FAILED; // Unexpected EOF
+ }
+ bytes_read += (size_t)result;
+ }
+
+ return FABRIC_SUCCESS;
+#endif
+}
+
+FabricError fabric_secure_random_uint32(uint32_t *result)
+{
+ FABRIC_CHECK_NULL(result);
+
+ return fabric_secure_random_bytes((uint8_t *)result, sizeof(uint32_t));
+}
+
+FabricError fabric_secure_random_range(uint32_t max_value, uint32_t *result)
+{
+ FABRIC_CHECK_NULL(result);
+ FABRIC_CHECK_CONDITION(max_value > 0, FABRIC_ERROR_INVALID_ARGUMENT);
+
+ if (max_value == 1)
+ {
+ *result = 0;
+ return FABRIC_SUCCESS;
+ }
+
+ // Use rejection sampling to avoid modulo bias
+ // Calculate the largest multiple of max_value that fits in uint32_t
+ uint32_t threshold = UINT32_MAX - (UINT32_MAX % max_value);
+
+ uint32_t random_value;
+ int attempts = 0;
+ const int max_attempts = 100; // Prevent infinite loops
+
+ do
+ {
+ FabricError rand_result = fabric_secure_random_uint32(&random_value);
+ if (rand_result != FABRIC_SUCCESS)
+ {
+ return rand_result;
+ }
+
+ attempts++;
+ if (attempts > max_attempts)
+ {
+ return FABRIC_ERROR_OPERATION_FAILED;
+ }
+ } while (random_value >= threshold);
+
+ *result = random_value % max_value;
+ return FABRIC_SUCCESS;
+}
+
+void fabric_secure_random_cleanup(void)
+{
+ if (!random_initialized)
+ {
+ return;
+ }
+
+#ifdef _WIN32
+ if (hCryptProv)
+ {
+ CryptReleaseContext(hCryptProv, 0);
+ hCryptProv = 0;
+ }
+#else
+ if (urandom_fd >= 0)
+ {
+ close(urandom_fd);
+ urandom_fd = -1;
+ }
+#endif
+
+ random_initialized = 0;
+}
+
+FabricError fabric_secure_random_test_entropy(void)
+{
+ enum { TEST_BYTES = 1024 };
+ uint8_t test_buffer[TEST_BYTES];
+ const size_t test_size = (size_t)TEST_BYTES;
+
+ // Generate test data
+ FabricError result = fabric_secure_random_bytes(test_buffer, test_size);
+ if (result != FABRIC_SUCCESS)
+ {
+ return result;
+ }
+
+ // Simple entropy tests
+
+ // Test 1: Check for all zeros or all ones
+ int all_zero = 1, all_one = 1;
+ for (size_t i = 0; i < test_size; i++)
+ {
+ if (test_buffer[i] != 0x00)
+ all_zero = 0;
+ if (test_buffer[i] != 0xFF)
+ all_one = 0;
+ }
+
+ if (all_zero || all_one)
+ {
+ return FABRIC_ERROR_CRYPTO_INIT_FAILED;
+ }
+
+ // Test 2: Basic frequency test (should be roughly balanced)
+ int bit_count = 0;
+ for (size_t i = 0; i < test_size; i++)
+ {
+ for (int bit = 0; bit < 8; bit++)
+ {
+ if (test_buffer[i] & (1 << bit))
+ {
+ bit_count++;
+ }
+ }
+ }
+
+ int total_bits = test_size * 8;
+ // Allow 10% deviation from 50%
+ if (bit_count < (total_bits * 0.4) || bit_count > (total_bits * 0.6))
+ {
+ return FABRIC_ERROR_CRYPTO_INIT_FAILED;
+ }
+
+ // Test 3: Check for obvious patterns (consecutive identical bytes)
+ int consecutive_count = 0;
+ int max_consecutive = 0;
+
+ for (size_t i = 1; i < test_size; i++)
+ {
+ if (test_buffer[i] == test_buffer[i - 1])
+ {
+ consecutive_count++;
+ }
+ else
+ {
+ if (consecutive_count > max_consecutive)
+ {
+ max_consecutive = consecutive_count;
+ }
+ consecutive_count = 0;
+ }
+ }
+
+ // Fail if more than 5% of bytes are consecutive duplicates
+ if (max_consecutive > (test_size * 0.05))
+ {
+ return FABRIC_ERROR_CRYPTO_INIT_FAILED;
+ }
+
+ return FABRIC_SUCCESS;
+}
diff --git a/src/sha2.c b/src/sha2.c
new file mode 100644
index 000000000..de2375ce5
--- /dev/null
+++ b/src/sha2.c
@@ -0,0 +1,359 @@
+#include "sha2.h"
+
+#include
+
+typedef struct {
+ uint64_t bitlen;
+ uint32_t state[8];
+ uint8_t data[64];
+ size_t datalen;
+} fabric_sha256_ctx;
+
+typedef struct {
+ uint64_t bitlen[2];
+ uint64_t state[8];
+ uint8_t data[128];
+ size_t datalen;
+} fabric_sha512_ctx;
+
+static const uint32_t K256[64] = {
+ 0x428a2f98U, 0x71374491U, 0xb5c0fbcfU, 0xe9b5dba5U, 0x3956c25bU, 0x59f111f1U, 0x923f82a4U, 0xab1c5ed5U,
+ 0xd807aa98U, 0x12835b01U, 0x243185beU, 0x550c7dc3U, 0x72be5d74U, 0x80deb1feU, 0x9bdc06a7U, 0xc19bf174U,
+ 0xe49b69c1U, 0xefbe4786U, 0x0fc19dc6U, 0x240ca1ccU, 0x2de92c6fU, 0x4a7484aaU, 0x5cb0a9dcU, 0x76f988daU,
+ 0x983e5152U, 0xa831c66dU, 0xb00327c8U, 0xbf597fc7U, 0xc6e00bf3U, 0xd5a79147U, 0x06ca6351U, 0x14292967U,
+ 0x27b70a85U, 0x2e1b2138U, 0x4d2c6dfcU, 0x53380d13U, 0x650a7354U, 0x766a0abbU, 0x81c2c92eU, 0x92722c85U,
+ 0xa2bfe8a1U, 0xa81a664bU, 0xc24b8b70U, 0xc76c51a3U, 0xd192e819U, 0xd6990624U, 0xf40e3585U, 0x106aa070U,
+ 0x19a4c116U, 0x1e376c08U, 0x2748774cU, 0x34b0bcb5U, 0x391c0cb3U, 0x4ed8aa4aU, 0x5b9cca4fU, 0x682e6ff3U,
+ 0x748f82eeU, 0x78a5636fU, 0x84c87814U, 0x8cc70208U, 0x90befffaU, 0xa4506cebU, 0xbef9a3f7U, 0xc67178f2U
+};
+
+static const uint64_t K512[80] = {
+ 0x428a2f98d728ae22ULL, 0x7137449123ef65cdULL, 0xb5c0fbcfec4d3b2fULL, 0xe9b5dba58189dbbcULL,
+ 0x3956c25bf348b538ULL, 0x59f111f1b605d019ULL, 0x923f82a4af194f9bULL, 0xab1c5ed5da6d8118ULL,
+ 0xd807aa98a3030242ULL, 0x12835b0145706fbeULL, 0x243185be4ee4b28cULL, 0x550c7dc3d5ffb4e2ULL,
+ 0x72be5d74f27b896fULL, 0x80deb1fe3b1696b1ULL, 0x9bdc06a725c71235ULL, 0xc19bf174cf692694ULL,
+ 0xe49b69c19ef14ad2ULL, 0xefbe4786384f25e3ULL, 0x0fc19dc68b8cd5b5ULL, 0x240ca1cc77ac9c65ULL,
+ 0x2de92c6f592b0275ULL, 0x4a7484aa6ea6e483ULL, 0x5cb0a9dcbd41fbd4ULL, 0x76f988da831153b5ULL,
+ 0x983e5152ee66dfabULL, 0xa831c66d2db43210ULL, 0xb00327c898fb213fULL, 0xbf597fc7beef0ee4ULL,
+ 0xc6e00bf33da88fc2ULL, 0xd5a79147930aa725ULL, 0x06ca6351e003826fULL, 0x142929670a0e6e70ULL,
+ 0x27b70a8546d22ffcULL, 0x2e1b21385c26c926ULL, 0x4d2c6dfc5ac42aedULL, 0x53380d139d95b3dfULL,
+ 0x650a73548baf63deULL, 0x766a0abb3c77b2a8ULL, 0x81c2c92e47edaee6ULL, 0x92722c851482353bULL,
+ 0xa2bfe8a14cf10364ULL, 0xa81a664bbc423001ULL, 0xc24b8b70d0f89791ULL, 0xc76c51a30654be30ULL,
+ 0xd192e819d6ef5218ULL, 0xd69906245565a910ULL, 0xf40e35855771202aULL, 0x106aa07032bbd1b8ULL,
+ 0x19a4c116b8d2d0c8ULL, 0x1e376c085141ab53ULL, 0x2748774cdf8eeb99ULL, 0x34b0bcb5e19b48a8ULL,
+ 0x391c0cb3c5c95a63ULL, 0x4ed8aa4ae3418acbULL, 0x5b9cca4f7763e373ULL, 0x682e6ff3d6b2b8a3ULL,
+ 0x748f82ee5defb2fcULL, 0x78a5636f43172f60ULL, 0x84c87814a1f0ab72ULL, 0x8cc702081a6439ecULL,
+ 0x90befffa23631e28ULL, 0xa4506cebde82bde9ULL, 0xbef9a3f7b2c67915ULL, 0xc67178f2e372532bULL,
+ 0xca273eceea26619cULL, 0xd186b8c721c0c207ULL, 0xeada7dd6cde0eb1eULL, 0xf57d4f7fee6ed178ULL,
+ 0x06f067aa72176fbaULL, 0x0a637dc5a2c898a6ULL, 0x113f9804bef90daeULL, 0x1b710b35131c471bULL,
+ 0x28db77f523047d84ULL, 0x32caab7b40c72493ULL, 0x3c9ebe0a15c9bebcULL, 0x431d67c49c100d4cULL,
+ 0x4cc5d4becb3e42b6ULL, 0x597f299cfc657e2aULL, 0x5fcb6fab3ad6faecULL, 0x6c44198c4a475817ULL
+};
+
+#define ROTRIGHT32(a, b) (((a) >> (b)) | ((a) << (32 - (b))))
+#define CH32(x, y, z) (((x) & (y)) ^ (~(x) & (z)))
+#define MAJ32(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
+#define EP0_32(x) (ROTRIGHT32((x), 2) ^ ROTRIGHT32((x), 13) ^ ROTRIGHT32((x), 22))
+#define EP1_32(x) (ROTRIGHT32((x), 6) ^ ROTRIGHT32((x), 11) ^ ROTRIGHT32((x), 25))
+#define SIG0_32(x) (ROTRIGHT32((x), 7) ^ ROTRIGHT32((x), 18) ^ ((x) >> 3))
+#define SIG1_32(x) (ROTRIGHT32((x), 17) ^ ROTRIGHT32((x), 19) ^ ((x) >> 10))
+
+#define ROTRIGHT64(a, b) (((a) >> (b)) | ((a) << (64 - (b))))
+#define CH64(x, y, z) (((x) & (y)) ^ (~(x) & (z)))
+#define MAJ64(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
+#define EP0_64(x) (ROTRIGHT64((x), 28) ^ ROTRIGHT64((x), 34) ^ ROTRIGHT64((x), 39))
+#define EP1_64(x) (ROTRIGHT64((x), 14) ^ ROTRIGHT64((x), 18) ^ ROTRIGHT64((x), 41))
+#define SIG0_64(x) (ROTRIGHT64((x), 1) ^ ROTRIGHT64((x), 8) ^ ((x) >> 7))
+#define SIG1_64(x) (ROTRIGHT64((x), 19) ^ ROTRIGHT64((x), 61) ^ ((x) >> 6))
+
+static void fabric_sha256_transform(fabric_sha256_ctx *ctx, const uint8_t data[])
+{
+ uint32_t m[64];
+ uint32_t a, b, c, d, e, f, g, h;
+ uint32_t t1, t2;
+ size_t i, j;
+
+ for (i = 0, j = 0; i < 16; ++i, j += 4) {
+ m[i] = ((uint32_t)data[j] << 24) | ((uint32_t)data[j + 1] << 16) | ((uint32_t)data[j + 2] << 8) | ((uint32_t)data[j + 3]);
+ }
+ for (; i < 64; ++i) {
+ m[i] = SIG1_32(m[i - 2]) + m[i - 7] + SIG0_32(m[i - 15]) + m[i - 16];
+ }
+
+ a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3];
+ e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7];
+
+ for (i = 0; i < 64; ++i) {
+ t1 = h + EP1_32(e) + CH32(e, f, g) + K256[i] + m[i];
+ t2 = EP0_32(a) + MAJ32(a, b, c);
+ h = g; g = f; f = e; e = d + t1;
+ d = c; c = b; b = a; a = t1 + t2;
+ }
+
+ ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d;
+ ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h;
+}
+
+static void fabric_sha256_init(fabric_sha256_ctx *ctx)
+{
+ ctx->datalen = 0;
+ ctx->bitlen = 0;
+ ctx->state[0] = 0x6a09e667U;
+ ctx->state[1] = 0xbb67ae85U;
+ ctx->state[2] = 0x3c6ef372U;
+ ctx->state[3] = 0xa54ff53aU;
+ ctx->state[4] = 0x510e527fU;
+ ctx->state[5] = 0x9b05688cU;
+ ctx->state[6] = 0x1f83d9abU;
+ ctx->state[7] = 0x5be0cd19U;
+}
+
+static void fabric_sha256_update(fabric_sha256_ctx *ctx, const uint8_t *data, size_t len)
+{
+ size_t i;
+ for (i = 0; i < len; ++i) {
+ ctx->data[ctx->datalen++] = data[i];
+ if (ctx->datalen == 64) {
+ fabric_sha256_transform(ctx, ctx->data);
+ ctx->bitlen += 512;
+ ctx->datalen = 0;
+ }
+ }
+}
+
+static void fabric_sha256_final(fabric_sha256_ctx *ctx, uint8_t hash[32])
+{
+ size_t i = ctx->datalen;
+ if (ctx->datalen < 56) {
+ ctx->data[i++] = 0x80;
+ while (i < 56) ctx->data[i++] = 0x00;
+ } else {
+ ctx->data[i++] = 0x80;
+ while (i < 64) ctx->data[i++] = 0x00;
+ fabric_sha256_transform(ctx, ctx->data);
+ memset(ctx->data, 0, 56);
+ }
+
+ ctx->bitlen += ctx->datalen * 8;
+ ctx->data[63] = (uint8_t)(ctx->bitlen);
+ ctx->data[62] = (uint8_t)(ctx->bitlen >> 8);
+ ctx->data[61] = (uint8_t)(ctx->bitlen >> 16);
+ ctx->data[60] = (uint8_t)(ctx->bitlen >> 24);
+ ctx->data[59] = (uint8_t)(ctx->bitlen >> 32);
+ ctx->data[58] = (uint8_t)(ctx->bitlen >> 40);
+ ctx->data[57] = (uint8_t)(ctx->bitlen >> 48);
+ ctx->data[56] = (uint8_t)(ctx->bitlen >> 56);
+ fabric_sha256_transform(ctx, ctx->data);
+
+ for (i = 0; i < 4; ++i) {
+ hash[i] = (uint8_t)((ctx->state[0] >> (24 - i * 8)) & 0xff);
+ hash[i + 4] = (uint8_t)((ctx->state[1] >> (24 - i * 8)) & 0xff);
+ hash[i + 8] = (uint8_t)((ctx->state[2] >> (24 - i * 8)) & 0xff);
+ hash[i + 12] = (uint8_t)((ctx->state[3] >> (24 - i * 8)) & 0xff);
+ hash[i + 16] = (uint8_t)((ctx->state[4] >> (24 - i * 8)) & 0xff);
+ hash[i + 20] = (uint8_t)((ctx->state[5] >> (24 - i * 8)) & 0xff);
+ hash[i + 24] = (uint8_t)((ctx->state[6] >> (24 - i * 8)) & 0xff);
+ hash[i + 28] = (uint8_t)((ctx->state[7] >> (24 - i * 8)) & 0xff);
+ }
+}
+
+static void fabric_sha512_transform(fabric_sha512_ctx *ctx, const uint8_t data[])
+{
+ uint64_t m[80];
+ uint64_t a, b, c, d, e, f, g, h;
+ uint64_t t1, t2;
+ size_t i, j;
+
+ for (i = 0, j = 0; i < 16; ++i, j += 8) {
+ m[i] = ((uint64_t)data[j] << 56) | ((uint64_t)data[j + 1] << 48) | ((uint64_t)data[j + 2] << 40) | ((uint64_t)data[j + 3] << 32) |
+ ((uint64_t)data[j + 4] << 24) | ((uint64_t)data[j + 5] << 16) | ((uint64_t)data[j + 6] << 8) | ((uint64_t)data[j + 7]);
+ }
+ for (; i < 80; ++i) {
+ m[i] = SIG1_64(m[i - 2]) + m[i - 7] + SIG0_64(m[i - 15]) + m[i - 16];
+ }
+
+ a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3];
+ e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7];
+
+ for (i = 0; i < 80; ++i) {
+ t1 = h + EP1_64(e) + CH64(e, f, g) + K512[i] + m[i];
+ t2 = EP0_64(a) + MAJ64(a, b, c);
+ h = g; g = f; f = e; e = d + t1;
+ d = c; c = b; b = a; a = t1 + t2;
+ }
+
+ ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d;
+ ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h;
+}
+
+static void fabric_sha512_init(fabric_sha512_ctx *ctx)
+{
+ ctx->datalen = 0;
+ ctx->bitlen[0] = 0;
+ ctx->bitlen[1] = 0;
+ ctx->state[0] = 0x6a09e667f3bcc908ULL;
+ ctx->state[1] = 0xbb67ae8584caa73bULL;
+ ctx->state[2] = 0x3c6ef372fe94f82bULL;
+ ctx->state[3] = 0xa54ff53a5f1d36f1ULL;
+ ctx->state[4] = 0x510e527fade682d1ULL;
+ ctx->state[5] = 0x9b05688c2b3e6c1fULL;
+ ctx->state[6] = 0x1f83d9abfb41bd6bULL;
+ ctx->state[7] = 0x5be0cd19137e2179ULL;
+}
+
+static void fabric_sha512_update(fabric_sha512_ctx *ctx, const uint8_t *data, size_t len)
+{
+ size_t i;
+ for (i = 0; i < len; ++i) {
+ ctx->data[ctx->datalen++] = data[i];
+ if (ctx->datalen == 128) {
+ fabric_sha512_transform(ctx, ctx->data);
+ if ((ctx->bitlen[0] += 1024) < 1024) ctx->bitlen[1]++;
+ ctx->datalen = 0;
+ }
+ }
+}
+
+static void fabric_sha512_final(fabric_sha512_ctx *ctx, uint8_t hash[64])
+{
+ size_t i = ctx->datalen;
+ if (i < 112) {
+ ctx->data[i++] = 0x80;
+ while (i < 112) ctx->data[i++] = 0x00;
+ } else {
+ ctx->data[i++] = 0x80;
+ while (i < 128) ctx->data[i++] = 0x00;
+ fabric_sha512_transform(ctx, ctx->data);
+ memset(ctx->data, 0, 112);
+ }
+
+ uint64_t low = ctx->bitlen[0] + (uint64_t)(ctx->datalen * 8);
+ uint64_t high = ctx->bitlen[1];
+ if (low < ctx->bitlen[0]) high++;
+
+ ctx->data[127] = (uint8_t)(low);
+ ctx->data[126] = (uint8_t)(low >> 8);
+ ctx->data[125] = (uint8_t)(low >> 16);
+ ctx->data[124] = (uint8_t)(low >> 24);
+ ctx->data[123] = (uint8_t)(low >> 32);
+ ctx->data[122] = (uint8_t)(low >> 40);
+ ctx->data[121] = (uint8_t)(low >> 48);
+ ctx->data[120] = (uint8_t)(low >> 56);
+ ctx->data[119] = (uint8_t)(high);
+ ctx->data[118] = (uint8_t)(high >> 8);
+ ctx->data[117] = (uint8_t)(high >> 16);
+ ctx->data[116] = (uint8_t)(high >> 24);
+ ctx->data[115] = (uint8_t)(high >> 32);
+ ctx->data[114] = (uint8_t)(high >> 40);
+ ctx->data[113] = (uint8_t)(high >> 48);
+ ctx->data[112] = (uint8_t)(high >> 56);
+ fabric_sha512_transform(ctx, ctx->data);
+
+ for (i = 0; i < 8; ++i) {
+ hash[i * 8 + 0] = (uint8_t)((ctx->state[i] >> 56) & 0xff);
+ hash[i * 8 + 1] = (uint8_t)((ctx->state[i] >> 48) & 0xff);
+ hash[i * 8 + 2] = (uint8_t)((ctx->state[i] >> 40) & 0xff);
+ hash[i * 8 + 3] = (uint8_t)((ctx->state[i] >> 32) & 0xff);
+ hash[i * 8 + 4] = (uint8_t)((ctx->state[i] >> 24) & 0xff);
+ hash[i * 8 + 5] = (uint8_t)((ctx->state[i] >> 16) & 0xff);
+ hash[i * 8 + 6] = (uint8_t)((ctx->state[i] >> 8) & 0xff);
+ hash[i * 8 + 7] = (uint8_t)(ctx->state[i] & 0xff);
+ }
+}
+
+int fabric_sha256(const uint8_t *data, size_t len, uint8_t out32[32])
+{
+ if (!data || !out32) return 0;
+ fabric_sha256_ctx ctx;
+ fabric_sha256_init(&ctx);
+ fabric_sha256_update(&ctx, data, len);
+ fabric_sha256_final(&ctx, out32);
+ return 1;
+}
+
+int fabric_sha512(const uint8_t *data, size_t len, uint8_t out64[64])
+{
+ if (!data || !out64) return 0;
+ fabric_sha512_ctx ctx;
+ fabric_sha512_init(&ctx);
+ fabric_sha512_update(&ctx, data, len);
+ fabric_sha512_final(&ctx, out64);
+ return 1;
+}
+
+int fabric_hmac_sha512(const uint8_t *key, size_t key_len,
+ const uint8_t *data, size_t data_len,
+ uint8_t out64[64])
+{
+ uint8_t key_block[128];
+ uint8_t key_hash[64];
+ uint8_t o_key_pad[128];
+ uint8_t i_key_pad[128];
+ uint8_t inner[64];
+ size_t i;
+
+ if (!key || !data || !out64) return 0;
+
+ memset(key_block, 0, sizeof(key_block));
+ if (key_len > sizeof(key_block)) {
+ if (!fabric_sha512(key, key_len, key_hash)) return 0;
+ memcpy(key_block, key_hash, sizeof(key_hash));
+ } else {
+ memcpy(key_block, key, key_len);
+ }
+
+ for (i = 0; i < sizeof(key_block); i++) {
+ o_key_pad[i] = key_block[i] ^ 0x5c;
+ i_key_pad[i] = key_block[i] ^ 0x36;
+ }
+
+ fabric_sha512_ctx ctx;
+ fabric_sha512_init(&ctx);
+ fabric_sha512_update(&ctx, i_key_pad, sizeof(i_key_pad));
+ fabric_sha512_update(&ctx, data, data_len);
+ fabric_sha512_final(&ctx, inner);
+
+ fabric_sha512_init(&ctx);
+ fabric_sha512_update(&ctx, o_key_pad, sizeof(o_key_pad));
+ fabric_sha512_update(&ctx, inner, sizeof(inner));
+ fabric_sha512_final(&ctx, out64);
+ return 1;
+}
+
+int fabric_pbkdf2_hmac_sha512(const uint8_t *password, size_t password_len,
+ const uint8_t *salt, size_t salt_len,
+ uint32_t iterations,
+ uint8_t *out, size_t out_len)
+{
+ uint32_t block_index = 1;
+ size_t generated = 0;
+ uint8_t u[64];
+ uint8_t t[64];
+ uint8_t sbuf[256];
+
+ if (!password || !salt || !out || iterations == 0) return 0;
+ if (salt_len + 4 > sizeof(sbuf)) return 0;
+
+ while (generated < out_len) {
+ memcpy(sbuf, salt, salt_len);
+ sbuf[salt_len + 0] = (uint8_t)((block_index >> 24) & 0xff);
+ sbuf[salt_len + 1] = (uint8_t)((block_index >> 16) & 0xff);
+ sbuf[salt_len + 2] = (uint8_t)((block_index >> 8) & 0xff);
+ sbuf[salt_len + 3] = (uint8_t)(block_index & 0xff);
+
+ if (!fabric_hmac_sha512(password, password_len, sbuf, salt_len + 4, u)) return 0;
+ memcpy(t, u, sizeof(t));
+
+ for (uint32_t i = 1; i < iterations; i++) {
+ if (!fabric_hmac_sha512(password, password_len, u, sizeof(u), u)) return 0;
+ for (size_t j = 0; j < sizeof(t); j++) t[j] ^= u[j];
+ }
+
+ size_t to_copy = (out_len - generated > sizeof(t)) ? sizeof(t) : (out_len - generated);
+ memcpy(out + generated, t, to_copy);
+ generated += to_copy;
+ block_index++;
+ }
+
+ return 1;
+}
diff --git a/src/sha2.h b/src/sha2.h
new file mode 100644
index 000000000..b9f9f64d6
--- /dev/null
+++ b/src/sha2.h
@@ -0,0 +1,17 @@
+#ifndef FABRIC_SHA2_H
+#define FABRIC_SHA2_H
+
+#include
+#include
+
+int fabric_sha256(const uint8_t *data, size_t len, uint8_t out32[32]);
+int fabric_sha512(const uint8_t *data, size_t len, uint8_t out64[64]);
+int fabric_hmac_sha512(const uint8_t *key, size_t key_len,
+ const uint8_t *data, size_t data_len,
+ uint8_t out64[64]);
+int fabric_pbkdf2_hmac_sha512(const uint8_t *password, size_t password_len,
+ const uint8_t *salt, size_t salt_len,
+ uint32_t iterations,
+ uint8_t *out, size_t out_len);
+
+#endif
From 509d886b505598a741efa59b09a80e24c99db187 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Thu, 16 Apr 2026 02:15:21 -0400
Subject: [PATCH 02/27] Update install report
---
reports/install.log | 15 ++++-----------
1 file changed, 4 insertions(+), 11 deletions(-)
diff --git a/reports/install.log b/reports/install.log
index b1d6484fc..ffac104ee 100644
--- a/reports/install.log
+++ b/reports/install.log
@@ -5,19 +5,12 @@ $ npm i
CXX(target) Release/obj.target/fabric/src/binding.o
- CC(target) Release/obj.target/fabric/native/sipa/segwit_addr.o
- CC(target) Release/obj.target/fabric/src/peer.o
- CC(target) Release/obj.target/fabric/src/message.o
- CC(target) Release/obj.target/fabric/src/errors.o
- CC(target) Release/obj.target/fabric/src/threads.o
- CC(target) Release/obj.target/fabric/src/scoring.o
- CC(target) Release/obj.target/fabric/src/validation.o
- CC(target) Release/obj.target/fabric/src/sha2.o
- CC(target) Release/obj.target/fabric/src/security.o
- CC(target) Release/obj.target/fabric/src/taproot.o
+ CC(target) Release/obj.target/fabric/src/crypto.o
+ CC(target) Release/obj.target/fabric/src/protocol.o
+ CC(target) Release/obj.target/fabric/src/p2p.o
SOLINK_MODULE(target) Release/fabric.node
-added 665 packages, and audited 666 packages in 33s
+added 665 packages, and audited 666 packages in 32s
109 packages are looking for funding
run `npm fund` for details
From ca26005a308100c20b3f13a3f0e089f2a24b8298 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Mon, 20 Apr 2026 10:58:04 -0400
Subject: [PATCH 03/27] Add a variety of new Lightning tests
---
functions/bolt12Semantics.js | 150 ++++++++++++++++
functions/fabricPaymentBech32.js | 123 ++++++++++++++
functions/lightningBolt12.js | 101 +++++++++++
package-lock.json | 34 ++--
services/lightning.js | 106 ++++++++++++
tests/bitcoin/transaction.js | 7 +-
tests/bolt12.semantics.unit.js | 54 ++++++
tests/fabric.chain.js | 16 ++
tests/fabricPaymentBech32.unit.js | 85 ++++++++++
tests/lightning/bolt12.classify.unit.js | 83 +++++++++
tests/lightning/lightning.service.unit.js | 197 ++++++++++++++++++++++
types/bitcoin/transaction.js | 39 ++++-
types/chain.js | 16 +-
13 files changed, 986 insertions(+), 25 deletions(-)
create mode 100644 functions/bolt12Semantics.js
create mode 100644 functions/fabricPaymentBech32.js
create mode 100644 functions/lightningBolt12.js
create mode 100644 tests/bolt12.semantics.unit.js
create mode 100644 tests/fabricPaymentBech32.unit.js
create mode 100644 tests/lightning/bolt12.classify.unit.js
diff --git a/functions/bolt12Semantics.js b/functions/bolt12Semantics.js
new file mode 100644
index 000000000..ea37e81ba
--- /dev/null
+++ b/functions/bolt12Semantics.js
@@ -0,0 +1,150 @@
+'use strict';
+
+/**
+ * BOLT #12 semantic helpers for interpreting **`Lightning#decodeLightning`** (`decode` RPC) JSON from Core Lightning.
+ *
+ * **BIP-340 signatures (BOLT #12 § Signature calculation):**
+ * - **Offers** (`lno1…`) do **not** carry a signature TLV.
+ * - **`invoice_request`** and **`invoice`** streams include **signature** (TLV type **240**…1000), keyed with
+ * `SIG("lightning" ‖ stream ‖ field, Merkle_root, key)` per the spec ([BOLT #12](https://github.com/lightning/bolts/blob/master/12-offer-encoding.md)).
+ * - This module does **not** verify Schnorr signatures; use **`lightningd`** / **`decode`** and payment flows for that.
+ *
+ * **Recurrence:** TLVs such as `offer_recurrence_optional` / `_compulsory`, `invreq_recurrence_*`, `invoice_recurrence_basetime`
+ * are summarized from flat **`decode`** objects when present.
+ *
+ * @module functions/bolt12Semantics
+ * @see docs/LIGHTNING_COMPAT.md
+ */
+
+/**
+ * TLV types named in BOLT #12 (offer / invoice_request / invoice ranges differ by stream).
+ * @readonly
+ */
+const BOLT12_TLV = Object.freeze({
+ INVREQ_METADATA: 0,
+ OFFER_CHAINS: 2,
+ OFFER_METADATA: 4,
+ OFFER_CURRENCY: 6,
+ OFFER_AMOUNT: 8,
+ OFFER_DESCRIPTION: 10,
+ OFFER_FEATURES: 12,
+ OFFER_ABSOLUTE_EXPIRY: 14,
+ OFFER_PATHS: 16,
+ OFFER_ISSUER: 18,
+ OFFER_QUANTITY_MAX: 20,
+ OFFER_ISSUER_ID: 22,
+ OFFER_RECURRENCE_COMPULSORY: 24,
+ OFFER_RECURRENCE_OPTIONAL: 25,
+ OFFER_RECURRENCE_BASE: 26,
+ OFFER_RECURRENCE_PAYWINDOW: 27,
+ OFFER_RECURRENCE_LIMIT: 29,
+ INVREQ_CHAIN: 80,
+ INVREQ_AMOUNT: 82,
+ INVREQ_QUANTITY: 86,
+ INVREQ_PAYER_ID: 88,
+ INVREQ_RECURRENCE_COUNTER: 92,
+ INVREQ_RECURRENCE_START: 93,
+ INVREQ_RECURRENCE_CANCEL: 94,
+ SIGNATURE_MIN: 240,
+ SIGNATURE_MAX: 1000,
+ INVOICE_PATHS: 160,
+ INVOICE_BLINDEDPAY: 162,
+ INVOICE_CREATED_AT: 164,
+ INVOICE_PAYMENT_HASH: 168,
+ INVOICE_AMOUNT: 170,
+ INVOICE_NODE_ID: 176,
+ INVOICE_RECURRENCE_BASETIME: 177
+});
+
+/**
+ * Normalized kind for a value returned by **`decode`** / **`decodeLightning`**.
+ * @readonly
+ * @enum {string}
+ */
+const Bolt12StreamKind = Object.freeze({
+ bolt12_offer: 'bolt12_offer',
+ bolt12_invoice_request: 'bolt12_invoice_request',
+ bolt12_invoice: 'bolt12_invoice',
+ bolt11_invoice: 'bolt11_invoice',
+ unknown: 'unknown'
+});
+
+/**
+ * Classify **`decode`** result from Core Lightning using its **`type`** field (wording varies slightly by version).
+ * @param {Object|null|undefined} decoded
+ * @returns {typeof Bolt12StreamKind[keyof typeof Bolt12StreamKind]}
+ */
+function classifyDecodedBolt12 (decoded) {
+ if (!decoded || typeof decoded !== 'object') return Bolt12StreamKind.unknown;
+ const t = decoded.type;
+ if (typeof t !== 'string') return Bolt12StreamKind.unknown;
+ const s = t.toLowerCase();
+ if (s.includes('bolt12') && s.includes('offer') && !s.includes('invoice')) {
+ return Bolt12StreamKind.bolt12_offer;
+ }
+ if (s.includes('invoice_request') || (s.includes('bolt12') && s.includes('invoice') && s.includes('request'))) {
+ return Bolt12StreamKind.bolt12_invoice_request;
+ }
+ if (s.includes('bolt12') && s.includes('invoice') && !s.includes('request')) {
+ return Bolt12StreamKind.bolt12_invoice;
+ }
+ if (s.includes('bolt11') || /^bitcoin\s*invoice/i.test(t)) {
+ return Bolt12StreamKind.bolt11_invoice;
+ }
+ return Bolt12StreamKind.unknown;
+}
+
+/**
+ * Whether BOLT #12 Merkle **BIP-340** signatures apply to this stream (not offers).
+ * @param {typeof Bolt12StreamKind[keyof typeof Bolt12StreamKind]} kind
+ * @returns {boolean}
+ */
+function bip340SignatureApplies (kind) {
+ return kind === Bolt12StreamKind.bolt12_invoice_request || kind === Bolt12StreamKind.bolt12_invoice;
+}
+
+/**
+ * Collect recurrence-related fields from a flat **`decode`** object (snake_case keys as CLN tends to emit).
+ * @param {Object|null|undefined} decoded
+ * @returns {Object|null} Plain object copy, or `null` if nothing recurrence-related.
+ */
+function summarizeBolt12RecurrenceFromDecode (decoded) {
+ if (!decoded || typeof decoded !== 'object') return null;
+ /** @type {Record} */
+ const out = {};
+
+ const offerKeys = [
+ 'offer_recurrence',
+ 'offer_recurrence_compulsory',
+ 'offer_recurrence_optional',
+ 'offer_recurrence_base',
+ 'offer_recurrence_paywindow',
+ 'offer_recurrence_limit'
+ ];
+ for (const k of offerKeys) {
+ if (decoded[k] != null) out[k] = decoded[k];
+ }
+
+ const invreqKeys = [
+ 'invreq_recurrence_counter',
+ 'invreq_recurrence_start',
+ 'invreq_recurrence_cancel'
+ ];
+ for (const k of invreqKeys) {
+ if (decoded[k] != null) out[k] = decoded[k];
+ }
+
+ if (decoded.invoice_recurrence_basetime != null) {
+ out.invoice_recurrence_basetime = decoded.invoice_recurrence_basetime;
+ }
+
+ return Object.keys(out).length ? out : null;
+}
+
+module.exports = {
+ BOLT12_TLV,
+ Bolt12StreamKind,
+ bip340SignatureApplies,
+ classifyDecodedBolt12,
+ summarizeBolt12RecurrenceFromDecode
+};
diff --git a/functions/fabricPaymentBech32.js b/functions/fabricPaymentBech32.js
new file mode 100644
index 000000000..8c30d1f79
--- /dev/null
+++ b/functions/fabricPaymentBech32.js
@@ -0,0 +1,123 @@
+'use strict';
+
+/**
+ * **Fabric-routed payment** references: **bech32m** with HRP **`fa`**, strings **`fa1…`**
+ * (same checksum family as BOLT12 / Taproot addresses per [BIP 350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki)).
+ *
+ * This is **not** a Lightning invoice; it is an opaque handle for Fabric / Hub routing (e.g. document
+ * purchase, mesh settlement) that wallets and UIs can recognize without conflicting with `ln…` strings.
+ *
+ * @module functions/fabricPaymentBech32
+ * @see docs/FABRIC_PAYMENT_BECH32.md
+ */
+
+const bech32 = require('./bech32');
+const lb = require('./lightningBolt12');
+
+const FABRIC_ROUTED_PAYMENT_HRP = 'fa';
+
+/** All such strings use the `1` separator after the HRP, e.g. `fa1…`. */
+const FABRIC_ROUTED_PAYMENT_PREFIX = `${FABRIC_ROUTED_PAYMENT_HRP}1`;
+
+/** Payload format byte (first byte after decode). */
+const FABRIC_PAYMENT_VERSION = 0;
+
+/**
+ * Route / binding type (second byte). `0` = 32-byte document **content hash** (see L1 document exchange).
+ * @readonly
+ * @enum {number}
+ */
+const FabricRouteType = Object.freeze({
+ /** SHA256 hash as used with `purchaseContentHashHex` / canonical publish preimage chain. */
+ DOCUMENT_CONTENT_HASH: 0
+});
+
+/**
+ * @param {string|null|undefined} s
+ * @returns {boolean}
+ */
+function isFabricRoutedPaymentString (s) {
+ return String(s || '').trim().toLowerCase().startsWith(FABRIC_ROUTED_PAYMENT_PREFIX);
+}
+
+/**
+ * Encode a v0 Fabric-routed payment reference: version + route type + 32-byte id (e.g. content hash).
+ * @param {Object} o
+ * @param {Buffer|Uint8Array|number[]} o.hash32 Exactly 32 bytes (e.g. document content hash).
+ * @param {number} [o.routeType=0] {@link FabricRouteType}
+ * @returns {string} bech32m string (`fa1…`)
+ */
+function encodeFabricRoutedPaymentV0 (o) {
+ if (!o || !o.hash32) throw new TypeError('encodeFabricRoutedPaymentV0 requires hash32');
+ let buf;
+ if (Buffer.isBuffer(o.hash32)) buf = o.hash32;
+ else if (o.hash32 instanceof Uint8Array) buf = Buffer.from(o.hash32);
+ else if (Array.isArray(o.hash32)) buf = Buffer.from(o.hash32);
+ else throw new TypeError('hash32 must be Buffer, Uint8Array, or byte array');
+ if (buf.length !== 32) throw new TypeError('hash32 must be exactly 32 bytes');
+ const routeType = o.routeType != null ? Number(o.routeType) : FabricRouteType.DOCUMENT_CONTENT_HASH;
+ if (!Number.isInteger(routeType) || routeType < 0 || routeType > 255) {
+ throw new TypeError('routeType must be an integer 0..255');
+ }
+ const payload = Buffer.concat([Buffer.from([FABRIC_PAYMENT_VERSION, routeType]), buf]);
+ const words = bech32.toWords(payload);
+ return bech32.encode(FABRIC_ROUTED_PAYMENT_HRP, words, 'bech32m');
+}
+
+/**
+ * Decode an `fa1…` string. Returns `null` if HRP/spec mismatch or payload shape is wrong.
+ * @param {string} str
+ * @returns {{ version: number, routeType: number, hash32: Buffer, hrp: string, spec: string }|null}
+ */
+function decodeFabricRoutedPayment (str) {
+ if (typeof str !== 'string' || !str.trim()) return null;
+ let d;
+ try {
+ d = bech32.decode(str.trim());
+ } catch {
+ return null;
+ }
+ if (d.hrp !== FABRIC_ROUTED_PAYMENT_HRP || d.spec !== 'bech32m') return null;
+ let raw;
+ try {
+ raw = bech32.fromWords(d.words);
+ } catch {
+ return null;
+ }
+ if (raw.length < 2 + 32) return null;
+ const version = raw[0];
+ const routeType = raw[1];
+ const hash32 = Buffer.from(raw.slice(2, 34));
+ if (version !== FABRIC_PAYMENT_VERSION) return null;
+ if (raw.length !== 34) return null;
+ return {
+ version,
+ routeType,
+ hash32,
+ hrp: d.hrp,
+ spec: d.spec
+ };
+}
+
+/**
+ * Classify a payment-related encoded string: Fabric `fa1…`, then Lightning `ln…` (see {@link ./lightningBolt12}).
+ * @param {string|null|undefined} s
+ * @returns {string}
+ */
+function classifyPaymentEncodingString (s) {
+ const t = String(s || '').trim();
+ if (!t) return 'empty';
+ if (isFabricRoutedPaymentString(t)) return 'fabric_routed_payment';
+ return lb.classifyLightningEncodedString(s);
+}
+
+module.exports = {
+ FABRIC_ROUTED_PAYMENT_HRP,
+ FABRIC_ROUTED_PAYMENT_PREFIX,
+ FABRIC_PAYMENT_VERSION,
+ FabricRouteType,
+ classifyPaymentEncodingString,
+ decodeFabricRoutedPayment,
+ encodeFabricRoutedPaymentV0,
+ isFabricRoutedPaymentString
+};
diff --git a/functions/lightningBolt12.js b/functions/lightningBolt12.js
new file mode 100644
index 000000000..c5767861c
--- /dev/null
+++ b/functions/lightningBolt12.js
@@ -0,0 +1,101 @@
+'use strict';
+
+/** BOLT12 offer encoding uses HRP `lno` → strings begin with `lno1` (bech32m). */
+const BOLT12_OFFER_PREFIX = 'lno1';
+
+/** Invoice request (`invoice_request` in spec) — Core Lightning exposes `lnr1…` strings. */
+const BOLT12_INVOICE_REQUEST_PREFIX = 'lnr1';
+
+/**
+ * Common BOLT11 invoice HRPs (first segment before `1`). Not exhaustive; unknown `ln…1` may still be BOLT11.
+ * @type {ReadonlyArray}
+ */
+const BOLT11_INVOICE_HRPS = Object.freeze([
+ 'lnbc', 'lntb', 'lnbcrt', 'lntbs', 'lnbs', 'lntbsc', 'lnsb', 'lnbsc', 'lntb4', 'lnbc4'
+]);
+
+/**
+ * @typedef {'bolt12_offer'|'bolt12_invoice_request'|'bolt11_invoice'|'unknown'} Bolt12Class
+ */
+
+function normalizeBolt12ScanString (s) {
+ let t = String(s ?? '').trim();
+ if (!t) return '';
+ t = t.replace(/\+\s*/g, '');
+ return t;
+}
+
+/**
+ * @param {string|null|undefined} s
+ * @returns {boolean}
+ */
+function isBolt12OfferString (s) {
+ const low = normalizeBolt12ScanString(s).toLowerCase();
+ return low.startsWith(BOLT12_OFFER_PREFIX);
+}
+
+/**
+ * @param {string|null|undefined} s
+ * @returns {boolean}
+ */
+function isBolt12InvoiceRequestString (s) {
+ const low = normalizeBolt12ScanString(s).toLowerCase();
+ return low.startsWith(BOLT12_INVOICE_REQUEST_PREFIX);
+}
+
+/**
+ * @param {string|null|undefined} s
+ * @returns {boolean}
+ */
+function isLikelyBolt11InvoiceString (s) {
+ const t = String(s || '').trim().toLowerCase();
+ if (!t.includes('1')) return false;
+ return BOLT11_INVOICE_HRPS.some((hrp) => t.startsWith(`${hrp}1`));
+}
+
+/**
+ * Classify a Lightning-encoded string for routing to `fetchInvoice`, `sendInvoice`, `pay`, etc.
+ * @param {string|null|undefined} s
+ * @returns {Bolt12Class|'empty'}
+ */
+function classifyLightningEncodedString (s) {
+ const t = normalizeBolt12ScanString(s);
+ if (!t) return 'empty';
+ const low = t.toLowerCase();
+ if (low.startsWith(BOLT12_OFFER_PREFIX)) return 'bolt12_offer';
+ if (low.startsWith(BOLT12_INVOICE_REQUEST_PREFIX)) return 'bolt12_invoice_request';
+ if (isLikelyBolt11InvoiceString(low)) return 'bolt11_invoice';
+ return 'unknown';
+}
+
+/**
+ * How Fabric **markets** and related layers relate to Lightning (see `docs/FABRIC_LIGHTNING_OFFERS.md`).
+ * @readonly
+ * @enum {string}
+ */
+const FabricLightningOfferRole = Object.freeze({
+ /** Fabric `Peer` `P2P_SESSION_OFFER` / peering — not a BOLT12 string. */
+ fabric_peer_handshake: 'fabric_peer_handshake',
+ /** Inventory `offerBtc` / document rate — market surface; may pair with a BOLT12 `bolt12` for LN pay. */
+ fabric_document_commerce: 'fabric_document_commerce',
+ /** BOLT12 `offer` / `lno1…` — Lightning payment offer (Core Lightning `offer` RPC). */
+ lightning_bolt12_offer: 'lightning_bolt12_offer',
+ /** BOLT12 `invoice_request` / `lnr1…` — payer-initiated flow (`invoicerequest` RPC). */
+ lightning_bolt12_invoice_request: 'lightning_bolt12_invoice_request'
+});
+
+/** @type {typeof FabricLightningOfferRole} Preferred name; same frozen object as {@link FabricLightningOfferRole}. */
+const FabricLightningMarketRole = FabricLightningOfferRole;
+
+module.exports = {
+ BOLT12_OFFER_PREFIX,
+ BOLT12_INVOICE_REQUEST_PREFIX,
+ BOLT11_INVOICE_HRPS,
+ FabricLightningMarketRole,
+ FabricLightningOfferRole,
+ classifyLightningEncodedString,
+ isBolt12OfferString,
+ isBolt12InvoiceRequestString,
+ isLikelyBolt11InvoiceString,
+ normalizeBolt12ScanString
+};
diff --git a/package-lock.json b/package-lock.json
index e9c76aa75..b9b0a43b6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -402,29 +402,43 @@
}
},
"node_modules/@humanfs/core": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
- "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
"dev": true,
"license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
- "version": "0.16.7",
- "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
- "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@humanfs/core": "^0.19.1",
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
"@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
}
},
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -3502,9 +3516,9 @@
}
},
"node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/services/lightning.js b/services/lightning.js
index 8f2616595..4403013d9 100644
--- a/services/lightning.js
+++ b/services/lightning.js
@@ -275,6 +275,88 @@ class Lightning extends Service {
};
}
+ async callRpc (method, params = [], timeoutMs = 30000) {
+ return this._makeRPCRequest(method, params, timeoutMs);
+ }
+
+ async decodeLightning (boltString) {
+ return this._makeRPCRequest('decode', [boltString]);
+ }
+
+ async decodePay (bolt11) {
+ return this._makeRPCRequest('decodepay', [bolt11]);
+ }
+
+ async createOffer (params) {
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
+ throw new Error('createOffer requires a params object (e.g. { amount_msat, description })');
+ }
+ return this._makeRPCRequest('offer', [params]);
+ }
+
+ async fetchInvoice (offerOrParams, invoiceParams = null) {
+ if (offerOrParams != null && typeof offerOrParams === 'object' && !Array.isArray(offerOrParams) && typeof offerOrParams.offer === 'string') {
+ return this._makeRPCRequest('fetchinvoice', [offerOrParams]);
+ }
+ if (typeof offerOrParams !== 'string') {
+ throw new Error('fetchInvoice requires offer string or params object with `offer`');
+ }
+ if (invoiceParams != null && typeof invoiceParams === 'object' && !Array.isArray(invoiceParams)) {
+ return this._makeRPCRequest('fetchinvoice', [offerOrParams, invoiceParams]);
+ }
+ return this._makeRPCRequest('fetchinvoice', [offerOrParams]);
+ }
+
+ async pay (invoiceOrParams, timeoutMs = 30000) {
+ return this._makeRPCRequest('pay', [invoiceOrParams], timeoutMs);
+ }
+
+ async listOffers (filter = null) {
+ if (filter == null) return this._makeRPCRequest('listoffers', []);
+ if (typeof filter === 'string') return this._makeRPCRequest('listoffers', [filter]);
+ if (typeof filter === 'object' && !Array.isArray(filter)) {
+ return this._makeRPCRequest('listoffers', [filter]);
+ }
+ throw new Error('listOffers expects offer_id string, filter object, or null');
+ }
+
+ async disableOffer (offerId) {
+ return this._makeRPCRequest('disableoffer', [offerId]);
+ }
+
+ async createInvoiceRequest (params) {
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
+ throw new Error('createInvoiceRequest requires a params object (e.g. { amount, description })');
+ }
+ return this._makeRPCRequest('invoicerequest', [params]);
+ }
+
+ async listInvoiceRequests (filter = null) {
+ if (filter == null) return this._makeRPCRequest('listinvoicerequests', []);
+ if (typeof filter === 'string') return this._makeRPCRequest('listinvoicerequests', [filter]);
+ if (typeof filter === 'object' && !Array.isArray(filter)) {
+ return this._makeRPCRequest('listinvoicerequests', [filter]);
+ }
+ throw new Error('listInvoiceRequests expects invreq_id string, filter object, or null');
+ }
+
+ async disableInvoiceRequest (invreqId) {
+ return this._makeRPCRequest('disableinvoicerequest', [invreqId]);
+ }
+
+ async sendInvoice (params) {
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
+ throw new Error('sendInvoice requires a params object (e.g. { invreq, label })');
+ }
+ return this._makeRPCRequest('sendinvoice', [params]);
+ }
+
+ async getRoute (destinationId, amountMsat, riskfactor = 10, maxHopsOrExtra = null) {
+ const args = [destinationId, amountMsat, riskfactor];
+ if (maxHopsOrExtra != null) args.push(maxHopsOrExtra);
+ return this._makeRPCRequest('getroute', args);
+ }
+
/**
* Computes the total liquidity of the Lightning node.
* @returns {Object} Liquidity in BTC.
@@ -885,15 +967,39 @@ class Lightning extends Service {
*/
Lightning.CLN_RPC_METHODS = Object.freeze([
'connect',
+ 'decode',
+ 'decodepay',
+ 'disableinvoicerequest',
+ 'disableoffer',
+ 'fetchinvoice',
'fundchannel',
'getinfo',
+ 'getroute',
'invoice',
+ 'invoicerequest',
'listchannels',
'listfunds',
+ 'listinvoicerequests',
+ 'listoffers',
'newaddr',
+ 'offer',
+ 'pay',
+ 'sendinvoice',
'stop'
]);
+Lightning.DOCS = Object.freeze({
+ boltCompatibility: 'docs/BOLT_COMPATIBILITY.md',
+ fabricLightningOffers: 'docs/FABRIC_LIGHTNING_OFFERS.md',
+ fabricLightningMarkets: 'docs/FABRIC_LIGHTNING_OFFERS.md',
+ fabricPaymentBech32: 'docs/FABRIC_PAYMENT_BECH32.md',
+ lightningCompat: 'docs/LIGHTNING_COMPAT.md'
+});
+
+Lightning.Bolt12 = lightningBolt12;
+
+Lightning.FabricPayment = fabricPaymentBech32;
+
Lightning.redactSensitiveCommandArg = redactSensitiveCommandArg;
module.exports = Lightning;
diff --git a/tests/bitcoin/transaction.js b/tests/bitcoin/transaction.js
index f77f2d2e1..84c9afbe0 100644
--- a/tests/bitcoin/transaction.js
+++ b/tests/bitcoin/transaction.js
@@ -23,9 +23,10 @@ describe('@fabric/core/types/bitcoin', function () {
it('exposes identifier properties', function () {
const tx = new BitcoinTransaction({ raw: 'deadbeef' });
- assert.strictEqual(tx.hash, '');
- assert.strictEqual(tx.id, '');
- assert.strictEqual(tx.txid, '');
+ assert.strictEqual(tx._state.content.raw, 'deadbeef');
+ assert.strictEqual(tx.hash, '281dd50f6f56bc6e867fe73dd614a73c55a647a479704f64804b574cafb0f5c5');
+ assert.strictEqual(tx.txid, 'c5f5b0af4c574b80644f7079a447a6553ca714d63de77f866ebc566f0fd51d28');
+ assert.strictEqual(tx.id, tx.txid);
});
it('can sign as holder', function () {
diff --git a/tests/bolt12.semantics.unit.js b/tests/bolt12.semantics.unit.js
new file mode 100644
index 000000000..a8b9ec992
--- /dev/null
+++ b/tests/bolt12.semantics.unit.js
@@ -0,0 +1,54 @@
+'use strict';
+
+const assert = require('assert');
+const bs = require('../functions/bolt12Semantics');
+const Lightning = require('../services/lightning');
+
+describe('@fabric/core/functions/bolt12Semantics', function () {
+ it('classifyDecodedBolt12 recognizes CLN-style type strings', function () {
+ assert.strictEqual(bs.classifyDecodedBolt12({ type: 'bolt12 offer' }), bs.Bolt12StreamKind.bolt12_offer);
+ assert.strictEqual(
+ bs.classifyDecodedBolt12({ type: 'bolt12 invoice_request' }),
+ bs.Bolt12StreamKind.bolt12_invoice_request
+ );
+ assert.strictEqual(
+ bs.classifyDecodedBolt12({ type: 'bolt12 invoice' }),
+ bs.Bolt12StreamKind.bolt12_invoice
+ );
+ assert.strictEqual(
+ bs.classifyDecodedBolt12({ type: 'bolt11 invoice' }),
+ bs.Bolt12StreamKind.bolt11_invoice
+ );
+ assert.strictEqual(bs.classifyDecodedBolt12(null), bs.Bolt12StreamKind.unknown);
+ });
+
+ it('bip340SignatureApplies only for invoice_request and invoice', function () {
+ assert.strictEqual(bs.bip340SignatureApplies(bs.Bolt12StreamKind.bolt12_offer), false);
+ assert.strictEqual(bs.bip340SignatureApplies(bs.Bolt12StreamKind.bolt12_invoice_request), true);
+ assert.strictEqual(bs.bip340SignatureApplies(bs.Bolt12StreamKind.bolt12_invoice), true);
+ });
+
+ it('summarizeBolt12RecurrenceFromDecode collects known keys', function () {
+ const decoded = {
+ offer_recurrence: { period: 7, time_unit: 1 },
+ invreq_recurrence_counter: 2,
+ invoice_recurrence_basetime: 1700000000
+ };
+ const sum = bs.summarizeBolt12RecurrenceFromDecode(decoded);
+ assert.ok(sum);
+ assert.strictEqual(sum.invreq_recurrence_counter, 2);
+ assert.ok(sum.offer_recurrence);
+ });
+
+ it('BOLT12_TLV exposes recurrence-related type numbers', function () {
+ assert.strictEqual(bs.BOLT12_TLV.OFFER_RECURRENCE_COMPULSORY, 24);
+ assert.strictEqual(bs.BOLT12_TLV.INVREQ_RECURRENCE_COUNTER, 92);
+ assert.strictEqual(bs.BOLT12_TLV.SIGNATURE_MIN, 240);
+ });
+});
+
+describe('@fabric/core/services/lightning Bolt12Semantics bridge', function () {
+ it('Lightning.Bolt12Semantics re-exports bolt12Semantics', function () {
+ assert.strictEqual(Lightning.Bolt12Semantics, bs);
+ });
+});
diff --git a/tests/fabric.chain.js b/tests/fabric.chain.js
index 6d6eef078..0ec5ee181 100644
--- a/tests/fabric.chain.js
+++ b/tests/fabric.chain.js
@@ -87,6 +87,22 @@ describe('@fabric/core/types/chain', function () {
assert.ok(chain);
});
+ it('proposeTransaction ignores duplicate ids (single mempool entry)', async function () {
+ const chain = new Chain();
+ const block = new Block({ debug: true, input: 'Hello, world.' });
+
+ await chain.start();
+ await chain.append(block);
+
+ const payload = { type: 'Transaction', input: 'duplicate-proposal' };
+ const tx1 = chain.proposeTransaction(payload);
+ const tx2 = chain.proposeTransaction(payload);
+
+ assert.strictEqual(tx1.id, tx2.id);
+ assert.strictEqual(chain.mempool.filter((id) => id === tx1.id).length, 1);
+ await chain.stop();
+ });
+
it('can mine a second block with transactions', async function () {
const chain = new Chain();
const block = new Block({
diff --git a/tests/fabricPaymentBech32.unit.js b/tests/fabricPaymentBech32.unit.js
new file mode 100644
index 000000000..40b1655a5
--- /dev/null
+++ b/tests/fabricPaymentBech32.unit.js
@@ -0,0 +1,85 @@
+'use strict';
+
+const assert = require('assert');
+const crypto = require('crypto');
+const fp = require('../functions/fabricPaymentBech32');
+
+describe('@fabric/core/functions/fabricPaymentBech32', function () {
+ it('encodeFabricRoutedPaymentV0 round-trips decodeFabricRoutedPayment', function () {
+ const hash32 = crypto.randomBytes(32);
+ const enc = fp.encodeFabricRoutedPaymentV0({ hash32 });
+ assert.ok(enc.toLowerCase().startsWith('fa1'), enc);
+ const dec = fp.decodeFabricRoutedPayment(enc);
+ assert.ok(dec);
+ assert.strictEqual(dec.version, 0);
+ assert.strictEqual(dec.routeType, 0);
+ assert.ok(Buffer.isBuffer(dec.hash32));
+ assert.deepStrictEqual(dec.hash32, hash32);
+ });
+
+ it('reject wrong hash length', function () {
+ assert.throws(() => fp.encodeFabricRoutedPaymentV0({ hash32: Buffer.alloc(31) }), /32 bytes/);
+ });
+
+ it('encodeFabricRoutedPaymentV0 rejects invalid inputs', function () {
+ assert.throws(() => fp.encodeFabricRoutedPaymentV0(), /hash32/);
+ assert.throws(() => fp.encodeFabricRoutedPaymentV0({ hash32: 'not-bytes' }), /Buffer/);
+ assert.throws(() => fp.encodeFabricRoutedPaymentV0({ hash32: Buffer.alloc(32), routeType: 1.5 }), /integer/);
+ assert.throws(() => fp.encodeFabricRoutedPaymentV0({ hash32: Buffer.alloc(32), routeType: 300 }), /255/);
+ });
+
+ it('decodeFabricRoutedPayment returns null for lnbc or garbage', function () {
+ assert.strictEqual(fp.decodeFabricRoutedPayment('lnbc1qqqqqqqqqqqq'), null);
+ assert.strictEqual(fp.decodeFabricRoutedPayment('fa1broken'), null);
+ });
+
+ it('classifyPaymentEncodingString prefers fa over ln', function () {
+ const h = crypto.randomBytes(32);
+ const fa = fp.encodeFabricRoutedPaymentV0({ hash32: h });
+ assert.strictEqual(fp.classifyPaymentEncodingString(fa), 'fabric_routed_payment');
+ assert.strictEqual(fp.classifyPaymentEncodingString('lno1qqqq'), 'bolt12_offer');
+ assert.strictEqual(fp.classifyPaymentEncodingString('lnr1qqqq'), 'bolt12_invoice_request');
+ });
+
+ it('classifyPaymentEncodingString empty and unknown', function () {
+ assert.strictEqual(fp.classifyPaymentEncodingString(''), 'empty');
+ assert.strictEqual(fp.classifyPaymentEncodingString(' '), 'empty');
+ assert.strictEqual(fp.classifyPaymentEncodingString('xyz'), 'unknown');
+ assert.strictEqual(fp.classifyPaymentEncodingString(null), 'empty');
+ });
+
+ it('isFabricRoutedPaymentString', function () {
+ assert.strictEqual(fp.isFabricRoutedPaymentString('fa1qqqq'), true);
+ assert.strictEqual(fp.isFabricRoutedPaymentString('FA1qqqq'), true);
+ assert.strictEqual(fp.isFabricRoutedPaymentString('lno1qq'), false);
+ assert.strictEqual(fp.isFabricRoutedPaymentString(''), false);
+ });
+
+ it('encodeFabricRoutedPaymentV0 accepts Uint8Array hash32', function () {
+ const u8 = new Uint8Array(32);
+ u8[0] = 9;
+ const enc = fp.encodeFabricRoutedPaymentV0({ hash32: u8 });
+ const dec = fp.decodeFabricRoutedPayment(enc);
+ assert.ok(dec);
+ assert.strictEqual(dec.hash32[0], 9);
+ });
+
+ it('encodeFabricRoutedPaymentV0 non-default routeType round-trips', function () {
+ const hash32 = crypto.randomBytes(32);
+ const enc = fp.encodeFabricRoutedPaymentV0({ hash32, routeType: 7 });
+ const dec = fp.decodeFabricRoutedPayment(enc);
+ assert.ok(dec);
+ assert.strictEqual(dec.routeType, 7);
+ });
+
+ it('decodeFabricRoutedPayment rejects empty input', function () {
+ assert.strictEqual(fp.decodeFabricRoutedPayment(''), null);
+ assert.strictEqual(fp.decodeFabricRoutedPayment(null), null);
+ });
+
+ it('exports constants', function () {
+ assert.strictEqual(fp.FABRIC_ROUTED_PAYMENT_HRP, 'fa');
+ assert.strictEqual(fp.FABRIC_ROUTED_PAYMENT_PREFIX, 'fa1');
+ assert.strictEqual(fp.FabricRouteType.DOCUMENT_CONTENT_HASH, 0);
+ });
+});
diff --git a/tests/lightning/bolt12.classify.unit.js b/tests/lightning/bolt12.classify.unit.js
new file mode 100644
index 000000000..6bd93f535
--- /dev/null
+++ b/tests/lightning/bolt12.classify.unit.js
@@ -0,0 +1,83 @@
+'use strict';
+
+const assert = require('assert');
+const lb = require('../../functions/lightningBolt12');
+const fp = require('../../functions/fabricPaymentBech32');
+const Lightning = require('../../services/lightning');
+
+describe('@fabric/core/functions/lightningBolt12', function () {
+ it('classifies lno1 as bolt12_offer', function () {
+ assert.strictEqual(lb.classifyLightningEncodedString('lno1qqqqqqqqqqqq'), 'bolt12_offer');
+ assert.strictEqual(lb.isBolt12OfferString('LNO1TEST'), true);
+ });
+
+ it('normalizeBolt12ScanString strips + continuations per BOLT12 § Encoding', function () {
+ assert.strictEqual(
+ lb.normalizeBolt12ScanString('lno1xxxx+\nyyyyyyyyyyyy+\nzzzzzz'),
+ 'lno1xxxxyyyyyyyyyyyyzzzzzz'
+ );
+ assert.strictEqual(lb.normalizeBolt12ScanString(' '), '');
+ assert.strictEqual(lb.classifyLightningEncodedString('lno1aa+\nbb'), 'bolt12_offer');
+ });
+
+ it('classifies lnr1 as bolt12_invoice_request', function () {
+ assert.strictEqual(lb.classifyLightningEncodedString('lnr1qqqqqqqqqqqq'), 'bolt12_invoice_request');
+ assert.strictEqual(lb.isBolt12InvoiceRequestString('lnr1x'), true);
+ });
+
+ it('classifies common bolt11 HRPs', function () {
+ assert.strictEqual(lb.classifyLightningEncodedString('lnbc1qqqq'), 'bolt11_invoice');
+ assert.strictEqual(lb.classifyLightningEncodedString('lntb1qqqq'), 'bolt11_invoice');
+ assert.strictEqual(lb.classifyLightningEncodedString('lnbcrt1qqqq'), 'bolt11_invoice');
+ assert.strictEqual(lb.isLikelyBolt11InvoiceString('lnbc1'), true);
+ });
+
+ it('returns empty or unknown appropriately', function () {
+ assert.strictEqual(lb.classifyLightningEncodedString(''), 'empty');
+ assert.strictEqual(lb.classifyLightningEncodedString(' '), 'empty');
+ assert.strictEqual(lb.classifyLightningEncodedString('notlightning'), 'unknown');
+ });
+
+ it('isBolt12OfferString and isLikelyBolt11InvoiceString edge cases', function () {
+ assert.strictEqual(lb.isBolt12OfferString('lnr1qqqq'), false);
+ assert.strictEqual(lb.isLikelyBolt11InvoiceString('lnbc'), false);
+ assert.strictEqual(lb.isLikelyBolt11InvoiceString('lnbc1'), true);
+ assert.strictEqual(lb.isLikelyBolt11InvoiceString('lnsb1qq'), true);
+ });
+
+ it('FabricLightningOfferRole has expected keys', function () {
+ assert.deepStrictEqual(Object.keys(lb.FabricLightningOfferRole).sort(), [
+ 'fabric_document_commerce',
+ 'fabric_peer_handshake',
+ 'lightning_bolt12_invoice_request',
+ 'lightning_bolt12_offer'
+ ]);
+ });
+
+ it('exports BOLT11_INVOICE_HRPS', function () {
+ assert.ok(Array.isArray(lb.BOLT11_INVOICE_HRPS));
+ assert.ok(lb.BOLT11_INVOICE_HRPS.includes('lnbc'));
+ });
+
+ it('FabricLightningMarketRole aliases FabricLightningOfferRole', function () {
+ assert.strictEqual(lb.FabricLightningMarketRole, lb.FabricLightningOfferRole);
+ assert.strictEqual(lb.FabricLightningMarketRole.lightning_bolt12_offer, 'lightning_bolt12_offer');
+ });
+});
+
+describe('@fabric/core/services/lightning Bolt12 bridge', function () {
+ it('Lightning.Bolt12 re-exports lightningBolt12', function () {
+ assert.strictEqual(Lightning.Bolt12, lb);
+ assert.strictEqual(Lightning.Bolt12.classifyLightningEncodedString('lno1x'), 'bolt12_offer');
+ });
+
+ it('Lightning.FabricPayment re-exports fabricPaymentBech32', function () {
+ assert.strictEqual(Lightning.FabricPayment, fp);
+ });
+
+ it('Lightning.DOCS includes fabricLightningMarkets, fabricLightningOffers, fabricPaymentBech32', function () {
+ assert.strictEqual(Lightning.DOCS.fabricLightningOffers, 'docs/FABRIC_LIGHTNING_OFFERS.md');
+ assert.strictEqual(Lightning.DOCS.fabricLightningMarkets, Lightning.DOCS.fabricLightningOffers);
+ assert.strictEqual(Lightning.DOCS.fabricPaymentBech32, 'docs/FABRIC_PAYMENT_BECH32.md');
+ });
+});
diff --git a/tests/lightning/lightning.service.unit.js b/tests/lightning/lightning.service.unit.js
index eda206cdf..97cabef36 100644
--- a/tests/lightning/lightning.service.unit.js
+++ b/tests/lightning/lightning.service.unit.js
@@ -10,6 +10,16 @@ const net = require('net');
const Lightning = require('../../services/lightning');
describe('@fabric/core/services/lightning (unit)', function () {
+ describe('Lightning.DOCS', function () {
+ it('exposes Markdown paths relative to the package root', function () {
+ assert.strictEqual(Lightning.DOCS.boltCompatibility, 'docs/BOLT_COMPATIBILITY.md');
+ assert.strictEqual(Lightning.DOCS.fabricLightningOffers, 'docs/FABRIC_LIGHTNING_OFFERS.md');
+ assert.strictEqual(Lightning.DOCS.fabricLightningMarkets, Lightning.DOCS.fabricLightningOffers);
+ assert.strictEqual(Lightning.DOCS.fabricPaymentBech32, 'docs/FABRIC_PAYMENT_BECH32.md');
+ assert.strictEqual(Lightning.DOCS.lightningCompat, 'docs/LIGHTNING_COMPAT.md');
+ });
+ });
+
describe('defaultListenPortForNetwork', function () {
it('maps networks to conventional lightningd bind ports', function () {
assert.strictEqual(Lightning.defaultListenPortForNetwork('mainnet'), 9735);
@@ -193,6 +203,193 @@ describe('@fabric/core/services/lightning (unit)', function () {
});
});
+ describe('JSON-RPC extension & BOLT12 helpers', function () {
+ it('callRpc forwards to _makeRPCRequest with timeout', async function () {
+ const ln = new Lightning();
+ let seenTimeout;
+ ln._makeRPCRequest = async (method, params, timeoutMs) => {
+ assert.strictEqual(method, 'listpeers');
+ assert.deepStrictEqual(params, []);
+ seenTimeout = timeoutMs;
+ return { peers: [] };
+ };
+ const out = await ln.callRpc('listpeers', [], 5000);
+ assert.deepStrictEqual(out, { peers: [] });
+ assert.strictEqual(seenTimeout, 5000);
+ });
+
+ it('constructor leaves rpc null without shadowing callRpc', async function () {
+ const ln = new Lightning();
+ assert.strictEqual(ln.rpc, null);
+ assert.strictEqual(typeof ln.callRpc, 'function');
+ });
+
+ it('decodeLightning calls decode', async function () {
+ const ln = new Lightning();
+ ln._makeRPCRequest = async (method, params) => {
+ assert.strictEqual(method, 'decode');
+ assert.deepStrictEqual(params, ['lno1xxx']);
+ return { type: 'bolt12 offer' };
+ };
+ const out = await ln.decodeLightning('lno1xxx');
+ assert.strictEqual(out.type, 'bolt12 offer');
+ });
+
+ it('decodePay calls decodepay', async function () {
+ const ln = new Lightning();
+ ln._makeRPCRequest = async (method, params) => {
+ assert.strictEqual(method, 'decodepay');
+ assert.deepStrictEqual(params, ['lnbc1xxx']);
+ return { payment_hash: 'ph' };
+ };
+ await ln.decodePay('lnbc1xxx');
+ });
+
+ it('createOffer calls offer with object', async function () {
+ const ln = new Lightning();
+ const p = { amount_msat: '1000', description: 'd' };
+ ln._makeRPCRequest = async (method, params) => {
+ assert.strictEqual(method, 'offer');
+ assert.deepStrictEqual(params, [p]);
+ return { bolt12: 'lno1...' };
+ };
+ const out = await ln.createOffer(p);
+ assert.strictEqual(out.bolt12, 'lno1...');
+ });
+
+ it('createOffer rejects non-object', async function () {
+ const ln = new Lightning();
+ await assert.rejects(() => ln.createOffer(null), /requires a params object/);
+ });
+
+ it('fetchInvoice passes offer only, offer + params, or single keyword object', async function () {
+ const ln = new Lightning();
+ let calls = 0;
+ ln._makeRPCRequest = async (method, params) => {
+ calls++;
+ assert.strictEqual(method, 'fetchinvoice');
+ if (calls === 1) assert.deepStrictEqual(params, ['lno1a']);
+ else if (calls === 2) assert.deepStrictEqual(params, ['lno1b', { amount_msat: '5000' }]);
+ else assert.deepStrictEqual(params, [{ offer: 'lno1c', amount_msat: '1000', payer_note: 'hi' }]);
+ return {};
+ };
+ await ln.fetchInvoice('lno1a');
+ await ln.fetchInvoice('lno1b', { amount_msat: '5000' });
+ await ln.fetchInvoice({ offer: 'lno1c', amount_msat: '1000', payer_note: 'hi' });
+ });
+
+ it('fetchInvoice rejects invalid first arg', async function () {
+ const ln = new Lightning();
+ await assert.rejects(() => ln.fetchInvoice(1), /offer string or params object/);
+ await assert.rejects(() => ln.fetchInvoice({}), /offer string or params object/);
+ });
+
+ it('pay passes string or object to pay RPC', async function () {
+ const ln = new Lightning();
+ let n = 0;
+ ln._makeRPCRequest = async (method, params, t) => {
+ n++;
+ assert.strictEqual(method, 'pay');
+ if (n === 1) assert.deepStrictEqual(params, ['lnbc1']);
+ else assert.deepStrictEqual(params, [{ bolt11: 'lnbc1', maxfeepercent: 0.5 }]);
+ assert.strictEqual(t, 8000);
+ return { status: 'complete' };
+ };
+ await ln.pay('lnbc1', 8000);
+ await ln.pay({ bolt11: 'lnbc1', maxfeepercent: 0.5 }, 8000);
+ });
+
+ it('listOffers with no filter, object, or offer_id string', async function () {
+ const ln = new Lightning();
+ let n = 0;
+ ln._makeRPCRequest = async (method, params) => {
+ n++;
+ assert.strictEqual(method, 'listoffers');
+ if (n === 1) assert.deepStrictEqual(params, []);
+ else if (n === 2) assert.deepStrictEqual(params, [{ active_only: true }]);
+ else assert.deepStrictEqual(params, ['offer_id_hex']);
+ return { offers: [] };
+ };
+ await ln.listOffers();
+ await ln.listOffers({ active_only: true });
+ await ln.listOffers('offer_id_hex');
+ });
+
+ it('createInvoiceRequest calls invoicerequest', async function () {
+ const ln = new Lightning();
+ const p = { amount: '1000sat', description: 'req' };
+ ln._makeRPCRequest = async (method, params) => {
+ assert.strictEqual(method, 'invoicerequest');
+ assert.deepStrictEqual(params, [p]);
+ return { bolt12: 'lnr1...' };
+ };
+ const out = await ln.createInvoiceRequest(p);
+ assert.strictEqual(out.bolt12, 'lnr1...');
+ });
+
+ it('listInvoiceRequests with null, string, or object', async function () {
+ const ln = new Lightning();
+ let n = 0;
+ ln._makeRPCRequest = async (method, params) => {
+ n++;
+ assert.strictEqual(method, 'listinvoicerequests');
+ if (n === 1) assert.deepStrictEqual(params, []);
+ else if (n === 2) assert.deepStrictEqual(params, ['invreqid']);
+ else assert.deepStrictEqual(params, [{ active_only: true }]);
+ return { invoicerequests: [] };
+ };
+ await ln.listInvoiceRequests();
+ await ln.listInvoiceRequests('invreqid');
+ await ln.listInvoiceRequests({ active_only: true });
+ });
+
+ it('disableInvoiceRequest calls disableinvoicerequest', async function () {
+ const ln = new Lightning();
+ ln._makeRPCRequest = async (method, params) => {
+ assert.strictEqual(method, 'disableinvoicerequest');
+ assert.deepStrictEqual(params, ['invreq_x']);
+ return { active: false };
+ };
+ await ln.disableInvoiceRequest('invreq_x');
+ });
+
+ it('sendInvoice calls sendinvoice with object', async function () {
+ const ln = new Lightning();
+ const p = { invreq: 'lnr1abc', label: 'lbl' };
+ ln._makeRPCRequest = async (method, params) => {
+ assert.strictEqual(method, 'sendinvoice');
+ assert.deepStrictEqual(params, [p]);
+ return { status: 'paid' };
+ };
+ const out = await ln.sendInvoice(p);
+ assert.strictEqual(out.status, 'paid');
+ });
+
+ it('disableOffer calls disableoffer', async function () {
+ const ln = new Lightning();
+ ln._makeRPCRequest = async (method, params) => {
+ assert.strictEqual(method, 'disableoffer');
+ assert.deepStrictEqual(params, ['offer_id_1']);
+ return { disabled: true };
+ };
+ await ln.disableOffer('offer_id_1');
+ });
+
+ it('getRoute passes three or four args', async function () {
+ const ln = new Lightning();
+ let n = 0;
+ ln._makeRPCRequest = async (method, params) => {
+ n++;
+ assert.strictEqual(method, 'getroute');
+ if (n === 1) assert.deepStrictEqual(params, ['dest', 1e7, 10]);
+ else assert.deepStrictEqual(params, ['dest', 1e7, 1, 0]);
+ return { route: [] };
+ };
+ await ln.getRoute('dest', 1e7);
+ await ln.getRoute('dest', 1e7, 1, 0);
+ });
+ });
+
describe('computeLiquidity', function () {
it('returns outbound and inbound formatted BTC', async function () {
const ln = new Lightning();
diff --git a/types/bitcoin/transaction.js b/types/bitcoin/transaction.js
index cf3f2af49..516a20da1 100644
--- a/types/bitcoin/transaction.js
+++ b/types/bitcoin/transaction.js
@@ -5,6 +5,29 @@ const crypto = require('crypto');
const Actor = require('../actor');
const Key = require('../key');
// TODO: PSBTs
+// PSBT support remains future work (see JS-PLAN / Bitcoin service).
+function rawTransactionBuffer (raw) {
+ if (raw == null) return Buffer.alloc(0);
+ if (Buffer.isBuffer(raw)) return raw;
+ const s = String(raw).replace(/\s+/g, '');
+ if (!s.length || s.length % 2 !== 0) return Buffer.alloc(0);
+ try {
+ return Buffer.from(s, 'hex');
+ } catch {
+ return Buffer.alloc(0);
+ }
+}
+
+function bitcoinTxidHex (buf) {
+ const h = crypto.createHash('sha256').update(buf).digest();
+ const h2 = crypto.createHash('sha256').update(h).digest();
+ return Buffer.from(h2).reverse().toString('hex');
+}
+
+function doubleSha256Hex (buf) {
+ const h = crypto.createHash('sha256').update(buf).digest();
+ return crypto.createHash('sha256').update(h).digest('hex');
+}
class BitcoinTransaction extends Actor {
constructor (settings = {}) {
@@ -23,7 +46,7 @@ class BitcoinTransaction extends Actor {
this._state = {
content: {
- raw: null
+ raw: this.settings.raw != null ? this.settings.raw : null
},
status: 'PAUSED'
};
@@ -31,21 +54,25 @@ class BitcoinTransaction extends Actor {
return this;
}
+ _rawBuf () {
+ return rawTransactionBuffer(this.settings.raw);
+ }
+
get hash () {
- return ''; // TODO: real hash
+ return doubleSha256Hex(this._rawBuf());
}
get id () {
- return ''; // TODO: Fabric ID
+ return this.txid;
}
get txid () {
- return ''; // TODO: bitcoin txid
+ return bitcoinTxidHex(this._rawBuf());
}
signAsHolder () {
- const hash = crypto.createHash('sha256').update('').digest('hex');
- this.signature = this.holder.sign(hash);
+ const digestHex = doubleSha256Hex(this._rawBuf());
+ this.signature = this.holder.sign(digestHex);
return this;
}
}
diff --git a/types/chain.js b/types/chain.js
index 46fd373b3..8d5b276ed 100644
--- a/types/chain.js
+++ b/types/chain.js
@@ -86,7 +86,7 @@ class Chain extends Actor {
}
get height () {
-
+ return this.blocks.length;
}
get leaves () {
@@ -119,12 +119,12 @@ class Chain extends Actor {
actor: proposal.actor || Actor.randomBytes(32).toString('hex'),
changes: proposal.changes,
mode: proposal.mode || 'NAIVE_SIGHASH_SINGLE',
- object: Buffer.concat(
+ object: Buffer.concat([
Buffer.alloc(32), // pubkey
Buffer.alloc(32), // parent
Buffer.alloc(32), // changes
- Buffer.alloc(64), // signature
- ),
+ Buffer.alloc(64) // signature
+ ]),
parent: this.id,
signature: Buffer.alloc(64),
state: this.state,
@@ -135,7 +135,11 @@ class Chain extends Actor {
proposeTransaction (transaction) {
const actor = new Transaction(transaction);
- // TODO: reject duplicate transactions
+ const prior = this._state.transactions[actor.id];
+ if (prior) {
+ return prior instanceof Transaction ? prior : new Transaction(prior);
+ }
+
this._state.transactions[actor.id] = actor;
this._state.mempool.push(actor.id);
@@ -152,7 +156,7 @@ class Chain extends Actor {
super.trust(source, 'TIMECHAIN');
- source.on('message', function TODO (message) {
+ source.on('message', function onTrustedSourceMessage (message) {
self.emit('debug', `Message from trusted source: ${message}`);
});
From f7da76a730418ae46945d4ef7dd43fd37004c00a Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Mon, 20 Apr 2026 11:11:14 -0400
Subject: [PATCH 04/27] Add missing require statements
---
services/lightning.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/services/lightning.js b/services/lightning.js
index 4403013d9..a7a6e4e67 100644
--- a/services/lightning.js
+++ b/services/lightning.js
@@ -17,6 +17,10 @@ const Machine = require('../types/machine');
// Contracts
const OP_TEST = require('../contracts/test');
+const lightningBolt12 = require('../functions/lightningBolt12');
+const bolt12Semantics = require('../functions/bolt12Semantics');
+const fabricPaymentBech32 = require('../functions/fabricPaymentBech32');
+
function redactSensitiveCommandArg (arg) {
return String(arg).replace(
/((?:--?rpcpassword|--?rpcuser|--?rpcauth|--bitcoin-rpcpassword|--bitcoin-rpcuser)=).*/i,
From 7b564b1dc7c4f8673e817e0f9cd90a4ad8ee7c82 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Mon, 20 Apr 2026 12:11:15 -0400
Subject: [PATCH 05/27] Removed bolt12 semantic unit test
---
functions/fabricPaymentBech32.js | 11 -----
tests/bolt12.semantics.unit.js | 54 -----------------------
tests/lightning/lightning.service.unit.js | 8 ++++
3 files changed, 8 insertions(+), 65 deletions(-)
delete mode 100644 tests/bolt12.semantics.unit.js
diff --git a/functions/fabricPaymentBech32.js b/functions/fabricPaymentBech32.js
index 8c30d1f79..88c52564f 100644
--- a/functions/fabricPaymentBech32.js
+++ b/functions/fabricPaymentBech32.js
@@ -1,16 +1,5 @@
'use strict';
-/**
- * **Fabric-routed payment** references: **bech32m** with HRP **`fa`**, strings **`fa1…`**
- * (same checksum family as BOLT12 / Taproot addresses per [BIP 350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki)).
- *
- * This is **not** a Lightning invoice; it is an opaque handle for Fabric / Hub routing (e.g. document
- * purchase, mesh settlement) that wallets and UIs can recognize without conflicting with `ln…` strings.
- *
- * @module functions/fabricPaymentBech32
- * @see docs/FABRIC_PAYMENT_BECH32.md
- */
-
const bech32 = require('./bech32');
const lb = require('./lightningBolt12');
diff --git a/tests/bolt12.semantics.unit.js b/tests/bolt12.semantics.unit.js
deleted file mode 100644
index a8b9ec992..000000000
--- a/tests/bolt12.semantics.unit.js
+++ /dev/null
@@ -1,54 +0,0 @@
-'use strict';
-
-const assert = require('assert');
-const bs = require('../functions/bolt12Semantics');
-const Lightning = require('../services/lightning');
-
-describe('@fabric/core/functions/bolt12Semantics', function () {
- it('classifyDecodedBolt12 recognizes CLN-style type strings', function () {
- assert.strictEqual(bs.classifyDecodedBolt12({ type: 'bolt12 offer' }), bs.Bolt12StreamKind.bolt12_offer);
- assert.strictEqual(
- bs.classifyDecodedBolt12({ type: 'bolt12 invoice_request' }),
- bs.Bolt12StreamKind.bolt12_invoice_request
- );
- assert.strictEqual(
- bs.classifyDecodedBolt12({ type: 'bolt12 invoice' }),
- bs.Bolt12StreamKind.bolt12_invoice
- );
- assert.strictEqual(
- bs.classifyDecodedBolt12({ type: 'bolt11 invoice' }),
- bs.Bolt12StreamKind.bolt11_invoice
- );
- assert.strictEqual(bs.classifyDecodedBolt12(null), bs.Bolt12StreamKind.unknown);
- });
-
- it('bip340SignatureApplies only for invoice_request and invoice', function () {
- assert.strictEqual(bs.bip340SignatureApplies(bs.Bolt12StreamKind.bolt12_offer), false);
- assert.strictEqual(bs.bip340SignatureApplies(bs.Bolt12StreamKind.bolt12_invoice_request), true);
- assert.strictEqual(bs.bip340SignatureApplies(bs.Bolt12StreamKind.bolt12_invoice), true);
- });
-
- it('summarizeBolt12RecurrenceFromDecode collects known keys', function () {
- const decoded = {
- offer_recurrence: { period: 7, time_unit: 1 },
- invreq_recurrence_counter: 2,
- invoice_recurrence_basetime: 1700000000
- };
- const sum = bs.summarizeBolt12RecurrenceFromDecode(decoded);
- assert.ok(sum);
- assert.strictEqual(sum.invreq_recurrence_counter, 2);
- assert.ok(sum.offer_recurrence);
- });
-
- it('BOLT12_TLV exposes recurrence-related type numbers', function () {
- assert.strictEqual(bs.BOLT12_TLV.OFFER_RECURRENCE_COMPULSORY, 24);
- assert.strictEqual(bs.BOLT12_TLV.INVREQ_RECURRENCE_COUNTER, 92);
- assert.strictEqual(bs.BOLT12_TLV.SIGNATURE_MIN, 240);
- });
-});
-
-describe('@fabric/core/services/lightning Bolt12Semantics bridge', function () {
- it('Lightning.Bolt12Semantics re-exports bolt12Semantics', function () {
- assert.strictEqual(Lightning.Bolt12Semantics, bs);
- });
-});
diff --git a/tests/lightning/lightning.service.unit.js b/tests/lightning/lightning.service.unit.js
index 97cabef36..c7a34dd42 100644
--- a/tests/lightning/lightning.service.unit.js
+++ b/tests/lightning/lightning.service.unit.js
@@ -20,6 +20,14 @@ describe('@fabric/core/services/lightning (unit)', function () {
});
});
+ describe('Lightning BOLT12 static exports', function () {
+ it('re-exports bolt12Semantics as Bolt12Semantics', function () {
+ const bolt12Semantics = require('../../functions/bolt12Semantics');
+ assert.ok(Lightning.Bolt12Semantics, 'services/lightning.js must assign Lightning.Bolt12Semantics');
+ assert.strictEqual(Lightning.Bolt12Semantics, bolt12Semantics);
+ });
+ });
+
describe('defaultListenPortForNetwork', function () {
it('maps networks to conventional lightningd bind ports', function () {
assert.strictEqual(Lightning.defaultListenPortForNetwork('mainnet'), 9735);
From e00df041e1ba30e20c7da28d8a69feb6c1fe2be2 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Mon, 20 Apr 2026 13:12:45 -0400
Subject: [PATCH 06/27] Rework imports
---
services/lightning.js | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/services/lightning.js b/services/lightning.js
index a7a6e4e67..27c9b1bc2 100644
--- a/services/lightning.js
+++ b/services/lightning.js
@@ -17,10 +17,6 @@ const Machine = require('../types/machine');
// Contracts
const OP_TEST = require('../contracts/test');
-const lightningBolt12 = require('../functions/lightningBolt12');
-const bolt12Semantics = require('../functions/bolt12Semantics');
-const fabricPaymentBech32 = require('../functions/fabricPaymentBech32');
-
function redactSensitiveCommandArg (arg) {
return String(arg).replace(
/((?:--?rpcpassword|--?rpcuser|--?rpcauth|--bitcoin-rpcpassword|--bitcoin-rpcuser)=).*/i,
@@ -1000,10 +996,12 @@ Lightning.DOCS = Object.freeze({
lightningCompat: 'docs/LIGHTNING_COMPAT.md'
});
-Lightning.Bolt12 = lightningBolt12;
-
-Lightning.FabricPayment = fabricPaymentBech32;
-
Lightning.redactSensitiveCommandArg = redactSensitiveCommandArg;
+Object.assign(Lightning, {
+ Bolt12: require('../functions/lightningBolt12'),
+ FabricPayment: require('../functions/fabricPaymentBech32'),
+ Bolt12Semantics: require('../functions/bolt12Semantics')
+});
+
module.exports = Lightning;
From 74c9ac6ba385a426bb90d655950a2b34489975f2 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Mon, 20 Apr 2026 13:17:48 -0400
Subject: [PATCH 07/27] Add additional BOLT12 semantics test
---
tests/lightning/bolt12.semantics.unit.js | 47 ++++++++++++++++++++++++
1 file changed, 47 insertions(+)
create mode 100644 tests/lightning/bolt12.semantics.unit.js
diff --git a/tests/lightning/bolt12.semantics.unit.js b/tests/lightning/bolt12.semantics.unit.js
new file mode 100644
index 000000000..a278b0969
--- /dev/null
+++ b/tests/lightning/bolt12.semantics.unit.js
@@ -0,0 +1,47 @@
+'use strict';
+
+const assert = require('assert');
+const bs = require('../../functions/bolt12Semantics');
+
+describe('@fabric/core/functions/bolt12Semantics', function () {
+ it('classifyDecodedBolt12 recognizes CLN-style type strings', function () {
+ assert.strictEqual(bs.classifyDecodedBolt12({ type: 'bolt12 offer' }), bs.Bolt12StreamKind.bolt12_offer);
+ assert.strictEqual(
+ bs.classifyDecodedBolt12({ type: 'bolt12 invoice_request' }),
+ bs.Bolt12StreamKind.bolt12_invoice_request
+ );
+ assert.strictEqual(
+ bs.classifyDecodedBolt12({ type: 'bolt12 invoice' }),
+ bs.Bolt12StreamKind.bolt12_invoice
+ );
+ assert.strictEqual(
+ bs.classifyDecodedBolt12({ type: 'bolt11 invoice' }),
+ bs.Bolt12StreamKind.bolt11_invoice
+ );
+ assert.strictEqual(bs.classifyDecodedBolt12(null), bs.Bolt12StreamKind.unknown);
+ });
+
+ it('bip340SignatureApplies only for invoice_request and invoice', function () {
+ assert.strictEqual(bs.bip340SignatureApplies(bs.Bolt12StreamKind.bolt12_offer), false);
+ assert.strictEqual(bs.bip340SignatureApplies(bs.Bolt12StreamKind.bolt12_invoice_request), true);
+ assert.strictEqual(bs.bip340SignatureApplies(bs.Bolt12StreamKind.bolt12_invoice), true);
+ });
+
+ it('summarizeBolt12RecurrenceFromDecode collects known keys', function () {
+ const decoded = {
+ offer_recurrence: { period: 7, time_unit: 1 },
+ invreq_recurrence_counter: 2,
+ invoice_recurrence_basetime: 1700000000
+ };
+ const sum = bs.summarizeBolt12RecurrenceFromDecode(decoded);
+ assert.ok(sum);
+ assert.strictEqual(sum.invreq_recurrence_counter, 2);
+ assert.ok(sum.offer_recurrence);
+ });
+
+ it('BOLT12_TLV exposes recurrence-related type numbers', function () {
+ assert.strictEqual(bs.BOLT12_TLV.OFFER_RECURRENCE_COMPULSORY, 24);
+ assert.strictEqual(bs.BOLT12_TLV.INVREQ_RECURRENCE_COUNTER, 92);
+ assert.strictEqual(bs.BOLT12_TLV.SIGNATURE_MIN, 240);
+ });
+});
From 73742379e9dfb7373b725eaeb79a43f167f02f09 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Mon, 20 Apr 2026 13:37:08 -0400
Subject: [PATCH 08/27] Rework the secure memory
---
functions/fabricPaymentBech32.js | 26 ++++++++--
src/security.c | 59 ++++++++++++++++++-----
src/sha2.c | 46 +++++++++++++++---
src/sha2.h | 8 +++
tests/bitcoin/transaction.js | 3 +-
tests/fabricPaymentBech32.unit.js | 23 +++++++++
tests/lightning/lightning.service.unit.js | 10 +++-
types/bitcoin/transaction.js | 18 ++++---
8 files changed, 158 insertions(+), 35 deletions(-)
diff --git a/functions/fabricPaymentBech32.js b/functions/fabricPaymentBech32.js
index 88c52564f..ddea2a379 100644
--- a/functions/fabricPaymentBech32.js
+++ b/functions/fabricPaymentBech32.js
@@ -21,6 +21,25 @@ const FabricRouteType = Object.freeze({
DOCUMENT_CONTENT_HASH: 0
});
+/**
+ * @param {Array|unknown} arr
+ * @returns {Buffer}
+ */
+function hash32ByteArrayToBuffer (arr) {
+ if (!Array.isArray(arr) || arr.length !== 32) {
+ throw new TypeError('hash32 array must have exactly 32 elements');
+ }
+ const out = Buffer.alloc(32);
+ for (let i = 0; i < 32; i++) {
+ const x = arr[i];
+ if (!Number.isInteger(x) || x < 0 || x > 255) {
+ throw new TypeError('hash32 array elements must be integers in 0..255');
+ }
+ out[i] = x;
+ }
+ return out;
+}
+
/**
* @param {string|null|undefined} s
* @returns {boolean}
@@ -41,7 +60,7 @@ function encodeFabricRoutedPaymentV0 (o) {
let buf;
if (Buffer.isBuffer(o.hash32)) buf = o.hash32;
else if (o.hash32 instanceof Uint8Array) buf = Buffer.from(o.hash32);
- else if (Array.isArray(o.hash32)) buf = Buffer.from(o.hash32);
+ else if (Array.isArray(o.hash32)) buf = hash32ByteArrayToBuffer(o.hash32);
else throw new TypeError('hash32 must be Buffer, Uint8Array, or byte array');
if (buf.length !== 32) throw new TypeError('hash32 must be exactly 32 bytes');
const routeType = o.routeType != null ? Number(o.routeType) : FabricRouteType.DOCUMENT_CONTENT_HASH;
@@ -73,12 +92,11 @@ function decodeFabricRoutedPayment (str) {
} catch {
return null;
}
- if (raw.length < 2 + 32) return null;
+ if (raw.length !== 34) return null;
const version = raw[0];
const routeType = raw[1];
const hash32 = Buffer.from(raw.slice(2, 34));
if (version !== FABRIC_PAYMENT_VERSION) return null;
- if (raw.length !== 34) return null;
return {
version,
routeType,
@@ -97,7 +115,7 @@ function classifyPaymentEncodingString (s) {
const t = String(s || '').trim();
if (!t) return 'empty';
if (isFabricRoutedPaymentString(t)) return 'fabric_routed_payment';
- return lb.classifyLightningEncodedString(s);
+ return lb.classifyLightningEncodedString(t);
}
module.exports = {
diff --git a/src/security.c b/src/security.c
index 307d7f925..2896c79db 100644
--- a/src/security.c
+++ b/src/security.c
@@ -91,19 +91,21 @@ void fabric_secure_memory_cleanup(void)
memory_initialized = 0;
}
-static void track_allocation(void *ptr, size_t size)
+/** @return 0 on success, -1 if tracking metadata could not be allocated */
+static int track_allocation(void *ptr, size_t size)
{
if (!ptr || size == 0)
- return;
+ return 0;
allocation_record_t *record = malloc(sizeof(allocation_record_t));
if (!record)
- return; // Unable to track, but don't fail the allocation
+ return -1;
record->ptr = ptr;
record->size = size;
record->next = allocations;
allocations = record;
+ return 0;
}
static size_t untrack_allocation(void *ptr)
@@ -153,8 +155,12 @@ void *fabric_secure_malloc(size_t size)
// Zero initialize
memset(ptr, 0, size);
- // Track the allocation
- track_allocation(ptr, size);
+ // Track the allocation (fail closed: untracked secure buffers are unsafe to hand out)
+ if (track_allocation(ptr, size) != 0)
+ {
+ free(ptr);
+ return NULL;
+ }
return ptr;
}
@@ -196,6 +202,11 @@ void *fabric_secure_realloc(void *ptr, size_t old_size, size_t new_size)
return fabric_secure_malloc(new_size);
}
+ if (old_size == 0)
+ {
+ old_size = fabric_secure_malloc_size(ptr);
+ }
+
void *new_ptr = fabric_secure_malloc(new_size);
if (!new_ptr)
{
@@ -381,16 +392,34 @@ FabricError fabric_secure_random_bytes(uint8_t *buffer, size_t length)
#ifdef __linux__
// Linux: Try getrandom() first (available since kernel 3.17)
- ssize_t result = getrandom(buffer, length, 0);
- if (result == (ssize_t)length)
{
- return FABRIC_SUCCESS;
- }
+ size_t offset = 0;
+ while (offset < length)
+ {
+ ssize_t result;
+ do
+ {
+ result = getrandom(buffer + offset, length - offset, 0);
+ } while (result < 0 && errno == EINTR);
- // If getrandom() fails, fall back to /dev/urandom
- if (result < 0 && errno != ENOSYS)
- {
- return FABRIC_ERROR_SYSTEM_CALL_FAILED;
+ if (result < 0)
+ {
+ if (errno != ENOSYS)
+ {
+ return FABRIC_ERROR_SYSTEM_CALL_FAILED;
+ }
+ break; /* fall back to /dev/urandom */
+ }
+ if (result == 0)
+ {
+ return FABRIC_ERROR_SYSTEM_CALL_FAILED;
+ }
+ offset += (size_t)result;
+ }
+ if (offset == length)
+ {
+ return FABRIC_SUCCESS;
+ }
}
#endif
@@ -580,6 +609,10 @@ FabricError fabric_secure_random_test_entropy(void)
consecutive_count = 0;
}
}
+ if (consecutive_count > max_consecutive)
+ {
+ max_consecutive = consecutive_count;
+ }
// Fail if more than 5% of bytes are consecutive duplicates
if (max_consecutive > (test_size * 0.05))
diff --git a/src/sha2.c b/src/sha2.c
index de2375ce5..1d810f831 100644
--- a/src/sha2.c
+++ b/src/sha2.c
@@ -1,4 +1,5 @@
#include "sha2.h"
+#include "memory.h"
#include
@@ -263,7 +264,8 @@ static void fabric_sha512_final(fabric_sha512_ctx *ctx, uint8_t hash[64])
int fabric_sha256(const uint8_t *data, size_t len, uint8_t out32[32])
{
- if (!data || !out32) return 0;
+ if (!out32) return 0;
+ if (!data && len != 0) return 0;
fabric_sha256_ctx ctx;
fabric_sha256_init(&ctx);
fabric_sha256_update(&ctx, data, len);
@@ -273,7 +275,8 @@ int fabric_sha256(const uint8_t *data, size_t len, uint8_t out32[32])
int fabric_sha512(const uint8_t *data, size_t len, uint8_t out64[64])
{
- if (!data || !out64) return 0;
+ if (!out64) return 0;
+ if (!data && len != 0) return 0;
fabric_sha512_ctx ctx;
fabric_sha512_init(&ctx);
fabric_sha512_update(&ctx, data, len);
@@ -292,13 +295,25 @@ int fabric_hmac_sha512(const uint8_t *key, size_t key_len,
uint8_t inner[64];
size_t i;
- if (!key || !data || !out64) return 0;
+#define FABRIC_HMAC_SHA512_WIPE() do { \
+ fabric_secure_zero(key_block, sizeof(key_block)); \
+ fabric_secure_zero(key_hash, sizeof(key_hash)); \
+ fabric_secure_zero(o_key_pad, sizeof(o_key_pad)); \
+ fabric_secure_zero(i_key_pad, sizeof(i_key_pad)); \
+ fabric_secure_zero(inner, sizeof(inner)); \
+ } while (0)
+
+ if (!out64) return 0;
+ if ((key == NULL && key_len > 0) || (data == NULL && data_len > 0)) return 0;
memset(key_block, 0, sizeof(key_block));
if (key_len > sizeof(key_block)) {
- if (!fabric_sha512(key, key_len, key_hash)) return 0;
+ if (!fabric_sha512(key, key_len, key_hash)) {
+ FABRIC_HMAC_SHA512_WIPE();
+ return 0;
+ }
memcpy(key_block, key_hash, sizeof(key_hash));
- } else {
+ } else if (key_len > 0 && key != NULL) {
memcpy(key_block, key, key_len);
}
@@ -317,6 +332,7 @@ int fabric_hmac_sha512(const uint8_t *key, size_t key_len,
fabric_sha512_update(&ctx, o_key_pad, sizeof(o_key_pad));
fabric_sha512_update(&ctx, inner, sizeof(inner));
fabric_sha512_final(&ctx, out64);
+ FABRIC_HMAC_SHA512_WIPE();
return 1;
}
@@ -331,7 +347,14 @@ int fabric_pbkdf2_hmac_sha512(const uint8_t *password, size_t password_len,
uint8_t t[64];
uint8_t sbuf[256];
- if (!password || !salt || !out || iterations == 0) return 0;
+#define FABRIC_PBKDF2_WIPE() do { \
+ fabric_secure_zero(u, sizeof(u)); \
+ fabric_secure_zero(t, sizeof(t)); \
+ fabric_secure_zero(sbuf, sizeof(sbuf)); \
+ } while (0)
+
+ if (!out || iterations == 0) return 0;
+ if ((password == NULL && password_len > 0) || (salt == NULL && salt_len > 0)) return 0;
if (salt_len + 4 > sizeof(sbuf)) return 0;
while (generated < out_len) {
@@ -341,11 +364,17 @@ int fabric_pbkdf2_hmac_sha512(const uint8_t *password, size_t password_len,
sbuf[salt_len + 2] = (uint8_t)((block_index >> 8) & 0xff);
sbuf[salt_len + 3] = (uint8_t)(block_index & 0xff);
- if (!fabric_hmac_sha512(password, password_len, sbuf, salt_len + 4, u)) return 0;
+ if (!fabric_hmac_sha512(password, password_len, sbuf, salt_len + 4, u)) {
+ FABRIC_PBKDF2_WIPE();
+ return 0;
+ }
memcpy(t, u, sizeof(t));
for (uint32_t i = 1; i < iterations; i++) {
- if (!fabric_hmac_sha512(password, password_len, u, sizeof(u), u)) return 0;
+ if (!fabric_hmac_sha512(password, password_len, u, sizeof(u), u)) {
+ FABRIC_PBKDF2_WIPE();
+ return 0;
+ }
for (size_t j = 0; j < sizeof(t); j++) t[j] ^= u[j];
}
@@ -355,5 +384,6 @@ int fabric_pbkdf2_hmac_sha512(const uint8_t *password, size_t password_len,
block_index++;
}
+ FABRIC_PBKDF2_WIPE();
return 1;
}
diff --git a/src/sha2.h b/src/sha2.h
index b9f9f64d6..4ddb203d2 100644
--- a/src/sha2.h
+++ b/src/sha2.h
@@ -4,6 +4,10 @@
#include
#include
+#ifdef __cplusplus
+extern "C" {
+#endif
+
int fabric_sha256(const uint8_t *data, size_t len, uint8_t out32[32]);
int fabric_sha512(const uint8_t *data, size_t len, uint8_t out64[64]);
int fabric_hmac_sha512(const uint8_t *key, size_t key_len,
@@ -14,4 +18,8 @@ int fabric_pbkdf2_hmac_sha512(const uint8_t *password, size_t password_len,
uint32_t iterations,
uint8_t *out, size_t out_len);
+#ifdef __cplusplus
+}
+#endif
+
#endif
diff --git a/tests/bitcoin/transaction.js b/tests/bitcoin/transaction.js
index 84c9afbe0..2908e6e9d 100644
--- a/tests/bitcoin/transaction.js
+++ b/tests/bitcoin/transaction.js
@@ -23,7 +23,8 @@ describe('@fabric/core/types/bitcoin', function () {
it('exposes identifier properties', function () {
const tx = new BitcoinTransaction({ raw: 'deadbeef' });
- assert.strictEqual(tx._state.content.raw, 'deadbeef');
+ assert.ok(Buffer.isBuffer(tx._state.content.raw));
+ assert.deepStrictEqual(tx._state.content.raw, Buffer.from('deadbeef', 'hex'));
assert.strictEqual(tx.hash, '281dd50f6f56bc6e867fe73dd614a73c55a647a479704f64804b574cafb0f5c5');
assert.strictEqual(tx.txid, 'c5f5b0af4c574b80644f7079a447a6553ca714d63de77f866ebc566f0fd51d28');
assert.strictEqual(tx.id, tx.txid);
diff --git a/tests/fabricPaymentBech32.unit.js b/tests/fabricPaymentBech32.unit.js
index 40b1655a5..3bc3e0892 100644
--- a/tests/fabricPaymentBech32.unit.js
+++ b/tests/fabricPaymentBech32.unit.js
@@ -55,6 +55,29 @@ describe('@fabric/core/functions/fabricPaymentBech32', function () {
assert.strictEqual(fp.isFabricRoutedPaymentString(''), false);
});
+ it('encodeFabricRoutedPaymentV0 rejects malformed byte array hash32', function () {
+ assert.throws(
+ () => fp.encodeFabricRoutedPaymentV0({ hash32: [0, 1, 2] }),
+ /exactly 32 elements/
+ );
+ assert.throws(
+ () => fp.encodeFabricRoutedPaymentV0({ hash32: Array(32).fill(1.5) }),
+ /0\.\.255/
+ );
+ assert.throws(
+ () => fp.encodeFabricRoutedPaymentV0({ hash32: Array(32).fill(256) }),
+ /0\.\.255/
+ );
+ });
+
+ it('encodeFabricRoutedPaymentV0 accepts validated 32-byte number array', function () {
+ const arr = Array.from(crypto.randomBytes(32));
+ const enc = fp.encodeFabricRoutedPaymentV0({ hash32: arr });
+ const dec = fp.decodeFabricRoutedPayment(enc);
+ assert.ok(dec);
+ assert.deepStrictEqual(Array.from(dec.hash32), arr);
+ });
+
it('encodeFabricRoutedPaymentV0 accepts Uint8Array hash32', function () {
const u8 = new Uint8Array(32);
u8[0] = 9;
diff --git a/tests/lightning/lightning.service.unit.js b/tests/lightning/lightning.service.unit.js
index c7a34dd42..bf6ee3fbf 100644
--- a/tests/lightning/lightning.service.unit.js
+++ b/tests/lightning/lightning.service.unit.js
@@ -23,7 +23,15 @@ describe('@fabric/core/services/lightning (unit)', function () {
describe('Lightning BOLT12 static exports', function () {
it('re-exports bolt12Semantics as Bolt12Semantics', function () {
const bolt12Semantics = require('../../functions/bolt12Semantics');
- assert.ok(Lightning.Bolt12Semantics, 'services/lightning.js must assign Lightning.Bolt12Semantics');
+ const lightningPath = require.resolve('../../services/lightning');
+ const src = fs.readFileSync(lightningPath, 'utf8');
+ assert.ok(
+ /\bBolt12Semantics\b/.test(src) && src.includes('bolt12Semantics'),
+ `expected ${path.relative(process.cwd(), lightningPath)} to wire Bolt12Semantics`
+ );
+ assert.ok(Lightning.Bolt12, 'Lightning.Bolt12');
+ assert.ok(Lightning.FabricPayment, 'Lightning.FabricPayment');
+ assert.ok(Lightning.Bolt12Semantics, 'Lightning.Bolt12Semantics');
assert.strictEqual(Lightning.Bolt12Semantics, bolt12Semantics);
});
});
diff --git a/types/bitcoin/transaction.js b/types/bitcoin/transaction.js
index 516a20da1..741dc6ffd 100644
--- a/types/bitcoin/transaction.js
+++ b/types/bitcoin/transaction.js
@@ -11,11 +11,8 @@ function rawTransactionBuffer (raw) {
if (Buffer.isBuffer(raw)) return raw;
const s = String(raw).replace(/\s+/g, '');
if (!s.length || s.length % 2 !== 0) return Buffer.alloc(0);
- try {
- return Buffer.from(s, 'hex');
- } catch {
- return Buffer.alloc(0);
- }
+ if (!/^[0-9a-fA-F]+$/.test(s)) return Buffer.alloc(0);
+ return Buffer.from(s, 'hex');
}
function bitcoinTxidHex (buf) {
@@ -29,6 +26,11 @@ function doubleSha256Hex (buf) {
return crypto.createHash('sha256').update(h).digest('hex');
}
+function doubleSha256Buf (buf) {
+ const h = crypto.createHash('sha256').update(buf).digest();
+ return crypto.createHash('sha256').update(h).digest();
+}
+
class BitcoinTransaction extends Actor {
constructor (settings = {}) {
super(settings);
@@ -46,7 +48,7 @@ class BitcoinTransaction extends Actor {
this._state = {
content: {
- raw: this.settings.raw != null ? this.settings.raw : null
+ raw: this.settings.raw != null ? rawTransactionBuffer(this.settings.raw) : null
},
status: 'PAUSED'
};
@@ -71,8 +73,8 @@ class BitcoinTransaction extends Actor {
}
signAsHolder () {
- const digestHex = doubleSha256Hex(this._rawBuf());
- this.signature = this.holder.sign(digestHex);
+ const digest = doubleSha256Buf(this._rawBuf());
+ this.signature = this.holder.signSchnorrHash(digest);
return this;
}
}
From 090ca709067d16c244354c6e26d2a7203709aaaa Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Tue, 21 Apr 2026 16:37:32 -0400
Subject: [PATCH 09/27] Update package-lock.json
---
package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index b9b0a43b6..604f4c3e0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4568,9 +4568,9 @@
}
},
"node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
From 21ff49d4a3ac5aa470cfb1562aa4c334298bd3f1 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Tue, 21 Apr 2026 18:28:30 -0400
Subject: [PATCH 10/27] Fix regression in environment detection
---
tests/fabric.environment.js | 16 ++++++++++++++++
types/environment.js | 9 ++++++---
2 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/tests/fabric.environment.js b/tests/fabric.environment.js
index 044a98977..3e686a0d7 100644
--- a/tests/fabric.environment.js
+++ b/tests/fabric.environment.js
@@ -30,6 +30,22 @@ describe('@fabric/core/types/environment', function () {
assert.ok(environment);
});
+ it('does not treat the BIP39 fixture as the default seed when NODE_ENV is not test (fresh install)', function () {
+ const prev = process.env.NODE_ENV;
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'fabric-env-'));
+ const store = path.join(home, '.fabric');
+ const walletPath = path.join(store, 'wallet.json');
+ process.env.NODE_ENV = 'production';
+ try {
+ const environment = new Environment({ home, path: walletPath, store });
+ environment.start();
+ assert.strictEqual(environment.wallet, false, 'in-memory wallet must not be created from the test fixture in production');
+ } finally {
+ process.env.NODE_ENV = prev;
+ fs.rmSync(home, { recursive: true, force: true });
+ }
+ });
+
it('can instantiate from a seed', async function () {
const environment = new Environment({ xpub: FIXTURE_SEED });
await environment.start();
diff --git a/types/environment.js b/types/environment.js
index 4069cb9ff..7e830f251 100644
--- a/types/environment.js
+++ b/types/environment.js
@@ -83,12 +83,15 @@ class Environment extends Entity {
}
get seed () {
- return [
- FIXTURE_SEED,
+ const explicit = [
this.settings.seed,
this['FABRIC_SEED'],
this.readVariable('FABRIC_SEED')
- ].find(any);
+ ];
+ if (process.env.NODE_ENV === 'test') {
+ explicit.unshift(FIXTURE_SEED);
+ }
+ return explicit.find(any);
}
get xprv () {
From 2faa6ce9bd2252e17b14a05237de83927933d3b1 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Tue, 21 Apr 2026 22:03:41 -0400
Subject: [PATCH 11/27] Address various issues, expand tests
---
functions/bolt12Semantics.js | 15 +++++++++++++--
services/lightning.js | 3 ---
tests/fabric.chain.js | 1 +
tests/lightning/bolt12.semantics.unit.js | 19 +++++++++++++++----
types/bitcoin/transaction.js | 5 ++++-
types/chain.js | 2 +-
types/environment.js | 5 +++--
7 files changed, 37 insertions(+), 13 deletions(-)
diff --git a/functions/bolt12Semantics.js b/functions/bolt12Semantics.js
index ea37e81ba..36c842b9c 100644
--- a/functions/bolt12Semantics.js
+++ b/functions/bolt12Semantics.js
@@ -17,10 +17,10 @@
*/
/**
- * TLV types named in BOLT #12 (offer / invoice_request / invoice ranges differ by stream).
+ * TLV type numbers for BOLT #12 and Core Lightning `decode` fields (offer / invoice_request / invoice).
* @readonly
*/
-const BOLT12_TLV = Object.freeze({
+const CLN_BOLT12_TLV = Object.freeze({
INVREQ_METADATA: 0,
OFFER_CHAINS: 2,
OFFER_METADATA: 4,
@@ -40,8 +40,12 @@ const BOLT12_TLV = Object.freeze({
OFFER_RECURRENCE_LIMIT: 29,
INVREQ_CHAIN: 80,
INVREQ_AMOUNT: 82,
+ INVREQ_FEATURES: 84,
INVREQ_QUANTITY: 86,
INVREQ_PAYER_ID: 88,
+ INVREQ_PAYER_NOTE: 89,
+ INVREQ_PATHS: 90,
+ INVREQ_BIP353_NAME: 91,
INVREQ_RECURRENCE_COUNTER: 92,
INVREQ_RECURRENCE_START: 93,
INVREQ_RECURRENCE_CANCEL: 94,
@@ -50,12 +54,18 @@ const BOLT12_TLV = Object.freeze({
INVOICE_PATHS: 160,
INVOICE_BLINDEDPAY: 162,
INVOICE_CREATED_AT: 164,
+ INVOICE_RELATIVE_EXPIRY: 166,
INVOICE_PAYMENT_HASH: 168,
INVOICE_AMOUNT: 170,
+ INVOICE_FALLBACKS: 172,
+ INVOICE_FEATURES: 174,
INVOICE_NODE_ID: 176,
INVOICE_RECURRENCE_BASETIME: 177
});
+/** @deprecated Use {@link CLN_BOLT12_TLV} — name kept for existing imports. */
+const BOLT12_TLV = CLN_BOLT12_TLV;
+
/**
* Normalized kind for a value returned by **`decode`** / **`decodeLightning`**.
* @readonly
@@ -143,6 +153,7 @@ function summarizeBolt12RecurrenceFromDecode (decoded) {
module.exports = {
BOLT12_TLV,
+ CLN_BOLT12_TLV,
Bolt12StreamKind,
bip340SignatureApplies,
classifyDecodedBolt12,
diff --git a/services/lightning.js b/services/lightning.js
index 27c9b1bc2..528fb4fb8 100644
--- a/services/lightning.js
+++ b/services/lightning.js
@@ -35,9 +35,6 @@ function shouldTreatLightningStderrAsError (line) {
);
}
-/**
- * Node does not chunk stderr on line boundaries; buffer and emit per logical line.
- */
function appendLightningStderrChunk (service, chunk) {
if (!service._lightningdStderrBuf) service._lightningdStderrBuf = '';
service._lightningdStderrBuf += chunk.toString('utf8');
diff --git a/tests/fabric.chain.js b/tests/fabric.chain.js
index 0ec5ee181..ae789a2d2 100644
--- a/tests/fabric.chain.js
+++ b/tests/fabric.chain.js
@@ -99,6 +99,7 @@ describe('@fabric/core/types/chain', function () {
const tx2 = chain.proposeTransaction(payload);
assert.strictEqual(tx1.id, tx2.id);
+ assert.strictEqual(tx1, tx2, 'duplicate proposal should return the same Transaction instance');
assert.strictEqual(chain.mempool.filter((id) => id === tx1.id).length, 1);
await chain.stop();
});
diff --git a/tests/lightning/bolt12.semantics.unit.js b/tests/lightning/bolt12.semantics.unit.js
index a278b0969..36359d919 100644
--- a/tests/lightning/bolt12.semantics.unit.js
+++ b/tests/lightning/bolt12.semantics.unit.js
@@ -39,9 +39,20 @@ describe('@fabric/core/functions/bolt12Semantics', function () {
assert.ok(sum.offer_recurrence);
});
- it('BOLT12_TLV exposes recurrence-related type numbers', function () {
- assert.strictEqual(bs.BOLT12_TLV.OFFER_RECURRENCE_COMPULSORY, 24);
- assert.strictEqual(bs.BOLT12_TLV.INVREQ_RECURRENCE_COUNTER, 92);
- assert.strictEqual(bs.BOLT12_TLV.SIGNATURE_MIN, 240);
+ it('BOLT12_TLV aliases CLN_BOLT12_TLV', function () {
+ assert.strictEqual(bs.BOLT12_TLV, bs.CLN_BOLT12_TLV);
+ });
+
+ it('CLN_BOLT12_TLV exposes recurrence and BOLT #12 invoice_request / invoice TLV ids', function () {
+ assert.strictEqual(bs.CLN_BOLT12_TLV.OFFER_RECURRENCE_COMPULSORY, 24);
+ assert.strictEqual(bs.CLN_BOLT12_TLV.INVREQ_FEATURES, 84);
+ assert.strictEqual(bs.CLN_BOLT12_TLV.INVREQ_PAYER_NOTE, 89);
+ assert.strictEqual(bs.CLN_BOLT12_TLV.INVREQ_PATHS, 90);
+ assert.strictEqual(bs.CLN_BOLT12_TLV.INVREQ_BIP353_NAME, 91);
+ assert.strictEqual(bs.CLN_BOLT12_TLV.INVREQ_RECURRENCE_COUNTER, 92);
+ assert.strictEqual(bs.CLN_BOLT12_TLV.INVOICE_RELATIVE_EXPIRY, 166);
+ assert.strictEqual(bs.CLN_BOLT12_TLV.INVOICE_FALLBACKS, 172);
+ assert.strictEqual(bs.CLN_BOLT12_TLV.INVOICE_FEATURES, 174);
+ assert.strictEqual(bs.CLN_BOLT12_TLV.SIGNATURE_MIN, 240);
});
});
diff --git a/types/bitcoin/transaction.js b/types/bitcoin/transaction.js
index 741dc6ffd..9396eb3c3 100644
--- a/types/bitcoin/transaction.js
+++ b/types/bitcoin/transaction.js
@@ -57,7 +57,10 @@ class BitcoinTransaction extends Actor {
}
_rawBuf () {
- return rawTransactionBuffer(this.settings.raw);
+ if (this._state.content.raw != null) {
+ return this._state.content.raw;
+ }
+ return Buffer.alloc(0);
}
get hash () {
diff --git a/types/chain.js b/types/chain.js
index 8d5b276ed..56836c18c 100644
--- a/types/chain.js
+++ b/types/chain.js
@@ -137,7 +137,7 @@ class Chain extends Actor {
const prior = this._state.transactions[actor.id];
if (prior) {
- return prior instanceof Transaction ? prior : new Transaction(prior);
+ return prior;
}
this._state.transactions[actor.id] = actor;
diff --git a/types/environment.js b/types/environment.js
index 7e830f251..3544ae3c3 100644
--- a/types/environment.js
+++ b/types/environment.js
@@ -86,10 +86,11 @@ class Environment extends Entity {
const explicit = [
this.settings.seed,
this['FABRIC_SEED'],
- this.readVariable('FABRIC_SEED')
+ this.readVariable('FABRIC_SEED'),
+ this.local
];
if (process.env.NODE_ENV === 'test') {
- explicit.unshift(FIXTURE_SEED);
+ explicit.push(FIXTURE_SEED);
}
return explicit.find(any);
}
From 429af98abafbde7208825299ea470fc8e39aae07 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Tue, 21 Apr 2026 22:23:18 -0400
Subject: [PATCH 12/27] Address a number of comments
---
reports/install.log | 2 +-
services/lightning.js | 22 +++++++++++++++++++--
tests/fabric.environment.js | 17 +++++++++++++---
tests/lightning/lightning.service.unit.js | 24 +++++++++++++++++++----
4 files changed, 55 insertions(+), 10 deletions(-)
diff --git a/reports/install.log b/reports/install.log
index ffac104ee..6cadf7348 100644
--- a/reports/install.log
+++ b/reports/install.log
@@ -10,7 +10,7 @@ $ npm i
CC(target) Release/obj.target/fabric/src/p2p.o
SOLINK_MODULE(target) Release/fabric.node
-added 665 packages, and audited 666 packages in 32s
+added 666 packages, and audited 667 packages in 54s
109 packages are looking for funding
run `npm fund` for details
diff --git a/services/lightning.js b/services/lightning.js
index 528fb4fb8..61c27cbca 100644
--- a/services/lightning.js
+++ b/services/lightning.js
@@ -293,6 +293,9 @@ class Lightning extends Service {
async fetchInvoice (offerOrParams, invoiceParams = null) {
if (offerOrParams != null && typeof offerOrParams === 'object' && !Array.isArray(offerOrParams) && typeof offerOrParams.offer === 'string') {
+ if (invoiceParams != null && typeof invoiceParams === 'object' && !Array.isArray(invoiceParams)) {
+ return this._makeRPCRequest('fetchinvoice', [Object.assign({}, offerOrParams, invoiceParams)]);
+ }
return this._makeRPCRequest('fetchinvoice', [offerOrParams]);
}
if (typeof offerOrParams !== 'string') {
@@ -348,9 +351,24 @@ class Lightning extends Service {
return this._makeRPCRequest('sendinvoice', [params]);
}
- async getRoute (destinationId, amountMsat, riskfactor = 10, maxHopsOrExtra = null) {
+ async getRoute (destinationId, amountMsat, riskfactor = 10, cltvOrRouteOptions = null) {
+ if (cltvOrRouteOptions != null && typeof cltvOrRouteOptions === 'object' && !Array.isArray(cltvOrRouteOptions)) {
+ const o = cltvOrRouteOptions;
+ return this._makeRPCRequest('getroute', [
+ destinationId,
+ amountMsat,
+ riskfactor,
+ o.cltv != null ? o.cltv : null,
+ o.fromid != null ? o.fromid : null,
+ o.fuzzpercent != null ? o.fuzzpercent : null,
+ o.exclude != null ? o.exclude : null,
+ o.maxhops != null ? o.maxhops : null
+ ]);
+ }
const args = [destinationId, amountMsat, riskfactor];
- if (maxHopsOrExtra != null) args.push(maxHopsOrExtra);
+ if (cltvOrRouteOptions != null) {
+ args.push(cltvOrRouteOptions);
+ }
return this._makeRPCRequest('getroute', args);
}
diff --git a/tests/fabric.environment.js b/tests/fabric.environment.js
index 3e686a0d7..fd58ab4eb 100644
--- a/tests/fabric.environment.js
+++ b/tests/fabric.environment.js
@@ -31,17 +31,28 @@ describe('@fabric/core/types/environment', function () {
});
it('does not treat the BIP39 fixture as the default seed when NODE_ENV is not test (fresh install)', function () {
- const prev = process.env.NODE_ENV;
+ const prev = {
+ NODE_ENV: process.env.NODE_ENV,
+ FABRIC_SEED: process.env.FABRIC_SEED,
+ FABRIC_XPRV: process.env.FABRIC_XPRV,
+ FABRIC_XPUB: process.env.FABRIC_XPUB
+ };
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'fabric-env-'));
const store = path.join(home, '.fabric');
const walletPath = path.join(store, 'wallet.json');
- process.env.NODE_ENV = 'production';
try {
+ delete process.env.FABRIC_SEED;
+ delete process.env.FABRIC_XPRV;
+ delete process.env.FABRIC_XPUB;
+ process.env.NODE_ENV = 'production';
const environment = new Environment({ home, path: walletPath, store });
environment.start();
assert.strictEqual(environment.wallet, false, 'in-memory wallet must not be created from the test fixture in production');
} finally {
- process.env.NODE_ENV = prev;
+ for (const k of Object.keys(prev)) {
+ if (prev[k] === undefined) delete process.env[k];
+ else process.env[k] = prev[k];
+ }
fs.rmSync(home, { recursive: true, force: true });
}
});
diff --git a/tests/lightning/lightning.service.unit.js b/tests/lightning/lightning.service.unit.js
index bf6ee3fbf..a65aa0200 100644
--- a/tests/lightning/lightning.service.unit.js
+++ b/tests/lightning/lightning.service.unit.js
@@ -278,7 +278,7 @@ describe('@fabric/core/services/lightning (unit)', function () {
await assert.rejects(() => ln.createOffer(null), /requires a params object/);
});
- it('fetchInvoice passes offer only, offer + params, or single keyword object', async function () {
+ it('fetchInvoice passes offer only, offer + params, keyword object, or merged keyword objects', async function () {
const ln = new Lightning();
let calls = 0;
ln._makeRPCRequest = async (method, params) => {
@@ -286,12 +286,21 @@ describe('@fabric/core/services/lightning (unit)', function () {
assert.strictEqual(method, 'fetchinvoice');
if (calls === 1) assert.deepStrictEqual(params, ['lno1a']);
else if (calls === 2) assert.deepStrictEqual(params, ['lno1b', { amount_msat: '5000' }]);
- else assert.deepStrictEqual(params, [{ offer: 'lno1c', amount_msat: '1000', payer_note: 'hi' }]);
+ else if (calls === 3) {
+ assert.deepStrictEqual(params, [{ offer: 'lno1c', amount_msat: '1000', payer_note: 'hi' }]);
+ } else {
+ assert.deepStrictEqual(params, [{
+ offer: 'lno1d',
+ amount_msat: '1000',
+ payer_note: 'merged'
+ }]);
+ }
return {};
};
await ln.fetchInvoice('lno1a');
await ln.fetchInvoice('lno1b', { amount_msat: '5000' });
await ln.fetchInvoice({ offer: 'lno1c', amount_msat: '1000', payer_note: 'hi' });
+ await ln.fetchInvoice({ offer: 'lno1d', amount_msat: '1000' }, { payer_note: 'merged' });
});
it('fetchInvoice rejects invalid first arg', async function () {
@@ -391,18 +400,25 @@ describe('@fabric/core/services/lightning (unit)', function () {
await ln.disableOffer('offer_id_1');
});
- it('getRoute passes three or four args', async function () {
+ it('getRoute passes three args, fourth as cltv, or full tail via routeOptions (e.g. maxhops)', async function () {
const ln = new Lightning();
let n = 0;
ln._makeRPCRequest = async (method, params) => {
n++;
assert.strictEqual(method, 'getroute');
if (n === 1) assert.deepStrictEqual(params, ['dest', 1e7, 10]);
- else assert.deepStrictEqual(params, ['dest', 1e7, 1, 0]);
+ else if (n === 2) assert.deepStrictEqual(params, ['dest', 1e7, 1, 0]);
+ else {
+ assert.deepStrictEqual(
+ params,
+ ['dest', 1e7, 10, null, null, null, null, 5]
+ );
+ }
return { route: [] };
};
await ln.getRoute('dest', 1e7);
await ln.getRoute('dest', 1e7, 1, 0);
+ await ln.getRoute('dest', 1e7, 10, { maxhops: 5 });
});
});
From e510e99bee6d6bf1761186630904fc9918284616 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Wed, 22 Apr 2026 20:10:44 -0400
Subject: [PATCH 13/27] Update install report
---
reports/install.log | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/reports/install.log b/reports/install.log
index 6cadf7348..a7277c23f 100644
--- a/reports/install.log
+++ b/reports/install.log
@@ -10,9 +10,14 @@ $ npm i
CC(target) Release/obj.target/fabric/src/p2p.o
SOLINK_MODULE(target) Release/fabric.node
-added 666 packages, and audited 667 packages in 54s
+added 666 packages, and audited 667 packages in 1m
109 packages are looking for funding
run `npm fund` for details
-found 0 vulnerabilities
+2 moderate severity vulnerabilities
+
+To address all issues (including breaking changes), run:
+ npm audit fix --force
+
+Run `npm audit` for details.
From 360e5f6dc583f816fc3f442fca24051aed2e96d5 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Wed, 22 Apr 2026 20:50:00 -0400
Subject: [PATCH 14/27] Specify uuid version to fix jayson
---
package-lock.json | 12 ++++++++----
package.json | 3 +++
reports/install.log | 11 +++--------
3 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 604f4c3e0..0201ade67 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7620,12 +7620,16 @@
"license": "MIT"
},
"node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
+ "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
"license": "MIT",
"bin": {
- "uuid": "dist/bin/uuid"
+ "uuid": "dist-node/bin/uuid"
}
},
"node_modules/v8-to-istanbul": {
diff --git a/package.json b/package.json
index 450ff0252..ee4e7bcca 100644
--- a/package.json
+++ b/package.json
@@ -166,6 +166,9 @@
},
"merkletreejs": {
"@noble/hashes": "=1.7.1"
+ },
+ "jayson": {
+ "uuid": "14.0.0"
}
},
"devDependencies": {
diff --git a/reports/install.log b/reports/install.log
index a7277c23f..89362d578 100644
--- a/reports/install.log
+++ b/reports/install.log
@@ -10,14 +10,9 @@ $ npm i
CC(target) Release/obj.target/fabric/src/p2p.o
SOLINK_MODULE(target) Release/fabric.node
-added 666 packages, and audited 667 packages in 1m
+added 666 packages, and audited 667 packages in 46s
-109 packages are looking for funding
+110 packages are looking for funding
run `npm fund` for details
-2 moderate severity vulnerabilities
-
-To address all issues (including breaking changes), run:
- npm audit fix --force
-
-Run `npm audit` for details.
+found 0 vulnerabilities
From 16cdecc7dcee2f09bd17e6f4a00c85d28a7eedbb Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Fri, 24 Apr 2026 07:19:25 -0400
Subject: [PATCH 15/27] Update package-lock.json
---
examples/http.js | 3 +++
package-lock.json | 6 +++---
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/examples/http.js b/examples/http.js
index fa5c4e1a3..79da24ec1 100644
--- a/examples/http.js
+++ b/examples/http.js
@@ -1,4 +1,7 @@
// # Exposing ARCs with HTTP
+// Downstream apps set `path` to their `assets/` (or similar). Files there are served first; unhandled
+// paths fall through to the packaged `@fabric/http` `assets/` (e.g. Fomantic / semantic) — no extra
+// settings required. See `FabricHTTPServer` in `@fabric/http` `types/server.js` static middleware order.
// Fabric makes it easy to publish applications to the Web,
// giving downstream users access to a hosted instance of the
// application.
diff --git a/package-lock.json b/package-lock.json
index 0201ade67..ebf638d29 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1021,9 +1021,9 @@
}
},
"node_modules/ajv": {
- "version": "6.14.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
- "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
"license": "MIT",
"dependencies": {
From 450a520d8fc25fd1ae5b5a5505c2013e7d1b52e8 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Mon, 27 Apr 2026 21:14:03 -0400
Subject: [PATCH 16/27] Update install report, package-lock.json
---
package-lock.json | 6 +++---
reports/install.log | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index ebf638d29..2ebde5338 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3919,9 +3919,9 @@
"license": "ISC"
},
"node_modules/ip-address": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
- "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.1.tgz",
+ "integrity": "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==",
"license": "MIT",
"engines": {
"node": ">= 12"
diff --git a/reports/install.log b/reports/install.log
index 89362d578..70f170044 100644
--- a/reports/install.log
+++ b/reports/install.log
@@ -10,7 +10,7 @@ $ npm i
CC(target) Release/obj.target/fabric/src/p2p.o
SOLINK_MODULE(target) Release/fabric.node
-added 666 packages, and audited 667 packages in 46s
+added 666 packages, and audited 667 packages in 24s
110 packages are looking for funding
run `npm fund` for details
From bb868b230bb726ce3173750ede4d634b0f772246 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Tue, 28 Apr 2026 00:20:07 -0400
Subject: [PATCH 17/27] Update INVENTORY-related API
---
tests/fabric.peer.js | 36 +++++++++++
types/peer.js | 139 ++++++++++++++++++++++++++++++++-----------
2 files changed, 141 insertions(+), 34 deletions(-)
diff --git a/tests/fabric.peer.js b/tests/fabric.peer.js
index 7c91dce75..4177f6fec 100644
--- a/tests/fabric.peer.js
+++ b/tests/fabric.peer.js
@@ -767,6 +767,22 @@ describe('@fabric/core/types/peer', function () {
});
peer._handleGenericMessage({ type: 'INVENTORY_REQUEST', object: {}, message: {}, origin: {} }, { name: 'o' });
});
+ it('normalizes FABRIC_DOCUMENT_OFFER envelope to INVENTORY_REQUEST', function (done) {
+ const peer = new Peer({ listen: false, peersDb: null });
+ peer.once('inventory', (ev) => {
+ assert.strictEqual(ev.message.type, 'INVENTORY_REQUEST');
+ done();
+ });
+ peer._handleGenericMessage({ type: 'FABRIC_DOCUMENT_OFFER', object: {}, message: {}, origin: {} }, { name: 'o' });
+ });
+ it('normalizes FABRIC_DOCUMENT_OFFER_RESPONSE envelope to INVENTORY_RESPONSE', function (done) {
+ const peer = new Peer({ listen: false, peersDb: null });
+ peer.once('inventoryResponse', (ev) => {
+ assert.strictEqual(ev.message.type, 'INVENTORY_RESPONSE');
+ done();
+ });
+ peer._handleGenericMessage({ type: 'FABRIC_DOCUMENT_OFFER_RESPONSE', object: { kind: 'documents', items: [] } }, { name: 'o' });
+ });
it('serveLocalDocumentInventory sends INVENTORY_RESPONSE for offerBtc requests', function () {
const peer = new Peer({ listen: false, peersDb: null, serveLocalDocumentInventory: true });
peer._state.content.documents = { doca: 'hello' };
@@ -783,6 +799,7 @@ describe('@fabric/core/types/peer', function () {
const back = Message.fromBuffer(written);
const inner = JSON.parse(back.data);
assert.strictEqual(back.type, 'P2P_INVENTORY_RESPONSE');
+ assert.strictEqual(inner.kind, 'documents');
assert.strictEqual(inner.items.length, 1);
assert.strictEqual(inner.items[0].id, 'doca');
assert.strictEqual(inner.items[0].rateSats, 1000);
@@ -819,6 +836,25 @@ describe('@fabric/core/types/peer', function () {
}, { name: '127.0.0.1:13' });
assert.strictEqual(writes, 0);
});
+ it('serveLocalDocumentInventory responds to Hub-style kind:documents catalog requests', function () {
+ const peer = new Peer({ listen: false, peersDb: null, serveLocalDocumentInventory: true });
+ peer._state.content.documents = { catdoc: 'hello catalog' };
+ let written = null;
+ peer.connections['127.0.0.1:14'] = {
+ _writeFabric: (b) => { written = b; }
+ };
+ peer._handleGenericMessage({
+ type: 'INVENTORY_REQUEST',
+ object: { kind: 'documents', created: Date.now() }
+ }, { name: '127.0.0.1:14' });
+ assert.ok(written && written.length);
+ const back = Message.fromBuffer(written);
+ const inner = JSON.parse(back.data);
+ assert.strictEqual(inner.kind, 'documents');
+ assert.strictEqual(inner.items.length, 1);
+ assert.strictEqual(inner.items[0].id, 'catdoc');
+ assert.strictEqual(inner.items[0].published, true);
+ });
it('relayInventoryRequest relays offerBtc INVENTORY_REQUEST when no local items', function () {
const peer = new Peer({ listen: false, peersDb: null, serveLocalDocumentInventory: true, relayInventoryRequest: true });
peer._state.content.documents = {};
diff --git a/types/peer.js b/types/peer.js
index 5b3f6e768..2e6b3703b 100644
--- a/types/peer.js
+++ b/types/peer.js
@@ -34,6 +34,9 @@ const {
whitelistedDocumentFields,
purchaseContentHashHex
} = require('../functions/publishedDocumentEnvelope');
+const {
+ normalizeFabricDocumentOfferEnvelopeForHandlers
+} = require('../functions/fabricDocumentOfferEnvelope');
// Strict JSON
const {
@@ -131,7 +134,8 @@ class Peer extends Service {
port: 7777,
listenPortAttempts: 20,
reconnectToKnownPeers: true,
- // When true, answers INVENTORY_REQUEST (offerBtc) using local documents/rates.
+ // When true, answers INVENTORY_REQUEST: `offerBtc` (L1 offers) and `kind: 'documents'`
+ // (Hub / browser catalog) using local `_state.content.documents` (+ optional rates/collections).
serveLocalDocumentInventory: false,
// Re-send canonical DocumentPublish + pricing to new inbound peers.
announceDocumentsOnPeerConnect: false,
@@ -1648,47 +1652,51 @@ class Peer extends Service {
}
_handleGenericMessage (message, origin = null, socket = null, wireMessage = null) {
- if (this.settings.debug) this.emit('debug', `Generic message:\n\tFrom: ${JSON.stringify(origin)}\n\tType: ${message.type}\n\tBody:\n\`\`\`\n${JSON.stringify(message.object, null, ' ')}\n\`\`\``);
+ const msg = normalizeFabricDocumentOfferEnvelopeForHandlers(message);
+ if (this.settings.debug) this.emit('debug', `Generic message:\n\tFrom: ${JSON.stringify(origin)}\n\tType: ${msg.type}\n\tBody:\n\`\`\`\n${JSON.stringify(msg.object, null, ' ')}\n\`\`\``);
const signerPubkeyHex = wireMessage
? this._verifiedFabricSignerPubkeyHex(wireMessage)
- : normalizePeerPubkeyHex(message && message.actor && (message.actor.publicKey || message.actor.pubkey));
+ : normalizePeerPubkeyHex(msg && msg.actor && (msg.actor.publicKey || msg.actor.pubkey));
// Lookup the appropriate Actor for the message's origin
const actor = new Actor(origin);
- switch (message.type) {
+ switch (msg.type) {
default:
- this.emit('debug', `Unhandled Generic Message: ${message.type} ${JSON.stringify(message, null, ' ')}`);
+ this.emit('debug', `Unhandled Generic Message: ${msg.type} ${JSON.stringify(msg, null, ' ')}`);
break;
case 'INVENTORY_REQUEST':
// Upstream Inventory request (typically for documents). Emit an 'inventory'
// event so higher-level services (e.g. hub) can respond appropriately.
- this.emit('inventory', { message, origin, socket });
+ // JSON `type` may be legacy `INVENTORY_REQUEST` or Fabric alias `FABRIC_DOCUMENT_OFFER` (see `functions/fabricDocumentOfferEnvelope.js`).
+ this.emit('inventory', { message: msg, origin, socket });
if (this.settings.serveLocalDocumentInventory) {
- const served = this._respondInventoryFromLocalDocuments(message, origin);
- const req = message.object || {};
+ const served = this._respondInventoryFromLocalDocuments(msg, origin);
+ const req = msg.object || {};
if (this.settings.relayInventoryRequest && !served && req.offerBtc === true) {
- this._relayGenericPayload(origin && origin.name, message, socket, wireMessage);
+ // Relay path matches L1 `offerBtc` requests; Hub-driven `kind:documents` relays use TTL in app code.
+ this._relayGenericPayload(origin && origin.name, msg, socket, wireMessage);
}
}
break;
case 'INVENTORY_RESPONSE':
// Document inventory reply (may include per-item L1 HTLC offers).
- this.emit('inventoryResponse', { message, origin, socket });
+ // JSON `type` may be legacy `INVENTORY_RESPONSE` or Fabric alias `FABRIC_DOCUMENT_OFFER_RESPONSE`.
+ this.emit('inventoryResponse', { message: msg, origin, socket });
if (this.settings.relayInventoryResponse) {
- this._relayGenericPayload(origin && origin.name, message, socket, wireMessage);
+ this._relayGenericPayload(origin && origin.name, msg, socket, wireMessage);
}
break;
case 'P2P_SESSION_OFFER':
- this._handleSessionOfferGenericMessage(message, origin, socket, signerPubkeyHex);
+ this._handleSessionOfferGenericMessage(msg, origin, socket, signerPubkeyHex);
break;
case 'P2P_SESSION_OPEN':
- this._handleSessionOpenGenericMessage(message, origin, signerPubkeyHex);
+ this._handleSessionOpenGenericMessage(msg, origin, signerPubkeyHex);
break;
case 'P2P_CHAT_MESSAGE':
- this.emit('chat', message);
- const relay = Message.fromVector(['ChatMessage', JSON.stringify(message)]);
+ this.emit('chat', msg);
+ const relay = Message.fromVector(['ChatMessage', JSON.stringify(msg)]);
relay.signWithKey(this.key);
// this.emit('debug', `Relayed chat message: ${JSON.stringify(relay.toGenericMessage())}`);
this.relayFrom(origin.name, relay);
@@ -1956,41 +1964,104 @@ class Peer extends Service {
}
/**
- * Reply to `INVENTORY_REQUEST` with `INVENTORY_RESPONSE` built from local documents and rates.
- * @param {{type: string, object: (Object|undefined)}} message Generic body from {@link Peer#_handleGenericMessage}
- * @param {{ name: string }} origin
- * @returns {boolean} true if an `INVENTORY_RESPONSE` was written to the requester
+ * Items for Hub-style `kind: 'documents'` inventory (Fabric UI / `@fabric/hub` merge expects `object.kind`).
+ * @param {Object} [req] request object subset
+ * @returns {object[]}
*/
- _respondInventoryFromLocalDocuments (message, origin) {
- if (!origin || !origin.name) return false;
- const req = message.object || {};
- if (req.offerBtc !== true) return false;
+ _collectDocumentCatalogInventoryItems (_req) {
const docs = this._state.content.documents;
- if (!docs || typeof docs !== 'object') return false;
+ if (!docs || typeof docs !== 'object') return [];
+ const collections = this._state.content.collections && typeof this._state.content.collections.documents === 'object'
+ ? this._state.content.collections.documents
+ : {};
const rates = this._state.content.documentRates || {};
- const maxSats = req.maxSats;
+ /** @type {object[]} */
const items = [];
for (const docId of Object.keys(docs)) {
const body = docs[docId];
const parsed = this._buildDocumentParsedForPublish(docId, body);
- const contentHash = purchaseContentHashHex(docId, parsed);
+ const row = collections[docId];
+ const purchaseFromCollection = row && Number(row.purchasePriceSats) > 0 ? Math.round(Number(row.purchasePriceSats)) : null;
const rateSats = Object.prototype.hasOwnProperty.call(rates, docId) ? rates[docId] : 0;
- if (maxSats != null && Number.isFinite(maxSats) && rateSats > maxSats) continue;
- items.push({ id: docId, rateSats, contentHash, network: 'bitcoin' });
+ const purchasePriceSats = purchaseFromCollection != null ? purchaseFromCollection : (Number(rateSats) > 0 ? Math.round(Number(rateSats)) : undefined);
+ const published = row ? !!row.published : true;
+ items.push({
+ id: parsed.id,
+ sha256: parsed.sha256 || parsed.id,
+ name: parsed.name,
+ mime: parsed.mime || 'application/octet-stream',
+ size: parsed.size,
+ created: parsed.created || new Date().toISOString(),
+ published,
+ ...(purchasePriceSats != null && purchasePriceSats > 0 ? { purchasePriceSats } : {}),
+ ...(row && row.bitcoinHeight != null && Number.isFinite(Number(row.bitcoinHeight))
+ ? { bitcoinHeight: Math.round(Number(row.bitcoinHeight)) }
+ : {}),
+ ...(row && row.bitcoinBlockHash ? { bitcoinBlockHash: String(row.bitcoinBlockHash) } : {}),
+ ...(row && row.bitcoinTxid ? { bitcoinTxid: String(row.bitcoinTxid) } : {})
+ });
}
- if (!items.length) return false;
- const conn = this.connections[origin.name];
+ return items;
+ }
+
+ /**
+ * Write {@link INVENTORY_RESPONSE} (`P2P_INVENTORY_RESPONSE`) compatible with `@fabric/hub` Bridge merging
+ * (body includes `kind: 'documents'` so the browser can merge `object.items`).
+ * @param {string} originName connection key {@link Peer#connections}
+ * @param {object[]} items
+ * @returns {boolean}
+ */
+ _sendLocalInventoryDocumentsWireResponse (originName, items) {
+ if (!originName || !items || !items.length) return false;
+ const conn = this.connections[originName];
if (!conn || !conn._writeFabric) return false;
- const payload = {
- type: 'INVENTORY_RESPONSE',
- object: { items }
+ const obj = {
+ kind: 'documents',
+ items,
+ created: Date.now()
};
- const m = Message.fromVector(['P2P_INVENTORY_RESPONSE', JSON.stringify(payload.object || {})]);
+ const m = Message.fromVector(['P2P_INVENTORY_RESPONSE', JSON.stringify(obj)]);
m.signWithKey(this.key);
conn._writeFabric(m.toBuffer());
return true;
}
+ /**
+ * Reply to `INVENTORY_REQUEST` with `INVENTORY_RESPONSE` built from local documents and rates.
+ * @param {{type: string, object: (Object|undefined)}} message Generic body from {@link Peer#_handleGenericMessage}
+ * @param {{ name: string }} origin
+ * @returns {boolean} true if an `INVENTORY_RESPONSE` was written to the requester
+ */
+ _respondInventoryFromLocalDocuments (message, origin) {
+ if (!origin || !origin.name) return false;
+ const req = message.object || {};
+ const docs = this._state.content.documents;
+ if (!docs || typeof docs !== 'object') return false;
+
+ if (req.offerBtc === true) {
+ const rates = this._state.content.documentRates || {};
+ const maxSats = req.maxSats;
+ /** @type {object[]} */
+ const items = [];
+ for (const docId of Object.keys(docs)) {
+ const body = docs[docId];
+ const parsed = this._buildDocumentParsedForPublish(docId, body);
+ const contentHash = purchaseContentHashHex(docId, parsed);
+ const rateSats = Object.prototype.hasOwnProperty.call(rates, docId) ? rates[docId] : 0;
+ if (maxSats != null && Number.isFinite(maxSats) && rateSats > maxSats) continue;
+ items.push({ id: docId, rateSats, contentHash, network: 'bitcoin' });
+ }
+ if (!items.length) return false;
+ return this._sendLocalInventoryDocumentsWireResponse(origin.name, items);
+ }
+
+ const reqKind = String(req.kind || '').trim().toLowerCase();
+ if (reqKind !== 'documents') return false;
+ const items = this._collectDocumentCatalogInventoryItems({});
+ if (!items.length) return false;
+ return this._sendLocalInventoryDocumentsWireResponse(origin.name, items);
+ }
+
/**
* Send a locally stored document to a connected peer as `P2P_FILE_SEND`.
* @param {string} documentId
From f7d8745f5ee56c8328509ff26fbfe1afe07e8e5c Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Tue, 28 Apr 2026 07:10:18 -0400
Subject: [PATCH 18/27] Add missing fabricDocumentOfferEnvelope function
---
functions/fabricDocumentOfferEnvelope.js | 57 ++++++++++++++++++++++++
package-lock.json | 8 ++--
2 files changed, 61 insertions(+), 4 deletions(-)
create mode 100644 functions/fabricDocumentOfferEnvelope.js
diff --git a/functions/fabricDocumentOfferEnvelope.js b/functions/fabricDocumentOfferEnvelope.js
new file mode 100644
index 000000000..88f643f1f
--- /dev/null
+++ b/functions/fabricDocumentOfferEnvelope.js
@@ -0,0 +1,57 @@
+'use strict';
+
+/**
+ * Fabric document-offer envelope types (JSON `type` on generic payloads). Conceptually aligned with
+ * Lightning BOLT12 *offers* (buyer-facing request → seller-facing response); wire remains
+ * `P2P_INVENTORY_REQUEST` / `P2P_INVENTORY_RESPONSE` opcodes. See `docs/FABRIC_DOCUMENT_OFFER.md`.
+ */
+
+/** Canonical JSON `type`: buyer/originator asks for catalog / settlement terms. */
+const FABRIC_DOCUMENT_OFFER = 'FABRIC_DOCUMENT_OFFER';
+/** Accepted synonym for `FABRIC_DOCUMENT_OFFER`. */
+const FABRIC_DOCUMENT_OFFER_REQUEST = 'FABRIC_DOCUMENT_OFFER_REQUEST';
+
+/** Canonical JSON `type`: seller/router replies (items, L1 `contentHash`, optional HTLC hooks). */
+const FABRIC_DOCUMENT_OFFER_RESPONSE = 'FABRIC_DOCUMENT_OFFER_RESPONSE';
+/** Accepted synonym for `FABRIC_DOCUMENT_OFFER_RESPONSE`. */
+const FABRIC_DOCUMENT_OFFER_REPLY = 'FABRIC_DOCUMENT_OFFER_REPLY';
+
+const TO_LEGACY_INVENTORY = Object.freeze({
+ [FABRIC_DOCUMENT_OFFER]: 'INVENTORY_REQUEST',
+ [FABRIC_DOCUMENT_OFFER_REQUEST]: 'INVENTORY_REQUEST',
+ [FABRIC_DOCUMENT_OFFER_RESPONSE]: 'INVENTORY_RESPONSE',
+ [FABRIC_DOCUMENT_OFFER_REPLY]: 'INVENTORY_RESPONSE'
+});
+
+/**
+ * Map Fabric document-offer envelope `type` strings to legacy handler types (`INVENTORY_*`).
+ * @param {unknown} type
+ * @returns {string|null}
+ */
+function fabricDocumentOfferEnvelopeToLegacy (type) {
+ if (typeof type !== 'string') return null;
+ const t = type.trim();
+ return TO_LEGACY_INVENTORY[t] || null;
+}
+
+/**
+ * Shallow-clone `message` with `type` set to `INVENTORY_REQUEST` / `INVENTORY_RESPONSE` when a Fabric alias is used.
+ * @param {object} message
+ * @returns {object}
+ */
+function normalizeFabricDocumentOfferEnvelopeForHandlers (message) {
+ if (!message || typeof message !== 'object') return message;
+ const legacy = fabricDocumentOfferEnvelopeToLegacy(message.type);
+ if (!legacy) return message;
+ return Object.assign({}, message, { type: legacy });
+}
+
+module.exports = {
+ FABRIC_DOCUMENT_OFFER,
+ FABRIC_DOCUMENT_OFFER_REQUEST,
+ FABRIC_DOCUMENT_OFFER_RESPONSE,
+ FABRIC_DOCUMENT_OFFER_REPLY,
+ TO_LEGACY_INVENTORY,
+ fabricDocumentOfferEnvelopeToLegacy,
+ normalizeFabricDocumentOfferEnvelopeForHandlers
+};
diff --git a/package-lock.json b/package-lock.json
index 2ebde5338..816434cfa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6957,12 +6957,12 @@
}
},
"node_modules/socks": {
- "version": "2.8.7",
- "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
- "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz",
+ "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==",
"license": "MIT",
"dependencies": {
- "ip-address": "^10.0.1",
+ "ip-address": "^10.1.1",
"smart-buffer": "^4.2.0"
},
"engines": {
From 39bd835f5d5c6bb3ad9b555c0f58c81695a259df Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Wed, 29 Apr 2026 01:02:45 -0400
Subject: [PATCH 19/27] Update package-lock.json
---
package-lock.json | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 816434cfa..869519bae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4412,19 +4412,19 @@
}
},
"node_modules/jsdoc-api": {
- "version": "9.3.5",
- "resolved": "https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-9.3.5.tgz",
- "integrity": "sha512-TQwh1jA8xtCkIbVwm/XA3vDRAa5JjydyKx1cC413Sh3WohDFxcMdwKSvn4LOsq2xWyAmOU/VnSChTQf6EF0R8g==",
+ "version": "9.3.6",
+ "resolved": "https://registry.npmjs.org/jsdoc-api/-/jsdoc-api-9.3.6.tgz",
+ "integrity": "sha512-8JW0532+rXVw8LoZ1LIAeKofsV8QQZhnY3chxMHV9hQdsuDTshsajwk0b4EMxCqen2vZ2op/r/qeEsqZxsEQyg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "array-back": "^6.2.2",
+ "array-back": "^6.2.3",
"cache-point": "^3.0.1",
- "current-module-paths": "^1.1.2",
+ "current-module-paths": "^1.1.3",
"file-set": "^5.3.0",
- "jsdoc": "^4.0.4",
+ "jsdoc": "^4.0.5",
"object-to-spawn-args": "^2.0.1",
- "walk-back": "^5.1.1"
+ "walk-back": "^5.1.2"
},
"engines": {
"node": ">=12.17"
From 9419d6d2507942be80bb20ba74d247567db985d7 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Wed, 29 Apr 2026 11:57:55 -0400
Subject: [PATCH 20/27] Continue hardening environment
---
types/environment.js | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/types/environment.js b/types/environment.js
index 3544ae3c3..396602c90 100644
--- a/types/environment.js
+++ b/types/environment.js
@@ -602,9 +602,17 @@ class Environment extends Entity {
});
} else if (this.walletExists()) {
const data = this.readWallet();
+ const text = typeof data === 'string' ? data : String(data ?? '');
+
+ if (text.trim() === '') {
+ if (this.emit) {
+ this.emit('warning', `[FABRIC:KEYGEN] Wallet file is empty (${this.settings.path}); remove it or regenerate with fabric setup`);
+ }
+ this.wallet = false;
+ return this;
+ }
try {
- const text = typeof data === 'string' ? data : String(data ?? '');
const pr = tryParsePersistedJson(text);
if (!pr.ok) throw pr.error;
const input = pr.value;
@@ -621,7 +629,9 @@ class Environment extends Entity {
}
});
} catch (exception) {
- if (this.emit) this.emit('error', `[FABRIC:KEYGEN] Could not load wallet data: ${exception.message || exception}`);
+ // Recoverable user-data issue; do not emit "error" (EventEmitter kills the process with no listeners).
+ if (this.emit) this.emit('warning', `[FABRIC:KEYGEN] Could not load wallet data: ${exception.message || exception}`);
+ this.wallet = false;
}
} else {
this.wallet = false;
From cb161b00f7b4b92960d629feed38bdd72118ba95 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Thu, 30 Apr 2026 08:02:53 -0400
Subject: [PATCH 21/27] Update package-lock.json, environment tests
---
package-lock.json | 772 +++++++++++++++++++++++-------------
tests/fabric.environment.js | 38 ++
2 files changed, 534 insertions(+), 276 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 869519bae..11f6e4b5d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -91,6 +91,16 @@
"@asciidoctor/core": "^2.0.0-rc.1"
}
},
+ "node_modules/@asciidoctor/cli/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/@asciidoctor/cli/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@@ -103,6 +113,59 @@
"wrap-ansi": "^7.0.0"
}
},
+ "node_modules/@asciidoctor/cli/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@asciidoctor/cli/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@asciidoctor/cli/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@asciidoctor/cli/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/@asciidoctor/cli/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
@@ -133,14 +196,14 @@
}
},
"node_modules/@asciidoctor/core": {
- "version": "2.2.8",
- "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-2.2.8.tgz",
- "integrity": "sha512-oozXk7ZO1RAd/KLFLkKOhqTcG4GO3CV44WwOFg2gMcCsqCUTarvMT7xERIoWW2WurKbB0/ce+98r01p8xPOlBw==",
+ "version": "2.2.9",
+ "resolved": "https://registry.npmjs.org/@asciidoctor/core/-/core-2.2.9.tgz",
+ "integrity": "sha512-tIPRHo1T2SFmAm+j77cDsj0RuaszP7xJxsaVTTAF5CwKyTbazw9TnIVlpIWM5yWfIWAWcAZy92RcnPgMJwny1w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "asciidoctor-opal-runtime": "0.3.3",
- "unxhr": "1.0.1"
+ "asciidoctor-opal-runtime": "0.3.4",
+ "unxhr": "~1.2"
},
"engines": {
"node": ">=8.11",
@@ -485,91 +548,6 @@
"node": ">=12"
}
},
- "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@isaacs/cliui/node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
- "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.2.2"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@@ -1048,13 +1026,16 @@
}
},
"node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=8"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
@@ -1132,14 +1113,14 @@
"license": "MIT"
},
"node_modules/asciidoctor": {
- "version": "2.2.8",
- "resolved": "https://registry.npmjs.org/asciidoctor/-/asciidoctor-2.2.8.tgz",
- "integrity": "sha512-G+sDYWnNo+QHRkIvN5k7ASbvrd2bHuNXHlZ83+PjVFYtl0//as5iebq+Bdf3aSwXrkM7akcEJPUpdTjjP0MgYw==",
+ "version": "2.2.9",
+ "resolved": "https://registry.npmjs.org/asciidoctor/-/asciidoctor-2.2.9.tgz",
+ "integrity": "sha512-lzviGTZ/tnnmDLIE+fY/m8aUc6lGTGRNh5rTC1HPevlc89G0iYez+sQFT60oZ87BPzOYllP+TeK1xh0D1wt/6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@asciidoctor/cli": "3.5.0",
- "@asciidoctor/core": "2.2.8"
+ "@asciidoctor/core": "2.2.9"
},
"bin": {
"asciidoctor": "bin/asciidoctor",
@@ -1152,14 +1133,14 @@
}
},
"node_modules/asciidoctor-opal-runtime": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.3.tgz",
- "integrity": "sha512-/CEVNiOia8E5BMO9FLooo+Kv18K4+4JBFRJp8vUy/N5dMRAg+fRNV4HA+o6aoSC79jVU/aT5XvUpxSxSsTS8FQ==",
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/asciidoctor-opal-runtime/-/asciidoctor-opal-runtime-0.3.4.tgz",
+ "integrity": "sha512-zqd6zn1LV+PZ69AP/kEbB00zuPHMIAJY3IX8+aZV+X1qOwatYvKGjsMmdMc5ApfhtkjZ4mYkqiTPJWnEnBiMJg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "glob": "7.1.3",
- "unxhr": "1.0.1"
+ "fast-glob": "~3.3",
+ "unxhr": "~1.2"
},
"engines": {
"node": ">=8.11"
@@ -1542,6 +1523,22 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/cacache/node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/cache-point": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/cache-point/-/cache-point-3.0.1.tgz",
@@ -1839,6 +1836,69 @@
"node": ">=12"
}
},
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@@ -2591,9 +2651,9 @@
}
},
"node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
@@ -3389,22 +3449,25 @@
"license": "Apache-2.0"
},
"node_modules/glob": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
- "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
"license": "ISC",
"dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
},
- "engines": {
- "node": "*"
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
@@ -3428,27 +3491,29 @@
"license": "MIT"
},
"node_modules/glob/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
+ "balanced-match": "^1.0.0"
}
},
"node_modules/glob/node_modules/minimatch": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
- "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
- "brace-expansion": "^1.1.7"
+ "brace-expansion": "^2.0.2"
},
"engines": {
- "node": "*"
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gopd": {
@@ -4268,85 +4333,6 @@
"node": ">=14"
}
},
- "node_modules/js-beautify/node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/js-beautify/node_modules/brace-expansion": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
- "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/js-beautify/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/js-beautify/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/js-beautify/node_modules/minimatch": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
- "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.2"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/js-beautify/node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
- },
- "engines": {
- "node": ">=16 || 14 >=14.18"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
@@ -5593,28 +5579,6 @@
"url": "https://paulmillr.com/funding/"
}
},
- "node_modules/mocha/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/mocha/node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -5628,13 +5592,6 @@
"js-yaml": "bin/js-yaml.js"
}
},
- "node_modules/mocha/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/mocha/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
@@ -5651,23 +5608,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/mocha/node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
- },
- "engines": {
- "node": ">=16 || 14 >=14.18"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/mocha/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -6292,21 +6232,29 @@
"license": "MIT"
},
"node_modules/path-scurry": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
- "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
- "lru-cache": "^11.0.0",
- "minipass": "^7.1.2"
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/path-to-regexp": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz",
@@ -6546,6 +6494,59 @@
"npm-normalize-package-bin": "^1.0.0"
}
},
+ "node_modules/read-package-json/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/read-package-json/node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/read-package-json/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/read-package-json/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
@@ -6678,6 +6679,59 @@
"rimraf": "bin.js"
}
},
+ "node_modules/rimraf/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rimraf/node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/rimraf/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -7159,18 +7213,21 @@
"dev": true
},
"node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
},
"engines": {
- "node": ">=8"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
@@ -7189,7 +7246,24 @@
"node": ">=8"
}
},
- "node_modules/strip-ansi": {
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
@@ -7202,6 +7276,22 @@
"node": ">=8"
}
},
+ "node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
@@ -7216,6 +7306,16 @@
"node": ">=8"
}
},
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -7326,6 +7426,23 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/test-exclude/node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/through2": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
@@ -7573,9 +7690,9 @@
}
},
"node_modules/unxhr": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.0.1.tgz",
- "integrity": "sha512-MAhukhVHyaLGDjyDYhy8gVjWJyhTECCdNsLwlMoGFoNJ3o79fpQhtQuzmAE4IxCMDwraF4cW8ZjpAV0m9CRQbg==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.2.0.tgz",
+ "integrity": "sha512-6cGpm8NFXPD9QbSNx0cD2giy7teZ6xOkCUH3U89WKVkL9N9rBrWjlCwhR94Re18ZlAop4MOc3WU1M3Hv/bgpIw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -7952,18 +8069,18 @@
"license": "Apache-2.0"
},
"node_modules/wrap-ansi": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
},
"engines": {
- "node": ">=10"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
@@ -7988,6 +8105,64 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -8095,6 +8270,51 @@
"node": ">=10"
}
},
+ "node_modules/yargs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/tests/fabric.environment.js b/tests/fabric.environment.js
index fd58ab4eb..1c9fe8862 100644
--- a/tests/fabric.environment.js
+++ b/tests/fabric.environment.js
@@ -57,6 +57,44 @@ describe('@fabric/core/types/environment', function () {
}
});
+ it('does not emit fatal error events when wallet file is empty or invalid JSON', function () {
+ const prev = {
+ NODE_ENV: process.env.NODE_ENV,
+ FABRIC_SEED: process.env.FABRIC_SEED,
+ FABRIC_XPRV: process.env.FABRIC_XPRV,
+ FABRIC_XPUB: process.env.FABRIC_XPUB
+ };
+
+ try {
+ delete process.env.FABRIC_SEED;
+ delete process.env.FABRIC_XPRV;
+ delete process.env.FABRIC_XPUB;
+ process.env.NODE_ENV = 'production';
+
+ for (const content of ['', '{']) {
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'fabric-env-bad-wallet-'));
+ const store = path.join(home, '.fabric');
+ const walletPath = path.join(store, 'wallet.json');
+ fs.mkdirSync(store, { recursive: true });
+ fs.writeFileSync(walletPath, content, 'utf8');
+
+ const environment = new Environment({ home, path: walletPath, store });
+ let emittedError = null;
+ environment.on('error', (err) => { emittedError = err; });
+
+ environment.start();
+ assert.strictEqual(emittedError, null, `unexpected error event for wallet content repr: ${JSON.stringify(content)}`);
+ assert.strictEqual(environment.wallet, false);
+ fs.rmSync(home, { recursive: true, force: true });
+ }
+ } finally {
+ for (const k of Object.keys(prev)) {
+ if (prev[k] === undefined) delete process.env[k];
+ else process.env[k] = prev[k];
+ }
+ }
+ });
+
it('can instantiate from a seed', async function () {
const environment = new Environment({ xpub: FIXTURE_SEED });
await environment.start();
From 20663cc4f92491caecdcebda01ce14a7581e151c Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Fri, 1 May 2026 06:55:20 -0400
Subject: [PATCH 22/27] Update packages
---
package-lock.json | 12 ++++++------
reports/install.log | 4 ++--
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 11f6e4b5d..475cb9e3d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -232,9 +232,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.29.2",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
- "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3984,9 +3984,9 @@
"license": "ISC"
},
"node_modules/ip-address": {
- "version": "10.1.1",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.1.tgz",
- "integrity": "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==",
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
+ "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"license": "MIT",
"engines": {
"node": ">= 12"
diff --git a/reports/install.log b/reports/install.log
index 70f170044..15a241230 100644
--- a/reports/install.log
+++ b/reports/install.log
@@ -10,9 +10,9 @@ $ npm i
CC(target) Release/obj.target/fabric/src/p2p.o
SOLINK_MODULE(target) Release/fabric.node
-added 666 packages, and audited 667 packages in 24s
+added 685 packages, and audited 686 packages in 29s
-110 packages are looking for funding
+111 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
From 641ff39db82aed3961fe1b7a154dbc60310e48cc Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Mon, 4 May 2026 20:02:36 -0400
Subject: [PATCH 23/27] Update reports, package-lock.json
---
TODO.md | 4 ++--
package-lock.json | 6 +++---
reports/TODO.txt | 19 +------------------
3 files changed, 6 insertions(+), 23 deletions(-)
diff --git a/TODO.md b/TODO.md
index e43f4870e..2161aab37 100644
--- a/TODO.md
+++ b/TODO.md
@@ -24,12 +24,12 @@ See also [GOALS.md][goals].
- [ ] Tests passing on Travis
- [ ] Tests passing on GitHub Actions
- [ ] Test Coverage 70%
-- [ ] Clean install from @portal/feed
+- [ ] Clean install from @fabric/fiat
- [x] Install Time < 300s
- [ ] Install Time < 30s
## Annoyances:
-Upon running `rm -rf node_modules && npm i` from `@portal/feed` repo:
+Upon running `rm -rf node_modules && npm i` from `@fabric/fiat` repo:
```
npm WARN deprecated source-map-url@0.4.1: See https://github.com/lydell/source-map-url#deprecated
npm WARN deprecated urix@0.1.0: Please see https://github.com/lydell/urix#deprecated
diff --git a/package-lock.json b/package-lock.json
index 475cb9e3d..1d44727f9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5099,9 +5099,9 @@
"license": "MIT"
},
"node_modules/lru-cache": {
- "version": "11.3.5",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
- "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",
+ "version": "11.3.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
+ "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
diff --git a/reports/TODO.txt b/reports/TODO.txt
index ab0dd325d..89ed2f661 100644
--- a/reports/TODO.txt
+++ b/reports/TODO.txt
@@ -26,8 +26,6 @@
./types/key.js: // TODO: determine if this makes sense / needs to be private
./types/key.js: // TODO: evaluate compression when treating seed phrase as ascii
./types/key.js: // TODO: consider using sha256(masterprivkey) or sha256(sha256(...))?
-./types/chain.js: // TODO: reject duplicate transactions
-./types/chain.js: source.on('message', function TODO (message) {
./types/chain.js: // TODO: _sortFees
./types/remote.js: // TODO: use onion address for secure mode
./types/remote.js: // TODO: warn about unexpected values
@@ -87,9 +85,6 @@
./types/circuit.js: // TODO: generate polynomial for circuit
./types/contract.js: // TODO: parse on-chain transaction for update to contract balance
./types/bitcoin/transaction.js:// TODO: PSBTs
-./types/bitcoin/transaction.js: return ''; // TODO: real hash
-./types/bitcoin/transaction.js: return ''; // TODO: Fabric ID
-./types/bitcoin/transaction.js: return ''; // TODO: bitcoin txid
./types/hash256.js: // TODO: document `hash256.value`
./types/bond.js: content: Buffer.alloc(4) // TODO: Bitcoin encoded values?
./types/actor.js: // TODO: encourage use of `state` over `_state`
@@ -152,7 +147,6 @@
./types/peer.js: // TODO: switch to child pubkey
./types/peer.js: // TODO: consider making this a FabricMessageID
./types/peer.js: // TODO: output stream
-./types/peer.js: // TODO: ensure no pong is handled when a ping was not previously sent
./types/peer.js: // TODO: reject and punish mis-behaving peers
./types/peer.js: // TODO: reject and punish mis-behaving peers
./types/peer.js: // TODO: reconcile APIs for these methods
@@ -200,14 +194,7 @@
./DEVELOPERS.md:- [ ] Sweep remaining `TODO` markers in repo-specific guides
./DEVELOPERS.md:## TODO
./DEVELOPERS.md:- [ ] Remove TODOs
-./.tmp-pr176.html:|**Reports & generated data** <br> `reports/*`, `reports/class-inheritance.json`, `reports/AUDIT.json`, `reports/TODO.txt`|Generated audit, class-inheritance, and updated TODO reports; updated reporting outputs.|
-./.tmp-pr176.html:Reports & generated data reports/*, reports/class-inheritance.json, reports/AUDIT.json, reports/TODO.txt |
-./.tmp-pr176.html:Generated audit, class-inheritance, and updated TODO reports; updated reporting outputs. |
-./.tmp-pr176.html:* `reports/TODO.txt`
-./.tmp-pr176.html:reports/TODO.txt
-./.tmp-pr176.html: message.amount = message.amount.fixed().toPrecision(8); // TODO: evaluate precision behavior
-./.tmp-pr176.html: message.amount = message.amount.fixed().toPrecision(8); // TODO: evaluate precision behavior |
-./package.json: "report:todo": "grep --exclude-dir=.git --exclude-dir=_book --exclude-dir=assets --exclude-dir=node_modules --exclude-dir=reports --exclude-dir=coverage --exclude-dir=docs -rEI \"TODO|FIXME\" . > reports/TODO.txt",
+./package.json: "report:todo": "grep --exclude-dir=.git --exclude-dir=_book --exclude-dir=assets --exclude-dir=node_modules --exclude-dir=reports --exclude-dir=coverage --exclude-dir=docs --exclude='.tmp-*.html' --exclude='*.tmp-pr*.html' -rEI \"TODO|FIXME\" . > reports/TODO.txt",
./package.json: "review:todo": "npm run report:todo && cat reports/TODO.txt && echo '\nOutstanding TODO items (@fabric/core):' && wc -l reports/TODO.txt && echo '\nIssues List: https://github.com/FabricLabs/fabric/issues\nDisclosures: security@fabric.pub\n\n'",
./guides/SETTINGS.md:### WARNING: TODO
./examples/bitcoin.js: // TODO: import these into core process logic
@@ -216,11 +203,7 @@
./examples/swarm.html: TODO: receive entities from seed node
./examples/swarm.html: TODO: create entities on swarm instance
./examples/agents.js:// TODO: ensure utilization of multiple cores
-./examples/agents.js: // TODO: do something productive here!
-./examples/agents.js: // TODO: replace this with Actor ID
-./examples/agents.js:// TODO: move these to `functions/`
./examples/agents.js: // TODO: document all descriptors
-./examples/agents.js: // TODO: migrate this to `functions/onParentMessage.js`
./examples/agents.js: // TODO: migrate to `functions/`
./examples/bitcoin.html: TODO: import these into core process logic
./examples/service.js: // TODO: revert to OPTIONS (this was a temporary fix for an NGINX bug)
From f93d3f18578003d85ecfb3ca8061d62584c36a8c Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Wed, 6 May 2026 19:03:01 -0400
Subject: [PATCH 24/27] Update package-lock.json
---
package-lock.json | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 1d44727f9..a1b1cff19 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -827,9 +827,9 @@
"license": "MIT"
},
"node_modules/@types/estree": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT"
},
@@ -4039,13 +4039,13 @@
}
},
"node_modules/is-core-module": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
- "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "hasown": "^2.0.2"
+ "hasown": "^2.0.3"
},
"engines": {
"node": ">= 0.4"
@@ -7378,9 +7378,9 @@
}
},
"node_modules/tar": {
- "version": "7.5.13",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
- "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
+ "version": "7.5.14",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.14.tgz",
+ "integrity": "sha512-/7sHKgQO3JLP9ESlwTYUUftHUadOURUqq23xs1vjcnp8Vss6k0wCfzulyEtk5g91pjvnuriimGlyG7k6msrzRw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
From 0444633439494820a8bc815c37c5a3528b41ba40 Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Thu, 7 May 2026 15:56:33 -0400
Subject: [PATCH 25/27] Further polish of Environment, Keygen
---
scripts/chat.js | 27 +++++++++++++++++++--------
scripts/keygen.js | 6 +++---
types/codec.js | 33 ++++++++++++---------------------
types/identity.js | 2 +-
types/key.js | 2 +-
types/promise.js | 1 -
types/store.js | 10 +++++-----
7 files changed, 41 insertions(+), 40 deletions(-)
diff --git a/scripts/chat.js b/scripts/chat.js
index a4706a971..60eac457e 100755
--- a/scripts/chat.js
+++ b/scripts/chat.js
@@ -51,11 +51,22 @@ const settings = {
};
// Define Main Program
+function _loadSeedData () {
+ if (!environment.walletExists()) return null;
+ try {
+ const raw = environment.readWallet();
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
+ return parsed['@data'] || parsed;
+ } catch (e) {
+ return null;
+ }
+}
+
async function main () {
if (!environment.walletExists()) {
- seed = await wallet._createSeed();
+ seed = await wallet._createFromFreshSeed();
} else {
- seed = environment.readWallet();
+ seed = _loadSeedData();
}
const COMMANDS = {
@@ -75,8 +86,8 @@ async function main () {
}
// Load from Seed
- settings.key.seed = seed['@data'];
- settings.wallet.seed = seed['@data'];
+ settings.key.seed = seed.phrase;
+ settings.wallet.seed = seed.phrase;
// Fabric CLI
const chat = new CLI(settings);
@@ -115,9 +126,9 @@ async function main () {
program.parse(process.argv);
if (!environment.walletExists() || (program.keygen && program.force)) {
- seed = await wallet._createSeed();
+ seed = await wallet._createFromFreshSeed();
} else {
- seed = environment.readWallet();
+ seed = _loadSeedData();
}
if (program.keygen) {
@@ -147,7 +158,7 @@ async function main () {
} else {
console.warn('[FABRIC:KEYGEN]', 'Key file exists, no data will be written. Use --force to override.');
console.warn('[FABRIC:KEYGEN]', '[WARNING]', '--force DESTROYS ALL DATA: DOUBLE-CHECK YOUR BACKUPS!');
- console.warn('[FABRIC:KEYGEN]', 'EXISTING_XPUB_PUBLIC', '=', seed['@data'].xpub.public);
+ console.warn('[FABRIC:KEYGEN]', 'EXISTING_XPUB', '=', seed.xpub);
}
// prevent further execution
@@ -155,7 +166,7 @@ async function main () {
} else if (program.receive) {
const wallet = new Wallet({
key: {
- seed: seed['@data'].seed
+ seed: seed.phrase
}
});
diff --git a/scripts/keygen.js b/scripts/keygen.js
index 298b45ee3..6feb5dd60 100644
--- a/scripts/keygen.js
+++ b/scripts/keygen.js
@@ -4,10 +4,10 @@ const Wallet = require('../types/wallet');
async function main () {
const wallet = new Wallet();
- const seed = await wallet._createSeed();
+ const seed = await wallet._createFromFreshSeed();
const keypair = await wallet.generateCleanKeyPair();
- console.warn('[FABRIC:KEYGEN]', 'Key generated:', keypair);
- console.warn('[FABRIC:KEYGEN]', 'SEED:', seed);
+ console.warn('[FABRIC:KEYGEN]', 'Key generated (public):', keypair.public ? keypair.public.toString('hex') : keypair);
+ console.warn('[FABRIC:KEYGEN]', 'SEED PHRASE:', seed.phrase);
process.exit();
}
diff --git a/types/codec.js b/types/codec.js
index beebffc62..2f6fb8180 100644
--- a/types/codec.js
+++ b/types/codec.js
@@ -50,30 +50,21 @@ class Codec {
encode (data) {
if (typeof data !== 'string') data = JSON.stringify(data);
- try {
- const actor = new Actor(data);
- const label = new Label(`${this.key.pubkey}/${actor.id}`);
- const blob = this.key.encrypt(data);
- this._registerActor(actor);
- this._registerLabel(label);
- return blob;
- } catch (exception) {
- console.error('err:', exception);
- return null;
- }
+ const actor = new Actor(data);
+ const label = new Label(`${this.key.pubkey}/${actor.id}`);
+ const blob = this.key.encrypt(data);
+ this._registerActor(actor);
+ this._registerLabel(label);
+ return blob;
}
decode (blob) {
- try {
- const actor = new Actor(blob);
- const label = new Label(`${this.key.pubkey}/${actor.id}`);
- const data = this.key.decrypt(blob);
- this._registerActor(actor);
- this._registerLabel(label);
- return data;
- } catch (exception) {
- console.error('err:', exception);
- }
+ const actor = new Actor(blob);
+ const label = new Label(`${this.key.pubkey}/${actor.id}`);
+ const data = this.key.decrypt(blob);
+ this._registerActor(actor);
+ this._registerLabel(label);
+ return data;
}
}
diff --git a/types/identity.js b/types/identity.js
index 450beb6a8..4ee82587a 100644
--- a/types/identity.js
+++ b/types/identity.js
@@ -141,7 +141,7 @@ class Identity extends Actor {
}
_verifyKeyIsChild (_key, _parent) {
-
+ throw new Error('_verifyKeyIsChild is not yet implemented.');
}
}
diff --git a/types/key.js b/types/key.js
index 69f564e7c..9e9b5c694 100644
--- a/types/key.js
+++ b/types/key.js
@@ -272,7 +272,7 @@ class Key extends EventEmitter {
case 'FROM_MNEMONIC':
seed = bip39.mnemonicToSeedSync(this.settings.mnemonic, this.settings.passphrase);
root = this.bip32.fromSeed(seed, bip32NetworkFromKeySettings(this.settings));
- this.seed = this.settings.seed;
+ this.seed = this.settings.mnemonic;
this.xprv = root.toBase58();
this.xpub = root.neutered().toBase58();
this.master = root;
diff --git a/types/promise.js b/types/promise.js
index 688a6a9b9..a8ae447e1 100644
--- a/types/promise.js
+++ b/types/promise.js
@@ -102,7 +102,6 @@ class EncryptedPromise {
const iv = Hash256.digest(password);
const aes = new SimpleAES(256, iv);
const data = aes.decrypt(iv, this._state.blob);
- console.log('decrypted:', data);
this._state.data = data;
this._state.state = 'DECRYPTED';
return data;
diff --git a/types/store.js b/types/store.js
index 12dac65d8..d50bbccdc 100644
--- a/types/store.js
+++ b/types/store.js
@@ -177,24 +177,24 @@ class Store extends Actor {
*/
async _REGISTER (obj) {
const actor = new Actor(obj);
- await this._GET(`/entities/${actor.id}`);
+ let result = null;
- store.log('[STORE]', '_REGISTER', vector.id, vector['@type']);
+ this.log('[STORE]', '_REGISTER', actor.id, actor.type);
try {
- await this._GET(`/entities/${vector.id}`);
+ await this._GET(`/entities/${actor.id}`);
} catch (E) {
this.warn('[STORE]', '_REGISTER', `Could not read from store:`, E);
}
try {
- await this._SET(`/types/${vector.id}`, vector['@type']);
+ await this._SET(`/types/${actor.id}`, actor.type);
} catch (E) {
this.error('Error creating object:', E, obj);
}
try {
- result = await this._SET(`/entities/${vector.id}`, vector['@data']);
+ result = await this._SET(`/entities/${actor.id}`, actor.toObject());
} catch (E) {
this.error('Error creating object:', E, obj);
}
From 4c86ff8af27c7a2ebdd0e62b668bed124e1ea1ce Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Fri, 8 May 2026 20:07:00 -0400
Subject: [PATCH 26/27] Update package-lock.json
---
package-lock.json | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index a1b1cff19..daae532cc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1338,9 +1338,9 @@
"license": "ISC"
},
"node_modules/brace-expansion": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
- "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -6783,9 +6783,9 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.7.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
- "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -7011,9 +7011,9 @@
}
},
"node_modules/socks": {
- "version": "2.8.8",
- "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz",
- "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==",
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
+ "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.1.1",
@@ -7378,9 +7378,9 @@
}
},
"node_modules/tar": {
- "version": "7.5.14",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.14.tgz",
- "integrity": "sha512-/7sHKgQO3JLP9ESlwTYUUftHUadOURUqq23xs1vjcnp8Vss6k0wCfzulyEtk5g91pjvnuriimGlyG7k6msrzRw==",
+ "version": "7.5.15",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz",
+ "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
From 612180ec20c2bd5bdae24aa28e845d72913a19cd Mon Sep 17 00:00:00 2001
From: Eric Martindale
Date: Sun, 10 May 2026 12:45:58 -0400
Subject: [PATCH 27/27] General update to C implementation, re-work NOISE
---
.gitignore | 1 +
binding.gyp | 1 +
package.json | 6 +-
reports/install.log | 3 +-
scripts/chat.js | 10 +
scripts/keygen.js | 4 +-
src/constants.h | 3 +
src/noise.c | 304 +++++++++++++++++++++++++++
src/noise.h | 18 +-
src/peer.c | 477 +++++++++++++++++-------------------------
src/security.c | 66 +++++-
tests/fabric.noise.js | 8 +-
types/codec.js | 3 +
types/environment.js | 20 +-
types/peer.js | 13 +-
15 files changed, 621 insertions(+), 316 deletions(-)
create mode 100644 src/noise.c
diff --git a/.gitignore b/.gitignore
index 3467785fb..49195f1ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,7 @@ src/*
!src/message.h
!src/memory.h
!src/noise.h
+!src/noise.c
!src/p2p.c
!src/peer.c
!src/peer.h
diff --git a/binding.gyp b/binding.gyp
index e033da3db..9aac101cf 100644
--- a/binding.gyp
+++ b/binding.gyp
@@ -5,6 +5,7 @@
"src/binding.cc",
"src/crypto.c",
"src/protocol.c",
+ "src/noise.c",
"src/p2p.c"
],
"include_dirs": [
diff --git a/package.json b/package.json
index ee4e7bcca..75a241f34 100644
--- a/package.json
+++ b/package.json
@@ -24,8 +24,8 @@
"build": "npm run make:all",
"chat": "node scripts/fabric.js shell",
"ci": "npm run smoke && npm run lint:types && npm run lint:pkg && npm test && npm run test:benchmark",
- "smoke": "node -e \"require('./types/fabric'); require('./types/distributedExecution'); require('./types/message'); require('./types/peer'); require('./types/hash256'); require('./functions/bytes'); require('./functions/fabricCanonicalJson'); require('./functions/wireJson'); require('./functions/fabricNativeAccel'); console.log('smoke ok');\"",
- "lint:pkg": "eslint scripts/fabric.js scripts/cli.js scripts/gen-quality-reports.js scripts/check-book-links.js scripts/coverage-baseline.js scripts/clean-jsdoc-html.js scripts/classtree.js scripts/report-noble-hashes.js scripts/fix-noble-hashes-compat.js scripts/install-native.js scripts/benchmark.js functions/bytes.js functions/fabricCanonicalJson.js functions/wireJson.js functions/fabricNativeAccel.js functions/base58.js functions/bech32.js functions/sipa/bech32.js functions/sipa/segwit_addr.js functions/bip39.js functions/bip32.js",
+ "smoke": "node -e \"require('./types/fabric'); require('./types/distributedExecution'); require('./types/message'); require('./types/peer'); require('./types/hash256'); require('./functions/bytes'); require('./functions/fabricCanonicalJson'); require('./functions/wireJson'); require('./functions/fabricNativeAccel'); require('./functions/lightningBolt12'); require('./functions/bolt12Semantics'); require('./functions/fabricPaymentBech32'); require('./functions/fabricDocumentOfferEnvelope'); console.log('smoke ok');\"",
+ "lint:pkg": "eslint scripts/fabric.js scripts/cli.js scripts/gen-quality-reports.js scripts/check-book-links.js scripts/coverage-baseline.js scripts/clean-jsdoc-html.js scripts/classtree.js scripts/report-noble-hashes.js scripts/fix-noble-hashes-compat.js scripts/install-native.js scripts/benchmark.js functions/bytes.js functions/fabricCanonicalJson.js functions/wireJson.js functions/fabricNativeAccel.js functions/base58.js functions/bech32.js functions/bolt12Semantics.js functions/fabricPaymentBech32.js functions/fabricDocumentOfferEnvelope.js functions/lightningBolt12.js functions/sipa/bech32.js functions/sipa/segwit_addr.js functions/bip39.js functions/bip32.js",
"exchange": "node scripts/fabric.js exchange",
"fabric:start": "node scripts/fabric.js start",
"clean:all": "rm -rf node_modules package-lock.json",
@@ -168,7 +168,7 @@
"@noble/hashes": "=1.7.1"
},
"jayson": {
- "uuid": "14.0.0"
+ "uuid": "=14.0.0"
}
},
"devDependencies": {
diff --git a/reports/install.log b/reports/install.log
index 15a241230..774ba3dd3 100644
--- a/reports/install.log
+++ b/reports/install.log
@@ -7,10 +7,11 @@ $ npm i
CXX(target) Release/obj.target/fabric/src/binding.o
CC(target) Release/obj.target/fabric/src/crypto.o
CC(target) Release/obj.target/fabric/src/protocol.o
+ CC(target) Release/obj.target/fabric/src/noise.o
CC(target) Release/obj.target/fabric/src/p2p.o
SOLINK_MODULE(target) Release/fabric.node
-added 685 packages, and audited 686 packages in 29s
+added 685 packages, and audited 686 packages in 23s
111 packages are looking for funding
run `npm fund` for details
diff --git a/scripts/chat.js b/scripts/chat.js
index 60eac457e..2a36cd1e9 100755
--- a/scripts/chat.js
+++ b/scripts/chat.js
@@ -85,6 +85,10 @@ async function main () {
SETTINGS.earn = true;
}
+ if (!seed || typeof seed !== 'object') {
+ throw new Error('[FABRIC:CHAT] Wallet data is corrupt or unreadable; run with --force --keygen to regenerate.');
+ }
+
// Load from Seed
settings.key.seed = seed.phrase;
settings.wallet.seed = seed.phrase;
@@ -158,12 +162,18 @@ async function main () {
} else {
console.warn('[FABRIC:KEYGEN]', 'Key file exists, no data will be written. Use --force to override.');
console.warn('[FABRIC:KEYGEN]', '[WARNING]', '--force DESTROYS ALL DATA: DOUBLE-CHECK YOUR BACKUPS!');
+ if (!seed || typeof seed !== 'object') {
+ throw new Error('[FABRIC:KEYGEN] Wallet data is corrupt or unreadable; run with --force --keygen to regenerate.');
+ }
console.warn('[FABRIC:KEYGEN]', 'EXISTING_XPUB', '=', seed.xpub);
}
// prevent further execution
process.exit();
} else if (program.receive) {
+ if (!seed || typeof seed !== 'object' || !seed.phrase) {
+ throw new Error('[FABRIC:CHAT] Wallet data is corrupt or unreadable; run with --force --keygen to regenerate.');
+ }
const wallet = new Wallet({
key: {
seed: seed.phrase
diff --git a/scripts/keygen.js b/scripts/keygen.js
index 6feb5dd60..024aa0f8b 100644
--- a/scripts/keygen.js
+++ b/scripts/keygen.js
@@ -6,7 +6,9 @@ async function main () {
const wallet = new Wallet();
const seed = await wallet._createFromFreshSeed();
const keypair = await wallet.generateCleanKeyPair();
- console.warn('[FABRIC:KEYGEN]', 'Key generated (public):', keypair.public ? keypair.public.toString('hex') : keypair);
+ console.warn('[FABRIC:KEYGEN]', 'Key generated (public):', keypair.public && typeof keypair.public.encodeCompressed === 'function'
+ ? keypair.public.encodeCompressed('hex')
+ : keypair.public);
console.warn('[FABRIC:KEYGEN]', 'SEED PHRASE:', seed.phrase);
process.exit();
}
diff --git a/src/constants.h b/src/constants.h
index 35334f5f7..71b25c317 100644
--- a/src/constants.h
+++ b/src/constants.h
@@ -9,6 +9,9 @@
/* Max payload bytes (4096 byte frame − 208-byte on-wire header: magic…signature) */
#define FABRIC_MAX_MESSAGE_BODY 3888
+/** Max ciphertext bytes per Noise transport frame (matches `noise-protocol-stream` MESSAGE_SIZE). */
+#define FABRIC_NOISE_MAX_FRAME_BYTES 65535
+
// Default networking
#define FABRIC_DEFAULT_PORT 7777
diff --git a/src/noise.c b/src/noise.c
new file mode 100644
index 000000000..c1f9681ed
--- /dev/null
+++ b/src/noise.c
@@ -0,0 +1,304 @@
+#include "noise.h"
+#include "constants.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#ifndef MSG_NOSIGNAL
+#define MSG_NOSIGNAL 0
+#endif
+
+/* Blocking write of exactly n bytes */
+static ssize_t noise_write_all(int fd, const uint8_t *p, size_t n)
+{
+ size_t off = 0;
+ while (off < n)
+ {
+ ssize_t s = send(fd, p + off, n - off, MSG_NOSIGNAL);
+ if (s <= 0)
+ return s;
+ off += (size_t)s;
+ }
+ return (ssize_t)off;
+}
+
+/* Blocking read of exactly n bytes */
+static ssize_t noise_read_all(int fd, uint8_t *p, size_t n)
+{
+ size_t off = 0;
+ while (off < n)
+ {
+ ssize_t r = recv(fd, p + off, n - off, 0);
+ if (r <= 0)
+ return r;
+ off += (size_t)r;
+ }
+ return (ssize_t)off;
+}
+
+/**
+ * Unsigned LEB128 varint write — compatible with npm package `varint` / `length-prefixed-stream`.
+ */
+static int fabric_varint_write_u64(int fd, uint64_t v)
+{
+ uint8_t tmp[16];
+ size_t n = 0;
+ while (v >= 0x80u && n + 1 < sizeof(tmp))
+ {
+ tmp[n++] = (uint8_t)((v & 0x7fu) | 0x80u);
+ v >>= 7;
+ }
+ if (n >= sizeof(tmp))
+ return -1;
+ tmp[n++] = (uint8_t)(v & 0xffu);
+ return noise_write_all(fd, tmp, n) <= 0 ? -1 : 0;
+}
+
+/**
+ * Unsigned LEB128 varint read — compatible with npm `varint.decode` on buffers.
+ */
+static int fabric_varint_read_u64_socket(int fd, uint64_t *out)
+{
+ uint64_t res = 0;
+ int shift = 0;
+ for (int i = 0; i < 10; i++)
+ {
+ uint8_t b;
+ if (noise_read_all(fd, &b, 1) <= 0)
+ return -1;
+ res |= (uint64_t)(b & 0x7fu) << shift;
+ if ((b & 0x80u) == 0)
+ {
+ *out = res;
+ return 0;
+ }
+ shift += 7;
+ }
+ return -1;
+}
+
+int fabric_noise_write_frame(int fd, const uint8_t *payload, size_t len)
+{
+ if (len > FABRIC_NOISE_MAX_FRAME_BYTES)
+ return -1;
+ if (fabric_varint_write_u64(fd, (uint64_t)len) != 0)
+ return -1;
+ if (len > 0 && noise_write_all(fd, payload, len) <= 0)
+ return -1;
+ return 0;
+}
+
+int fabric_noise_read_frame(int fd, uint8_t *payload_out, size_t out_cap, size_t *out_len)
+{
+ uint64_t L;
+ if (fabric_varint_read_u64_socket(fd, &L) != 0)
+ return -1;
+ if (L > FABRIC_NOISE_MAX_FRAME_BYTES || L > out_cap)
+ return -1;
+ if (L > 0 && noise_read_all(fd, payload_out, (size_t)L) <= 0)
+ return -1;
+ *out_len = (size_t)L;
+ return 0;
+}
+
+void noise_get_protocol_id(NoiseAppProtocol app, NoiseProtocolId *out)
+{
+ if (!out)
+ return;
+ /* Noise XX + ChaChaPoly + SHA256 + Curve25519 — same profile as `noise-protocol-stream` WASM. */
+ out->prefix_id = NOISE_PREFIX_STANDARD;
+ out->pattern_id = NOISE_PATTERN_XX;
+ out->cipher_id = NOISE_CIPHER_CHACHAPOLY;
+ out->hash_id = NOISE_HASH_SHA256;
+ out->dh_id = NOISE_DH_CURVE25519;
+ (void)app;
+}
+
+const uint8_t *noise_get_prologue(NoiseAppProtocol app, size_t *out_len)
+{
+ static const uint8_t fabric_prologue[] = "FABRIC";
+ /*
+ * Lightning BOLT #8 uses protocol names like Noise_XK_secp256k1_… with prologue "lightning".
+ * Native Lightning interop would use XK + secp256k1 — not this Curve25519 XX path.
+ */
+ static const uint8_t lightning_prologue[] = "lightning";
+ if (out_len)
+ *out_len = 0;
+ if (app == NOISE_PROTOCOL_LIGHTNING)
+ {
+ if (out_len)
+ *out_len = sizeof(lightning_prologue) - 1;
+ return lightning_prologue;
+ }
+ if (out_len)
+ *out_len = sizeof(fabric_prologue) - 1;
+ return fabric_prologue;
+}
+
+int noise_perform_xx_handshake(int sock,
+ int is_initiator,
+ NoiseAppProtocol app,
+ const uint8_t *local_curve25519_private32_or_null,
+ const uint8_t *remote_curve25519_public32_or_null,
+ NoiseCipherState **out_send,
+ NoiseCipherState **out_recv)
+{
+ static int inited = 0;
+ if (!inited)
+ {
+ if (noise_init() != 0)
+ { /* ignore */
+ }
+ inited = 1;
+ }
+ if (!out_send || !out_recv)
+ return -1;
+ *out_send = NULL;
+ *out_recv = NULL;
+
+ NoiseProtocolId pid;
+ noise_get_protocol_id(app, &pid);
+ NoiseHandshakeState *hs = NULL;
+ if (noise_handshakestate_new_by_id(&hs, &pid, is_initiator ? NOISE_ROLE_INITIATOR : NOISE_ROLE_RESPONDER) != NOISE_ERROR_NONE)
+ {
+ fprintf(stderr, "[noise] new_by_id failed\n");
+ return -1;
+ }
+
+ size_t prologue_len = 0;
+ const uint8_t *prologue = noise_get_prologue(app, &prologue_len);
+ if (noise_handshakestate_set_prologue(hs, prologue, prologue_len) != NOISE_ERROR_NONE)
+ {
+ fprintf(stderr, "[noise] set_prologue failed\n");
+ noise_handshakestate_free(hs);
+ return -1;
+ }
+
+ NoiseDHState *dh = noise_handshakestate_get_local_keypair_dh(hs);
+ if (!dh)
+ {
+ noise_handshakestate_free(hs);
+ return -1;
+ }
+ if (local_curve25519_private32_or_null)
+ {
+ if (noise_dhstate_set_keypair_private(dh, local_curve25519_private32_or_null, 32) != NOISE_ERROR_NONE)
+ {
+ fprintf(stderr, "[noise] set_keypair_private failed\n");
+ noise_handshakestate_free(hs);
+ return -1;
+ }
+ }
+ else if (noise_dhstate_generate_keypair(dh) != NOISE_ERROR_NONE)
+ {
+ fprintf(stderr, "[noise] generate_keypair failed\n");
+ noise_handshakestate_free(hs);
+ return -1;
+ }
+
+ if (remote_curve25519_public32_or_null)
+ {
+ dh = noise_handshakestate_get_remote_public_key_dh(hs);
+ if (!dh || noise_dhstate_set_public_key(dh, remote_curve25519_public32_or_null, 32) != NOISE_ERROR_NONE)
+ {
+ fprintf(stderr, "[noise] set_public_key failed\n");
+ noise_handshakestate_free(hs);
+ return -1;
+ }
+ }
+
+ if (noise_handshakestate_start(hs) != NOISE_ERROR_NONE)
+ {
+ fprintf(stderr, "[noise] start failed\n");
+ noise_handshakestate_free(hs);
+ return -1;
+ }
+
+ int orig_flags = fcntl(sock, F_GETFL, 0);
+ if (orig_flags >= 0)
+ (void)fcntl(sock, F_SETFL, orig_flags & ~O_NONBLOCK);
+
+ uint8_t buf[4096];
+ int complete = 0;
+
+ while (1)
+ {
+ int action = noise_handshakestate_get_action(hs);
+ if (action == NOISE_ACTION_WRITE_MESSAGE)
+ {
+ NoiseBuffer mb;
+ mb.data = buf;
+ mb.size = 0;
+ mb.max_size = sizeof(buf);
+ if (noise_handshakestate_write_message(hs, &mb, NULL) != NOISE_ERROR_NONE)
+ {
+ fprintf(stderr, "[noise] write_message failed\n");
+ noise_handshakestate_free(hs);
+ if (orig_flags >= 0)
+ (void)fcntl(sock, F_SETFL, orig_flags);
+ return -1;
+ }
+ if (fabric_noise_write_frame(sock, mb.data, mb.size) != 0)
+ {
+ fprintf(stderr, "[noise] write frame failed\n");
+ noise_handshakestate_free(hs);
+ if (orig_flags >= 0)
+ (void)fcntl(sock, F_SETFL, orig_flags);
+ return -1;
+ }
+ }
+
+ action = noise_handshakestate_get_action(hs);
+ if (action == NOISE_ACTION_READ_MESSAGE)
+ {
+ size_t need = 0;
+ if (fabric_noise_read_frame(sock, buf, sizeof(buf), &need) != 0)
+ {
+ fprintf(stderr, "[noise] read frame failed\n");
+ noise_handshakestate_free(hs);
+ if (orig_flags >= 0)
+ (void)fcntl(sock, F_SETFL, orig_flags);
+ return -1;
+ }
+ NoiseBuffer mb;
+ mb.data = buf;
+ mb.size = need;
+ mb.max_size = sizeof(buf);
+ if (noise_handshakestate_read_message(hs, &mb, NULL) != NOISE_ERROR_NONE)
+ {
+ fprintf(stderr, "[noise] read_message failed\n");
+ noise_handshakestate_free(hs);
+ if (orig_flags >= 0)
+ (void)fcntl(sock, F_SETFL, orig_flags);
+ return -1;
+ }
+ }
+
+ action = noise_handshakestate_get_action(hs);
+ if (action == NOISE_ACTION_SPLIT)
+ {
+ if (noise_handshakestate_split(hs, out_send, out_recv) != NOISE_ERROR_NONE)
+ {
+ fprintf(stderr, "[noise] split failed\n");
+ noise_handshakestate_free(hs);
+ if (orig_flags >= 0)
+ (void)fcntl(sock, F_SETFL, orig_flags);
+ return -1;
+ }
+ complete = 1;
+ break;
+ }
+ }
+
+ if (orig_flags >= 0)
+ (void)fcntl(sock, F_SETFL, orig_flags);
+
+ noise_handshakestate_free(hs);
+ return complete ? 0 : -1;
+}
diff --git a/src/noise.h b/src/noise.h
index 3adc7051d..532dd91ca 100644
--- a/src/noise.h
+++ b/src/noise.h
@@ -13,11 +13,25 @@ typedef enum {
void noise_get_protocol_id(NoiseAppProtocol app, NoiseProtocolId *out);
const uint8_t *noise_get_prologue(NoiseAppProtocol app, size_t *out_len);
+/**
+ * One encrypted payload on the TCP connection after Noise split.
+ * Uses unsigned varint length + ciphertext — same as `@fabric/core` → `noise-protocol-stream`
+ * → npm `length-prefixed-stream` (NOT Lightning BOLT #8’s 2-byte big-endian length).
+ */
+int fabric_noise_write_frame(int fd, const uint8_t *payload, size_t len);
+
+/** Blocking read of one varint-length frame into payload_out (capacity out_cap). */
+int fabric_noise_read_frame(int fd, uint8_t *payload_out, size_t out_cap, size_t *out_len);
+
+/**
+ * Noise XX handshake matching JS defaults: ephemeral Curve25519 each session unless
+ * local_curve25519_private32_or_null is set (32 bytes).
+ */
int noise_perform_xx_handshake(int sock,
int is_initiator,
NoiseAppProtocol app,
- const uint8_t *local_private32,
- const uint8_t *remote_public32_or_null,
+ const uint8_t *local_curve25519_private32_or_null,
+ const uint8_t *remote_curve25519_public32_or_null,
NoiseCipherState **out_send,
NoiseCipherState **out_recv);
diff --git a/src/peer.c b/src/peer.c
index 709d931c9..192b08826 100644
--- a/src/peer.c
+++ b/src/peer.c
@@ -19,6 +19,7 @@
#include "validation.h"
#include "scoring.h"
#include "datadir.h"
+#include "constants.h"
#ifndef MSG_NOSIGNAL
#define MSG_NOSIGNAL 0
@@ -162,150 +163,27 @@ static NoiseProtocolId protocol_id = {0};
FabricError perform_handshake(Peer *peer, Connection *conn, int is_initiator)
{
- NoiseHandshakeState *handshake = NULL;
- uint8_t buffer[PEER_BUFFER_SIZE];
- int handshake_complete = 0;
- int timeout_count = 0;
- const int max_timeout = 100; // 100 attempts before timeout
-
- // Initialize handshake state with protocol id selected for current peer app
- NoiseProtocolId pid; noise_get_protocol_id(peer->app_protocol, &pid);
- if (noise_handshakestate_new_by_id(&handshake, &pid,
- is_initiator ? NOISE_ROLE_INITIATOR : NOISE_ROLE_RESPONDER) != NOISE_ERROR_NONE)
- {
- return FABRIC_ERROR_NOISE_INIT_FAILED;
- }
-
- // Set prologue according to peer app_protocol
- size_t prologue_len = 0;
- const uint8_t *prologue = noise_get_prologue(conn->is_lightning ? NOISE_PROTOCOL_LIGHTNING : NOISE_PROTOCOL_FABRIC, &prologue_len);
- if (noise_handshakestate_set_prologue(handshake, prologue, prologue_len) != NOISE_ERROR_NONE)
- {
- goto cleanup;
- }
-
- // Set keypairs
- NoiseDHState *dh = noise_handshakestate_get_local_keypair_dh(handshake);
- if (!dh)
- goto cleanup;
-
- if (noise_dhstate_set_keypair_private(dh, peer->private_key, 32) != NOISE_ERROR_NONE)
- {
- goto cleanup;
- }
-
- if (conn->remote_pubkey[0] || conn->remote_pubkey[1])
- {
- dh = noise_handshakestate_get_remote_public_key_dh(handshake);
- if (!dh)
- goto cleanup;
- if (noise_dhstate_set_public_key(dh, conn->remote_pubkey, 32) != NOISE_ERROR_NONE)
- {
- goto cleanup;
- }
- }
-
- // Start handshake
- if (noise_handshakestate_start(handshake) != NOISE_ERROR_NONE)
- {
- goto cleanup;
- }
-
- // Perform handshake
- while (!handshake_complete && timeout_count < max_timeout)
- {
- int action = noise_handshakestate_get_action(handshake);
-
- switch (action)
- {
- case NOISE_ACTION_WRITE_MESSAGE:
- {
- NoiseBuffer mbuf;
- mbuf.data = buffer;
- mbuf.size = 0;
- mbuf.max_size = sizeof(buffer);
-
- if (noise_handshakestate_write_message(handshake, &mbuf, NULL) != NOISE_ERROR_NONE)
- {
- goto cleanup;
- }
-
- ssize_t sent = send(conn->sock, mbuf.data, mbuf.size, MSG_NOSIGNAL);
- if (sent < 0)
- {
- if (errno == EAGAIN || errno == EWOULDBLOCK)
- {
- usleep(1000); // 1ms delay
- timeout_count++;
- continue;
- }
- goto cleanup;
- }
- if ((size_t)sent != mbuf.size)
- {
- goto cleanup;
- }
- break;
- }
-
- case NOISE_ACTION_READ_MESSAGE:
- {
- ssize_t bytes = recv(conn->sock, buffer, sizeof(buffer), 0);
- if (bytes < 0)
- {
- if (errno == EAGAIN || errno == EWOULDBLOCK)
- {
- usleep(1000); // 1ms delay
- timeout_count++;
- continue;
- }
- goto cleanup;
- }
- if (bytes == 0)
- {
- // Connection closed
- goto cleanup;
- }
-
- NoiseBuffer mbuf;
- mbuf.data = buffer;
- mbuf.size = (size_t)bytes;
- mbuf.max_size = sizeof(buffer);
-
- if (noise_handshakestate_read_message(handshake, &mbuf, NULL) != NOISE_ERROR_NONE)
- {
- goto cleanup;
- }
- break;
- }
-
- case NOISE_ACTION_SPLIT:
- {
- if (noise_handshakestate_split(handshake, &conn->send_cipher, &conn->recv_cipher) != NOISE_ERROR_NONE)
- {
- goto cleanup;
- }
- handshake_complete = 1;
- break;
- }
+ if (!peer || !conn || conn->sock < 0)
+ return FABRIC_ERROR_PEER_INIT_FAILED;
- default:
- goto cleanup;
- }
- }
+ NoiseCipherState *send = NULL;
+ NoiseCipherState *recv = NULL;
+ const uint8_t *remote_pk = (conn->remote_pubkey[0] || conn->remote_pubkey[1]) ? conn->remote_pubkey : NULL;
- if (timeout_count >= max_timeout)
+ if (noise_perform_xx_handshake(conn->sock,
+ is_initiator ? 1 : 0,
+ peer->app_protocol,
+ NULL,
+ remote_pk,
+ &send,
+ &recv) != 0)
{
- goto cleanup;
+ return FABRIC_ERROR_NOISE_HANDSHAKE_FAILED;
}
- noise_handshakestate_free(handshake);
+ conn->send_cipher = send;
+ conn->recv_cipher = recv;
return FABRIC_SUCCESS;
-
-cleanup:
- if (handshake)
- noise_handshakestate_free(handshake);
- return FABRIC_ERROR_NOISE_HANDSHAKE_FAILED;
}
Peer *peer_create(void)
@@ -761,7 +639,7 @@ static FabricError peer_connect_lightning(Peer *peer, const char *host, int port
fcntl(conn->sock, F_SETFL, flags | O_NONBLOCK);
conn->is_initiator = 1;
conn->is_lightning = 0;
- if (noise_perform_xx_handshake(conn->sock, 1, peer->app_protocol, peer->private_key, NULL, &conn->send_cipher, &conn->recv_cipher) != 0)
+ if (noise_perform_xx_handshake(conn->sock, 1, peer->app_protocol, NULL, NULL, &conn->send_cipher, &conn->recv_cipher) != 0)
{
close(conn->sock);
FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
@@ -881,7 +759,7 @@ FabricError peer_connect(Peer *peer, const char *host, int port)
// Perform Noise XX handshake via noise.c API
fcntl(conn->sock, F_SETFL, flags | O_NONBLOCK);
conn->is_initiator = 1; conn->is_lightning = 0;
- if (noise_perform_xx_handshake(conn->sock, 1, peer->app_protocol, peer->private_key, NULL, &conn->send_cipher, &conn->recv_cipher) != 0)
+ if (noise_perform_xx_handshake(conn->sock, 1, peer->app_protocol, NULL, NULL, &conn->send_cipher, &conn->recv_cipher) != 0)
{ close(conn->sock); FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION); return FABRIC_ERROR_NOISE_HANDSHAKE_FAILED; }
fcntl(conn->sock, F_SETFL, flags);
@@ -1041,7 +919,7 @@ FabricError peer_accept(Peer *peer, int port)
// Perform handshake as responder
conn->is_initiator = 0;
- if (noise_perform_xx_handshake(conn->sock, 0, peer->app_protocol, peer->private_key, NULL, &conn->send_cipher, &conn->recv_cipher) != 0)
+ if (noise_perform_xx_handshake(conn->sock, 0, peer->app_protocol, NULL, NULL, &conn->send_cipher, &conn->recv_cipher) != 0)
{
fabric_condition_destroy(&conn->conn_condition);
fabric_mutex_destroy(&conn->conn_mutex);
@@ -1179,20 +1057,31 @@ FabricError peer_send_message(Peer *peer, int connection_id, const Message *mess
conn->rl_last_refill = now;
}
- uint8_t buffer[PEER_BUFFER_SIZE];
+ size_t scratch_cap = (size_t)FABRIC_NOISE_MAX_FRAME_BYTES + 512;
+ uint8_t *buffer = malloc(scratch_cap);
+ if (!buffer)
+ {
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ return FABRIC_ERROR_OUT_OF_MEMORY;
+ }
+
size_t message_size = sizeof(Message) - sizeof(uint8_t *) + message->size;
// Enforce per-connection byte/token limits
if (message_size > conn->rl_byte_tokens || conn->rl_msg_tokens == 0)
{
+ free(buffer);
FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
return FABRIC_ERROR_RESOURCE_UNAVAILABLE;
}
// Serialize header via protocol helper (BE magic)
- size_t header_size = protocol_serialize_header(message, buffer, sizeof(buffer));
- if (header_size == 0) {
+ size_t header_size = protocol_serialize_header(message, buffer, scratch_cap);
+ if (header_size == 0)
+ {
+ free(buffer);
FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
return FABRIC_ERROR_MESSAGE_SERIALIZATION_FAILED;
@@ -1201,6 +1090,7 @@ FabricError peer_send_message(Peer *peer, int connection_id, const Message *mess
if (message_verify_body_hash(message) != FABRIC_SUCCESS)
{
printf("[fabricd] abort send: invalid body hash on conn %d\n", connection_id);
+ free(buffer);
FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
return FABRIC_ERROR_VERIFICATION_FAILED;
@@ -1212,6 +1102,7 @@ FabricError peer_send_message(Peer *peer, int connection_id, const Message *mess
if (vr != FABRIC_SUCCESS)
{
printf("[fabricd] abort send: unsigned/invalid signature on conn %d\n", connection_id);
+ free(buffer);
FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
return FABRIC_ERROR_INVALID_SIGNATURE;
@@ -1222,49 +1113,48 @@ FabricError peer_send_message(Peer *peer, int connection_id, const Message *mess
memcpy(buffer + header_size, message->body, message->size);
}
- // Encrypt
+ // Encrypt (plaintext → ciphertext in-place in buffer)
NoiseBuffer noise_buffer;
noise_buffer.data = buffer;
noise_buffer.size = message_size;
- noise_buffer.max_size = sizeof(buffer);
+ noise_buffer.max_size = scratch_cap;
if (noise_cipherstate_encrypt(conn->send_cipher, &noise_buffer) != NOISE_ERROR_NONE)
{
+ free(buffer);
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
return FABRIC_ERROR_NOISE_WRITE_FAILED;
}
- // Add retry logic to send function
- int retries = 3;
- while (retries > 0)
- {
- ssize_t sent = send(conn->sock, noise_buffer.data, noise_buffer.size, MSG_NOSIGNAL);
- if (sent > 0 && (size_t)sent == noise_buffer.size)
- {
- // Consume tokens on success
- if (conn->rl_msg_tokens > 0) conn->rl_msg_tokens--;
- if (conn->rl_byte_tokens >= noise_buffer.size) conn->rl_byte_tokens -= (uint32_t)noise_buffer.size;
- // Unlock connection and release read lock
- FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- return FABRIC_SUCCESS;
- }
+ int sock_flags = fcntl(conn->sock, F_GETFL, 0);
+ if (sock_flags >= 0)
+ (void)fcntl(conn->sock, F_SETFL, sock_flags & ~O_NONBLOCK);
- if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EPIPE)
- {
- break;
- }
- if (errno == EPIPE) {
- // Peer closed; no more retries
- break;
- }
- retries--;
- usleep(1000); // 1ms backoff
+ int fw = fabric_noise_write_frame(conn->sock, noise_buffer.data, noise_buffer.size);
+
+ if (sock_flags >= 0)
+ (void)fcntl(conn->sock, F_SETFL, sock_flags);
+
+ if (fw != 0)
+ {
+ free(buffer);
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ return FABRIC_ERROR_SOCKET_SEND_FAILED;
}
- // Unlock connection and release read lock on error
+
+ free(buffer);
+
+ if (conn->rl_msg_tokens > 0)
+ conn->rl_msg_tokens--;
+ size_t wire_approx = noise_buffer.size + 9;
+ if (conn->rl_byte_tokens >= wire_approx)
+ conn->rl_byte_tokens -= (uint32_t)wire_approx;
+
FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
-
- return FABRIC_ERROR_SOCKET_SEND_FAILED;
+ return FABRIC_SUCCESS;
}
FabricError peer_receive_message(Peer *peer, int connection_id, Message *message)
@@ -1301,138 +1191,147 @@ FabricError peer_receive_message(Peer *peer, int connection_id, Message *message
conn->rl_last_refill = now;
}
- uint8_t buffer[PEER_BUFFER_SIZE];
+ size_t scratch_cap = (size_t)FABRIC_NOISE_MAX_FRAME_BYTES + 512;
+ uint8_t *buffer = malloc(scratch_cap);
+ if (!buffer)
+ {
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ return FABRIC_ERROR_OUT_OF_MEMORY;
+ }
- // Set socket to non-blocking for receive with timeout
int flags = fcntl(conn->sock, F_GETFL, 0);
- fcntl(conn->sock, F_SETFL, flags | O_NONBLOCK);
+ if (flags >= 0)
+ (void)fcntl(conn->sock, F_SETFL, flags & ~O_NONBLOCK);
+
+ size_t enc_len = 0;
+ int frame_rc = fabric_noise_read_frame(conn->sock, buffer, scratch_cap, &enc_len);
+
+ if (flags >= 0)
+ (void)fcntl(conn->sock, F_SETFL, flags);
- // Add retry logic to receive function with timeout
- int retries = 10; // More retries for timeout
- while (retries > 0)
+ if (frame_rc != 0)
{
- ssize_t received = recv(conn->sock, buffer, sizeof(buffer), 0);
- if (received > 0)
- {
- // Setup noise buffer for decryption
- NoiseBuffer noise_buffer;
- noise_buffer.data = buffer;
- noise_buffer.size = (size_t)received;
- noise_buffer.max_size = sizeof(buffer);
-
- // Decrypt the message
- int nd = noise_cipherstate_decrypt(conn->recv_cipher, &noise_buffer);
- if (nd != NOISE_ERROR_NONE)
- {
- printf("[fabricd] decrypt failed (%d) on conn %d\n", nd, connection_id);
- return FABRIC_ERROR_NOISE_READ_FAILED;
- }
+ free(buffer);
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ return FABRIC_ERROR_SOCKET_RECV_FAILED;
+ }
- // Deserialize the message header
- if (noise_buffer.size < sizeof(Message) - sizeof(uint8_t *))
- {
- FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- printf("[fabricd] protocol violation: short header from conn %d\n", connection_id);
- peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "short header");
- return FABRIC_ERROR_MESSAGE_INVALID_FORMAT;
- }
- // Parse header via protocol helper
- FabricError ph = protocol_parse_header(buffer, noise_buffer.size, message);
- if (ph != FABRIC_SUCCESS)
- {
- FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "bad header");
- return ph;
- }
+ NoiseBuffer noise_buffer;
+ noise_buffer.data = buffer;
+ noise_buffer.size = enc_len;
+ noise_buffer.max_size = scratch_cap;
- // Verify message size (defensive bounds)
- size_t body_offset = sizeof(Message) - sizeof(uint8_t *);
- if (message->size > MESSAGE_BODY_SIZE_MAX)
- {
- FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- printf("[fabricd] protocol violation: oversize body %u from conn %d\n", message->size, connection_id);
- peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "oversize body");
- return FABRIC_ERROR_MESSAGE_TOO_LARGE;
- }
- if (message->size > 0)
- {
- if (noise_buffer.size < body_offset + message->size)
- {
- FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- printf("[fabricd] protocol violation: truncated body from conn %d\n", connection_id);
- peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "truncated body");
- return FABRIC_ERROR_MESSAGE_INVALID_FORMAT;
- }
- // Rate limit: ensure we have tokens to accept this message
- if (conn->rl_msg_tokens == 0 || conn->rl_byte_tokens < (body_offset + message->size))
- {
- FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- printf("[fabricd] spam detected: rate limit exceeded on conn %d\n", connection_id);
- peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_SPAM_DETECTED, "rate limit exceeded");
- return FABRIC_ERROR_RESOURCE_UNAVAILABLE;
- }
- // Allocate and copy body
- message->body = malloc(message->size);
- if (!message->body)
- {
- return FABRIC_ERROR_OUT_OF_MEMORY;
- }
- memcpy(message->body, buffer + body_offset, message->size);
- // Verify body hash matches; penalize on mismatch
- if (message_verify_body_hash(message) != FABRIC_SUCCESS)
- {
- FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- printf("[fabricd] protocol violation: bad body hash on conn %d\n", connection_id);
- peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "bad body hash");
- return FABRIC_ERROR_VERIFICATION_FAILED;
- }
- // Enforce signature validity; penalize on failure
- if (peer->secp256k1_ctx && message_verify(message, peer->secp256k1_ctx) != FABRIC_SUCCESS)
- {
- FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- printf("[fabricd] protocol violation: unsigned/invalid signature on conn %d\n", connection_id);
- peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "invalid signature");
- return FABRIC_ERROR_INVALID_SIGNATURE;
- }
- // Consume tokens (header+body as received)
- if (conn->rl_msg_tokens > 0) conn->rl_msg_tokens--;
- size_t consumed = noise_buffer.size; // encrypted size approx; use received length
- if (conn->rl_byte_tokens >= consumed) conn->rl_byte_tokens -= (uint32_t)consumed;
- }
- else
- {
- message->body = NULL;
- }
+ if (noise_cipherstate_decrypt(conn->recv_cipher, &noise_buffer) != NOISE_ERROR_NONE)
+ {
+ free(buffer);
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ printf("[fabricd] decrypt failed on conn %d\n", connection_id);
+ peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "noise decrypt failed");
+ return FABRIC_ERROR_NOISE_READ_FAILED;
+ }
+
+ if (noise_buffer.size < sizeof(Message) - sizeof(uint8_t *))
+ {
+ free(buffer);
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ printf("[fabricd] protocol violation: short header from conn %d\n", connection_id);
+ peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "short header");
+ return FABRIC_ERROR_MESSAGE_INVALID_FORMAT;
+ }
+
+ FabricError ph = protocol_parse_header(buffer, noise_buffer.size, message);
+ if (ph != FABRIC_SUCCESS)
+ {
+ free(buffer);
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "bad header");
+ return ph;
+ }
- // Unlock connection and release read lock on success
+ size_t body_offset = sizeof(Message) - sizeof(uint8_t *);
+ if (message->size > MESSAGE_BODY_SIZE_MAX)
+ {
+ free(buffer);
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ printf("[fabricd] protocol violation: oversize body %u from conn %d\n", message->size, connection_id);
+ peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "oversize body");
+ return FABRIC_ERROR_MESSAGE_TOO_LARGE;
+ }
+
+ if (message->size > 0)
+ {
+ if (noise_buffer.size < body_offset + message->size)
+ {
+ free(buffer);
FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
-
- return FABRIC_SUCCESS;
+ printf("[fabricd] protocol violation: truncated body from conn %d\n", connection_id);
+ peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "truncated body");
+ return FABRIC_ERROR_MESSAGE_INVALID_FORMAT;
}
- if (errno != EAGAIN && errno != EWOULDBLOCK)
+ if (conn->rl_msg_tokens == 0 || conn->rl_byte_tokens < (body_offset + message->size))
{
- break;
+ free(buffer);
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ printf("[fabricd] spam detected: rate limit exceeded on conn %d\n", connection_id);
+ peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_SPAM_DETECTED, "rate limit exceeded");
+ return FABRIC_ERROR_RESOURCE_UNAVAILABLE;
+ }
+ message->body = malloc(message->size);
+ if (!message->body)
+ {
+ free(buffer);
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ return FABRIC_ERROR_OUT_OF_MEMORY;
+ }
+ memcpy(message->body, buffer + body_offset, message->size);
+ if (message_verify_body_hash(message) != FABRIC_SUCCESS)
+ {
+ free(message->body);
+ message->body = NULL;
+ free(buffer);
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ printf("[fabricd] protocol violation: bad body hash on conn %d\n", connection_id);
+ peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "bad body hash");
+ return FABRIC_ERROR_VERIFICATION_FAILED;
}
- retries--;
- usleep(100000); // 100ms backoff for timeout
+ if (peer->secp256k1_ctx && message_verify(message, peer->secp256k1_ctx) != FABRIC_SUCCESS)
+ {
+ free(message->body);
+ message->body = NULL;
+ free(buffer);
+ FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
+ printf("[fabricd] protocol violation: unsigned/invalid signature on conn %d\n", connection_id);
+ peer_flag_and_disconnect(peer, connection_id, PEER_BEHAVIOR_PROTOCOL_VIOLATION, "invalid signature");
+ return FABRIC_ERROR_INVALID_SIGNATURE;
+ }
+ if (conn->rl_msg_tokens > 0)
+ conn->rl_msg_tokens--;
+ size_t consumed = enc_len + 9;
+ if (conn->rl_byte_tokens >= consumed)
+ conn->rl_byte_tokens -= (uint32_t)consumed;
+ }
+ else
+ {
+ message->body = NULL;
}
- // Restore socket to blocking mode
- fcntl(conn->sock, F_SETFL, flags);
+ free(buffer);
- // Unlock connection and release read lock on error
FABRIC_MUTEX_UNLOCK_OR(&conn->conn_mutex, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
FABRIC_RWLOCK_UNLOCK_OR(&peer->connections_rwlock, return FABRIC_ERROR_INTERNAL_STATE_CORRUPTION);
- return FABRIC_ERROR_SOCKET_RECV_FAILED;
+ return FABRIC_SUCCESS;
}
// Receive messages, auto-handle basic protocol, and return only application messages
@@ -1868,7 +1767,7 @@ static void *listener_thread_function(void *arg)
// Perform handshake as responder (accepted socket already non-blocking)
memcpy(conn->remote_pubkey, remote_pubkey + (33 - 32), 32);
- if (noise_perform_xx_handshake(client_sock, 0, peer->app_protocol, peer->private_key, conn->remote_pubkey, &conn->send_cipher, &conn->recv_cipher) != 0)
+ if (noise_perform_xx_handshake(client_sock, 0, peer->app_protocol, NULL, conn->remote_pubkey, &conn->send_cipher, &conn->recv_cipher) != 0)
{
fabric_condition_destroy(&conn->conn_condition);
fabric_mutex_destroy(&conn->conn_mutex);
diff --git a/src/security.c b/src/security.c
index 2896c79db..fafecb0c6 100644
--- a/src/security.c
+++ b/src/security.c
@@ -25,6 +25,10 @@
#include
#endif
+#ifndef _WIN32
+#include
+#endif
+
// Internal allocation tracking for secure free operations
typedef struct allocation_record
{
@@ -36,6 +40,38 @@ typedef struct allocation_record
static allocation_record_t *allocations = NULL;
static int memory_initialized = 0;
+#ifndef _WIN32
+static pthread_mutex_t g_secure_alloc_mutex;
+static int g_secure_alloc_mutex_ready = 0;
+
+static void secure_alloc_mutex_init (void)
+{
+ if (g_secure_alloc_mutex_ready)
+ return;
+
+ pthread_mutexattr_t attr;
+ pthread_mutexattr_init(&attr);
+ pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
+ pthread_mutex_init(&g_secure_alloc_mutex, &attr);
+ pthread_mutexattr_destroy(&attr);
+ g_secure_alloc_mutex_ready = 1;
+}
+
+static inline void secure_alloc_lock (void)
+{
+ secure_alloc_mutex_init();
+ pthread_mutex_lock(&g_secure_alloc_mutex);
+}
+
+static inline void secure_alloc_unlock (void)
+{
+ pthread_mutex_unlock(&g_secure_alloc_mutex);
+}
+#else
+static inline void secure_alloc_lock (void) {}
+static inline void secure_alloc_unlock (void) {}
+#endif
+
// Platform-specific secure zero implementation
#ifdef _WIN32
#define SECURE_ZERO_IMPL(ptr, len) SecureZeroMemory((ptr), (len))
@@ -56,20 +92,25 @@ static void secure_zero_impl(volatile void *ptr, size_t len)
FabricError fabric_secure_memory_init(void)
{
+ secure_alloc_lock();
if (memory_initialized)
{
+ secure_alloc_unlock();
return FABRIC_SUCCESS;
}
allocations = NULL;
memory_initialized = 1;
+ secure_alloc_unlock();
return FABRIC_SUCCESS;
}
void fabric_secure_memory_cleanup(void)
{
+ secure_alloc_lock();
if (!memory_initialized)
{
+ secure_alloc_unlock();
return;
}
@@ -89,29 +130,42 @@ void fabric_secure_memory_cleanup(void)
allocations = NULL;
memory_initialized = 0;
+ secure_alloc_unlock();
}
/** @return 0 on success, -1 if tracking metadata could not be allocated */
static int track_allocation(void *ptr, size_t size)
{
+ secure_alloc_lock();
if (!ptr || size == 0)
+ {
+ secure_alloc_unlock();
return 0;
+ }
allocation_record_t *record = malloc(sizeof(allocation_record_t));
if (!record)
+ {
+ secure_alloc_unlock();
return -1;
+ }
record->ptr = ptr;
record->size = size;
record->next = allocations;
allocations = record;
+ secure_alloc_unlock();
return 0;
}
static size_t untrack_allocation(void *ptr)
{
+ secure_alloc_lock();
if (!ptr)
+ {
+ secure_alloc_unlock();
return 0;
+ }
allocation_record_t **current = &allocations;
while (*current)
@@ -122,11 +176,13 @@ static size_t untrack_allocation(void *ptr)
size_t size = to_remove->size;
*current = to_remove->next;
free(to_remove);
+ secure_alloc_unlock();
return size;
}
current = &(*current)->next;
}
+ secure_alloc_unlock();
return 0; // Not found
}
@@ -205,6 +261,10 @@ void *fabric_secure_realloc(void *ptr, size_t old_size, size_t new_size)
if (old_size == 0)
{
old_size = fabric_secure_malloc_size(ptr);
+ if (old_size == 0)
+ {
+ return NULL;
+ }
}
void *new_ptr = fabric_secure_malloc(new_size);
@@ -322,16 +382,20 @@ size_t fabric_secure_malloc_size(void *ptr)
if (!ptr)
return 0;
+ secure_alloc_lock();
allocation_record_t *current = allocations;
while (current)
{
if (current->ptr == ptr)
{
- return current->size;
+ size_t sz = current->size;
+ secure_alloc_unlock();
+ return sz;
}
current = current->next;
}
+ secure_alloc_unlock();
return 0; // Not found
}
diff --git a/tests/fabric.noise.js b/tests/fabric.noise.js
index 593d855e9..1f6ec89ae 100644
--- a/tests/fabric.noise.js
+++ b/tests/fabric.noise.js
@@ -19,7 +19,7 @@ describe('@fabric/core/types/noise', function () {
// Create NOISE handler
const handler = noise({
- prologue: Buffer.from('fabric'),
+ prologue: Buffer.from('FABRIC'),
verify: (localPrivateKey, localPublicKey, remotePublicKey, done) => {
// Verify the remote public key using our Key class
const remoteKey = new Key({ public: remotePublicKey.toString('hex') });
@@ -44,7 +44,7 @@ describe('@fabric/core/types/noise', function () {
// Create NOISE handlers
const handler1 = noise({
- prologue: Buffer.from('fabric'),
+ prologue: Buffer.from('FABRIC'),
verify: (localPrivateKey, localPublicKey, remotePublicKey, done) => {
const remoteKey = new Key({ public: remotePublicKey.toString('hex') });
const message = 'Hello, Fabric!';
@@ -55,7 +55,7 @@ describe('@fabric/core/types/noise', function () {
});
const handler2 = noise({
- prologue: Buffer.from('fabric'),
+ prologue: Buffer.from('FABRIC'),
verify: (localPrivateKey, localPublicKey, remotePublicKey, done) => {
const remoteKey = new Key({ public: remotePublicKey.toString('hex') });
const message = 'Hello, Fabric!';
@@ -75,7 +75,7 @@ describe('@fabric/core/types/noise', function () {
});
const handler = noise({
- prologue: Buffer.from('fabric'),
+ prologue: Buffer.from('FABRIC'),
verify: (localPrivateKey, localPublicKey, remotePublicKey, done) => {
const remoteKey = new Key({ public: remotePublicKey.toString('hex') });
const message = 'Hello, Fabric!';
diff --git a/types/codec.js b/types/codec.js
index 2f6fb8180..52aaeeb45 100644
--- a/types/codec.js
+++ b/types/codec.js
@@ -53,6 +53,9 @@ class Codec {
const actor = new Actor(data);
const label = new Label(`${this.key.pubkey}/${actor.id}`);
const blob = this.key.encrypt(data);
+ if (blob == null) {
+ throw new Error('[FABRIC:CODEC] Encryption failed — private key unavailable or cipher error');
+ }
this._registerActor(actor);
this._registerLabel(label);
return blob;
diff --git a/types/environment.js b/types/environment.js
index 396602c90..312ee26cf 100644
--- a/types/environment.js
+++ b/types/environment.js
@@ -88,7 +88,7 @@ class Environment extends Entity {
this['FABRIC_SEED'],
this.readVariable('FABRIC_SEED'),
this.local
- ];
+ ].map((candidate) => (typeof candidate === 'string' ? candidate.trim() : candidate));
if (process.env.NODE_ENV === 'test') {
explicit.push(FIXTURE_SEED);
}
@@ -601,18 +601,18 @@ class Environment extends Entity {
}
});
} else if (this.walletExists()) {
- const data = this.readWallet();
- const text = typeof data === 'string' ? data : String(data ?? '');
+ try {
+ const data = this.readWallet();
+ const text = typeof data === 'string' ? data : String(data ?? '');
- if (text.trim() === '') {
- if (this.emit) {
- this.emit('warning', `[FABRIC:KEYGEN] Wallet file is empty (${this.settings.path}); remove it or regenerate with fabric setup`);
+ if (text.trim() === '') {
+ if (this.emit) {
+ this.emit('warning', `[FABRIC:KEYGEN] Wallet file is empty (${this.settings.path}); remove it or regenerate with fabric setup`);
+ }
+ this.wallet = false;
+ return this;
}
- this.wallet = false;
- return this;
- }
- try {
const pr = tryParsePersistedJson(text);
if (!pr.ok) throw pr.error;
const input = pr.value;
diff --git a/types/peer.js b/types/peer.js
index 2e6b3703b..8df12381e 100644
--- a/types/peer.js
+++ b/types/peer.js
@@ -1694,13 +1694,14 @@ class Peer extends Service {
case 'P2P_SESSION_OPEN':
this._handleSessionOpenGenericMessage(msg, origin, signerPubkeyHex);
break;
- case 'P2P_CHAT_MESSAGE':
+ case 'P2P_CHAT_MESSAGE': {
this.emit('chat', msg);
const relay = Message.fromVector(['ChatMessage', JSON.stringify(msg)]);
relay.signWithKey(this.key);
// this.emit('debug', `Relayed chat message: ${JSON.stringify(relay.toGenericMessage())}`);
this.relayFrom(origin.name, relay);
break;
+ }
case 'P2P_STATE_ANNOUNCE':
const state = new Actor(message.object.state);
this.emit('debug', `state_announce ${JSON.stringify(message.object || '')} ${state.toGenericMessage()}`);
@@ -2009,10 +2010,13 @@ class Peer extends Service {
* (body includes `kind: 'documents'` so the browser can merge `object.items`).
* @param {string} originName connection key {@link Peer#connections}
* @param {object[]} items
+ * @param {{ allowEmpty?: boolean }} [opts]
* @returns {boolean}
*/
- _sendLocalInventoryDocumentsWireResponse (originName, items) {
- if (!originName || !items || !items.length) return false;
+ _sendLocalInventoryDocumentsWireResponse (originName, items, opts = {}) {
+ const allowEmpty = !!(opts && opts.allowEmpty);
+ if (!originName || !Array.isArray(items)) return false;
+ if (!allowEmpty && items.length === 0) return false;
const conn = this.connections[originName];
if (!conn || !conn._writeFabric) return false;
const obj = {
@@ -2058,8 +2062,7 @@ class Peer extends Service {
const reqKind = String(req.kind || '').trim().toLowerCase();
if (reqKind !== 'documents') return false;
const items = this._collectDocumentCatalogInventoryItems({});
- if (!items.length) return false;
- return this._sendLocalInventoryDocumentsWireResponse(origin.name, items);
+ return this._sendLocalInventoryDocumentsWireResponse(origin.name, items, { allowEmpty: true });
}
/**