@bcts/uniform-resources 1.0.0-alpha.10
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 +48 -0
- package/README.md +17 -0
- package/dist/index.cjs +8373 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +761 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +761 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.iife.js +8377 -0
- package/dist/index.iife.js.map +1 -0
- package/dist/index.mjs +8336 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +74 -0
- package/src/error.ts +88 -0
- package/src/fountain.ts +397 -0
- package/src/index.ts +61 -0
- package/src/multipart-decoder.ts +204 -0
- package/src/multipart-encoder.ts +166 -0
- package/src/ur-codable.ts +48 -0
- package/src/ur-decodable.ts +56 -0
- package/src/ur-encodable.ts +47 -0
- package/src/ur-type.ts +87 -0
- package/src/ur.ts +215 -0
- package/src/utils.ts +802 -0
- package/src/xoshiro.ts +180 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { decodeCbor } from "@bcts/dcbor";
|
|
2
|
+
import { InvalidSchemeError, InvalidTypeError, UnexpectedTypeError, URError } from "./error.js";
|
|
3
|
+
import { UR } from "./ur.js";
|
|
4
|
+
import { URType } from "./ur-type.js";
|
|
5
|
+
import { FountainDecoder, type FountainPart } from "./fountain.js";
|
|
6
|
+
import { decodeBytewords, BytewordsStyle } from "./utils.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Decodes multiple UR parts back into a single UR.
|
|
10
|
+
*
|
|
11
|
+
* This reassembles multipart URs that were encoded using fountain codes.
|
|
12
|
+
* The decoder can handle out-of-order reception and packet loss.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const decoder = new MultipartDecoder();
|
|
17
|
+
*
|
|
18
|
+
* for (const urPart of urParts) {
|
|
19
|
+
* decoder.receive(urPart);
|
|
20
|
+
* if (decoder.isComplete()) {
|
|
21
|
+
* const ur = decoder.message();
|
|
22
|
+
* break;
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class MultipartDecoder {
|
|
28
|
+
private _urType: URType | null = null;
|
|
29
|
+
private _fountainDecoder: FountainDecoder | null = null;
|
|
30
|
+
private _decodedMessage: UR | null = null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Receives a UR part string.
|
|
34
|
+
*
|
|
35
|
+
* @param part - A UR part string (e.g., "ur:bytes/1-10/..." or "ur:bytes/...")
|
|
36
|
+
* @throws {InvalidSchemeError} If the part doesn't start with "ur:"
|
|
37
|
+
* @throws {UnexpectedTypeError} If the type doesn't match previous parts
|
|
38
|
+
*/
|
|
39
|
+
receive(part: string): void {
|
|
40
|
+
const { urType, partInfo } = this._parsePart(part);
|
|
41
|
+
|
|
42
|
+
// Validate type consistency
|
|
43
|
+
if (this._urType === null) {
|
|
44
|
+
this._urType = urType;
|
|
45
|
+
} else if (!this._urType.equals(urType)) {
|
|
46
|
+
throw new UnexpectedTypeError(this._urType.string(), urType.string());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle the part
|
|
50
|
+
if (partInfo.isSinglePart) {
|
|
51
|
+
// Single-part UR - decode immediately
|
|
52
|
+
this._decodedMessage = UR.fromURString(part);
|
|
53
|
+
} else {
|
|
54
|
+
// Multipart UR - use fountain decoder
|
|
55
|
+
this._fountainDecoder ??= new FountainDecoder();
|
|
56
|
+
|
|
57
|
+
const fountainPart = this._decodeFountainPart(partInfo);
|
|
58
|
+
this._fountainDecoder.receive(fountainPart);
|
|
59
|
+
|
|
60
|
+
// Try to get the complete message
|
|
61
|
+
if (this._fountainDecoder.isComplete()) {
|
|
62
|
+
const message = this._fountainDecoder.message();
|
|
63
|
+
if (message !== null) {
|
|
64
|
+
const cbor = decodeCbor(message);
|
|
65
|
+
this._decodedMessage = UR.new(this._urType, cbor);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parses a UR part string to extract type and part info.
|
|
73
|
+
*/
|
|
74
|
+
private _parsePart(part: string): { urType: URType; partInfo: PartInfo } {
|
|
75
|
+
const lowercased = part.toLowerCase();
|
|
76
|
+
|
|
77
|
+
if (!lowercased.startsWith("ur:")) {
|
|
78
|
+
throw new InvalidSchemeError();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const afterScheme = lowercased.substring(3);
|
|
82
|
+
const components = afterScheme.split("/");
|
|
83
|
+
|
|
84
|
+
if (components.length === 0 || components[0] === "") {
|
|
85
|
+
throw new InvalidTypeError();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const urType = new URType(components[0]);
|
|
89
|
+
|
|
90
|
+
// Check if this is a multipart UR (format: type/seqNum-seqLen/data)
|
|
91
|
+
if (components.length >= 3) {
|
|
92
|
+
const seqPart = components[1];
|
|
93
|
+
const seqMatch = /^(\d+)-(\d+)$/.exec(seqPart);
|
|
94
|
+
|
|
95
|
+
if (seqMatch !== null) {
|
|
96
|
+
const seqNum = parseInt(seqMatch[1], 10);
|
|
97
|
+
const seqLen = parseInt(seqMatch[2], 10);
|
|
98
|
+
const encodedData = components.slice(2).join("/");
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
urType,
|
|
102
|
+
partInfo: {
|
|
103
|
+
isSinglePart: false,
|
|
104
|
+
seqNum,
|
|
105
|
+
seqLen,
|
|
106
|
+
encodedData,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Single-part UR
|
|
113
|
+
return {
|
|
114
|
+
urType,
|
|
115
|
+
partInfo: { isSinglePart: true },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Decodes a multipart UR's fountain part data.
|
|
121
|
+
*/
|
|
122
|
+
private _decodeFountainPart(partInfo: MultipartInfo): FountainPart {
|
|
123
|
+
// Decode bytewords
|
|
124
|
+
const rawData = decodeBytewords(partInfo.encodedData, BytewordsStyle.Minimal);
|
|
125
|
+
|
|
126
|
+
if (rawData.length < 8) {
|
|
127
|
+
throw new URError("Invalid multipart data: too short");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Extract metadata
|
|
131
|
+
const messageLen =
|
|
132
|
+
((rawData[0] << 24) | (rawData[1] << 16) | (rawData[2] << 8) | rawData[3]) >>> 0;
|
|
133
|
+
|
|
134
|
+
const checksum =
|
|
135
|
+
((rawData[4] << 24) | (rawData[5] << 16) | (rawData[6] << 8) | rawData[7]) >>> 0;
|
|
136
|
+
|
|
137
|
+
const data = rawData.slice(8);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
seqNum: partInfo.seqNum,
|
|
141
|
+
seqLen: partInfo.seqLen,
|
|
142
|
+
messageLen,
|
|
143
|
+
checksum,
|
|
144
|
+
data,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Checks if the message is complete.
|
|
150
|
+
*/
|
|
151
|
+
isComplete(): boolean {
|
|
152
|
+
return this._decodedMessage !== null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Gets the decoded UR message.
|
|
157
|
+
*
|
|
158
|
+
* @returns The decoded UR, or null if not yet complete
|
|
159
|
+
*/
|
|
160
|
+
message(): UR | null {
|
|
161
|
+
return this._decodedMessage;
|
|
162
|
+
}
|
|
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
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Part info for single-part URs.
|
|
189
|
+
*/
|
|
190
|
+
interface SinglePartInfo {
|
|
191
|
+
isSinglePart: true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Part info for multipart URs.
|
|
196
|
+
*/
|
|
197
|
+
interface MultipartInfo {
|
|
198
|
+
isSinglePart: false;
|
|
199
|
+
seqNum: number;
|
|
200
|
+
seqLen: number;
|
|
201
|
+
encodedData: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
type PartInfo = SinglePartInfo | MultipartInfo;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { UR } from "./ur.js";
|
|
2
|
+
import { URError } from "./error.js";
|
|
3
|
+
import { FountainEncoder, type FountainPart } from "./fountain.js";
|
|
4
|
+
import { encodeBytewords, BytewordsStyle } from "./utils.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Encodes a UR as multiple parts using fountain codes.
|
|
8
|
+
*
|
|
9
|
+
* This allows large CBOR structures to be split into multiple UR strings
|
|
10
|
+
* that can be transmitted separately and reassembled. The encoder uses
|
|
11
|
+
* fountain codes for resilient transmission over lossy channels.
|
|
12
|
+
*
|
|
13
|
+
* For single-part URs (small payloads), use the regular UR.string() method.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const ur = UR.new('bytes', cbor);
|
|
18
|
+
* const encoder = new MultipartEncoder(ur, 100);
|
|
19
|
+
*
|
|
20
|
+
* // Generate all pure parts
|
|
21
|
+
* while (!encoder.isComplete()) {
|
|
22
|
+
* const part = encoder.nextPart();
|
|
23
|
+
* console.log(part); // "ur:bytes/1-10/..."
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* // Generate additional rateless parts for redundancy
|
|
27
|
+
* for (let i = 0; i < 5; i++) {
|
|
28
|
+
* const part = encoder.nextPart();
|
|
29
|
+
* console.log(part); // "ur:bytes/11-10/..."
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class MultipartEncoder {
|
|
34
|
+
private readonly _ur: UR;
|
|
35
|
+
private readonly _fountainEncoder: FountainEncoder;
|
|
36
|
+
private _currentIndex = 0;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new multipart encoder for the given UR.
|
|
40
|
+
*
|
|
41
|
+
* @param ur - The UR to encode
|
|
42
|
+
* @param maxFragmentLen - Maximum length of each fragment in bytes
|
|
43
|
+
* @throws {URError} If encoding fails
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const encoder = new MultipartEncoder(ur, 100);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
constructor(ur: UR, maxFragmentLen: number) {
|
|
51
|
+
if (maxFragmentLen < 1) {
|
|
52
|
+
throw new URError("Max fragment length must be at least 1");
|
|
53
|
+
}
|
|
54
|
+
this._ur = ur;
|
|
55
|
+
|
|
56
|
+
// Create fountain encoder from CBOR data
|
|
57
|
+
const cborData = ur.cbor().toData();
|
|
58
|
+
this._fountainEncoder = new FountainEncoder(cborData, maxFragmentLen);
|
|
59
|
+
}
|
|
60
|
+
|
|
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
|
+
/**
|
|
71
|
+
* Gets the next part of the encoding.
|
|
72
|
+
*
|
|
73
|
+
* Parts 1 through seqLen are "pure" fragments containing one piece each.
|
|
74
|
+
* Parts beyond seqLen are "mixed" fragments using fountain codes for redundancy.
|
|
75
|
+
*
|
|
76
|
+
* @returns The next UR string part
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* const part = encoder.nextPart();
|
|
81
|
+
* // Returns: "ur:bytes/1-3/lsadaoaxjygonesw"
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
nextPart(): string {
|
|
85
|
+
const part = this._fountainEncoder.nextPart();
|
|
86
|
+
this._currentIndex++;
|
|
87
|
+
return this._encodePart(part);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Encodes a fountain part as a UR string.
|
|
92
|
+
*/
|
|
93
|
+
private _encodePart(part: FountainPart): string {
|
|
94
|
+
// For single-part messages, use simple format
|
|
95
|
+
if (part.seqLen === 1) {
|
|
96
|
+
return this._ur.string();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Encode the part data as CBOR: [seqNum, seqLen, messageLen, checksum, data]
|
|
100
|
+
// Using a simple format: seqNum-seqLen prefix followed by bytewords-encoded part
|
|
101
|
+
const partData = this._encodePartData(part);
|
|
102
|
+
const encoded = encodeBytewords(partData, BytewordsStyle.Minimal);
|
|
103
|
+
|
|
104
|
+
return `ur:${this._ur.urTypeStr()}/${part.seqNum}-${part.seqLen}/${encoded}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Encodes part metadata and data into bytes for bytewords encoding.
|
|
109
|
+
*/
|
|
110
|
+
private _encodePartData(part: FountainPart): Uint8Array {
|
|
111
|
+
// Simple encoding: messageLen (4 bytes) + checksum (4 bytes) + data
|
|
112
|
+
const result = new Uint8Array(8 + part.data.length);
|
|
113
|
+
|
|
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;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Gets the current part index.
|
|
134
|
+
*/
|
|
135
|
+
currentIndex(): number {
|
|
136
|
+
return this._currentIndex;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Gets the total number of pure parts.
|
|
141
|
+
*
|
|
142
|
+
* Note: Fountain codes can generate unlimited parts beyond this count
|
|
143
|
+
* for additional redundancy.
|
|
144
|
+
*/
|
|
145
|
+
partsCount(): number {
|
|
146
|
+
return this._fountainEncoder.seqLen;
|
|
147
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { UREncodable } from "./ur-encodable.js";
|
|
2
|
+
import type { URDecodable } from "./ur-decodable.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A type that can be both encoded to and decoded from a UR.
|
|
6
|
+
*
|
|
7
|
+
* This combines the UREncodable and URDecodable interfaces for types
|
|
8
|
+
* that support bidirectional UR conversion.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* class MyType implements URCodable {
|
|
13
|
+
* ur(): UR {
|
|
14
|
+
* // Encode to UR
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* urString(): string {
|
|
18
|
+
* // Return UR string
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* fromUR(ur: UR): MyType {
|
|
22
|
+
* // Decode from UR
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* fromURString(urString: string): MyType {
|
|
26
|
+
* // Decode from UR string (convenience method)
|
|
27
|
+
* return this.fromUR(UR.fromURString(urString));
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export interface URCodable extends UREncodable, URDecodable {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Helper function to check if an object implements URCodable.
|
|
36
|
+
*/
|
|
37
|
+
export function isURCodable(obj: unknown): obj is URCodable {
|
|
38
|
+
return (
|
|
39
|
+
typeof obj === "object" &&
|
|
40
|
+
obj !== null &&
|
|
41
|
+
"ur" in obj &&
|
|
42
|
+
"urString" in obj &&
|
|
43
|
+
"fromUR" in obj &&
|
|
44
|
+
typeof (obj as Record<string, unknown>)["ur"] === "function" &&
|
|
45
|
+
typeof (obj as Record<string, unknown>)["urString"] === "function" &&
|
|
46
|
+
typeof (obj as Record<string, unknown>)["fromUR"] === "function"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { UR } from "./ur.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A type that can be decoded from a UR (Uniform Resource).
|
|
5
|
+
*
|
|
6
|
+
* Types implementing this interface should be able to create themselves
|
|
7
|
+
* from a UR containing their data.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* class MyType implements URDecodable {
|
|
12
|
+
* fromUR(ur: UR): MyType {
|
|
13
|
+
* const cbor = ur.cbor();
|
|
14
|
+
* // Decode from CBOR and return MyType instance
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* fromURString(urString: string): MyType {
|
|
18
|
+
* return this.fromUR(UR.fromURString(urString));
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export interface URDecodable {
|
|
24
|
+
/**
|
|
25
|
+
* Creates an instance of this type from a UR.
|
|
26
|
+
*
|
|
27
|
+
* @param ur - The UR to decode from
|
|
28
|
+
* @returns An instance of this type
|
|
29
|
+
* @throws If the UR type is wrong or data is malformed
|
|
30
|
+
*/
|
|
31
|
+
fromUR(ur: UR): unknown;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates an instance of this type from a UR string.
|
|
35
|
+
*
|
|
36
|
+
* This is a convenience method that parses the UR string and then
|
|
37
|
+
* calls fromUR().
|
|
38
|
+
*
|
|
39
|
+
* @param urString - The UR string to decode from (e.g., "ur:type/...")
|
|
40
|
+
* @returns An instance of this type
|
|
41
|
+
* @throws If the UR string is invalid or data is malformed
|
|
42
|
+
*/
|
|
43
|
+
fromURString?(urString: string): unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Helper function to check if an object implements URDecodable.
|
|
48
|
+
*/
|
|
49
|
+
export function isURDecodable(obj: unknown): obj is URDecodable {
|
|
50
|
+
return (
|
|
51
|
+
typeof obj === "object" &&
|
|
52
|
+
obj !== null &&
|
|
53
|
+
"fromUR" in obj &&
|
|
54
|
+
typeof (obj as Record<string, unknown>)["fromUR"] === "function"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { UR } from "./ur.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A type that can be encoded to a UR (Uniform Resource).
|
|
5
|
+
*
|
|
6
|
+
* Types implementing this interface should be able to convert themselves
|
|
7
|
+
* to CBOR data and associate that with a UR type identifier.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* class MyType implements UREncodable {
|
|
12
|
+
* toCBOR(): CBOR {
|
|
13
|
+
* // Convert to CBOR
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* ur(): UR {
|
|
17
|
+
* const cbor = this.toCBOR();
|
|
18
|
+
* return UR.new('mytype', cbor);
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export interface UREncodable {
|
|
24
|
+
/**
|
|
25
|
+
* Returns the UR representation of the object.
|
|
26
|
+
*/
|
|
27
|
+
ur(): UR;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns the UR string representation of the object.
|
|
31
|
+
*/
|
|
32
|
+
urString(): string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Helper function to check if an object implements UREncodable.
|
|
37
|
+
*/
|
|
38
|
+
export function isUREncodable(obj: unknown): obj is UREncodable {
|
|
39
|
+
return (
|
|
40
|
+
typeof obj === "object" &&
|
|
41
|
+
obj !== null &&
|
|
42
|
+
"ur" in obj &&
|
|
43
|
+
"urString" in obj &&
|
|
44
|
+
typeof (obj as Record<string, unknown>)["ur"] === "function" &&
|
|
45
|
+
typeof (obj as Record<string, unknown>)["urString"] === "function"
|
|
46
|
+
);
|
|
47
|
+
}
|
package/src/ur-type.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { InvalidTypeError } from "./error";
|
|
2
|
+
import { isValidURType } from "./utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents a UR (Uniform Resource) type identifier.
|
|
6
|
+
*
|
|
7
|
+
* Valid UR types contain only lowercase letters, digits, and hyphens.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const urType = new URType('test');
|
|
12
|
+
* console.log(urType.string()); // "test"
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export class URType {
|
|
16
|
+
private readonly _type: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates a new URType from the provided type string.
|
|
20
|
+
*
|
|
21
|
+
* @param urType - The UR type as a string
|
|
22
|
+
* @throws {InvalidTypeError} If the type contains invalid characters
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const urType = new URType('test');
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
constructor(urType: string) {
|
|
30
|
+
if (!isValidURType(urType)) {
|
|
31
|
+
throw new InvalidTypeError();
|
|
32
|
+
}
|
|
33
|
+
this._type = urType;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns the string representation of the URType.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const urType = new URType('test');
|
|
42
|
+
* console.log(urType.string()); // "test"
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
string(): string {
|
|
46
|
+
return this._type;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Checks equality with another URType based on the type string.
|
|
51
|
+
*/
|
|
52
|
+
equals(other: URType): boolean {
|
|
53
|
+
return this._type === other._type;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns the string representation.
|
|
58
|
+
*/
|
|
59
|
+
toString(): string {
|
|
60
|
+
return this._type;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates a URType from a string, throwing an error if invalid.
|
|
65
|
+
*
|
|
66
|
+
* @param value - The UR type string
|
|
67
|
+
* @returns A new URType instance
|
|
68
|
+
* @throws {InvalidTypeError} If the type is invalid
|
|
69
|
+
*/
|
|
70
|
+
static from(value: string): URType {
|
|
71
|
+
return new URType(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Safely creates a URType, returning an error if invalid.
|
|
76
|
+
*
|
|
77
|
+
* @param value - The UR type string
|
|
78
|
+
* @returns Either a URType or an error
|
|
79
|
+
*/
|
|
80
|
+
static tryFrom(value: string): URType | InvalidTypeError {
|
|
81
|
+
try {
|
|
82
|
+
return new URType(value);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return error as InvalidTypeError;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|