@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/dist/encoding.d.ts +6 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/index.cjs.js +1707 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.es.js +1703 -0
- package/package.json +52 -0
- package/src/encoding.ts +101 -0
- package/src/index.ts +73 -0
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
|
+
}
|
package/src/encoding.ts
ADDED
|
@@ -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
|
+
}
|