@atproto/lex-json 0.0.0

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/dist/blob.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { BlobRef, LexMap } from '@atproto/lex-data';
2
+ export declare function parseBlobRef(input: LexMap, options?: {
3
+ strict?: boolean;
4
+ }): BlobRef | undefined;
5
+ //# sourceMappingURL=blob.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blob.d.ts","sourceRoot":"","sources":["../src/blob.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAa,MAAM,mBAAmB,CAAA;AAG9D,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7B,OAAO,GAAG,SAAS,CAwBrB"}
package/dist/blob.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseBlobRef = parseBlobRef;
4
+ const lex_data_1 = require("@atproto/lex-data");
5
+ const link_js_1 = require("./link.js");
6
+ function parseBlobRef(input, options) {
7
+ if (input.$type !== 'blob')
8
+ return undefined;
9
+ const ref = input?.ref;
10
+ if (!ref || typeof ref !== 'object')
11
+ return undefined;
12
+ // @NOTE Because json to lex conversion can be performed both in a depth-first
13
+ // manner (e.g. via lexParse) or in a breadth-first manner (e.g. via
14
+ // jsonToLex), the `ref` property may either be a LexMap with a $link
15
+ // property, or it may already be a CID instance.
16
+ if ('$link' in ref) {
17
+ const cid = (0, link_js_1.parseLexLink)(ref);
18
+ if (!cid)
19
+ return undefined;
20
+ const blob = { ...input, ref: cid };
21
+ if ((0, lex_data_1.isBlobRef)(blob, options))
22
+ return blob;
23
+ }
24
+ if ((0, lex_data_1.isBlobRef)(input)) {
25
+ return input;
26
+ }
27
+ return undefined;
28
+ }
29
+ //# sourceMappingURL=blob.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blob.js","sourceRoot":"","sources":["../src/blob.ts"],"names":[],"mappings":";;AAGA,oCA2BC;AA9BD,gDAA8D;AAC9D,uCAAwC;AAExC,SAAgB,YAAY,CAC1B,KAAa,EACb,OAA8B;IAE9B,IAAI,KAAK,CAAC,KAAK,KAAK,MAAM;QAAE,OAAO,SAAS,CAAA;IAE5C,MAAM,GAAG,GAAG,KAAK,EAAE,GAAG,CAAA;IACtB,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IAErD,8EAA8E;IAC9E,oEAAoE;IACpE,qEAAqE;IACrE,iDAAiD;IAEjD,IAAI,OAAO,IAAI,GAAG,EAAE,CAAC;QACnB,MAAM,GAAG,GAAG,IAAA,sBAAY,EAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAA;QAE1B,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,CAAA;QACnC,IAAI,IAAA,oBAAS,EAAC,IAAI,EAAE,OAAO,CAAC;YAAE,OAAO,IAAI,CAAA;IAC3C,CAAC;IAED,IAAI,IAAA,oBAAS,EAAC,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC","sourcesContent":["import { BlobRef, LexMap, isBlobRef } from '@atproto/lex-data'\nimport { parseLexLink } from './link.js'\n\nexport function parseBlobRef(\n input: LexMap,\n options?: { strict?: boolean },\n): BlobRef | undefined {\n if (input.$type !== 'blob') return undefined\n\n const ref = input?.ref\n if (!ref || typeof ref !== 'object') return undefined\n\n // @NOTE Because json to lex conversion can be performed both in a depth-first\n // manner (e.g. via lexParse) or in a breadth-first manner (e.g. via\n // jsonToLex), the `ref` property may either be a LexMap with a $link\n // property, or it may already be a CID instance.\n\n if ('$link' in ref) {\n const cid = parseLexLink(ref)\n if (!cid) return undefined\n\n const blob = { ...input, ref: cid }\n if (isBlobRef(blob, options)) return blob\n }\n\n if (isBlobRef(input)) {\n return input\n }\n\n return undefined\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import { JsonValue } from './json.js';
2
+ export declare function parseLexBytes(input?: {
3
+ $bytes?: unknown;
4
+ }): Uint8Array | undefined;
5
+ export declare function encodeLexBytes(bytes: Uint8Array): JsonValue;
6
+ //# sourceMappingURL=bytes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE;IACpC,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,GAAG,UAAU,GAAG,SAAS,CAgBzB;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,CAE3D"}
package/dist/bytes.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseLexBytes = parseLexBytes;
4
+ exports.encodeLexBytes = encodeLexBytes;
5
+ const lex_data_1 = require("@atproto/lex-data");
6
+ function parseLexBytes(input) {
7
+ if (!input || !('$bytes' in input)) {
8
+ return undefined;
9
+ }
10
+ for (const key in input) {
11
+ if (key !== '$bytes') {
12
+ return undefined;
13
+ }
14
+ }
15
+ if (typeof input.$bytes !== 'string') {
16
+ throw new TypeError('$bytes must be a base64-encoded string');
17
+ }
18
+ return (0, lex_data_1.fromBase64)(input.$bytes);
19
+ }
20
+ function encodeLexBytes(bytes) {
21
+ return { $bytes: (0, lex_data_1.toBase64)(bytes) };
22
+ }
23
+ //# sourceMappingURL=bytes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bytes.js","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":";;AAGA,sCAkBC;AAED,wCAEC;AAzBD,gDAAwD;AAGxD,SAAgB,aAAa,CAAC,KAE7B;IACC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,QAAQ,IAAI,KAAK,CAAC,EAAE,CAAC;QACnC,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;YACrB,OAAO,SAAS,CAAA;QAClB,CAAC;IACH,CAAC;IAED,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACrC,MAAM,IAAI,SAAS,CAAC,wCAAwC,CAAC,CAAA;IAC/D,CAAC;IAED,OAAO,IAAA,qBAAU,EAAC,KAAK,CAAC,MAAM,CAAC,CAAA;AACjC,CAAC;AAED,SAAgB,cAAc,CAAC,KAAiB;IAC9C,OAAO,EAAE,MAAM,EAAE,IAAA,mBAAQ,EAAC,KAAK,CAAC,EAAE,CAAA;AACpC,CAAC","sourcesContent":["import { fromBase64, toBase64 } from '@atproto/lex-data'\nimport { JsonValue } from './json.js'\n\nexport function parseLexBytes(input?: {\n $bytes?: unknown\n}): Uint8Array | undefined {\n if (!input || !('$bytes' in input)) {\n return undefined\n }\n\n for (const key in input) {\n if (key !== '$bytes') {\n return undefined\n }\n }\n\n if (typeof input.$bytes !== 'string') {\n throw new TypeError('$bytes must be a base64-encoded string')\n }\n\n return fromBase64(input.$bytes)\n}\n\nexport function encodeLexBytes(bytes: Uint8Array): JsonValue {\n return { $bytes: toBase64(bytes) }\n}\n"]}
@@ -0,0 +1,5 @@
1
+ export * from './bytes.js';
2
+ export * from './json.js';
3
+ export * from './lex-json.js';
4
+ export * from './link.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,WAAW,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./bytes.js"), exports);
5
+ tslib_1.__exportStar(require("./json.js"), exports);
6
+ tslib_1.__exportStar(require("./lex-json.js"), exports);
7
+ tslib_1.__exportStar(require("./link.js"), exports);
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAA0B;AAC1B,oDAAyB;AACzB,wDAA6B;AAC7B,oDAAyB","sourcesContent":["export * from './bytes.js'\nexport * from './json.js'\nexport * from './lex-json.js'\nexport * from './link.js'\n"]}
package/dist/json.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export type JsonScalar = number | string | boolean | null;
2
+ export type JsonValue = JsonScalar | JsonValue[] | {
3
+ [_ in string]?: JsonValue;
4
+ };
5
+ export type JsonObject = {
6
+ [_ in string]?: JsonValue;
7
+ };
8
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;AACzD,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,SAAS,EAAE,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,EAAE,SAAS;CAAE,CAAA;AAChF,MAAM,MAAM,UAAU,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,EAAE,SAAS;CAAE,CAAA"}
package/dist/json.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.js","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"","sourcesContent":["export type JsonScalar = number | string | boolean | null\nexport type JsonValue = JsonScalar | JsonValue[] | { [_ in string]?: JsonValue }\nexport type JsonObject = { [_ in string]?: JsonValue }\n"]}
@@ -0,0 +1,14 @@
1
+ import { LexValue } from '@atproto/lex-data';
2
+ import { JsonValue } from './json.js';
3
+ export declare function lexStringify(input: LexValue): string;
4
+ export type LexParseOptions = {
5
+ /**
6
+ * Forbids the presence of invalid Lex values (e.g. non-integer numbers,
7
+ * malformed $link, $bytes, blob objects, etc.)
8
+ */
9
+ strict?: boolean;
10
+ };
11
+ export declare function lexParse(input: string, options?: LexParseOptions): LexValue;
12
+ export declare function jsonToLex(value: JsonValue, options?: LexParseOptions): LexValue;
13
+ export declare function lexToJson(value: LexValue): JsonValue;
14
+ //# sourceMappingURL=lex-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lex-json.d.ts","sourceRoot":"","sources":["../src/lex-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,QAAQ,EAET,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAc,SAAS,EAAE,MAAM,WAAW,CAAA;AAGjD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAKpD;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,wBAAgB,QAAQ,CACtB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,eAAmC,GAC3C,QAAQ,CAiBV;AAED,wBAAgB,SAAS,CACvB,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE,eAAmC,GAC3C,QAAQ,CAsBV;AA+CD,wBAAgB,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAqBpD"}
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.lexStringify = lexStringify;
4
+ exports.lexParse = lexParse;
5
+ exports.jsonToLex = jsonToLex;
6
+ exports.lexToJson = lexToJson;
7
+ const lex_data_1 = require("@atproto/lex-data");
8
+ const blob_js_1 = require("./blob.js");
9
+ const bytes_js_1 = require("./bytes.js");
10
+ const link_js_1 = require("./link.js");
11
+ function lexStringify(input) {
12
+ // @NOTE Because of the way the "replacer" works in JSON.stringify, it's
13
+ // simpler to convert Lex to JSON first rather than trying to do it
14
+ // on-the-fly.
15
+ return JSON.stringify(lexToJson(input));
16
+ }
17
+ function lexParse(input, options = { strict: false }) {
18
+ return JSON.parse(input, function (key, value) {
19
+ switch (typeof value) {
20
+ case 'object':
21
+ if (value === null)
22
+ return null;
23
+ if (Array.isArray(value))
24
+ return value;
25
+ return parseSpecialJsonObject(value, options) ?? value;
26
+ case 'number':
27
+ if (Number.isInteger(value))
28
+ return value;
29
+ if (options.strict) {
30
+ throw new TypeError(`Invalid non-integer number: ${value}`);
31
+ }
32
+ // fallthrough
33
+ default:
34
+ return value;
35
+ }
36
+ });
37
+ }
38
+ function jsonToLex(value, options = { strict: false }) {
39
+ switch (typeof value) {
40
+ case 'object': {
41
+ if (value === null)
42
+ return null;
43
+ if (Array.isArray(value))
44
+ return jsonArrayToLex(value, options);
45
+ return (parseSpecialJsonObject(value, options) ??
46
+ jsonObjectToLexMap(value, options));
47
+ }
48
+ case 'number':
49
+ if (Number.isInteger(value))
50
+ return value;
51
+ if (options.strict) {
52
+ throw new TypeError(`Invalid non-integer number: ${value}`);
53
+ }
54
+ // fallthrough
55
+ case 'boolean':
56
+ case 'string':
57
+ return value;
58
+ default:
59
+ throw new TypeError(`Invalid JSON value: ${typeof value}`);
60
+ }
61
+ }
62
+ function jsonArrayToLex(input, options) {
63
+ // Lazily copy value
64
+ let copy;
65
+ for (let i = 0; i < input.length; i++) {
66
+ const inputItem = input[i];
67
+ const item = jsonToLex(inputItem, options);
68
+ if (item !== inputItem) {
69
+ copy ?? (copy = Array.from(input));
70
+ copy[i] = item;
71
+ }
72
+ }
73
+ return copy ?? input;
74
+ }
75
+ function jsonObjectToLexMap(input, options) {
76
+ // Lazily copy value
77
+ let copy = undefined;
78
+ for (const [key, jsonValue] of Object.entries(input)) {
79
+ // Prevent prototype pollution
80
+ if (key === '__proto__') {
81
+ throw new TypeError('Invalid key: __proto__');
82
+ }
83
+ // Ignore (strip) undefined values
84
+ if (jsonValue === undefined) {
85
+ copy ?? (copy = { ...input });
86
+ delete copy[key];
87
+ continue;
88
+ }
89
+ const value = jsonToLex(jsonValue, options);
90
+ if (value !== jsonValue) {
91
+ copy ?? (copy = { ...input });
92
+ copy[key] = value;
93
+ }
94
+ }
95
+ return copy ?? input;
96
+ }
97
+ function lexToJson(value) {
98
+ switch (typeof value) {
99
+ case 'object':
100
+ if (value === null) {
101
+ return value;
102
+ }
103
+ else if (Array.isArray(value)) {
104
+ return lexArrayToJson(value);
105
+ }
106
+ else if ((0, lex_data_1.isCid)(value)) {
107
+ return (0, link_js_1.encodeLexLink)(value);
108
+ }
109
+ else if (value instanceof Uint8Array) {
110
+ return (0, bytes_js_1.encodeLexBytes)(value);
111
+ }
112
+ else {
113
+ return encodeLexMap(value);
114
+ }
115
+ case 'boolean':
116
+ case 'string':
117
+ case 'number':
118
+ return value;
119
+ default:
120
+ throw new TypeError(`Invalid Lex value: ${typeof value}`);
121
+ }
122
+ }
123
+ function lexArrayToJson(input) {
124
+ // Lazily copy value
125
+ let copy;
126
+ for (let i = 0; i < input.length; i++) {
127
+ const inputItem = input[i];
128
+ const item = lexToJson(inputItem);
129
+ if (item !== inputItem) {
130
+ copy ?? (copy = Array.from(input));
131
+ copy[i] = item;
132
+ }
133
+ }
134
+ return copy ?? input;
135
+ }
136
+ function encodeLexMap(input) {
137
+ // Lazily copy value
138
+ let copy = undefined;
139
+ for (const [key, lexValue] of Object.entries(input)) {
140
+ // Prevent prototype pollution
141
+ if (key === '__proto__') {
142
+ throw new TypeError('Invalid key: __proto__');
143
+ }
144
+ // Ignore (strip) undefined values
145
+ if (lexValue === undefined) {
146
+ copy ?? (copy = { ...input });
147
+ delete copy[key];
148
+ continue;
149
+ }
150
+ const jsonValue = lexToJson(lexValue);
151
+ if (jsonValue !== lexValue) {
152
+ copy ?? (copy = { ...input });
153
+ copy[key] = jsonValue;
154
+ }
155
+ }
156
+ return copy ?? input;
157
+ }
158
+ function parseSpecialJsonObject(input, options) {
159
+ // Hot path: use hints to avoid parsing when possible
160
+ if (input.$link !== undefined) {
161
+ const cid = (0, link_js_1.parseLexLink)(input);
162
+ if (cid)
163
+ return cid;
164
+ if (options.strict)
165
+ throw new TypeError(`Invalid $link object`);
166
+ }
167
+ else if (input.$bytes !== undefined) {
168
+ const bytes = (0, bytes_js_1.parseLexBytes)(input);
169
+ if (bytes)
170
+ return bytes;
171
+ if (options.strict)
172
+ throw new TypeError(`Invalid $bytes object`);
173
+ }
174
+ else if (input.$type !== undefined) {
175
+ // @NOTE Since blobs are "just" regular lex objects with a special shape,
176
+ // and because an object that does not conform to the blob shape would still
177
+ // result in undefined being returned, we only attempt to parse blobs when
178
+ // the strict option is enabled.
179
+ if (options.strict) {
180
+ if (input.$type === 'blob') {
181
+ const blob = (0, blob_js_1.parseBlobRef)(input, options);
182
+ if (blob)
183
+ return blob;
184
+ throw new TypeError(`Invalid blob object`);
185
+ }
186
+ else if (typeof input.$type !== 'string') {
187
+ throw new TypeError(`Invalid $type property (${typeof input.$type})`);
188
+ }
189
+ else if (input.$type.length === 0) {
190
+ throw new TypeError(`Empty $type property`);
191
+ }
192
+ }
193
+ }
194
+ // @NOTE We ignore legacy blob representation here. They can be handled at the
195
+ // application level if needed.
196
+ return undefined;
197
+ }
198
+ //# sourceMappingURL=lex-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lex-json.js","sourceRoot":"","sources":["../src/lex-json.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BlobRef,\n CID,\n LexArray,\n LexMap,\n LexValue,\n isCid,\n} from '@atproto/lex-data'\nimport { parseBlobRef } from './blob.js'\nimport { encodeLexBytes, parseLexBytes } from './bytes.js'\nimport { JsonObject, JsonValue } from './json.js'\nimport { encodeLexLink, parseLexLink } from './link.js'\n\nexport function lexStringify(input: LexValue): string {\n // @NOTE Because of the way the \"replacer\" works in JSON.stringify, it's\n // simpler to convert Lex to JSON first rather than trying to do it\n // on-the-fly.\n return JSON.stringify(lexToJson(input))\n}\n\nexport type LexParseOptions = {\n /**\n * Forbids the presence of invalid Lex values (e.g. non-integer numbers,\n * malformed $link, $bytes, blob objects, etc.)\n */\n strict?: boolean\n}\n\nexport function lexParse(\n input: string,\n options: LexParseOptions = { strict: false },\n): LexValue {\n return JSON.parse(input, function (key: string, value: JsonValue): LexValue {\n switch (typeof value) {\n case 'object':\n if (value === null) return null\n if (Array.isArray(value)) return value\n return parseSpecialJsonObject(value, options) ?? value\n case 'number':\n if (Number.isInteger(value)) return value\n if (options.strict) {\n throw new TypeError(`Invalid non-integer number: ${value}`)\n }\n // fallthrough\n default:\n return value\n }\n })\n}\n\nexport function jsonToLex(\n value: JsonValue,\n options: LexParseOptions = { strict: false },\n): LexValue {\n switch (typeof value) {\n case 'object': {\n if (value === null) return null\n if (Array.isArray(value)) return jsonArrayToLex(value, options)\n return (\n parseSpecialJsonObject(value, options) ??\n jsonObjectToLexMap(value, options)\n )\n }\n case 'number':\n if (Number.isInteger(value)) return value\n if (options.strict) {\n throw new TypeError(`Invalid non-integer number: ${value}`)\n }\n // fallthrough\n case 'boolean':\n case 'string':\n return value\n default:\n throw new TypeError(`Invalid JSON value: ${typeof value}`)\n }\n}\n\nfunction jsonArrayToLex(\n input: JsonValue[],\n options: LexParseOptions,\n): LexValue[] {\n // Lazily copy value\n let copy: LexValue[] | undefined\n for (let i = 0; i < input.length; i++) {\n const inputItem = input[i]\n const item = jsonToLex(inputItem, options)\n if (item !== inputItem) {\n copy ??= Array.from(input)\n copy[i] = item\n }\n }\n return copy ?? input\n}\n\nfunction jsonObjectToLexMap(\n input: JsonObject,\n options: LexParseOptions,\n): LexMap {\n // Lazily copy value\n let copy: LexMap | undefined = undefined\n for (const [key, jsonValue] of Object.entries(input)) {\n // Prevent prototype pollution\n if (key === '__proto__') {\n throw new TypeError('Invalid key: __proto__')\n }\n\n // Ignore (strip) undefined values\n if (jsonValue === undefined) {\n copy ??= { ...input }\n delete copy[key]\n continue\n }\n\n const value = jsonToLex(jsonValue!, options)\n if (value !== jsonValue) {\n copy ??= { ...input }\n copy[key] = value\n }\n }\n return copy ?? input\n}\n\nexport function lexToJson(value: LexValue): JsonValue {\n switch (typeof value) {\n case 'object':\n if (value === null) {\n return value\n } else if (Array.isArray(value)) {\n return lexArrayToJson(value)\n } else if (isCid(value)) {\n return encodeLexLink(value)\n } else if (value instanceof Uint8Array) {\n return encodeLexBytes(value)\n } else {\n return encodeLexMap(value)\n }\n case 'boolean':\n case 'string':\n case 'number':\n return value\n default:\n throw new TypeError(`Invalid Lex value: ${typeof value}`)\n }\n}\n\nfunction lexArrayToJson(input: LexArray): JsonValue[] {\n // Lazily copy value\n let copy: JsonValue[] | undefined\n for (let i = 0; i < input.length; i++) {\n const inputItem = input[i]\n const item = lexToJson(inputItem)\n if (item !== inputItem) {\n copy ??= Array.from(input) as JsonValue[]\n copy[i] = item\n }\n }\n return copy ?? (input as JsonValue[])\n}\n\nfunction encodeLexMap(input: LexMap): JsonObject {\n // Lazily copy value\n let copy: JsonObject | undefined = undefined\n for (const [key, lexValue] of Object.entries(input)) {\n // Prevent prototype pollution\n if (key === '__proto__') {\n throw new TypeError('Invalid key: __proto__')\n }\n\n // Ignore (strip) undefined values\n if (lexValue === undefined) {\n copy ??= { ...input } as JsonObject\n delete copy[key]\n continue\n }\n\n const jsonValue = lexToJson(lexValue!)\n if (jsonValue !== lexValue) {\n copy ??= { ...input } as JsonObject\n copy[key] = jsonValue\n }\n }\n return copy ?? (input as JsonObject)\n}\n\nfunction parseSpecialJsonObject(\n input: LexMap,\n options: LexParseOptions,\n): CID | Uint8Array | BlobRef | undefined {\n // Hot path: use hints to avoid parsing when possible\n\n if (input.$link !== undefined) {\n const cid = parseLexLink(input)\n if (cid) return cid\n if (options.strict) throw new TypeError(`Invalid $link object`)\n } else if (input.$bytes !== undefined) {\n const bytes = parseLexBytes(input)\n if (bytes) return bytes\n if (options.strict) throw new TypeError(`Invalid $bytes object`)\n } else if (input.$type !== undefined) {\n // @NOTE Since blobs are \"just\" regular lex objects with a special shape,\n // and because an object that does not conform to the blob shape would still\n // result in undefined being returned, we only attempt to parse blobs when\n // the strict option is enabled.\n if (options.strict) {\n if (input.$type === 'blob') {\n const blob = parseBlobRef(input, options)\n if (blob) return blob\n throw new TypeError(`Invalid blob object`)\n } else if (typeof input.$type !== 'string') {\n throw new TypeError(`Invalid $type property (${typeof input.$type})`)\n } else if (input.$type.length === 0) {\n throw new TypeError(`Empty $type property`)\n }\n }\n }\n\n // @NOTE We ignore legacy blob representation here. They can be handled at the\n // application level if needed.\n\n return undefined\n}\n"]}
package/dist/link.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { CID } from '@atproto/lex-data';
2
+ import { JsonValue } from './json.js';
3
+ export declare function parseLexLink(input?: {
4
+ $link?: unknown;
5
+ }): CID | undefined;
6
+ export declare function encodeLexLink(cid: CID): JsonValue;
7
+ //# sourceMappingURL=link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,GAAG,GAAG,SAAS,CA6BzE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,SAAS,CAEjD"}
package/dist/link.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseLexLink = parseLexLink;
4
+ exports.encodeLexLink = encodeLexLink;
5
+ const lex_data_1 = require("@atproto/lex-data");
6
+ function parseLexLink(input) {
7
+ if (!input || !('$link' in input)) {
8
+ return undefined;
9
+ }
10
+ for (const key in input) {
11
+ if (key !== '$link') {
12
+ return undefined;
13
+ }
14
+ }
15
+ if (typeof input.$link !== 'string') {
16
+ throw new TypeError('$link must be a base32-encoded CID string');
17
+ }
18
+ if (input.$link.length === 0) {
19
+ throw new TypeError('CID string in $link cannot be empty');
20
+ }
21
+ // Arbitrary limit to prevent DoS via extremely long CIDs
22
+ if (input.$link.length > 2048) {
23
+ throw new TypeError('CID string in $link is too long');
24
+ }
25
+ try {
26
+ return lex_data_1.CID.parse(input.$link);
27
+ }
28
+ catch (cause) {
29
+ throw new TypeError('Invalid CID string in $link', { cause });
30
+ }
31
+ }
32
+ function encodeLexLink(cid) {
33
+ return { $link: cid.toString() };
34
+ }
35
+ //# sourceMappingURL=link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.js","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":";;AAGA,oCA6BC;AAED,sCAEC;AApCD,gDAAuC;AAGvC,SAAgB,YAAY,CAAC,KAA2B;IACtD,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,OAAO,IAAI,KAAK,CAAC,EAAE,CAAC;QAClC,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;YACpB,OAAO,SAAS,CAAA;QAClB,CAAC;IACH,CAAC;IAED,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,SAAS,CAAC,2CAA2C,CAAC,CAAA;IAClE,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,SAAS,CAAC,qCAAqC,CAAC,CAAA;IAC5D,CAAC;IAED,yDAAyD;IACzD,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;QAC9B,MAAM,IAAI,SAAS,CAAC,iCAAiC,CAAC,CAAA;IACxD,CAAC;IAED,IAAI,CAAC;QACH,OAAO,cAAG,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,SAAS,CAAC,6BAA6B,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IAC/D,CAAC;AACH,CAAC;AAED,SAAgB,aAAa,CAAC,GAAQ;IACpC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAA;AAClC,CAAC","sourcesContent":["import { CID } from '@atproto/lex-data'\nimport { JsonValue } from './json.js'\n\nexport function parseLexLink(input?: { $link?: unknown }): CID | undefined {\n if (!input || !('$link' in input)) {\n return undefined\n }\n\n for (const key in input) {\n if (key !== '$link') {\n return undefined\n }\n }\n\n if (typeof input.$link !== 'string') {\n throw new TypeError('$link must be a base32-encoded CID string')\n }\n\n if (input.$link.length === 0) {\n throw new TypeError('CID string in $link cannot be empty')\n }\n\n // Arbitrary limit to prevent DoS via extremely long CIDs\n if (input.$link.length > 2048) {\n throw new TypeError('CID string in $link is too long')\n }\n\n try {\n return CID.parse(input.$link)\n } catch (cause) {\n throw new TypeError('Invalid CID string in $link', { cause })\n }\n}\n\nexport function encodeLexLink(cid: CID): JsonValue {\n return { $link: cid.toString() }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@atproto/lex-json",
3
+ "version": "0.0.0",
4
+ "license": "MIT",
5
+ "description": "Lexicon encoding utilities for AT Lexicon data in JSON format",
6
+ "keywords": [
7
+ "atproto",
8
+ "lex",
9
+ "data",
10
+ "json",
11
+ "encoding",
12
+ "utilities"
13
+ ],
14
+ "homepage": "https://atproto.com",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/bluesky-social/atproto",
18
+ "directory": "packages/lex/lex-json"
19
+ },
20
+ "files": [
21
+ "./src",
22
+ "./dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "type": "commonjs",
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "browser": "./dist/index.js",
31
+ "import": "./dist/index.js",
32
+ "require": "./dist/index.js",
33
+ "types": "./dist/index.d.ts"
34
+ }
35
+ },
36
+ "dependencies": {
37
+ "@atproto/lex-data": "workspace:*",
38
+ "tslib": "^2.8.1"
39
+ },
40
+ "devDependencies": {
41
+ "jest": "^28.1.2"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc --build tsconfig.build.json",
45
+ "test": "jest"
46
+ }
47
+ }
package/src/blob.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { BlobRef, LexMap, isBlobRef } from '@atproto/lex-data'
2
+ import { parseLexLink } from './link.js'
3
+
4
+ export function parseBlobRef(
5
+ input: LexMap,
6
+ options?: { strict?: boolean },
7
+ ): BlobRef | undefined {
8
+ if (input.$type !== 'blob') return undefined
9
+
10
+ const ref = input?.ref
11
+ if (!ref || typeof ref !== 'object') return undefined
12
+
13
+ // @NOTE Because json to lex conversion can be performed both in a depth-first
14
+ // manner (e.g. via lexParse) or in a breadth-first manner (e.g. via
15
+ // jsonToLex), the `ref` property may either be a LexMap with a $link
16
+ // property, or it may already be a CID instance.
17
+
18
+ if ('$link' in ref) {
19
+ const cid = parseLexLink(ref)
20
+ if (!cid) return undefined
21
+
22
+ const blob = { ...input, ref: cid }
23
+ if (isBlobRef(blob, options)) return blob
24
+ }
25
+
26
+ if (isBlobRef(input)) {
27
+ return input
28
+ }
29
+
30
+ return undefined
31
+ }
package/src/bytes.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { fromBase64, toBase64 } from '@atproto/lex-data'
2
+ import { JsonValue } from './json.js'
3
+
4
+ export function parseLexBytes(input?: {
5
+ $bytes?: unknown
6
+ }): Uint8Array | undefined {
7
+ if (!input || !('$bytes' in input)) {
8
+ return undefined
9
+ }
10
+
11
+ for (const key in input) {
12
+ if (key !== '$bytes') {
13
+ return undefined
14
+ }
15
+ }
16
+
17
+ if (typeof input.$bytes !== 'string') {
18
+ throw new TypeError('$bytes must be a base64-encoded string')
19
+ }
20
+
21
+ return fromBase64(input.$bytes)
22
+ }
23
+
24
+ export function encodeLexBytes(bytes: Uint8Array): JsonValue {
25
+ return { $bytes: toBase64(bytes) }
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './bytes.js'
2
+ export * from './json.js'
3
+ export * from './lex-json.js'
4
+ export * from './link.js'
package/src/json.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type JsonScalar = number | string | boolean | null
2
+ export type JsonValue = JsonScalar | JsonValue[] | { [_ in string]?: JsonValue }
3
+ export type JsonObject = { [_ in string]?: JsonValue }
@@ -0,0 +1,511 @@
1
+ import { CID, LexValue, lexEquals } from '@atproto/lex-data'
2
+ import { JsonValue } from './json.js'
3
+ import { jsonToLex, lexParse, lexStringify, lexToJson } from './lex-json.js'
4
+
5
+ export const validVectors: Array<{
6
+ name: string
7
+ json: JsonValue
8
+ lex: LexValue
9
+ }> = [
10
+ {
11
+ name: 'pure json',
12
+ json: {
13
+ string: 'abc',
14
+ unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧',
15
+ integer: 123,
16
+ bool: true,
17
+ null: null,
18
+ array: ['abc', 'def', 'ghi'],
19
+ object: {
20
+ string: 'abc',
21
+ number: 123,
22
+ bool: true,
23
+ arr: ['abc', 'def', 'ghi'],
24
+ },
25
+ },
26
+ lex: {
27
+ string: 'abc',
28
+ unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧',
29
+ integer: 123,
30
+ bool: true,
31
+ null: null,
32
+ array: ['abc', 'def', 'ghi'],
33
+ object: {
34
+ string: 'abc',
35
+ number: 123,
36
+ bool: true,
37
+ arr: ['abc', 'def', 'ghi'],
38
+ },
39
+ },
40
+ },
41
+ {
42
+ name: 'lex data',
43
+ json: {
44
+ a: {
45
+ $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
46
+ },
47
+ b: {
48
+ $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
49
+ },
50
+ c: {
51
+ $type: 'blob',
52
+ ref: {
53
+ $link: 'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',
54
+ },
55
+ mimeType: 'image/jpeg',
56
+ size: 10000,
57
+ },
58
+ },
59
+ lex: {
60
+ a: CID.parse(
61
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
62
+ ),
63
+ b: new Uint8Array([
64
+ 156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253,
65
+ 65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194,
66
+ 204, 141,
67
+ ]),
68
+ c: {
69
+ $type: 'blob',
70
+ ref: CID.parse(
71
+ 'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',
72
+ ),
73
+ mimeType: 'image/jpeg',
74
+ size: 10000,
75
+ },
76
+ },
77
+ },
78
+ {
79
+ name: 'lexArray',
80
+ json: [
81
+ {
82
+ $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
83
+ },
84
+ {
85
+ $link: 'bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q',
86
+ },
87
+ {
88
+ $link: 'bafyreiaizynclnqiolq7byfpjjtgqzn4sfrsgn7z2hhf6bo4utdwkin7ke',
89
+ },
90
+ {
91
+ $link: 'bafyreifd4w4tcr5tluxz7osjtnofffvtsmgdqcfrfi6evjde4pl27lrjpy',
92
+ },
93
+ ],
94
+ lex: [
95
+ CID.parse('bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a'),
96
+ CID.parse('bafyreigoxt64qghytzkr6ik7qvtzc7lyytiq5xbbrokbxjows2wp7vmo6q'),
97
+ CID.parse('bafyreiaizynclnqiolq7byfpjjtgqzn4sfrsgn7z2hhf6bo4utdwkin7ke'),
98
+ CID.parse('bafyreifd4w4tcr5tluxz7osjtnofffvtsmgdqcfrfi6evjde4pl27lrjpy'),
99
+ ],
100
+ },
101
+ {
102
+ name: 'root cid',
103
+ json: {
104
+ $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
105
+ },
106
+ lex: CID.parse(
107
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
108
+ ),
109
+ },
110
+ {
111
+ name: 'root bytes',
112
+ json: {
113
+ $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
114
+ },
115
+ lex: new Uint8Array([
116
+ 156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253, 65,
117
+ 60, 242, 11, 98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194, 204,
118
+ 141,
119
+ ]),
120
+ },
121
+ {
122
+ name: 'lexNested',
123
+ json: {
124
+ a: {
125
+ b: [
126
+ {
127
+ d: [
128
+ {
129
+ $link:
130
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
131
+ },
132
+ {
133
+ $link:
134
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
135
+ },
136
+ ],
137
+ e: [
138
+ {
139
+ $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
140
+ },
141
+ {
142
+ $bytes: 'iE+sPoHobU9tSIqGI+309LLCcWQIRmEXwxcoDt19tas',
143
+ },
144
+ ],
145
+ },
146
+ ],
147
+ },
148
+ },
149
+ lex: {
150
+ a: {
151
+ b: [
152
+ {
153
+ d: [
154
+ CID.parse(
155
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
156
+ ),
157
+ CID.parse(
158
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
159
+ ),
160
+ ],
161
+ e: [
162
+ new Uint8Array([
163
+ 156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174,
164
+ 161, 253, 65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238,
165
+ 190, 176, 26, 194, 204, 141,
166
+ ]),
167
+ new Uint8Array([
168
+ 136, 79, 172, 62, 129, 232, 109, 79, 109, 72, 138, 134, 35, 237,
169
+ 244, 244, 178, 194, 113, 100, 8, 70, 97, 23, 195, 23, 40, 14,
170
+ 221, 125, 181, 171,
171
+ ]),
172
+ ],
173
+ },
174
+ ],
175
+ },
176
+ },
177
+ },
178
+ {
179
+ name: 'empty structures',
180
+ json: {
181
+ emptyObject: {},
182
+ emptyArray: [],
183
+ emtyBytes: { $bytes: '' },
184
+ },
185
+ lex: {
186
+ emptyObject: {},
187
+ emptyArray: [],
188
+ emtyBytes: new Uint8Array([]),
189
+ },
190
+ },
191
+ {
192
+ name: 'mixed types in array',
193
+ json: {
194
+ arr: [
195
+ 'string',
196
+ 123,
197
+ true,
198
+ null,
199
+ {
200
+ $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
201
+ },
202
+ { $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0' },
203
+ { nested: 'object' },
204
+ ['nested', 'array'],
205
+ ],
206
+ },
207
+ lex: {
208
+ arr: [
209
+ 'string',
210
+ 123,
211
+ true,
212
+ null,
213
+ CID.parse(
214
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
215
+ ),
216
+ new Uint8Array([
217
+ 156, 81, 17, 142, 242, 203, 139, 15, 106, 155, 142, 73, 174, 161, 253,
218
+ 65, 60, 242, 11, 98, 238, 213, 118, 248, 157, 238, 190, 176, 26, 194,
219
+ 204, 141,
220
+ ]),
221
+ { nested: 'object' },
222
+ ['nested', 'array'],
223
+ ],
224
+ },
225
+ },
226
+ {
227
+ name: "mismatched order in object doesn't affect equality",
228
+ json: {
229
+ a: 'valueA',
230
+ b: 'valueB',
231
+ c: {
232
+ $link: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
233
+ },
234
+ d: {
235
+ a: 'valueA',
236
+ b: 'valueB',
237
+ },
238
+ },
239
+ lex: {
240
+ c: CID.parse(
241
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
242
+ ),
243
+ d: {
244
+ b: 'valueB',
245
+ a: 'valueA',
246
+ },
247
+ a: 'valueA',
248
+ b: 'valueB',
249
+ },
250
+ },
251
+ ]
252
+
253
+ export const acceptableVectors: Array<{
254
+ note: string
255
+ json: JsonValue
256
+ }> = [
257
+ {
258
+ note: 'non string $type',
259
+ json: {
260
+ $type: 3124,
261
+ foo: 'bar',
262
+ },
263
+ },
264
+ {
265
+ note: 'object with float values',
266
+ json: {
267
+ a: 1.5,
268
+ },
269
+ },
270
+ {
271
+ note: 'blob with wrong field type',
272
+ json: {
273
+ $type: 'blob',
274
+ ref: 'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',
275
+ mimeType: 'image/jpeg',
276
+ size: 10000,
277
+ },
278
+ },
279
+ {
280
+ note: 'blob with missing key',
281
+ json: {
282
+ $type: 'blob',
283
+ mimeType: 'image/jpeg',
284
+ size: 10000,
285
+ },
286
+ },
287
+ {
288
+ note: 'blob with extra fields',
289
+ json: {
290
+ $type: 'blob',
291
+ ref: {
292
+ $link: 'bafkreig77vqcdozl2wyk6z3cscaj5q5fggi53aoh64fewkdiri3cdauyn4',
293
+ },
294
+ mimeType: 'image/jpeg',
295
+ size: 10000,
296
+ other: 'blah',
297
+ },
298
+ },
299
+ {
300
+ note: 'bytes with extra fields',
301
+ json: {
302
+ $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
303
+ other: 'blah',
304
+ },
305
+ },
306
+ {
307
+ note: 'link with extra fields',
308
+ json: {
309
+ $link: 'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',
310
+ other: 'blah',
311
+ },
312
+ },
313
+ {
314
+ note: '$bytes and $link',
315
+ json: {
316
+ $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
317
+ $link: 'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',
318
+ },
319
+ },
320
+ {
321
+ note: '$bytes and $type',
322
+ json: {
323
+ $type: 'bytes',
324
+ $bytes: 'nFERjvLLiw9qm45JrqH9QTzyC2Lu1Xb4ne6+sBrCzI0',
325
+ },
326
+ },
327
+ {
328
+ note: '$link and $type',
329
+ json: {
330
+ $type: 'blob',
331
+ $link: 'bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity',
332
+ },
333
+ },
334
+ ]
335
+
336
+ export const invalidVectors: Array<{
337
+ note: string
338
+ json: JsonValue
339
+ }> = [
340
+ {
341
+ note: 'bytes with wrong field type',
342
+ json: {
343
+ $bytes: [1, 2, 3],
344
+ },
345
+ },
346
+ {
347
+ note: 'invalid base64 in $bytes',
348
+ json: {
349
+ $bytes: '🐻',
350
+ },
351
+ },
352
+ {
353
+ note: 'link with wrong field type',
354
+ json: {
355
+ $link: 1234,
356
+ },
357
+ },
358
+ {
359
+ note: 'link with bogus CID',
360
+ json: {
361
+ $link: '.',
362
+ },
363
+ },
364
+ ]
365
+
366
+ describe('lexParse', () => {
367
+ describe('valid vectors', () => {
368
+ for (const { name, json, lex } of validVectors) {
369
+ describe(name, () => {
370
+ it('parses lex from string', () => {
371
+ expect(
372
+ lexEquals(lex, lexParse(JSON.stringify(json), { strict: false })),
373
+ ).toBe(true)
374
+ expect(
375
+ lexEquals(lex, lexParse(JSON.stringify(json), { strict: true })),
376
+ ).toBe(true)
377
+ })
378
+ })
379
+ }
380
+ })
381
+
382
+ describe('acceptable vectors', () => {
383
+ for (const { note, json } of acceptableVectors) {
384
+ describe(note, () => {
385
+ it('parses lex from string in non-strict mode', () => {
386
+ expect(() =>
387
+ lexParse(JSON.stringify(json), { strict: false }),
388
+ ).not.toThrow()
389
+ })
390
+
391
+ it('parses lex from string in strict mode', () => {
392
+ expect(() =>
393
+ lexParse(JSON.stringify(json), { strict: true }),
394
+ ).toThrow()
395
+ })
396
+ })
397
+ }
398
+ })
399
+
400
+ describe('invalid vectors', () => {
401
+ for (const { note, json } of invalidVectors) {
402
+ describe(note, () => {
403
+ it('throws when parsing malformed JSON', () => {
404
+ expect(() =>
405
+ lexParse(JSON.stringify(json), { strict: false }),
406
+ ).toThrow()
407
+ expect(() =>
408
+ lexParse(JSON.stringify(json), { strict: true }),
409
+ ).toThrow()
410
+ })
411
+ })
412
+ }
413
+ })
414
+ })
415
+
416
+ describe('lexStringify', () => {
417
+ describe('valid vectors', () => {
418
+ for (const { name, json, lex } of validVectors) {
419
+ describe(name, () => {
420
+ it('stringifies lex to string', () => {
421
+ expect(JSON.parse(lexStringify(lex))).toEqual(json)
422
+ })
423
+ })
424
+ }
425
+ })
426
+ })
427
+
428
+ describe('jsonToLex', () => {
429
+ describe('valid vectors', () => {
430
+ for (const { name, json, lex } of validVectors) {
431
+ describe(name, () => {
432
+ it('converts json to lex (in strict mode)', () => {
433
+ expect(lexEquals(jsonToLex(json, { strict: true }), lex)).toBe(true)
434
+ expect(lexEquals(lex, jsonToLex(json, { strict: true }))).toBe(true)
435
+ })
436
+
437
+ it('converts json to lex (in non-strict mode)', () => {
438
+ expect(lexEquals(jsonToLex(json, { strict: false }), lex)).toBe(true)
439
+ expect(lexEquals(lex, jsonToLex(json, { strict: false }))).toBe(true)
440
+ })
441
+ })
442
+ }
443
+ })
444
+
445
+ describe('acceptable vectors', () => {
446
+ for (const { note, json } of acceptableVectors) {
447
+ describe(note, () => {
448
+ it('parses lex from json in strict mode', () => {
449
+ expect(() => jsonToLex(json, { strict: true })).toThrow()
450
+ })
451
+
452
+ it('parses lex from json in non-strict mode', () => {
453
+ expect(() => jsonToLex(json, { strict: false })).not.toThrow()
454
+ })
455
+ })
456
+ }
457
+ })
458
+
459
+ describe('invalid vectors', () => {
460
+ for (const { note, json } of invalidVectors) {
461
+ describe(note, () => {
462
+ it(`throws for malformed object`, () => {
463
+ expect(() => jsonToLex(json, { strict: true })).toThrow()
464
+ })
465
+
466
+ it('throws for nested malformed object', () => {
467
+ expect(() => jsonToLex({ nested: json }, { strict: true })).toThrow()
468
+ expect(() => jsonToLex([json], { strict: true })).toThrow()
469
+ })
470
+ })
471
+ }
472
+ })
473
+ })
474
+
475
+ describe('lexToJson', () => {
476
+ describe('valid vectors', () => {
477
+ for (const { name, json, lex } of validVectors) {
478
+ describe(name, () => {
479
+ it('converts lex to json', () => {
480
+ expect(lexToJson(lex)).toEqual(json)
481
+ expect(lexToJson(lex)).toEqual(json)
482
+ })
483
+ })
484
+ }
485
+ })
486
+ })
487
+
488
+ describe('json > lex > json', () => {
489
+ describe('valid vectors', () => {
490
+ for (const { name, json } of validVectors) {
491
+ describe(name, () => {
492
+ it('converts json to lex', () => {
493
+ expect(lexToJson(jsonToLex(json))).toEqual(json)
494
+ })
495
+ })
496
+ }
497
+ })
498
+ })
499
+
500
+ describe('lex > json > lex', () => {
501
+ describe('valid vectors', () => {
502
+ for (const { name, lex } of validVectors) {
503
+ describe(name, () => {
504
+ it('converts lex to json', () => {
505
+ expect(lexEquals(jsonToLex(lexToJson(lex)), lex)).toBe(true)
506
+ expect(lexEquals(lex, jsonToLex(lexToJson(lex)))).toBe(true)
507
+ })
508
+ })
509
+ }
510
+ })
511
+ })
@@ -0,0 +1,221 @@
1
+ import {
2
+ BlobRef,
3
+ CID,
4
+ LexArray,
5
+ LexMap,
6
+ LexValue,
7
+ isCid,
8
+ } from '@atproto/lex-data'
9
+ import { parseBlobRef } from './blob.js'
10
+ import { encodeLexBytes, parseLexBytes } from './bytes.js'
11
+ import { JsonObject, JsonValue } from './json.js'
12
+ import { encodeLexLink, parseLexLink } from './link.js'
13
+
14
+ export function lexStringify(input: LexValue): string {
15
+ // @NOTE Because of the way the "replacer" works in JSON.stringify, it's
16
+ // simpler to convert Lex to JSON first rather than trying to do it
17
+ // on-the-fly.
18
+ return JSON.stringify(lexToJson(input))
19
+ }
20
+
21
+ export type LexParseOptions = {
22
+ /**
23
+ * Forbids the presence of invalid Lex values (e.g. non-integer numbers,
24
+ * malformed $link, $bytes, blob objects, etc.)
25
+ */
26
+ strict?: boolean
27
+ }
28
+
29
+ export function lexParse(
30
+ input: string,
31
+ options: LexParseOptions = { strict: false },
32
+ ): LexValue {
33
+ return JSON.parse(input, function (key: string, value: JsonValue): LexValue {
34
+ switch (typeof value) {
35
+ case 'object':
36
+ if (value === null) return null
37
+ if (Array.isArray(value)) return value
38
+ return parseSpecialJsonObject(value, options) ?? value
39
+ case 'number':
40
+ if (Number.isInteger(value)) return value
41
+ if (options.strict) {
42
+ throw new TypeError(`Invalid non-integer number: ${value}`)
43
+ }
44
+ // fallthrough
45
+ default:
46
+ return value
47
+ }
48
+ })
49
+ }
50
+
51
+ export function jsonToLex(
52
+ value: JsonValue,
53
+ options: LexParseOptions = { strict: false },
54
+ ): LexValue {
55
+ switch (typeof value) {
56
+ case 'object': {
57
+ if (value === null) return null
58
+ if (Array.isArray(value)) return jsonArrayToLex(value, options)
59
+ return (
60
+ parseSpecialJsonObject(value, options) ??
61
+ jsonObjectToLexMap(value, options)
62
+ )
63
+ }
64
+ case 'number':
65
+ if (Number.isInteger(value)) return value
66
+ if (options.strict) {
67
+ throw new TypeError(`Invalid non-integer number: ${value}`)
68
+ }
69
+ // fallthrough
70
+ case 'boolean':
71
+ case 'string':
72
+ return value
73
+ default:
74
+ throw new TypeError(`Invalid JSON value: ${typeof value}`)
75
+ }
76
+ }
77
+
78
+ function jsonArrayToLex(
79
+ input: JsonValue[],
80
+ options: LexParseOptions,
81
+ ): LexValue[] {
82
+ // Lazily copy value
83
+ let copy: LexValue[] | undefined
84
+ for (let i = 0; i < input.length; i++) {
85
+ const inputItem = input[i]
86
+ const item = jsonToLex(inputItem, options)
87
+ if (item !== inputItem) {
88
+ copy ??= Array.from(input)
89
+ copy[i] = item
90
+ }
91
+ }
92
+ return copy ?? input
93
+ }
94
+
95
+ function jsonObjectToLexMap(
96
+ input: JsonObject,
97
+ options: LexParseOptions,
98
+ ): LexMap {
99
+ // Lazily copy value
100
+ let copy: LexMap | undefined = undefined
101
+ for (const [key, jsonValue] of Object.entries(input)) {
102
+ // Prevent prototype pollution
103
+ if (key === '__proto__') {
104
+ throw new TypeError('Invalid key: __proto__')
105
+ }
106
+
107
+ // Ignore (strip) undefined values
108
+ if (jsonValue === undefined) {
109
+ copy ??= { ...input }
110
+ delete copy[key]
111
+ continue
112
+ }
113
+
114
+ const value = jsonToLex(jsonValue!, options)
115
+ if (value !== jsonValue) {
116
+ copy ??= { ...input }
117
+ copy[key] = value
118
+ }
119
+ }
120
+ return copy ?? input
121
+ }
122
+
123
+ export function lexToJson(value: LexValue): JsonValue {
124
+ switch (typeof value) {
125
+ case 'object':
126
+ if (value === null) {
127
+ return value
128
+ } else if (Array.isArray(value)) {
129
+ return lexArrayToJson(value)
130
+ } else if (isCid(value)) {
131
+ return encodeLexLink(value)
132
+ } else if (value instanceof Uint8Array) {
133
+ return encodeLexBytes(value)
134
+ } else {
135
+ return encodeLexMap(value)
136
+ }
137
+ case 'boolean':
138
+ case 'string':
139
+ case 'number':
140
+ return value
141
+ default:
142
+ throw new TypeError(`Invalid Lex value: ${typeof value}`)
143
+ }
144
+ }
145
+
146
+ function lexArrayToJson(input: LexArray): JsonValue[] {
147
+ // Lazily copy value
148
+ let copy: JsonValue[] | undefined
149
+ for (let i = 0; i < input.length; i++) {
150
+ const inputItem = input[i]
151
+ const item = lexToJson(inputItem)
152
+ if (item !== inputItem) {
153
+ copy ??= Array.from(input) as JsonValue[]
154
+ copy[i] = item
155
+ }
156
+ }
157
+ return copy ?? (input as JsonValue[])
158
+ }
159
+
160
+ function encodeLexMap(input: LexMap): JsonObject {
161
+ // Lazily copy value
162
+ let copy: JsonObject | undefined = undefined
163
+ for (const [key, lexValue] of Object.entries(input)) {
164
+ // Prevent prototype pollution
165
+ if (key === '__proto__') {
166
+ throw new TypeError('Invalid key: __proto__')
167
+ }
168
+
169
+ // Ignore (strip) undefined values
170
+ if (lexValue === undefined) {
171
+ copy ??= { ...input } as JsonObject
172
+ delete copy[key]
173
+ continue
174
+ }
175
+
176
+ const jsonValue = lexToJson(lexValue!)
177
+ if (jsonValue !== lexValue) {
178
+ copy ??= { ...input } as JsonObject
179
+ copy[key] = jsonValue
180
+ }
181
+ }
182
+ return copy ?? (input as JsonObject)
183
+ }
184
+
185
+ function parseSpecialJsonObject(
186
+ input: LexMap,
187
+ options: LexParseOptions,
188
+ ): CID | Uint8Array | BlobRef | undefined {
189
+ // Hot path: use hints to avoid parsing when possible
190
+
191
+ if (input.$link !== undefined) {
192
+ const cid = parseLexLink(input)
193
+ if (cid) return cid
194
+ if (options.strict) throw new TypeError(`Invalid $link object`)
195
+ } else if (input.$bytes !== undefined) {
196
+ const bytes = parseLexBytes(input)
197
+ if (bytes) return bytes
198
+ if (options.strict) throw new TypeError(`Invalid $bytes object`)
199
+ } else if (input.$type !== undefined) {
200
+ // @NOTE Since blobs are "just" regular lex objects with a special shape,
201
+ // and because an object that does not conform to the blob shape would still
202
+ // result in undefined being returned, we only attempt to parse blobs when
203
+ // the strict option is enabled.
204
+ if (options.strict) {
205
+ if (input.$type === 'blob') {
206
+ const blob = parseBlobRef(input, options)
207
+ if (blob) return blob
208
+ throw new TypeError(`Invalid blob object`)
209
+ } else if (typeof input.$type !== 'string') {
210
+ throw new TypeError(`Invalid $type property (${typeof input.$type})`)
211
+ } else if (input.$type.length === 0) {
212
+ throw new TypeError(`Empty $type property`)
213
+ }
214
+ }
215
+ }
216
+
217
+ // @NOTE We ignore legacy blob representation here. They can be handled at the
218
+ // application level if needed.
219
+
220
+ return undefined
221
+ }
package/src/link.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { CID } from '@atproto/lex-data'
2
+ import { JsonValue } from './json.js'
3
+
4
+ export function parseLexLink(input?: { $link?: unknown }): CID | undefined {
5
+ if (!input || !('$link' in input)) {
6
+ return undefined
7
+ }
8
+
9
+ for (const key in input) {
10
+ if (key !== '$link') {
11
+ return undefined
12
+ }
13
+ }
14
+
15
+ if (typeof input.$link !== 'string') {
16
+ throw new TypeError('$link must be a base32-encoded CID string')
17
+ }
18
+
19
+ if (input.$link.length === 0) {
20
+ throw new TypeError('CID string in $link cannot be empty')
21
+ }
22
+
23
+ // Arbitrary limit to prevent DoS via extremely long CIDs
24
+ if (input.$link.length > 2048) {
25
+ throw new TypeError('CID string in $link is too long')
26
+ }
27
+
28
+ try {
29
+ return CID.parse(input.$link)
30
+ } catch (cause) {
31
+ throw new TypeError('Invalid CID string in $link', { cause })
32
+ }
33
+ }
34
+
35
+ export function encodeLexLink(cid: CID): JsonValue {
36
+ return { $link: cid.toString() }
37
+ }