├── .eslintrc.cjs ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── cit.cborld ├── cit.jsonld ├── didKey-gen.js ├── didKey.cborld ├── didKey.jsonld ├── empty-array.cborld ├── empty-array.jsonld ├── empty-object.cborld ├── empty-object.jsonld ├── note.cborld ├── note.jsonld ├── prc.cborld ├── prc.jsonld ├── uncompressible.cborld └── uncompressible.jsonld ├── karma.conf.cjs ├── lib ├── ActiveContext.js ├── CborldError.js ├── Compressor.js ├── ContextLoader.js ├── Converter.js ├── Decompressor.js ├── codecs │ ├── Base58DidUrlDecoder.js │ ├── Base58DidUrlEncoder.js │ ├── CborldDecoder.js │ ├── CborldEncoder.js │ ├── ContextDecoder.js │ ├── ContextEncoder.js │ ├── DataUrlDecoder.js │ ├── DataUrlEncoder.js │ ├── HttpUrlDecoder.js │ ├── HttpUrlEncoder.js │ ├── MultibaseDecoder.js │ ├── MultibaseEncoder.js │ ├── UrlDecoder.js │ ├── UrlEncoder.js │ ├── UuidUrnDecoder.js │ ├── UuidUrnEncoder.js │ ├── ValueDecoder.js │ ├── ValueEncoder.js │ ├── XsdDateDecoder.js │ ├── XsdDateEncoder.js │ ├── XsdDateTimeDecoder.js │ └── XsdDateTimeEncoder.js ├── decode.js ├── encode.js ├── helpers.js ├── index.js ├── parser.js ├── tables.js ├── util-browser.js └── util.js ├── package.json └── tests ├── .eslintrc.cjs ├── UuidUrnDecoder.spec.js ├── UuidUrnEncoder.spec.js ├── XsdDateDecoder.spec.js ├── XsdDateEncoder.spec.js ├── XsdDateTimeDecoder.spec.js ├── XsdDateTimeEncoder.spec.js ├── contexts └── activitystreams.jsonld ├── decode.spec.js ├── encode.spec.js ├── examples.spec.js ├── roundtrip.spec.js ├── test.legacy-range.spec.js └── test.legacy-singleton.spec.js /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'digitalbazaar', 8 | 'digitalbazaar/jsdoc', 9 | 'digitalbazaar/module' 10 | ], 11 | rules: { 12 | 'unicorn/prefer-node-protocol': 'error' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.cborld binary 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [20.x] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: npm install 18 | - name: Run eslint 19 | run: npm run lint 20 | test-node: 21 | needs: [lint] 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 10 24 | strategy: 25 | matrix: 26 | node-version: [18.x, 20.x] 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | - run: npm install 34 | - name: Run test with Node.js ${{ matrix.node-version }} 35 | run: npm run test-node 36 | test-karma: 37 | needs: [lint] 38 | runs-on: ubuntu-latest 39 | timeout-minutes: 10 40 | strategy: 41 | matrix: 42 | node-version: [20.x] 43 | steps: 44 | - uses: actions/checkout@v4 45 | - name: Use Node.js ${{ matrix.node-version }} 46 | uses: actions/setup-node@v4 47 | with: 48 | node-version: ${{ matrix.node-version }} 49 | - run: npm install 50 | - name: Run karma tests 51 | run: npm run test-karma 52 | coverage: 53 | needs: [test-node, test-karma] 54 | runs-on: ubuntu-latest 55 | timeout-minutes: 10 56 | strategy: 57 | matrix: 58 | node-version: [20.x] 59 | steps: 60 | - uses: actions/checkout@v4 61 | - name: Use Node.js ${{ matrix.node-version }} 62 | uses: actions/setup-node@v4 63 | with: 64 | node-version: ${{ matrix.node-version }} 65 | - run: npm install 66 | - name: Generate coverage report 67 | run: npm run coverage-ci 68 | - name: Upload coverage to Codecov 69 | uses: codecov/codecov-action@v4 70 | with: 71 | file: ./coverage/lcov.info 72 | fail_ci_if_error: true 73 | token: ${{ secrets.CODECOV_TOKEN }} 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.sw[nop] 3 | *~ 4 | .DS_Store 5 | .cache 6 | .nyc_output 7 | .project 8 | .settings 9 | .vscode 10 | TAGS 11 | coverage 12 | dist 13 | node_modules 14 | reports 15 | tests/testTagValues.js 16 | package-lock.json 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @digitalbazaar/cborld ChangeLog 2 | 3 | ## 8.0.1 - 2025-05-21 4 | 5 | ### Fixed 6 | - Ensure omitted `"@id"` values in term definitions are resolved using the 7 | term key if possible (for keys that are CURIEs or absolute URLs). 8 | 9 | ## 8.0.0 - 2025-04-24 10 | 11 | ### Changed 12 | - **BREAKING**: The default `format` for `encode()` is now "cbor-ld-1.0". To 13 | generate output using pre-1.0 CBOR-LD tags (legacy tags), pass a different 14 | format (e.g, "legacy-range" or "legacy-singleton"). 15 | - **BREAKING**: The `registryEntryId` parameter for `encode()` can no longer 16 | be the string "legacy"; it can only be a number. To output pre-1.0 CBOR-LD 17 | tags 1280/1281 (0x0500/0x0501) pass "legacy-singleton" for `format`. 18 | 19 | ### Removed 20 | - **BREAKING**: Remove `typeTable` parameter from `encode()` and `decode()`; 21 | use `typeTableLoader` instead. 22 | 23 | ## 7.3.0 - 2025-04-24 24 | 25 | ### Added 26 | - Add a new tag system for use with a single CBOR tag (0xcb1d). The tagged 27 | item is always an array of two elements. The first element is a CBOR integer 28 | representation of the registry entry ID for the payload, and the second 29 | element is the encoded JSON-LD. To use the new mode when encoding, pass 30 | `format: 'cbor-ld-1.0'` as an option to `encode()`. When decoding, the new 31 | mode will be automatically supported. Both the legacy tag 0x0500/01 and the 32 | legacy tag range 0x0600-0x06FF are still supported for both encoding and 33 | decoding. 34 | 35 | ## 7.2.1 - 2025-04-14 36 | 37 | ### Fixed 38 | - Fix registry entries `0` and `1`. Previously, entry `0` could not be passed 39 | without an error; it can now be used to produce output with no compression. 40 | Entry `1` could not previously be used when specifying a type table loader 41 | even though the loader should not need to return the default (empty) type 42 | table for entry `1`, which should not be overridden. The tests have also 43 | been updated to use entry `2` for custom `typeTable` entries to encourage 44 | that mechanism NOT to be used to override entry `1`, which should be 45 | prevented from being overridden entirely in a future major release. 46 | 47 | ## 7.2.0 - 2024-10-21 48 | 49 | ### Added 50 | - Add `async function typeTableLoader({registryEntryId})` option to look up the 51 | `typeTable` to use by id for both `encode` and `decode`. 52 | 53 | ### Changed 54 | - **NOTE**: The handling of `typeTable` and `typeTableLoader` is more strict 55 | than before and requies one option be used when appropriate. This could cause 56 | issues with code that was depending on undefined behavior. 57 | - Refactor `registryEntryId` encoding and decoding logic. Trying to be more 58 | readable and handle more error and edge cases. This is a work in progress. 59 | 60 | ## 7.1.3 - 2024-10-16 61 | 62 | ### Fixed 63 | - Fix varint processing when registry IDs require multiple bytes. 64 | - Fix bug with registry IDs that are expressed using varints larger than 65 | 1 byte in size. This would break any previous use of this, but that 66 | previous use is invalid and non-interoperable. When a registry entry is 67 | used that requires more than one byte, the payload is now appropriately 68 | a two element tagged array containing a bytestring and the encoded 69 | JSON-LD document instead of a sequence containing a tagged 70 | bytestring and the encoded document. 71 | 72 | ## 7.1.2 - 2024-08-13 73 | 74 | ### Fixed 75 | - Fix compression of values in arrays of arrays. 76 | - Internal refactor for simpler code, better maintenance, and better 77 | better separation of concerns. 78 | 79 | ## 7.1.1 - 2024-08-12 80 | 81 | ### Fixed 82 | - Fix property-scope processing bug that applied property-scope context 83 | without removing any type-scope context first. 84 | - Ensure error is thrown if unexpected protected term redefinition is 85 | attempted. 86 | - Ensure `@propagate` context flag is considered. 87 | - Add missing `@propagate` to keywords table. 88 | 89 | ### Fixed 90 | 91 | ## 7.1.0 - 2024-07-14 92 | 93 | ### Added 94 | - Added support for passing through (without semantic compression) terms not 95 | defined in contexts and other plain values. 96 | 97 | ### Changed 98 | - Restructure term registry system to be more general with a type table. The 99 | type table expresses type-namespaced tables of values that can be used when encoding and decoding. This type table (of tables) can be passed by the user 100 | when encoding and decoding and will be given preference over any processing- 101 | mode-specific codecs (such as multibase codecs). 102 | The supported types include: 103 | - context: for JSON-LD context URLs, this subsumes the old 'appContextMap' 104 | and any contexts expressed in the old arbitrary string table as well. 105 | - url: for any JSON-LD URL value, such as values associated with `@id` 106 | or `@type` (or their aliases) JSON keys. 107 | - none: for any untyped or plain literal values. 108 | - \: for any values associated with 109 | any custom JSON-LD types. 110 | - Restructure CBOR-LD tag system to use a range of tags where 111 | the tag value informs what values for the tables above should be used 112 | via a registry. Legacy tags (0x0501) are still supported, and new tags are 113 | in the range 0x0600-0x06FF. In addition, the tag value is part of a varint 114 | that begins the CBOR-LD paylaod if necessary that allows for more registry 115 | entries than the number of tags in that range. 116 | 117 | ## 7.0.0 - 2024-07-01 118 | 119 | ### Added 120 | - **BREAKING**: Add support for compressing RFC 2397 data URLs. Base64 encoded 121 | data is compressed as binary. 122 | - Add VCDM v2 context value. 123 | 124 | ### Changed 125 | - **BREAKING**: Encode cryptosuite strings using a separate registry (specific 126 | to cryptosuites) to further reduce their encoded size (instead of using the 127 | global table). This approach is incompatible with previous encodings that 128 | used the global table. 129 | - **BREAKING**: Encode all URIs using the vocab term codec. This allows 130 | arbitrary URIs to be encoded with the same small integers as other terms. 131 | The mapping in a context needs to use a redundant form: `{"URI":"URI"}`. This 132 | technique assume a use case where the CBOR-LD size has high priority over 133 | context size. 134 | - Improve and test examples. 135 | - Update dependencies. 136 | 137 | ### Removed 138 | - **BREAKING**: Remove and reserve cryptosuite values from term registry in 139 | favor of the new cryptosuite codec. 140 | 141 | ## 6.0.3 - 2023-12-19 142 | 143 | ### Fixed 144 | - Use `appContextMap` in `CryptosuiteEncoder`. 145 | - Ensure custom `appContextMap` is used first before registered table of term 146 | codecs in cryptosuite codecs. 147 | 148 | ## 6.0.2 - 2023-11-29 149 | 150 | ### Fixed 151 | - Ensure custom `appContextMap` is used first before registered table of 152 | term codecs. 153 | 154 | ## 6.0.1 - 2023-11-29 155 | 156 | ### Changed 157 | - **BREAKING**: Require node 18+ (current non-EOL node versions). 158 | 159 | ## 6.0.0 - 2023-11-29 160 | 161 | ### Added 162 | - **BREAKING**: Add support for compressing base64url-encoded multibase values. 163 | - **BREAKING**: Add support for compressing Data Integrity cryptosuite strings. 164 | 165 | ## 5.2.0 - 2023-11-15 166 | 167 | ### Changed 168 | - Update dependencies: 169 | - Use `cborg@4`. 170 | - Use `uuid@9`. 171 | 172 | ## 5.1.0 - 2023-11-15 173 | 174 | ### Added 175 | - Add `https://w3id.org/security/data-integrity/v2` registered context. 176 | - Add `https://purl.imsglobal.org/spec/ob/v3p0/context.json` registered context. 177 | 178 | ## 5.0.0 - 2022-06-09 179 | 180 | ### Changed 181 | - **BREAKING**: Convert to module (ESM). 182 | - **BREAKING**: Require Node.js >=14. 183 | - Update dependencies. 184 | - Lint module. 185 | 186 | ## 4.4.0 - 2022-01-27 187 | 188 | ### Added 189 | - Add `https://w3id.org/vc/status-list/v1` registered context. 190 | 191 | ## 4.3.0 - 2021-09-17 192 | 193 | ### Added 194 | - Add `https://w3id.org/dcc/v1c` registered context. 195 | 196 | ## 4.2.0 - 2021-04-22 197 | 198 | ### Added 199 | - Add `https://w3id.org/vc-revocation-list-2020/v1` registered context. 200 | 201 | ## 4.1.0 - 2021-04-22 202 | 203 | ### Added 204 | - Add latest contexts from the cborld term codec registry. 205 | 206 | ## 4.0.1 - 2021-04-16 207 | 208 | ### Fixed 209 | - **BREAKING**: Fixed broken security ed25519 suite context URLs. 210 | 211 | ## 4.0.0 - 2021-04-16 212 | 213 | ### Removed 214 | - **BREAKING**: Removed CLI, see: https://github.com/digitalbazaar/cborld-cli 215 | 216 | ### Changed 217 | - **BREAKING**: Use `cborg` as the underlying CBOR library. 218 | - **BREAKING**: Assign term IDs for defined terms in pairs using even numbers 219 | for terms with single values and odd numbers for multiple values. This 220 | approach avoids adding additional tags for multiple values and is based 221 | on the fact that JSON-LD keywords themselves will be assigned term IDs that 222 | exceed the CBOR single byte value limit of 24. Custom term IDs therefore 223 | start at 100 and will use two bytes per term up to `65536-100` custom 224 | terms. 225 | - **BREAKING**: Support embedded and scoped contexts in JSON-LD and, more 226 | generally, any JSON-LD document that has all of its terms defined. The 227 | only exceptions are contexts that use change the `@propagate` setting 228 | from its default; these are not presently supported but may be in a future 229 | version. 230 | - **BREAKING**: Treat the last 8-bits of the CBOR-LD tag as a compression 231 | mode with support for mode=0 (no compression) or mode=1 (compression as 232 | implemented with the above term ID rules, sorting, etc. -- to be updated 233 | in the spec). Future compression modes can be added to support other 234 | algorithms. 235 | - **BREAKING**: Type encoding depends on `@type` in term definitions and does 236 | not require a CBOR Map to be used when encoding values. If a value cannot 237 | be encoded to match the `@type`, it is the encoders job to provide a 238 | default uncompressed value that is distinguishable from a compressed one. For 239 | example, a compressed `@type` value for `foo` may use a `Uint8Array` but an 240 | uncompressed value may use any other major CBOR type. 241 | 242 | ## 3.1.1 - 2021-03-30 243 | 244 | ### Fixed 245 | - Update to latest `@digitalbazaar/cbor` version that has the `ignoreBOM` fix. 246 | 247 | ## 3.1.0 - 2021-03-29 248 | ### Added 249 | - Add entry for X25519 2020 crypto suite context codec. 250 | 251 | ## 3.0.0 - 2021-03-24 252 | 253 | ### Changed 254 | - **BREAKING**: Use `@digitalbazaar/cbor` as a temporary measure to get better 255 | Web/browser compatiblity for cbor-ld. The plan is to switch to `cborg` 256 | library in another subsequent major release. 257 | - **BREAKING**: Temporarily disable `diagnose` mode. This will be re-enabled 258 | in some way once the transition to the `cborg` library is complete. 259 | - **BREAKING**: Disabled errors that throw when using an app context map with 260 | tag numbers from the registry. This is currently allowed to enable people 261 | to use the library even when a new version hasn't been released that supports 262 | newly registered tags. We'll explore if this is right path forward or if 263 | we need to do something else in the future. 264 | 265 | ### Removed 266 | - **BREAKING**: Removed support for node 10.x. Node 10.x reaches EOL at 267 | the end of April 2021 (a little over 30 days from the time this entry was 268 | written) and it is lacking in features required to run `@digitalbazaar/cbor`. Support 269 | is therefore being removed. 270 | 271 | ## 2.2.0 - 2021-03-22 272 | 273 | ### Added 274 | - Add support for compressing `xsd:date` types. 275 | 276 | ## 2.1.0 - 2021-02-20 277 | 278 | ### Changed 279 | - Use `match-all` for compatibility with Node.js <12. 280 | - **NOTE**: The required Node.js version may be increased to v12 sometime after 281 | v10 hits end-of-life. The above `match-all` support may be removed at that 282 | time. 283 | 284 | ## Fixed 285 | - UrlCodec decode dropped base58 multibase prefix. 286 | 287 | ## 2.0.1 - 2020-10-21 288 | 289 | ### Fixed 290 | - Get entries based on cborMap type. 291 | 292 | ## 2.0.0 - 2020-08-18 293 | 294 | ### Fixed 295 | - Handle decoding of encoded empty object. 296 | - Refactoring to allow simple karma tests to pass. 297 | 298 | ### Changed 299 | - **BREAKING**: Move documentLoader towards alignment with JSON-LD spec. Allows 300 | reuse of jsonld.js document loaders. 301 | 302 | ## 1.0.0 - 2020-07-23 303 | 304 | ### Note 305 | - Browser support unfinished in this release. 306 | 307 | ### Added 308 | - Add core files. 309 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Digital Bazaar, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript CBOR-LD Processor 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/digitalbazaar/cborld/main.yml)](https://github.com/digitalbazaar/cborld/actions/workflows/main.yml) 4 | [![Coverage Status](https://img.shields.io/codecov/c/github/digitalbazaar/cborld)](https://codecov.io/gh/digitalbazaar/cborld) 5 | 6 | > A JavaScript CBOR-LD Process for Web browsers and Node.js apps. 7 | 8 | ## Table of Contents 9 | 10 | - [Background](#background) 11 | - [Install](#install) 12 | - [Usage](#usage) 13 | - [API](#api) 14 | - [Contribute](#contribute) 15 | - [Commercial Support](#commercial-support) 16 | - [License](#license) 17 | 18 | ## Background 19 | 20 | This library provides a CBOR-LD Processor for Web browsers and Node.js 21 | applications. 22 | 23 | ## Install 24 | 25 | - Browsers and Node.js 18+ are supported. 26 | 27 | ### NPM 28 | 29 | ``` 30 | npm install @digitalbazaar/cborld 31 | ``` 32 | 33 | ### Git 34 | 35 | To install locally (for development): 36 | 37 | ``` 38 | git clone https://github.com/digitalbazaar/cborld.git 39 | cd cborld 40 | npm install 41 | ``` 42 | 43 | ## Usage 44 | 45 | This library provides two primary functions for encoding and decoding 46 | CBOR-LD data. 47 | 48 | ### Encode to CBOR-LD 49 | 50 | To encode a JSON-LD document as CBOR-LD: 51 | 52 | ```js 53 | import {encode} from '@digitalbazaar/cborld'; 54 | 55 | const jsonldDocument = { 56 | '@context': 'https://www.w3.org/ns/activitystreams', 57 | type: 'Note', 58 | summary: 'CBOR-LD', 59 | content: 'CBOR-LD is awesome!' 60 | }; 61 | 62 | // encode a JSON-LD Javascript object into CBOR-LD bytes 63 | // Note: user must provide their own JSON-LD `documentLoader` 64 | const cborldBytes = await encode({ 65 | jsonldDocument, 66 | documentLoader, 67 | // use standard compression (set to `0` to use no compression) 68 | registryEntryId: 1 69 | }); 70 | ``` 71 | 72 | To decode a CBOR-LD document to JSON-LD: 73 | 74 | ```js 75 | import {decode} from '@digitalbazaar/cborld'; 76 | 77 | // get the CBOR-LD bytes 78 | const cborldBytes = await fs.promises.readFile('out.cborld'); 79 | 80 | // decode the CBOR-LD bytes into a Javascript object 81 | // Note: user must provide their own JSON-LD `documentLoader` 82 | const jsonldDocument = await cborld.decode({cborldBytes, documentLoader}); 83 | ``` 84 | 85 | ## API 86 | 87 | **NOTE**: Please check `encode.js` and `decode.js` for the latest API options. 88 | 89 | ### Functions 90 | 91 |
92 |
encode(options)Uint8Array
93 |
94 |

Encodes a given JSON-LD document into a CBOR-LD byte array.

95 |
96 |
decode(options)object
97 |
98 |

Decodes a CBOR-LD byte array into a JSON-LD document.

99 |
100 |
101 | 102 | ### Typedefs 103 | 104 |
diagnosticFunction : 105 | function
106 |
107 |

A diagnostic function that is called with 108 | diagnostic information. Typically set to console.log when 109 | debugging.

110 |
documentLoaderFunction ⇒ 111 | string
112 |
113 |

Fetches a resource given a URL and returns it 114 | as a string.

115 |
116 |
117 | 118 | 119 | 120 | ### encode(options) ⇒ Promise<Uint8Array> 121 | Encodes a given JSON-LD document into a CBOR-LD byte array. 122 | 123 | **Kind**: global function 124 | **Returns**: Uint8Array - - The encoded CBOR-LD bytes. 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 141 | 142 | 143 | 144 | 145 | 149 | 150 | 151 | 152 | 153 | 156 | 157 | 158 | 159 | 160 | 164 | 165 | 166 | 167 | 168 | 172 | 173 | 174 | 175 | 176 | 180 | 181 | 182 |
ParamTypeDescription
optionsobject 138 |

The 139 | options to use when encoding to CBOR-LD.

140 |
options.jsonldDocumentobject 146 |

The JSON-LD 147 | Document to convert to CBOR-LD bytes.

148 |
options.documentLoaderdocumentLoaderFunction 154 |

The document loader to use when resolving JSON-LD Context URLs.

155 |
[options.appContextMap]Map 161 |

A map of JSON-LD Context URLs and their encoded CBOR-LD values 162 | (must be values greater than 32767 (0x7FFF)).

163 |
[options.appTermMap]Map 169 |

A map of JSON-LD terms and their associated 170 | CBOR-LD term codecs.

171 |
[options.diagnose]diagnosticFunction 177 |

A function that, if provided, is called with diagnostic 178 | information.

179 |
183 | 184 | 185 | ### decode(options) ⇒ Promise<object> 186 | Decodes a CBOR-LD byte array into a JSON-LD document. 187 | 188 | **Kind**: global function 189 | **Returns**: object - - The decoded JSON-LD Document. 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 205 | 206 | 207 | 208 | 209 | 213 | 214 | 215 | 216 | 217 | 221 | 222 | 223 | 224 | 225 | 230 | 231 | 232 | 233 | 234 | 238 | 239 | 240 | 241 | 242 | 246 | 247 | 248 |
ParamTypeDescription
optionsobject 203 |

The options to use when decoding CBOR-LD.

204 |
options.cborldBytesUint8Array 210 |

The encoded CBOR-LD bytes to 211 | decode.

212 |
options.documentLoaderfunction 218 |

The document loader to use when 219 | resolving JSON-LD Context URLs.

220 |
[options.appContextMap]Map 226 |

A map of JSON-LD Context URLs and 227 | their associated CBOR-LD values. The values must be greater than 228 | 32767 (0x7FFF)).

229 |
[options.appTermMap]Map 235 |

A map of JSON-LD terms and 236 | their associated CBOR-LD term codecs.

237 |
[options.diagnose]diagnosticFunction 243 |

A function that, if 244 | provided, is called with diagnostic information.

245 |
249 | 250 | 251 | ### diagnosticFunction : function 252 | A diagnostic function that is called with diagnostic information. Typically 253 | set to `console.log` when debugging. 254 | 255 | **Kind**: global typedef 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 266 | 267 |
ParamTypeDescription
messagestring

The diagnostic message.

265 |
268 | 269 | 270 | 271 | ### documentLoaderFunction ⇒ string 272 | Fetches a resource given a URL and returns it as a string. 273 | 274 | **Kind**: global typedef 275 | **Returns**: string - The resource associated with the URL as a string. 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 291 | 292 | 293 |
ParamTypeDescription
urlstring 289 |

The URL to retrieve.

290 |
294 | 295 | Examples: 296 | 297 | ``` 298 | TBD 299 | ``` 300 | 301 | ## Contribute 302 | 303 | Please follow the existing code style. 304 | 305 | PRs accepted. 306 | 307 | If editing the README, please conform to the 308 | [standard-readme](https://github.com/RichardLitt/standard-readme) specification. 309 | 310 | ## Commercial Support 311 | 312 | Commercial support for this library is available upon request from 313 | Digital Bazaar: support@digitalbazaar.com 314 | 315 | ## License 316 | 317 | [BSD-3-Clause](LICENSE.md) © Digital Bazaar 318 | -------------------------------------------------------------------------------- /examples/cit.cborld: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalbazaar/cborld/349f9b7d5d7b1d2b7e1580c4d0d783d9829c906e/examples/cit.cborld -------------------------------------------------------------------------------- /examples/cit.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/2018/credentials/v1", 3 | "type": "VerifiablePresentation", 4 | "verifiableCredential": { 5 | "@context": [ 6 | "https://www.w3.org/2018/credentials/v1", 7 | "https://w3id.org/security/suites/ed25519-2020/v1", 8 | "https://w3id.org/cit/v1" 9 | ], 10 | "id": "urn:uuid:188e8450-269e-11eb-b545-d3692cf35398", 11 | "type": ["VerifiableCredential", "ConcealedIdTokenCredential"], 12 | "issuer": "did:key:z6MkhNZxXHvf4YMbtZkEkgA9QAz6gN8f9ZtP47EdCEJMF5Hh", 13 | "issuanceDate": "2020-07-14T19:23:24Z", 14 | "expirationDate": "2020-08-14T19:23:24Z", 15 | "credentialSubject": { 16 | "concealedIdToken": "z2BJYfNtmWRiouWhDrbDQmC2zicUkN66gCMqtFaW5ioLB4CrdZM1dwFfb8tJaZTYnE2XWez3PQbW3PgdP6y8JueQUk6vSqV71wfyMAGmGqFvwC1VxoN4Bt" 17 | }, 18 | "proof": { 19 | "type": "Ed25519Signature2020", 20 | "created": "2020-02-03T17:23:49Z", 21 | "proofValue": "z4peo48uwK2EF4Fta8PbpFnqsq5ehpVhx2P9bXixhHGWjZccPRWHYpyZF4vzZGD9r8zeSovgkhgKHzQMDYJ34r9gL", 22 | "proofPurpose": "assertionMethod", 23 | "verificationMethod": "did:key:z6MkhNZxXHvf4YMbtZkEkgA9QAz6gN8f9ZtP47EdCEJMF5Hh#z6MkexTPNH9NFaSogyN17dQhm1X5e9ZTgmChE2enWG8r1Tep" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/didKey-gen.js: -------------------------------------------------------------------------------- 1 | // generate example did key document 2 | 3 | import * as didMethodKey from '@digitalbazaar/did-method-key'; 4 | import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey'; 5 | 6 | const didKeyDriver = didMethodKey.driver(); 7 | didKeyDriver.use({ 8 | multibaseMultikeyHeader: 'z6Mk', 9 | fromMultibase: Ed25519Multikey.from 10 | }); 11 | 12 | const verificationKeyPair = await Ed25519Multikey.generate(); 13 | const {didDocument} = await didKeyDriver.fromKeyPair({verificationKeyPair}); 14 | console.log(JSON.stringify(didDocument, null, 2)); 15 | 16 | -------------------------------------------------------------------------------- /examples/didKey.cborld: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalbazaar/cborld/349f9b7d5d7b1d2b7e1580c4d0d783d9829c906e/examples/didKey.cborld -------------------------------------------------------------------------------- /examples/didKey.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "https://www.w3.org/ns/did/v1", 4 | "https://w3id.org/security/multikey/v1" 5 | ], 6 | "id": "did:key:z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv", 7 | "verificationMethod": [ 8 | { 9 | "id": "did:key:z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv#z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv", 10 | "type": "Multikey", 11 | "controller": "did:key:z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv", 12 | "publicKeyMultibase": "z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv" 13 | } 14 | ], 15 | "authentication": [ 16 | "did:key:z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv#z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv" 17 | ], 18 | "assertionMethod": [ 19 | "did:key:z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv#z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv" 20 | ], 21 | "capabilityDelegation": [ 22 | "did:key:z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv#z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv" 23 | ], 24 | "capabilityInvocation": [ 25 | "did:key:z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv#z6Mkw5LtCiW1Dn7dopLTqtcczWoBTQiBQLP6FrZeRYuvtAWv" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/empty-array.cborld: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalbazaar/cborld/349f9b7d5d7b1d2b7e1580c4d0d783d9829c906e/examples/empty-array.cborld -------------------------------------------------------------------------------- /examples/empty-array.jsonld: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /examples/empty-object.cborld: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalbazaar/cborld/349f9b7d5d7b1d2b7e1580c4d0d783d9829c906e/examples/empty-object.cborld -------------------------------------------------------------------------------- /examples/empty-object.jsonld: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/note.cborld: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalbazaar/cborld/349f9b7d5d7b1d2b7e1580c4d0d783d9829c906e/examples/note.cborld -------------------------------------------------------------------------------- /examples/note.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "type": "Note", 4 | "summary": "A note", 5 | "content": "This is an example note." 6 | } 7 | -------------------------------------------------------------------------------- /examples/prc.cborld: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalbazaar/cborld/349f9b7d5d7b1d2b7e1580c4d0d783d9829c906e/examples/prc.cborld -------------------------------------------------------------------------------- /examples/prc.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "https://www.w3.org/2018/credentials/v1", 4 | "https://w3id.org/citizenship/v1" 5 | ], 6 | "id": "https://issuer.oidp.uscis.gov/credentials/83627465", 7 | "type": ["VerifiableCredential", "PermanentResidentCard"], 8 | "issuer": "did:example:28394728934792387", 9 | "identifier": "83627465", 10 | "name": "Permanent Resident Card", 11 | "description": "Government of Example Permanent Resident Card.", 12 | "issuanceDate": "2019-12-03T12:19:52Z", 13 | "expirationDate": "2029-12-03T12:19:52Z", 14 | "credentialSubject": { 15 | "id": "did:example:b34ca6cd37bbf23", 16 | "type": ["PermanentResident", "Person"], 17 | "givenName": "JOHN", 18 | "familyName": "SMITH", 19 | "gender": "Male", 20 | "image": "data:image/png;base64,iVBORw0KGgo...kJggg==", 21 | "residentSince": "2015-01-01", 22 | "lprCategory": "C09", 23 | "lprNumber": "999-999-999", 24 | "commuterClassification": "C1", 25 | "birthCountry": "Bahamas", 26 | "birthDate": "1958-07-17" 27 | }, 28 | "proof": { 29 | "type": "Ed25519Signature2018", 30 | "created": "2020-02-03T17:23:49Z", 31 | "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..AUQ3AJ23WM5vMOWNtYKuqZBekRAOUibOMH9XuvOd39my1sO-X9R4QyAXLD2ospssLvIuwmQVhJa-F0xMOnkvBg", 32 | "proofPurpose": "assertionMethod", 33 | "verificationMethod": "did:example:z6MkhNZxXHvf4YMbtZkEkgA9QAz6gN8f9ZtP47EdCEJMF5Hh#z6MkexTPNH9NFaSogyN17dQhm1X5e9ZTgmChE2enWG8r1Tep" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/uncompressible.cborld: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalbazaar/cborld/349f9b7d5d7b1d2b7e1580c4d0d783d9829c906e/examples/uncompressible.cborld -------------------------------------------------------------------------------- /examples/uncompressible.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "https://www.w3.org/ns/activitystreams", 4 | { 5 | "uncompressible": "https://example.com/vocabs#uncompressible" 6 | } 7 | ], 8 | "type": "Note", 9 | "summary": "A note", 10 | "content": "This is an example note.", 11 | "uncompressible": "This is defined inline and is uncompressible." 12 | } 13 | -------------------------------------------------------------------------------- /karma.conf.cjs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | module.exports = function(config) { 5 | 6 | config.set({ 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['mocha', 'chai'], 13 | 14 | // list of files / patterns to load in the browser 15 | files: [ 16 | 'tests/*.spec.js' 17 | ], 18 | 19 | // list of files to exclude 20 | exclude: [ 21 | 'tests/examples.spec.js' 22 | ], 23 | 24 | // preprocess matching files before serving them to the browser 25 | // preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 26 | preprocessors: { 27 | 'tests/*.js': ['webpack', 'sourcemap'] 28 | }, 29 | 30 | webpack: { 31 | mode: 'development', 32 | devtool: 'inline-source-map' 33 | }, 34 | 35 | // test results reporter to use 36 | // possible values: 'dots', 'progress' 37 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 38 | //reporters: ['progress'], 39 | reporters: ['mocha'], 40 | 41 | // web server port 42 | port: 9876, 43 | 44 | // enable / disable colors in the output (reporters and logs) 45 | colors: true, 46 | 47 | // level of logging 48 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || 49 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 50 | logLevel: config.LOG_INFO, 51 | 52 | // enable / disable watching file and executing tests whenever any 53 | // file changes 54 | autoWatch: false, 55 | 56 | // start these browsers 57 | // browser launchers: https://npmjs.org/browse/keyword/karma-launcher 58 | //browsers: ['ChromeHeadless', 'Chrome', 'Firefox', 'Safari'], 59 | browsers: ['ChromeHeadless'], 60 | 61 | // Continuous Integration mode 62 | // if true, Karma captures browsers, runs the tests and exits 63 | singleRun: true, 64 | 65 | // Concurrency level 66 | // how many browser should be started simultaneous 67 | concurrency: Infinity, 68 | 69 | // Mocha 70 | client: { 71 | mocha: { 72 | // increase from default 2s 73 | timeout: 10000, 74 | reporter: 'html' 75 | //delay: true 76 | } 77 | } 78 | }); 79 | }; 80 | -------------------------------------------------------------------------------- /lib/ActiveContext.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2025 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {CborldError} from './CborldError.js'; 5 | 6 | export class ActiveContext { 7 | constructor({ 8 | termMap = new Map(), 9 | previous, 10 | contextLoader = previous?.contextLoader, 11 | } = {}) { 12 | this.termMap = termMap; 13 | this.previous = previous; 14 | this.contextLoader = contextLoader; 15 | 16 | // compute all type terms (`@type` and aliases) 17 | this.typeTerms = ['@type']; 18 | for(const [term, def] of termMap) { 19 | if(def['@id'] === '@type') { 20 | this.typeTerms.push(term); 21 | } 22 | } 23 | } 24 | 25 | /** 26 | * Apply the embedded JSON-LD contexts in the given object to produce a new 27 | * active context which can be used to get type information about the object 28 | * prior to applying any type-scoped contexts. 29 | * 30 | * @param {object} options - The options to use. 31 | * @param {object} options.obj - The object to get the active context for. 32 | * 33 | * @returns {Promise} - The active context instance. 34 | */ 35 | async applyEmbeddedContexts({obj}) { 36 | // add any local embedded contexts to active term map 37 | const termMap = await _updateTermMap({ 38 | activeTermMap: this.termMap, 39 | contexts: obj['@context'], 40 | contextLoader: this.contextLoader 41 | }); 42 | return new ActiveContext({termMap, previous: this}); 43 | } 44 | 45 | /** 46 | * Apply any property-scoped JSON-LD context associated with the given term 47 | * to produce a new active context that can be used with the values 48 | * associated with the term. 49 | * 50 | * @param {object} options - The options to use. 51 | * @param {string} options.term - The term to get the active context for. 52 | * 53 | * @returns {Promise} - The active context instance. 54 | */ 55 | async applyPropertyScopedContext({term}) { 56 | // always revert active context's term map when recursing into a property 57 | // to remove any non-propagating terms 58 | const termMap = await _updateTermMap({ 59 | activeTermMap: _revertTermMap({activeCtx: this}), 60 | // get context from current active context (not reverted one) 61 | contexts: this.termMap.get(term)?.['@context'], 62 | contextLoader: this.contextLoader, 63 | propertyScope: true 64 | }); 65 | return new ActiveContext({termMap, previous: this}); 66 | } 67 | 68 | /** 69 | * Apply any type-scoped JSON-LD contexts associated with the given object 70 | * types to produce a new active context that can be used to get the term 71 | * information for each key in the object. 72 | * 73 | * @param {object} options - The options to use. 74 | * @param {Set} options.objectTypes - The set of object types (strings) to 75 | * to use to produce the new active context. 76 | * 77 | * @returns {Promise} - The active context instance. 78 | */ 79 | async applyTypeScopedContexts({objectTypes}) { 80 | // apply type-scoped contexts in lexicographically type-sorted order 81 | // (per JSON-LD spec) 82 | objectTypes = [...objectTypes].sort(); 83 | let {termMap} = this; 84 | for(const type of objectTypes) { 85 | termMap = await _updateTermMap({ 86 | activeTermMap: termMap, 87 | contexts: termMap.get(type)?.['@context'], 88 | contextLoader: this.contextLoader, 89 | typeScope: true 90 | }); 91 | } 92 | return new ActiveContext({termMap, previous: this}); 93 | } 94 | 95 | getIdForTerm({term, plural}) { 96 | return this.contextLoader.getIdForTerm({term, plural}); 97 | } 98 | 99 | getTermDefinition({term}) { 100 | return this.termMap.get(term) ?? {}; 101 | } 102 | 103 | getTermInfo({id}) { 104 | // get term and term definition 105 | const {term, plural} = this.contextLoader.getTermForId({id}); 106 | const def = this.getTermDefinition({term}); 107 | return {term, termId: id, plural, def}; 108 | } 109 | 110 | getTypeTerms() { 111 | return this.typeTerms; 112 | } 113 | } 114 | 115 | function _deepEqual(obj1, obj2, top = false) { 116 | const isObject1 = obj1 && typeof obj1 === 'object'; 117 | const isObject2 = obj2 && typeof obj2 === 'object'; 118 | if(isObject1 !== isObject2) { 119 | return false; 120 | } 121 | if(!isObject1) { 122 | return obj1 === obj2; 123 | } 124 | const keys1 = Object.keys(obj1); 125 | const keys2 = Object.keys(obj2); 126 | if(keys1.length !== keys2.length) { 127 | return false; 128 | } 129 | for(const k of keys1) { 130 | if(top && (k === 'protected' || k === 'propagate')) { 131 | continue; 132 | } 133 | if(!_deepEqual(obj1[k], obj2[k])) { 134 | return false; 135 | } 136 | } 137 | return true; 138 | } 139 | 140 | function _resolveCurie({activeTermMap, context, possibleCurie}) { 141 | if(possibleCurie === undefined || !possibleCurie.includes(':')) { 142 | return possibleCurie; 143 | } 144 | // check for potential CURIE values 145 | const [prefix, ...suffix] = possibleCurie.split(':'); 146 | const prefixDef = context[prefix] ?? activeTermMap.get(prefix); 147 | if(prefixDef === undefined) { 148 | // no CURIE 149 | return possibleCurie; 150 | } 151 | // handle CURIE 152 | const id = typeof prefixDef === 'string' ? prefixDef : prefixDef['@id']; 153 | possibleCurie = id + suffix.join(':'); 154 | return _resolveCurie({activeTermMap, context, possibleCurie}); 155 | } 156 | 157 | function _resolveCuries({activeTermMap, context, newTermMap}) { 158 | for(const [key, def] of newTermMap.entries()) { 159 | const id = def['@id']; 160 | const type = def['@type']; 161 | if(id !== undefined) { 162 | def['@id'] = _resolveCurie({ 163 | activeTermMap, context, possibleCurie: id 164 | }); 165 | } else { 166 | // if `key` is a CURIE/absolute URL, then "@id" can be computed 167 | const resolved = _resolveCurie({ 168 | activeTermMap, context, possibleCurie: key 169 | }); 170 | if(resolved.includes(':')) { 171 | def['@id'] = resolved; 172 | } 173 | } 174 | if(type !== undefined) { 175 | def['@type'] = _resolveCurie({ 176 | activeTermMap, context, possibleCurie: type 177 | }); 178 | } 179 | if(typeof def['@id'] !== 'string') { 180 | throw new CborldError( 181 | 'ERR_INVALID_TERM_DEFINITION', 182 | `Invalid JSON-LD term definition for "${key}"; the "@id" value ` + 183 | 'could not be determined.'); 184 | } 185 | } 186 | } 187 | 188 | function _revertTermMap({activeCtx}) { 189 | const newTermMap = new Map(); 190 | 191 | // keep every propagating term 192 | const {termMap} = activeCtx; 193 | const nonPropagating = []; 194 | for(const [term, def] of termMap) { 195 | if(!def.propagate) { 196 | nonPropagating.push(term); 197 | continue; 198 | } 199 | newTermMap.set(term, def); 200 | } 201 | 202 | // revert every non-propagating term 203 | for(const term of nonPropagating) { 204 | let currentCtx = activeCtx; 205 | let def; 206 | do { 207 | currentCtx = currentCtx.previous; 208 | def = currentCtx.termMap.get(term); 209 | } while(def !== undefined && !def.propagate); 210 | if(def !== undefined) { 211 | newTermMap.set(term, def); 212 | } 213 | } 214 | 215 | return newTermMap; 216 | } 217 | 218 | async function _updateTermMap({ 219 | activeTermMap, contexts = [{}], contextLoader, 220 | propertyScope = false, typeScope = false 221 | }) { 222 | // normalize new contexts to an array 223 | if(!Array.isArray(contexts)) { 224 | contexts = [contexts]; 225 | } 226 | 227 | // set flags based on context scope 228 | const allowProtectedOverride = propertyScope; 229 | const propagateDefault = typeScope ? false : true; 230 | 231 | // load each context 232 | for(let context of contexts) { 233 | // load and get newly resolved context 234 | const entry = await contextLoader.load({context}); 235 | ({context} = entry); 236 | 237 | // clone entry `termMap` for creating new active context 238 | const propagate = context['@propagate'] ?? propagateDefault; 239 | // shallow-copy term definitions and set `propagate` value 240 | const newTermMap = new Map([...entry.termMap.entries()].map( 241 | ([k, v]) => [k, {...v, propagate}])); 242 | 243 | // resolve any CURIE values in definitions 244 | _resolveCuries({activeTermMap, context, newTermMap}); 245 | 246 | // update new terms map based on existing `activeTermMap` 247 | for(const [term, activeDef] of activeTermMap) { 248 | let def = newTermMap.get(term); 249 | if(def !== undefined) { 250 | // disallow overriding of protected terms unless explicitly permitted 251 | if(activeDef.protected) { 252 | if(!allowProtectedOverride && !_deepEqual(def, activeDef, true)) { 253 | throw new CborldError( 254 | 'ERR_PROTECTED_TERM_REDEFINITION', 255 | `Unexpected redefinition of protected term "${term}".`); 256 | } 257 | // ensure `def` remains protected, propagation can change as it 258 | // does not affect protection 259 | def = {...activeDef, propagate: def.propagate}; 260 | } 261 | } else if(context[term] !== null) { 262 | // since `context` does not explictly clear `key`, copy it 263 | newTermMap.set(term, {...activeDef}); 264 | } 265 | } 266 | 267 | // update active term map 268 | activeTermMap = newTermMap; 269 | } 270 | 271 | return activeTermMap; 272 | } 273 | -------------------------------------------------------------------------------- /lib/CborldError.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | export class CborldError extends Error { 5 | constructor(code, message) { 6 | super(message); 7 | this.code = code; 8 | // backwards compatibility, `this.value` 9 | this.value = code; 10 | this.stack = (new Error(`${code}: ${message}`)).stack; 11 | this.name = this.constructor.name; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/Compressor.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {ContextEncoder} from './codecs/ContextEncoder.js'; 5 | import {KEYWORDS_TABLE} from './tables.js'; 6 | import {ValueEncoder} from './codecs/ValueEncoder.js'; 7 | 8 | const CONTEXT_TERM_ID = KEYWORDS_TABLE.get('@context'); 9 | const CONTEXT_TERM_ID_PLURAL = CONTEXT_TERM_ID + 1; 10 | 11 | export class Compressor { 12 | /** 13 | * Creates a new Compressor for generating compressed CBOR-LD from a 14 | * JSON-LD document. The created instance may only be used on a single 15 | * JSON-LD document at a time. 16 | * 17 | * @param {object} options - The options to use. 18 | * @param {Map} options.typeTable - A map of possible value types, including 19 | * `context`, `url`, `none`, and any JSON-LD type, each of which maps to 20 | * another map of values of that type to their associated CBOR-LD integer 21 | * values. 22 | */ 23 | constructor({typeTable} = {}) { 24 | this.typeTable = typeTable; 25 | } 26 | 27 | addOutputEntry({termInfo, values, output}) { 28 | output.set(termInfo.termId, values); 29 | } 30 | 31 | async convertContexts({activeCtx, input: obj, output}) { 32 | activeCtx = activeCtx.applyEmbeddedContexts({obj}); 33 | 34 | // if no `@context` is present, return early 35 | const context = obj['@context']; 36 | if(!context) { 37 | return activeCtx; 38 | } 39 | 40 | // encode `@context`... 41 | const {typeTable} = this; 42 | const encodedContexts = []; 43 | const isArray = Array.isArray(context); 44 | const contexts = isArray ? context : [context]; 45 | for(const value of contexts) { 46 | const encoder = ContextEncoder.createEncoder({value, typeTable}); 47 | encodedContexts.push(encoder || value); 48 | } 49 | const id = isArray ? CONTEXT_TERM_ID_PLURAL : CONTEXT_TERM_ID; 50 | output.set(id, isArray ? encodedContexts : encodedContexts[0]); 51 | 52 | return activeCtx; 53 | } 54 | 55 | convertValue({termType, value, termInfo, converter}) { 56 | if(typeof value === 'object') { 57 | return; 58 | } 59 | return ValueEncoder.createEncoder({value, converter, termInfo, termType}); 60 | } 61 | 62 | createNewOutput() { 63 | return new Map(); 64 | } 65 | 66 | getInputEntries({activeCtx, input}) { 67 | // get input entries to be converted and sort by *term* to ensure term 68 | // IDs will be assigned in the same order that the decompressor will 69 | const entries = []; 70 | const keys = Object.keys(input).sort(); 71 | for(const key of keys) { 72 | // skip `@context`; context already converted early 73 | if(key === '@context') { 74 | continue; 75 | } 76 | // create term info 77 | const value = input[key]; 78 | const plural = Array.isArray(value); 79 | const termId = activeCtx.getIdForTerm({term: key, plural}); 80 | const def = activeCtx.getTermDefinition({term: key}); 81 | entries.push([{term: key, termId, plural, def}, value]); 82 | } 83 | return entries; 84 | } 85 | 86 | getObjectTypes({activeCtx, input}) { 87 | const objectTypes = new Set(); 88 | const typeTerms = activeCtx.getTypeTerms(); 89 | for(const term of typeTerms) { 90 | const types = input[term]; 91 | if(types !== undefined) { 92 | if(Array.isArray(types)) { 93 | types.forEach(objectTypes.add, objectTypes); 94 | } else { 95 | objectTypes.add(types); 96 | } 97 | } 98 | } 99 | return objectTypes; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/ContextLoader.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2025 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {FIRST_CUSTOM_TERM_ID, KEYWORDS_TABLE, reverseMap} from './tables.js'; 5 | import {CborldError} from './CborldError.js'; 6 | 7 | export class ContextLoader { 8 | /** 9 | * Creates a new ContextLoader. 10 | * 11 | * @param {object} options - The options to use. 12 | * @param {documentLoaderFunction} options.documentLoader -The document 13 | * loader to use when resolving JSON-LD Context URLs. 14 | * @param {boolean} [options.buildReverseMap=false] - `true` to build a 15 | * reverse map, `false` not to. 16 | */ 17 | constructor({documentLoader, buildReverseMap = false} = {}) { 18 | this.documentLoader = documentLoader; 19 | this.contextMap = new Map(); 20 | this.nextTermId = FIRST_CUSTOM_TERM_ID; 21 | this.termToId = new Map(KEYWORDS_TABLE); 22 | if(buildReverseMap) { 23 | this.idToTerm = reverseMap(KEYWORDS_TABLE); 24 | } 25 | } 26 | 27 | getIdForTerm({term, plural = false}) { 28 | // check `termToId` table 29 | const id = this.termToId.get(term); 30 | if(id === undefined) { 31 | // return uncompressed `term` as-is 32 | return term; 33 | } 34 | return plural ? id + 1 : id; 35 | } 36 | 37 | getTermForId({id}) { 38 | if(typeof id === 'string') { 39 | // dynamically generate term info for uncompressed term; `plural` 40 | // as `false` will cause the associated value to pass through 41 | // without any valence-related modifications 42 | return {term: id, plural: false}; 43 | } 44 | const plural = (id & 1) === 1; 45 | const term = this.idToTerm.get(plural ? id - 1 : id); 46 | if(term === undefined) { 47 | throw new CborldError( 48 | 'ERR_UNKNOWN_CBORLD_TERM_ID', 49 | `Unknown term ID "${id}" was detected in the CBOR-LD input.`); 50 | } 51 | return {term, plural}; 52 | } 53 | 54 | hasTermId({id}) { 55 | return this.idToTerm.has(id); 56 | } 57 | 58 | async load({context}) { 59 | const entry = this.contextMap.get(context); 60 | if(entry) { 61 | // already loaded, return it 62 | return entry; 63 | } 64 | let ctx = context; 65 | let contextUrl; 66 | if(typeof context === 'string') { 67 | // fetch context 68 | contextUrl = context; 69 | ({'@context': ctx} = await this._getDocument({url: contextUrl})); 70 | } 71 | // FIXME: validate `ctx` to ensure its a valid JSON-LD context value 72 | // add context 73 | return this._addContext({context: ctx, contextUrl}); 74 | } 75 | 76 | async _addContext({context, contextUrl}) { 77 | const {contextMap, termToId, idToTerm} = this; 78 | 79 | // handle `@import` 80 | const importUrl = context['@import']; 81 | if(importUrl) { 82 | let importEntry = contextMap.get(importUrl); 83 | if(!importEntry) { 84 | const {'@context': importCtx} = await this._getDocument({ 85 | url: importUrl 86 | }); 87 | importEntry = await this._addContext({ 88 | context: importCtx, contextUrl: importUrl 89 | }); 90 | } 91 | context = {...importEntry.context, ...context}; 92 | } 93 | 94 | // create context entry 95 | const termMap = new Map(); 96 | const entry = {context, termMap}; 97 | 98 | // process context keys in sorted order to ensure term IDs are assigned 99 | // consistently 100 | const keys = Object.keys(context).sort(); 101 | const isProtected = !!context['@protected']; 102 | for(const key of keys) { 103 | if(KEYWORDS_TABLE.has(key)) { 104 | // skip `KEYWORDS_TABLE` to avoid adding unnecessary term defns 105 | continue; 106 | } 107 | 108 | let def = context[key]; 109 | if(def === null) { 110 | // no term definition 111 | continue; 112 | } 113 | 114 | // normalize definition to an object 115 | if(typeof def === 'string') { 116 | def = {'@id': def}; 117 | } else if(Object.prototype.toString.call(def) !== '[object Object]') { 118 | throw new CborldError( 119 | 'ERR_INVALID_TERM_DEFINITION', 120 | `Invalid JSON-LD term definition for "${key}"; it must be ` + 121 | 'a string or an object.'); 122 | } 123 | 124 | // set term definition 125 | termMap.set(key, {...def, protected: isProtected}); 126 | 127 | // ensure the term has been assigned an ID 128 | if(!termToId.has(key)) { 129 | const id = this.nextTermId; 130 | this.nextTermId += 2; 131 | termToId.set(key, id); 132 | if(idToTerm) { 133 | idToTerm.set(id, key); 134 | } 135 | } 136 | } 137 | 138 | // add entry for context URL or context object 139 | contextMap.set(contextUrl || context, entry); 140 | 141 | return entry; 142 | } 143 | 144 | async _getDocument({url}) { 145 | const {document} = await this.documentLoader(url); 146 | if(typeof document === 'string') { 147 | return JSON.parse(document); 148 | } 149 | return document; 150 | } 151 | } 152 | 153 | /** 154 | * Fetches a resource given a URL and returns it as a string. 155 | * 156 | * @callback documentLoaderFunction 157 | * @param {string} url - The URL to retrieve. 158 | * 159 | * @returns {string} The resource associated with the URL as a string. 160 | */ 161 | -------------------------------------------------------------------------------- /lib/Converter.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2025 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import { 5 | LEGACY_TYPE_TABLE_ENCODED_AS_BYTES, 6 | TYPE_TABLE_ENCODED_AS_BYTES 7 | } from './helpers.js'; 8 | import {ActiveContext} from './ActiveContext.js'; 9 | import {ContextLoader} from './ContextLoader.js'; 10 | 11 | export class Converter { 12 | /** 13 | * Creates a new Converter for converting CBOR-LD <=> JSON-LD. 14 | * 15 | * @param {object} options - The options to use. 16 | * @param {object} options.strategy - The conversion strategy to use, 17 | * e.g., a compressor or decompressor. 18 | * @param {documentLoaderFunction} options.documentLoader -The document 19 | * loader to use when resolving JSON-LD Context URLs. 20 | * @param {boolean} [options.legacy=false] - True if legacy mode is in 21 | * effect, false if not. 22 | */ 23 | constructor({strategy, documentLoader, legacy = false} = {}) { 24 | this.strategy = strategy; 25 | this.legacy = legacy; 26 | const contextLoader = new ContextLoader({ 27 | documentLoader, buildReverseMap: !!strategy.reverseTypeTable 28 | }); 29 | this.contextLoader = contextLoader; 30 | this.initialActiveCtx = new ActiveContext({contextLoader}); 31 | 32 | // FIXME: consider moving to strategies for better separation of concerns 33 | this.typeTableEncodedAsBytesSet = legacy ? 34 | LEGACY_TYPE_TABLE_ENCODED_AS_BYTES : TYPE_TABLE_ENCODED_AS_BYTES; 35 | } 36 | 37 | /** 38 | * CBOR-LD is a semantic compression format; it uses the contextual 39 | * information provided by JSON-LD contexts to compress JSON-LD objects 40 | * to CBOR-LD (and vice versa). 41 | * 42 | * This `convert()` function is used to either convert from JSON-LD to 43 | * CBOR-LD (compression) or vice versa (decompression). The type of 44 | * conversion (compression or decompression) is called a `strategy`; this 45 | * `strategy` is passed to the `Converter` constructor so it can be used 46 | * during conversion. 47 | * 48 | * When compressing, the conversion maps JSON keys (strings) encountered in 49 | * the JSON-LD object to CBOR-LD IDs (integers) and value encoders. 50 | * 51 | * When decompressing, the conversion maps CBOR-LD IDs (integers) and 52 | * decoded values from value decoders. 53 | * 54 | * A follow-on process can then serialize these abstract outputs to either 55 | * JSON or CBOR by using the `cborg` library. 56 | * 57 | * In order to match each JSON key / CBOR-LD ID with the right context term 58 | * definition, it's important to understand how context is applied in 59 | * JSON-LD, i.e., which context is "active" at a certain place in the input. 60 | * 61 | * There are three ways contexts become active: 62 | * 63 | * 1. Embedded contexts. An embedded context is expressed by using the 64 | * `@context` keyword in a JSON-LD object. It is active on the object 65 | * where it appears and propagates to any nested objects. 66 | * 2. Type-scoped contexts. Such a context is defined in another context 67 | * and bound to a particular type. It will become active based on the 68 | * presence of the `@type` property (or an alias of it) and a matching 69 | * type value. By default, it *does not* propagate to nested objects, 70 | * i.e., it becomes inactive when a nested object does not have a matching 71 | * type. 72 | * 3. Property-scoped contexts. Such a context is defined in another context 73 | * and bound to a particular property. It will become active based on the 74 | * presence of a particular term, i.e., JSON key. By default, it 75 | * propagates, i.e., all terms defined by the context will be applicable 76 | * to the whole subtree associated with the property in the JSON object. 77 | * 78 | * The internal conversion process follows this basic algorithm, which takes 79 | * an input and an output (to be populated): 80 | * 81 | * 1. Converting any contexts in the input (i.e., "embedded contexts") and 82 | * producing an active context for converting other elements in the input. 83 | * Every term in the top-level contexts (excludes scoped-contexts) will be 84 | * auto-assigned CBOR-LD IDs. 85 | * 2. Getting all type information associated with the input and using it 86 | * to update the active context. Any terms from any type-scoped contexts 87 | * will be auto-assigned CBOR-LD IDs. 88 | * 3. For every term => value(s) entry in the input: 89 | * 3.1. Update the active context using any property-scoped contextt 90 | * associated with the term. Any terms in this property-scoped 91 | * context will be auto-assigned CBOR-LD IDs. 92 | * 3.2. Create an array of outputs to be populated from converting 93 | * all of the value(s). 94 | * 3.3. For every value in value(s), perform conversion: 95 | * 3.3.1. If the value is `null`, add it to the outputs as-is and 96 | * continue. 97 | * 3.3.2. Try to use the strategy to convert the value; this involves 98 | * checking the value type and using value-specific codecs, 99 | * only values that must be recursed (e.g., objects and 100 | * arrays) will not be converted by a strategy; add any 101 | * successfully converted value to the outputs and continue. 102 | * 3.2.3. If the value is an array, create an output array and 103 | * recursively convert each of its elements; add the output 104 | * array to the outputs and continue. 105 | * 3.2.4. Create a new output according to the strategy, add it to 106 | * the outputs and recurse with the value as the new input 107 | * and the new output as the new output. 108 | * 3.4. Add an term => value(s) entry to the output using the current term 109 | * information and the outputs as the value(s). 110 | * 111 | * @param {object} options - The options to use. 112 | * @param {Map|object} options.input - The input to convert. 113 | * 114 | * @returns {Promise} - The output. 115 | */ 116 | async convert({input} = {}) { 117 | // handle single or multiple inputs 118 | const isArray = Array.isArray(input); 119 | const inputs = isArray ? input : [input]; 120 | const outputs = []; 121 | // note: could be performed in parallel as long as order is preserved 122 | for(const input of inputs) { 123 | const output = this.strategy.createNewOutput(); 124 | outputs.push(await this._convert({input, output})); 125 | } 126 | return isArray ? outputs : outputs[0]; 127 | } 128 | 129 | async _convert({activeCtx = this.initialActiveCtx, input, output}) { 130 | // convert contexts according to strategy 131 | const {strategy} = this; 132 | activeCtx = await strategy.convertContexts({activeCtx, input, output}); 133 | 134 | // get unique `@type` (and alias) values for the input 135 | const objectTypes = strategy.getObjectTypes({ 136 | activeCtx, input, output, converter: this 137 | }); 138 | 139 | // apply type-scoped contexts 140 | activeCtx = await activeCtx.applyTypeScopedContexts({objectTypes}); 141 | 142 | // walk each term => value(s) input entry to convert them all 143 | const termEntries = strategy.getInputEntries({activeCtx, input}); 144 | for(const [termInfo, value] of termEntries) { 145 | // apply any property-scoped context for `term` to get active context 146 | // to use with each value 147 | const {term} = termInfo; 148 | const valueActiveCtx = await activeCtx.applyPropertyScopedContext({term}); 149 | 150 | // iterate through all values for the current term to produce new outputs 151 | const {plural, def: {'@type': termType}} = termInfo; 152 | const values = plural ? value : [value]; 153 | const outputs = []; 154 | // note: could be performed in parallel as long as order is preserved 155 | for(const value of values) { 156 | outputs.push(await this._convertValue({ 157 | activeCtx: valueActiveCtx, termType, value, termInfo 158 | })); 159 | } 160 | strategy.addOutputEntry({ 161 | termInfo, values: plural ? outputs : outputs[0], output 162 | }); 163 | } 164 | return output; 165 | } 166 | 167 | async _convertValue({activeCtx, termType, value, termInfo}) { 168 | // `null` is never converted 169 | if(value === null) { 170 | return null; 171 | } 172 | // convert value via strategy if possible 173 | let output = this.strategy.convertValue({ 174 | termType, value, termInfo, converter: this 175 | }); 176 | if(output !== undefined) { 177 | return output; 178 | } 179 | if(Array.isArray(value)) { 180 | // recurse into array 181 | const array = value; 182 | const outputs = []; 183 | // note: could be performed in parallel as long as order is preserved 184 | for(const value of array) { 185 | outputs.push(await this._convertValue({ 186 | activeCtx, termType, value, termInfo 187 | })); 188 | } 189 | return outputs; 190 | } 191 | // recurse into object and populate new output 192 | output = this.strategy.createNewOutput(); 193 | return this._convert({activeCtx, input: value, output}); 194 | } 195 | } 196 | 197 | /** 198 | * Fetches a resource given a URL and returns it as a string. 199 | * 200 | * @callback documentLoaderFunction 201 | * @param {string} url - The URL to retrieve. 202 | * 203 | * @returns {string} The resource associated with the URL as a string. 204 | */ 205 | -------------------------------------------------------------------------------- /lib/Decompressor.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {KEYWORDS_TABLE, reverseMap} from './tables.js'; 5 | import {CborldError} from './CborldError.js'; 6 | import {ContextDecoder} from './codecs/ContextDecoder.js'; 7 | import {ValueDecoder} from './codecs/ValueDecoder.js'; 8 | 9 | const CONTEXT_TERM_ID = KEYWORDS_TABLE.get('@context'); 10 | const CONTEXT_TERM_ID_PLURAL = CONTEXT_TERM_ID + 1; 11 | 12 | export class Decompressor { 13 | /** 14 | * Creates a new Decompressor for generating a JSON-LD document from 15 | * compressed CBOR-LD. The created instance may only be used on a single 16 | * CBOR-LD input at a time. 17 | * 18 | * @param {object} options - The options to use. 19 | * @param {Map} options.typeTable - A map of possible value types, including 20 | * `context`, `url`, `none`, and any JSON-LD type, each of which maps to 21 | * another map of values of that type to their associated CBOR-LD integer 22 | * values. 23 | */ 24 | constructor({typeTable} = {}) { 25 | this.typeTable = typeTable; 26 | 27 | // build reverse compression tables, the `typeTable` is a map of maps, 28 | // just reverse each inner map 29 | const reverseTypeTable = new Map(); 30 | if(typeTable) { 31 | for(const [k, map] of typeTable) { 32 | reverseTypeTable.set(k, reverseMap(map)); 33 | } 34 | } 35 | this.reverseTypeTable = reverseTypeTable; 36 | } 37 | 38 | addOutputEntry({termInfo, values, output}) { 39 | output[termInfo.term] = values; 40 | } 41 | 42 | async convertContexts({activeCtx, input, output}) { 43 | const {reverseTypeTable} = this; 44 | const decoder = ContextDecoder.createDecoder({reverseTypeTable}); 45 | 46 | // decode `@context` in `input`, if any 47 | const encodedContext = input.get(CONTEXT_TERM_ID); 48 | if(encodedContext) { 49 | output['@context'] = decoder.decode({value: encodedContext}); 50 | } 51 | const encodedContexts = input.get(CONTEXT_TERM_ID_PLURAL); 52 | if(encodedContexts) { 53 | if(encodedContext) { 54 | // can't use *both* the singular and plural context term ID 55 | throw new CborldError( 56 | 'ERR_INVALID_ENCODED_CONTEXT', 57 | 'Both singular and plural context IDs were found in the ' + 58 | 'CBOR-LD input.'); 59 | } 60 | if(!Array.isArray(encodedContexts)) { 61 | // `encodedContexts` must be an array 62 | throw new CborldError( 63 | 'ERR_INVALID_ENCODED_CONTEXT', 64 | 'Encoded plural context value must be an array.'); 65 | } 66 | const contexts = []; 67 | for(const value of encodedContexts) { 68 | contexts.push(decoder.decode({value})); 69 | } 70 | output['@context'] = contexts; 71 | } 72 | 73 | return activeCtx.applyEmbeddedContexts({obj: output}); 74 | } 75 | 76 | convertValue({termType, value, termInfo, converter}) { 77 | if(value instanceof Map) { 78 | return; 79 | } 80 | const decoder = ValueDecoder.createDecoder({ 81 | value, converter, termInfo, termType 82 | }); 83 | return decoder?.decode({value}); 84 | } 85 | 86 | createNewOutput() { 87 | return {}; 88 | } 89 | 90 | getInputEntries({activeCtx, input}) { 91 | // get input entries to be converted and sort by *term* to ensure term 92 | // IDs will be assigned in the same order that the compressor assigned them 93 | const entries = []; 94 | for(const [key, value] of input) { 95 | // skip `@context`; convert already converted early 96 | if(key === CONTEXT_TERM_ID || key === CONTEXT_TERM_ID_PLURAL) { 97 | continue; 98 | } 99 | const {term, termId, plural, def} = activeCtx.getTermInfo({id: key}); 100 | entries.push([{term, termId, plural, def}, value]); 101 | } 102 | return entries.sort(_sortEntriesByTerm); 103 | } 104 | 105 | getObjectTypes({activeCtx, input, converter}) { 106 | const objectTypes = new Set(); 107 | // must decode object types to get their original values 108 | const typeTerms = activeCtx.getTypeTerms(); 109 | for(const typeTerm of typeTerms) { 110 | // check for encoded singular and plural term IDs 111 | const termId = activeCtx.getIdForTerm({term: typeTerm}); 112 | let value = input.get(termId) ?? input.get(termId + 1); 113 | if(value === undefined) { 114 | // encoded type term is not present in payload 115 | continue; 116 | } 117 | // decode each value associated with the type term 118 | const termInfo = activeCtx.getTermInfo({id: termId}); 119 | value = Array.isArray(value) ? value : [value]; 120 | value.forEach(value => { 121 | const decoder = ValueDecoder.createDecoder({ 122 | value, converter, termInfo, termType: '@vocab' 123 | }); 124 | objectTypes.add(decoder?.decode({value}) ?? value); 125 | }); 126 | } 127 | return objectTypes; 128 | } 129 | } 130 | 131 | function _sortEntriesByTerm([{term: t1}], [{term: t2}]) { 132 | return t1 < t2 ? -1 : t1 > t2 ? 1 : 0; 133 | } 134 | -------------------------------------------------------------------------------- /lib/codecs/Base58DidUrlDecoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {CborldDecoder} from './CborldDecoder.js'; 5 | import {encode as encodeBase58} from 'base58-universal'; 6 | import {REVERSE_URL_SCHEME_TABLE} from '../tables.js'; 7 | 8 | export class Base58DidUrlDecoder extends CborldDecoder { 9 | constructor({prefix} = {}) { 10 | super(); 11 | this.prefix = prefix; 12 | } 13 | 14 | decode({value} = {}) { 15 | let url = this.prefix; 16 | if(typeof value[1] === 'string') { 17 | url += value[1]; 18 | } else { 19 | url += `z${encodeBase58(value[1])}`; 20 | } 21 | if(value.length > 2) { 22 | if(typeof value[2] === 'string') { 23 | url += `#${value[2]}`; 24 | } else { 25 | url += `#z${encodeBase58(value[2])}`; 26 | } 27 | } 28 | return url; 29 | } 30 | 31 | static createDecoder({value} = {}) { 32 | if(value.length > 1 && value.length <= 3) { 33 | const prefix = REVERSE_URL_SCHEME_TABLE.get(value[0]); 34 | return new Base58DidUrlDecoder({prefix}); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/codecs/Base58DidUrlEncoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {Token, Type} from 'cborg'; 5 | import {CborldEncoder} from './CborldEncoder.js'; 6 | import {decode as decodeBase58} from 'base58-universal'; 7 | 8 | const SCHEME_TO_ID = new Map([ 9 | ['did:v1:nym:', 1024], 10 | ['did:key:', 1025] 11 | ]); 12 | 13 | export class Base58DidUrlEncoder extends CborldEncoder { 14 | constructor({value, scheme, schemeCompressed} = {}) { 15 | super(); 16 | this.value = value; 17 | this.scheme = scheme; 18 | this.schemeCompressed = schemeCompressed; 19 | } 20 | 21 | encode() { 22 | const {value, scheme, schemeCompressed} = this; 23 | const suffix = value.slice(scheme.length); 24 | const [authority, fragment] = suffix.split('#'); 25 | const entries = [ 26 | new Token(Type.uint, schemeCompressed), 27 | _multibase58ToToken(authority) 28 | ]; 29 | if(fragment !== undefined) { 30 | entries.push(_multibase58ToToken(fragment)); 31 | } 32 | return [new Token(Type.array, entries.length), entries]; 33 | } 34 | 35 | static createEncoder({value} = {}) { 36 | for(const [key, schemeCompressed] of SCHEME_TO_ID) { 37 | if(value.startsWith(key)) { 38 | return new Base58DidUrlEncoder({ 39 | value, 40 | scheme: key, 41 | schemeCompressed 42 | }); 43 | } 44 | } 45 | } 46 | } 47 | 48 | function _multibase58ToToken(str) { 49 | if(str.startsWith('z')) { 50 | const decoded = decodeBase58(str.slice(1)); 51 | if(decoded) { 52 | return new Token(Type.bytes, decoded); 53 | } 54 | } 55 | // cannot compress suffix 56 | return new Token(Type.string, str); 57 | } 58 | -------------------------------------------------------------------------------- /lib/codecs/CborldDecoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | export class CborldDecoder { 5 | decode() { 6 | throw new Error('Must be implemented by derived class.'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/codecs/CborldEncoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | export class CborldEncoder { 5 | encode() { 6 | throw new Error('Must be implemented by derived class.'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/codecs/ContextDecoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {CborldDecoder} from './CborldDecoder.js'; 5 | import {CborldError} from '../CborldError.js'; 6 | 7 | export class ContextDecoder extends CborldDecoder { 8 | constructor({reverseContextTable} = {}) { 9 | super(); 10 | this.reverseContextTable = reverseContextTable; 11 | } 12 | 13 | decode({value} = {}) { 14 | // handle uncompressed context 15 | if(typeof value !== 'number') { 16 | return _mapToObject(value); 17 | } 18 | 19 | // handle compressed context 20 | const url = this.reverseContextTable.get(value); 21 | if(url === undefined) { 22 | throw new CborldError( 23 | 'ERR_UNDEFINED_COMPRESSED_CONTEXT', 24 | `Undefined compressed context "${value}".`); 25 | } 26 | return url; 27 | } 28 | 29 | static createDecoder({reverseTypeTable} = {}) { 30 | const reverseContextTable = reverseTypeTable.get('context'); 31 | return new ContextDecoder({reverseContextTable}); 32 | } 33 | } 34 | 35 | function _mapToObject(map) { 36 | if(Array.isArray(map)) { 37 | return map.map(_mapToObject); 38 | } 39 | if(!(map instanceof Map)) { 40 | return map; 41 | } 42 | 43 | const obj = {}; 44 | for(const [key, value] of map) { 45 | obj[key] = _mapToObject(value); 46 | } 47 | return obj; 48 | } 49 | -------------------------------------------------------------------------------- /lib/codecs/ContextEncoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {Token, Type} from 'cborg'; 5 | import {CborldEncoder} from './CborldEncoder.js'; 6 | 7 | export class ContextEncoder extends CborldEncoder { 8 | constructor({context, contextTable} = {}) { 9 | super(); 10 | this.context = context; 11 | this.contextTable = contextTable; 12 | } 13 | 14 | encode() { 15 | const {context, contextTable} = this; 16 | const id = contextTable.get(context); 17 | if(id === undefined) { 18 | return new Token(Type.string, context); 19 | } 20 | return new Token(Type.uint, id); 21 | } 22 | 23 | static createEncoder({value, typeTable} = {}) { 24 | if(typeof value !== 'string') { 25 | return; 26 | } 27 | const contextTable = typeTable.get('context'); 28 | return new ContextEncoder({context: value, contextTable}); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/codecs/DataUrlDecoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {Base64} from 'js-base64'; 5 | import {CborldDecoder} from './CborldDecoder.js'; 6 | 7 | export class DataUrlDecoder extends CborldDecoder { 8 | constructor() { 9 | super(); 10 | } 11 | 12 | decode({value} = {}) { 13 | if(value.length === 3) { 14 | return `data:${value[1]};base64,${Base64.fromUint8Array(value[2])}`; 15 | } 16 | return `data:${value[1]}`; 17 | } 18 | 19 | static createDecoder({value} = {}) { 20 | if(value.length === 3 && typeof value[1] === 'string' && 21 | value[2] instanceof Uint8Array) { 22 | return new DataUrlDecoder({value}); 23 | } 24 | if(value.length === 2 && typeof value[1] === 'string') { 25 | return new DataUrlDecoder({value}); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/codecs/DataUrlEncoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {Token, Type} from 'cborg'; 5 | import {Base64} from 'js-base64'; 6 | import {CborldEncoder} from './CborldEncoder.js'; 7 | import {URL_SCHEME_TABLE} from '../tables.js'; 8 | 9 | /* 10 | Data URL codec for base64 data. 11 | 12 | References: 13 | https://www.rfc-editor.org/rfc/rfc2397 14 | https://fetch.spec.whatwg.org/#data-urls 15 | https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs 16 | 17 | Data URL format: 18 | data:[][;base64], 19 | 20 | This codec must be able to round-trip the data. The parsing is minimal and 21 | designed to only binary encode well-formed base64 data that can be decoded to 22 | the same input. The mediatype (with optional parameters) is stored as-is, with 23 | the exception of ";base64" if correct base64 encoding is detected. In the case 24 | of a non-base64 data URI, all but the "data:" prefix is encoded as a string. 25 | 26 | base64 encoding: [string, data] 27 | non-base64 encoding: [string] 28 | 29 | TODO: An optimization could use a registry of well-known base mediatypes (ie, 30 | image/png, text/plain, etc). 31 | */ 32 | 33 | // base64 data uri regex 34 | // when using this, use round trip code to ensure decoder will work 35 | const DATA_BASE64_REGEX = /^data:(?.*);base64,(?.*)$/; 36 | 37 | export class DataUrlEncoder extends CborldEncoder { 38 | constructor({value, base64} = {}) { 39 | super(); 40 | this.value = value; 41 | this.base64 = base64; 42 | } 43 | 44 | encode() { 45 | const {value, base64} = this; 46 | 47 | const entries = [new Token(Type.uint, URL_SCHEME_TABLE.get('data:'))]; 48 | 49 | if(base64) { 50 | // base64 mode 51 | // [string, bytes] 52 | const parsed = DATA_BASE64_REGEX.exec(value); 53 | entries.push( 54 | new Token(Type.string, parsed.groups.mediatype)); 55 | entries.push( 56 | new Token(Type.bytes, Base64.toUint8Array(parsed.groups.data))); 57 | } else { 58 | // non-base64 mode 59 | // [string] 60 | entries.push( 61 | new Token(Type.string, value.slice('data:'.length))); 62 | } 63 | 64 | return [new Token(Type.array, entries.length), entries]; 65 | } 66 | 67 | static createEncoder({value} = {}) { 68 | // quick check 69 | if(!value.startsWith('data:')) { 70 | return; 71 | } 72 | // attempt to parse as a base64 73 | const parsed = DATA_BASE64_REGEX.exec(value); 74 | if(parsed) { 75 | // check to ensure data can be restored 76 | // this avoids issues with variations in encoding 77 | const data = parsed.groups.data; 78 | if(data === Base64.fromUint8Array(Base64.toUint8Array(data))) { 79 | return new DataUrlEncoder({value, base64: true}); 80 | } 81 | } 82 | return new DataUrlEncoder({value, base64: false}); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/codecs/HttpUrlDecoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {CborldDecoder} from './CborldDecoder.js'; 5 | import {REVERSE_URL_SCHEME_TABLE} from '../tables.js'; 6 | 7 | export class HttpUrlDecoder extends CborldDecoder { 8 | constructor({scheme} = {}) { 9 | super(); 10 | this.scheme = scheme; 11 | } 12 | 13 | decode({value} = {}) { 14 | return `${this.scheme}${value[1]}`; 15 | } 16 | 17 | static createDecoder({value} = {}) { 18 | if(value.length === 2 && typeof value[1] === 'string') { 19 | const scheme = REVERSE_URL_SCHEME_TABLE.get(value[0]); 20 | return new HttpUrlDecoder({scheme}); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/codecs/HttpUrlEncoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {Token, Type} from 'cborg'; 5 | import {CborldEncoder} from './CborldEncoder.js'; 6 | import {URL_SCHEME_TABLE} from '../tables.js'; 7 | 8 | export class HttpUrlEncoder extends CborldEncoder { 9 | constructor({value, scheme} = {}) { 10 | super(); 11 | this.value = value; 12 | this.scheme = scheme; 13 | } 14 | 15 | encode() { 16 | const {value, scheme} = this; 17 | const entries = [ 18 | new Token(Type.uint, URL_SCHEME_TABLE.get(scheme)), 19 | new Token(Type.string, value.slice(scheme.length)) 20 | ]; 21 | return [new Token(Type.array, entries.length), entries]; 22 | } 23 | 24 | static createEncoder({value} = {}) { 25 | // presume HTTPS is more common, check for it first 26 | if(value.startsWith('https://')) { 27 | return new HttpUrlEncoder({value, scheme: 'https://'}); 28 | } 29 | if(value.startsWith('http://')) { 30 | return new HttpUrlEncoder({value, scheme: 'http://'}); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/codecs/MultibaseDecoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import * as base64url from 'base64url-universal'; 5 | import {Base64} from 'js-base64'; 6 | import {CborldDecoder} from './CborldDecoder.js'; 7 | import {encode as encodeBase58} from 'base58-universal'; 8 | 9 | // this class is used to encode a multibase encoded value in CBOR-LD, which 10 | // actually means transforming bytes to a multibase-encoded string 11 | export class MultibaseDecoder extends CborldDecoder { 12 | constructor() { 13 | super(); 14 | } 15 | 16 | decode({value} = {}) { 17 | const {buffer, byteOffset, length} = value; 18 | const suffix = new Uint8Array(buffer, byteOffset + 1, length - 1); 19 | if(value[0] === 0x7a) { 20 | // 0x7a === 'z' (multibase code for base58btc) 21 | return `z${encodeBase58(suffix)}`; 22 | } 23 | if(value[0] === 0x75) { 24 | // 0x75 === 'u' (multibase code for base64url) 25 | return `u${base64url.encode(suffix)}`; 26 | } 27 | if(value[0] === 0x4d) { 28 | // 0x4d === 'M' (multibase code for base64pad) 29 | return `M${Base64.fromUint8Array(suffix)}`; 30 | } 31 | return value; 32 | } 33 | 34 | static createDecoder({value} = {}) { 35 | if(!(value instanceof Uint8Array)) { 36 | return; 37 | } 38 | 39 | // supported multibase encodings: 40 | // 0x7a === 'z' (multibase code for base58btc) 41 | // 0x75 === 'u' (multibase code for base64url) 42 | // 0x4d === 'M' (multibase code for base64pad) 43 | if(value[0] === 0x7a || value[0] === 0x75 || value[0] === 0x4d) { 44 | return new MultibaseDecoder(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/codecs/MultibaseEncoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import * as base64url from 'base64url-universal'; 5 | import {Token, Type} from 'cborg'; 6 | import {Base64} from 'js-base64'; 7 | import {CborldEncoder} from './CborldEncoder.js'; 8 | import {decode as decodeBase58} from 'base58-universal'; 9 | 10 | // this class is used to encode a multibase encoded value in CBOR-LD, which 11 | // actually means transforming a multibase-encoded string to bytes 12 | export class MultibaseEncoder extends CborldEncoder { 13 | constructor({value} = {}) { 14 | super(); 15 | this.value = value; 16 | } 17 | 18 | encode() { 19 | const {value} = this; 20 | 21 | let prefix; 22 | let suffix; 23 | if(value[0] === 'z') { 24 | // 0x7a === 'z' (multibase code for base58btc) 25 | prefix = 0x7a; 26 | suffix = decodeBase58(value.slice(1)); 27 | } else if(value[0] === 'u') { 28 | // 0x75 === 'u' (multibase code for base64url) 29 | prefix = 0x75; 30 | const buffer = base64url.decode(value.slice(1)); 31 | suffix = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.length); 32 | } else if(value[0] === 'M') { 33 | // 0x4d === 'M' (multibase code for base64pad) 34 | prefix = 0x4d; 35 | suffix = Base64.toUint8Array(value.slice(1)); 36 | } 37 | 38 | const bytes = new Uint8Array(1 + suffix.length); 39 | bytes[0] = prefix; 40 | bytes.set(suffix, 1); 41 | return new Token(Type.bytes, bytes); 42 | } 43 | 44 | static createEncoder({value} = {}) { 45 | if(typeof value !== 'string') { 46 | return; 47 | } 48 | // supported multibase encodings: 49 | // 0x7a === 'z' (multibase code for base58btc) 50 | // 0x75 === 'u' (multibase code for base64url) 51 | // 0x4d === 'M' (multibase code for base64pad) 52 | if(value[0] === 'z' || value[0] === 'u' || value[0] === 'M') { 53 | return new MultibaseEncoder({value}); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/codecs/UrlDecoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {Base58DidUrlDecoder} from './Base58DidUrlDecoder.js'; 5 | import {CborldDecoder} from './CborldDecoder.js'; 6 | import {CborldError} from '../CborldError.js'; 7 | import {DataUrlDecoder} from './DataUrlDecoder.js'; 8 | import {HttpUrlDecoder} from './HttpUrlDecoder.js'; 9 | import {REVERSE_URL_SCHEME_TABLE} from '../tables.js'; 10 | import {UuidUrnDecoder} from './UuidUrnDecoder.js'; 11 | 12 | const SCHEME_ID_TO_DECODER = new Map([ 13 | ['http://', HttpUrlDecoder], 14 | ['https://', HttpUrlDecoder], 15 | ['urn:uuid:', UuidUrnDecoder], 16 | ['data:', DataUrlDecoder], 17 | ['did:v1:nym:', Base58DidUrlDecoder], 18 | ['did:key:', Base58DidUrlDecoder] 19 | ]); 20 | 21 | export class UrlDecoder extends CborldDecoder { 22 | constructor({term} = {}) { 23 | super(); 24 | this.term = term; 25 | } 26 | 27 | decode() { 28 | return this.term; 29 | } 30 | 31 | static createDecoder({value, converter} = {}) { 32 | // pass uncompressed URL values through 33 | if(typeof value === 'string') { 34 | return; 35 | } 36 | if(Array.isArray(value)) { 37 | const DecoderClass = SCHEME_ID_TO_DECODER.get( 38 | REVERSE_URL_SCHEME_TABLE.get(value[0])); 39 | const decoder = DecoderClass?.createDecoder({value, converter}); 40 | if(!decoder) { 41 | throw new CborldError( 42 | 'ERR_UNKNOWN_COMPRESSED_VALUE', 43 | `Unknown compressed URL "${value}".`); 44 | } 45 | return decoder; 46 | } 47 | const {term} = converter.contextLoader.getTermForId({id: value}); 48 | return new UrlDecoder({term}); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/codecs/UrlEncoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {Token, Type} from 'cborg'; 5 | import {Base58DidUrlEncoder} from './Base58DidUrlEncoder.js'; 6 | import {CborldEncoder} from './CborldEncoder.js'; 7 | import {DataUrlEncoder} from './DataUrlEncoder.js'; 8 | import {HttpUrlEncoder} from './HttpUrlEncoder.js'; 9 | import {UuidUrnEncoder} from './UuidUrnEncoder.js'; 10 | 11 | // an encoded URL is an array with the first element being an integer that 12 | // signals which encoder was used: 13 | // `0` reserved 14 | // `1` http 15 | // `2` https 16 | // `3` urn:uuid 17 | // `4` data (RFC 2397) 18 | // `1024` did:v1:nym 19 | // `1025` did:key 20 | const SCHEME_TO_ENCODER = new Map([ 21 | ['http', HttpUrlEncoder], 22 | ['https', HttpUrlEncoder], 23 | ['urn:uuid', UuidUrnEncoder], 24 | ['data', DataUrlEncoder], 25 | ['did:v1:nym', Base58DidUrlEncoder], 26 | ['did:key', Base58DidUrlEncoder] 27 | ]); 28 | 29 | export class UrlEncoder extends CborldEncoder { 30 | constructor({termId} = {}) { 31 | super(); 32 | this.termId = termId; 33 | } 34 | 35 | encode() { 36 | return new Token(Type.uint, this.termId); 37 | } 38 | 39 | static createEncoder({value, converter} = {}) { 40 | // see if a term ID exists that matches the value first 41 | const termId = converter.contextLoader.getIdForTerm({term: value}); 42 | if(typeof termId !== 'string') { 43 | return new UrlEncoder({termId}); 44 | } 45 | 46 | // check URI prefix codecs 47 | // get full colon-delimited prefix 48 | let scheme; 49 | try { 50 | // this handles URIs both with authority followed by `//` and without 51 | const {protocol, pathname} = new URL(value); 52 | scheme = protocol; 53 | if(pathname.includes(':')) { 54 | scheme += pathname; 55 | } 56 | const split = value.split(':'); 57 | split.pop(); 58 | scheme = split.join(':'); 59 | } catch(e) { 60 | return; 61 | } 62 | 63 | const EncoderClass = SCHEME_TO_ENCODER.get(scheme); 64 | return EncoderClass && EncoderClass.createEncoder({value, converter}); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/codecs/UuidUrnDecoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {CborldDecoder} from './CborldDecoder.js'; 5 | import {stringify} from 'uuid'; 6 | 7 | export class UuidUrnDecoder extends CborldDecoder { 8 | constructor() { 9 | super(); 10 | } 11 | decode({value} = {}) { 12 | const uuid = typeof value[1] === 'string' ? 13 | value[1] : stringify(value[1]); 14 | return `urn:uuid:${uuid}`; 15 | } 16 | 17 | static createDecoder({value} = {}) { 18 | if(value.length === 2 && 19 | (typeof value[1] === 'string' || value[1] instanceof Uint8Array)) { 20 | return new UuidUrnDecoder(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/codecs/UuidUrnEncoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {Token, Type} from 'cborg'; 5 | import {CborldEncoder} from './CborldEncoder.js'; 6 | import {parse} from 'uuid'; 7 | import {URL_SCHEME_TABLE} from '../tables.js'; 8 | 9 | export class UuidUrnEncoder extends CborldEncoder { 10 | constructor({value} = {}) { 11 | super(); 12 | this.value = value; 13 | } 14 | 15 | encode() { 16 | const {value} = this; 17 | const rest = value.slice('urn:uuid:'.length); 18 | const entries = [new Token( 19 | Type.uint, 20 | URL_SCHEME_TABLE.get('urn:uuid:'))]; 21 | if(rest.toLowerCase() === rest) { 22 | const uuidBytes = parse(rest); 23 | entries.push(new Token(Type.bytes, uuidBytes)); 24 | } else { 25 | // cannot compress UUID value 26 | entries.push(new Token(Type.string, rest)); 27 | } 28 | return [new Token(Type.array, entries.length), entries]; 29 | } 30 | 31 | static createEncoder({value} = {}) { 32 | if(!value.startsWith('urn:uuid:')) { 33 | return; 34 | } 35 | return new UuidUrnEncoder({value}); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/codecs/ValueDecoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import { 5 | getTableType, 6 | intFromBytes, 7 | uintFromBytes 8 | } from '../helpers.js'; 9 | import {CborldDecoder} from './CborldDecoder.js'; 10 | import {CborldError} from '../CborldError.js'; 11 | import {MultibaseDecoder} from './MultibaseDecoder.js'; 12 | import {UrlDecoder} from './UrlDecoder.js'; 13 | import {XsdDateDecoder} from './XsdDateDecoder.js'; 14 | import {XsdDateTimeDecoder} from './XsdDateTimeDecoder.js'; 15 | 16 | // constants based on "default" processing mode: 17 | const PROCESSING_MODE_TYPE_DECODERS = new Map([ 18 | ['url', UrlDecoder], 19 | ['https://w3id.org/security#multibase', MultibaseDecoder], 20 | ['http://www.w3.org/2001/XMLSchema#date', XsdDateDecoder], 21 | ['http://www.w3.org/2001/XMLSchema#dateTime', XsdDateTimeDecoder] 22 | ]); 23 | 24 | export class ValueDecoder extends CborldDecoder { 25 | constructor({decoded} = {}) { 26 | super(); 27 | this.decoded = decoded; 28 | } 29 | 30 | decode() { 31 | return this.decoded; 32 | } 33 | 34 | static createDecoder({value, converter, termType, termInfo} = {}) { 35 | const tableType = getTableType({termInfo, termType}); 36 | const subTable = converter.strategy.reverseTypeTable.get(tableType); 37 | 38 | // handle decoding value for term with a subtable 39 | if(subTable) { 40 | let intValue; 41 | let useTable = false; 42 | const isBytes = value instanceof Uint8Array; 43 | const {typeTableEncodedAsBytesSet} = converter; 44 | const tableEncodingUsesBytes = typeTableEncodedAsBytesSet.has(tableType); 45 | if(isBytes && tableEncodingUsesBytes) { 46 | useTable = true; 47 | intValue = uintFromBytes({bytes: value}); 48 | } else if(Number.isInteger(value) && !tableEncodingUsesBytes) { 49 | useTable = true; 50 | intValue = value; 51 | } 52 | 53 | // either the subtable must be used and an error will be thrown if the 54 | // unsigned integer expression of the value is not in the table, or 55 | // the value might need to be decoded to a signed integer from bytes 56 | let decoded; 57 | if(useTable) { 58 | decoded = subTable.get(intValue); 59 | /* Note: If the value isn't in the subtable, this is considered an 60 | error in non-legacy mode and, for non-legacy mode, an error except for 61 | CBOR-LD IDs that also match a term in a context (which may or may not 62 | actually match the expected URL). For legacy mode, this means that if a 63 | URL table is appended to after initial registration, then a consumer 64 | without the table update will misinterpret the new entry as some term 65 | from a context. */ 66 | const {legacy} = converter; 67 | if(decoded === undefined && 68 | !(legacy && tableType === 'url' && 69 | converter.contextLoader.hasTermId({id: intValue}))) { 70 | // FIXME: change error name? 71 | throw new CborldError( 72 | 'ERR_UNKNOWN_COMPRESSED_VALUE', 73 | `Compressed value "${intValue}" not found`); 74 | } 75 | } else if(isBytes && tableType !== 'none') { 76 | /* Note: For the unusual case that a type subtable has been defined 77 | for a custom type, but the encoded value is in bytes, it means that the 78 | user data included a native JSON integer. This integer then had to be 79 | expressed in CBOR as bytes during encoding, to ensure no conflicts with 80 | any subtable values which are expressed as CBOR integers. Despite being 81 | an unusual case, it is supported here by decoding the signed integer 82 | from bytes (two's complement). */ 83 | decoded = intFromBytes({bytes: value}); 84 | } 85 | 86 | if(decoded !== undefined) { 87 | return new ValueDecoder({decoded}); 88 | } 89 | } 90 | 91 | // try to get a processing-mode-specific type decoder 92 | const decoder = PROCESSING_MODE_TYPE_DECODERS.get(tableType)?.createDecoder( 93 | {value, converter, termInfo}); 94 | if(decoder) { 95 | return decoder; 96 | } 97 | 98 | // if value is not an array, pass it through directly 99 | if(!Array.isArray(value)) { 100 | return new ValueDecoder({decoded: value}); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/codecs/ValueEncoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import { 5 | bytesFromInt, 6 | bytesFromUint, 7 | getTableType 8 | } from '../helpers.js'; 9 | import {Token, Type} from 'cborg'; 10 | import {CborldEncoder} from './CborldEncoder.js'; 11 | import {CborldError} from '../CborldError.js'; 12 | import {MultibaseEncoder} from './MultibaseEncoder.js'; 13 | import {UrlEncoder} from './UrlEncoder.js'; 14 | import {XsdDateEncoder} from './XsdDateEncoder.js'; 15 | import {XsdDateTimeEncoder} from './XsdDateTimeEncoder.js'; 16 | 17 | // constants based on "default" processing mode 18 | const PROCESSING_MODE_TYPE_ENCODERS = new Map([ 19 | ['url', UrlEncoder], 20 | ['https://w3id.org/security#multibase', MultibaseEncoder], 21 | ['http://www.w3.org/2001/XMLSchema#date', XsdDateEncoder], 22 | ['http://www.w3.org/2001/XMLSchema#dateTime', XsdDateTimeEncoder] 23 | ]); 24 | 25 | export class ValueEncoder extends CborldEncoder { 26 | constructor({intValue, convertToBytes, includeSign} = {}) { 27 | super(); 28 | this.intValue = intValue; 29 | this.convertToBytes = convertToBytes; 30 | this.includeSign = includeSign; 31 | } 32 | 33 | encode() { 34 | const {intValue, convertToBytes, includeSign} = this; 35 | if(convertToBytes) { 36 | const toBytes = includeSign ? bytesFromInt : bytesFromUint; 37 | const bytes = toBytes({intValue}); 38 | return new Token(Type.bytes, bytes); 39 | } 40 | return new Token(Type.uint, intValue); 41 | } 42 | 43 | static createEncoder({value, converter, termInfo, termType} = {}) { 44 | const tableType = getTableType({termInfo, termType}); 45 | if(tableType === 'url' && typeof value !== 'string') { 46 | throw new CborldError( 47 | 'ERR_UNSUPPORTED_JSON_TYPE', 48 | `Invalid value type "${typeof value}" for URL; expected "string".`); 49 | } 50 | 51 | // if a subtable exists for `tableType`... 52 | const subTable = converter.strategy.typeTable.get(tableType); 53 | if(subTable) { 54 | let intValue = subTable.get(value); 55 | let convertToBytes; 56 | let includeSign; 57 | if(intValue !== undefined) { 58 | // determine if ID from table must be expressed as bytes for `tableType` 59 | const {typeTableEncodedAsBytesSet} = converter; 60 | convertToBytes = typeTableEncodedAsBytesSet.has(tableType); 61 | includeSign = false; 62 | } else if(tableType !== 'none' && Number.isInteger(value)) { 63 | /* Note: Here is an unusual case that a type subtable has been defined 64 | for a custom type, but the user data still includes a native JSON 65 | integer. This integer has to be encoded to CBOR as bytes to ensure no 66 | conflicts with any subtable values which are encoded as CBOR integers. 67 | Despite being an unusual case, it is supported here by encoding the 68 | integer as a signed integer expressed in bytes (two's complement). */ 69 | intValue = value; 70 | convertToBytes = includeSign = true; 71 | } 72 | if(intValue !== undefined) { 73 | return new ValueEncoder({intValue, convertToBytes, includeSign}); 74 | } 75 | } 76 | 77 | // lastly, try to get a processing-mode-specific type encoder 78 | const encoder = PROCESSING_MODE_TYPE_ENCODERS.get(tableType)?.createEncoder( 79 | {value, converter, termInfo}); 80 | if(encoder) { 81 | return encoder; 82 | } 83 | 84 | // return passthrough value 85 | return value; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/codecs/XsdDateDecoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {CborldDecoder} from './CborldDecoder.js'; 5 | 6 | export class XsdDateDecoder extends CborldDecoder { 7 | constructor({decoded} = {}) { 8 | super(); 9 | this.decoded = decoded; 10 | } 11 | 12 | decode({value} = {}) { 13 | const dateString = new Date(value * 1000).toISOString(); 14 | return dateString.slice(0, dateString.indexOf('T')); 15 | } 16 | 17 | static createDecoder({value} = {}) { 18 | if(typeof value === 'number') { 19 | return new XsdDateDecoder(); 20 | } 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /lib/codecs/XsdDateEncoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {Token, Type} from 'cborg'; 5 | import {CborldEncoder} from './CborldEncoder.js'; 6 | 7 | export class XsdDateEncoder extends CborldEncoder { 8 | constructor({value, parsed} = {}) { 9 | super(); 10 | this.value = value; 11 | this.parsed = parsed; 12 | } 13 | 14 | encode() { 15 | const {value, parsed} = this; 16 | 17 | const secondsSinceEpoch = Math.floor(parsed / 1000); 18 | const dateString = new Date(secondsSinceEpoch * 1000).toISOString(); 19 | const expectedDate = dateString.slice(0, dateString.indexOf('T')); 20 | if(value !== expectedDate) { 21 | // compression would be lossy, do not compress 22 | return new Token(Type.string, value); 23 | } 24 | return new Token(Type.uint, secondsSinceEpoch); 25 | } 26 | 27 | static createEncoder({value} = {}) { 28 | if(value.includes('T')) { 29 | // time included, cannot compress 30 | return; 31 | } 32 | const parsed = Date.parse(value); 33 | if(isNaN(parsed)) { 34 | // no date parsed, cannot compress 35 | return; 36 | } 37 | return new XsdDateEncoder({value, parsed}); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/codecs/XsdDateTimeDecoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {CborldDecoder} from './CborldDecoder.js'; 5 | 6 | export class XsdDateTimeDecoder extends CborldDecoder { 7 | constructor() { 8 | super(); 9 | } 10 | 11 | decode({value} = {}) { 12 | if(typeof value === 'number') { 13 | return new Date(value * 1000).toISOString().replace('.000Z', 'Z'); 14 | } 15 | return new Date(value[0] * 1000 + value[1]).toISOString(); 16 | } 17 | 18 | static createDecoder({value} = {}) { 19 | if(typeof value === 'number') { 20 | return new XsdDateTimeDecoder(); 21 | } 22 | if(Array.isArray(value) && value.length === 2 && 23 | (typeof value[0] === 'number' || typeof value[1] === 'number')) { 24 | return new XsdDateTimeDecoder(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/codecs/XsdDateTimeEncoder.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {Token, Type} from 'cborg'; 5 | import {CborldEncoder} from './CborldEncoder.js'; 6 | 7 | export class XsdDateTimeEncoder extends CborldEncoder { 8 | constructor({value, parsed} = {}) { 9 | super(); 10 | this.value = value; 11 | this.parsed = parsed; 12 | } 13 | 14 | encode() { 15 | const {value, parsed} = this; 16 | const secondsSinceEpoch = Math.floor(parsed / 1000); 17 | const secondsToken = new Token(Type.uint, secondsSinceEpoch); 18 | const millisecondIndex = value.indexOf('.'); 19 | if(millisecondIndex === -1) { 20 | const expectedDate = new Date( 21 | secondsSinceEpoch * 1000).toISOString().replace('.000Z', 'Z'); 22 | if(value !== expectedDate) { 23 | // compression would be lossy, do not compress 24 | return new Token(Type.string, value); 25 | } 26 | // compress with second precision 27 | return secondsToken; 28 | } 29 | 30 | const milliseconds = parseInt(value.slice(millisecondIndex + 1), 10); 31 | const expectedDate = new Date( 32 | secondsSinceEpoch * 1000 + milliseconds).toISOString(); 33 | if(value !== expectedDate) { 34 | // compress would be lossy, do not compress 35 | return new Token(Type.string, value); 36 | } 37 | 38 | // compress with subsecond precision 39 | const entries = [ 40 | secondsToken, 41 | new Token(Type.uint, milliseconds) 42 | ]; 43 | return [new Token(Type.array, entries.length), entries]; 44 | } 45 | 46 | static createEncoder({value} = {}) { 47 | if(!value.includes('T')) { 48 | // no time included, cannot compress 49 | return; 50 | } 51 | const parsed = Date.parse(value); 52 | if(isNaN(parsed)) { 53 | // no date parsed, cannot compress 54 | return; 55 | } 56 | return new XsdDateTimeEncoder({value, parsed}); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/decode.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2025 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {createLegacyTypeTable, createTypeTable} from './tables.js'; 5 | import {CborldError} from './CborldError.js'; 6 | import {Converter} from './Converter.js'; 7 | import {Decompressor} from './Decompressor.js'; 8 | import {inspect} from './util.js'; 9 | import {parse} from './parser.js'; 10 | 11 | /** 12 | * Decodes a CBOR-LD byte array into a JSON-LD document. 13 | * 14 | * @param {object} options - The options to use when decoding CBOR-LD. 15 | * @param {Uint8Array} options.cborldBytes - The encoded CBOR-LD bytes to 16 | * decode. 17 | * @param {documentLoaderFunction} options.documentLoader - The document loader 18 | * to use when resolving JSON-LD Context URLs. 19 | * @param {Function} [options.typeTableLoader] - The `typeTable` loader to use 20 | * to resolve a `registryEntryId` to a `typeTable`. A `typeTable` is a Map of 21 | * possible value types, including `context`, `url`, `none`, and any JSON-LD 22 | * type, each of which maps to another Map of values of that type to their 23 | * associated CBOR-LD integer values. 24 | * @param {diagnosticFunction} [options.diagnose] - A function that, if 25 | * provided, is called with diagnostic information. 26 | * @param {Map} [options.appContextMap] - For use with "legacy-singleton" 27 | * formatted inputs only; a map of context string values to their associated 28 | * CBOR-LD integer values. 29 | * @param {*} [options.typeTable] - NOT permitted; use `typeTableLoader`. 30 | * 31 | * @returns {Promise} - The decoded JSON-LD Document. 32 | */ 33 | export async function decode({ 34 | cborldBytes, 35 | documentLoader, 36 | typeTableLoader, 37 | diagnose, 38 | // for "legacy-singleton" format only 39 | appContextMap, 40 | // no longer permitted 41 | typeTable 42 | } = {}) { 43 | if(!(cborldBytes instanceof Uint8Array)) { 44 | throw new TypeError('"cborldBytes" must be a Uint8Array.'); 45 | } 46 | // throw on `typeTable` param which is no longer permitted 47 | if(typeTable !== undefined) { 48 | throw new TypeError('"typeTable" is not allowed; use "typeTableLoader".'); 49 | } 50 | 51 | // parse CBOR-LD into a header and payload 52 | const {header, payload} = parse({cborldBytes}); 53 | if(!header.payloadCompressed) { 54 | return payload; 55 | } 56 | 57 | // parsed payload is the abstract CBOR-LD input for conversion to JSON-LD 58 | const input = payload; 59 | if(diagnose) { 60 | diagnose('Diagnostic CBOR-LD decompression transform map(s):'); 61 | diagnose(inspect(input, {depth: null, colors: true})); 62 | } 63 | 64 | // get `typeTable` by `registryEntryId` / `format` 65 | const {format, registryEntryId} = header; 66 | if(format === 'legacy-singleton') { 67 | // generate legacy type table 68 | typeTable = createLegacyTypeTable({appContextMap}); 69 | } else if(registryEntryId === 1) { 70 | // use default table (empty) for registry entry ID `1` 71 | typeTable = createTypeTable({typeTable}); 72 | } else { 73 | if(typeof typeTableLoader === 'function') { 74 | typeTable = await typeTableLoader({registryEntryId}); 75 | } 76 | if(!(typeTable instanceof Map)) { 77 | throw new CborldError( 78 | 'ERR_NO_TYPETABLE', 79 | '"typeTable" not found for "registryEntryId" ' + 80 | `"${registryEntryId}".`); 81 | } 82 | // check to make sure unsupported types not present in `typeTable` 83 | if(typeTable.has('http://www.w3.org/2001/XMLSchema#integer') || 84 | typeTable.has('http://www.w3.org/2001/XMLSchema#double') || 85 | typeTable.has('http://www.w3.org/2001/XMLSchema#boolean')) { 86 | throw new CborldError( 87 | 'ERR_UNSUPPORTED_LITERAL_TYPE', 88 | '"typeTable" must not contain XSD integers, doubles, or booleans.'); 89 | } 90 | // normalize type table 91 | typeTable = createTypeTable({typeTable}); 92 | } 93 | 94 | const converter = new Converter({ 95 | // decompress CBOR-LD => JSON-LD 96 | strategy: new Decompressor({typeTable}), 97 | documentLoader, 98 | // FIXME: try to eliminate need for legacy flag 99 | legacy: format === 'legacy-singleton' 100 | }); 101 | const output = await converter.convert({input}); 102 | if(diagnose) { 103 | diagnose('Diagnostic JSON-LD result:'); 104 | diagnose(inspect(output, {depth: null, colors: true})); 105 | } 106 | return output; 107 | } 108 | 109 | /** 110 | * A diagnostic function that is called with diagnostic information. Typically 111 | * set to `console.log` when debugging. 112 | * 113 | * @callback diagnosticFunction 114 | * @param {string} message - The diagnostic message. 115 | */ 116 | 117 | /** 118 | * Fetches a resource given a URL and returns it as a string. 119 | * 120 | * @callback documentLoaderFunction 121 | * @param {string} url - The URL to retrieve. 122 | 123 | * @returns {Promise} The resource associated with the URL as a string. 124 | */ 125 | -------------------------------------------------------------------------------- /lib/encode.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2025 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import * as cborg from 'cborg'; 5 | import * as varint from 'varint'; 6 | import {createLegacyTypeTable, createTypeTable} from './tables.js'; 7 | import {CborldEncoder} from './codecs/CborldEncoder.js'; 8 | import {CborldError} from './CborldError.js'; 9 | import {Compressor} from './Compressor.js'; 10 | import {Converter} from './Converter.js'; 11 | import {inspect} from './util.js'; 12 | 13 | // override cborg object encoder to use cborld encoders 14 | const typeEncoders = { 15 | Object(obj) { 16 | if(obj instanceof CborldEncoder) { 17 | return obj.encode({obj}); 18 | } 19 | } 20 | }; 21 | 22 | /** 23 | * Encodes a given JSON-LD document into a CBOR-LD byte array. 24 | * 25 | * @param {object} options - The options to use when encoding to CBOR-LD. 26 | * @param {object} options.jsonldDocument - The JSON-LD Document to convert to 27 | * CBOR-LD bytes. 28 | * @param {documentLoaderFunction} options.documentLoader - The document loader 29 | * to use when resolving JSON-LD Context URLs. 30 | * @param {string} [options.format='cbor-ld-1.0'] - The CBOR-LD output format 31 | * to use; this will default to `cbor-ld-1.0` to use CBOR-LD 1.0 tag 32 | * `0xcb1d` (51997); to create output with a pre-1.0 CBOR-LD tag, then 33 | * 'legacy-range' can be passed to use tags `0x0600-0x06ff` (1526-1791) and 34 | * 'legacy-singleton' can be passed to use tags `0x0500-0x0501`. 35 | * @param {number|string} [options.registryEntryId] - The registry 36 | * entry ID for the registry entry associated with the resulting CBOR-LD 37 | * payload. 38 | * @param {Function} [options.typeTableLoader] - The `typeTable` loader to use 39 | * to resolve `registryEntryId` to a `typeTable`. A `typeTable` is a Map of 40 | * possible value types, including `context`, `url`, `none`, and any JSON-LD 41 | * type, each of which maps to another Map of values of that type to their 42 | * associated CBOR-LD integer values. 43 | * @param {diagnosticFunction} [options.diagnose] - A function that, if 44 | * provided, is called with diagnostic information. 45 | * @param {Map} [options.appContextMap] - Only for use with the 46 | * 'legacy-singleton' format. 47 | * @param {number} [options.compressionMode] - Only for use with the 48 | * 'legacy-singleton' format. 49 | * @param {*} [options.typeTable] - NOT permitted; use `typeTableLoader`. 50 | * 51 | * @returns {Promise} - The encoded CBOR-LD bytes. 52 | */ 53 | export async function encode({ 54 | jsonldDocument, 55 | documentLoader, 56 | format = 'cbor-ld-1.0', 57 | registryEntryId, 58 | typeTableLoader, 59 | diagnose, 60 | // for "legacy-singleton" format only 61 | appContextMap, 62 | compressionMode, 63 | // no longer permitted 64 | typeTable 65 | } = {}) { 66 | // validate `format` 67 | if(!(format === 'cbor-ld-1.0' || format === 'legacy-range' || 68 | format === 'legacy-singleton')) { 69 | throw new TypeError( 70 | `Invalid "format" "${format}"; "format" must be ` + 71 | '"cbor-ld-1.0" (tag 0xcb1d = 51997) or ' + 72 | '"legacy-range" (tags 0x0600-0x06ff = 1536-1791) or ' + 73 | '"legacy-singleton" (tags 0x0500-0x0501 = 1280-1281).'); 74 | } 75 | // throw on `typeTable` param which is no longer permitted 76 | if(typeTable !== undefined) { 77 | throw new TypeError('"typeTable" is not allowed; use "typeTableLoader".'); 78 | } 79 | 80 | // validate parameter combinations 81 | let compressPayload; 82 | if(format === 'legacy-singleton') { 83 | if(registryEntryId !== undefined) { 84 | throw new TypeError( 85 | '"registryEntryId" must not be used with format "legacy-singleton".'); 86 | } 87 | if(typeTableLoader !== undefined) { 88 | throw new TypeError( 89 | '"typeTableLoader" must not be used with format "legacy-singleton".'); 90 | } 91 | 92 | // default compression mode to `1` 93 | compressionMode = compressionMode ?? 1; 94 | if(!(compressionMode === 0 || compressionMode === 1)) { 95 | throw new TypeError( 96 | '"compressionMode" must be "0" (no compression) or "1" ' + 97 | 'for compression mode version 1.'); 98 | } 99 | compressPayload = compressionMode === 1; 100 | 101 | // generate legacy type table 102 | typeTable = createLegacyTypeTable({typeTable, appContextMap}); 103 | } else { 104 | // validate that an acceptable value for `registryEntryId` was passed 105 | if(!(typeof registryEntryId === 'number' && registryEntryId >= 0)) { 106 | throw new TypeError('"registryEntryId" must be a non-negative integer.'); 107 | } 108 | 109 | if(appContextMap !== undefined) { 110 | throw new TypeError( 111 | '"appContextMap" must only be used with format "legacy-singleton".'); 112 | } 113 | if(compressionMode !== undefined) { 114 | throw new TypeError( 115 | '"compressionMode" must only be used with format "legacy-singleton".'); 116 | } 117 | if(registryEntryId !== 0) { 118 | if(registryEntryId === 1) { 119 | // use default table (empty) for registry entry ID `1` 120 | typeTable = createTypeTable({typeTable}); 121 | } else { 122 | if(typeof typeTableLoader !== 'function') { 123 | throw new TypeError('"typeTableLoader" must be a function.'); 124 | } 125 | typeTable = await typeTableLoader({registryEntryId}); 126 | if(!(typeTable instanceof Map)) { 127 | throw new CborldError( 128 | 'ERR_NO_TYPETABLE', 129 | '"typeTable" not found for "registryEntryId" ' + 130 | `"${registryEntryId}".`); 131 | } 132 | // check to make sure unsupported types not present in `typeTable` 133 | if(typeTable.has('http://www.w3.org/2001/XMLSchema#integer') || 134 | typeTable.has('http://www.w3.org/2001/XMLSchema#double') || 135 | typeTable.has('http://www.w3.org/2001/XMLSchema#boolean')) { 136 | throw new CborldError( 137 | 'ERR_UNSUPPORTED_LITERAL_TYPE', 138 | '"typeTable" must not contain XSD integers, doubles, or booleans.'); 139 | } 140 | // normalize type table 141 | typeTable = createTypeTable({typeTable}); 142 | } 143 | } 144 | // any registry entry ID other than zero uses compression by default 145 | compressPayload = registryEntryId !== 0; 146 | } 147 | 148 | // compute CBOR-LD suffix 149 | let suffix; 150 | if(!compressPayload) { 151 | // output uncompressed CBOR-LD 152 | suffix = cborg.encode(jsonldDocument); 153 | } else { 154 | const converter = new Converter({ 155 | // compress JSON-LD => CBOR-LD 156 | strategy: new Compressor({typeTable}), 157 | documentLoader, 158 | legacy: format === 'legacy-singleton' 159 | }); 160 | const output = await converter.convert({input: jsonldDocument}); 161 | if(diagnose) { 162 | diagnose('Diagnostic CBOR-LD compression transform map(s):'); 163 | diagnose(inspect(output, {depth: null, colors: true})); 164 | } 165 | suffix = cborg.encode(output, {typeEncoders}); 166 | } 167 | 168 | // concatenate prefix and suffix 169 | const prefix = _getPrefix({format, compressionMode, registryEntryId}); 170 | const length = prefix.length + suffix.length; 171 | const bytes = new Uint8Array(length); 172 | bytes.set(prefix); 173 | bytes.set(suffix, prefix.length); 174 | 175 | if(diagnose) { 176 | diagnose('Diagnostic CBOR-LD result:'); 177 | diagnose(inspect(bytes, {depth: null, colors: true})); 178 | } 179 | 180 | return bytes; 181 | } 182 | 183 | function _getPrefix({format, compressionMode, registryEntryId}) { 184 | if(format === 'legacy-singleton') { 185 | return new Uint8Array([ 186 | 0xd9, // CBOR major type 6 + 2 byte tag size 187 | 0x05, // legacy CBOR-LD tag 188 | compressionMode // compression flag 189 | ]); 190 | } 191 | 192 | if(format === 'legacy-range') { 193 | if(registryEntryId < 128) { 194 | return new Uint8Array([ 195 | 0xd9, // CBOR major type 6 + 2 byte tag size 196 | 0x06, // non-legacy CBOR-LD tag 197 | registryEntryId // low-value type table id 198 | // encoded document appended in caller 199 | ]); 200 | } 201 | const idVarint = varint.encode(registryEntryId); 202 | 203 | return new Uint8Array([ 204 | 0xd9, // CBOR major type 6 + 2 byte tag size 205 | 0x06, // non-legacy CBOR-LD tag 206 | idVarint[0], 207 | ...[ 208 | 0x82, // 2 element array 209 | ...cborg.encode(Uint8Array.from(idVarint.slice(1))) 210 | // encoded document appended as second element in caller 211 | ] 212 | ]); 213 | } 214 | 215 | // otherwise, use current tag system 216 | return new Uint8Array([ 217 | 0xd9, // CBOR major type 6 + 2 byte tag size 218 | 0xcb, 0x1d, // CBOR-LD 1.0 tag 219 | ...[ 220 | 0x82, // 2 element array 221 | ...cborg.encode(registryEntryId) 222 | // encoded document appended as second element in caller 223 | ] 224 | ]); 225 | } 226 | 227 | /** 228 | * A diagnostic function that is called with diagnostic information. Typically 229 | * set to `console.log` when debugging. 230 | * 231 | * @callback diagnosticFunction 232 | * @param {string} message - The diagnostic message. 233 | */ 234 | 235 | /** 236 | * Fetches a resource given a URL and returns it as a string. 237 | * 238 | * @callback documentLoaderFunction 239 | * @param {string} url - The URL to retrieve. 240 | 241 | * @returns {Promise} The resource associated with the URL as a string. 242 | */ 243 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {CborldError} from './CborldError.js'; 5 | 6 | // if a value with `termType` was compressed using the type table, then 7 | // the encoded value will either be a `number` or a `Uint8Array` 8 | export const LEGACY_TYPE_TABLE_ENCODED_AS_BYTES = new Set([ 9 | /* Note: In legacy mode, the `url` table values are encoded as integers, 10 | which means that if the legacy global URL table is appended to and an 11 | encoder uses the new version whilst a decoder uses the old version, the 12 | decoder might misinterpret the new entry as some term from a context. This 13 | can also happen with `appContextMap`, but could only occur if the number 14 | of terms used in the contexts in play were greater than 32K. The non-legacy 15 | version of CBOR-LD encodes values from the URL table as bytes, preventing 16 | any possibility of collision -- and any different view of a table would 17 | instead result in an error. */ 18 | 'none', 19 | 'http://www.w3.org/2001/XMLSchema#date', 20 | 'http://www.w3.org/2001/XMLSchema#dateTime' 21 | ]); 22 | export const TYPE_TABLE_ENCODED_AS_BYTES = new Set( 23 | LEGACY_TYPE_TABLE_ENCODED_AS_BYTES); 24 | TYPE_TABLE_ENCODED_AS_BYTES.add('url'); 25 | 26 | export function bytesFromUint({intValue}) { 27 | let buffer; 28 | let dataview; 29 | if(intValue < 0xFF) { 30 | buffer = new ArrayBuffer(1); 31 | dataview = new DataView(buffer); 32 | dataview.setUint8(0, intValue); 33 | } else if(intValue < 0xFFFF) { 34 | buffer = new ArrayBuffer(2); 35 | dataview = new DataView(buffer); 36 | dataview.setUint16(0, intValue); 37 | } else if(intValue < 0xFFFFFFFF) { 38 | buffer = new ArrayBuffer(4); 39 | dataview = new DataView(buffer); 40 | dataview.setUint32(0, intValue); 41 | } else if(intValue < Number.MAX_SAFE_INTEGER) { 42 | buffer = new ArrayBuffer(8); 43 | dataview = new DataView(buffer); 44 | dataview.setBigUint64(0, BigInt(intValue)); 45 | } else { 46 | throw new CborldError( 47 | 'ERR_COMPRESSION_VALUE_TOO_LARGE', 48 | `Compression value "${intValue}" too large.`); 49 | } 50 | const bytes = new Uint8Array(buffer); 51 | return bytes; 52 | } 53 | 54 | export function bytesFromInt({intValue}) { 55 | let buffer; 56 | let dataview; 57 | if(intValue < 0x7F) { 58 | buffer = new ArrayBuffer(1); 59 | dataview = new DataView(buffer); 60 | dataview.setInt8(0, intValue); 61 | } else if(intValue < 0x7FFF) { 62 | buffer = new ArrayBuffer(2); 63 | dataview = new DataView(buffer); 64 | dataview.setInt16(0, intValue); 65 | } else if(intValue < 0x7FFFFFFF) { 66 | buffer = new ArrayBuffer(4); 67 | dataview = new DataView(buffer); 68 | dataview.setInt32(0, intValue); 69 | } else if(intValue < Number.MAX_SAFE_INTEGER) { 70 | buffer = new ArrayBuffer(8); 71 | dataview = new DataView(buffer); 72 | dataview.setBigInt64(0, BigInt(intValue)); 73 | } else { 74 | throw new CborldError( 75 | 'ERR_COMPRESSION_VALUE_TOO_LARGE', 76 | `Compression value "${intValue}" too large.`); 77 | } 78 | const bytes = new Uint8Array(buffer); 79 | return bytes; 80 | } 81 | 82 | // FIXME: consider moving this out of helpers elsewhere 83 | export function getTableType({termInfo, termType}) { 84 | const {term, def} = termInfo; 85 | 86 | // handle `@id`, `@type`, their aliases, and `@vocab` 87 | if(term === '@id' || def['@id'] === '@id' || 88 | term === '@type' || def['@id'] === '@type' || 89 | termType === '@id' || termType === '@vocab') { 90 | return 'url'; 91 | } 92 | return termType ?? 'none'; 93 | } 94 | 95 | export function uintFromBytes({bytes}) { 96 | const buffer = bytes.buffer; 97 | const dataview = new DataView(buffer); 98 | let intValue; 99 | if(dataview.byteLength === 1) { 100 | intValue = dataview.getUint8(0); 101 | } else if(dataview.byteLength === 2) { 102 | intValue = dataview.getUint16(0); 103 | } else if(dataview.byteLength === 4) { 104 | intValue = dataview.getUint32(0); 105 | } else { 106 | throw new CborldError( 107 | 'ERR_UNRECOGNIZED_BYTES', 108 | `Improperly formatted bytes "${bytes}" found.`); 109 | } 110 | return intValue; 111 | } 112 | 113 | export function intFromBytes({bytes}) { 114 | const buffer = bytes.buffer; 115 | const dataview = new DataView(buffer); 116 | let intValue; 117 | if(dataview.byteLength === 1) { 118 | intValue = dataview.getInt8(0); 119 | } else if(dataview.byteLength === 2) { 120 | intValue = dataview.getInt16(0); 121 | } else if(dataview.byteLength === 4) { 122 | intValue = dataview.getInt32(0); 123 | } else if(dataview.byteLength === 8) { 124 | intValue = dataview.getBigInt64(0); 125 | if(intValue > Number.MAX_SAFE_INTEGER) { 126 | throw new CborldError( 127 | 'ERR_COMPRESSION_VALUE_TOO_LARGE', 128 | `Compression value "${intValue}" too large.`); 129 | } 130 | intValue = Number(intValue); 131 | } else { 132 | throw new CborldError( 133 | 'ERR_UNRECOGNIZED_BYTES', 134 | `Improperly formatted bytes "${bytes}" found.`); 135 | } 136 | return intValue; 137 | } 138 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2021 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | export {decode} from './decode.js'; 5 | export {encode} from './encode.js'; 6 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2025 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {decode, Token, Tokenizer, Type} from 'cborg'; 5 | import {CborldError} from './CborldError.js'; 6 | import {default as varint} from 'varint'; 7 | 8 | const FORMAT_1_0 = 'cbor-ld-1.0'; 9 | const FORMAT_LEGACY_RANGE = 'legacy-range'; 10 | const FORMAT_LEGACY_SINGLETON = 'legacy-singleton'; 11 | 12 | // 0xd9 == 11011001 13 | // 110 = CBOR major type 6 14 | // 11001 = 25, 16-bit tag size (65536 possible values) 15 | // 0xcb1d = the first 16-bits of a CBOR-LD 1.0 tag 16 | // 0x0600-0x06ff = the first 16-bits of a legacy range CBOR-LD tag 17 | // 0x0500-0x0501 = the first 16-bits of a legacy singleton CBOR-LD tag 18 | 19 | // hex = 0xcb1d; modern implementations only need to implement this tag 20 | const TAG_1_0 = 51997; 21 | // hex = 0x0600; rarely used tag 22 | const TAG_LEGACY_RANGE_START = 1536; 23 | // hex = 0x06ff; rarely used tag 24 | const TAG_LEGACY_RANGE_END = 1791; 25 | // hex = 0x0500; some legacy production software uses this tag 26 | const TAG_LEGACY_UNCOMPRESSED = 1280; 27 | // hex = 0x0501; some legacy production software uses this tag 28 | const TAG_LEGACY_COMPRESSED = 1281; 29 | 30 | class Parser extends Tokenizer { 31 | constructor(data, options) { 32 | super(data, options); 33 | this.expectedTokenTypes = [Type.tag]; 34 | this.header = null; 35 | this.headerParsed = false; 36 | this.shouldParseRemainder = false; 37 | } 38 | 39 | next() { 40 | while(true) { 41 | // finalize parsing if requested 42 | if(this.shouldParseRemainder) { 43 | // parse remainder, only use maps when compression is on 44 | return this._parseRemainder({useMaps: this.header.payloadCompressed}); 45 | } 46 | 47 | // get next token to consider 48 | const nextToken = super.next(); 49 | 50 | // header fully parsed, pass any token through 51 | if(this.headerParsed) { 52 | return nextToken; 53 | } 54 | 55 | // check expected token type 56 | const expectedTokenType = this.expectedTokenTypes.shift(); 57 | if(nextToken.type !== expectedTokenType) { 58 | throw new CborldError( 59 | 'ERR_NOT_CBORLD', 60 | `Unexpected CBOR type "${nextToken.type.name}" in CBOR-LD data; ` + 61 | `expecting "${expectedTokenType.name}".`); 62 | } 63 | 64 | // start parsing header 65 | const {header} = this; 66 | if(header === null) { 67 | this._handleTag(nextToken); 68 | continue; 69 | } 70 | 71 | // no more expected tokens for parsing the header; finish parsing it 72 | if(this.expectedTokenTypes.length === 0) { 73 | // get registry entry ID 74 | header.registryEntryId = header.format === FORMAT_LEGACY_RANGE ? 75 | this._varintToRegistryEntryId({nextToken}) : nextToken.value; 76 | // anything other than `registryEntryId === 0` means compression 77 | header.payloadCompressed = header.registryEntryId !== 0; 78 | if(!header.payloadCompressed) { 79 | // set flag to cause remainder to be parsed on next call to `next()` 80 | this.shouldParseRemainder = true; 81 | } 82 | this.headerParsed = true; 83 | } 84 | 85 | return nextToken; 86 | } 87 | } 88 | 89 | _handleTag(tagToken) { 90 | const header = { 91 | format: '', 92 | registryEntryId: undefined, 93 | tagToken, 94 | payloadCompressed: false 95 | }; 96 | this.header = header; 97 | const {value: tag} = tagToken; 98 | 99 | if(tag === TAG_1_0) { 100 | header.format = FORMAT_1_0; 101 | this.expectedTokenTypes = [Type.array, Type.uint]; 102 | } else if(tag >= TAG_LEGACY_RANGE_START && tag <= TAG_LEGACY_RANGE_END) { 103 | header.format = FORMAT_LEGACY_RANGE; 104 | // registry entry ID is expressed as varint where the first byte of the 105 | // varint resides in the tag; if the tag minus the start of the legacy 106 | // tag range is under `128` then the varint is 1 byte and the registry 107 | // entry ID is that value 108 | const registryEntryIdStart = tag - TAG_LEGACY_RANGE_START; 109 | if(registryEntryIdStart < 128) { 110 | header.registryEntryId = registryEntryIdStart; 111 | // anything other than `registryEntryId === 0` means compression 112 | header.payloadCompressed = header.registryEntryId !== 0; 113 | this.shouldParseRemainder = true; 114 | this.headerParsed = true; 115 | } else { 116 | // remainder of varint is expressed as bytes in an array's 1st element 117 | this.expectedTokenTypes = [Type.array, Type.bytes]; 118 | } 119 | } else if(tag === TAG_LEGACY_UNCOMPRESSED || 120 | tag === TAG_LEGACY_COMPRESSED) { 121 | header.format = FORMAT_LEGACY_SINGLETON; 122 | this.header.payloadCompressed = tag === TAG_LEGACY_COMPRESSED; 123 | this.shouldParseRemainder = true; 124 | this.headerParsed = true; 125 | } else { 126 | throw new CborldError( 127 | 'ERR_NOT_CBORLD', `Unknown CBOR-LD tag "${tag}".`); 128 | } 129 | } 130 | 131 | _varintToRegistryEntryId({nextToken}) { 132 | // get varint bytes that express the registry entry ID by using the 133 | // relative tag value (i.e., subtracting start of legacy range from the 134 | // tag value) and appending the parsed byte array 135 | const varintBytes = new Uint8Array(nextToken.value.length + 1); 136 | varintBytes[0] = this.header.tagToken.value - TAG_LEGACY_RANGE_START; 137 | varintBytes.set(nextToken.value, 1); 138 | // ensure varint isn't too large 139 | if(varintBytes.length >= 24) { 140 | throw new CborldError( 141 | 'ERR_NOT_CBORLD', 'CBOR-LD encoded registry entry ID too large.'); 142 | } 143 | // decode varint and ensure it is well formed 144 | const registryEntryId = varint.decode(varintBytes); 145 | if(varint.decode.bytes !== varintBytes.length) { 146 | throw new CborldError( 147 | 'ERR_NOT_CBORLD', 'CBOR-LD registry entry ID varint encoding error.'); 148 | } 149 | return registryEntryId; 150 | } 151 | 152 | // this function is needed to parse the remaining bytes while allowing the 153 | // value of `useMaps` to change; the underlying `cborg` lib doesn't allow 154 | // the option to be changed after parsing has started -- and it must be 155 | // `false` for uncompressed payloads and `true` for compressed ones 156 | _parseRemainder({useMaps}) { 157 | const remainder = this.data.subarray(this.pos()); 158 | this._pos += remainder.length; 159 | const value = decode(remainder, {useMaps}); 160 | // return as undefined token which is "terminal" and will cause the 161 | // `value` to be used directly 162 | return new Token(Type.undefined, value); 163 | } 164 | } 165 | 166 | export function parse({cborldBytes} = {}) { 167 | const options = {tokenizer: null, useMaps: true}; 168 | options.tokenizer = new Parser(cborldBytes, options); 169 | const suffix = decode(cborldBytes, options); 170 | const {header} = options.tokenizer; 171 | if(header.format === FORMAT_1_0 || header.format === FORMAT_LEGACY_RANGE) { 172 | return {header, payload: Array.isArray(suffix) ? suffix[1] : suffix}; 173 | } 174 | return {header, payload: suffix}; 175 | } 176 | -------------------------------------------------------------------------------- /lib/tables.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | export const KEYWORDS_TABLE = new Map([ 5 | // ordered is important, do not change 6 | ['@context', 0], 7 | ['@type', 2], 8 | ['@id', 4], 9 | ['@value', 6], 10 | // alphabetized after `@context`, `@type`, `@id`, `@value` 11 | // IDs <= 24 represented with 1 byte, IDs > 24 use 2+ bytes 12 | ['@direction', 8], 13 | ['@graph', 10], 14 | ['@included', 12], 15 | ['@index', 14], 16 | ['@json', 16], 17 | ['@language', 18], 18 | ['@list', 20], 19 | ['@nest', 22], 20 | ['@reverse', 24], 21 | // these only appear in frames and contexts, not docs 22 | ['@base', 26], 23 | ['@container', 28], 24 | ['@default', 30], 25 | ['@embed', 32], 26 | ['@explicit', 34], 27 | ['@none', 36], 28 | ['@omitDefault', 38], 29 | ['@prefix', 40], 30 | ['@preserve', 42], 31 | ['@protected', 44], 32 | ['@requireAll', 46], 33 | ['@set', 48], 34 | ['@version', 50], 35 | ['@vocab', 52], 36 | // `@propagate` added later 37 | ['@propagate', 54] 38 | ]); 39 | 40 | /** 41 | * These are from the legacy registry. 42 | * 43 | * @see https://digitalbazaar.github.io/cbor-ld-spec/#term-codec-registry 44 | */ 45 | export const STRING_TABLE = new Map([ 46 | // 0x00 - 0x0F: reserved 47 | ['https://www.w3.org/ns/activitystreams', 16], 48 | ['https://www.w3.org/2018/credentials/v1', 17], 49 | ['https://www.w3.org/ns/did/v1', 18], 50 | ['https://w3id.org/security/suites/ed25519-2018/v1', 19], 51 | ['https://w3id.org/security/suites/ed25519-2020/v1', 20], 52 | ['https://w3id.org/cit/v1', 21], 53 | ['https://w3id.org/age/v1', 22], 54 | ['https://w3id.org/security/suites/x25519-2020/v1', 23], 55 | ['https://w3id.org/veres-one/v1', 24], 56 | ['https://w3id.org/webkms/v1', 25], 57 | ['https://w3id.org/zcap/v1', 26], 58 | ['https://w3id.org/security/suites/hmac-2019/v1', 27], 59 | ['https://w3id.org/security/suites/aes-2019/v1', 28], 60 | ['https://w3id.org/vaccination/v1', 29], 61 | ['https://w3id.org/vc-revocation-list-2020/v1', 30], 62 | ['https://w3id.org/dcc/v1', 31], 63 | ['https://w3id.org/vc/status-list/v1', 32], 64 | ['https://www.w3.org/ns/credentials/v2', 33], 65 | // 0x22 - 0x2F (34-47): available 66 | ['https://w3id.org/security/data-integrity/v1', 48], 67 | ['https://w3id.org/security/multikey/v1', 49], 68 | // 0x32 (50): reserved (in legacy spec) 69 | // FIXME: Unclear on how to handle the openbadges URLs and versioning. 70 | // This value was never in the spec, but was added here, and potentially 71 | // elsewhere. 72 | ['https://purl.imsglobal.org/spec/ob/v3p0/context.json', 50], 73 | ['https://w3id.org/security/data-integrity/v2', 51] 74 | // 0x34 - 0x36 (52-54): reserved; removed experimental cryptosuite 75 | // registrations 76 | ]); 77 | 78 | export const URL_SCHEME_TABLE = new Map([ 79 | ['http://', 1], 80 | ['https://', 2], 81 | ['urn:uuid:', 3], 82 | ['data:', 4], 83 | ['did:v1:nym:', 1024], 84 | ['did:key:', 1025] 85 | ]); 86 | 87 | export const REVERSE_URL_SCHEME_TABLE = reverseMap(URL_SCHEME_TABLE); 88 | 89 | const cryptosuiteTypedTable = new Map([ 90 | ['ecdsa-rdfc-2019', 1], 91 | ['ecdsa-sd-2023', 2], 92 | ['eddsa-rdfc-2022', 3], 93 | ['ecdsa-xi-2023', 4] 94 | ]); 95 | 96 | export const TYPE_TABLE = new Map([ 97 | ['context', STRING_TABLE], 98 | ['url', STRING_TABLE], 99 | ['none', STRING_TABLE], 100 | ['https://w3id.org/security#cryptosuiteString', cryptosuiteTypedTable] 101 | ]); 102 | export const FIRST_CUSTOM_TERM_ID = 100; 103 | 104 | export function createLegacyTypeTable({typeTable, appContextMap} = {}) { 105 | if(typeTable) { 106 | throw new TypeError( 107 | '"typeTable" must not be passed when using "legacy" mode.'); 108 | } 109 | 110 | // generate legacy type table 111 | typeTable = new Map(TYPE_TABLE); 112 | 113 | if(appContextMap) { 114 | // add `appContextMap` to legacy mode combined string table 115 | const stringTable = new Map(STRING_TABLE); 116 | for(const [key, value] of appContextMap) { 117 | stringTable.set(key, value); 118 | } 119 | typeTable.set('context', stringTable); 120 | typeTable.set('url', stringTable); 121 | typeTable.set('none', stringTable); 122 | } 123 | 124 | return typeTable; 125 | } 126 | 127 | export function createTypeTable({typeTable} = {}) { 128 | // ensure `typeTable` has empty maps for core types 129 | typeTable = new Map(typeTable); 130 | if(!typeTable.has('context')) { 131 | typeTable.set('context', new Map()); 132 | } 133 | if(!typeTable.has('url')) { 134 | typeTable.set('url', new Map()); 135 | } 136 | if(!typeTable.has('none')) { 137 | typeTable.set('none', new Map()); 138 | } 139 | return typeTable; 140 | } 141 | 142 | export function reverseMap(m) { 143 | return new Map(Array.from(m, e => e.reverse())); 144 | } 145 | -------------------------------------------------------------------------------- /lib/util-browser.js: -------------------------------------------------------------------------------- 1 | // browser support 2 | /* eslint-env browser */ 3 | /* eslint-disable-next-line no-unused-vars */ 4 | export function inspect(data, options) { 5 | return JSON.stringify(data, null, 2); 6 | } 7 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | export {inspect} from 'node:util'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@digitalbazaar/cborld", 3 | "version": "8.0.2-0", 4 | "description": "A CBOR-LD encoder/decoder for Javascript.", 5 | "license": "BSD-3-Clause", 6 | "author": { 7 | "name": "Digital Bazaar, Inc.", 8 | "email": "support@digitalbazaar.com", 9 | "url": "https://digitalbazaar.com/" 10 | }, 11 | "homepage": "https://github.com/digitalbazaar/cborld", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/digitalbazaar/cborld" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/digitalbazaar/cborld/issues" 18 | }, 19 | "keywords": [ 20 | "json-ld", 21 | "cbor", 22 | "linked data", 23 | "compression" 24 | ], 25 | "type": "module", 26 | "exports": "./lib/index.js", 27 | "browser": { 28 | "./lib/util.js": "./lib/util-browser.js" 29 | }, 30 | "files": [ 31 | "lib/**/*.js" 32 | ], 33 | "engines": { 34 | "node": ">=18" 35 | }, 36 | "scripts": { 37 | "test": "npm run test-node", 38 | "test-node": "cross-env NODE_ENV=test mocha --preserve-symlinks -t 30000 -A -R ${REPORTER:-spec} tests/*.spec.js", 39 | "test-karma": "karma start karma.conf.cjs", 40 | "test-watch": "cross-env NODE_ENV=test mocha --watch --preserve-symlinks -t 30000 -A -R ${REPORTER:-spec} tests/*.spec.js", 41 | "coverage": "cross-env NODE_ENV=test c8 npm run test-node", 42 | "coverage-ci": "cross-env NODE_ENV=test c8 --reporter=lcovonly --reporter=text-summary --reporter=text npm run test-node", 43 | "coverage-report": "c8 report", 44 | "lint": "eslint --ext .cjs,.js ." 45 | }, 46 | "dependencies": { 47 | "base58-universal": "^2.0.0", 48 | "base64url-universal": "^2.0.0", 49 | "cborg": "^4.2.2", 50 | "js-base64": "^3.7.7", 51 | "uuid": "^10.0.0", 52 | "varint": "^6.0.0" 53 | }, 54 | "devDependencies": { 55 | "@digitalbazaar/did-method-key": "^5.2.0", 56 | "@digitalbazaar/ed25519-multikey": "^1.1.0", 57 | "@digitalbazaar/multikey-context": "^2.0.1", 58 | "c8": "^10.1.2", 59 | "chai": "^4.4.1", 60 | "chai-bytes": "^0.1.2", 61 | "cit-context": "^2.0.1", 62 | "citizenship-context": "^3.0.0", 63 | "credentials-context": "^2.0.0", 64 | "cross-env": "^7.0.3", 65 | "did-context": "^3.1.1", 66 | "ed25519-signature-2020-context": "^1.1.0", 67 | "eslint": "^8.57.0", 68 | "eslint-config-digitalbazaar": "^5.2.0", 69 | "eslint-plugin-jsdoc": "^48.5.0", 70 | "eslint-plugin-unicorn": "^54.0.0", 71 | "karma": "^6.4.3", 72 | "karma-chai": "^0.1.0", 73 | "karma-chrome-launcher": "^3.2.0", 74 | "karma-mocha": "^2.0.1", 75 | "karma-mocha-reporter": "^2.2.5", 76 | "karma-sourcemap-loader": "^0.4.0", 77 | "karma-webpack": "^5.0.1", 78 | "mocha": "^10.5.2", 79 | "mocha-lcov-reporter": "^1.3.0", 80 | "webpack": "^5.92.1" 81 | }, 82 | "c8": { 83 | "reporter": [ 84 | "lcov", 85 | "text-summary", 86 | "text" 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/UuidUrnDecoder.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {expect} from 'chai'; 5 | import {UuidUrnDecoder} from '../lib/codecs/UuidUrnDecoder.js'; 6 | 7 | describe('UuidUrnDecoder', () => { 8 | it('should not create an instance if value is an array with length != 2', 9 | async () => { 10 | const value = []; 11 | const decoder = UuidUrnDecoder.createDecoder({value}); 12 | expect(decoder).to.be.undefined; 13 | }); 14 | 15 | // FIXME: add more tests 16 | }); 17 | -------------------------------------------------------------------------------- /tests/UuidUrnEncoder.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {expect} from 'chai'; 5 | import {UuidUrnEncoder} from '../lib/codecs/UuidUrnEncoder.js'; 6 | 7 | describe('UuidUrnEncoder', () => { 8 | it('should not create an instance if value does not start with "urn:uuid:"', 9 | async () => { 10 | const value = 'not:urn:uuid:c828c352-cb59-4ca9-b769-6a6a91008730'; 11 | const encoder = UuidUrnEncoder.createEncoder({value}); 12 | expect(encoder).to.be.undefined; 13 | }); 14 | 15 | it('should create an instance if value starts with "urn:uuid:"', async () => { 16 | const value = 'urn:uuid:c828c352-cb59-4ca9-b769-6a6a91008730'; 17 | const encoder = UuidUrnEncoder.createEncoder({value}); 18 | expect(encoder).to.be.instanceOf(UuidUrnEncoder); 19 | }); 20 | 21 | // FIXME: add more tests 22 | }); 23 | -------------------------------------------------------------------------------- /tests/XsdDateDecoder.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2021 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | // import { 5 | // expect 6 | // } from 'chai'; 7 | // import {XsdDateDecoder} from '../lib/codecs/XsdDateDecoder.js'; 8 | 9 | describe.skip('XsdDateDecoder', () => { 10 | /* 11 | it('should roundtrip encode-decode successfully', async () => { 12 | const input = '2020-07-14'; 13 | const mockEncoder = new MockEncoder(); 14 | 15 | const encoderCodec = new XsdDateCodec(); 16 | encoderCodec.set({value: input}); 17 | encoderCodec.encodeCBOR(mockEncoder); 18 | 19 | const decoderCodec = new XsdDateCodec(); 20 | decoderCodec.set({value: mockEncoder.result}); 21 | const decodedResult = decoderCodec.decodeCBOR(); 22 | 23 | expect(input).equal(decodedResult); 24 | }); 25 | 26 | it.skip('should throw exception when input includes time component', 27 | async () => { 28 | const input = '2020-07-14T19:23:24Z'; 29 | const mockEncoder = new MockEncoder(); 30 | 31 | const encoderCodec = new XsdDateCodec(); 32 | encoderCodec.set({value: input}); 33 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 34 | .to 35 | .throw('Invalid input, date includes time \ 36 | component lost of precision expected'); 37 | }); 38 | 39 | it('should throw exception when input is not a date string', async () => { 40 | const input = 'bad'; 41 | const mockEncoder = new MockEncoder(); 42 | 43 | const encoderCodec = new XsdDateCodec(); 44 | encoderCodec.set({value: input}); 45 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 46 | .to 47 | .throw('Invalid input, could not parse value as date'); 48 | }); 49 | 50 | it('should throw exception when input is an empty string', async () => { 51 | const input = ''; 52 | const mockEncoder = new MockEncoder(); 53 | 54 | const encoderCodec = new XsdDateCodec(); 55 | encoderCodec.set({value: input}); 56 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 57 | .to 58 | .throw('Invalid input, could not parse value as date'); 59 | }); 60 | 61 | it('should throw exception when input is not valid value type', async () => { 62 | const input = true; 63 | const mockEncoder = new MockEncoder(); 64 | 65 | const encoderCodec = new XsdDateCodec(); 66 | encoderCodec.set({value: input}); 67 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 68 | .to 69 | .throw('Invalid input, expected value to be of type string'); 70 | }); 71 | */ 72 | }); 73 | -------------------------------------------------------------------------------- /tests/XsdDateEncoder.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | // import { 5 | // expect 6 | // } from 'chai'; 7 | // import {XsdDateEncoder} from '../lib/codecs/XsdDateEncoder.js'; 8 | 9 | describe.skip('XsdDateEncoder', () => { 10 | /* 11 | it('should roundtrip encode-decode successfully', async () => { 12 | const input = '2020-07-14'; 13 | const mockEncoder = new MockEncoder(); 14 | 15 | const encoderCodec = new XsdDateCodec(); 16 | encoderCodec.set({value: input}); 17 | encoderCodec.encodeCBOR(mockEncoder); 18 | 19 | const decoderCodec = new XsdDateCodec(); 20 | decoderCodec.set({value: mockEncoder.result}); 21 | const decodedResult = decoderCodec.decodeCBOR(); 22 | 23 | expect(input).equal(decodedResult); 24 | }); 25 | 26 | it('should throw exception when input includes time component', async () => { 27 | const input = '2020-07-14T19:23:24Z'; 28 | const mockEncoder = new MockEncoder(); 29 | 30 | const encoderCodec = new XsdDateCodec(); 31 | encoderCodec.set({value: input}); 32 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 33 | .to 34 | .throw('Invalid input, date includes time \ 35 | component lost of precision expected'); 36 | }); 37 | 38 | it('should throw exception when input is not a date string', async () => { 39 | const input = 'bad'; 40 | const mockEncoder = new MockEncoder(); 41 | 42 | const encoderCodec = new XsdDateCodec(); 43 | encoderCodec.set({value: input}); 44 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 45 | .to 46 | .throw('Invalid input, could not parse value as date'); 47 | }); 48 | 49 | it('should throw exception when input is an empty string', async () => { 50 | const input = ''; 51 | const mockEncoder = new MockEncoder(); 52 | 53 | const encoderCodec = new XsdDateCodec(); 54 | encoderCodec.set({value: input}); 55 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 56 | .to 57 | .throw('Invalid input, could not parse value as date'); 58 | }); 59 | 60 | it('should throw exception when input is not valid value type', async () => { 61 | const input = true; 62 | const mockEncoder = new MockEncoder(); 63 | 64 | const encoderCodec = new XsdDateCodec(); 65 | encoderCodec.set({value: input}); 66 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 67 | .to 68 | .throw('Invalid input, expected value to be of type string'); 69 | }); 70 | */ 71 | }); 72 | -------------------------------------------------------------------------------- /tests/XsdDateTimeDecoder.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | // import { 5 | // expect 6 | // } from 'chai'; 7 | // import {XsdDateTimeEncoder} from '../lib/codecs/XsdDateTimeCodec'; 8 | 9 | describe.skip('XsdDateTimeCodec', () => { 10 | /* 11 | it('should roundtrip encode-decode successfully', async () => { 12 | const input = '2020-07-14T19:23:24Z'; 13 | const mockEncoder = new MockEncoder(); 14 | 15 | const encoderCodec = new XsdDateTimeCodec(); 16 | encoderCodec.set({value: input}); 17 | encoderCodec.encodeCBOR(mockEncoder); 18 | 19 | const decoderCodec = new XsdDateTimeCodec(); 20 | decoderCodec.set({value: mockEncoder.result}); 21 | const decodedResult = decoderCodec.decodeCBOR(); 22 | 23 | expect(input).equal(decodedResult); 24 | }); 25 | 26 | it('should throw exception when input missing time component', async () => { 27 | const input = '2020-07-14'; 28 | const mockEncoder = new MockEncoder(); 29 | 30 | const encoderCodec = new XsdDateTimeCodec(); 31 | encoderCodec.set({value: input}); 32 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 33 | .to 34 | .throw('Invalid input, date missing time component'); 35 | }); 36 | 37 | it('should throw exception when input is not a date string', async () => { 38 | const input = 'bad'; 39 | const mockEncoder = new MockEncoder(); 40 | 41 | const encoderCodec = new XsdDateTimeCodec(); 42 | encoderCodec.set({value: input}); 43 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 44 | .to 45 | .throw('Invalid input, could not parse value as date'); 46 | }); 47 | 48 | it('should throw exception when input is an empty string', async () => { 49 | const input = ''; 50 | const mockEncoder = new MockEncoder(); 51 | 52 | const encoderCodec = new XsdDateTimeCodec(); 53 | encoderCodec.set({value: input}); 54 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 55 | .to 56 | .throw('Invalid input, could not parse value as date'); 57 | }); 58 | 59 | it('should throw exception when input is not valid value type', async () => { 60 | const input = true; 61 | const mockEncoder = new MockEncoder(); 62 | 63 | const encoderCodec = new XsdDateTimeCodec(); 64 | encoderCodec.set({value: input}); 65 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 66 | .to 67 | .throw('Invalid input, expected value to be of type string'); 68 | }); 69 | */ 70 | }); 71 | -------------------------------------------------------------------------------- /tests/XsdDateTimeEncoder.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | // import { 5 | // expect 6 | // } from 'chai'; 7 | // import {XsdDateTimeEncoder} from '../lib/codecs/XsdDateTimeEncoder.js'; 8 | 9 | describe.skip('XsdDateTimeEncoder', () => { 10 | /* 11 | it('should roundtrip encode-decode successfully', async () => { 12 | const input = '2020-07-14T19:23:24Z'; 13 | const mockEncoder = new MockEncoder(); 14 | 15 | const encoderCodec = new XsdDateTimeCodec(); 16 | encoderCodec.set({value: input}); 17 | encoderCodec.encodeCBOR(mockEncoder); 18 | 19 | const decoderCodec = new XsdDateTimeCodec(); 20 | decoderCodec.set({value: mockEncoder.result}); 21 | const decodedResult = decoderCodec.decodeCBOR(); 22 | 23 | expect(input).equal(decodedResult); 24 | }); 25 | 26 | it('should throw exception when input missing time component', async () => { 27 | const input = '2020-07-14'; 28 | const mockEncoder = new MockEncoder(); 29 | 30 | const encoderCodec = new XsdDateTimeCodec(); 31 | encoderCodec.set({value: input}); 32 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 33 | .to 34 | .throw('Invalid input, date missing time component'); 35 | }); 36 | 37 | it('should throw exception when input is not a date string', async () => { 38 | const input = 'bad'; 39 | const mockEncoder = new MockEncoder(); 40 | 41 | const encoderCodec = new XsdDateTimeCodec(); 42 | encoderCodec.set({value: input}); 43 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 44 | .to 45 | .throw('Invalid input, could not parse value as date'); 46 | }); 47 | 48 | it('should throw exception when input is an empty string', async () => { 49 | const input = ''; 50 | const mockEncoder = new MockEncoder(); 51 | 52 | const encoderCodec = new XsdDateTimeCodec(); 53 | encoderCodec.set({value: input}); 54 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 55 | .to 56 | .throw('Invalid input, could not parse value as date'); 57 | }); 58 | 59 | it('should throw exception when input is not valid value type', async () => { 60 | const input = true; 61 | const mockEncoder = new MockEncoder(); 62 | 63 | const encoderCodec = new XsdDateTimeCodec(); 64 | encoderCodec.set({value: input}); 65 | expect(() => encoderCodec.encodeCBOR(mockEncoder)) 66 | .to 67 | .throw('Invalid input, expected value to be of type string'); 68 | }); 69 | */ 70 | }); 71 | -------------------------------------------------------------------------------- /tests/contexts/activitystreams.jsonld: -------------------------------------------------------------------------------- 1 | { 2 | "@context": { 3 | "@vocab": "_:", 4 | "xsd": "http://www.w3.org/2001/XMLSchema#", 5 | "as": "https://www.w3.org/ns/activitystreams#", 6 | "ldp": "http://www.w3.org/ns/ldp#", 7 | "vcard": "http://www.w3.org/2006/vcard/ns#", 8 | "id": "@id", 9 | "type": "@type", 10 | "Accept": "as:Accept", 11 | "Activity": "as:Activity", 12 | "IntransitiveActivity": "as:IntransitiveActivity", 13 | "Add": "as:Add", 14 | "Announce": "as:Announce", 15 | "Application": "as:Application", 16 | "Arrive": "as:Arrive", 17 | "Article": "as:Article", 18 | "Audio": "as:Audio", 19 | "Block": "as:Block", 20 | "Collection": "as:Collection", 21 | "CollectionPage": "as:CollectionPage", 22 | "Relationship": "as:Relationship", 23 | "Create": "as:Create", 24 | "Delete": "as:Delete", 25 | "Dislike": "as:Dislike", 26 | "Document": "as:Document", 27 | "Event": "as:Event", 28 | "Follow": "as:Follow", 29 | "Flag": "as:Flag", 30 | "Group": "as:Group", 31 | "Ignore": "as:Ignore", 32 | "Image": "as:Image", 33 | "Invite": "as:Invite", 34 | "Join": "as:Join", 35 | "Leave": "as:Leave", 36 | "Like": "as:Like", 37 | "Link": "as:Link", 38 | "Mention": "as:Mention", 39 | "Note": "as:Note", 40 | "Object": "as:Object", 41 | "Offer": "as:Offer", 42 | "OrderedCollection": "as:OrderedCollection", 43 | "OrderedCollectionPage": "as:OrderedCollectionPage", 44 | "Organization": "as:Organization", 45 | "Page": "as:Page", 46 | "Person": "as:Person", 47 | "Place": "as:Place", 48 | "Profile": "as:Profile", 49 | "Question": "as:Question", 50 | "Reject": "as:Reject", 51 | "Remove": "as:Remove", 52 | "Service": "as:Service", 53 | "TentativeAccept": "as:TentativeAccept", 54 | "TentativeReject": "as:TentativeReject", 55 | "Tombstone": "as:Tombstone", 56 | "Undo": "as:Undo", 57 | "Update": "as:Update", 58 | "Video": "as:Video", 59 | "View": "as:View", 60 | "Listen": "as:Listen", 61 | "Read": "as:Read", 62 | "Move": "as:Move", 63 | "Travel": "as:Travel", 64 | "IsFollowing": "as:IsFollowing", 65 | "IsFollowedBy": "as:IsFollowedBy", 66 | "IsContact": "as:IsContact", 67 | "IsMember": "as:IsMember", 68 | "subject": { 69 | "@id": "as:subject", 70 | "@type": "@id" 71 | }, 72 | "relationship": { 73 | "@id": "as:relationship", 74 | "@type": "@id" 75 | }, 76 | "actor": { 77 | "@id": "as:actor", 78 | "@type": "@id" 79 | }, 80 | "attributedTo": { 81 | "@id": "as:attributedTo", 82 | "@type": "@id" 83 | }, 84 | "attachment": { 85 | "@id": "as:attachment", 86 | "@type": "@id" 87 | }, 88 | "bcc": { 89 | "@id": "as:bcc", 90 | "@type": "@id" 91 | }, 92 | "bto": { 93 | "@id": "as:bto", 94 | "@type": "@id" 95 | }, 96 | "cc": { 97 | "@id": "as:cc", 98 | "@type": "@id" 99 | }, 100 | "context": { 101 | "@id": "as:context", 102 | "@type": "@id" 103 | }, 104 | "current": { 105 | "@id": "as:current", 106 | "@type": "@id" 107 | }, 108 | "first": { 109 | "@id": "as:first", 110 | "@type": "@id" 111 | }, 112 | "generator": { 113 | "@id": "as:generator", 114 | "@type": "@id" 115 | }, 116 | "icon": { 117 | "@id": "as:icon", 118 | "@type": "@id" 119 | }, 120 | "image": { 121 | "@id": "as:image", 122 | "@type": "@id" 123 | }, 124 | "inReplyTo": { 125 | "@id": "as:inReplyTo", 126 | "@type": "@id" 127 | }, 128 | "items": { 129 | "@id": "as:items", 130 | "@type": "@id" 131 | }, 132 | "instrument": { 133 | "@id": "as:instrument", 134 | "@type": "@id" 135 | }, 136 | "orderedItems": { 137 | "@id": "as:items", 138 | "@type": "@id", 139 | "@container": "@list" 140 | }, 141 | "last": { 142 | "@id": "as:last", 143 | "@type": "@id" 144 | }, 145 | "location": { 146 | "@id": "as:location", 147 | "@type": "@id" 148 | }, 149 | "next": { 150 | "@id": "as:next", 151 | "@type": "@id" 152 | }, 153 | "object": { 154 | "@id": "as:object", 155 | "@type": "@id" 156 | }, 157 | "oneOf": { 158 | "@id": "as:oneOf", 159 | "@type": "@id" 160 | }, 161 | "anyOf": { 162 | "@id": "as:anyOf", 163 | "@type": "@id" 164 | }, 165 | "closed": { 166 | "@id": "as:closed", 167 | "@type": "xsd:dateTime" 168 | }, 169 | "origin": { 170 | "@id": "as:origin", 171 | "@type": "@id" 172 | }, 173 | "accuracy": { 174 | "@id": "as:accuracy", 175 | "@type": "xsd:float" 176 | }, 177 | "prev": { 178 | "@id": "as:prev", 179 | "@type": "@id" 180 | }, 181 | "preview": { 182 | "@id": "as:preview", 183 | "@type": "@id" 184 | }, 185 | "replies": { 186 | "@id": "as:replies", 187 | "@type": "@id" 188 | }, 189 | "result": { 190 | "@id": "as:result", 191 | "@type": "@id" 192 | }, 193 | "audience": { 194 | "@id": "as:audience", 195 | "@type": "@id" 196 | }, 197 | "partOf": { 198 | "@id": "as:partOf", 199 | "@type": "@id" 200 | }, 201 | "tag": { 202 | "@id": "as:tag", 203 | "@type": "@id" 204 | }, 205 | "target": { 206 | "@id": "as:target", 207 | "@type": "@id" 208 | }, 209 | "to": { 210 | "@id": "as:to", 211 | "@type": "@id" 212 | }, 213 | "url": { 214 | "@id": "as:url", 215 | "@type": "@id" 216 | }, 217 | "altitude": { 218 | "@id": "as:altitude", 219 | "@type": "xsd:float" 220 | }, 221 | "content": "as:content", 222 | "contentMap": { 223 | "@id": "as:content", 224 | "@container": "@language" 225 | }, 226 | "name": "as:name", 227 | "nameMap": { 228 | "@id": "as:name", 229 | "@container": "@language" 230 | }, 231 | "duration": { 232 | "@id": "as:duration", 233 | "@type": "xsd:duration" 234 | }, 235 | "endTime": { 236 | "@id": "as:endTime", 237 | "@type": "xsd:dateTime" 238 | }, 239 | "height": { 240 | "@id": "as:height", 241 | "@type": "xsd:nonNegativeInteger" 242 | }, 243 | "href": { 244 | "@id": "as:href", 245 | "@type": "@id" 246 | }, 247 | "hreflang": "as:hreflang", 248 | "latitude": { 249 | "@id": "as:latitude", 250 | "@type": "xsd:float" 251 | }, 252 | "longitude": { 253 | "@id": "as:longitude", 254 | "@type": "xsd:float" 255 | }, 256 | "mediaType": "as:mediaType", 257 | "published": { 258 | "@id": "as:published", 259 | "@type": "xsd:dateTime" 260 | }, 261 | "radius": { 262 | "@id": "as:radius", 263 | "@type": "xsd:float" 264 | }, 265 | "rel": "as:rel", 266 | "startIndex": { 267 | "@id": "as:startIndex", 268 | "@type": "xsd:nonNegativeInteger" 269 | }, 270 | "startTime": { 271 | "@id": "as:startTime", 272 | "@type": "xsd:dateTime" 273 | }, 274 | "summary": "as:summary", 275 | "summaryMap": { 276 | "@id": "as:summary", 277 | "@container": "@language" 278 | }, 279 | "totalItems": { 280 | "@id": "as:totalItems", 281 | "@type": "xsd:nonNegativeInteger" 282 | }, 283 | "units": "as:units", 284 | "updated": { 285 | "@id": "as:updated", 286 | "@type": "xsd:dateTime" 287 | }, 288 | "width": { 289 | "@id": "as:width", 290 | "@type": "xsd:nonNegativeInteger" 291 | }, 292 | "describes": { 293 | "@id": "as:describes", 294 | "@type": "@id" 295 | }, 296 | "formerType": { 297 | "@id": "as:formerType", 298 | "@type": "@id" 299 | }, 300 | "deleted": { 301 | "@id": "as:deleted", 302 | "@type": "xsd:dateTime" 303 | }, 304 | "inbox": { 305 | "@id": "ldp:inbox", 306 | "@type": "@id" 307 | }, 308 | "outbox": { 309 | "@id": "as:outbox", 310 | "@type": "@id" 311 | }, 312 | "following": { 313 | "@id": "as:following", 314 | "@type": "@id" 315 | }, 316 | "followers": { 317 | "@id": "as:followers", 318 | "@type": "@id" 319 | }, 320 | "streams": { 321 | "@id": "as:streams", 322 | "@type": "@id" 323 | }, 324 | "preferredUsername": "as:preferredUsername", 325 | "endpoints": { 326 | "@id": "as:endpoints", 327 | "@type": "@id" 328 | }, 329 | "uploadMedia": { 330 | "@id": "as:uploadMedia", 331 | "@type": "@id" 332 | }, 333 | "proxyUrl": { 334 | "@id": "as:proxyUrl", 335 | "@type": "@id" 336 | }, 337 | "liked": { 338 | "@id": "as:liked", 339 | "@type": "@id" 340 | }, 341 | "oauthAuthorizationEndpoint": { 342 | "@id": "as:oauthAuthorizationEndpoint", 343 | "@type": "@id" 344 | }, 345 | "oauthTokenEndpoint": { 346 | "@id": "as:oauthTokenEndpoint", 347 | "@type": "@id" 348 | }, 349 | "provideClientKey": { 350 | "@id": "as:provideClientKey", 351 | "@type": "@id" 352 | }, 353 | "signClientKey": { 354 | "@id": "as:signClientKey", 355 | "@type": "@id" 356 | }, 357 | "sharedInbox": { 358 | "@id": "as:sharedInbox", 359 | "@type": "@id" 360 | }, 361 | "Public": { 362 | "@id": "as:Public", 363 | "@type": "@id" 364 | }, 365 | "source": "as:source", 366 | "likes": { 367 | "@id": "as:likes", 368 | "@type": "@id" 369 | }, 370 | "shares": { 371 | "@id": "as:shares", 372 | "@type": "@id" 373 | }, 374 | "alsoKnownAs": { 375 | "@id": "as:alsoKnownAs", 376 | "@type": "@id" 377 | } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /tests/decode.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2025 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import { 5 | default as chai, 6 | expect 7 | } from 'chai'; 8 | import {default as chaiBytes} from 'chai-bytes'; 9 | chai.use(chaiBytes); 10 | 11 | import {decode, encode} from '../lib/index.js'; 12 | import { 13 | STRING_TABLE, 14 | TYPE_TABLE, 15 | } from '../lib/tables.js'; 16 | 17 | function _makeTypeTableLoader(entries) { 18 | const typeTables = new Map(entries); 19 | return async function({registryEntryId}) { 20 | return typeTables.get(registryEntryId); 21 | }; 22 | } 23 | 24 | describe('cborld decode', () => { 25 | it('should decode CBOR-LD bytes (no type table loader)', 26 | async () => { 27 | const cborldBytes = new Uint8Array([0xd9, 0xcb, 0x1d, 0x82, 0x01, 0xa0]); 28 | const jsonldDocument = await decode({cborldBytes}); 29 | expect(jsonldDocument).deep.equal({}); 30 | }); 31 | 32 | it('should decode CBOR-LD bytes (no compression)', 33 | async () => { 34 | const cborldBytes = new Uint8Array([0xd9, 0xcb, 0x1d, 0x82, 0x00, 0xa0]); 35 | const jsonldDocument = await decode({cborldBytes}); 36 | expect(jsonldDocument).deep.equal({}); 37 | }); 38 | 39 | it('should decode CBOR-LD bytes (type table loader)', 40 | async () => { 41 | const cborldBytes = new Uint8Array([0xd9, 0xcb, 0x1d, 0x82, 0x01, 0xa0]); 42 | const jsonldDocument = await decode({ 43 | cborldBytes, 44 | typeTableLoader: _makeTypeTableLoader([[0x01, new Map()]]) 45 | }); 46 | expect(jsonldDocument).deep.equal({}); 47 | }); 48 | 49 | it('should fail to decode with no typeTable or typeTableLoader', 50 | async () => { 51 | const cborldBytes = new Uint8Array([0xd9, 0xcb, 0x1d, 0x82, 0x02, 0xa0]); 52 | let result; 53 | let error; 54 | try { 55 | result = await decode({ 56 | cborldBytes 57 | }); 58 | } catch(e) { 59 | error = e; 60 | } 61 | expect(result).to.eql(undefined); 62 | expect(error?.code).to.eql('ERR_NO_TYPETABLE'); 63 | }); 64 | 65 | it('should fail to decode with no typeTableLoader id found', 66 | async () => { 67 | const cborldBytes = new Uint8Array([0xd9, 0xcb, 0x1d, 0x82, 0x02, 0xa0]); 68 | let result; 69 | let error; 70 | try { 71 | result = await decode({ 72 | cborldBytes, 73 | typeTableLoader: _makeTypeTableLoader([]) 74 | }); 75 | } catch(e) { 76 | error = e; 77 | } 78 | expect(result).to.eql(undefined); 79 | expect(error?.code).to.eql('ERR_NO_TYPETABLE'); 80 | }); 81 | 82 | it('should fail with typeTable', 83 | async () => { 84 | const cborldBytes = new Uint8Array([0xd9, 0xcb, 0x1d, 0x82, 0x01, 0xa0]); 85 | let result; 86 | let error; 87 | try { 88 | result = await decode({ 89 | cborldBytes, 90 | typeTable: new Map() 91 | }); 92 | } catch(e) { 93 | error = e; 94 | } 95 | expect(result).to.eql(undefined); 96 | expect(error?.name).to.eql('TypeError'); 97 | }); 98 | 99 | it('should decode empty document CBOR-LD bytes', async () => { 100 | const cborldBytes = new Uint8Array([0xd9, 0xcb, 0x1d, 0x82, 0x01, 0xa0]); 101 | const jsonldDocument = await decode({ 102 | cborldBytes, 103 | typeTableLoader: _makeTypeTableLoader([[0x01, new Map()]]) 104 | }); 105 | expect(jsonldDocument).deep.equal({}); 106 | }); 107 | 108 | it('should decode empty JSON-LD document bytes with varint', async () => { 109 | const cborldBytes = new Uint8Array([0xd9, 0xcb, 0x1d, 0x82, 0x10, 0xa0]); 110 | const jsonldDocument = await decode({ 111 | cborldBytes, 112 | typeTableLoader: _makeTypeTableLoader([[0x10, new Map()]]) 113 | }); 114 | expect(jsonldDocument).deep.equal({}); 115 | }); 116 | 117 | it('should decode empty JSON-LD document bytes with registry entry >1 byte', 118 | async () => { 119 | const cborldBytes = new Uint8Array( 120 | [0xd9, 0xcb, 0x1d, 0x82, 0x18, 0x64, 0xa0]); 121 | const jsonldDocument = await decode({ 122 | cborldBytes, 123 | typeTableLoader: _makeTypeTableLoader([[100, new Map()]]) 124 | }); 125 | expect(jsonldDocument).deep.equal({}); 126 | }); 127 | 128 | it('should decode empty JSON-LD document with multiple byte registry entry', 129 | async () => { 130 | const cborldBytes = new Uint8Array( 131 | [0xd9, 0xcb, 0x1d, 0x82, 0x1a, 0x3b, 0x9a, 0xca, 0x00, 0xa0]); 132 | const jsonldDocument = await decode({ 133 | cborldBytes, 134 | typeTableLoader: _makeTypeTableLoader([[1000000000, new Map()]]) 135 | }); 136 | expect(jsonldDocument).deep.equal({}); 137 | }); 138 | 139 | it('should throw on undefined compressed context', async () => { 140 | const CONTEXT_URL = 'urn:foo'; 141 | const CONTEXT = { 142 | '@context': { 143 | foo: { 144 | '@id': 'ex:foo', 145 | '@type': 'https://w3id.org/security#multibase' 146 | } 147 | } 148 | }; 149 | 150 | const documentLoader = url => { 151 | if(url === CONTEXT_URL) { 152 | return { 153 | contextUrl: null, 154 | document: CONTEXT, 155 | documentUrl: url 156 | }; 157 | } 158 | throw new Error(`Refused to load URL "${url}".`); 159 | }; 160 | 161 | const cborldBytes = _hexToUint8Array( 162 | 'd9cb1d8202a200198000186583444d010203447a0102034475010203'); 163 | 164 | const typeTable = new Map(TYPE_TABLE); 165 | 166 | const contextTable = new Map(STRING_TABLE); 167 | typeTable.set('context', contextTable); 168 | 169 | let result; 170 | let error; 171 | try { 172 | result = await decode({ 173 | cborldBytes, 174 | documentLoader, 175 | typeTableLoader: () => typeTable 176 | }); 177 | } catch(e) { 178 | error = e; 179 | } 180 | expect(result).to.eql(undefined); 181 | expect(error?.code).to.eql('ERR_UNDEFINED_COMPRESSED_CONTEXT'); 182 | }); 183 | 184 | it('should decompress multibase-typed values', async () => { 185 | const CONTEXT_URL = 'urn:foo'; 186 | const CONTEXT = { 187 | '@context': { 188 | foo: { 189 | '@id': 'ex:foo', 190 | '@type': 'https://w3id.org/security#multibase' 191 | } 192 | } 193 | }; 194 | const jsonldDocument = { 195 | '@context': CONTEXT_URL, 196 | foo: ['MAQID', 'zLdp', 'uAQID'] 197 | }; 198 | 199 | const documentLoader = url => { 200 | if(url === CONTEXT_URL) { 201 | return { 202 | contextUrl: null, 203 | document: CONTEXT, 204 | documentUrl: url 205 | }; 206 | } 207 | throw new Error(`Refused to load URL "${url}".`); 208 | }; 209 | 210 | const cborldBytes = _hexToUint8Array( 211 | 'd9cb1d8202a200198000186583444d010203447a0102034475010203'); 212 | 213 | const typeTable = new Map(TYPE_TABLE); 214 | 215 | const contextTable = new Map(STRING_TABLE); 216 | contextTable.set(CONTEXT_URL, 0x8000); 217 | typeTable.set('context', contextTable); 218 | 219 | const decodedDocument = await decode({ 220 | cborldBytes, 221 | documentLoader, 222 | typeTableLoader: () => typeTable 223 | }); 224 | expect(decodedDocument).to.eql(jsonldDocument); 225 | }); 226 | 227 | it('should decompress multibase-typed values using type table', 228 | async () => { 229 | const CONTEXT_URL = 'urn:foo'; 230 | const CONTEXT = { 231 | '@context': { 232 | foo: { 233 | '@id': 'ex:foo', 234 | '@type': 'https://w3id.org/security#multibase' 235 | } 236 | } 237 | }; 238 | const jsonldDocument = { 239 | '@context': CONTEXT_URL, 240 | foo: ['MAQID', 'zLdp', 'uAQID'] 241 | }; 242 | 243 | const documentLoader = url => { 244 | if(url === CONTEXT_URL) { 245 | return { 246 | contextUrl: null, 247 | document: CONTEXT, 248 | documentUrl: url 249 | }; 250 | } 251 | throw new Error(`Refused to load URL "${url}".`); 252 | }; 253 | 254 | const cborldBytes = _hexToUint8Array( 255 | 'd9cb1d8202a200198000186583198001198002198003'); 256 | 257 | const typeTable = new Map(TYPE_TABLE); 258 | 259 | const contextTable = new Map(STRING_TABLE); 260 | contextTable.set(CONTEXT_URL, 0x8000); 261 | typeTable.set('context', contextTable); 262 | 263 | const multibaseTable = new Map(); 264 | multibaseTable.set('MAQID', 0x8001); 265 | multibaseTable.set('zLdp', 0x8002); 266 | multibaseTable.set('uAQID', 0x8003); 267 | typeTable.set( 268 | 'https://w3id.org/security#multibase', 269 | multibaseTable); 270 | 271 | const decodedDocument = await decode({ 272 | cborldBytes, 273 | documentLoader, 274 | typeTableLoader: () => typeTable 275 | }); 276 | expect(decodedDocument).to.eql(jsonldDocument); 277 | }); 278 | it( 279 | 'should round trip multibase-typed values using type table if possible', 280 | async () => { 281 | const CONTEXT_URL = 'urn:foo'; 282 | const CONTEXT = { 283 | '@context': { 284 | foo: { 285 | '@id': 'ex:foo', 286 | '@type': 'https://w3id.org/security#multibase' 287 | } 288 | } 289 | }; 290 | const jsonldDocument = { 291 | '@context': CONTEXT_URL, 292 | foo: 'MAQID' 293 | }; 294 | 295 | const documentLoader = url => { 296 | if(url === CONTEXT_URL) { 297 | return { 298 | contextUrl: null, 299 | document: CONTEXT, 300 | documentUrl: url 301 | }; 302 | } 303 | throw new Error(`Refused to load URL "${url}".`); 304 | }; 305 | 306 | const typeTable = new Map(TYPE_TABLE); 307 | 308 | const contextTable = new Map(STRING_TABLE); 309 | contextTable.set(CONTEXT_URL, 0x8000); 310 | typeTable.set('context', contextTable); 311 | 312 | const multibaseTable = new Map(); 313 | multibaseTable.set('MAQID', 0x8001); 314 | typeTable.set( 315 | 'https://w3id.org/security#multibase', 316 | multibaseTable); 317 | 318 | const cborldBytes = await encode({ 319 | jsonldDocument, 320 | registryEntryId: 2, 321 | documentLoader, 322 | typeTableLoader: () => typeTable 323 | }); 324 | 325 | const decodedDocument = await decode({ 326 | cborldBytes, 327 | documentLoader, 328 | typeTableLoader: () => typeTable 329 | }); 330 | expect(decodedDocument).to.eql(jsonldDocument); 331 | }); 332 | 333 | it('should decompress cryptosuite strings', async () => { 334 | const CONTEXT_URL = 'urn:foo'; 335 | const CONTEXT = { 336 | '@context': { 337 | foo: { 338 | '@id': 'ex:foo', 339 | '@type': 'https://w3id.org/security#cryptosuiteString' 340 | } 341 | } 342 | }; 343 | const jsonldDocument = { 344 | '@context': CONTEXT_URL, 345 | foo: [ 346 | 'ecdsa-rdfc-2019', 347 | 'ecdsa-sd-2023', 348 | 'eddsa-rdfc-2022' 349 | ] 350 | }; 351 | 352 | const documentLoader = url => { 353 | if(url === CONTEXT_URL) { 354 | return { 355 | contextUrl: null, 356 | document: CONTEXT, 357 | documentUrl: url 358 | }; 359 | } 360 | throw new Error(`Refused to load URL "${url}".`); 361 | }; 362 | 363 | const cborldBytes = _hexToUint8Array('d9cb1d8202a200198000186583010203'); 364 | 365 | const typeTable = new Map(TYPE_TABLE); 366 | 367 | const contextTable = new Map(STRING_TABLE); 368 | contextTable.set(CONTEXT_URL, 0x8000); 369 | typeTable.set('context', contextTable); 370 | 371 | const decodedDocument = await decode({ 372 | cborldBytes, 373 | documentLoader, 374 | typeTableLoader: () => typeTable 375 | }); 376 | expect(decodedDocument).to.eql(jsonldDocument); 377 | }); 378 | 379 | it('should decompress xsd date', async () => { 380 | const CONTEXT_URL = 'urn:foo'; 381 | const CONTEXT = { 382 | '@context': { 383 | arbitraryPrefix: 'http://www.w3.org/2001/XMLSchema#', 384 | foo: { 385 | '@id': 'ex:foo', 386 | '@type': 'arbitraryPrefix:date' 387 | } 388 | } 389 | }; 390 | const date = '2021-04-09'; 391 | const jsonldDocument = { 392 | '@context': CONTEXT_URL, 393 | foo: date 394 | }; 395 | 396 | const documentLoader = url => { 397 | if(url === CONTEXT_URL) { 398 | return { 399 | contextUrl: null, 400 | document: CONTEXT, 401 | documentUrl: url 402 | }; 403 | } 404 | throw new Error(`Refused to load URL "${url}".`); 405 | }; 406 | 407 | const cborldBytes = _hexToUint8Array('d9cb1d8202a20019800018661a606f9900'); 408 | 409 | const typeTable = new Map(TYPE_TABLE); 410 | 411 | const contextTable = new Map(STRING_TABLE); 412 | contextTable.set(CONTEXT_URL, 0x8000); 413 | typeTable.set('context', contextTable); 414 | 415 | const decodedDocument = await decode({ 416 | cborldBytes, 417 | documentLoader, 418 | typeTableLoader: () => typeTable 419 | }); 420 | expect(decodedDocument).to.eql(jsonldDocument); 421 | }); 422 | 423 | it('should decompress xsd dateTime', async () => { 424 | const CONTEXT_URL = 'urn:foo'; 425 | const CONTEXT = { 426 | '@context': { 427 | arbitraryPrefix: 'http://www.w3.org/2001/XMLSchema#', 428 | foo: { 429 | '@id': 'ex:foo', 430 | '@type': 'arbitraryPrefix:dateTime' 431 | } 432 | } 433 | }; 434 | const date = '2021-04-09T20:38:55Z'; 435 | const jsonldDocument = { 436 | '@context': CONTEXT_URL, 437 | foo: date 438 | }; 439 | 440 | const documentLoader = url => { 441 | if(url === CONTEXT_URL) { 442 | return { 443 | contextUrl: null, 444 | document: CONTEXT, 445 | documentUrl: url 446 | }; 447 | } 448 | throw new Error(`Refused to load URL "${url}".`); 449 | }; 450 | 451 | const cborldBytes = _hexToUint8Array('d9cb1d8202a20019800018661a6070bb5f'); 452 | 453 | const typeTable = new Map(TYPE_TABLE); 454 | 455 | const contextTable = new Map(STRING_TABLE); 456 | contextTable.set(CONTEXT_URL, 0x8000); 457 | typeTable.set('context', contextTable); 458 | 459 | const decodedDocument = await decode({ 460 | cborldBytes, 461 | documentLoader, 462 | typeTableLoader: () => typeTable 463 | }); 464 | expect(decodedDocument).to.eql(jsonldDocument); 465 | }); 466 | 467 | it('should decompress xsd dateTime with type table when possible', 468 | async () => { 469 | const CONTEXT_URL = 'urn:foo'; 470 | const CONTEXT = { 471 | '@context': { 472 | arbitraryPrefix: 'http://www.w3.org/2001/XMLSchema#', 473 | foo: { 474 | '@id': 'ex:foo', 475 | '@type': 'arbitraryPrefix:dateTime' 476 | } 477 | } 478 | }; 479 | const date = '2021-04-09T20:38:55Z'; 480 | const jsonldDocument = { 481 | '@context': CONTEXT_URL, 482 | foo: date 483 | }; 484 | 485 | const documentLoader = url => { 486 | if(url === CONTEXT_URL) { 487 | return { 488 | contextUrl: null, 489 | document: CONTEXT, 490 | documentUrl: url 491 | }; 492 | } 493 | throw new Error(`Refused to load URL "${url}".`); 494 | }; 495 | 496 | const cborldBytes = _hexToUint8Array('d9cb1d8202a2001980001866428001'); 497 | 498 | const typeTable = new Map(TYPE_TABLE); 499 | 500 | const contextTable = new Map(STRING_TABLE); 501 | contextTable.set(CONTEXT_URL, 0x8000); 502 | typeTable.set('context', contextTable); 503 | 504 | typeTable.set( 505 | 'http://www.w3.org/2001/XMLSchema#dateTime', 506 | new Map([['2021-04-09T20:38:55Z', 0x8001]])); 507 | 508 | const decodedDocument = await decode({ 509 | cborldBytes, 510 | documentLoader, 511 | typeTableLoader: () => typeTable 512 | }); 513 | expect(decodedDocument).to.eql(jsonldDocument); 514 | }); 515 | 516 | it('should decode lowercase urn:uuid', async () => { 517 | const CONTEXT_URL = 'urn:foo'; 518 | const CONTEXT = { 519 | '@context': { 520 | id: '@id', 521 | arbitraryPrefix: 'http://www.w3.org/2001/XMLSchema#', 522 | foo: { 523 | '@id': 'ex:foo', 524 | '@type': 'arbitraryPrefix:dateTime' 525 | } 526 | } 527 | }; 528 | const date = '2021-04-09T20:38:55Z'; 529 | const jsonldDocument = { 530 | '@context': CONTEXT_URL, 531 | id: 'urn:uuid:75ef3fcc-9ae3-11eb-8e3e-10bf48838a41', 532 | foo: date 533 | }; 534 | 535 | const documentLoader = url => { 536 | if(url === CONTEXT_URL) { 537 | return { 538 | contextUrl: null, 539 | document: CONTEXT, 540 | documentUrl: url 541 | }; 542 | } 543 | throw new Error(`Refused to load URL "${url}".`); 544 | }; 545 | 546 | const cborldBytes = _hexToUint8Array( 547 | 'd9cb1d8202a30019800018661a6070bb5f186882035075ef3fcc9ae311eb8e3e' + 548 | '10bf48838a41'); 549 | 550 | const typeTable = new Map(TYPE_TABLE); 551 | 552 | const contextTable = new Map(STRING_TABLE); 553 | contextTable.set(CONTEXT_URL, 0x8000); 554 | typeTable.set('context', contextTable); 555 | 556 | const decodedDocument = await decode({ 557 | cborldBytes, 558 | documentLoader, 559 | typeTableLoader: () => typeTable 560 | }); 561 | expect(decodedDocument).to.eql(jsonldDocument); 562 | }); 563 | 564 | it('should decode uppercase urn:uuid', async () => { 565 | const CONTEXT_URL = 'urn:foo'; 566 | const CONTEXT = { 567 | '@context': { 568 | id: '@id', 569 | arbitraryPrefix: 'http://www.w3.org/2001/XMLSchema#', 570 | foo: { 571 | '@id': 'ex:foo', 572 | '@type': 'arbitraryPrefix:dateTime' 573 | } 574 | } 575 | }; 576 | const date = '2021-04-09T20:38:55Z'; 577 | const jsonldDocument = { 578 | '@context': CONTEXT_URL, 579 | id: 'urn:uuid:75EF3FCC-9AE3-11EB-8E3E-10BF48838A41', 580 | foo: date 581 | }; 582 | 583 | const documentLoader = url => { 584 | if(url === CONTEXT_URL) { 585 | return { 586 | contextUrl: null, 587 | document: CONTEXT, 588 | documentUrl: url 589 | }; 590 | } 591 | throw new Error(`Refused to load URL "${url}".`); 592 | }; 593 | 594 | const cborldBytes = _hexToUint8Array( 595 | 'd9cb1d8202a30019800018661a6070bb5f1868820378243735454633464343' + 596 | '2d394145332d313145422d384533452d313042463438383338413431'); 597 | 598 | const typeTable = new Map(TYPE_TABLE); 599 | 600 | const contextTable = new Map(STRING_TABLE); 601 | contextTable.set(CONTEXT_URL, 0x8000); 602 | typeTable.set('context', contextTable); 603 | 604 | const decodedDocument = await decode({ 605 | cborldBytes, 606 | documentLoader, 607 | typeTableLoader: () => typeTable 608 | }); 609 | expect(decodedDocument).to.eql(jsonldDocument); 610 | }); 611 | 612 | it('should decode https URL', async () => { 613 | const CONTEXT_URL = 'urn:foo'; 614 | const CONTEXT = { 615 | '@context': { 616 | id: '@id', 617 | arbitraryPrefix: 'http://www.w3.org/2001/XMLSchema#', 618 | foo: { 619 | '@id': 'ex:foo', 620 | '@type': 'arbitraryPrefix:dateTime' 621 | } 622 | } 623 | }; 624 | const date = '2021-04-09T20:38:55Z'; 625 | const jsonldDocument = { 626 | '@context': CONTEXT_URL, 627 | id: 'https://test.example', 628 | foo: date 629 | }; 630 | 631 | const documentLoader = url => { 632 | if(url === CONTEXT_URL) { 633 | return { 634 | contextUrl: null, 635 | document: CONTEXT, 636 | documentUrl: url 637 | }; 638 | } 639 | throw new Error(`Refused to load URL "${url}".`); 640 | }; 641 | 642 | const cborldBytes = _hexToUint8Array( 643 | 'd9cb1d8202a30019800018661a6070bb5f186882026c746573742e6578616d706c65'); 644 | 645 | const typeTable = new Map(TYPE_TABLE); 646 | 647 | const contextTable = new Map(STRING_TABLE); 648 | contextTable.set(CONTEXT_URL, 0x8000); 649 | typeTable.set('context', contextTable); 650 | 651 | const decodedDocument = await decode({ 652 | cborldBytes, 653 | documentLoader, 654 | typeTableLoader: () => typeTable 655 | }); 656 | expect(decodedDocument).to.eql(jsonldDocument); 657 | }); 658 | 659 | it('should decode http URL', async () => { 660 | const CONTEXT_URL = 'urn:foo'; 661 | const CONTEXT = { 662 | '@context': { 663 | id: '@id', 664 | arbitraryPrefix: 'http://www.w3.org/2001/XMLSchema#', 665 | foo: { 666 | '@id': 'ex:foo', 667 | '@type': 'arbitraryPrefix:dateTime' 668 | } 669 | } 670 | }; 671 | const date = '2021-04-09T20:38:55Z'; 672 | const jsonldDocument = { 673 | '@context': CONTEXT_URL, 674 | id: 'http://test.example', 675 | foo: date 676 | }; 677 | 678 | const documentLoader = url => { 679 | if(url === CONTEXT_URL) { 680 | return { 681 | contextUrl: null, 682 | document: CONTEXT, 683 | documentUrl: url 684 | }; 685 | } 686 | throw new Error(`Refused to load URL "${url}".`); 687 | }; 688 | 689 | const cborldBytes = _hexToUint8Array( 690 | 'd9cb1d8202a30019800018661a6070bb5f186882016c746573742e6578616d706c65'); 691 | 692 | const typeTable = new Map(TYPE_TABLE); 693 | 694 | const contextTable = new Map(STRING_TABLE); 695 | contextTable.set(CONTEXT_URL, 0x8000); 696 | typeTable.set('context', contextTable); 697 | 698 | const decodedDocument = await decode({ 699 | cborldBytes, 700 | documentLoader, 701 | typeTableLoader: () => typeTable 702 | }); 703 | expect(decodedDocument).to.eql(jsonldDocument); 704 | }); 705 | 706 | it('should decode a CIT type token', async () => { 707 | // note: CIT type tokens are presently only encoded using tag 0x0501 708 | const cborldBytes = _hexToUint8Array( 709 | 'd90501a40015186c1864186e4c7ad90501a2011987430518411870583b7a' + 710 | '0000e190818fdd92908425370e0b5dad9ad92dc956b5ec2ab41ce76b8c70' + 711 | 'cb859a7c88ca6ba68b1ff238a70ed674999b6ff5179b0ebb10140b23'); 712 | 713 | const CONTEXT_URL = 'https://w3id.org/cit/v1'; 714 | /* eslint-disable max-len */ 715 | const CONTEXT = { 716 | '@context': { 717 | '@protected': true, 718 | type: '@type', 719 | ConcealedIdTokenCredential: 'https://w3id.org/cit#ConcealedIdTokenCredential', 720 | concealedIdToken: { 721 | '@id': 'https://w3id.org/cit#concealedIdToken', 722 | '@type': '@id' 723 | }, 724 | ConcealedIdToken: { 725 | '@id': 'https://w3id.org/cit#ConcealedIdToken', 726 | '@context': { 727 | '@protected': true, 728 | meta: {'@id': 'https://w3id.org/cit#meta', '@type': 'https://w3id.org/security#multibase'}, 729 | payload: {'@id': 'https://w3id.org/cit#payload', '@type': 'https://w3id.org/security#multibase'} 730 | } 731 | }, 732 | Ed25519Signature2020: { 733 | '@id': 'https://w3id.org/security#Ed25519Signature2020', 734 | '@context': { 735 | '@protected': true, 736 | 737 | id: '@id', 738 | type: '@type', 739 | 740 | sec: 'https://w3id.org/security#', 741 | xsd: 'http://www.w3.org/2001/XMLSchema#', 742 | 743 | challenge: 'sec:challenge', 744 | created: {'@id': 'http://purl.org/dc/terms/created', '@type': 'xsd:dateTime'}, 745 | domain: 'sec:domain', 746 | expires: {'@id': 'sec:expiration', '@type': 'xsd:dateTime'}, 747 | nonce: 'sec:nonce', 748 | proofPurpose: { 749 | '@id': 'sec:proofPurpose', 750 | '@type': '@vocab', 751 | '@context': { 752 | '@protected': true, 753 | 754 | id: '@id', 755 | type: '@type', 756 | 757 | sec: 'https://w3id.org/security#', 758 | 759 | assertionMethod: {'@id': 'sec:assertionMethod', '@type': '@id', '@container': '@set'}, 760 | authentication: {'@id': 'sec:authenticationMethod', '@type': '@id', '@container': '@set'} 761 | } 762 | }, 763 | proofValue: { 764 | '@id': 'https://w3id.org/security#proofValue', 765 | '@type': 'https://w3id.org/security#multibase' 766 | }, 767 | verificationMethod: {'@id': 'sec:verificationMethod', '@type': '@id'} 768 | } 769 | } 770 | } 771 | }; 772 | /* eslint-enable max-len */ 773 | 774 | const documentLoader = url => { 775 | if(url === CONTEXT_URL) { 776 | return { 777 | contextUrl: null, 778 | document: CONTEXT, 779 | documentUrl: url 780 | }; 781 | } 782 | throw new Error(`Refused to load URL "${url}".`); 783 | }; 784 | 785 | const jsonldDocument = { 786 | '@context': 'https://w3id.org/cit/v1', 787 | type: 'ConcealedIdToken', 788 | meta: 'zvpJ2L5sbowrJPdA', 789 | // eslint-disable-next-line max-len 790 | payload: 'z1177JK4h25dHEAXAVMUMpn2zWcxLCeMLP3oVFQFQ11xHFtE9BhyoU2g47D6Xod1Mu99JR9YJdY184HY' 791 | }; 792 | 793 | const decodedDocument = await decode({ 794 | cborldBytes, 795 | documentLoader 796 | }); 797 | 798 | expect(decodedDocument).to.eql(jsonldDocument); 799 | }); 800 | }); 801 | 802 | function _hexToUint8Array(hex) { 803 | return new Uint8Array(hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); 804 | } 805 | -------------------------------------------------------------------------------- /tests/examples.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2024-2025 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import { 5 | default as chai, 6 | expect 7 | } from 'chai'; 8 | import {default as chaiBytes} from 'chai-bytes'; 9 | import {fileURLToPath} from 'node:url'; 10 | import fs from 'node:fs'; 11 | import fsp from 'node:fs/promises'; 12 | import path from 'node:path'; 13 | chai.use(chaiBytes); 14 | 15 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 16 | 17 | import {decode, encode} from '../lib/index.js'; 18 | 19 | import * as citizenship_context from 'citizenship-context'; 20 | import * as credentials_context from 'credentials-context'; 21 | import * as did_context from 'did-context'; 22 | import * as ed25519_2020_context from 'ed25519-signature-2020-context'; 23 | import * as multikey_context from '@digitalbazaar/multikey-context'; 24 | import cit_context from 'cit-context'; 25 | 26 | const activitystreams_ctx = 27 | JSON.parse(fs.readFileSync( 28 | path.join(__dirname, 'contexts', 'activitystreams.jsonld'), 'utf8')); 29 | const activitystreams_context = { 30 | contexts: new Map([ 31 | ['https://www.w3.org/ns/activitystreams', activitystreams_ctx] 32 | ]) 33 | }; 34 | 35 | const _contextUrls = [ 36 | ['https://w3id.org/cit/v1', cit_context.contexts], 37 | ['https://w3id.org/citizenship/v1', citizenship_context.contexts], 38 | ['https://w3id.org/security/multikey/v1', multikey_context.contexts], 39 | ['https://w3id.org/security/suites/ed25519-2020/v1', ed25519_2020_context.contexts], 40 | ['https://www.w3.org/2018/credentials/v1', credentials_context.contexts], 41 | ['https://www.w3.org/ns/activitystreams', activitystreams_context.contexts], 42 | ['https://www.w3.org/ns/did/v1', did_context.contexts], 43 | ]; 44 | const contextMap = new Map(_contextUrls.map(c => [c[0], c[1].get(c[0])])); 45 | 46 | const documentLoader = url => { 47 | if(contextMap.has(url)) { 48 | return { 49 | contextUrl: null, 50 | document: contextMap.get(url), 51 | documentUrl: url 52 | }; 53 | } 54 | throw new Error(`Unkonwn URL "${url}".`); 55 | }; 56 | 57 | //const allfiles = fs.readdirSync(path.join(__dirname, '..', 'examples')); 58 | const files = [ 59 | 'cit.jsonld', 60 | 'cit.cborld', 61 | 'didKey.jsonld', 62 | 'didKey.cborld', 63 | 'empty-array.jsonld', 64 | 'empty-array.cborld', 65 | 'empty-object.jsonld', 66 | 'empty-object.cborld', 67 | 'note.jsonld', 68 | 'note.cborld', 69 | 'prc.jsonld', 70 | 'prc.cborld', 71 | 'uncompressible.jsonld', 72 | 'uncompressible.cborld', 73 | ]; 74 | 75 | describe('cborld examples', () => { 76 | // FIXME: outdated examples. Update examples to 77 | // use new tag processing 78 | for(const f of files) { 79 | if(f.endsWith('.jsonld')) { 80 | it(`check encode of JSON-LD: ${f}`, async () => { 81 | const jfn = path.join(__dirname, '..', 'examples', f); 82 | const cfn = jfn.replace('.jsonld', '.cborld'); 83 | const jsonldDocument = JSON.parse(await fsp.readFile(jfn, 'utf8')); 84 | const expectedCborldBytes = await fsp.readFile(cfn, null); 85 | const cborldBytes = await encode({ 86 | jsonldDocument, 87 | format: 'legacy-singleton', 88 | documentLoader 89 | }); 90 | expect(cborldBytes).equalBytes(expectedCborldBytes); 91 | }); 92 | } 93 | if(f.endsWith('.cborld')) { 94 | it(`check decode of CBOR-LD: ${f}`, async () => { 95 | const cfn = path.join(__dirname, '..', 'examples', f); 96 | const jfn = cfn.replace('.cborld', '.jsonld'); 97 | const cborldBytes = await fsp.readFile(cfn, null); 98 | const expectedJsonldDocument = 99 | JSON.parse(await fsp.readFile(jfn, 'utf8')); 100 | const jsonldDocument = await decode({ 101 | cborldBytes, 102 | documentLoader 103 | }); 104 | expect(jsonldDocument).to.deep.equal(expectedJsonldDocument); 105 | }); 106 | } 107 | } 108 | }); 109 | --------------------------------------------------------------------------------