@bcts/uniform-resources 1.0.0-alpha.13 → 1.0.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,5 +1,5 @@
1
- Copyright © 2023 Blockchain Commons, LLC
2
- Copyright © 2025 Leonardo Amoroso Custodio
1
+ Copyright © 2023-2026 Blockchain Commons, LLC
2
+ Copyright © 2025-2026 Leonardo Amoroso Custodio
3
3
 
4
4
  Redistribution and use in source and binary forms, with or without modification,
5
5
  are permitted provided that the following conditions are met:
package/dist/index.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ let _bcts_crypto = require("@bcts/crypto");
1
2
 
2
3
  //#region ../dcbor/dist/index.mjs
3
4
  var __create = Object.create;
@@ -7245,7 +7246,7 @@ function encodeBytemojisIdentifier(data) {
7245
7246
  let BytewordsStyle = /* @__PURE__ */ function(BytewordsStyle$1) {
7246
7247
  /** Full 4-letter words separated by spaces */
7247
7248
  BytewordsStyle$1["Standard"] = "standard";
7248
- /** Full 4-letter words without separators */
7249
+ /** Full 4-letter words separated by hyphens (URI-safe) */
7249
7250
  BytewordsStyle$1["Uri"] = "uri";
7250
7251
  /** First and last character only (minimal) - used by UR encoding */
7251
7252
  BytewordsStyle$1["Minimal"] = "minimal";
@@ -7321,7 +7322,7 @@ function encodeBytewords(data, style = BytewordsStyle.Minimal) {
7321
7322
  }
7322
7323
  switch (style) {
7323
7324
  case BytewordsStyle.Standard: return words.join(" ");
7324
- case BytewordsStyle.Uri:
7325
+ case BytewordsStyle.Uri: return words.join("-");
7325
7326
  case BytewordsStyle.Minimal: return words.join("");
7326
7327
  }
7327
7328
  }
@@ -7341,14 +7342,11 @@ function decodeBytewords(encoded, style = BytewordsStyle.Minimal) {
7341
7342
  });
7342
7343
  break;
7343
7344
  case BytewordsStyle.Uri:
7344
- if (lowercased.length % 4 !== 0) throw new Error("Invalid URI bytewords length");
7345
- bytes = [];
7346
- for (let i = 0; i < lowercased.length; i += 4) {
7347
- const word = lowercased.slice(i, i + 4);
7345
+ bytes = lowercased.split("-").map((word) => {
7348
7346
  const index = BYTEWORDS_MAP.get(word);
7349
7347
  if (index === void 0) throw new Error(`Invalid byteword: ${word}`);
7350
- bytes.push(index);
7351
- }
7348
+ return index;
7349
+ });
7352
7350
  break;
7353
7351
  case BytewordsStyle.Minimal:
7354
7352
  if (lowercased.length % 2 !== 0) throw new Error("Invalid minimal bytewords length");
@@ -7653,6 +7651,7 @@ function isURCodable(obj) {
7653
7651
  * for deterministic fragment selection in fountain codes.
7654
7652
  *
7655
7653
  * Reference: https://prng.di.unimi.it/
7654
+ * BC-UR Reference: https://github.com/nicklockwood/fountain-codes
7656
7655
  */
7657
7656
  const MAX_UINT64 = BigInt("0xffffffffffffffff");
7658
7657
  /**
@@ -7672,21 +7671,28 @@ function rotl(x, k) {
7672
7671
  var Xoshiro256 = class Xoshiro256 {
7673
7672
  s;
7674
7673
  /**
7675
- * Creates a new Xoshiro256** instance from a seed.
7674
+ * Creates a new Xoshiro256** instance from a 32-byte seed.
7676
7675
  *
7677
- * The seed is hashed using SHA-256 to initialize the state.
7678
- * For consistent results across encoder/decoder, use the same seed.
7676
+ * The seed must be exactly 32 bytes (256 bits). The bytes are interpreted
7677
+ * using the BC-UR reference algorithm: each 8-byte chunk is read as
7678
+ * big-endian then stored as little-endian for the state.
7679
7679
  *
7680
- * @param seed - The seed bytes (any length)
7680
+ * @param seed - The seed bytes (must be exactly 32 bytes)
7681
7681
  */
7682
7682
  constructor(seed) {
7683
- const hash = this.hashSeed(seed);
7684
- this.s = [
7685
- this.bytesToBigInt(hash.slice(0, 8)),
7686
- this.bytesToBigInt(hash.slice(8, 16)),
7687
- this.bytesToBigInt(hash.slice(16, 24)),
7688
- this.bytesToBigInt(hash.slice(24, 32))
7683
+ if (seed.length !== 32) throw new Error(`Seed must be 32 bytes, got ${seed.length}`);
7684
+ const s = [
7685
+ 0n,
7686
+ 0n,
7687
+ 0n,
7688
+ 0n
7689
7689
  ];
7690
+ for (let i = 0; i < 4; i++) {
7691
+ let v = 0n;
7692
+ for (let n = 0; n < 8; n++) v = v << 8n | BigInt(seed[8 * i + n] ?? 0);
7693
+ s[i] = v;
7694
+ }
7695
+ this.s = s;
7690
7696
  }
7691
7697
  /**
7692
7698
  * Creates a Xoshiro256** instance from raw state values.
@@ -7703,33 +7709,6 @@ var Xoshiro256 = class Xoshiro256 {
7703
7709
  return instance;
7704
7710
  }
7705
7711
  /**
7706
- * Simple hash function for seeding.
7707
- * This is a basic implementation - in production use SHA-256.
7708
- */
7709
- hashSeed(seed) {
7710
- const result = new Uint8Array(32);
7711
- if (seed.length === 0) return result;
7712
- for (let i = 0; i < 32; i++) {
7713
- let hash = 0;
7714
- for (const byte of seed) hash = hash * 31 + byte + i >>> 0;
7715
- hash ^= hash >>> 16;
7716
- hash = hash * 2246822507 >>> 0;
7717
- hash ^= hash >>> 13;
7718
- hash = hash * 3266489909 >>> 0;
7719
- hash ^= hash >>> 16;
7720
- result[i] = hash & 255;
7721
- }
7722
- return result;
7723
- }
7724
- /**
7725
- * Converts 8 bytes to a 64-bit BigInt (little-endian).
7726
- */
7727
- bytesToBigInt(bytes) {
7728
- let result = 0n;
7729
- for (let i = 7; i >= 0; i--) result = result << 8n | BigInt(bytes[i] ?? 0);
7730
- return result;
7731
- }
7732
- /**
7733
7712
  * Generates the next 64-bit random value.
7734
7713
  */
7735
7714
  next() {
@@ -7745,17 +7724,19 @@ var Xoshiro256 = class Xoshiro256 {
7745
7724
  }
7746
7725
  /**
7747
7726
  * Generates a random double in [0, 1).
7727
+ * Matches BC-UR reference: self.next() as f64 / (u64::MAX as f64 + 1.0)
7748
7728
  */
7749
7729
  nextDouble() {
7750
7730
  const value = this.next();
7751
- return Number(value >> 11n) / Number(1n << 53n);
7731
+ return Number(value) / 0x10000000000000000;
7752
7732
  }
7753
7733
  /**
7754
- * Generates a random integer in [low, high).
7734
+ * Generates a random integer in [low, high] (inclusive).
7735
+ * Matches BC-UR reference: (self.next_double() * ((high - low + 1) as f64)) as u64 + low
7755
7736
  */
7756
7737
  nextInt(low, high) {
7757
- const range$1 = high - low;
7758
- return low + Math.floor(this.nextDouble() * range$1);
7738
+ const range$1 = high - low + 1;
7739
+ return Math.floor(this.nextDouble() * range$1) + low;
7759
7740
  }
7760
7741
  /**
7761
7742
  * Generates a random byte [0, 255].
@@ -7771,24 +7752,102 @@ var Xoshiro256 = class Xoshiro256 {
7771
7752
  for (let i = 0; i < count; i++) result[i] = this.nextByte();
7772
7753
  return result;
7773
7754
  }
7755
+ /**
7756
+ * Shuffles items by repeatedly picking random indices.
7757
+ * Matches BC-UR reference implementation.
7758
+ */
7759
+ shuffled(items) {
7760
+ const source = [...items];
7761
+ const shuffled = [];
7762
+ while (source.length > 0) {
7763
+ const index = this.nextInt(0, source.length - 1);
7764
+ const item = source.splice(index, 1)[0];
7765
+ if (item !== void 0) shuffled.push(item);
7766
+ }
7767
+ return shuffled;
7768
+ }
7769
+ /**
7770
+ * Chooses the degree (number of fragments to mix) using a weighted sampler.
7771
+ * Uses the robust soliton distribution with weights [1/1, 1/2, 1/3, ..., 1/n].
7772
+ * Matches BC-UR reference implementation.
7773
+ */
7774
+ chooseDegree(seqLen) {
7775
+ const weights = [];
7776
+ for (let i = 1; i <= seqLen; i++) weights.push(1 / i);
7777
+ return new WeightedSampler(weights).next(this) + 1;
7778
+ }
7779
+ };
7780
+ /**
7781
+ * Weighted sampler using Vose's alias method.
7782
+ * Allows O(1) sampling from a discrete probability distribution.
7783
+ */
7784
+ var WeightedSampler = class {
7785
+ aliases;
7786
+ probs;
7787
+ constructor(weights) {
7788
+ const n = weights.length;
7789
+ if (n === 0) throw new Error("Weights array cannot be empty");
7790
+ const sum = weights.reduce((a, b) => a + b, 0);
7791
+ if (sum <= 0) throw new Error("Weights must sum to a positive value");
7792
+ const normalized = weights.map((w) => w * n / sum);
7793
+ this.aliases = Array.from({ length: n }).fill(0);
7794
+ this.probs = Array.from({ length: n }).fill(0);
7795
+ const small = [];
7796
+ const large = [];
7797
+ for (let i = n - 1; i >= 0; i--) if (normalized[i] < 1) small.push(i);
7798
+ else large.push(i);
7799
+ while (small.length > 0 && large.length > 0) {
7800
+ const a = small.pop();
7801
+ const g = large.pop();
7802
+ if (a === void 0 || g === void 0) break;
7803
+ this.probs[a] = normalized[a] ?? 0;
7804
+ this.aliases[a] = g;
7805
+ normalized[g] = (normalized[g] ?? 0) + (normalized[a] ?? 0) - 1;
7806
+ if (normalized[g] !== void 0 && normalized[g] < 1) small.push(g);
7807
+ else large.push(g);
7808
+ }
7809
+ while (large.length > 0) {
7810
+ const g = large.pop();
7811
+ if (g === void 0) break;
7812
+ this.probs[g] = 1;
7813
+ }
7814
+ while (small.length > 0) {
7815
+ const a = small.pop();
7816
+ if (a === void 0) break;
7817
+ this.probs[a] = 1;
7818
+ }
7819
+ }
7820
+ /**
7821
+ * Sample from the distribution.
7822
+ */
7823
+ next(rng) {
7824
+ const r1 = rng.nextDouble();
7825
+ const r2 = rng.nextDouble();
7826
+ const n = this.probs.length;
7827
+ const i = Math.floor(n * r1);
7828
+ if (r2 < this.probs[i]) return i;
7829
+ else return this.aliases[i];
7830
+ }
7774
7831
  };
7775
7832
  /**
7776
- * Creates a seed for the Xoshiro PRNG from message checksum and sequence number.
7833
+ * Creates a Xoshiro256 PRNG instance from message checksum and sequence number.
7834
+ *
7835
+ * This creates an 8-byte seed by concatenating seqNum and checksum (both in
7836
+ * big-endian), then hashes it with SHA-256 to get the 32-byte seed for Xoshiro.
7777
7837
  *
7778
- * This ensures that both encoder and decoder produce the same random sequence
7779
- * for a given message and part number.
7838
+ * This matches the BC-UR reference implementation.
7780
7839
  */
7781
7840
  function createSeed(checksum, seqNum) {
7782
- const seed = new Uint8Array(8);
7783
- seed[0] = checksum >>> 24 & 255;
7784
- seed[1] = checksum >>> 16 & 255;
7785
- seed[2] = checksum >>> 8 & 255;
7786
- seed[3] = checksum & 255;
7787
- seed[4] = seqNum >>> 24 & 255;
7788
- seed[5] = seqNum >>> 16 & 255;
7789
- seed[6] = seqNum >>> 8 & 255;
7790
- seed[7] = seqNum & 255;
7791
- return seed;
7841
+ const seed8 = new Uint8Array(8);
7842
+ seed8[0] = seqNum >>> 24 & 255;
7843
+ seed8[1] = seqNum >>> 16 & 255;
7844
+ seed8[2] = seqNum >>> 8 & 255;
7845
+ seed8[3] = seqNum & 255;
7846
+ seed8[4] = checksum >>> 24 & 255;
7847
+ seed8[5] = checksum >>> 16 & 255;
7848
+ seed8[6] = checksum >>> 8 & 255;
7849
+ seed8[7] = checksum & 255;
7850
+ return (0, _bcts_crypto.sha256)(seed8);
7792
7851
  }
7793
7852
 
7794
7853
  //#endregion
@@ -7835,6 +7894,11 @@ function xorBytes(a, b) {
7835
7894
  * This uses a seeded Xoshiro256** PRNG to deterministically select fragments,
7836
7895
  * ensuring encoder and decoder agree without explicit coordination.
7837
7896
  *
7897
+ * The algorithm matches the BC-UR reference implementation:
7898
+ * 1. For pure parts (seqNum <= seqLen), return single fragment index
7899
+ * 2. For mixed parts, use weighted sampling to choose degree
7900
+ * 3. Shuffle all indices and take the first 'degree' indices
7901
+ *
7838
7902
  * @param seqNum - The sequence number (1-based)
7839
7903
  * @param seqLen - Total number of pure fragments
7840
7904
  * @param checksum - CRC32 checksum of the message
@@ -7843,26 +7907,10 @@ function xorBytes(a, b) {
7843
7907
  function chooseFragments(seqNum, seqLen, checksum) {
7844
7908
  if (seqNum <= seqLen) return [seqNum - 1];
7845
7909
  const rng = new Xoshiro256(createSeed(checksum, seqNum));
7846
- const degree = chooseDegree(rng, seqLen);
7847
- const indices = /* @__PURE__ */ new Set();
7848
- while (indices.size < degree) {
7849
- const index = rng.nextInt(0, seqLen);
7850
- indices.add(index);
7851
- }
7852
- return Array.from(indices).sort((a, b) => a - b);
7853
- }
7854
- /**
7855
- * Chooses the degree (number of fragments to mix) using a simplified
7856
- * robust soliton distribution.
7857
- *
7858
- * This ensures good coverage of fragments for efficient decoding.
7859
- */
7860
- function chooseDegree(rng, seqLen) {
7861
- const r = rng.nextDouble();
7862
- if (r < .5) return 1;
7863
- else if (r < .75) return 2;
7864
- else if (r < .9) return Math.min(3, seqLen);
7865
- else return Math.min(rng.nextInt(4, seqLen + 1), seqLen);
7910
+ const degree = rng.chooseDegree(seqLen);
7911
+ const allIndices = [];
7912
+ for (let i = 0; i < seqLen; i++) allIndices.push(i);
7913
+ return rng.shuffled(allIndices).slice(0, degree);
7866
7914
  }
7867
7915
  /**
7868
7916
  * Mixes the selected fragments using XOR.
@@ -8129,20 +8177,17 @@ var MultipartEncoder = class {
8129
8177
  return `ur:${this._ur.urTypeStr()}/${part.seqNum}-${part.seqLen}/${encoded}`;
8130
8178
  }
8131
8179
  /**
8132
- * Encodes part metadata and data into bytes for bytewords encoding.
8180
+ * Encodes part metadata and data as CBOR for bytewords encoding.
8181
+ * Format: CBOR array [seqNum, seqLen, messageLen, checksum, data]
8133
8182
  */
8134
8183
  _encodePartData(part) {
8135
- const result = new Uint8Array(8 + part.data.length);
8136
- result[0] = part.messageLen >>> 24 & 255;
8137
- result[1] = part.messageLen >>> 16 & 255;
8138
- result[2] = part.messageLen >>> 8 & 255;
8139
- result[3] = part.messageLen & 255;
8140
- result[4] = part.checksum >>> 24 & 255;
8141
- result[5] = part.checksum >>> 16 & 255;
8142
- result[6] = part.checksum >>> 8 & 255;
8143
- result[7] = part.checksum & 255;
8144
- result.set(part.data, 8);
8145
- return result;
8184
+ return cbor([
8185
+ part.seqNum,
8186
+ part.seqLen,
8187
+ part.messageLen,
8188
+ part.checksum,
8189
+ part.data
8190
+ ]).toData();
8146
8191
  }
8147
8192
  /**
8148
8193
  * Gets the current part index.
@@ -8240,16 +8285,23 @@ var MultipartDecoder = class {
8240
8285
  }
8241
8286
  /**
8242
8287
  * Decodes a multipart UR's fountain part data.
8288
+ *
8289
+ * The multipart body is a CBOR array: [seqNum, seqLen, messageLen, checksum, data]
8243
8290
  */
8244
8291
  _decodeFountainPart(partInfo) {
8245
- const rawData = decodeBytewords(partInfo.encodedData, BytewordsStyle.Minimal);
8246
- if (rawData.length < 8) throw new URError("Invalid multipart data: too short");
8247
- const messageLen = (rawData[0] << 24 | rawData[1] << 16 | rawData[2] << 8 | rawData[3]) >>> 0;
8248
- const checksum = (rawData[4] << 24 | rawData[5] << 16 | rawData[6] << 8 | rawData[7]) >>> 0;
8249
- const data = rawData.slice(8);
8292
+ const decoded = decodeCbor(decodeBytewords(partInfo.encodedData, BytewordsStyle.Minimal));
8293
+ if (decoded.type !== MajorType.Array) throw new URError("Invalid multipart data: expected CBOR array");
8294
+ const items = decoded.value;
8295
+ if (items.length !== 5) throw new URError(`Invalid multipart data: expected 5 elements, got ${items.length}`);
8296
+ const seqNum = Number(items[0].value);
8297
+ const seqLen = Number(items[1].value);
8298
+ const messageLen = Number(items[2].value);
8299
+ const checksum = Number(items[3].value);
8300
+ const data = items[4].value;
8301
+ if (seqNum !== partInfo.seqNum || seqLen !== partInfo.seqLen) throw new URError(`Multipart metadata mismatch: URL says ${partInfo.seqNum}-${partInfo.seqLen}, CBOR says ${seqNum}-${seqLen}`);
8250
8302
  return {
8251
- seqNum: partInfo.seqNum,
8252
- seqLen: partInfo.seqLen,
8303
+ seqNum,
8304
+ seqLen,
8253
8305
  messageLen,
8254
8306
  checksum,
8255
8307
  data