@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 +2 -2
- package/dist/index.cjs +153 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +5 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.iife.js +154 -103
- package/dist/index.iife.js.map +1 -1
- package/dist/index.mjs +154 -101
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/fountain.ts +15 -35
- package/src/multipart-decoder.ts +30 -14
- package/src/multipart-encoder.ts +6 -19
- package/src/utils.ts +7 -10
- package/src/xoshiro.ts +170 -76
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sha256 } from "@bcts/crypto";
|
|
2
|
+
|
|
1
3
|
//#region ../dcbor/dist/index.mjs
|
|
2
4
|
var __create = Object.create;
|
|
3
5
|
var __defProp = Object.defineProperty;
|
|
@@ -7244,7 +7246,7 @@ function encodeBytemojisIdentifier(data) {
|
|
|
7244
7246
|
let BytewordsStyle = /* @__PURE__ */ function(BytewordsStyle$1) {
|
|
7245
7247
|
/** Full 4-letter words separated by spaces */
|
|
7246
7248
|
BytewordsStyle$1["Standard"] = "standard";
|
|
7247
|
-
/** Full 4-letter words
|
|
7249
|
+
/** Full 4-letter words separated by hyphens (URI-safe) */
|
|
7248
7250
|
BytewordsStyle$1["Uri"] = "uri";
|
|
7249
7251
|
/** First and last character only (minimal) - used by UR encoding */
|
|
7250
7252
|
BytewordsStyle$1["Minimal"] = "minimal";
|
|
@@ -7320,7 +7322,7 @@ function encodeBytewords(data, style = BytewordsStyle.Minimal) {
|
|
|
7320
7322
|
}
|
|
7321
7323
|
switch (style) {
|
|
7322
7324
|
case BytewordsStyle.Standard: return words.join(" ");
|
|
7323
|
-
case BytewordsStyle.Uri:
|
|
7325
|
+
case BytewordsStyle.Uri: return words.join("-");
|
|
7324
7326
|
case BytewordsStyle.Minimal: return words.join("");
|
|
7325
7327
|
}
|
|
7326
7328
|
}
|
|
@@ -7340,14 +7342,11 @@ function decodeBytewords(encoded, style = BytewordsStyle.Minimal) {
|
|
|
7340
7342
|
});
|
|
7341
7343
|
break;
|
|
7342
7344
|
case BytewordsStyle.Uri:
|
|
7343
|
-
|
|
7344
|
-
bytes = [];
|
|
7345
|
-
for (let i = 0; i < lowercased.length; i += 4) {
|
|
7346
|
-
const word = lowercased.slice(i, i + 4);
|
|
7345
|
+
bytes = lowercased.split("-").map((word) => {
|
|
7347
7346
|
const index = BYTEWORDS_MAP.get(word);
|
|
7348
7347
|
if (index === void 0) throw new Error(`Invalid byteword: ${word}`);
|
|
7349
|
-
|
|
7350
|
-
}
|
|
7348
|
+
return index;
|
|
7349
|
+
});
|
|
7351
7350
|
break;
|
|
7352
7351
|
case BytewordsStyle.Minimal:
|
|
7353
7352
|
if (lowercased.length % 2 !== 0) throw new Error("Invalid minimal bytewords length");
|
|
@@ -7652,6 +7651,7 @@ function isURCodable(obj) {
|
|
|
7652
7651
|
* for deterministic fragment selection in fountain codes.
|
|
7653
7652
|
*
|
|
7654
7653
|
* Reference: https://prng.di.unimi.it/
|
|
7654
|
+
* BC-UR Reference: https://github.com/nicklockwood/fountain-codes
|
|
7655
7655
|
*/
|
|
7656
7656
|
const MAX_UINT64 = BigInt("0xffffffffffffffff");
|
|
7657
7657
|
/**
|
|
@@ -7671,21 +7671,28 @@ function rotl(x, k) {
|
|
|
7671
7671
|
var Xoshiro256 = class Xoshiro256 {
|
|
7672
7672
|
s;
|
|
7673
7673
|
/**
|
|
7674
|
-
* Creates a new Xoshiro256** instance from a seed.
|
|
7674
|
+
* Creates a new Xoshiro256** instance from a 32-byte seed.
|
|
7675
7675
|
*
|
|
7676
|
-
* The seed
|
|
7677
|
-
*
|
|
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.
|
|
7678
7679
|
*
|
|
7679
|
-
* @param seed - The seed bytes (
|
|
7680
|
+
* @param seed - The seed bytes (must be exactly 32 bytes)
|
|
7680
7681
|
*/
|
|
7681
7682
|
constructor(seed) {
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
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
|
|
7688
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;
|
|
7689
7696
|
}
|
|
7690
7697
|
/**
|
|
7691
7698
|
* Creates a Xoshiro256** instance from raw state values.
|
|
@@ -7702,33 +7709,6 @@ var Xoshiro256 = class Xoshiro256 {
|
|
|
7702
7709
|
return instance;
|
|
7703
7710
|
}
|
|
7704
7711
|
/**
|
|
7705
|
-
* Simple hash function for seeding.
|
|
7706
|
-
* This is a basic implementation - in production use SHA-256.
|
|
7707
|
-
*/
|
|
7708
|
-
hashSeed(seed) {
|
|
7709
|
-
const result = new Uint8Array(32);
|
|
7710
|
-
if (seed.length === 0) return result;
|
|
7711
|
-
for (let i = 0; i < 32; i++) {
|
|
7712
|
-
let hash = 0;
|
|
7713
|
-
for (const byte of seed) hash = hash * 31 + byte + i >>> 0;
|
|
7714
|
-
hash ^= hash >>> 16;
|
|
7715
|
-
hash = hash * 2246822507 >>> 0;
|
|
7716
|
-
hash ^= hash >>> 13;
|
|
7717
|
-
hash = hash * 3266489909 >>> 0;
|
|
7718
|
-
hash ^= hash >>> 16;
|
|
7719
|
-
result[i] = hash & 255;
|
|
7720
|
-
}
|
|
7721
|
-
return result;
|
|
7722
|
-
}
|
|
7723
|
-
/**
|
|
7724
|
-
* Converts 8 bytes to a 64-bit BigInt (little-endian).
|
|
7725
|
-
*/
|
|
7726
|
-
bytesToBigInt(bytes) {
|
|
7727
|
-
let result = 0n;
|
|
7728
|
-
for (let i = 7; i >= 0; i--) result = result << 8n | BigInt(bytes[i] ?? 0);
|
|
7729
|
-
return result;
|
|
7730
|
-
}
|
|
7731
|
-
/**
|
|
7732
7712
|
* Generates the next 64-bit random value.
|
|
7733
7713
|
*/
|
|
7734
7714
|
next() {
|
|
@@ -7744,17 +7724,19 @@ var Xoshiro256 = class Xoshiro256 {
|
|
|
7744
7724
|
}
|
|
7745
7725
|
/**
|
|
7746
7726
|
* Generates a random double in [0, 1).
|
|
7727
|
+
* Matches BC-UR reference: self.next() as f64 / (u64::MAX as f64 + 1.0)
|
|
7747
7728
|
*/
|
|
7748
7729
|
nextDouble() {
|
|
7749
7730
|
const value = this.next();
|
|
7750
|
-
return Number(value
|
|
7731
|
+
return Number(value) / 0x10000000000000000;
|
|
7751
7732
|
}
|
|
7752
7733
|
/**
|
|
7753
|
-
* 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
|
|
7754
7736
|
*/
|
|
7755
7737
|
nextInt(low, high) {
|
|
7756
|
-
const range$1 = high - low;
|
|
7757
|
-
return
|
|
7738
|
+
const range$1 = high - low + 1;
|
|
7739
|
+
return Math.floor(this.nextDouble() * range$1) + low;
|
|
7758
7740
|
}
|
|
7759
7741
|
/**
|
|
7760
7742
|
* Generates a random byte [0, 255].
|
|
@@ -7770,24 +7752,102 @@ var Xoshiro256 = class Xoshiro256 {
|
|
|
7770
7752
|
for (let i = 0; i < count; i++) result[i] = this.nextByte();
|
|
7771
7753
|
return result;
|
|
7772
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
|
+
}
|
|
7773
7831
|
};
|
|
7774
7832
|
/**
|
|
7775
|
-
* Creates a
|
|
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.
|
|
7776
7837
|
*
|
|
7777
|
-
* This
|
|
7778
|
-
* for a given message and part number.
|
|
7838
|
+
* This matches the BC-UR reference implementation.
|
|
7779
7839
|
*/
|
|
7780
7840
|
function createSeed(checksum, seqNum) {
|
|
7781
|
-
const
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
return
|
|
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 sha256(seed8);
|
|
7791
7851
|
}
|
|
7792
7852
|
|
|
7793
7853
|
//#endregion
|
|
@@ -7834,6 +7894,11 @@ function xorBytes(a, b) {
|
|
|
7834
7894
|
* This uses a seeded Xoshiro256** PRNG to deterministically select fragments,
|
|
7835
7895
|
* ensuring encoder and decoder agree without explicit coordination.
|
|
7836
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
|
+
*
|
|
7837
7902
|
* @param seqNum - The sequence number (1-based)
|
|
7838
7903
|
* @param seqLen - Total number of pure fragments
|
|
7839
7904
|
* @param checksum - CRC32 checksum of the message
|
|
@@ -7842,26 +7907,10 @@ function xorBytes(a, b) {
|
|
|
7842
7907
|
function chooseFragments(seqNum, seqLen, checksum) {
|
|
7843
7908
|
if (seqNum <= seqLen) return [seqNum - 1];
|
|
7844
7909
|
const rng = new Xoshiro256(createSeed(checksum, seqNum));
|
|
7845
|
-
const degree = chooseDegree(
|
|
7846
|
-
const
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
indices.add(index);
|
|
7850
|
-
}
|
|
7851
|
-
return Array.from(indices).sort((a, b) => a - b);
|
|
7852
|
-
}
|
|
7853
|
-
/**
|
|
7854
|
-
* Chooses the degree (number of fragments to mix) using a simplified
|
|
7855
|
-
* robust soliton distribution.
|
|
7856
|
-
*
|
|
7857
|
-
* This ensures good coverage of fragments for efficient decoding.
|
|
7858
|
-
*/
|
|
7859
|
-
function chooseDegree(rng, seqLen) {
|
|
7860
|
-
const r = rng.nextDouble();
|
|
7861
|
-
if (r < .5) return 1;
|
|
7862
|
-
else if (r < .75) return 2;
|
|
7863
|
-
else if (r < .9) return Math.min(3, seqLen);
|
|
7864
|
-
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);
|
|
7865
7914
|
}
|
|
7866
7915
|
/**
|
|
7867
7916
|
* Mixes the selected fragments using XOR.
|
|
@@ -8128,20 +8177,17 @@ var MultipartEncoder = class {
|
|
|
8128
8177
|
return `ur:${this._ur.urTypeStr()}/${part.seqNum}-${part.seqLen}/${encoded}`;
|
|
8129
8178
|
}
|
|
8130
8179
|
/**
|
|
8131
|
-
* Encodes part metadata and data
|
|
8180
|
+
* Encodes part metadata and data as CBOR for bytewords encoding.
|
|
8181
|
+
* Format: CBOR array [seqNum, seqLen, messageLen, checksum, data]
|
|
8132
8182
|
*/
|
|
8133
8183
|
_encodePartData(part) {
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
result[6] = part.checksum >>> 8 & 255;
|
|
8142
|
-
result[7] = part.checksum & 255;
|
|
8143
|
-
result.set(part.data, 8);
|
|
8144
|
-
return result;
|
|
8184
|
+
return cbor([
|
|
8185
|
+
part.seqNum,
|
|
8186
|
+
part.seqLen,
|
|
8187
|
+
part.messageLen,
|
|
8188
|
+
part.checksum,
|
|
8189
|
+
part.data
|
|
8190
|
+
]).toData();
|
|
8145
8191
|
}
|
|
8146
8192
|
/**
|
|
8147
8193
|
* Gets the current part index.
|
|
@@ -8239,16 +8285,23 @@ var MultipartDecoder = class {
|
|
|
8239
8285
|
}
|
|
8240
8286
|
/**
|
|
8241
8287
|
* Decodes a multipart UR's fountain part data.
|
|
8288
|
+
*
|
|
8289
|
+
* The multipart body is a CBOR array: [seqNum, seqLen, messageLen, checksum, data]
|
|
8242
8290
|
*/
|
|
8243
8291
|
_decodeFountainPart(partInfo) {
|
|
8244
|
-
const
|
|
8245
|
-
if (
|
|
8246
|
-
const
|
|
8247
|
-
|
|
8248
|
-
const
|
|
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}`);
|
|
8249
8302
|
return {
|
|
8250
|
-
seqNum
|
|
8251
|
-
seqLen
|
|
8303
|
+
seqNum,
|
|
8304
|
+
seqLen,
|
|
8252
8305
|
messageLen,
|
|
8253
8306
|
checksum,
|
|
8254
8307
|
data
|