@bcts/uniform-resources 1.0.0-alpha.5

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/src/utils.ts ADDED
@@ -0,0 +1,802 @@
1
+ import { InvalidTypeError } from "./error.js";
2
+
3
+ /**
4
+ * Checks if a character is a valid UR type character.
5
+ * Valid characters are lowercase letters, digits, and hyphens.
6
+ */
7
+ export function isURTypeChar(char: string): boolean {
8
+ const code = char.charCodeAt(0);
9
+ // Check for lowercase letters (a-z)
10
+ if (code >= 97 && code <= 122) return true;
11
+ // Check for digits (0-9)
12
+ if (code >= 48 && code <= 57) return true;
13
+ // Check for hyphen (-)
14
+ if (code === 45) return true;
15
+ return false;
16
+ }
17
+
18
+ /**
19
+ * Checks if a string is a valid UR type.
20
+ * Valid UR types contain only lowercase letters, digits, and hyphens.
21
+ */
22
+ export function isValidURType(urType: string): boolean {
23
+ if (urType.length === 0) return false;
24
+ return Array.from(urType).every((char) => isURTypeChar(char));
25
+ }
26
+
27
+ /**
28
+ * Validates and returns a UR type, or throws an error if invalid.
29
+ */
30
+ export function validateURType(urType: string): string {
31
+ if (!isValidURType(urType)) {
32
+ throw new InvalidTypeError();
33
+ }
34
+ return urType;
35
+ }
36
+
37
+ /**
38
+ * Bytewords for encoding/decoding bytes as words.
39
+ * See: https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-004-bytewords.md
40
+ */
41
+ export const BYTEWORDS: string[] = [
42
+ "able",
43
+ "acid",
44
+ "also",
45
+ "apex",
46
+ "aqua",
47
+ "arch",
48
+ "atom",
49
+ "aunt",
50
+ "away",
51
+ "axis",
52
+ "back",
53
+ "bald",
54
+ "barn",
55
+ "belt",
56
+ "beta",
57
+ "bias",
58
+ "blue",
59
+ "body",
60
+ "brag",
61
+ "brew",
62
+ "bulb",
63
+ "buzz",
64
+ "calm",
65
+ "cash",
66
+ "cats",
67
+ "chef",
68
+ "city",
69
+ "claw",
70
+ "code",
71
+ "cola",
72
+ "cook",
73
+ "cost",
74
+ "crux",
75
+ "curl",
76
+ "cusp",
77
+ "cyan",
78
+ "dark",
79
+ "data",
80
+ "days",
81
+ "deli",
82
+ "dice",
83
+ "diet",
84
+ "door",
85
+ "down",
86
+ "draw",
87
+ "drop",
88
+ "drum",
89
+ "dull",
90
+ "duty",
91
+ "each",
92
+ "easy",
93
+ "echo",
94
+ "edge",
95
+ "epic",
96
+ "even",
97
+ "exam",
98
+ "exit",
99
+ "eyes",
100
+ "fact",
101
+ "fair",
102
+ "fern",
103
+ "figs",
104
+ "film",
105
+ "fish",
106
+ "fizz",
107
+ "flap",
108
+ "flew",
109
+ "flux",
110
+ "foxy",
111
+ "free",
112
+ "frog",
113
+ "fuel",
114
+ "fund",
115
+ "gala",
116
+ "game",
117
+ "gear",
118
+ "gems",
119
+ "gift",
120
+ "girl",
121
+ "glow",
122
+ "good",
123
+ "gray",
124
+ "grim",
125
+ "guru",
126
+ "gush",
127
+ "gyro",
128
+ "half",
129
+ "hang",
130
+ "hard",
131
+ "hawk",
132
+ "heat",
133
+ "help",
134
+ "high",
135
+ "hill",
136
+ "holy",
137
+ "hope",
138
+ "horn",
139
+ "huts",
140
+ "iced",
141
+ "idea",
142
+ "idle",
143
+ "inch",
144
+ "inky",
145
+ "into",
146
+ "iris",
147
+ "iron",
148
+ "item",
149
+ "jade",
150
+ "jazz",
151
+ "join",
152
+ "jolt",
153
+ "jowl",
154
+ "judo",
155
+ "jugs",
156
+ "jump",
157
+ "junk",
158
+ "jury",
159
+ "keep",
160
+ "keno",
161
+ "kept",
162
+ "keys",
163
+ "kick",
164
+ "kiln",
165
+ "king",
166
+ "kite",
167
+ "kiwi",
168
+ "knob",
169
+ "lamb",
170
+ "lava",
171
+ "lazy",
172
+ "leaf",
173
+ "legs",
174
+ "liar",
175
+ "limp",
176
+ "lion",
177
+ "list",
178
+ "logo",
179
+ "loud",
180
+ "love",
181
+ "luau",
182
+ "luck",
183
+ "lung",
184
+ "main",
185
+ "many",
186
+ "math",
187
+ "maze",
188
+ "memo",
189
+ "menu",
190
+ "meow",
191
+ "mild",
192
+ "mint",
193
+ "miss",
194
+ "monk",
195
+ "nail",
196
+ "navy",
197
+ "need",
198
+ "news",
199
+ "next",
200
+ "noon",
201
+ "note",
202
+ "numb",
203
+ "obey",
204
+ "oboe",
205
+ "omit",
206
+ "onyx",
207
+ "open",
208
+ "oval",
209
+ "owls",
210
+ "paid",
211
+ "part",
212
+ "peck",
213
+ "play",
214
+ "plus",
215
+ "poem",
216
+ "pool",
217
+ "pose",
218
+ "puff",
219
+ "puma",
220
+ "purr",
221
+ "quad",
222
+ "quiz",
223
+ "race",
224
+ "ramp",
225
+ "real",
226
+ "redo",
227
+ "rich",
228
+ "road",
229
+ "rock",
230
+ "roof",
231
+ "ruby",
232
+ "ruin",
233
+ "runs",
234
+ "rust",
235
+ "safe",
236
+ "saga",
237
+ "scar",
238
+ "sets",
239
+ "silk",
240
+ "skew",
241
+ "slot",
242
+ "soap",
243
+ "solo",
244
+ "song",
245
+ "stub",
246
+ "surf",
247
+ "swan",
248
+ "taco",
249
+ "task",
250
+ "taxi",
251
+ "tent",
252
+ "tied",
253
+ "time",
254
+ "tiny",
255
+ "toil",
256
+ "tomb",
257
+ "toys",
258
+ "trip",
259
+ "tuna",
260
+ "twin",
261
+ "ugly",
262
+ "undo",
263
+ "unit",
264
+ "urge",
265
+ "user",
266
+ "vast",
267
+ "very",
268
+ "veto",
269
+ "vial",
270
+ "vibe",
271
+ "view",
272
+ "visa",
273
+ "void",
274
+ "vows",
275
+ "wall",
276
+ "wand",
277
+ "warm",
278
+ "wasp",
279
+ "wave",
280
+ "waxy",
281
+ "webs",
282
+ "what",
283
+ "when",
284
+ "whiz",
285
+ "wolf",
286
+ "work",
287
+ "yank",
288
+ "yawn",
289
+ "yell",
290
+ "yoga",
291
+ "yurt",
292
+ "zaps",
293
+ "zero",
294
+ "zest",
295
+ "zinc",
296
+ "zone",
297
+ "zoom",
298
+ ];
299
+
300
+ /**
301
+ * Create a reverse mapping for fast byteword lookup.
302
+ */
303
+ function createBytewordsMap(): Map<string, number> {
304
+ const map = new Map<string, number>();
305
+ BYTEWORDS.forEach((word, index) => {
306
+ map.set(word, index);
307
+ });
308
+ return map;
309
+ }
310
+
311
+ export const BYTEWORDS_MAP = createBytewordsMap();
312
+
313
+ /**
314
+ * Bytemojis for encoding/decoding bytes as emojis.
315
+ * See: https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2024-008-bytemoji.md
316
+ */
317
+ export const BYTEMOJIS: string[] = [
318
+ "😀",
319
+ "😂",
320
+ "😆",
321
+ "😉",
322
+ "🙄",
323
+ "😋",
324
+ "😎",
325
+ "😍",
326
+ "😘",
327
+ "😭",
328
+ "🫠",
329
+ "🥱",
330
+ "🤩",
331
+ "😶",
332
+ "🤨",
333
+ "🫥",
334
+ "🥵",
335
+ "🥶",
336
+ "😳",
337
+ "🤪",
338
+ "😵",
339
+ "😡",
340
+ "🤢",
341
+ "😇",
342
+ "🤠",
343
+ "🤡",
344
+ "🥳",
345
+ "🥺",
346
+ "😬",
347
+ "🤑",
348
+ "🙃",
349
+ "🤯",
350
+ "😈",
351
+ "👹",
352
+ "👺",
353
+ "💀",
354
+ "👻",
355
+ "👽",
356
+ "😺",
357
+ "😹",
358
+ "😻",
359
+ "😽",
360
+ "🙀",
361
+ "😿",
362
+ "🫶",
363
+ "🤲",
364
+ "🙌",
365
+ "🤝",
366
+ "👍",
367
+ "👎",
368
+ "👈",
369
+ "👆",
370
+ "💪",
371
+ "👄",
372
+ "🦷",
373
+ "👂",
374
+ "👃",
375
+ "🧠",
376
+ "👀",
377
+ "🤚",
378
+ "🦶",
379
+ "🍎",
380
+ "🍊",
381
+ "🍋",
382
+ "🍌",
383
+ "🍉",
384
+ "🍇",
385
+ "🍓",
386
+ "🫐",
387
+ "🍒",
388
+ "🍑",
389
+ "🍍",
390
+ "🥝",
391
+ "🍆",
392
+ "🥑",
393
+ "🥦",
394
+ "🍅",
395
+ "🌽",
396
+ "🥕",
397
+ "🫒",
398
+ "🧄",
399
+ "🥐",
400
+ "🥯",
401
+ "🍞",
402
+ "🧀",
403
+ "🥚",
404
+ "🍗",
405
+ "🌭",
406
+ "🍔",
407
+ "🍟",
408
+ "🍕",
409
+ "🌮",
410
+ "🥙",
411
+ "🍱",
412
+ "🍜",
413
+ "🍤",
414
+ "🍚",
415
+ "🥠",
416
+ "🍨",
417
+ "🍦",
418
+ "🎂",
419
+ "🪴",
420
+ "🌵",
421
+ "🌱",
422
+ "💐",
423
+ "🍁",
424
+ "🍄",
425
+ "🌹",
426
+ "🌺",
427
+ "🌼",
428
+ "🌻",
429
+ "🌸",
430
+ "💨",
431
+ "🌊",
432
+ "💧",
433
+ "💦",
434
+ "🌀",
435
+ "🌈",
436
+ "🌞",
437
+ "🌝",
438
+ "🌛",
439
+ "🌜",
440
+ "🌙",
441
+ "🌎",
442
+ "💫",
443
+ "⭐",
444
+ "🪐",
445
+ "🌐",
446
+ "💛",
447
+ "💔",
448
+ "💘",
449
+ "💖",
450
+ "💕",
451
+ "🏁",
452
+ "🚩",
453
+ "💬",
454
+ "💯",
455
+ "🚫",
456
+ "🔴",
457
+ "🔷",
458
+ "🟩",
459
+ "🛑",
460
+ "🔺",
461
+ "🚗",
462
+ "🚑",
463
+ "🚒",
464
+ "🚜",
465
+ "🛵",
466
+ "🚨",
467
+ "🚀",
468
+ "🚁",
469
+ "🛟",
470
+ "🚦",
471
+ "🏰",
472
+ "🎡",
473
+ "🎢",
474
+ "🎠",
475
+ "🏠",
476
+ "🔔",
477
+ "🔑",
478
+ "🚪",
479
+ "🪑",
480
+ "🎈",
481
+ "💌",
482
+ "📦",
483
+ "📫",
484
+ "📖",
485
+ "📚",
486
+ "📌",
487
+ "🧮",
488
+ "🔒",
489
+ "💎",
490
+ "📷",
491
+ "⏰",
492
+ "⏳",
493
+ "📡",
494
+ "💡",
495
+ "💰",
496
+ "🧲",
497
+ "🧸",
498
+ "🎁",
499
+ "🎀",
500
+ "🎉",
501
+ "🪭",
502
+ "👑",
503
+ "🫖",
504
+ "🔭",
505
+ "🛁",
506
+ "🏆",
507
+ "🥁",
508
+ "🎷",
509
+ "🎺",
510
+ "🏀",
511
+ "🏈",
512
+ "🎾",
513
+ "🏓",
514
+ "✨",
515
+ "🔥",
516
+ "💥",
517
+ "👕",
518
+ "👚",
519
+ "👖",
520
+ "🩳",
521
+ "👗",
522
+ "👔",
523
+ "🧢",
524
+ "👓",
525
+ "🧶",
526
+ "🧵",
527
+ "💍",
528
+ "👠",
529
+ "👟",
530
+ "🧦",
531
+ "🧤",
532
+ "👒",
533
+ "👜",
534
+ "🐱",
535
+ "🐶",
536
+ "🐭",
537
+ "🐹",
538
+ "🐰",
539
+ "🦊",
540
+ "🐻",
541
+ "🐼",
542
+ "🐨",
543
+ "🐯",
544
+ "🦁",
545
+ "🐮",
546
+ "🐷",
547
+ "🐸",
548
+ "🐵",
549
+ "🐔",
550
+ "🐥",
551
+ "🦆",
552
+ "🦉",
553
+ "🐴",
554
+ "🦄",
555
+ "🐝",
556
+ "🐛",
557
+ "🦋",
558
+ "🐌",
559
+ "🐞",
560
+ "🐢",
561
+ "🐺",
562
+ "🐍",
563
+ "🪽",
564
+ "🐙",
565
+ "🦑",
566
+ "🪼",
567
+ "🦞",
568
+ "🦀",
569
+ "🐚",
570
+ "🦭",
571
+ "🐟",
572
+ "🐬",
573
+ "🐳",
574
+ ];
575
+
576
+ /**
577
+ * Encodes a 4-byte slice as a string of bytewords for identification.
578
+ */
579
+ export function encodeBytewordsIdentifier(data: Uint8Array): string {
580
+ if (data.length !== 4) {
581
+ throw new Error("Identifier data must be exactly 4 bytes");
582
+ }
583
+ const words: string[] = [];
584
+ for (let i = 0; i < 4; i++) {
585
+ const byte = data[i];
586
+ if (byte === undefined) throw new Error("Invalid byte");
587
+ const word = BYTEWORDS[byte];
588
+ if (word === "" || word === undefined) throw new Error("Invalid byteword mapping");
589
+ words.push(word);
590
+ }
591
+ return words.join(" ");
592
+ }
593
+
594
+ /**
595
+ * Encodes a 4-byte slice as a string of bytemojis for identification.
596
+ */
597
+ export function encodeBytemojisIdentifier(data: Uint8Array): string {
598
+ if (data.length !== 4) {
599
+ throw new Error("Identifier data must be exactly 4 bytes");
600
+ }
601
+ const emojis: string[] = [];
602
+ for (let i = 0; i < 4; i++) {
603
+ const byte = data[i];
604
+ if (byte === undefined) throw new Error("Invalid byte");
605
+ const emoji = BYTEMOJIS[byte];
606
+ if (emoji === "" || emoji === undefined) throw new Error("Invalid bytemoji mapping");
607
+ emojis.push(emoji);
608
+ }
609
+ return emojis.join(" ");
610
+ }
611
+
612
+ /**
613
+ * Bytewords encoding style.
614
+ */
615
+ export enum BytewordsStyle {
616
+ /** Full 4-letter words separated by spaces */
617
+ Standard = "standard",
618
+ /** Full 4-letter words without separators */
619
+ Uri = "uri",
620
+ /** First and last character only (minimal) - used by UR encoding */
621
+ Minimal = "minimal",
622
+ }
623
+
624
+ /**
625
+ * Create a reverse mapping for minimal bytewords (first+last char) lookup.
626
+ */
627
+ function createMinimalBytewordsMap(): Map<string, number> {
628
+ const map = new Map<string, number>();
629
+ BYTEWORDS.forEach((word, index) => {
630
+ // Minimal encoding uses first and last character
631
+ const minimal = word[0] + word[3];
632
+ map.set(minimal, index);
633
+ });
634
+ return map;
635
+ }
636
+
637
+ export const MINIMAL_BYTEWORDS_MAP = createMinimalBytewordsMap();
638
+
639
+ /**
640
+ * CRC32 lookup table (IEEE polynomial).
641
+ */
642
+ const CRC32_TABLE: number[] = (() => {
643
+ const table: number[] = [];
644
+ for (let i = 0; i < 256; i++) {
645
+ let c = i;
646
+ for (let j = 0; j < 8; j++) {
647
+ c = (c & 1) !== 0 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
648
+ }
649
+ table.push(c >>> 0);
650
+ }
651
+ return table;
652
+ })();
653
+
654
+ /**
655
+ * Calculate CRC32 checksum of data.
656
+ */
657
+ export function crc32(data: Uint8Array): number {
658
+ let crc = 0xffffffff;
659
+ for (const byte of data) {
660
+ crc = (CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8)) >>> 0;
661
+ }
662
+ return (crc ^ 0xffffffff) >>> 0;
663
+ }
664
+
665
+ /**
666
+ * Convert a 32-bit number to 4 bytes (big-endian).
667
+ */
668
+ function uint32ToBytes(value: number): Uint8Array {
669
+ return new Uint8Array([
670
+ (value >>> 24) & 0xff,
671
+ (value >>> 16) & 0xff,
672
+ (value >>> 8) & 0xff,
673
+ value & 0xff,
674
+ ]);
675
+ }
676
+
677
+ /**
678
+ * Encode data as bytewords with the specified style.
679
+ * Includes CRC32 checksum.
680
+ */
681
+ export function encodeBytewords(
682
+ data: Uint8Array,
683
+ style: BytewordsStyle = BytewordsStyle.Minimal,
684
+ ): string {
685
+ // Append CRC32 checksum
686
+ const checksum = crc32(data);
687
+ const checksumBytes = uint32ToBytes(checksum);
688
+ const dataWithChecksum = new Uint8Array(data.length + 4);
689
+ dataWithChecksum.set(data);
690
+ dataWithChecksum.set(checksumBytes, data.length);
691
+
692
+ const words: string[] = [];
693
+ for (const byte of dataWithChecksum) {
694
+ const word = BYTEWORDS[byte];
695
+ if (word === undefined) throw new Error(`Invalid byte value: ${byte}`);
696
+
697
+ switch (style) {
698
+ case BytewordsStyle.Standard:
699
+ words.push(word);
700
+ break;
701
+ case BytewordsStyle.Uri:
702
+ words.push(word);
703
+ break;
704
+ case BytewordsStyle.Minimal:
705
+ // First and last character
706
+ words.push(word[0] + word[3]);
707
+ break;
708
+ }
709
+ }
710
+
711
+ switch (style) {
712
+ case BytewordsStyle.Standard:
713
+ return words.join(" ");
714
+ case BytewordsStyle.Uri:
715
+ case BytewordsStyle.Minimal:
716
+ return words.join("");
717
+ }
718
+ }
719
+
720
+ /**
721
+ * Decode bytewords string back to data.
722
+ * Validates and removes CRC32 checksum.
723
+ */
724
+ export function decodeBytewords(
725
+ encoded: string,
726
+ style: BytewordsStyle = BytewordsStyle.Minimal,
727
+ ): Uint8Array {
728
+ const lowercased = encoded.toLowerCase();
729
+ let bytes: number[];
730
+
731
+ switch (style) {
732
+ case BytewordsStyle.Standard: {
733
+ const words = lowercased.split(" ");
734
+ bytes = words.map((word) => {
735
+ const index = BYTEWORDS_MAP.get(word);
736
+ if (index === undefined) {
737
+ throw new Error(`Invalid byteword: ${word}`);
738
+ }
739
+ return index;
740
+ });
741
+ break;
742
+ }
743
+ 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);
751
+ const index = BYTEWORDS_MAP.get(word);
752
+ if (index === undefined) {
753
+ throw new Error(`Invalid byteword: ${word}`);
754
+ }
755
+ bytes.push(index);
756
+ }
757
+ break;
758
+ }
759
+ case BytewordsStyle.Minimal: {
760
+ // 2-character minimal words with no separator
761
+ if (lowercased.length % 2 !== 0) {
762
+ throw new Error("Invalid minimal bytewords length");
763
+ }
764
+ bytes = [];
765
+ for (let i = 0; i < lowercased.length; i += 2) {
766
+ const minimal = lowercased.slice(i, i + 2);
767
+ const index = MINIMAL_BYTEWORDS_MAP.get(minimal);
768
+ if (index === undefined) {
769
+ throw new Error(`Invalid minimal byteword: ${minimal}`);
770
+ }
771
+ bytes.push(index);
772
+ }
773
+ break;
774
+ }
775
+ }
776
+
777
+ if (bytes.length < 4) {
778
+ throw new Error("Bytewords data too short (missing checksum)");
779
+ }
780
+
781
+ // Extract data and checksum
782
+ const dataWithChecksum = new Uint8Array(bytes);
783
+ const data = dataWithChecksum.slice(0, -4);
784
+ const checksumBytes = dataWithChecksum.slice(-4);
785
+
786
+ // Verify checksum
787
+ const expectedChecksum = crc32(data);
788
+ const actualChecksum =
789
+ ((checksumBytes[0] << 24) |
790
+ (checksumBytes[1] << 16) |
791
+ (checksumBytes[2] << 8) |
792
+ checksumBytes[3]) >>>
793
+ 0;
794
+
795
+ if (expectedChecksum !== actualChecksum) {
796
+ throw new Error(
797
+ `Bytewords checksum mismatch: expected ${expectedChecksum.toString(16)}, got ${actualChecksum.toString(16)}`,
798
+ );
799
+ }
800
+
801
+ return data;
802
+ }