@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/ur.ts ADDED
@@ -0,0 +1,215 @@
1
+ import type { Cbor } from "@bcts/dcbor";
2
+ import { decodeCbor } from "@bcts/dcbor";
3
+ import { InvalidSchemeError, TypeUnspecifiedError, UnexpectedTypeError, URError } from "./error.js";
4
+ import { URType } from "./ur-type.js";
5
+ import { encodeBytewords, decodeBytewords, BytewordsStyle } from "./utils.js";
6
+
7
+ /**
8
+ * A Uniform Resource (UR) is a URI-encoded CBOR object.
9
+ *
10
+ * URs are defined in [BCR-2020-005: Uniform Resources](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md).
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { UR } from '@bcts/uniform-resources';
15
+ * import { CBOR } from '@bcts/dcbor';
16
+ *
17
+ * // Create a UR from a CBOR object
18
+ * const cbor = CBOR.fromArray([1, 2, 3]);
19
+ * const ur = UR.new('test', cbor);
20
+ *
21
+ * // Encode to string
22
+ * const urString = ur.string();
23
+ * console.log(urString); // "ur:test/..."
24
+ *
25
+ * // Decode from string
26
+ * const decodedUR = UR.fromURString(urString);
27
+ * console.log(decodedUR.urTypeStr()); // "test"
28
+ * ```
29
+ */
30
+ export class UR {
31
+ private readonly _urType: URType;
32
+ private readonly _cbor: Cbor;
33
+
34
+ /**
35
+ * Creates a new UR from the provided type and CBOR data.
36
+ *
37
+ * @param urType - The UR type (will be validated)
38
+ * @param cbor - The CBOR data to encode
39
+ * @throws {InvalidTypeError} If the type is invalid
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const ur = UR.new('bytes', CBOR.fromString('hello'));
44
+ * ```
45
+ */
46
+ static new(urType: string | URType, cbor: Cbor): UR {
47
+ const type = typeof urType === "string" ? new URType(urType) : urType;
48
+ return new UR(type, cbor);
49
+ }
50
+
51
+ /**
52
+ * Creates a new UR from a UR string.
53
+ *
54
+ * @param urString - A UR string like "ur:test/..."
55
+ * @throws {InvalidSchemeError} If the string doesn't start with "ur:"
56
+ * @throws {TypeUnspecifiedError} If no type is specified
57
+ * @throws {NotSinglePartError} If the UR is multi-part
58
+ * @throws {URError} If decoding fails
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * const ur = UR.fromURString('ur:test/lsadaoaxjygonesw');
63
+ * ```
64
+ */
65
+ static fromURString(urString: string): UR {
66
+ // Decode the UR string to get the type and CBOR data
67
+ const decodedUR = URStringDecoder.decode(urString);
68
+ if (decodedUR === null || decodedUR === undefined) {
69
+ throw new URError("Failed to decode UR string");
70
+ }
71
+ const { urType, cbor } = decodedUR;
72
+ return new UR(new URType(urType), cbor);
73
+ }
74
+
75
+ private constructor(urType: URType, cbor: Cbor) {
76
+ this._urType = urType;
77
+ this._cbor = cbor;
78
+ }
79
+
80
+ /**
81
+ * Returns the UR type.
82
+ */
83
+ urType(): URType {
84
+ return this._urType;
85
+ }
86
+
87
+ /**
88
+ * Returns the UR type as a string.
89
+ */
90
+ urTypeStr(): string {
91
+ return this._urType.string();
92
+ }
93
+
94
+ /**
95
+ * Returns the CBOR data.
96
+ */
97
+ cbor(): Cbor {
98
+ return this._cbor;
99
+ }
100
+
101
+ /**
102
+ * Returns the string representation of the UR (lowercase, suitable for display).
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const ur = UR.new('test', CBOR.fromArray([1, 2, 3]));
107
+ * console.log(ur.string()); // "ur:test/lsadaoaxjygonesw"
108
+ * ```
109
+ */
110
+ string(): string {
111
+ const cborData = this._cbor.toData();
112
+ return URStringEncoder.encode(this._urType.string(), cborData);
113
+ }
114
+
115
+ /**
116
+ * Returns the QR string representation (uppercase, most efficient for QR codes).
117
+ */
118
+ qrString(): string {
119
+ return this.string().toUpperCase();
120
+ }
121
+
122
+ /**
123
+ * Returns the QR data as bytes (uppercase UR string as UTF-8).
124
+ */
125
+ qrData(): Uint8Array {
126
+ // Use a helper to convert string to bytes across platforms
127
+ const str = this.qrString();
128
+ const bytes = new Uint8Array(str.length);
129
+ for (let i = 0; i < str.length; i++) {
130
+ bytes[i] = str.charCodeAt(i);
131
+ }
132
+ return bytes;
133
+ }
134
+
135
+ /**
136
+ * Checks if the UR type matches the expected type.
137
+ *
138
+ * @param expectedType - The expected type
139
+ * @throws {UnexpectedTypeError} If the types don't match
140
+ */
141
+ checkType(expectedType: string | URType): void {
142
+ const expected = typeof expectedType === "string" ? new URType(expectedType) : expectedType;
143
+ if (!this._urType.equals(expected)) {
144
+ throw new UnexpectedTypeError(expected.string(), this._urType.string());
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Returns the string representation.
150
+ */
151
+ toString(): string {
152
+ return this.string();
153
+ }
154
+
155
+ /**
156
+ * Checks equality with another UR.
157
+ */
158
+ equals(other: UR): boolean {
159
+ return (
160
+ this._urType.equals(other._urType) &&
161
+ this._cbor.toData().toString() === other._cbor.toData().toString()
162
+ );
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Encodes a UR string using Bytewords minimal encoding.
168
+ * This handles single-part URs according to BCR-2020-005.
169
+ */
170
+ class URStringEncoder {
171
+ static encode(urType: string, cborData: Uint8Array): string {
172
+ // Encode CBOR data using bytewords minimal style (with CRC32 checksum)
173
+ const encoded = encodeBytewords(cborData, BytewordsStyle.Minimal);
174
+ return `ur:${urType}/${encoded}`;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Decodes a UR string back to its components.
180
+ */
181
+ class URStringDecoder {
182
+ static decode(urString: string): { urType: string; cbor: Cbor } | null {
183
+ const lowercased = urString.toLowerCase();
184
+
185
+ // Check scheme
186
+ if (!lowercased.startsWith("ur:")) {
187
+ throw new InvalidSchemeError();
188
+ }
189
+
190
+ // Strip scheme
191
+ const afterScheme = lowercased.substring(3);
192
+
193
+ // Split into type and data
194
+ const [urType, ...dataParts] = afterScheme.split("/");
195
+
196
+ if (urType === "" || urType === undefined) {
197
+ throw new TypeUnspecifiedError();
198
+ }
199
+
200
+ const data = dataParts.join("/");
201
+ if (data === "" || data === undefined) {
202
+ throw new TypeUnspecifiedError();
203
+ }
204
+
205
+ try {
206
+ // Decode the bytewords-encoded data (validates CRC32 checksum)
207
+ const cborData = decodeBytewords(data, BytewordsStyle.Minimal);
208
+ const cbor = decodeCbor(cborData);
209
+ return { urType, cbor };
210
+ } catch (error) {
211
+ const errorMessage = error instanceof Error ? error.message : String(error);
212
+ throw new URError(`Failed to decode UR: ${errorMessage}`);
213
+ }
214
+ }
215
+ }