@bcts/uniform-resources 1.0.0-alpha.12 → 1.0.0-alpha.14
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/dist/index.cjs +191 -213
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -249
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +13 -249
- package/dist/index.d.mts.map +1 -1
- package/dist/index.iife.js +192 -215
- package/dist/index.iife.js.map +1 -1
- package/dist/index.mjs +192 -200
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -5
- package/src/error.ts +11 -0
- package/src/fountain.ts +15 -35
- package/src/index.ts +5 -24
- package/src/multipart-decoder.ts +30 -36
- package/src/multipart-encoder.ts +6 -46
- package/src/utils.ts +7 -10
- package/src/xoshiro.ts +170 -76
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bcts/uniform-resources",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Blockchain Commons Uniform Resources (UR) for TypeScript",
|
|
6
6
|
"license": "BSD-2-Clause-Patent",
|
|
@@ -47,11 +47,12 @@
|
|
|
47
47
|
"dev": "tsdown --watch",
|
|
48
48
|
"test": "vitest run",
|
|
49
49
|
"test:watch": "vitest",
|
|
50
|
-
"lint": "eslint 'src/**/*.ts'",
|
|
51
|
-
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
|
50
|
+
"lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'",
|
|
51
|
+
"lint:fix": "eslint 'src/**/*.ts' 'tests/**/*.ts' --fix",
|
|
52
52
|
"typecheck": "tsc --noEmit",
|
|
53
53
|
"clean": "rm -rf dist",
|
|
54
|
-
"docs": "typedoc"
|
|
54
|
+
"docs": "typedoc",
|
|
55
|
+
"prepublishOnly": "npm run clean && npm run build && npm test"
|
|
55
56
|
},
|
|
56
57
|
"keywords": [
|
|
57
58
|
"uniform-resources",
|
|
@@ -66,7 +67,8 @@
|
|
|
66
67
|
"node": ">=18.0.0"
|
|
67
68
|
},
|
|
68
69
|
"dependencies": {
|
|
69
|
-
"@bcts/
|
|
70
|
+
"@bcts/crypto": "^1.0.0-alpha.14",
|
|
71
|
+
"@bcts/dcbor": "^1.0.0-alpha.14"
|
|
70
72
|
},
|
|
71
73
|
"devDependencies": {
|
|
72
74
|
"@bcts/eslint": "^0.1.0",
|
package/src/error.ts
CHANGED
|
@@ -78,6 +78,17 @@ export class CBORError extends URError {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Error type for UR decoder errors.
|
|
83
|
+
* Matches Rust's Error::UR(String) variant.
|
|
84
|
+
*/
|
|
85
|
+
export class URDecodeError extends URError {
|
|
86
|
+
constructor(message: string) {
|
|
87
|
+
super(`UR decoder error (${message})`);
|
|
88
|
+
this.name = "URDecodeError";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
export type Result<T> = T | Error;
|
|
82
93
|
|
|
83
94
|
/**
|
package/src/fountain.ts
CHANGED
|
@@ -71,6 +71,11 @@ export function xorBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
|
71
71
|
* This uses a seeded Xoshiro256** PRNG to deterministically select fragments,
|
|
72
72
|
* ensuring encoder and decoder agree without explicit coordination.
|
|
73
73
|
*
|
|
74
|
+
* The algorithm matches the BC-UR reference implementation:
|
|
75
|
+
* 1. For pure parts (seqNum <= seqLen), return single fragment index
|
|
76
|
+
* 2. For mixed parts, use weighted sampling to choose degree
|
|
77
|
+
* 3. Shuffle all indices and take the first 'degree' indices
|
|
78
|
+
*
|
|
74
79
|
* @param seqNum - The sequence number (1-based)
|
|
75
80
|
* @param seqLen - Total number of pure fragments
|
|
76
81
|
* @param checksum - CRC32 checksum of the message
|
|
@@ -86,43 +91,18 @@ export function chooseFragments(seqNum: number, seqLen: number, checksum: number
|
|
|
86
91
|
const seed = createSeed(checksum, seqNum);
|
|
87
92
|
const rng = new Xoshiro256(seed);
|
|
88
93
|
|
|
89
|
-
// Choose degree
|
|
90
|
-
|
|
91
|
-
const degree = chooseDegree(rng, seqLen);
|
|
94
|
+
// Choose degree using weighted sampler (1/k distribution)
|
|
95
|
+
const degree = rng.chooseDegree(seqLen);
|
|
92
96
|
|
|
93
|
-
//
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
indices.add(index);
|
|
97
|
+
// Create array of all indices [0, 1, 2, ..., seqLen-1]
|
|
98
|
+
const allIndices: number[] = [];
|
|
99
|
+
for (let i = 0; i < seqLen; i++) {
|
|
100
|
+
allIndices.push(i);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Chooses the degree (number of fragments to mix) using a simplified
|
|
105
|
-
* robust soliton distribution.
|
|
106
|
-
*
|
|
107
|
-
* This ensures good coverage of fragments for efficient decoding.
|
|
108
|
-
*/
|
|
109
|
-
function chooseDegree(rng: Xoshiro256, seqLen: number): number {
|
|
110
|
-
// Use a simplified distribution that tends toward lower degrees
|
|
111
|
-
// but can occasionally include more fragments
|
|
112
|
-
const r = rng.nextDouble();
|
|
113
|
-
|
|
114
|
-
// Probability distribution favoring lower degrees
|
|
115
|
-
// Based on robust soliton distribution
|
|
116
|
-
if (r < 0.5) {
|
|
117
|
-
return 1;
|
|
118
|
-
} else if (r < 0.75) {
|
|
119
|
-
return 2;
|
|
120
|
-
} else if (r < 0.9) {
|
|
121
|
-
return Math.min(3, seqLen);
|
|
122
|
-
} else {
|
|
123
|
-
// Higher degrees are less common but help with convergence
|
|
124
|
-
return Math.min(rng.nextInt(4, seqLen + 1), seqLen);
|
|
125
|
-
}
|
|
103
|
+
// Shuffle all indices and take the first 'degree' indices
|
|
104
|
+
const shuffled = rng.shuffled(allIndices);
|
|
105
|
+
return shuffled.slice(0, degree);
|
|
126
106
|
}
|
|
127
107
|
|
|
128
108
|
/**
|
|
@@ -264,7 +244,7 @@ export class FountainDecoder {
|
|
|
264
244
|
const indices = chooseFragments(part.seqNum, this.seqLen, this.checksum);
|
|
265
245
|
|
|
266
246
|
if (indices.length === 1) {
|
|
267
|
-
// Pure fragment
|
|
247
|
+
// Pure fragment (or degree-1 mixed that acts like pure)
|
|
268
248
|
const index = indices[0];
|
|
269
249
|
if (!this.pureFragments.has(index)) {
|
|
270
250
|
this.pureFragments.set(index, part.data);
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
export { UR } from "./ur";
|
|
3
3
|
export { URType } from "./ur-type";
|
|
4
4
|
|
|
5
|
-
// Error types
|
|
5
|
+
// Error types (matching Rust's Error enum variants)
|
|
6
6
|
export {
|
|
7
7
|
URError,
|
|
8
|
+
URDecodeError,
|
|
8
9
|
InvalidSchemeError,
|
|
9
10
|
TypeUnspecifiedError,
|
|
10
11
|
InvalidTypeError,
|
|
@@ -29,33 +30,13 @@ export type { URCodable } from "./ur-codable";
|
|
|
29
30
|
export { MultipartEncoder } from "./multipart-encoder";
|
|
30
31
|
export { MultipartDecoder } from "./multipart-decoder";
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
+
// Bytewords module (matching Rust's pub mod bytewords)
|
|
33
34
|
export {
|
|
34
|
-
FountainEncoder,
|
|
35
|
-
FountainDecoder,
|
|
36
|
-
splitMessage,
|
|
37
|
-
xorBytes,
|
|
38
|
-
chooseFragments,
|
|
39
|
-
mixFragments,
|
|
40
|
-
} from "./fountain";
|
|
41
|
-
export type { FountainPart } from "./fountain";
|
|
42
|
-
|
|
43
|
-
// PRNG for deterministic fountain code mixing
|
|
44
|
-
export { Xoshiro256, createSeed } from "./xoshiro";
|
|
45
|
-
|
|
46
|
-
// Utilities
|
|
47
|
-
export {
|
|
48
|
-
isURTypeChar,
|
|
49
|
-
isValidURType,
|
|
50
|
-
validateURType,
|
|
51
35
|
BYTEWORDS,
|
|
52
|
-
BYTEWORDS_MAP,
|
|
53
36
|
BYTEMOJIS,
|
|
54
|
-
encodeBytewordsIdentifier,
|
|
55
|
-
encodeBytemojisIdentifier,
|
|
56
37
|
BytewordsStyle,
|
|
57
38
|
encodeBytewords,
|
|
58
39
|
decodeBytewords,
|
|
59
|
-
|
|
60
|
-
|
|
40
|
+
encodeBytewordsIdentifier,
|
|
41
|
+
encodeBytemojisIdentifier,
|
|
61
42
|
} from "./utils";
|
package/src/multipart-decoder.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeCbor } from "@bcts/dcbor";
|
|
1
|
+
import { decodeCbor, MajorType, type Cbor } from "@bcts/dcbor";
|
|
2
2
|
import { InvalidSchemeError, InvalidTypeError, UnexpectedTypeError, URError } from "./error.js";
|
|
3
3
|
import { UR } from "./ur.js";
|
|
4
4
|
import { URType } from "./ur-type.js";
|
|
@@ -118,27 +118,43 @@ export class MultipartDecoder {
|
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
120
|
* Decodes a multipart UR's fountain part data.
|
|
121
|
+
*
|
|
122
|
+
* The multipart body is a CBOR array: [seqNum, seqLen, messageLen, checksum, data]
|
|
121
123
|
*/
|
|
122
124
|
private _decodeFountainPart(partInfo: MultipartInfo): FountainPart {
|
|
123
|
-
// Decode bytewords
|
|
124
|
-
const
|
|
125
|
+
// Decode bytewords to get CBOR data
|
|
126
|
+
const cborData = decodeBytewords(partInfo.encodedData, BytewordsStyle.Minimal);
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
128
|
+
// Decode the CBOR array
|
|
129
|
+
const decoded = decodeCbor(cborData);
|
|
129
130
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
// The decoded value should be an array with 5 elements
|
|
132
|
+
if (decoded.type !== MajorType.Array) {
|
|
133
|
+
throw new URError("Invalid multipart data: expected CBOR array");
|
|
134
|
+
}
|
|
133
135
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
+
const items = decoded.value as Cbor[];
|
|
137
|
+
if (items.length !== 5) {
|
|
138
|
+
throw new URError(`Invalid multipart data: expected 5 elements, got ${items.length}`);
|
|
139
|
+
}
|
|
136
140
|
|
|
137
|
-
|
|
141
|
+
// Extract the fields: [seqNum, seqLen, messageLen, checksum, data]
|
|
142
|
+
const seqNum = Number(items[0].value);
|
|
143
|
+
const seqLen = Number(items[1].value);
|
|
144
|
+
const messageLen = Number(items[2].value);
|
|
145
|
+
const checksum = Number(items[3].value);
|
|
146
|
+
const data = items[4].value as Uint8Array;
|
|
147
|
+
|
|
148
|
+
// Verify seqNum and seqLen match the URL path values
|
|
149
|
+
if (seqNum !== partInfo.seqNum || seqLen !== partInfo.seqLen) {
|
|
150
|
+
throw new URError(
|
|
151
|
+
`Multipart metadata mismatch: URL says ${partInfo.seqNum}-${partInfo.seqLen}, CBOR says ${seqNum}-${seqLen}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
138
154
|
|
|
139
155
|
return {
|
|
140
|
-
seqNum
|
|
141
|
-
seqLen
|
|
156
|
+
seqNum,
|
|
157
|
+
seqLen,
|
|
142
158
|
messageLen,
|
|
143
159
|
checksum,
|
|
144
160
|
data,
|
|
@@ -160,28 +176,6 @@ export class MultipartDecoder {
|
|
|
160
176
|
message(): UR | null {
|
|
161
177
|
return this._decodedMessage;
|
|
162
178
|
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Returns the decoding progress as a fraction (0 to 1).
|
|
166
|
-
*/
|
|
167
|
-
progress(): number {
|
|
168
|
-
if (this._decodedMessage !== null) {
|
|
169
|
-
return 1;
|
|
170
|
-
}
|
|
171
|
-
if (this._fountainDecoder === null) {
|
|
172
|
-
return 0;
|
|
173
|
-
}
|
|
174
|
-
return this._fountainDecoder.progress();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Resets the decoder to receive a new message.
|
|
179
|
-
*/
|
|
180
|
-
reset(): void {
|
|
181
|
-
this._urType = null;
|
|
182
|
-
this._fountainDecoder = null;
|
|
183
|
-
this._decodedMessage = null;
|
|
184
|
-
}
|
|
185
179
|
}
|
|
186
180
|
|
|
187
181
|
/**
|
package/src/multipart-encoder.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { UR } from "./ur.js";
|
|
|
2
2
|
import { URError } from "./error.js";
|
|
3
3
|
import { FountainEncoder, type FountainPart } from "./fountain.js";
|
|
4
4
|
import { encodeBytewords, BytewordsStyle } from "./utils.js";
|
|
5
|
+
import { cbor } from "@bcts/dcbor";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Encodes a UR as multiple parts using fountain codes.
|
|
@@ -58,15 +59,6 @@ export class MultipartEncoder {
|
|
|
58
59
|
this._fountainEncoder = new FountainEncoder(cborData, maxFragmentLen);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
/**
|
|
62
|
-
* Returns whether the message fits in a single part.
|
|
63
|
-
*
|
|
64
|
-
* For single-part messages, consider using UR.string() directly.
|
|
65
|
-
*/
|
|
66
|
-
isSinglePart(): boolean {
|
|
67
|
-
return this._fountainEncoder.isSinglePart();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
62
|
/**
|
|
71
63
|
* Gets the next part of the encoding.
|
|
72
64
|
*
|
|
@@ -105,28 +97,14 @@ export class MultipartEncoder {
|
|
|
105
97
|
}
|
|
106
98
|
|
|
107
99
|
/**
|
|
108
|
-
* Encodes part metadata and data
|
|
100
|
+
* Encodes part metadata and data as CBOR for bytewords encoding.
|
|
101
|
+
* Format: CBOR array [seqNum, seqLen, messageLen, checksum, data]
|
|
109
102
|
*/
|
|
110
103
|
private _encodePartData(part: FountainPart): Uint8Array {
|
|
111
|
-
//
|
|
112
|
-
const
|
|
104
|
+
// Create CBOR array with 5 elements: [seqNum, seqLen, messageLen, checksum, data]
|
|
105
|
+
const cborArray = cbor([part.seqNum, part.seqLen, part.messageLen, part.checksum, part.data]);
|
|
113
106
|
|
|
114
|
-
|
|
115
|
-
result[0] = (part.messageLen >>> 24) & 0xff;
|
|
116
|
-
result[1] = (part.messageLen >>> 16) & 0xff;
|
|
117
|
-
result[2] = (part.messageLen >>> 8) & 0xff;
|
|
118
|
-
result[3] = part.messageLen & 0xff;
|
|
119
|
-
|
|
120
|
-
// Checksum (big-endian)
|
|
121
|
-
result[4] = (part.checksum >>> 24) & 0xff;
|
|
122
|
-
result[5] = (part.checksum >>> 16) & 0xff;
|
|
123
|
-
result[6] = (part.checksum >>> 8) & 0xff;
|
|
124
|
-
result[7] = part.checksum & 0xff;
|
|
125
|
-
|
|
126
|
-
// Fragment data
|
|
127
|
-
result.set(part.data, 8);
|
|
128
|
-
|
|
129
|
-
return result;
|
|
107
|
+
return cborArray.toData();
|
|
130
108
|
}
|
|
131
109
|
|
|
132
110
|
/**
|
|
@@ -145,22 +123,4 @@ export class MultipartEncoder {
|
|
|
145
123
|
partsCount(): number {
|
|
146
124
|
return this._fountainEncoder.seqLen;
|
|
147
125
|
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Checks if all pure parts have been emitted.
|
|
151
|
-
*
|
|
152
|
-
* Even after this returns true, you can continue calling nextPart()
|
|
153
|
-
* to generate additional rateless parts for redundancy.
|
|
154
|
-
*/
|
|
155
|
-
isComplete(): boolean {
|
|
156
|
-
return this._fountainEncoder.isComplete();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Resets the encoder to start from the beginning.
|
|
161
|
-
*/
|
|
162
|
-
reset(): void {
|
|
163
|
-
this._currentIndex = 0;
|
|
164
|
-
this._fountainEncoder.reset();
|
|
165
|
-
}
|
|
166
126
|
}
|
package/src/utils.ts
CHANGED
|
@@ -615,7 +615,7 @@ export function encodeBytemojisIdentifier(data: Uint8Array): string {
|
|
|
615
615
|
export enum BytewordsStyle {
|
|
616
616
|
/** Full 4-letter words separated by spaces */
|
|
617
617
|
Standard = "standard",
|
|
618
|
-
/** Full 4-letter words
|
|
618
|
+
/** Full 4-letter words separated by hyphens (URI-safe) */
|
|
619
619
|
Uri = "uri",
|
|
620
620
|
/** First and last character only (minimal) - used by UR encoding */
|
|
621
621
|
Minimal = "minimal",
|
|
@@ -712,6 +712,7 @@ export function encodeBytewords(
|
|
|
712
712
|
case BytewordsStyle.Standard:
|
|
713
713
|
return words.join(" ");
|
|
714
714
|
case BytewordsStyle.Uri:
|
|
715
|
+
return words.join("-");
|
|
715
716
|
case BytewordsStyle.Minimal:
|
|
716
717
|
return words.join("");
|
|
717
718
|
}
|
|
@@ -741,19 +742,15 @@ export function decodeBytewords(
|
|
|
741
742
|
break;
|
|
742
743
|
}
|
|
743
744
|
case BytewordsStyle.Uri: {
|
|
744
|
-
// 4-character words
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
bytes = [];
|
|
749
|
-
for (let i = 0; i < lowercased.length; i += 4) {
|
|
750
|
-
const word = lowercased.slice(i, i + 4);
|
|
745
|
+
// 4-character words separated by hyphens
|
|
746
|
+
const words = lowercased.split("-");
|
|
747
|
+
bytes = words.map((word) => {
|
|
751
748
|
const index = BYTEWORDS_MAP.get(word);
|
|
752
749
|
if (index === undefined) {
|
|
753
750
|
throw new Error(`Invalid byteword: ${word}`);
|
|
754
751
|
}
|
|
755
|
-
|
|
756
|
-
}
|
|
752
|
+
return index;
|
|
753
|
+
});
|
|
757
754
|
break;
|
|
758
755
|
}
|
|
759
756
|
case BytewordsStyle.Minimal: {
|