@atproto/lex-cbor 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/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@atproto/lex-cbor",
3
+ "version": "0.0.0",
4
+ "license": "MIT",
5
+ "description": "Lexicon encoding utilities for AT Lexicon data in CBOR format",
6
+ "keywords": [
7
+ "atproto",
8
+ "lex",
9
+ "data",
10
+ "cbor",
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-cbor"
19
+ },
20
+ "files": [
21
+ "./src",
22
+ "./dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "main": "./dist/index.cjs.js",
26
+ "module": "./dist/index.es.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.es.js",
32
+ "require": "./dist/index.cjs.js"
33
+ }
34
+ },
35
+ "dependencies": {
36
+ "@atproto/lex-data": "workspace:*",
37
+ "multiformats": "^9.9.0",
38
+ "tslib": "^2.8.1"
39
+ },
40
+ "devDependencies": {
41
+ "@atproto/lex-json": "workspace:*",
42
+ "cborg": "^4.3.0",
43
+ "jest": "^28.1.2",
44
+ "vite": "^6.2.0"
45
+ },
46
+ "scripts": {
47
+ "dev": "vite build --watch",
48
+ "prebuild": "vite build --emptyOutDir",
49
+ "build": "tsc --build tsconfig.build.json",
50
+ "test": "jest"
51
+ }
52
+ }
@@ -0,0 +1,101 @@
1
+ import {
2
+ DecodeOptions,
3
+ EncodeOptions,
4
+ TagDecoder,
5
+ Token,
6
+ Type,
7
+ decode as cborgDecode,
8
+ decodeFirst as cborgDecodeFirst,
9
+ encode as cborgEncode,
10
+ } from 'cborg'
11
+ import type { ByteView } from 'multiformats/block'
12
+ import { CID, LexValue } from '@atproto/lex-data'
13
+
14
+ // @NOTE This was inspired by @ipld/dag-cbor implementation, but adapted to
15
+ // match the AT Data Model constraints. Floats, in particular, are not allowed.
16
+
17
+ // @NOTE "cborg" version 4 is required to support multi-decoding via the
18
+ // "decodeFirst" function. However, that version only exposes ES modules.
19
+ // Because this package is using "commonjs", "cborg" will be bundled instead of
20
+ // depending on it directly.
21
+
22
+ const CID_CBOR_TAG = 42
23
+
24
+ function cidEncoder(obj: object): Token[] | null {
25
+ const cid = CID.asCID(obj)
26
+ if (!cid) return null
27
+
28
+ const bytes = new Uint8Array(cid.bytes.byteLength + 1)
29
+ bytes.set(cid.bytes, 1) // prefix is 0x00, for historical reasons
30
+ return [new Token(Type.tag, CID_CBOR_TAG), new Token(Type.bytes, bytes)]
31
+ }
32
+
33
+ function undefinedEncoder(): null {
34
+ throw new Error('`undefined` is not allowed by the AT Data Model')
35
+ }
36
+
37
+ function numberEncoder(num: number): null {
38
+ if (Number.isInteger(num)) return null
39
+
40
+ throw new Error('Non-integer numbers are not allowed by the AT Data Model')
41
+ }
42
+
43
+ function mapEncoder(map: Map<unknown, unknown>): null {
44
+ for (const key of map.keys()) {
45
+ if (typeof key !== 'string') {
46
+ throw new Error(
47
+ 'Only string keys are allowed in CBOR "map" by the AT Data Model',
48
+ )
49
+ }
50
+ }
51
+ // @NOTE Map will be encoded as CBOR "map", which will be decoded as object.
52
+ return null
53
+ }
54
+
55
+ const encodeOptions: EncodeOptions = {
56
+ typeEncoders: {
57
+ Map: mapEncoder,
58
+ Object: cidEncoder,
59
+ undefined: undefinedEncoder,
60
+ number: numberEncoder,
61
+ },
62
+ }
63
+
64
+ function cidDecoder(bytes: Uint8Array): CID {
65
+ if (bytes[0] !== 0) {
66
+ throw new Error('Invalid CID for CBOR tag 42; expected leading 0x00')
67
+ }
68
+ return CID.decode(bytes.subarray(1)) // ignore leading 0x00
69
+ }
70
+
71
+ const tagDecoders: TagDecoder[] = []
72
+ tagDecoders[CID_CBOR_TAG] = cidDecoder
73
+ const decodeOptions: DecodeOptions = {
74
+ allowIndefinite: false,
75
+ coerceUndefinedToNull: true,
76
+ allowNaN: false,
77
+ allowInfinity: false,
78
+ allowBigInt: true,
79
+ strict: true,
80
+ useMaps: false,
81
+ rejectDuplicateMapKeys: true,
82
+ tags: tagDecoders,
83
+ }
84
+
85
+ export function encode<T extends LexValue>(data: T): ByteView<T> {
86
+ return cborgEncode(data, encodeOptions)
87
+ }
88
+
89
+ export function decode<T extends LexValue>(bytes: ByteView<T>): T {
90
+ return cborgDecode(bytes, decodeOptions)
91
+ }
92
+
93
+ export function* decodeAll<T = LexValue>(
94
+ data: ByteView<T>,
95
+ ): Generator<T, void, unknown> {
96
+ do {
97
+ const [result, remainingBytes] = cborgDecodeFirst(data, decodeOptions)
98
+ yield result
99
+ data = remainingBytes
100
+ } while (data.byteLength > 0)
101
+ }
package/src/index.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { create as createDigest } from 'multiformats/hashes/digest'
2
+ import { sha256 as hasher } from 'multiformats/hashes/sha2'
3
+ import {
4
+ CID,
5
+ DAG_CBOR_MULTICODEC,
6
+ LexValue,
7
+ RAW_BIN_MULTICODEC,
8
+ SHA2_256_MULTIHASH_CODE,
9
+ } from '@atproto/lex-data'
10
+ import { encode } from './encoding.js'
11
+
12
+ export { CID, hasher }
13
+ export { decode, decodeAll, encode } from './encoding.js'
14
+ export type { LexValue }
15
+
16
+ export async function cidForLex(value: LexValue): Promise<CID> {
17
+ return cidForCbor(encode(value))
18
+ }
19
+
20
+ export async function cidForCbor(bytes: Uint8Array): Promise<CID> {
21
+ const digest = await hasher.digest(bytes)
22
+ return CID.createV1(DAG_CBOR_MULTICODEC, digest)
23
+ }
24
+
25
+ export async function verifyCidForBytes(cid: CID, bytes: Uint8Array) {
26
+ const digest = await hasher.digest(bytes)
27
+ const expected = CID.createV1(cid.code, digest)
28
+ if (!cid.equals(expected)) {
29
+ throw new Error(
30
+ `Not a valid CID for bytes. Expected: ${expected.toString()} Got: ${cid.toString()}`,
31
+ )
32
+ }
33
+ }
34
+
35
+ export async function cidForRawBytes(bytes: Uint8Array): Promise<CID> {
36
+ const digest = await hasher.digest(bytes)
37
+ return CID.createV1(RAW_BIN_MULTICODEC, digest)
38
+ }
39
+
40
+ export function cidForRawHash(hash: Uint8Array): CID {
41
+ const digest = createDigest(hasher.code, hash)
42
+ return CID.createV1(RAW_BIN_MULTICODEC, digest)
43
+ }
44
+
45
+ /**
46
+ * @note Only supports DASL CIDs
47
+ * @see {@link https://dasl.ing/cid.html}
48
+ * @throws if the input do not represent a valid DASL {@link CID}
49
+ */
50
+ export function parseCidFromBytes(cidBytes: Uint8Array): CID {
51
+ const version = cidBytes[0]
52
+ if (version !== 0x01) {
53
+ throw new Error(`Unsupported CID version: ${version}`)
54
+ }
55
+ const code = cidBytes[1]
56
+ if (code !== RAW_BIN_MULTICODEC && code !== DAG_CBOR_MULTICODEC) {
57
+ throw new Error(`Unsupported CID codec: ${code}`)
58
+ }
59
+ const hashType = cidBytes[2]
60
+ if (hashType !== SHA2_256_MULTIHASH_CODE) {
61
+ throw new Error(`Unsupported CID hash function: ${hashType}`)
62
+ }
63
+ const hashLength = cidBytes[3]
64
+ if (hashLength !== 32) {
65
+ throw new Error(`Unexpected CID hash length: ${hashLength}`)
66
+ }
67
+ if (hashLength !== cidBytes.length - 4) {
68
+ throw new Error(`Unexpected CID bytes length: ${hashLength}`)
69
+ }
70
+ const hashBytes = cidBytes.slice(4)
71
+ const digest = createDigest(hashType, hashBytes)
72
+ return CID.create(version, code, digest)
73
+ }