@hraness/message-like-me 0.8.0 → 0.8.2

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.
@@ -0,0 +1,372 @@
1
+ const MAX_SAFE_INTEGER = 9_007_199_254_740_991n;
2
+
3
+ export type JsonValue =
4
+ | null
5
+ | boolean
6
+ | number
7
+ | string
8
+ | JsonValue[]
9
+ | JsonObject;
10
+
11
+ export type JsonObject = { [key: string]: JsonValue };
12
+
13
+ export class ArchiveError extends Error {
14
+ override readonly name = "ArchiveError";
15
+ }
16
+
17
+ export class PacketValidationError extends Error {
18
+ override readonly name = "PacketValidationError";
19
+ }
20
+
21
+ export function failPacket(path: string, message: string): never {
22
+ throw new PacketValidationError(`${path}: ${message}`);
23
+ }
24
+
25
+ function containsUnpairedSurrogate(value: string): boolean {
26
+ for (let index = 0; index < value.length; index += 1) {
27
+ const code = value.charCodeAt(index);
28
+ if (code >= 0xd800 && code <= 0xdbff) {
29
+ const next = value.charCodeAt(index + 1);
30
+ if (!Number.isInteger(next) || next < 0xdc00 || next > 0xdfff) return true;
31
+ index += 1;
32
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
33
+ return true;
34
+ }
35
+ }
36
+ return false;
37
+ }
38
+
39
+ function unsupportedType(value: unknown): string {
40
+ if (value === undefined) return "undefined";
41
+ if (typeof value === "symbol") return "symbol";
42
+ if (typeof value === "bigint") return "bigint";
43
+ if (typeof value === "function") return "function";
44
+ return typeof value;
45
+ }
46
+
47
+ function encodeCanonical(value: unknown): string {
48
+ if (value === null) return "null";
49
+ if (value === true) return "true";
50
+ if (value === false) return "false";
51
+ if (typeof value === "number") {
52
+ if (!Number.isInteger(value)) {
53
+ throw new ArchiveError("floating-point values are not supported in source packets");
54
+ }
55
+ if (!Number.isSafeInteger(value)) {
56
+ throw new ArchiveError("integer exceeds the interoperable JSON range");
57
+ }
58
+ return Object.is(value, -0) ? "0" : String(value);
59
+ }
60
+ if (typeof value === "string") {
61
+ if (containsUnpairedSurrogate(value)) {
62
+ throw new ArchiveError("unpaired Unicode surrogate in source packet");
63
+ }
64
+ return JSON.stringify(value);
65
+ }
66
+ if (Array.isArray(value)) {
67
+ const encoded: string[] = [];
68
+ for (let index = 0; index < value.length; index += 1) {
69
+ encoded.push(encodeCanonical(value[index]));
70
+ }
71
+ return `[${encoded.join(",")}]`;
72
+ }
73
+ if (typeof value === "object") {
74
+ const prototype = Object.getPrototypeOf(value);
75
+ if (prototype !== Object.prototype && prototype !== null) {
76
+ throw new ArchiveError(`unsupported source packet value: ${unsupportedType(value)}`);
77
+ }
78
+ const symbols = Object.getOwnPropertySymbols(value);
79
+ if (symbols.length !== 0) {
80
+ throw new ArchiveError("source packet object keys must be strings");
81
+ }
82
+ const record = value as Record<string, unknown>;
83
+ const keys = Object.keys(record).sort();
84
+ return `{${keys
85
+ .map((key) => `${encodeCanonical(key)}:${encodeCanonical(record[key])}`)
86
+ .join(",")}}`;
87
+ }
88
+ throw new ArchiveError(`unsupported source packet value: ${unsupportedType(value)}`);
89
+ }
90
+
91
+ /** Encode Ensoul's integer-only JSON subset using RFC 8785 key ordering. */
92
+ export function canonicalText(value: unknown): string {
93
+ return encodeCanonical(value);
94
+ }
95
+
96
+ /** Encode Ensoul's integer-only JSON subset using RFC 8785 JCS. */
97
+ export function canonicalBytes(value: unknown): Uint8Array {
98
+ return new TextEncoder().encode(canonicalText(value));
99
+ }
100
+
101
+ export function sha256Hex(bytes: Uint8Array): string {
102
+ return new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
103
+ }
104
+
105
+ export function rejectNonIJson(value: unknown, path = "packet"): asserts value is JsonValue {
106
+ if (value === null || typeof value === "boolean") return;
107
+ if (typeof value === "number") {
108
+ if (!Number.isInteger(value)) {
109
+ failPacket(path, "floating-point values are not allowed by this packet schema");
110
+ }
111
+ if (!Number.isSafeInteger(value)) {
112
+ failPacket(path, "integer exceeds the interoperable JSON range");
113
+ }
114
+ return;
115
+ }
116
+ if (typeof value === "string") {
117
+ if (containsUnpairedSurrogate(value)) {
118
+ failPacket(path, "contains an unpaired Unicode surrogate");
119
+ }
120
+ return;
121
+ }
122
+ if (Array.isArray(value)) {
123
+ value.forEach((member, index) => rejectNonIJson(member, `${path}[${index}]`));
124
+ return;
125
+ }
126
+ if (typeof value === "object") {
127
+ const prototype = Object.getPrototypeOf(value);
128
+ if (prototype !== Object.prototype && prototype !== null) {
129
+ failPacket(path, "contains a non-JSON value");
130
+ }
131
+ if (Object.getOwnPropertySymbols(value).length !== 0) {
132
+ failPacket(`${path}.key`, "contains a non-JSON value");
133
+ }
134
+ for (const [key, member] of Object.entries(value)) {
135
+ rejectNonIJson(key, `${path}.key`);
136
+ rejectNonIJson(member, `${path}.${key}`);
137
+ }
138
+ return;
139
+ }
140
+ failPacket(path, "contains a non-JSON value");
141
+ }
142
+
143
+ class JsonSyntaxError extends Error {}
144
+
145
+ class StrictJsonParser {
146
+ private index = 0;
147
+
148
+ constructor(private readonly text: string) {}
149
+
150
+ parse(): JsonValue {
151
+ this.skipWhitespace();
152
+ const value = this.parseValue();
153
+ this.skipWhitespace();
154
+ if (this.index !== this.text.length) throw new JsonSyntaxError();
155
+ return value;
156
+ }
157
+
158
+ private parseValue(): JsonValue {
159
+ const character = this.text[this.index];
160
+ if (character === '"') return this.parseString();
161
+ if (character === "{") return this.parseObject();
162
+ if (character === "[") return this.parseArray();
163
+ if (this.text.startsWith("true", this.index)) {
164
+ this.index += 4;
165
+ return true;
166
+ }
167
+ if (this.text.startsWith("false", this.index)) {
168
+ this.index += 5;
169
+ return false;
170
+ }
171
+ if (this.text.startsWith("null", this.index)) {
172
+ this.index += 4;
173
+ return null;
174
+ }
175
+ if (
176
+ this.text.startsWith("NaN", this.index) ||
177
+ this.text.startsWith("Infinity", this.index) ||
178
+ this.text.startsWith("-Infinity", this.index)
179
+ ) {
180
+ failPacket("packet", "contains a non-finite number");
181
+ }
182
+ if (character === "-" || (character !== undefined && character >= "0" && character <= "9")) {
183
+ return this.parseNumber();
184
+ }
185
+ throw new JsonSyntaxError();
186
+ }
187
+
188
+ private parseString(): string {
189
+ this.index += 1;
190
+ let value = "";
191
+ while (this.index < this.text.length) {
192
+ const code = this.text.charCodeAt(this.index);
193
+ const character = this.text[this.index]!;
194
+ if (character === '"') {
195
+ this.index += 1;
196
+ return value;
197
+ }
198
+ if (code < 0x20) throw new JsonSyntaxError();
199
+ if (character !== "\\") {
200
+ value += character;
201
+ this.index += 1;
202
+ continue;
203
+ }
204
+ this.index += 1;
205
+ const escape = this.text[this.index];
206
+ if (escape === undefined) throw new JsonSyntaxError();
207
+ if (escape === "u") {
208
+ const digits = this.text.slice(this.index + 1, this.index + 5);
209
+ if (digits.length !== 4 || !/^[0-9a-fA-F]{4}$/u.test(digits)) {
210
+ throw new JsonSyntaxError();
211
+ }
212
+ value += String.fromCharCode(Number.parseInt(digits, 16));
213
+ this.index += 5;
214
+ } else if (escape === '"' || escape === "\\" || escape === "/") {
215
+ value += escape;
216
+ this.index += 1;
217
+ } else if (escape === "b") {
218
+ value += "\b";
219
+ this.index += 1;
220
+ } else if (escape === "f") {
221
+ value += "\f";
222
+ this.index += 1;
223
+ } else if (escape === "n") {
224
+ value += "\n";
225
+ this.index += 1;
226
+ } else if (escape === "r") {
227
+ value += "\r";
228
+ this.index += 1;
229
+ } else if (escape === "t") {
230
+ value += "\t";
231
+ this.index += 1;
232
+ } else {
233
+ throw new JsonSyntaxError();
234
+ }
235
+ }
236
+ throw new JsonSyntaxError();
237
+ }
238
+
239
+ private parseObject(): { [key: string]: JsonValue } {
240
+ this.index += 1;
241
+ this.skipWhitespace();
242
+ const value: { [key: string]: JsonValue } = Object.create(null) as {
243
+ [key: string]: JsonValue;
244
+ };
245
+ const keys = new Set<string>();
246
+ if (this.text[this.index] === "}") {
247
+ this.index += 1;
248
+ return value;
249
+ }
250
+ while (true) {
251
+ if (this.text[this.index] !== '"') throw new JsonSyntaxError();
252
+ const key = this.parseString();
253
+ if (keys.has(key)) failPacket("packet", "contains a duplicate object member");
254
+ keys.add(key);
255
+ this.skipWhitespace();
256
+ if (this.text[this.index] !== ":") throw new JsonSyntaxError();
257
+ this.index += 1;
258
+ this.skipWhitespace();
259
+ value[key] = this.parseValue();
260
+ this.skipWhitespace();
261
+ const delimiter = this.text[this.index];
262
+ if (delimiter === "}") {
263
+ this.index += 1;
264
+ return value;
265
+ }
266
+ if (delimiter !== ",") throw new JsonSyntaxError();
267
+ this.index += 1;
268
+ this.skipWhitespace();
269
+ }
270
+ }
271
+
272
+ private parseArray(): JsonValue[] {
273
+ this.index += 1;
274
+ this.skipWhitespace();
275
+ const value: JsonValue[] = [];
276
+ if (this.text[this.index] === "]") {
277
+ this.index += 1;
278
+ return value;
279
+ }
280
+ while (true) {
281
+ value.push(this.parseValue());
282
+ this.skipWhitespace();
283
+ const delimiter = this.text[this.index];
284
+ if (delimiter === "]") {
285
+ this.index += 1;
286
+ return value;
287
+ }
288
+ if (delimiter !== ",") throw new JsonSyntaxError();
289
+ this.index += 1;
290
+ this.skipWhitespace();
291
+ }
292
+ }
293
+
294
+ private parseNumber(): number {
295
+ const start = this.index;
296
+ if (this.text[this.index] === "-") this.index += 1;
297
+ if (this.text[this.index] === "0") {
298
+ this.index += 1;
299
+ const next = this.text[this.index];
300
+ if (next !== undefined && next >= "0" && next <= "9") throw new JsonSyntaxError();
301
+ } else {
302
+ const first = this.text[this.index];
303
+ if (first === undefined || first < "1" || first > "9") throw new JsonSyntaxError();
304
+ while (this.isDigit(this.text[this.index])) this.index += 1;
305
+ }
306
+
307
+ let floatingPoint = false;
308
+ if (this.text[this.index] === ".") {
309
+ floatingPoint = true;
310
+ this.index += 1;
311
+ if (!this.isDigit(this.text[this.index])) throw new JsonSyntaxError();
312
+ while (this.isDigit(this.text[this.index])) this.index += 1;
313
+ }
314
+ const exponent = this.text[this.index];
315
+ if (exponent === "e" || exponent === "E") {
316
+ floatingPoint = true;
317
+ this.index += 1;
318
+ const sign = this.text[this.index];
319
+ if (sign === "+" || sign === "-") this.index += 1;
320
+ if (!this.isDigit(this.text[this.index])) throw new JsonSyntaxError();
321
+ while (this.isDigit(this.text[this.index])) this.index += 1;
322
+ }
323
+ if (floatingPoint) failPacket("packet", "floating-point numbers are not allowed");
324
+
325
+ const parsed = BigInt(this.text.slice(start, this.index));
326
+ if (parsed > MAX_SAFE_INTEGER || parsed < -MAX_SAFE_INTEGER) {
327
+ failPacket("packet", "integer exceeds the interoperable JSON range");
328
+ }
329
+ return Number(parsed);
330
+ }
331
+
332
+ private isDigit(character: string | undefined): boolean {
333
+ return character !== undefined && character >= "0" && character <= "9";
334
+ }
335
+
336
+ private skipWhitespace(): void {
337
+ while (true) {
338
+ const character = this.text[this.index];
339
+ if (character !== " " && character !== "\t" && character !== "\n" && character !== "\r") return;
340
+ this.index += 1;
341
+ }
342
+ }
343
+ }
344
+
345
+ function decodeJsonInput(data: Uint8Array | string): string {
346
+ if (typeof data === "string") {
347
+ if (data.startsWith("\ufeff")) failPacket("packet", "UTF-8 BOM is not allowed");
348
+ return data;
349
+ }
350
+ if (data.length >= 3 && data[0] === 0xef && data[1] === 0xbb && data[2] === 0xbf) {
351
+ failPacket("packet", "UTF-8 BOM is not allowed");
352
+ }
353
+ try {
354
+ return new TextDecoder("utf-8", { fatal: true }).decode(data);
355
+ } catch {
356
+ failPacket("packet", "must be valid UTF-8");
357
+ }
358
+ }
359
+
360
+ /** Parse duplicate-free, integer-only interoperable JSON without external dependencies. */
361
+ export function strictJsonParse(data: Uint8Array | string): JsonValue {
362
+ const text = decodeJsonInput(data);
363
+ let value: JsonValue;
364
+ try {
365
+ value = new StrictJsonParser(text).parse();
366
+ } catch (error) {
367
+ if (error instanceof PacketValidationError) throw error;
368
+ failPacket("packet", "is not valid JSON");
369
+ }
370
+ rejectNonIJson(value);
371
+ return value;
372
+ }