@atproto/lex-json 0.1.1 → 0.1.3

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/lex-json.ts DELETED
@@ -1,352 +0,0 @@
1
- import {
2
- BlobRef,
3
- Cid,
4
- LexArray,
5
- LexMap,
6
- LexValue,
7
- isCid,
8
- utf8FromBytes,
9
- } from '@atproto/lex-data'
10
- import { parseTypedBlobRef } from './blob.js'
11
- import { encodeLexBytes, parseLexBytes } from './bytes.js'
12
- import { JsonObject, JsonValue } from './json.js'
13
- import { encodeLexLink, parseLexLink } from './link.js'
14
-
15
- /**
16
- * Serialize a Lex value to a JSON string.
17
- *
18
- * This function serializes AT Protocol data model values to JSON, automatically
19
- * encoding special types:
20
- * - `Cid` instances are encoded as `{$link: string}`
21
- * - `Uint8Array` instances are encoded as `{$bytes: string}` (base64)
22
- *
23
- * @param input - The Lex value to stringify
24
- * @returns A JSON string representation of the value
25
- *
26
- * @example
27
- * ```typescript
28
- * import { lexStringify } from '@atproto/lex'
29
- *
30
- * // Stringify with CID and bytes encoding
31
- * const json = lexStringify({
32
- * ref: someCid,
33
- * data: new Uint8Array([72, 101, 108, 108, 111])
34
- * })
35
- * // json is '{"ref":{"$link":"bafyrei..."},"data":{"$bytes":"SGVsbG8="}}'
36
- * ```
37
- */
38
- export function lexStringify(input: LexValue): string {
39
- // @NOTE Because of the way the "replacer" works in JSON.stringify, it's
40
- // simpler to convert Lex to JSON first rather than trying to do it
41
- // on-the-fly.
42
- return JSON.stringify(lexToJson(input))
43
- }
44
-
45
- /**
46
- * Options for parsing JSON to Lex values.
47
- */
48
- export type LexParseOptions = {
49
- /**
50
- * When enabled, forbids the presence of invalid Lex values such as:
51
- * - Non-integer numbers (only safe integers are valid in the Lex data model)
52
- * - Malformed `$link` objects
53
- * - Malformed `$bytes` objects
54
- * - Objects with invalid or empty `$type` properties
55
- * - Invalid {@link BlobRef} (`$type: 'blob'`) objects
56
- *
57
- * When disabled (default), invalid special objects are left as plain objects.
58
- *
59
- * @default false
60
- */
61
- strict?: boolean
62
- }
63
-
64
- /**
65
- * Parses a JSON string into Lex values.
66
- *
67
- * This function parses JSON and automatically decodes AT Protocol special types:
68
- * - `{$link: string}` objects are decoded to `Cid` instances
69
- * - `{$bytes: string}` objects are decoded to `Uint8Array` instances
70
- * - `{$type: 'blob'}` objects are validated
71
- *
72
- * @typeParam T - Type cast for the resulting Lex value. Use when you want to specify the expected structure of the parsed data.
73
- * @param input - The JSON string to parse
74
- * @param options - Parsing options (e.g., strict mode)
75
- * @returns The parsed Lex value
76
- * @throws {SyntaxError} If the input is not valid JSON
77
- * @throws {TypeError} If strict mode is enabled and invalid Lex values are found
78
- *
79
- * @example
80
- * ```typescript
81
- * import { lexParse } from '@atproto/lex'
82
- *
83
- * // Parse JSON with $link and $bytes decoding
84
- * const parsed = lexParse<{
85
- * ref: Cid
86
- * data: Uint8Array
87
- * }>(`{
88
- * "ref": { "$link": "bafyrei..." },
89
- * "data": { "$bytes": "SGVsbG8sIHdvcmxkIQ==" }
90
- * }`)
91
- *
92
- * // Parse a single CID
93
- * const someCid = lexParse<Cid>('{"$link": "bafyrei..."}')
94
- *
95
- * // Parse binary data
96
- * const someBytes = lexParse<Uint8Array>('{"$bytes": "SGVsbG8sIHdvcmxkIQ=="}')
97
- * ```
98
- */
99
- export function lexParse<T extends LexValue = LexValue>(
100
- input: string,
101
- options: LexParseOptions = { strict: false },
102
- ): T {
103
- // @NOTE see ./lex-json.bench.ts for performance comparison of implementation
104
- // that uses a reviver function in JSON.parse vs. the current implementation.
105
- return jsonToLex(JSON.parse(input), options) as T
106
- }
107
-
108
- /**
109
- * Parses a JSON string from a byte array into Lex values.
110
- */
111
- export function lexParseJsonBytes(
112
- bytes: Uint8Array,
113
- options?: LexParseOptions,
114
- ): LexValue {
115
- // @NOTE see ./json-bytes-decoder.bench.ts for performance comparison of
116
- // implementation that uses a decoder class that operates directly on bytes
117
- // vs. the current implementation that first decodes bytes to string and then
118
- // parses JSON. For more common cases, it seems that the trivial
119
- // implementation works better than the decoder based solution, while having a
120
- // small overhead for slower cases (~2% difference). Because of this, we keep
121
- // the trivial implementation:
122
- return lexParse(utf8FromBytes(bytes), options)
123
- }
124
-
125
- /**
126
- * Converts a parsed JSON representation of Lexicon value to a {@link LexValue}.
127
- *
128
- * This function transforms already-parsed JSON objects into Lex values by
129
- * decoding AT Protocol special types:
130
- * - `{$link: string}` objects are converted to `Cid` instances
131
- * - `{$bytes: string}` objects are converted to `Uint8Array` instances
132
- *
133
- * Use this when you have a JavaScript object (e.g., from `JSON.parse()`) and
134
- * need to convert it to the Lex data model. For parsing JSON strings directly,
135
- * use {@link lexParse} instead.
136
- *
137
- * @param value - The JSON value to convert
138
- * @param options - Parsing options (e.g., strict mode)
139
- * @returns The converted Lex value
140
- * @throws {TypeError} If strict mode is enabled and invalid Lex values are found
141
- * @throws {TypeError} If the value contains unsupported types (e.g., undefined at top level)
142
- *
143
- * @example
144
- * ```typescript
145
- * import { jsonToLex } from '@atproto/lex'
146
- *
147
- * // Convert parsed JSON to Lex values
148
- * const lex = jsonToLex({
149
- * ref: { $link: 'bafyrei...' }, // Converted to Cid
150
- * data: { $bytes: 'SGVsbG8sIHdvcmxkIQ==' } // Converted to Uint8Array
151
- * })
152
- * ```
153
- */
154
- export function jsonToLex(
155
- value: JsonValue,
156
- options: LexParseOptions = { strict: false },
157
- ): LexValue {
158
- switch (typeof value) {
159
- case 'object': {
160
- if (value === null) return null
161
- if (Array.isArray(value)) return jsonArrayToLex(value, options)
162
- return (
163
- parseSpecialJsonObject(value, options) ??
164
- jsonObjectToLexMap(value, options)
165
- )
166
- }
167
- case 'number':
168
- if (Number.isSafeInteger(value)) return value
169
- if (options.strict === false) return value
170
- throw new TypeError(`Invalid non-integer number: ${value}`)
171
- case 'boolean':
172
- case 'string':
173
- return value
174
- default:
175
- throw new TypeError(`Invalid JSON value: ${typeof value}`)
176
- }
177
- }
178
-
179
- function jsonArrayToLex(
180
- input: JsonValue[],
181
- options: LexParseOptions,
182
- ): LexValue[] {
183
- // Lazily copy value
184
- let copy: LexValue[] | undefined
185
- for (let i = 0; i < input.length; i++) {
186
- const inputItem = input[i]
187
- const item = jsonToLex(inputItem, options)
188
- if (item !== inputItem) {
189
- copy ??= Array.from(input)
190
- copy[i] = item
191
- }
192
- }
193
- return copy ?? input
194
- }
195
-
196
- function jsonObjectToLexMap(
197
- input: JsonObject,
198
- options: LexParseOptions,
199
- ): LexMap {
200
- // Lazily copy value
201
- let copy: LexMap | undefined = undefined
202
- for (const [key, jsonValue] of Object.entries(input)) {
203
- // Prevent prototype pollution
204
- if (key === '__proto__') {
205
- throw new TypeError('Invalid key: __proto__')
206
- }
207
-
208
- // Ignore (strip) undefined values
209
- if (jsonValue === undefined) {
210
- copy ??= { ...input }
211
- delete copy[key]
212
- continue
213
- }
214
-
215
- const value = jsonToLex(jsonValue!, options)
216
- if (value !== jsonValue) {
217
- copy ??= { ...input }
218
- copy[key] = value
219
- }
220
- }
221
- return copy ?? input
222
- }
223
-
224
- /**
225
- * Converts a Lex value to a JSON-compatible value.
226
- *
227
- * This function transforms Lex data model values into plain JavaScript objects
228
- * suitable for JSON serialization:
229
- * - `Cid` instances are converted to `{$link: string}` objects
230
- * - `Uint8Array` instances are converted to `{$bytes: string}` objects (base64)
231
- *
232
- * Use this when you need to convert Lex values to plain objects (e.g., for
233
- * custom serialization or inspection). For direct JSON string output, use
234
- * {@link lexStringify} instead.
235
- *
236
- * @param value - The Lex value to convert
237
- * @returns The JSON-compatible value
238
- * @throws {TypeError} If the value contains unsupported types
239
- *
240
- * @example
241
- * ```typescript
242
- * import { lexToJson } from '@atproto/lex'
243
- *
244
- * // Convert Lex values to JSON-compatible objects
245
- * const obj = lexToJson({
246
- * ref: someCid, // Converted to { $link: string }
247
- * data: someBytes // Converted to { $bytes: string }
248
- * })
249
- * ```
250
- */
251
- export function lexToJson(value: LexValue): JsonValue {
252
- switch (typeof value) {
253
- case 'object':
254
- if (value === null) {
255
- return value
256
- } else if (Array.isArray(value)) {
257
- return lexArrayToJson(value)
258
- } else if (isCid(value)) {
259
- return encodeLexLink(value)
260
- } else if (ArrayBuffer.isView(value)) {
261
- return encodeLexBytes(value)
262
- } else {
263
- return encodeLexMap(value)
264
- }
265
- case 'boolean':
266
- case 'string':
267
- case 'number':
268
- return value
269
- default:
270
- throw new TypeError(`Invalid Lex value: ${typeof value}`)
271
- }
272
- }
273
-
274
- function lexArrayToJson(input: LexArray): JsonValue[] {
275
- // Lazily copy value
276
- let copy: JsonValue[] | undefined
277
- for (let i = 0; i < input.length; i++) {
278
- const inputItem = input[i]
279
- const item = lexToJson(inputItem)
280
- if (item !== inputItem) {
281
- copy ??= Array.from(input) as JsonValue[]
282
- copy[i] = item
283
- }
284
- }
285
- return copy ?? (input as JsonValue[])
286
- }
287
-
288
- function encodeLexMap(input: LexMap): JsonObject {
289
- // Lazily copy value
290
- let copy: JsonObject | undefined = undefined
291
- for (const [key, lexValue] of Object.entries(input)) {
292
- // Prevent prototype pollution
293
- if (key === '__proto__') {
294
- throw new TypeError('Invalid key: __proto__')
295
- }
296
-
297
- // Ignore (strip) undefined values
298
- if (lexValue === undefined) {
299
- copy ??= { ...input } as JsonObject
300
- delete copy[key]
301
- continue
302
- }
303
-
304
- const jsonValue = lexToJson(lexValue!)
305
- if (jsonValue !== lexValue) {
306
- copy ??= { ...input } as JsonObject
307
- copy[key] = jsonValue
308
- }
309
- }
310
- return copy ?? (input as JsonObject)
311
- }
312
-
313
- /**
314
- * @internal
315
- */
316
- export function parseSpecialJsonObject(
317
- input: LexMap,
318
- options: LexParseOptions,
319
- ): Cid | Uint8Array | BlobRef | undefined {
320
- // Hot path: use hints to avoid parsing when possible
321
-
322
- if (input.$link !== undefined) {
323
- const cid = parseLexLink(input)
324
- if (cid) return cid
325
- if (options.strict) throw new TypeError(`Invalid $link object`)
326
- } else if (input.$bytes !== undefined) {
327
- const bytes = parseLexBytes(input)
328
- if (bytes) return bytes
329
- if (options.strict) throw new TypeError(`Invalid $bytes object`)
330
- } else if (input.$type !== undefined) {
331
- // @NOTE Since blobs are "just" regular lex objects with a special shape,
332
- // and because an object that does not conform to the blob shape would still
333
- // result in undefined being returned, we only attempt to parse blobs when
334
- // the strict option is enabled.
335
- if (options.strict) {
336
- if (input.$type === 'blob') {
337
- const blob = parseTypedBlobRef(input, options)
338
- if (blob) return blob
339
- throw new TypeError(`Invalid blob object`)
340
- } else if (typeof input.$type !== 'string') {
341
- throw new TypeError(`Invalid $type property (${typeof input.$type})`)
342
- } else if (input.$type.length === 0) {
343
- throw new TypeError(`Empty $type property`)
344
- }
345
- }
346
- }
347
-
348
- // @NOTE We ignore legacy blob representation here. They can be handled at the
349
- // application level if needed.
350
-
351
- return undefined
352
- }
package/src/link.ts DELETED
@@ -1,101 +0,0 @@
1
- import {
2
- CheckCidOptions,
3
- Cid,
4
- InferCheckedCid,
5
- parseCid,
6
- } from '@atproto/lex-data'
7
- import { JsonValue } from './json.js'
8
-
9
- /**
10
- * Parses a `{$link: string}` JSON object into a {@link Cid} instance.
11
- *
12
- * In the AT Protocol data model, CID references are represented in JSON as an
13
- * object with a single `$link` property containing a base32-encoded CID string,
14
- * prefixed with "b". This function decodes that representation into a `Cid`
15
- * object.
16
- *
17
- * @param input - An object potentially containing a `{$link: string}` property
18
- * @param options - Optional CID validation options
19
- * @returns The parsed {@link Cid} if the input is a valid `$link` object,
20
- * or `undefined` if the input is not a valid `$link` representation
21
- * @throws {TypeError} If `$link` is present but is not a valid CID string
22
- *
23
- * @example
24
- * ```typescript
25
- * // Parse a $link object to Cid
26
- * const cid = parseLexLink({ $link: 'bafyreib2rxk3rybloqtqwbo' })
27
- * // cid is a Cid instance
28
- *
29
- * // Returns undefined for non-$link objects
30
- * const result = parseLexLink({ foo: 'bar' })
31
- * // result is undefined
32
- *
33
- * // Returns undefined for objects with extra properties
34
- * const invalid = parseLexLink({ $link: 'bafyrei...', extra: true })
35
- * // invalid is undefined
36
- * ```
37
- */
38
- export function parseLexLink<TOptions extends CheckCidOptions>(
39
- input: undefined | Record<string, unknown>,
40
- options: TOptions,
41
- ): InferCheckedCid<TOptions> | undefined
42
- export function parseLexLink(
43
- input?: Record<string, unknown>,
44
- options?: CheckCidOptions,
45
- ): Cid | undefined
46
- export function parseLexLink(
47
- input?: Record<string, unknown>,
48
- options?: CheckCidOptions,
49
- ): Cid | undefined {
50
- if (!input || !('$link' in input)) {
51
- return undefined
52
- }
53
-
54
- for (const key in input) {
55
- if (key !== '$link') {
56
- return undefined
57
- }
58
- }
59
-
60
- const { $link } = input
61
-
62
- if (typeof $link !== 'string') {
63
- return undefined
64
- }
65
-
66
- if ($link.length === 0) {
67
- return undefined
68
- }
69
-
70
- // Arbitrary limit to prevent DoS via extremely long CIDs
71
- if ($link.length > 2048) {
72
- return undefined
73
- }
74
-
75
- try {
76
- return parseCid($link, options)
77
- } catch {
78
- return undefined
79
- }
80
- }
81
-
82
- /**
83
- * Encodes a {@link Cid} instance into a `{$link: string}` JSON representation.
84
- *
85
- * In the AT Protocol data model, CID references are represented in JSON as an
86
- * object with a single `$link` property containing a base32-encoded CID string,
87
- * prefixed with "b". This function performs that encoding.
88
- *
89
- * @param cid - The CID to encode
90
- * @returns An object with a `$link` property containing the string representation of the CID
91
- *
92
- * @example
93
- * ```typescript
94
- * const cid = CID.parse('bafyreib2rxk3rybloqtqwbo')
95
- * const encoded = encodeLexLink(cid)
96
- * // encoded is { $link: 'bafyreib2rxk3rybloqtqwbo' }
97
- * ```
98
- */
99
- export function encodeLexLink(cid: Cid): JsonValue {
100
- return { $link: cid.toString() }
101
- }
@@ -1,11 +0,0 @@
1
- {
2
- "extends": ["../../../tsconfig/isomorphic.json"],
3
- "include": ["./src"],
4
- "exclude": ["**/*.bench.ts", "**/*.test.ts"],
5
- "compilerOptions": {
6
- "noImplicitAny": true,
7
- "importHelpers": true,
8
- "rootDir": "./src",
9
- "outDir": "./dist",
10
- },
11
- }
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "include": [],
3
- "references": [
4
- { "path": "./tsconfig.build.json" },
5
- { "path": "./tsconfig.tests.json" },
6
- ],
7
- }
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig/vitest.json",
3
- "include": ["./tests", "./src/**/*.bench.ts", "./src/**/*.test.ts"],
4
- "compilerOptions": {
5
- "noImplicitAny": true,
6
- "rootDir": "./",
7
- },
8
- }