@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bcts/uniform-resources",
3
- "version": "1.0.0-alpha.12",
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/dcbor": "^1.0.0-alpha.12"
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 (number of fragments to mix)
90
- // Uses a simplified soliton distribution
91
- const degree = chooseDegree(rng, seqLen);
94
+ // Choose degree using weighted sampler (1/k distribution)
95
+ const degree = rng.chooseDegree(seqLen);
92
96
 
93
- // Choose which fragments to include
94
- const indices = new Set<number>();
95
- while (indices.size < degree) {
96
- const index = rng.nextInt(0, seqLen);
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
- return Array.from(indices).sort((a, b) => a - b);
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
- // Fountain codes (for advanced multipart handling)
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
- crc32,
60
- MINIMAL_BYTEWORDS_MAP,
40
+ encodeBytewordsIdentifier,
41
+ encodeBytemojisIdentifier,
61
42
  } from "./utils";
@@ -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 rawData = decodeBytewords(partInfo.encodedData, BytewordsStyle.Minimal);
125
+ // Decode bytewords to get CBOR data
126
+ const cborData = decodeBytewords(partInfo.encodedData, BytewordsStyle.Minimal);
125
127
 
126
- if (rawData.length < 8) {
127
- throw new URError("Invalid multipart data: too short");
128
- }
128
+ // Decode the CBOR array
129
+ const decoded = decodeCbor(cborData);
129
130
 
130
- // Extract metadata
131
- const messageLen =
132
- ((rawData[0] << 24) | (rawData[1] << 16) | (rawData[2] << 8) | rawData[3]) >>> 0;
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 checksum =
135
- ((rawData[4] << 24) | (rawData[5] << 16) | (rawData[6] << 8) | rawData[7]) >>> 0;
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
- const data = rawData.slice(8);
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: partInfo.seqNum,
141
- seqLen: partInfo.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
  /**
@@ -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 into bytes for bytewords encoding.
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
- // Simple encoding: messageLen (4 bytes) + checksum (4 bytes) + data
112
- const result = new Uint8Array(8 + part.data.length);
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
- // Message length (big-endian)
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 without separators */
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 with no separator
745
- if (lowercased.length % 4 !== 0) {
746
- throw new Error("Invalid URI bytewords length");
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
- bytes.push(index);
756
- }
752
+ return index;
753
+ });
757
754
  break;
758
755
  }
759
756
  case BytewordsStyle.Minimal: {