├── .c8rc ├── .codesandbox └── tasks.json ├── .dependency-cruiser.cjs ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── add-hacktoberfest-labels.yml │ ├── document-gen.yml │ ├── integrity-check.yml │ ├── npm-publish-unstable.yml │ ├── npm-publish.yml │ └── take.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── GOVERNANCE.md ├── LICENSE ├── Q_AND_A.md ├── README.md ├── benchmarks └── store │ ├── index │ ├── index-level.js │ └── search-index.js │ └── message │ └── message-store-level.js ├── build ├── compile-validators.js ├── create-browser-bundle.cjs ├── create-cjs-bundle.cjs ├── esbuild-browser-config.cjs ├── license-check.cjs └── publish-unstable.sh ├── codecov.yml ├── eslint.config.cjs ├── images ├── dwn-architecture.excalidraw └── dwn-architecture.png ├── json-schemas ├── authorization-delegated-grant.json ├── authorization-owner.json ├── authorization.json ├── definitions.json ├── general-jws.json ├── interface-methods │ ├── messages-filter.json │ ├── messages-query.json │ ├── messages-read.json │ ├── messages-subscribe.json │ ├── number-range-filter.json │ ├── pagination-cursor.json │ ├── protocol-definition.json │ ├── protocol-rule-set.json │ ├── protocols-configure.json │ ├── protocols-query.json │ ├── records-delete.json │ ├── records-filter.json │ ├── records-query.json │ ├── records-read.json │ ├── records-subscribe.json │ ├── records-write-data-encoded.json │ ├── records-write-unidentified.json │ ├── records-write.json │ └── string-range-filter.json ├── jwk-verification-method.json ├── jwk │ ├── general-jwk.json │ └── public-jwk.json ├── permissions │ ├── permission-grant-data.json │ ├── permission-request-data.json │ ├── permission-revocation-data.json │ ├── permissions-definitions.json │ └── scopes.json └── signature-payloads │ ├── generic-signature-payload.json │ └── records-write-signature-payload.json ├── karma.conf.cjs ├── karma.conf.debug.cjs ├── package-lock.json ├── package.json ├── src ├── core │ ├── abstract-message.ts │ ├── auth.ts │ ├── dwn-constant.ts │ ├── dwn-error.ts │ ├── grant-authorization.ts │ ├── message-reply.ts │ ├── message.ts │ ├── messages-grant-authorization.ts │ ├── protocol-authorization.ts │ ├── protocols-grant-authorization.ts │ ├── records-grant-authorization.ts │ ├── resumable-task-manager.ts │ └── tenant-gate.ts ├── dwn.ts ├── enums │ └── dwn-interface-method.ts ├── event-log │ ├── event-emitter-stream.ts │ └── event-log-level.ts ├── handlers │ ├── messages-query.ts │ ├── messages-read.ts │ ├── messages-subscribe.ts │ ├── protocols-configure.ts │ ├── protocols-query.ts │ ├── records-delete.ts │ ├── records-query.ts │ ├── records-read.ts │ ├── records-subscribe.ts │ └── records-write.ts ├── index.ts ├── interfaces │ ├── messages-query.ts │ ├── messages-read.ts │ ├── messages-subscribe.ts │ ├── protocols-configure.ts │ ├── protocols-query.ts │ ├── records-delete.ts │ ├── records-query.ts │ ├── records-read.ts │ ├── records-subscribe.ts │ └── records-write.ts ├── jose │ ├── algorithms │ │ └── signing │ │ │ ├── ed25519.ts │ │ │ └── signature-algorithms.ts │ └── jws │ │ └── general │ │ ├── builder.ts │ │ └── verifier.ts ├── protocols │ ├── permission-grant.ts │ ├── permission-request.ts │ └── permissions.ts ├── schema-validator.ts ├── store │ ├── blockstore-level.ts │ ├── blockstore-mock.ts │ ├── data-store-level.ts │ ├── index-level.ts │ ├── level-wrapper.ts │ ├── message-store-level.ts │ ├── resumable-task-store-level.ts │ └── storage-controller.ts ├── types │ ├── cache.ts │ ├── data-store.ts │ ├── event-log.ts │ ├── jose-types.ts │ ├── jws-types.ts │ ├── message-interface.ts │ ├── message-store.ts │ ├── message-types.ts │ ├── messages-types.ts │ ├── method-handler.ts │ ├── permission-types.ts │ ├── protocols-types.ts │ ├── query-types.ts │ ├── records-types.ts │ ├── resumable-task-store.ts │ ├── signer.ts │ └── subscriptions.ts └── utils │ ├── abort.ts │ ├── array.ts │ ├── cid.ts │ ├── data-stream.ts │ ├── encoder.ts │ ├── encryption.ts │ ├── filter.ts │ ├── hd-key.ts │ ├── jws.ts │ ├── memory-cache.ts │ ├── messages.ts │ ├── object.ts │ ├── private-key-signer.ts │ ├── protocols.ts │ ├── records.ts │ ├── secp256k1.ts │ ├── secp256r1.ts │ ├── string.ts │ ├── time.ts │ └── url.ts ├── tests ├── core │ ├── auth.spec.ts │ ├── message-reply.spec.ts │ ├── message.spec.ts │ └── protocol-authorization.spec.ts ├── dwn.spec.ts ├── event-log │ ├── event-emitter-stream.spec.ts │ ├── event-log-level.spec.ts │ ├── event-log.spec.ts │ └── event-stream.spec.ts ├── features │ ├── author-delegated-grant.spec.ts │ ├── owner-delegated-grant.spec.ts │ ├── owner-signature.spec.ts │ ├── permissions.spec.ts │ ├── protocol-create-action.spec.ts │ ├── protocol-delete-action.spec.ts │ ├── protocol-update-action.spec.ts │ ├── records-prune.spec.ts │ ├── records-tags.spec.ts │ └── resumable-tasks.spec.ts ├── handlers │ ├── messages-query.spec.ts │ ├── messages-read.spec.ts │ ├── messages-subscribe.spec.ts │ ├── protocols-configure.spec.ts │ ├── protocols-query.spec.ts │ ├── records-delete.spec.ts │ ├── records-query.spec.ts │ ├── records-read.spec.ts │ ├── records-subscribe.spec.ts │ └── records-write.spec.ts ├── interfaces │ ├── messages-get.spec.ts │ ├── messages-subscribe.spec.ts │ ├── messagess-query.spec.ts │ ├── protocols-configure.spec.ts │ ├── protocols-query.spec.ts │ ├── records-delete.spec.ts │ ├── records-query.spec.ts │ ├── records-read.spec.ts │ ├── records-subscribe.spec.ts │ └── records-write.spec.ts ├── jose │ └── jws │ │ └── general.spec.ts ├── protocols │ ├── permission-request.spec.ts │ └── permissions.spec.ts ├── scenarios │ ├── aggregator.spec.ts │ ├── deleted-record.spec.ts │ ├── end-to-end-tests.spec.ts │ ├── messages-query.spec.ts │ ├── nested-roles.spec.ts │ └── subscriptions.spec.ts ├── store-dependent-tests.spec.ts ├── store │ ├── blockstore-mock.spec.ts │ ├── data-store-level.spec.ts │ ├── index-level.spec.ts │ ├── message-store-level.spec.ts │ └── message-store.spec.ts ├── test-event-stream.ts ├── test-stores.ts ├── test-suite.ts ├── utils │ ├── cid.spec.ts │ ├── data-stream.spec.ts │ ├── encryption.spec.ts │ ├── filters.spec.ts │ ├── hd-key.spec.ts │ ├── jws.spec.ts │ ├── memory-cache.spec.ts │ ├── messages.spec.ts │ ├── object.spec.ts │ ├── poller.ts │ ├── private-key-signer.spec.ts │ ├── records.spec.ts │ ├── secp256k1.spec.ts │ ├── secp256r1.spec.ts │ ├── test-data-generator.ts │ ├── test-stub-generator.ts │ ├── time.spec.ts │ └── url.spec.ts ├── validation │ └── json-schemas │ │ ├── definitions.spec.ts │ │ ├── jwk-verification-method.spec.ts │ │ ├── jwk │ │ ├── general-jwk.spec.ts │ │ └── public-jwk.spec.ts │ │ ├── protocols │ │ └── protocols-configure.spec.ts │ │ └── records │ │ ├── records-query.spec.ts │ │ └── records-write.spec.ts └── vectors │ └── protocol-definitions │ ├── anyone-collaborate.json │ ├── author-can.json │ ├── chat.json │ ├── contribution-reward.json │ ├── credential-issuance.json │ ├── dex.json │ ├── email.json │ ├── free-for-all.json │ ├── friend-role.json │ ├── message.json │ ├── minimal.json │ ├── nested.json │ ├── post-comment.json │ ├── private-protocol.json │ ├── recipient-can.json │ ├── slack.json │ ├── social-media.json │ └── thread-role.json └── tsconfig.json /.c8rc: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "cache": false, 4 | "extension": [ 5 | ".js" 6 | ], 7 | "include": [ 8 | "dist/esm/src/**" 9 | ], 10 | "exclude": [ 11 | "dist/esm/src/types/**" 12 | ], 13 | "reporter": [ 14 | "text", 15 | "cobertura", 16 | "html", 17 | "json-summary" 18 | ] 19 | } -------------------------------------------------------------------------------- /.codesandbox/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://codesandbox.io/schemas/tasks.json", 3 | "setupTasks": [ 4 | { 5 | "name": "Installing Dependencies", 6 | "command": "npm install" 7 | } 8 | ], 9 | "tasks": { 10 | "dev": { 11 | "name": "Build DWN SDK", 12 | "command": "npm run build", 13 | "runAtStart": true, 14 | "restartOn": { 15 | "files": ["package-lock.json"] 16 | } 17 | }, 18 | "tests": { 19 | "name": "Run tests", 20 | "command": "npm run test:node" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | ## What type of PR is this? (check all applicable) 18 | 19 | - [ ] ♻️ Refactor 20 | - [ ] ✨ New Feature 21 | - [ ] 🐛 Bug Fix 22 | - [ ] 📝 Documentation Update 23 | - [ ] 👷 Example Application 24 | - [ ] 🧑‍💻 Code Snippet 25 | - [ ] 🎨 Design 26 | - [ ] 📖 Content 27 | - [ ] 🧪 Tests 28 | - [ ] 🔖 Release 29 | - [ ] 🚩 Other 30 | 31 | ## Description 32 | 33 | 34 | 35 | This PR [adds/removes/fixes/replaces] this [feature/bug/etc]. 36 | 37 | ## Related Tickets & Documents 38 | 42 | Resolves # 43 | 44 | ## Mobile & Desktop Screenshots/Recordings 45 | 46 | 47 | 48 | ## Added code snippets? 49 | - [ ] 👍 yes 50 | - [ ] 🙅 no, because they aren't needed 51 | 52 | ## Added tests? 53 | 54 | - [ ] 👍 yes 55 | - [ ] 🙅 no, because they aren't needed 56 | - [ ] 🙋 no, because I need help 57 | 58 | ### No tests? Add a note 59 | 62 | 63 | ## Added to documentation? 64 | 65 | - [ ] 📜 readme 66 | - [ ] 📜 contributing.md 67 | - [ ] 📓 general documentation 68 | - [ ] 🙅 no documentation needed 69 | 70 | ### No docs? Add a note 71 | 74 | 75 | ## [optional] Are there any post-deployment tasks we need to perform? 76 | 77 | 78 | 79 | ## [optional] What gif best describes this PR or how it makes you feel? 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /.github/workflows/document-gen.yml: -------------------------------------------------------------------------------- 1 | name: Documentation generator 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout repository 15 | uses: actions/checkout@v4 16 | - name: setup node.js 17 | uses: actions/setup-node@v4 18 | 19 | - name: install dependencies 20 | run: | 21 | npm clean-install 22 | npm i -d @types/node 23 | - run: npm run build 24 | - name: build typedoc site 25 | run: | 26 | npx typedoc 27 | - name: make git repo to push to github actions 28 | run: | 29 | cd documentation 30 | git init 31 | git add -A 32 | git config user.name 'GitHub Actions' 33 | git config user.email 'actions@github.com' 34 | git commit -sam "$(date -Iseconds)" 35 | touch .nojekyll 36 | 37 | - name: push documentation files to docs branch 38 | uses: ad-m/github-push-action@v0.6.0 39 | with: 40 | github_token: ${{ secrets.GITHUB_TOKEN }} 41 | branch: gh-pages 42 | force: true 43 | directory: documentation 44 | -------------------------------------------------------------------------------- /.github/workflows/integrity-check.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs a handful of integrity checks. 2 | 3 | # when: 4 | # - a pull request is opened against main 5 | # - commits are pushed to main 6 | 7 | # what: 8 | # - transpiles ts -> js and builds browser bundles to ensure everything builds without error. 9 | # - runs type checker 10 | # - runs tests (currently in node environment only) 11 | # - runs linter. Will fail if there are any warnings 12 | 13 | name: Continuous Integration Checks 14 | 15 | on: 16 | push: 17 | branches: [main] 18 | pull_request: 19 | branches: [main] 20 | # used to run action manually via the UI 21 | workflow_dispatch: 22 | 23 | jobs: 24 | security-check: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Use Node.js 18.17.0 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 18.17.0 32 | - run: npm audit 33 | 34 | test-with-node: 35 | runs-on: ubuntu-latest 36 | # read more about matrix strategies here: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix 37 | strategy: 38 | matrix: 39 | node-version: [18.17.0, 20.3.0] 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Use Node.js ${{ matrix.node-version }} 43 | uses: actions/setup-node@v4 44 | with: 45 | node-version: ${{ matrix.node-version }} 46 | # https://docs.npmjs.com/cli/v8/commands/npm-ci 47 | - run: npm clean-install 48 | # check the licenses allow list to avoid incorrectly licensed dependencies creeping in: 49 | - run: npm run license-check 50 | # builds all bundles 51 | - run: npm run build 52 | # run circular dependency check, job will fail if there is an error 53 | - run: npm run circular-dependency-check 54 | # runs linter. Job will fail if there are any warnings or errors 55 | - run: npm run lint 56 | # runs tests in node environment 57 | - run: npm run test:node 58 | 59 | - name: Upload coverage reports to Codecov 60 | uses: codecov/codecov-action@v4 61 | with: 62 | token: ${{ secrets.CODECOV_TOKEN }} 63 | fail_ci_if_error: true 64 | verbose: true 65 | 66 | test-with-browsers: 67 | # Run browser tests using macOS so that WebKit tests don't fail under a Linux environment 68 | runs-on: macos-latest 69 | steps: 70 | - name: Checkout source 71 | uses: actions/checkout@v4 72 | 73 | - name: Set up Node.js 74 | uses: actions/setup-node@v4 75 | with: 76 | node-version: 18 77 | registry-url: https://registry.npmjs.org/ 78 | 79 | - name: Install latest npm 80 | run: npm install -g npm@latest 81 | 82 | - name: Install dependencies 83 | run: npm ci 84 | 85 | - name: Install Playwright Browsers 86 | run: npx playwright install --with-deps 87 | 88 | - name: Build all workspace packages 89 | run: npm run build 90 | 91 | - name: Run tests for all packages 92 | run: npm run test:browser 93 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish-unstable.yml: -------------------------------------------------------------------------------- 1 | name: publish unstable 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | 11 | publish-npm-unstable: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 18.8.0 18 | registry-url: https://registry.npmjs.org/ 19 | - run: npm install 20 | # builds all bundles 21 | - run: npm run build 22 | # Note - this is not required but it gives a clean failure prior to attempting a release if the GH workflow runner is not authenticated with npm.js 23 | - run: npm whoami 24 | env: 25 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 26 | - run: npm run publish:unstable 27 | env: 28 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: publish stable 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | publish-npm: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 18.8.0 18 | registry-url: https://registry.npmjs.org/ 19 | - run: npm clean-install 20 | # check the licences allow list to avoid incorrectly licensed dependencies creeping in: 21 | - run: npm run license-check 22 | # builds all bundles 23 | - run: npm run build 24 | # runs tests in node environment 25 | - run: npm run test:node 26 | # Note - this is not required but it gives a clean failure prior to attempting a release if the GH workflow runner is not authenticated with npm.js 27 | - run: npm whoami 28 | env: 29 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 30 | - run: npm publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 33 | -------------------------------------------------------------------------------- /.github/workflows/take.yml: -------------------------------------------------------------------------------- 1 | name: Auto-assign issue to contributor 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | jobs: 8 | assign: 9 | name: Take an issue 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | steps: 14 | - name: Check if it's October 15 | id: check-month 16 | run: | 17 | current_month=$(date -u +%m) 18 | if [[ $current_month == "10" ]]; then 19 | echo "is_october=true" >> $GITHUB_OUTPUT 20 | else 21 | echo "is_october=false" >> $GITHUB_OUTPUT 22 | fi 23 | 24 | - name: Take the issue 25 | if: steps.check-month.outputs.is_october == 'true' 26 | uses: bdougie/take-action@1439165ac45a7461c2d89a59952cd7d941964b87 27 | with: 28 | message: Thanks for taking this issue! Let us know if you have any questions! 29 | trigger: .take 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Log when outside October 33 | if: steps.check-month.outputs.is_october == 'false' 34 | run: echo "Action skipped because the current date is not in October." -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # bundle metadata 2 | bundle-metadata.json 3 | 4 | # js deps 5 | node_modules 6 | # bundler output location 7 | dist 8 | # locally used file as a ts playground 9 | try.ts 10 | # locally used file as a js playground 11 | try.js 12 | 13 | # default location for levelDB data storage in a non-browser env 14 | MESSAGESTORE 15 | DATASTORE 16 | RESUMABLE-TASK-STORE 17 | EVENTLOG 18 | RESOLVERCACHE 19 | # location for levelDB data storage for non-browser tests 20 | TEST-DATASTORE 21 | TEST-MESSAGESTORE 22 | TEST-RESUMABLE-TASK-STORE 23 | TEST-EVENTLOG 24 | 25 | # default location for index specific levelDB data storage in a non-browser env 26 | INDEX 27 | # location for index specific levelDB data storage levelDB data storage for non-browser tests 28 | TEST-INDEX 29 | BENCHMARK-INDEX 30 | BENCHMARK-BLOCK 31 | # folders used by code coverage 32 | .nyc_output/ 33 | coverage 34 | 35 | generated 36 | 37 | license-report.json -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Tests - Node", 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha", 12 | "runtimeArgs": [ 13 | "dist/esm/tests/**/*.spec.js" 14 | ], 15 | "console": "internalConsole" 16 | }, 17 | { 18 | "type": "chrome", 19 | "request": "attach", 20 | "name": "Attach - Chrome", 21 | "address": "localhost", 22 | "port": 9333, 23 | "pathMapping": { 24 | "/": "${workspaceRoot}/", 25 | "/base/": "${workspaceRoot}/" 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This CODEOWNERS file denotes the project leads 2 | # and encodes their responsibilities for code review. 3 | 4 | # Instructions: At a minimum, replace the '@GITHUB_USER_NAME_GOES_HERE' 5 | # here with at least one project lead. 6 | 7 | # Lines starting with '#' are comments. 8 | # Each line is a file pattern followed by one or more owners. 9 | # The format is described: https://github.blog/2017-07-06-introducing-code-owners/ 10 | # These owners will be the default owners for everything in the repo. 11 | * @mistermoe @csuwildcat @thehenrytsai @diehuxx @lirancohen 12 | 13 | # ----------------------------------------------- 14 | # BELOW THIS LINE ARE TEMPLATES, UNUSED 15 | # ----------------------------------------------- 16 | # Order is important. The last matching pattern has the most precedence. 17 | # So if a pull request only touches javascript files, only these owners 18 | # will be requested to review. 19 | # *.js @octocat @github/js 20 | 21 | # You can also use email addresses if you prefer. 22 | # docs/* docs@example.com -------------------------------------------------------------------------------- /GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # TBD Open Source Project Governance 2 | 3 | 4 | 5 | * [Contributors](#contributors) 6 | * [Maintainers](#maintainers) 7 | * [Governance Committee](#governance-committee) 8 | 9 | 10 | 11 | ## Contributors 12 | 13 | Anyone may be a contributor to TBD projects. Contribution may take the form of: 14 | 15 | * Asking and answering questions on the development forums 16 | * Filing an issue 17 | * Offering a feature or bug fix via a Pull Request 18 | * Suggesting documentation improvements 19 | * ...and more! 20 | 21 | Anyone with a GitHub account may use the project issue trackers and communications channels. We welcome newcomers, so don't hesitate to say hi! 22 | 23 | ## Maintainers 24 | 25 | Maintainers have write access to GitHub repositories and act as project administrators. They approve and merge pull requests, cut releases, and guide collaboration with the community. They have: 26 | 27 | * Commit access to their project's repositories 28 | * Write access to continuous integration (CI) jobs 29 | 30 | Both maintainers and non-maintainers may propose changes to 31 | source code. The mechanism to propose such a change is a GitHub pull request. Maintainers review and merge (_land_) pull requests. 32 | 33 | If a maintainer opposes a proposed change, then the change cannot land. The exception is if the Governance Committee (GC) votes to approve the change despite the opposition. Usually, involving the GC is unnecessary. 34 | 35 | See: 36 | 37 | * [List of maintainers - `CODEOWNERS`](https://github.com/TBD54566975/dwn-sdk-js/blob/main/CODEOWNERS) 38 | * [Contribution Guide - `CONTRIBUTING.md`](https://github.com/TBD54566975/dwn-sdk-js/blob/main/CONTRIBUTING.md) 39 | 40 | ### Maintainer activities 41 | 42 | * Helping users and novice contributors 43 | * Contributing code and documentation changes that improve the project 44 | * Reviewing and commenting on issues and pull requests 45 | * Participation in working groups 46 | * Merging pull requests 47 | 48 | ## Governance Committee 49 | 50 | The TBD Open Source Governance Committee (GC) has final authority over this project, including: 51 | 52 | * Technical direction 53 | * Project governance and process (including this policy) 54 | * Contribution policy 55 | * GitHub repository hosting 56 | * Conduct guidelines 57 | * Maintaining the list of maintainers 58 | 59 | The current GC members are: 60 | 61 | * Angie Jones, Head of Developer Relations, TBD 62 | * Julie Kim, Head of Legal, TBD 63 | * Nidhi Nahar, Head of Patents and Open Source, Block 64 | * Andrew Lee Rubinger, Head of Open Source, TBD 65 | * Max Sills, Counsel for Intellectual Property, Block 66 | 67 | Members are not to be contacted individually. The GC may be reached through `tbd-open-source-governance@squareup.com` and is an available resource in mediation or for sensitive cases beyond the scope of project maintainers. It operates as a "Self-appointing council or board" as defined by Red Hat: [Open Source Governance Models](https://www.redhat.com/en/blog/understanding-open-source-governance-models). -------------------------------------------------------------------------------- /benchmarks/store/index/index-level.js: -------------------------------------------------------------------------------- 1 | import { IndexLevel } from '../../../dist/esm/src/store/index-level.js'; 2 | import { v4 as uuid } from 'uuid'; 3 | 4 | const tenant = 'did:xyz:alice'; 5 | 6 | // create 7 | 8 | const createStart = Date.now(); 9 | const index = new IndexLevel({ 10 | location: 'BENCHMARK-INDEX' 11 | }); 12 | await index.open(); 13 | const createEnd = Date.now(); 14 | console.log('create', createEnd - createStart); 15 | 16 | // clear - before 17 | 18 | const clearBeforeStart = Date.now(); 19 | await index.clear(); 20 | const clearBeforeEnd = Date.now(); 21 | console.log('clear - before', clearBeforeEnd - clearBeforeStart); 22 | 23 | // put 24 | 25 | const putStart = Date.now(); 26 | await Promise.all(Array(10_000).fill().map((_,i) => { 27 | const id = uuid(); 28 | const doc = { test: 'foo', number: Math.random() }; 29 | return index.put(tenant, id, doc, doc, { index: i, number: Math.random(), id }); 30 | })); 31 | const putEnd = Date.now(); 32 | console.log('put', putEnd - putStart); 33 | 34 | // query - equal 35 | 36 | const queryEqualStart = Date.now(); 37 | await index.query(tenant, [{ 38 | 'test': 'foo' 39 | }], { sortProperty: 'id' }); 40 | const queryEqualEnd = Date.now(); 41 | console.log('query - equal', queryEqualEnd - queryEqualStart); 42 | 43 | // query - range 44 | 45 | const queryRangeStart = Date.now(); 46 | await index.query(tenant, [{ 47 | 'number': { gte: 0.5 } 48 | }],{ sortProperty: 'id' }); 49 | const queryRangeEnd = Date.now(); 50 | console.log('query - range', queryRangeEnd - queryRangeStart); 51 | 52 | const multipleRangeStart = Date.now(); 53 | await index.query(tenant, [ 54 | { 'number': { lte: 0.1 } }, 55 | { 'number': { gte: 0.5 } } 56 | ],{ sortProperty: 'id' }); 57 | const multipleRangeEnd = Date.now(); 58 | console.log('query - multiple range', multipleRangeEnd - multipleRangeStart); 59 | 60 | // clear - after 61 | 62 | const clearAfterStart = Date.now(); 63 | await index.clear(); 64 | const clearAfterEnd = Date.now(); 65 | console.log('clear - after', clearAfterEnd - clearAfterStart); 66 | -------------------------------------------------------------------------------- /benchmarks/store/index/search-index.js: -------------------------------------------------------------------------------- 1 | import searchIndex from 'search-index'; 2 | import { v4 as uuid } from 'uuid'; 3 | 4 | // create 5 | 6 | const createStart = Date.now(); 7 | const index = await searchIndex({ name: 'BENCHMARK-INDEX' }); 8 | const createEnd = Date.now(); 9 | console.log('create', createEnd - createStart); 10 | 11 | // clear - before 12 | 13 | const clearBeforeStart = Date.now(); 14 | await index.FLUSH(); 15 | const clearBeforeEnd = Date.now(); 16 | console.log('clear - before', clearBeforeEnd - clearBeforeStart); 17 | 18 | // put 19 | 20 | const putStart = Date.now(); 21 | await Promise.all(Array(10_000).fill().map(() => index.PUT([ { 22 | _id : uuid(), 23 | test : 'foo', 24 | number : String(Math.random()) 25 | } ], { tokenSplitRegex: /.+/ }))); 26 | const putEnd = Date.now(); 27 | console.log('put', putEnd - putStart); 28 | 29 | // query - equal 30 | 31 | const queryEqualStart = Date.now(); 32 | await index.QUERY({ AND: [ { 33 | FIELD : 'test', 34 | VALUE : 'foo' 35 | } ] }); 36 | const queryEqualEnd = Date.now(); 37 | console.log('query - equal', queryEqualEnd - queryEqualStart); 38 | 39 | // query - range 40 | 41 | const queryRangeStart = Date.now(); 42 | await index.QUERY({ AND: [ { 43 | FIELD : 'number', 44 | VALUE : { GTE: '0.5' } 45 | } ] }); 46 | const queryRangeEnd = Date.now(); 47 | console.log('query - range', queryRangeEnd - queryRangeStart); 48 | 49 | const multipleRangeStart = Date.now(); 50 | await index.QUERY({ AND: [ { 51 | FIELD : 'number', 52 | VALUE : { LTE: '0.1' } 53 | },{ 54 | FIELD : 'number', 55 | VALUE : { GTE: '0.5' } 56 | } ] }); 57 | const multipleRangeEnd = Date.now(); 58 | console.log('query - multiple range', multipleRangeEnd - multipleRangeStart); 59 | 60 | // clear - after 61 | 62 | const clearAfterStart = Date.now(); 63 | await index.FLUSH(); 64 | const clearAfterEnd = Date.now(); 65 | console.log('clear - after', clearAfterEnd - clearAfterStart); 66 | -------------------------------------------------------------------------------- /build/create-browser-bundle.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const esbuild = require('esbuild'); 3 | const browserConfig = require('./esbuild-browser-config.cjs'); 4 | 5 | esbuild.build({ 6 | ...browserConfig, 7 | outfile : 'dist/bundles/dwn.js', 8 | metafile : true, 9 | sourcemap : false, 10 | }).then(result => { 11 | const serializedMetafile = JSON.stringify(result.metafile, null, 4); 12 | fs.writeFileSync(`${__dirname}/../bundle-metadata.json`, serializedMetafile, { encoding: 'utf8' }); 13 | }); 14 | -------------------------------------------------------------------------------- /build/create-cjs-bundle.cjs: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const packageJson = require('../package.json'); 3 | 4 | // list of dependencies that _dont_ ship cjs 5 | const includeList = new Set([ 6 | '@ipld/dag-cbor', 7 | '@noble/ed25519', 8 | '@noble/secp256k1', 9 | 'blockstore-core', 10 | 'ipfs-unixfs-exporter', 11 | 'ipfs-unixfs-importer', 12 | 'multiformats', 13 | 'uint8arrays' 14 | ]); 15 | 16 | // create list of dependencies that we _do not_ want to include in our bundle 17 | const excludeList = []; 18 | for (const dependency in packageJson.dependencies) { 19 | if (includeList.has(dependency)) { 20 | continue; 21 | } else { 22 | excludeList.push(dependency); 23 | } 24 | } 25 | 26 | /** @type {import('esbuild').BuildOptions} */ 27 | const baseConfig = { 28 | platform : 'node', 29 | format : 'cjs', 30 | bundle : true, 31 | external : excludeList, 32 | }; 33 | 34 | const indexConfig = { 35 | ...baseConfig, 36 | entryPoints : ['./dist/esm/src/index.js'], 37 | outfile : './dist/cjs/index.js', 38 | }; 39 | 40 | esbuild.buildSync(indexConfig); 41 | -------------------------------------------------------------------------------- /build/esbuild-browser-config.cjs: -------------------------------------------------------------------------------- 1 | const polyfillProviderPlugin = require('node-stdlib-browser/helpers/esbuild/plugin'); 2 | const stdLibBrowser = require('node-stdlib-browser'); 3 | 4 | /** @type {import('esbuild').BuildOptions} */ 5 | module.exports = { 6 | entryPoints : ['./src/index.ts'], 7 | bundle : true, 8 | format : 'esm', 9 | sourcemap : true, 10 | minify : true, 11 | platform : 'browser', 12 | target : ['chrome101', 'firefox108', 'safari16'], 13 | inject : [require.resolve('node-stdlib-browser/helpers/esbuild/shim')], 14 | plugins : [polyfillProviderPlugin(stdLibBrowser)], 15 | define : { 16 | 'global': 'globalThis' 17 | } 18 | }; -------------------------------------------------------------------------------- /build/license-check.cjs: -------------------------------------------------------------------------------- 1 | /* 2 | * use an allowlist 3 | */ 4 | allowedLicences = ['ISC','MIT', 'BSD', 'Apache-2.0', 'CC0-1.0']; 5 | 6 | 7 | function main() { 8 | var fs = require('fs'); 9 | var license = fs.readFileSync('license-report.json', 'utf8'); 10 | 11 | var licenseJson = JSON.parse(license); 12 | 13 | for (var i = 0; i < licenseJson.length; i++) { 14 | var licenseType = licenseJson[i].licenseType; 15 | 16 | allowed = false; 17 | for (var j = 0; j < allowedLicences.length; j++) { 18 | if (licenseType === allowedLicences[j]) { 19 | allowed = true; 20 | } 21 | if (!allowed && licenseType.includes(allowedLicences[j])) { 22 | allowed = true; 23 | } 24 | } 25 | 26 | if (!allowed) { 27 | //exit with error 28 | console.log('Found unapproved license: ' + licenseType + '. If this is a valid license, please add it to the allowlist.'); 29 | console.log(licenseJson[i]); 30 | process.exit(1); 31 | } 32 | 33 | } 34 | } 35 | 36 | 37 | main(); -------------------------------------------------------------------------------- /build/publish-unstable.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script handles the publishing of the current 4 | # commits as an npm based unstable package 5 | 6 | # Add dev dependencies to current path 7 | export PATH="$PATH:node_modules/.bin" 8 | 9 | # Fetch the current version from the package.json 10 | new_version=$(node -pe "require('./package.json').version") 11 | 12 | # Generate the new unstable version 13 | new_unstable_version=$new_version"-unstable-$(date +'%Y-%m-%d-%M-%S')-$(git rev-parse --short HEAD)" 14 | 15 | # Set the unstable version in the package.json 16 | npm version $new_unstable_version --no-git-tag-version 17 | 18 | # Publish the unstable version 19 | npm publish --tag unstable --no-git-tag-version 20 | 21 | # Reset changes to the package.json 22 | git checkout -- package.json 23 | git checkout -- package-lock.json -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | 7 | ignore: 8 | - "src/types" -------------------------------------------------------------------------------- /eslint.config.cjs: -------------------------------------------------------------------------------- 1 | const tsParser = require("@typescript-eslint/parser"); 2 | 3 | const tsPlugin = require("@typescript-eslint/eslint-plugin"); 4 | const todoPlzPlugin = require('eslint-plugin-todo-plz'); 5 | 6 | module.exports = [{ 7 | languageOptions: { 8 | parser: tsParser, 9 | parserOptions: { 10 | ecmaFeatures: { modules: true }, 11 | ecmaVersion: 'latest', 12 | project: './tsconfig.json', 13 | }, 14 | }, 15 | plugins: { 16 | "@typescript-eslint": tsPlugin, 17 | 'todo-plz': todoPlzPlugin // for enforcing TODO formatting to require "github.com/TBD54566975/dwn-sdk-js/issues/" 18 | }, 19 | files: [ 20 | '**/*.ts' 21 | ], 22 | // IMPORTANT and confusing: `ignores` only exclude files from the `files` setting. 23 | // To exclude *.js files entirely, you need to have a separate config object altogether. (See another `ignores` below.) 24 | ignores: [ 25 | '**/*.d.ts', 26 | ], 27 | rules: { 28 | 'curly' : ['error', 'all'], 29 | 'no-console' : 'off', 30 | 'indent' : [ 31 | 'error', 32 | 2 33 | ], 34 | 'object-curly-spacing' : ['error', 'always'], 35 | 'linebreak-style' : [ 36 | 'error', 37 | 'unix' 38 | ], 39 | 'quotes': [ 40 | 'error', 41 | 'single', 42 | { 'allowTemplateLiterals': true } 43 | ], 44 | '@typescript-eslint/semi' : ['error', 'always'], 45 | 'semi' : ['off'], 46 | 'no-multi-spaces' : ['error'], 47 | 'no-trailing-spaces' : ['error'], 48 | 'max-len' : ['error', { 'code': 150, 'ignoreStrings': true }], 49 | 'key-spacing' : [ 50 | 'error', 51 | { 52 | 'align': { 53 | 'beforeColon' : true, 54 | 'afterColon' : true, 55 | 'on' : 'colon' 56 | } 57 | } 58 | ], 59 | 'keyword-spacing' : ['error', { 'before': true, 'after': true }], 60 | '@typescript-eslint/explicit-function-return-type' : ['error'], 61 | 'no-unused-vars' : 'off', 62 | // enforce `import type` when an import is not used at runtime, allowing transpilers/bundlers to drop imports as an optimization 63 | '@typescript-eslint/consistent-type-imports' : 'error', 64 | '@typescript-eslint/no-unused-vars' : [ 65 | 'error', 66 | { 67 | 'vars' : 'all', 68 | 'args' : 'after-used', 69 | 'ignoreRestSiblings' : true, 70 | 'argsIgnorePattern' : '^_', 71 | 'varsIgnorePattern' : '^_' 72 | } 73 | ], 74 | 75 | 'prefer-const' : ['error', { 'destructuring': 'all' }], 76 | 'sort-imports' : ['error', { 77 | 'ignoreCase' : true, 78 | 'ignoreDeclarationSort' : false, 79 | 'ignoreMemberSort' : false, 80 | 'memberSyntaxSortOrder' : ['none', 'all', 'single', 'multiple'], 81 | 'allowSeparatedGroups' : true 82 | }], 83 | // enforce github issue reference for every TO-DO comment 84 | 'todo-plz/ticket-ref': ['error', { 'commentPattern': '.*github\.com\/TBD54566975\/dwn-sdk-js\/issues\/.*' }], 85 | } 86 | }, { 87 | ignores: [ 88 | '**/*.js', 89 | ], 90 | }]; 91 | -------------------------------------------------------------------------------- /images/dwn-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentralized-identity/dwn-sdk-js/0e903014464388bcfcdcd42da74c40fdd902fd23/images/dwn-architecture.png -------------------------------------------------------------------------------- /json-schemas/authorization-delegated-grant.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://identity.foundation/dwn/json-schemas/authorization-delegated-grant.json", 3 | "$schema": "https://json-schema.org/draft/2020-12/schema", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "properties": { 7 | "signature": { 8 | "$ref": "https://identity.foundation/dwn/json-schemas/general-jws.json" 9 | }, 10 | "authorDelegatedGrant": { 11 | "$ref": "https://identity.foundation/dwn/json-schemas/records-write-data-encoded.json" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /json-schemas/authorization-owner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://identity.foundation/dwn/json-schemas/authorization-owner.json", 3 | "$schema": "https://json-schema.org/draft/2020-12/schema", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "properties": { 7 | "signature": { 8 | "$ref": "https://identity.foundation/dwn/json-schemas/general-jws.json" 9 | }, 10 | "authorDelegatedGrant": { 11 | "$ref": "https://identity.foundation/dwn/json-schemas/records-write-data-encoded.json" 12 | }, 13 | "ownerSignature": { 14 | "$ref": "https://identity.foundation/dwn/json-schemas/general-jws.json" 15 | }, 16 | "ownerDelegatedGrant": { 17 | "$ref": "https://identity.foundation/dwn/json-schemas/records-write-data-encoded.json" 18 | } 19 | }, 20 | "description": "`signature` can exist by itself. But if `ownerSignature` is present, then `signature` must also exist", 21 | "dependencies": { 22 | "ownerSignature": [ 23 | "signature" 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /json-schemas/authorization.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://identity.foundation/dwn/json-schemas/authorization.json", 3 | "$schema": "https://json-schema.org/draft/2020-12/schema", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "properties": { 7 | "signature": { 8 | "$ref": "https://identity.foundation/dwn/json-schemas/general-jws.json" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /json-schemas/definitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/defs.json", 4 | "type": "object", 5 | "$defs": { 6 | "base64url": { 7 | "type": "string", 8 | "pattern": "^[A-Za-z0-9_-]+$" 9 | }, 10 | "uuid": { 11 | "type": "string", 12 | "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" 13 | }, 14 | "did": { 15 | "type": "string", 16 | "pattern": "^did:([a-z0-9]+):((?:(?:[a-zA-Z0-9._-]|(?:%[0-9a-fA-F]{2}))*:)*((?:[a-zA-Z0-9._-]|(?:%[0-9a-fA-F]{2}))+))((;[a-zA-Z0-9_.:%-]+=[a-zA-Z0-9_.:%-]*)*)(\/[^#?]*)?([?][^#]*)?(#.*)?$" 17 | }, 18 | "date-time": { 19 | "type": "string", 20 | "pattern": "^\\d{4}-[0-1]\\d-[0-3]\\dT(?:[0-2]\\d:[0-5]\\d:[0-5]\\d|23:59:60)\\.\\d{6}Z$" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /json-schemas/general-jws.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://identity.foundation/dwn/json-schemas/general-jws.json", 3 | "$schema": "https://json-schema.org/draft/2020-12/schema", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "properties": { 7 | "payload": { 8 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/base64url" 9 | }, 10 | "signatures": { 11 | "type": "array", 12 | "minItems": 1, 13 | "items": { 14 | "type": "object", 15 | "properties": { 16 | "protected": { 17 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/base64url" 18 | }, 19 | "signature": { 20 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/base64url" 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/messages-filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/messages-filter.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "minProperties": 1, 7 | "properties": { 8 | "interface": { 9 | "enum": [ 10 | "Protocols", 11 | "Records" 12 | ], 13 | "type": "string" 14 | }, 15 | "method": { 16 | "enum": [ 17 | "Configure", 18 | "Delete", 19 | "Write" 20 | ], 21 | "type": "string" 22 | }, 23 | "protocol": { 24 | "type": "string" 25 | }, 26 | "messageTimestamp": { 27 | "type": "object", 28 | "minProperties": 1, 29 | "additionalProperties": false, 30 | "properties": { 31 | "from": { 32 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" 33 | }, 34 | "to": { 35 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" 36 | } 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/messages-query.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/messages-query.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "authorization", 8 | "descriptor" 9 | ], 10 | "properties": { 11 | "authorization": { 12 | "$ref": "https://identity.foundation/dwn/json-schemas/authorization.json" 13 | }, 14 | "descriptor": { 15 | "type": "object", 16 | "additionalProperties": false, 17 | "required": [ 18 | "interface", 19 | "method", 20 | "messageTimestamp", 21 | "filters" 22 | ], 23 | "properties": { 24 | "interface": { 25 | "enum": [ 26 | "Messages" 27 | ], 28 | "type": "string" 29 | }, 30 | "method": { 31 | "enum": [ 32 | "Query" 33 | ], 34 | "type": "string" 35 | }, 36 | "messageTimestamp": { 37 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" 38 | }, 39 | "filters": { 40 | "type": "array", 41 | "items": { 42 | "$ref": "https://identity.foundation/dwn/json-schemas/messages-filter.json" 43 | } 44 | }, 45 | "cursor": { 46 | "$ref": "https://identity.foundation/dwn/json-schemas/pagination-cursor.json" 47 | } 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/messages-read.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/messages-read.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "authorization", 8 | "descriptor" 9 | ], 10 | "properties": { 11 | "authorization": { 12 | "$ref": "https://identity.foundation/dwn/json-schemas/authorization.json" 13 | }, 14 | "descriptor": { 15 | "type": "object", 16 | "additionalProperties": false, 17 | "required": [ 18 | "interface", 19 | "method", 20 | "messageTimestamp" 21 | ], 22 | "properties": { 23 | "interface": { 24 | "enum": [ 25 | "Messages" 26 | ], 27 | "type": "string" 28 | }, 29 | "method": { 30 | "enum": [ 31 | "Read" 32 | ], 33 | "type": "string" 34 | }, 35 | "messageTimestamp": { 36 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" 37 | }, 38 | "messageCid": { 39 | "type": "string" 40 | } 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/messages-subscribe.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/messages-subscribe.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "descriptor", 8 | "authorization" 9 | ], 10 | "properties": { 11 | "authorization": { 12 | "$ref": "https://identity.foundation/dwn/json-schemas/authorization.json" 13 | }, 14 | "descriptor": { 15 | "type": "object", 16 | "additionalProperties": false, 17 | "required": [ 18 | "interface", 19 | "method", 20 | "messageTimestamp", 21 | "filters" 22 | ], 23 | "properties": { 24 | "interface": { 25 | "enum": [ 26 | "Messages" 27 | ], 28 | "type": "string" 29 | }, 30 | "method": { 31 | "enum": [ 32 | "Subscribe" 33 | ], 34 | "type": "string" 35 | }, 36 | "messageTimestamp": { 37 | "type": "string" 38 | }, 39 | "filters": { 40 | "type": "array", 41 | "items": { 42 | "$ref": "https://identity.foundation/dwn/json-schemas/messages-filter.json" 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/number-range-filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/number-range-filter.json", 4 | "type": "object", 5 | "minProperties": 1, 6 | "additionalProperties": false, 7 | "properties": { 8 | "gt": { 9 | "type": "number" 10 | }, 11 | "gte": { 12 | "type": "number" 13 | }, 14 | "lt": { 15 | "type": "number" 16 | }, 17 | "lte": { 18 | "type": "number" 19 | } 20 | }, 21 | "dependencies": { 22 | "gt": { 23 | "not": { 24 | "required": [ 25 | "gte" 26 | ] 27 | } 28 | }, 29 | "gte": { 30 | "not": { 31 | "required": [ 32 | "gt" 33 | ] 34 | } 35 | }, 36 | "lt": { 37 | "not": { 38 | "required": [ 39 | "lte" 40 | ] 41 | } 42 | }, 43 | "lte": { 44 | "not": { 45 | "required": [ 46 | "lt" 47 | ] 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/pagination-cursor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/pagination-cursor.json", 4 | "type": "object", 5 | "minProperties": 1, 6 | "additionalProperties": false, 7 | "required": [ 8 | "messageCid", 9 | "value" 10 | ], 11 | "properties": { 12 | "messageCid": { 13 | "type": "string" 14 | }, 15 | "value": { 16 | "type": [ 17 | "string", 18 | "number" 19 | ] 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/protocol-definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://identity.foundation/dwn/json-schemas/protocol-definition.json", 3 | "$schema": "https://json-schema.org/draft/2020-12/schema", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "protocol", 8 | "published", 9 | "types", 10 | "structure" 11 | ], 12 | "properties": { 13 | "protocol": { 14 | "type": "string" 15 | }, 16 | "published": { 17 | "type": "boolean" 18 | }, 19 | "types": { 20 | "type": "object", 21 | "patternProperties": { 22 | ".*": { 23 | "type": "object", 24 | "additionalProperties": false, 25 | "properties": { 26 | "schema": { 27 | "type": "string" 28 | }, 29 | "dataFormats": { 30 | "type": "array", 31 | "minItems": 1, 32 | "items": { 33 | "type": "string" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | }, 40 | "structure": { 41 | "type": "object", 42 | "patternProperties": { 43 | ".*": { 44 | "$ref": "https://identity.foundation/dwn/json-schemas/protocol-rule-set.json" 45 | } 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/protocols-configure.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/protocols-configure.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "authorization", 8 | "descriptor" 9 | ], 10 | "properties": { 11 | "authorization": { 12 | "$ref": "https://identity.foundation/dwn/json-schemas/authorization-delegated-grant.json" 13 | }, 14 | "descriptor": { 15 | "type": "object", 16 | "additionalProperties": false, 17 | "required": [ 18 | "interface", 19 | "method", 20 | "messageTimestamp", 21 | "definition" 22 | ], 23 | "properties": { 24 | "interface": { 25 | "enum": [ 26 | "Protocols" 27 | ], 28 | "type": "string" 29 | }, 30 | "method": { 31 | "enum": [ 32 | "Configure" 33 | ], 34 | "type": "string" 35 | }, 36 | "messageTimestamp": { 37 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" 38 | }, 39 | "definition": { 40 | "$ref": "https://identity.foundation/dwn/json-schemas/protocol-definition.json" 41 | } 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/protocols-query.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/protocols-query.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "descriptor" 8 | ], 9 | "properties": { 10 | "authorization": { 11 | "$ref": "https://identity.foundation/dwn/json-schemas/authorization.json" 12 | }, 13 | "descriptor": { 14 | "type": "object", 15 | "additionalProperties": false, 16 | "required": [ 17 | "interface", 18 | "method", 19 | "messageTimestamp" 20 | ], 21 | "properties": { 22 | "interface": { 23 | "enum": [ 24 | "Protocols" 25 | ], 26 | "type": "string" 27 | }, 28 | "method": { 29 | "enum": [ 30 | "Query" 31 | ], 32 | "type": "string" 33 | }, 34 | "messageTimestamp": { 35 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" 36 | }, 37 | "filter": { 38 | "type": "object", 39 | "minProperties": 1, 40 | "additionalProperties": false, 41 | "properties": { 42 | "protocol": { 43 | "type": "string" 44 | }, 45 | "recipient": { 46 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/did" 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/records-delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/records-delete.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "authorization", 8 | "descriptor" 9 | ], 10 | "properties": { 11 | "authorization": { 12 | "$ref": "https://identity.foundation/dwn/json-schemas/authorization-delegated-grant.json" 13 | }, 14 | "descriptor": { 15 | "type": "object", 16 | "additionalProperties": false, 17 | "required": [ 18 | "interface", 19 | "method", 20 | "messageTimestamp", 21 | "recordId", 22 | "prune" 23 | ], 24 | "properties": { 25 | "interface": { 26 | "enum": [ 27 | "Records" 28 | ], 29 | "type": "string" 30 | }, 31 | "method": { 32 | "enum": [ 33 | "Delete" 34 | ], 35 | "type": "string" 36 | }, 37 | "messageTimestamp": { 38 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" 39 | }, 40 | "recordId": { 41 | "type": "string" 42 | }, 43 | "prune": { 44 | "type": "boolean" 45 | } 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/records-query.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/records-query.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "descriptor" 8 | ], 9 | "properties": { 10 | "authorization": { 11 | "$ref": "https://identity.foundation/dwn/json-schemas/authorization-delegated-grant.json" 12 | }, 13 | "descriptor": { 14 | "type": "object", 15 | "additionalProperties": false, 16 | "required": [ 17 | "interface", 18 | "method", 19 | "messageTimestamp", 20 | "filter" 21 | ], 22 | "properties": { 23 | "interface": { 24 | "enum": [ 25 | "Records" 26 | ], 27 | "type": "string" 28 | }, 29 | "method": { 30 | "enum": [ 31 | "Query" 32 | ], 33 | "type": "string" 34 | }, 35 | "messageTimestamp": { 36 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" 37 | }, 38 | "filter": { 39 | "$ref": "https://identity.foundation/dwn/json-schemas/records-filter.json" 40 | }, 41 | "pagination": { 42 | "type": "object", 43 | "additionalProperties": false, 44 | "properties": { 45 | "limit": { 46 | "type": "number", 47 | "minimum": 1 48 | }, 49 | "cursor": { 50 | "$ref": "https://identity.foundation/dwn/json-schemas/pagination-cursor.json" 51 | } 52 | } 53 | }, 54 | "dateSort": { 55 | "enum": [ 56 | "createdAscending", 57 | "createdDescending", 58 | "publishedAscending", 59 | "publishedDescending" 60 | ], 61 | "type": "string" 62 | } 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/records-read.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/records-read.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "descriptor" 8 | ], 9 | "properties": { 10 | "authorization": { 11 | "$ref": "https://identity.foundation/dwn/json-schemas/authorization-delegated-grant.json" 12 | }, 13 | "descriptor": { 14 | "type": "object", 15 | "additionalProperties": false, 16 | "required": [ 17 | "interface", 18 | "method", 19 | "messageTimestamp", 20 | "filter" 21 | ], 22 | "properties": { 23 | "interface": { 24 | "enum": [ 25 | "Records" 26 | ], 27 | "type": "string" 28 | }, 29 | "method": { 30 | "enum": [ 31 | "Read" 32 | ], 33 | "type": "string" 34 | }, 35 | "messageTimestamp": { 36 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" 37 | }, 38 | "filter": { 39 | "$ref": "https://identity.foundation/dwn/json-schemas/records-filter.json" 40 | } 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/records-subscribe.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/records-subscribe.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "descriptor" 8 | ], 9 | "properties": { 10 | "authorization": { 11 | "$ref": "https://identity.foundation/dwn/json-schemas/authorization-delegated-grant.json" 12 | }, 13 | "descriptor": { 14 | "type": "object", 15 | "additionalProperties": false, 16 | "required": [ 17 | "interface", 18 | "method", 19 | "messageTimestamp", 20 | "filter" 21 | ], 22 | "properties": { 23 | "interface": { 24 | "enum": [ 25 | "Records" 26 | ], 27 | "type": "string" 28 | }, 29 | "method": { 30 | "enum": [ 31 | "Subscribe" 32 | ], 33 | "type": "string" 34 | }, 35 | "messageTimestamp": { 36 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" 37 | }, 38 | "filter": { 39 | "$ref": "https://identity.foundation/dwn/json-schemas/records-filter.json" 40 | } 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/records-write-data-encoded.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://identity.foundation/dwn/json-schemas/records-write-data-encoded.json", 3 | "$ref": "https://identity.foundation/dwn/json-schemas/records-write-unidentified.json", 4 | "unevaluatedProperties": false, 5 | "type": "object", 6 | "required": [ 7 | "recordId", 8 | "authorization", 9 | "encodedData" 10 | ], 11 | "properties": { 12 | "encodedData": { 13 | "type": "string" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/records-write.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://identity.foundation/dwn/json-schemas/records-write.json", 3 | "$ref": "https://identity.foundation/dwn/json-schemas/records-write-unidentified.json", 4 | "unevaluatedProperties": false, 5 | "type": "object", 6 | "required": [ 7 | "recordId", 8 | "authorization" 9 | ] 10 | } -------------------------------------------------------------------------------- /json-schemas/interface-methods/string-range-filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/string-range-filter.json", 4 | "type": "object", 5 | "minProperties": 1, 6 | "additionalProperties": false, 7 | "properties": { 8 | "gt": { 9 | "type": "string" 10 | }, 11 | "gte": { 12 | "type": "string" 13 | }, 14 | "lt": { 15 | "type": "string" 16 | }, 17 | "lte": { 18 | "type": "string" 19 | } 20 | }, 21 | "dependencies": { 22 | "gt": { 23 | "not": { 24 | "required": [ 25 | "gte" 26 | ] 27 | } 28 | }, 29 | "gte": { 30 | "not": { 31 | "required": [ 32 | "gt" 33 | ] 34 | } 35 | }, 36 | "lt": { 37 | "not": { 38 | "required": [ 39 | "lte" 40 | ] 41 | } 42 | }, 43 | "lte": { 44 | "not": { 45 | "required": [ 46 | "lt" 47 | ] 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /json-schemas/jwk-verification-method.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://identity.foundation/dwn/json-schemas/jwk-verification-method.json", 3 | "$schema": "https://json-schema.org/draft/2020-12/schema", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "id", 8 | "type", 9 | "controller", 10 | "publicKeyJwk" 11 | ], 12 | "properties": { 13 | "id": { 14 | "type": "string" 15 | }, 16 | "type": { 17 | "enum": [ 18 | "JsonWebKey", 19 | "JsonWebKey2020" 20 | ] 21 | }, 22 | "controller": { 23 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/did" 24 | }, 25 | "publicKeyJwk": { 26 | "$ref": "https://identity.foundation/dwn/json-schemas/public-jwk.json" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /json-schemas/jwk/general-jwk.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://identity.foundation/dwn/json-schemas/general-jwk.json", 3 | "$schema": "https://json-schema.org/draft/2020-12/schema", 4 | "type": "object", 5 | "required": [ 6 | "kty" 7 | ], 8 | "properties": { 9 | "alg": { 10 | "type": "string" 11 | }, 12 | "kid": { 13 | "type": "string" 14 | }, 15 | "kty": { 16 | "enum": [ 17 | "EC", 18 | "RSA", 19 | "oct", 20 | "OKP" 21 | ] 22 | }, 23 | "crv": { 24 | "type": "string" 25 | }, 26 | "use": { 27 | "type": "string" 28 | }, 29 | "key_ops": { 30 | "type": "string" 31 | }, 32 | "x5u": { 33 | "type": "string" 34 | }, 35 | "x5c": { 36 | "type": "string" 37 | }, 38 | "x5t": { 39 | "type": "string" 40 | }, 41 | "x5t#S256": { 42 | "type": "string" 43 | } 44 | }, 45 | "oneOf": [ 46 | { 47 | "properties": { 48 | "kty": { 49 | "const": "EC" 50 | }, 51 | "crv": { 52 | "type": "string" 53 | }, 54 | "x": { 55 | "type": "string" 56 | }, 57 | "y": { 58 | "type": "string" 59 | }, 60 | "d": { 61 | "type": "string" 62 | } 63 | }, 64 | "required": [ 65 | "crv", 66 | "x" 67 | ] 68 | }, 69 | { 70 | "properties": { 71 | "kty": { 72 | "const": "OKP" 73 | }, 74 | "crv": { 75 | "type": "string" 76 | }, 77 | "x": { 78 | "type": "string" 79 | }, 80 | "d": { 81 | "type": "string" 82 | } 83 | }, 84 | "required": [ 85 | "crv", 86 | "x" 87 | ] 88 | }, 89 | { 90 | "properties": { 91 | "kty": { 92 | "const": "RSA" 93 | }, 94 | "n": { 95 | "type": "string" 96 | }, 97 | "e": { 98 | "type": "string" 99 | }, 100 | "d": { 101 | "type": "string" 102 | }, 103 | "p": { 104 | "type": "string" 105 | }, 106 | "q": { 107 | "type": "string" 108 | }, 109 | "dp": { 110 | "type": "string" 111 | }, 112 | "dq": { 113 | "type": "string" 114 | }, 115 | "qi": { 116 | "type": "string" 117 | }, 118 | "oth": { 119 | "type": "object" 120 | } 121 | }, 122 | "required": [ 123 | "n", 124 | "e" 125 | ] 126 | }, 127 | { 128 | "properties": { 129 | "kty": { 130 | "const": "oct" 131 | }, 132 | "k": { 133 | "type": "string" 134 | } 135 | }, 136 | "required": [ 137 | "k" 138 | ] 139 | } 140 | ] 141 | } -------------------------------------------------------------------------------- /json-schemas/jwk/public-jwk.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://identity.foundation/dwn/json-schemas/public-jwk.json", 3 | "$schema": "https://json-schema.org/draft/2020-12/schema", 4 | "$ref": "https://identity.foundation/dwn/json-schemas/general-jwk.json", 5 | "not": { 6 | "anyOf": [ 7 | { 8 | "type": "object", 9 | "properties": { 10 | "kty": { 11 | "const": "EC" 12 | } 13 | }, 14 | "anyOf": [ 15 | { 16 | "required": [ 17 | "d" 18 | ] 19 | } 20 | ] 21 | }, 22 | { 23 | "type": "object", 24 | "properties": { 25 | "kty": { 26 | "const": "OKP" 27 | } 28 | }, 29 | "anyOf": [ 30 | { 31 | "required": [ 32 | "d" 33 | ] 34 | } 35 | ] 36 | }, 37 | { 38 | "type": "object", 39 | "properties": { 40 | "kty": { 41 | "const": "RSA" 42 | }, 43 | "d": {}, 44 | "p": {}, 45 | "q": {}, 46 | "dp": {}, 47 | "dq": {}, 48 | "qi": {}, 49 | "oth": { 50 | "type": "object" 51 | } 52 | }, 53 | "anyOf": [ 54 | { 55 | "required": [ 56 | "d" 57 | ] 58 | }, 59 | { 60 | "required": [ 61 | "p" 62 | ] 63 | }, 64 | { 65 | "required": [ 66 | "q" 67 | ] 68 | }, 69 | { 70 | "required": [ 71 | "dp" 72 | ] 73 | }, 74 | { 75 | "required": [ 76 | "dq" 77 | ] 78 | }, 79 | { 80 | "required": [ 81 | "qi" 82 | ] 83 | }, 84 | { 85 | "required": [ 86 | "oth" 87 | ] 88 | } 89 | ] 90 | } 91 | ] 92 | } 93 | } -------------------------------------------------------------------------------- /json-schemas/permissions/permission-grant-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/permission-grant-data.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "dateExpires", 8 | "scope" 9 | ], 10 | "properties": { 11 | "description": { 12 | "type": "string" 13 | }, 14 | "dateExpires": { 15 | "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/$defs/date-time" 16 | }, 17 | "requestId": { 18 | "type": "string" 19 | }, 20 | "delegated": { 21 | "type": "boolean" 22 | }, 23 | "scope": { 24 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/defs.json#/$defs/scope" 25 | }, 26 | "conditions": { 27 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/defs.json#/$defs/conditions" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /json-schemas/permissions/permission-request-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/permission-request-data.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "delegated", 8 | "scope" 9 | ], 10 | "properties": { 11 | "description": { 12 | "type": "string" 13 | }, 14 | "delegated": { 15 | "type": "boolean" 16 | }, 17 | "scope": { 18 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/defs.json#/$defs/scope" 19 | }, 20 | "conditions": { 21 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/defs.json#/$defs/conditions" 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /json-schemas/permissions/permission-revocation-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/permission-revoke-data.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "properties": { 7 | "description": { 8 | "type": "string" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /json-schemas/permissions/permissions-definitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/permissions/defs.json", 4 | "type": "object", 5 | "$defs": { 6 | "scope": { 7 | "oneOf": [ 8 | { 9 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/messages-query-scope" 10 | }, 11 | { 12 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/messages-read-scope" 13 | }, 14 | { 15 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/messages-subscribe-scope" 16 | }, 17 | { 18 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/protocols-configure-scope" 19 | }, 20 | { 21 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/protocols-query-scope" 22 | }, 23 | { 24 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/records-read-scope" 25 | }, 26 | { 27 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/records-delete-scope" 28 | }, 29 | { 30 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/records-write-scope" 31 | }, 32 | { 33 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/records-query-scope" 34 | }, 35 | { 36 | "$ref": "https://identity.foundation/dwn/json-schemas/permissions/scopes.json#/$defs/records-subscribe-scope" 37 | } 38 | ] 39 | }, 40 | "conditions": { 41 | "type": "object", 42 | "additionalProperties": false, 43 | "properties": { 44 | "publication": { 45 | "enum": [ 46 | "Required", 47 | "Prohibited" 48 | ], 49 | "type": "string" 50 | } 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /json-schemas/signature-payloads/generic-signature-payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/signature-payloads/generic-signature-payload.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "descriptorCid" 8 | ], 9 | "properties": { 10 | "descriptorCid": { 11 | "type": "string" 12 | }, 13 | "delegatedGrantId": { 14 | "type": "string" 15 | }, 16 | "permissionGrantId": { 17 | "type": "string" 18 | }, 19 | "protocolRole": { 20 | "$comment": "Used in the Records interface to authorize role-authorized actions for protocol records", 21 | "type": "string" 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /json-schemas/signature-payloads/records-write-signature-payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://identity.foundation/dwn/json-schemas/signature-payloads/records-write-signature-payload.json", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "descriptorCid", 8 | "recordId" 9 | ], 10 | "properties": { 11 | "descriptorCid": { 12 | "type": "string" 13 | }, 14 | "recordId": { 15 | "type": "string" 16 | }, 17 | "contextId": { 18 | "type": "string" 19 | }, 20 | "attestationCid": { 21 | "type": "string" 22 | }, 23 | "encryptionCid": { 24 | "type": "string" 25 | }, 26 | "delegatedGrantId": { 27 | "type": "string" 28 | }, 29 | "permissionGrantId": { 30 | "type": "string" 31 | }, 32 | "protocolRole": { 33 | "type": "string" 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /karma.conf.cjs: -------------------------------------------------------------------------------- 1 | // Karma is what we're using to run our tests in browser environments 2 | // Karma does not support .mjs 3 | 4 | // playwright acts as a safari executable on windows and mac 5 | const playwright = require('playwright'); 6 | const esbuildBrowserConfig = require('./build/esbuild-browser-config.cjs'); 7 | 8 | // use playwright chrome exec path as run target for chromium tests 9 | process.env.CHROME_BIN = playwright.chromium.executablePath(); 10 | 11 | // use playwright webkit exec path as run target for safari tests 12 | process.env.WEBKIT_HEADLESS_BIN = playwright.webkit.executablePath(); 13 | 14 | // use playwright firefox exec path as run target for firefox tests 15 | process.env.FIREFOX_BIN = playwright.firefox.executablePath(); 16 | 17 | /** @typedef {import('karma').Config} KarmaConfig */ 18 | 19 | /** 20 | * 21 | * @param {KarmaConfig} config 22 | */ 23 | module.exports = function configure(config) { 24 | config.set({ 25 | plugins: [ 26 | require('karma-chrome-launcher'), 27 | require('karma-firefox-launcher'), 28 | require('karma-webkit-launcher'), 29 | require('karma-esbuild'), 30 | require('karma-mocha'), 31 | require('karma-mocha-reporter') 32 | ], 33 | 34 | // frameworks to use 35 | // available frameworks: https://www.npmjs.com/search?q=keywords:karma-adapter 36 | frameworks: ['mocha'], 37 | 38 | client: { 39 | // Increase Mocha's default timeout of 2 seconds to prevent timeouts during GitHub CI runs. 40 | mocha: { 41 | timeout: 10000 // 10 seconds 42 | } 43 | }, 44 | 45 | // list of files / patterns to load in the browser 46 | files: [ 47 | { pattern: 'tests/**/*.spec.ts', watched: false } 48 | ], 49 | // preprocess matching files before serving them to the browser 50 | // available preprocessors: https://www.npmjs.com/search?q=keywords:karma-preprocessor 51 | preprocessors: { 52 | 'tests/**/*.ts': ['esbuild'] 53 | }, 54 | 55 | esbuild: esbuildBrowserConfig, 56 | 57 | // list of files / patterns to exclude 58 | exclude: [], 59 | 60 | // test results reporter to use 61 | // available reporters: https://www.npmjs.com/search?q=keywords:karma-reporter 62 | reporters: ['mocha'], 63 | 64 | // web server port 65 | port: 9876, 66 | 67 | // enable / disable colors in the output (reporters and logs) 68 | colors: true, 69 | 70 | // level of logging 71 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || 72 | // config.LOG_INFO || config.LOG_DEBUG 73 | logLevel: config.LOG_INFO, 74 | 75 | concurrency: 1, 76 | 77 | // start these browsers 78 | // available browser launchers: https://www.npmjs.com/search?q=keywords:karma-launcher 79 | browsers: ['ChromeHeadless', 'FirefoxHeadless', 'WebkitHeadless'], 80 | 81 | // Continuous Integration mode 82 | // if true, Karma captures browsers, runs the tests and exits 83 | singleRun: true, 84 | 85 | // Increase browser timeouts to avoid DISCONNECTED messages during GitHub CI runs. 86 | browserDisconnectTimeout : 10000, // default 2000 87 | browserDisconnectTolerance : 1, // default 0 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /karma.conf.debug.cjs: -------------------------------------------------------------------------------- 1 | // Karma is what we're using to run our tests in browser environments 2 | // Karma does not support .mjs 3 | 4 | const esbuildBrowserConfig = require('./build/esbuild-browser-config.cjs'); 5 | 6 | module.exports = function(config) { 7 | config.set({ 8 | plugins: [ 9 | require('karma-chrome-launcher'), 10 | require('karma-esbuild'), 11 | require('karma-mocha'), 12 | require('karma-mocha-reporter') 13 | ], 14 | 15 | // frameworks to use 16 | // available frameworks: https://www.npmjs.com/search?q=keywords:karma-adapter 17 | frameworks: ['mocha'], 18 | 19 | 20 | // list of files / patterns to load in the browser 21 | files: [ 22 | { pattern: 'tests/**/*.spec.ts', watched: false } 23 | ], 24 | // preprocess matching files before serving them to the browser 25 | // available preprocessors: https://www.npmjs.com/search?q=keywords:karma-preprocessor 26 | preprocessors: { 27 | 'tests/**/*.ts': ['esbuild'] 28 | }, 29 | 30 | esbuild: esbuildBrowserConfig, 31 | 32 | // list of files / patterns to exclude 33 | exclude: [], 34 | 35 | // test results reporter to use 36 | // available reporters: https://www.npmjs.com/search?q=keywords:karma-reporter 37 | reporters: ['mocha'], 38 | 39 | // web server port 40 | port: 9876, 41 | 42 | // enable / disable colors in the output (reporters and logs) 43 | colors: true, 44 | 45 | // level of logging 46 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || 47 | // config.LOG_INFO || config.LOG_DEBUG 48 | logLevel: config.LOG_INFO, 49 | 50 | // start these browsers 51 | // available browser launchers: https://www.npmjs.com/search?q=keywords:karma-launcher 52 | browsers: [ 53 | 'ChromeDebugging' 54 | ], 55 | 56 | customLaunchers: { 57 | ChromeDebugging: { 58 | base : 'Chrome', 59 | flags : [ '--remote-debugging-port=9333' ] 60 | } 61 | } 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /src/core/abstract-message.ts: -------------------------------------------------------------------------------- 1 | import type { MessageInterface } from '../types/message-interface.js'; 2 | import type { GenericMessage, GenericSignaturePayload } from '../types/message-types.js'; 3 | 4 | import { Jws } from '../utils/jws.js'; 5 | import { Message } from './message.js'; 6 | 7 | /** 8 | * An abstract implementation of the `MessageInterface` interface. 9 | */ 10 | export abstract class AbstractMessage implements MessageInterface { 11 | private _message: M; 12 | public get message(): M { 13 | return this._message as M; 14 | } 15 | 16 | private _signer: string | undefined; 17 | public get signer(): string | undefined { 18 | return this._signer; 19 | } 20 | 21 | private _author: string | undefined; 22 | public get author(): string | undefined { 23 | return this._author; 24 | } 25 | 26 | private _signaturePayload: GenericSignaturePayload | undefined; 27 | public get signaturePayload(): GenericSignaturePayload | undefined { 28 | return this._signaturePayload; 29 | } 30 | 31 | /** 32 | * If this message is signed by an author-delegate. 33 | */ 34 | public get isSignedByAuthorDelegate(): boolean { 35 | return Message.isSignedByAuthorDelegate(this._message); 36 | } 37 | 38 | protected constructor(message: M) { 39 | this._message = message; 40 | 41 | if (message.authorization !== undefined) { 42 | this._signer = Message.getSigner(message); 43 | 44 | // if the message authorization contains author delegated grant, the author would be the grantor of the grant 45 | // else the author would be the signer of the message 46 | if (message.authorization.authorDelegatedGrant !== undefined) { 47 | this._author = Message.getSigner(message.authorization.authorDelegatedGrant); 48 | } else { 49 | this._author = this._signer; 50 | } 51 | 52 | this._signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature); 53 | } 54 | } 55 | 56 | /** 57 | * Called by `JSON.stringify(...)` automatically. 58 | */ 59 | toJSON(): GenericMessage { 60 | return this.message; 61 | } 62 | } -------------------------------------------------------------------------------- /src/core/auth.ts: -------------------------------------------------------------------------------- 1 | import type { AuthorizationModel } from '../types/message-types.js'; 2 | import type { DidResolver } from '@web5/dids'; 3 | 4 | import { GeneralJwsVerifier } from '../jose/jws/general/verifier.js'; 5 | import { RecordsWrite } from '../interfaces/records-write.js'; 6 | import { DwnError, DwnErrorCode } from './dwn-error.js'; 7 | 8 | /** 9 | * Verifies all the signature(s) within the authorization property. 10 | * 11 | * @throws {Error} if fails authentication 12 | */ 13 | export async function authenticate(authorizationModel: AuthorizationModel | undefined, didResolver: DidResolver): Promise { 14 | 15 | if (authorizationModel === undefined) { 16 | throw new DwnError(DwnErrorCode.AuthenticateJwsMissing, 'Missing JWS.'); 17 | } 18 | 19 | await GeneralJwsVerifier.verifySignatures(authorizationModel.signature, didResolver); 20 | 21 | if (authorizationModel.ownerSignature !== undefined) { 22 | await GeneralJwsVerifier.verifySignatures(authorizationModel.ownerSignature, didResolver); 23 | } 24 | 25 | if (authorizationModel.authorDelegatedGrant !== undefined) { 26 | // verify the signature of the grantor of the author-delegated grant 27 | const authorDelegatedGrant = await RecordsWrite.parse(authorizationModel.authorDelegatedGrant); 28 | await GeneralJwsVerifier.verifySignatures(authorDelegatedGrant.message.authorization.signature, didResolver); 29 | } 30 | 31 | if (authorizationModel.ownerDelegatedGrant !== undefined) { 32 | // verify the signature of the grantor of the owner-delegated grant 33 | const ownerDelegatedGrant = await RecordsWrite.parse(authorizationModel.ownerDelegatedGrant); 34 | await GeneralJwsVerifier.verifySignatures(ownerDelegatedGrant.message.authorization.signature, didResolver); 35 | } 36 | } -------------------------------------------------------------------------------- /src/core/dwn-constant.ts: -------------------------------------------------------------------------------- 1 | export class DwnConstant { 2 | /** 3 | * The maximum size of raw data that will be returned as `encodedData`. 4 | * 5 | * We chose 30k, as after encoding it would give plenty of headroom up to the 65k limit in most SQL variants. 6 | * We currently encode using base64url which is a 33% increase in size. 7 | */ 8 | public static readonly maxDataSizeAllowedToBeEncoded = 30_000; 9 | } -------------------------------------------------------------------------------- /src/core/message-reply.ts: -------------------------------------------------------------------------------- 1 | import type { MessagesReadReplyEntry } from '../types/messages-types.js'; 2 | import type { PaginationCursor } from '../types/query-types.js'; 3 | import type { ProtocolsConfigureMessage } from '../types/protocols-types.js'; 4 | import type { RecordsReadReplyEntry } from '../types/records-types.js'; 5 | import type { GenericMessageReply, MessageSubscription, QueryResultEntry } from '../types/message-types.js'; 6 | 7 | export function messageReplyFromError(e: unknown, code: number): GenericMessageReply { 8 | 9 | const detail = e instanceof Error ? e.message : 'Error'; 10 | 11 | return { status: { code, detail } }; 12 | } 13 | 14 | /** 15 | * Catch-all message reply type. It is recommended to use GenericMessageReply or a message-specific reply type wherever possible. 16 | */ 17 | export type UnionMessageReply = GenericMessageReply & { 18 | /** 19 | * A container for the data returned from a `RecordsRead` or `MessagesRead`. 20 | * Mutually exclusive with (`entries` + `cursor`) and `subscription`. 21 | */ 22 | entry?: MessagesReadReplyEntry & RecordsReadReplyEntry; 23 | 24 | /** 25 | * Resulting message entries or events returned from the invocation of the corresponding message. 26 | * e.g. the resulting messages from a RecordsQuery, or array of messageCid strings for MessagesQuery 27 | * Mutually exclusive with `record`. 28 | */ 29 | entries?: QueryResultEntry[] | ProtocolsConfigureMessage[] | string[]; 30 | 31 | /** 32 | * A cursor for pagination if applicable (e.g. RecordsQuery). 33 | * Mutually exclusive with `record`. 34 | */ 35 | cursor?: PaginationCursor; 36 | 37 | /** 38 | * A subscription object if a subscription was requested. 39 | */ 40 | subscription?: MessageSubscription; 41 | }; -------------------------------------------------------------------------------- /src/core/tenant-gate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The result of the isActiveTenant() call. 3 | */ 4 | export type ActiveTenantCheckResult = { 5 | /** 6 | * `true` if the given DID is an active tenant of the DWN; `false` otherwise. 7 | */ 8 | isActiveTenant: boolean; 9 | 10 | /** 11 | * An optional detail message if the given DID is not an active tenant of the DWN. 12 | */ 13 | detail?: string; 14 | }; 15 | 16 | /** 17 | * An interface that gates tenant access to the DWN. 18 | */ 19 | export interface TenantGate { 20 | /** 21 | * @returns `true` if the given DID is an active tenant of the DWN; `false` otherwise 22 | */ 23 | isActiveTenant(did: string): Promise; 24 | } 25 | 26 | /** 27 | * A tenant gate that treats every DID as an active tenant. 28 | */ 29 | export class AllowAllTenantGate implements TenantGate { 30 | public async isActiveTenant(_did: string): Promise { 31 | return { isActiveTenant: true }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/enums/dwn-interface-method.ts: -------------------------------------------------------------------------------- 1 | export enum DwnInterfaceName { 2 | Messages = 'Messages', 3 | Protocols = 'Protocols', 4 | Records = 'Records' 5 | } 6 | 7 | export enum DwnMethodName { 8 | Configure = 'Configure', 9 | Query = 'Query', 10 | Read = 'Read', 11 | Write = 'Write', 12 | Delete = 'Delete', 13 | Subscribe = 'Subscribe' 14 | } 15 | -------------------------------------------------------------------------------- /src/event-log/event-emitter-stream.ts: -------------------------------------------------------------------------------- 1 | import type { KeyValues } from '../types/query-types.js'; 2 | import type { EventListener, EventStream, EventSubscription, MessageEvent } from '../types/subscriptions.js'; 3 | 4 | import { EventEmitter } from 'events'; 5 | import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; 6 | 7 | const EVENTS_LISTENER_CHANNEL = 'events'; 8 | 9 | export interface EventEmitterStreamConfig { 10 | /** 11 | * An optional error handler in order to be able to react to any errors or warnings triggers by `EventEmitter`. 12 | * By default we log errors with `console.error`. 13 | */ 14 | errorHandler?: (error: any) => void; 15 | }; 16 | 17 | export class EventEmitterStream implements EventStream { 18 | private eventEmitter: EventEmitter; 19 | private isOpen: boolean = false; 20 | 21 | constructor(config: EventEmitterStreamConfig = {}) { 22 | // we capture the rejections and currently just log the errors that are produced 23 | this.eventEmitter = new EventEmitter({ captureRejections: true }); 24 | 25 | // number of listeners per particular eventName before a warning is emitted 26 | // we set to 0 which represents infinity. 27 | // https://nodejs.org/api/events.html#emittersetmaxlistenersn 28 | this.eventEmitter.setMaxListeners(0); 29 | 30 | if (config.errorHandler) { 31 | this.errorHandler = config.errorHandler; 32 | } 33 | 34 | this.eventEmitter.on('error', this.errorHandler); 35 | } 36 | 37 | /** 38 | * we subscribe to the `EventEmitter` error handler with a provided handler or set one which logs the errors. 39 | */ 40 | private errorHandler: (error:any) => void = (error) => { console.error('event emitter error', error); }; 41 | 42 | async subscribe(tenant: string, id: string, listener: EventListener): Promise { 43 | this.eventEmitter.on(`${tenant}_${EVENTS_LISTENER_CHANNEL}`, listener); 44 | return { 45 | id, 46 | close: async (): Promise => { this.eventEmitter.off(`${tenant}_${EVENTS_LISTENER_CHANNEL}`, listener); } 47 | }; 48 | } 49 | 50 | async open(): Promise { 51 | this.isOpen = true; 52 | } 53 | 54 | async close(): Promise { 55 | this.isOpen = false; 56 | this.eventEmitter.removeAllListeners(); 57 | } 58 | 59 | emit(tenant: string, event: MessageEvent, indexes: KeyValues): void { 60 | if (!this.isOpen) { 61 | this.errorHandler(new DwnError( 62 | DwnErrorCode.EventEmitterStreamNotOpenError, 63 | 'a message emitted when EventEmitterStream is closed' 64 | )); 65 | return; 66 | } 67 | this.eventEmitter.emit(`${tenant}_${EVENTS_LISTENER_CHANNEL}`, tenant, event, indexes); 68 | } 69 | } -------------------------------------------------------------------------------- /src/event-log/event-log-level.ts: -------------------------------------------------------------------------------- 1 | import type { EventLog } from '../types/event-log.js'; 2 | import type { EventStream } from '../types/subscriptions.js'; 3 | import type { ULIDFactory } from 'ulidx'; 4 | import type { Filter, KeyValues, PaginationCursor } from '../types/query-types.js'; 5 | 6 | import { createLevelDatabase } from '../store/level-wrapper.js'; 7 | import { IndexLevel } from '../store/index-level.js'; 8 | import { monotonicFactory } from 'ulidx'; 9 | 10 | export type EventLogLevelConfig = { 11 | /** 12 | * must be a directory path (relative or absolute) where 13 | * LevelDB will store its files, or in browsers, the name of the 14 | * {@link https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase IDBDatabase} to be opened. 15 | */ 16 | location?: string, 17 | createLevelDatabase?: typeof createLevelDatabase, 18 | eventStream?: EventStream, 19 | }; 20 | 21 | export class EventLogLevel implements EventLog { 22 | ulidFactory: ULIDFactory; 23 | index: IndexLevel; 24 | 25 | constructor(config?: EventLogLevelConfig) { 26 | this.index = new IndexLevel({ 27 | location: 'EVENTLOG', 28 | createLevelDatabase, 29 | ...config, 30 | }); 31 | 32 | this.ulidFactory = monotonicFactory(); 33 | } 34 | 35 | async open(): Promise { 36 | return this.index.open(); 37 | } 38 | 39 | async close(): Promise { 40 | return this.index.close(); 41 | } 42 | 43 | async clear(): Promise { 44 | return this.index.clear(); 45 | } 46 | 47 | async append(tenant: string, messageCid: string, indexes: KeyValues): Promise { 48 | const watermark = this.ulidFactory(); 49 | await this.index.put(tenant, messageCid, { ...indexes, watermark }); 50 | } 51 | 52 | async queryEvents(tenant: string, filters: Filter[], cursor?: PaginationCursor): Promise<{ events: string[], cursor?: PaginationCursor }> { 53 | const results = await this.index.query(tenant, filters, { sortProperty: 'watermark', cursor }); 54 | return { 55 | events : results.map(({ messageCid }) => messageCid), 56 | cursor : IndexLevel.createCursorFromLastArrayItem(results, 'watermark'), 57 | }; 58 | } 59 | 60 | async getEvents(tenant: string, cursor?: PaginationCursor): Promise<{ events: string[], cursor?: PaginationCursor }> { 61 | return this.queryEvents(tenant, [], cursor); 62 | } 63 | 64 | async deleteEventsByCid(tenant: string, messageCids: Array): Promise { 65 | const indexDeletePromises: Promise[] = []; 66 | for (const messageCid of messageCids) { 67 | indexDeletePromises.push(this.index.delete(tenant, messageCid)); 68 | } 69 | 70 | await Promise.all(indexDeletePromises); 71 | } 72 | } -------------------------------------------------------------------------------- /src/handlers/messages-query.ts: -------------------------------------------------------------------------------- 1 | import type { DidResolver } from '@web5/dids'; 2 | import type { EventLog } from '../types/event-log.js'; 3 | import type { MessageStore } from '../types/message-store.js'; 4 | import type { MethodHandler } from '../types/method-handler.js'; 5 | import type { MessagesQueryMessage, MessagesQueryReply } from '../types/messages-types.js'; 6 | 7 | import { authenticate } from '../core/auth.js'; 8 | import { messageReplyFromError } from '../core/message-reply.js'; 9 | import { Messages } from '../utils/messages.js'; 10 | import { MessagesGrantAuthorization } from '../core/messages-grant-authorization.js'; 11 | import { MessagesQuery } from '../interfaces/messages-query.js'; 12 | import { PermissionsProtocol } from '../protocols/permissions.js'; 13 | import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; 14 | 15 | 16 | export class MessagesQueryHandler implements MethodHandler { 17 | 18 | constructor(private didResolver: DidResolver, private messageStore: MessageStore, private eventLog: EventLog) { } 19 | 20 | public async handle({ 21 | tenant, 22 | message 23 | }: {tenant: string, message: MessagesQueryMessage}): Promise { 24 | let messagesQuery: MessagesQuery; 25 | 26 | try { 27 | messagesQuery = await MessagesQuery.parse(message); 28 | } catch (e) { 29 | return messageReplyFromError(e, 400); 30 | } 31 | 32 | try { 33 | await authenticate(message.authorization, this.didResolver); 34 | await MessagesQueryHandler.authorizeMessagesQuery(tenant, messagesQuery, this.messageStore); 35 | } catch (e) { 36 | return messageReplyFromError(e, 401); 37 | } 38 | 39 | // an empty array of filters means no filtering and all events are returned 40 | const eventFilters = Messages.convertFilters(message.descriptor.filters); 41 | const { events, cursor } = await this.eventLog.queryEvents(tenant, eventFilters, message.descriptor.cursor); 42 | 43 | return { 44 | status : { code: 200, detail: 'OK' }, 45 | entries : events, 46 | cursor 47 | }; 48 | } 49 | 50 | private static async authorizeMessagesQuery(tenant: string, messagesQuery: MessagesQuery, messageStore: MessageStore): Promise { 51 | // if `MessagesQuery` author is the same as the target tenant, we can directly grant access 52 | if (messagesQuery.author === tenant) { 53 | return; 54 | } else if (messagesQuery.author !== undefined && messagesQuery.signaturePayload!.permissionGrantId !== undefined) { 55 | const permissionGrant = await PermissionsProtocol.fetchGrant(tenant, messageStore, messagesQuery.signaturePayload!.permissionGrantId); 56 | await MessagesGrantAuthorization.authorizeQueryOrSubscribe({ 57 | incomingMessage : messagesQuery.message, 58 | expectedGrantor : tenant, 59 | expectedGrantee : messagesQuery.author, 60 | permissionGrant, 61 | messageStore 62 | }); 63 | } else { 64 | throw new DwnError(DwnErrorCode.MessagesQueryAuthorizationFailed, 'message failed authorization'); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/interfaces/messages-query.ts: -------------------------------------------------------------------------------- 1 | import type { PaginationCursor } from '../types/query-types.js'; 2 | import type { Signer } from '../types/signer.js'; 3 | import type { MessagesFilter, MessagesQueryDescriptor, MessagesQueryMessage } from '../types/messages-types.js'; 4 | 5 | import { AbstractMessage } from '../core/abstract-message.js'; 6 | import { Message } from '../core/message.js'; 7 | import { Messages } from '../utils/messages.js'; 8 | import { removeUndefinedProperties } from '../utils/object.js'; 9 | import { Time } from '../utils/time.js'; 10 | import { validateProtocolUrlNormalized } from '../utils/url.js'; 11 | import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; 12 | 13 | export type MessagesQueryOptions = { 14 | signer: Signer; 15 | filters?: MessagesFilter[]; 16 | cursor?: PaginationCursor; 17 | messageTimestamp?: string; 18 | permissionGrantId?: string; 19 | }; 20 | 21 | export class MessagesQuery extends AbstractMessage{ 22 | 23 | public static async parse(message: MessagesQueryMessage): Promise { 24 | Message.validateJsonSchema(message); 25 | await Message.validateSignatureStructure(message.authorization.signature, message.descriptor); 26 | 27 | for (const filter of message.descriptor.filters) { 28 | if ('protocol' in filter && filter.protocol !== undefined) { 29 | validateProtocolUrlNormalized(filter.protocol); 30 | } 31 | } 32 | 33 | return new MessagesQuery(message); 34 | } 35 | 36 | public static async create(options: MessagesQueryOptions): Promise { 37 | const descriptor: MessagesQueryDescriptor = { 38 | interface : DwnInterfaceName.Messages, 39 | method : DwnMethodName.Query, 40 | filters : options.filters ? Messages.normalizeFilters(options.filters) : [], 41 | messageTimestamp : options.messageTimestamp ?? Time.getCurrentTimestamp(), 42 | cursor : options.cursor, 43 | }; 44 | 45 | removeUndefinedProperties(descriptor); 46 | 47 | const { permissionGrantId, signer } = options; 48 | const authorization = await Message.createAuthorization({ 49 | descriptor, 50 | signer, 51 | permissionGrantId 52 | }); 53 | 54 | const message = { descriptor, authorization }; 55 | 56 | Message.validateJsonSchema(message); 57 | 58 | return new MessagesQuery(message); 59 | } 60 | } -------------------------------------------------------------------------------- /src/interfaces/messages-read.ts: -------------------------------------------------------------------------------- 1 | import type { Signer } from '../types/signer.js'; 2 | import type { MessagesReadDescriptor, MessagesReadMessage } from '../types/messages-types.js'; 3 | 4 | import { AbstractMessage } from '../core/abstract-message.js'; 5 | import { Cid } from '../utils/cid.js'; 6 | import { Message } from '../core/message.js'; 7 | import { Time } from '../utils/time.js'; 8 | import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; 9 | import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; 10 | 11 | export type MessagesReadOptions = { 12 | messageCid: string; 13 | signer: Signer; 14 | messageTimestamp?: string; 15 | permissionGrantId?: string; 16 | }; 17 | 18 | export class MessagesRead extends AbstractMessage { 19 | public static async parse(message: MessagesReadMessage): Promise { 20 | Message.validateJsonSchema(message); 21 | this.validateMessageCid(message.descriptor.messageCid); 22 | 23 | await Message.validateSignatureStructure(message.authorization.signature, message.descriptor); 24 | Time.validateTimestamp(message.descriptor.messageTimestamp); 25 | 26 | return new MessagesRead(message); 27 | } 28 | 29 | public static async create(options: MessagesReadOptions): Promise { 30 | const descriptor: MessagesReadDescriptor = { 31 | interface : DwnInterfaceName.Messages, 32 | method : DwnMethodName.Read, 33 | messageCid : options.messageCid, 34 | messageTimestamp : options.messageTimestamp ?? Time.getCurrentTimestamp(), 35 | }; 36 | 37 | const { signer, permissionGrantId } = options; 38 | const authorization = await Message.createAuthorization({ 39 | descriptor, 40 | signer, 41 | permissionGrantId, 42 | }); 43 | const message = { descriptor, authorization }; 44 | 45 | Message.validateJsonSchema(message); 46 | MessagesRead.validateMessageCid(options.messageCid); 47 | 48 | return new MessagesRead(message); 49 | } 50 | 51 | /** 52 | * validates the provided cid 53 | * @param messageCid - the cid in question 54 | * @throws {DwnError} if an invalid cid is found. 55 | */ 56 | private static validateMessageCid(messageCid: string): void { 57 | try { 58 | Cid.parseCid(messageCid); 59 | } catch (_) { 60 | throw new DwnError(DwnErrorCode.MessagesReadInvalidCid, `${messageCid} is not a valid CID`); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/interfaces/messages-subscribe.ts: -------------------------------------------------------------------------------- 1 | import type { MessagesFilter } from '../types/messages-types.js'; 2 | import type { Signer } from '../types/signer.js'; 3 | import type { MessagesSubscribeDescriptor, MessagesSubscribeMessage } from '../types/messages-types.js'; 4 | 5 | import { AbstractMessage } from '../core/abstract-message.js'; 6 | import { Message } from '../core/message.js'; 7 | import { removeUndefinedProperties } from '../utils/object.js'; 8 | import { Time } from '../utils/time.js'; 9 | import { validateProtocolUrlNormalized } from '../utils/url.js'; 10 | import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; 11 | 12 | 13 | export type MessagesSubscribeOptions = { 14 | signer: Signer; 15 | messageTimestamp?: string; 16 | filters?: MessagesFilter[] 17 | permissionGrantId?: string; 18 | }; 19 | 20 | export class MessagesSubscribe extends AbstractMessage { 21 | public static async parse(message: MessagesSubscribeMessage): Promise { 22 | Message.validateJsonSchema(message); 23 | await Message.validateSignatureStructure(message.authorization.signature, message.descriptor); 24 | 25 | for (const filter of message.descriptor.filters) { 26 | if ('protocol' in filter && filter.protocol !== undefined) { 27 | validateProtocolUrlNormalized(filter.protocol); 28 | } 29 | } 30 | 31 | Time.validateTimestamp(message.descriptor.messageTimestamp); 32 | return new MessagesSubscribe(message); 33 | } 34 | 35 | /** 36 | * Creates a MessagesSubscribe message. 37 | * 38 | * @throws {DwnError} if json schema validation fails. 39 | */ 40 | public static async create( 41 | options: MessagesSubscribeOptions 42 | ): Promise { 43 | const currentTime = Time.getCurrentTimestamp(); 44 | 45 | const descriptor: MessagesSubscribeDescriptor = { 46 | interface : DwnInterfaceName.Messages, 47 | method : DwnMethodName.Subscribe, 48 | filters : options.filters ?? [], 49 | messageTimestamp : options.messageTimestamp ?? currentTime, 50 | }; 51 | 52 | removeUndefinedProperties(descriptor); 53 | const { permissionGrantId, signer } = options; 54 | const authorization = await Message.createAuthorization({ 55 | descriptor, 56 | signer, 57 | permissionGrantId 58 | }); 59 | 60 | const message: MessagesSubscribeMessage = { descriptor, authorization }; 61 | Message.validateJsonSchema(message); 62 | return new MessagesSubscribe(message); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/jose/algorithms/signing/ed25519.ts: -------------------------------------------------------------------------------- 1 | import * as Ed25519 from '@noble/ed25519'; 2 | import type { PrivateJwk, PublicJwk, SignatureAlgorithm } from '../../../types/jose-types.js'; 3 | 4 | import { Encoder } from '../../../utils/encoder.js'; 5 | import { DwnError, DwnErrorCode } from '../../../core/dwn-error.js'; 6 | 7 | function validateKey(jwk: PrivateJwk | PublicJwk): void { 8 | if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519') { 9 | throw new DwnError(DwnErrorCode.Ed25519InvalidJwk, 'invalid jwk. kty MUST be OKP. crv MUST be Ed25519'); 10 | } 11 | } 12 | 13 | function publicKeyToJwk(publicKeyBytes: Uint8Array): PublicJwk { 14 | const x = Encoder.bytesToBase64Url(publicKeyBytes); 15 | 16 | const publicJwk: PublicJwk = { 17 | alg : 'EdDSA', 18 | kty : 'OKP', 19 | crv : 'Ed25519', 20 | x 21 | }; 22 | 23 | return publicJwk; 24 | } 25 | 26 | export const ed25519: SignatureAlgorithm = { 27 | sign: async (content: Uint8Array, privateJwk: PrivateJwk): Promise => { 28 | validateKey(privateJwk); 29 | 30 | const privateKeyBytes = Encoder.base64UrlToBytes(privateJwk.d); 31 | 32 | return Ed25519.signAsync(content, privateKeyBytes); 33 | }, 34 | 35 | verify: async (content: Uint8Array, signature: Uint8Array, publicJwk: PublicJwk): Promise => { 36 | validateKey(publicJwk); 37 | 38 | const publicKeyBytes = Encoder.base64UrlToBytes(publicJwk.x); 39 | 40 | return Ed25519.verifyAsync(signature, content, publicKeyBytes); 41 | }, 42 | 43 | generateKeyPair: async (): Promise<{publicJwk: PublicJwk, privateJwk: PrivateJwk}> => { 44 | const privateKeyBytes = Ed25519.utils.randomPrivateKey(); 45 | const publicKeyBytes = await Ed25519.getPublicKeyAsync(privateKeyBytes); 46 | 47 | const d = Encoder.bytesToBase64Url(privateKeyBytes); 48 | 49 | const publicJwk = publicKeyToJwk(publicKeyBytes); 50 | const privateJwk: PrivateJwk = { ...publicJwk, d }; 51 | 52 | return { publicJwk, privateJwk }; 53 | }, 54 | 55 | publicKeyToJwk: async (publicKeyBytes: Uint8Array): Promise => { 56 | return publicKeyToJwk(publicKeyBytes); 57 | } 58 | }; -------------------------------------------------------------------------------- /src/jose/algorithms/signing/signature-algorithms.ts: -------------------------------------------------------------------------------- 1 | import type { SignatureAlgorithm } from '../../../types/jose-types.js'; 2 | 3 | import { ed25519 } from './ed25519.js'; 4 | import { Secp256k1 } from '../../../utils/secp256k1.js'; 5 | import { Secp256r1 } from '../../../utils/secp256r1.js'; 6 | 7 | // the key should be the appropriate `crv` value 8 | export const signatureAlgorithms: Record = { 9 | 'Ed25519' : ed25519, 10 | 'secp256k1' : { 11 | sign : Secp256k1.sign, 12 | verify : Secp256k1.verify, 13 | generateKeyPair : Secp256k1.generateKeyPair, 14 | publicKeyToJwk : Secp256k1.publicKeyToJwk 15 | }, 16 | 'P-256': { 17 | sign : Secp256r1.sign, 18 | verify : Secp256r1.verify, 19 | generateKeyPair : Secp256r1.generateKeyPair, 20 | publicKeyToJwk : Secp256r1.publicKeyToJwk, 21 | }, 22 | }; -------------------------------------------------------------------------------- /src/jose/jws/general/builder.ts: -------------------------------------------------------------------------------- 1 | import type { GeneralJws } from '../../../types/jws-types.js'; 2 | import type { Signer } from '../../../types/signer.js'; 3 | 4 | import { Encoder } from '../../../utils/encoder.js'; 5 | 6 | export class GeneralJwsBuilder { 7 | private jws: GeneralJws; 8 | 9 | private constructor(jws: GeneralJws) { 10 | this.jws = jws; 11 | } 12 | 13 | static async create(payload: Uint8Array, signers: Signer[] = []): Promise { 14 | const jws: GeneralJws = { 15 | payload : Encoder.bytesToBase64Url(payload), 16 | signatures : [] 17 | }; 18 | 19 | const builder = new GeneralJwsBuilder(jws); 20 | 21 | for (const signer of signers) { 22 | await builder.addSignature(signer); 23 | } 24 | 25 | return builder; 26 | } 27 | 28 | async addSignature(signer: Signer): Promise { 29 | const protectedHeader = { 30 | kid : signer.keyId, 31 | alg : signer.algorithm 32 | }; 33 | const protectedHeaderString = JSON.stringify(protectedHeader); 34 | const protectedHeaderBase64UrlString = Encoder.stringToBase64Url(protectedHeaderString); 35 | 36 | const signingInputString = `${protectedHeaderBase64UrlString}.${this.jws.payload}`; 37 | const signingInputBytes = Encoder.stringToBytes(signingInputString); 38 | 39 | const signatureBytes = await signer.sign(signingInputBytes); 40 | const signature = Encoder.bytesToBase64Url(signatureBytes); 41 | 42 | this.jws.signatures.push({ protected: protectedHeaderBase64UrlString, signature }); 43 | } 44 | 45 | getJws(): GeneralJws { 46 | return this.jws; 47 | } 48 | } -------------------------------------------------------------------------------- /src/protocols/permission-grant.ts: -------------------------------------------------------------------------------- 1 | import type { DataEncodedRecordsWriteMessage } from '../types/records-types.js'; 2 | import type { PermissionConditions, PermissionGrantData, PermissionScope } from '../types/permission-types.js'; 3 | 4 | import { Encoder } from '../utils/encoder.js'; 5 | import { Message } from '../core/message.js'; 6 | 7 | 8 | /** 9 | * A class representing a Permission Grant for a more convenient abstraction. 10 | */ 11 | export class PermissionGrant { 12 | 13 | /** 14 | * The ID of the permission grant, which is the record ID DWN message. 15 | */ 16 | public readonly id: string; 17 | 18 | /** 19 | * The grantor of the permission. 20 | */ 21 | public readonly grantor: string; 22 | 23 | /** 24 | * The grantee of the permission. 25 | */ 26 | public readonly grantee: string; 27 | 28 | /** 29 | * The date at which the grant was given. 30 | */ 31 | public readonly dateGranted: string; 32 | 33 | /** 34 | * Optional string that communicates what the grant would be used for 35 | */ 36 | public readonly description?: string; 37 | 38 | /** 39 | * Optional CID of a permission request. This is optional because grants may be given without being officially requested 40 | */ 41 | public readonly requestId?: string; 42 | 43 | /** 44 | * Timestamp at which this grant will no longer be active. 45 | */ 46 | public readonly dateExpires: string; 47 | 48 | /** 49 | * Whether this grant is delegated or not. If `true`, the `grantedTo` will be able to act as the `grantedTo` within the scope of this grant. 50 | */ 51 | public readonly delegated?: boolean; 52 | 53 | /** 54 | * The scope of the allowed access. 55 | */ 56 | public readonly scope: PermissionScope; 57 | 58 | /** 59 | * Optional conditions that must be met when the grant is used. 60 | */ 61 | public readonly conditions?: PermissionConditions; 62 | 63 | public static async parse(message: DataEncodedRecordsWriteMessage): Promise { 64 | const permissionGrant = new PermissionGrant(message); 65 | return permissionGrant; 66 | } 67 | 68 | private constructor(message: DataEncodedRecordsWriteMessage) { 69 | // properties derived from the generic DWN message properties 70 | this.id = message.recordId; 71 | this.grantor = Message.getSigner(message)!; 72 | this.grantee = message.descriptor.recipient!; 73 | this.dateGranted = message.descriptor.dateCreated; 74 | 75 | // properties from the data payload itself. 76 | const permissionGrantEncoded = message.encodedData; 77 | const permissionGrant = Encoder.base64UrlToObject(permissionGrantEncoded) as PermissionGrantData; 78 | this.dateExpires = permissionGrant.dateExpires; 79 | this.delegated = permissionGrant.delegated; 80 | this.description = permissionGrant.description; 81 | this.requestId = permissionGrant.requestId; 82 | this.scope = permissionGrant.scope; 83 | this.conditions = permissionGrant.conditions; 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /src/protocols/permission-request.ts: -------------------------------------------------------------------------------- 1 | import type { DataEncodedRecordsWriteMessage } from '../types/records-types.js'; 2 | import type { PermissionConditions, PermissionRequestData, PermissionScope } from '../types/permission-types.js'; 3 | 4 | import { Encoder } from '../utils/encoder.js'; 5 | import { Message } from '../core/message.js'; 6 | 7 | 8 | /** 9 | * A class representing a Permission Request for a more convenient abstraction. 10 | */ 11 | export class PermissionRequest { 12 | 13 | /** 14 | * The ID of the permission request, which is the record ID DWN message. 15 | */ 16 | public readonly id: string; 17 | 18 | /** 19 | * The requester for of the permission. 20 | */ 21 | public readonly requester: string; 22 | 23 | /** 24 | * Optional string that communicates what the requested grant would be used for. 25 | */ 26 | public readonly description?: string; 27 | 28 | /** 29 | * Whether the requested grant is delegated or not. 30 | * If `true`, the `requestor` will be able to act as the grantor of the permission within the scope of the requested grant. 31 | */ 32 | public readonly delegated?: boolean; 33 | 34 | /** 35 | * The scope of the allowed access. 36 | */ 37 | public readonly scope: PermissionScope; 38 | 39 | /** 40 | * Optional conditions that must be met when the requested grant is used. 41 | */ 42 | public readonly conditions?: PermissionConditions; 43 | 44 | public static async parse(message: DataEncodedRecordsWriteMessage): Promise { 45 | const permissionRequest = new PermissionRequest(message); 46 | return permissionRequest; 47 | } 48 | 49 | private constructor(message: DataEncodedRecordsWriteMessage) { 50 | // properties derived from the generic DWN message properties 51 | this.id = message.recordId; 52 | this.requester = Message.getSigner(message)!; 53 | 54 | // properties from the data payload itself. 55 | const permissionRequestEncodedData = message.encodedData; 56 | const permissionRequestData = Encoder.base64UrlToObject(permissionRequestEncodedData) as PermissionRequestData; 57 | this.delegated = permissionRequestData.delegated; 58 | this.description = permissionRequestData.description; 59 | this.scope = permissionRequestData.scope; 60 | this.conditions = permissionRequestData.conditions; 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/schema-validator.ts: -------------------------------------------------------------------------------- 1 | import * as precompiledValidators from '../generated/precompiled-validators.js'; 2 | import { DwnError, DwnErrorCode } from './core/dwn-error.js'; 3 | 4 | /** 5 | * Validates the given payload using JSON schema keyed by the given schema name. Throws if the given payload fails validation. 6 | * @param schemaName the schema name use to look up the JSON schema to be used for schema validation. 7 | * The list of schema names can be found in compile-validators.js 8 | * @param payload javascript object to be validated 9 | */ 10 | export function validateJsonSchema(schemaName: string, payload: any): void { 11 | // const validateFn = validator.getSchema(schemaName); 12 | const validateFn = (precompiledValidators as any)[schemaName]; 13 | 14 | if (!validateFn) { 15 | throw new DwnError(DwnErrorCode.SchemaValidatorSchemaNotFound, `schema for ${schemaName} not found.`); 16 | } 17 | 18 | validateFn(payload); 19 | 20 | if (!validateFn.errors) { 21 | return; 22 | } 23 | 24 | // AJV is configured by default to stop validating after the 1st error is encountered which means 25 | // there will only ever be one error; 26 | const [ errorObj ] = validateFn.errors; 27 | let { instancePath, message, keyword } = errorObj; 28 | 29 | if (!instancePath) { 30 | instancePath = schemaName; 31 | } 32 | 33 | // handle a few frequently occurred errors to give more meaningful error for debugging 34 | 35 | if (keyword === 'additionalProperties') { 36 | const keyword = errorObj.params.additionalProperty; 37 | throw new DwnError(DwnErrorCode.SchemaValidatorAdditionalPropertyNotAllowed, `${message}: ${instancePath}: ${keyword}`); 38 | } 39 | 40 | if (keyword === 'unevaluatedProperties') { 41 | const keyword = errorObj.params.unevaluatedProperty; 42 | throw new DwnError(DwnErrorCode.SchemaValidatorUnevaluatedPropertyNotAllowed, `${message}: ${instancePath}: ${keyword}`); 43 | } 44 | 45 | throw new DwnError(DwnErrorCode.SchemaValidatorFailure, `${instancePath}: ${message}`); 46 | } -------------------------------------------------------------------------------- /src/store/blockstore-mock.ts: -------------------------------------------------------------------------------- 1 | import { CID } from 'multiformats'; 2 | import type { AbortOptions, AwaitIterable } from 'interface-store'; 3 | import type { Blockstore, Pair } from 'interface-blockstore'; 4 | 5 | /** 6 | * Mock implementation for the Blockstore interface. 7 | * 8 | * WARNING!!! Purely to be used with `ipfs-unixfs-importer` to compute CID without needing consume any memory. 9 | * This is particularly useful when dealing with large files and a necessity in a large-scale production service environment. 10 | */ 11 | export class BlockstoreMock implements Blockstore { 12 | 13 | async open(): Promise { 14 | } 15 | 16 | async close(): Promise { 17 | } 18 | 19 | async put(key: CID, _val: Uint8Array, _options?: AbortOptions): Promise { 20 | return key; 21 | } 22 | 23 | async get(_key: CID, _options?: AbortOptions): Promise { 24 | return new Uint8Array(); 25 | } 26 | 27 | async has(_key: CID, _options?: AbortOptions): Promise { 28 | return false; 29 | } 30 | 31 | async delete(_key: CID, _options?: AbortOptions): Promise { 32 | } 33 | 34 | async isEmpty(_options?: AbortOptions): Promise { 35 | return true; 36 | } 37 | 38 | async * putMany(source: AwaitIterable, options?: AbortOptions): AsyncIterable { 39 | for await (const entry of source) { 40 | await this.put(entry.cid, entry.block, options); 41 | 42 | yield entry.cid; 43 | } 44 | } 45 | 46 | async * getMany(source: AwaitIterable, options?: AbortOptions): AsyncIterable { 47 | for await (const key of source) { 48 | yield { 49 | cid : key, 50 | block : await this.get(key, options) 51 | }; 52 | } 53 | } 54 | 55 | async * getAll(options?: AbortOptions): AsyncIterable { 56 | // @ts-expect-error keyEncoding is 'buffer' but types for db.iterator always return the key type as 'string' 57 | const li: AsyncGenerator<[Uint8Array, Uint8Array]> = this.db.iterator({ 58 | keys : true, 59 | keyEncoding : 'buffer' 60 | }, options); 61 | 62 | for await (const [key, value] of li) { 63 | yield { cid: CID.decode(key), block: value }; 64 | } 65 | } 66 | 67 | async * deleteMany(source: AwaitIterable, options?: AbortOptions): AsyncIterable { 68 | for await (const key of source) { 69 | await this.delete(key, options); 70 | 71 | yield key; 72 | } 73 | } 74 | 75 | /** 76 | * deletes all entries 77 | */ 78 | async clear(): Promise { 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/types/cache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A generalized cache interface. 3 | * The motivation behind this interface is so that code that depend on the cache can remain independent to the underlying implementation. 4 | */ 5 | export interface Cache { 6 | /** 7 | * Sets a key-value pair. Does not throw error. 8 | */ 9 | set(key: string, value: any): Promise; 10 | 11 | /** 12 | * Gets the value corresponding to the given key. 13 | * @returns value stored corresponding to the given key; `undefined` if key is not found or expired 14 | */ 15 | get(key: string): Promise; 16 | } -------------------------------------------------------------------------------- /src/types/data-store.ts: -------------------------------------------------------------------------------- 1 | import type { Readable } from 'readable-stream'; 2 | 3 | /** 4 | * The interface that defines how to store and fetch data associated with a message. 5 | */ 6 | export interface DataStore { 7 | /** 8 | * Opens a connection to the underlying store. 9 | */ 10 | open(): Promise; 11 | 12 | /** 13 | * Closes the connection to the underlying store. 14 | */ 15 | close(): Promise; 16 | 17 | /** 18 | * Stores the given data. 19 | * @param recordId The logical ID of the record that references the data. 20 | * @param dataCid The IPFS CID of the data. 21 | */ 22 | put(tenant: string, recordId: string, dataCid: string, dataStream: Readable): Promise; 23 | 24 | /** 25 | * Fetches the specified data. 26 | * @param recordId The logical ID of the record that references the data. 27 | * @param dataCid The IPFS CID of the data. 28 | * @returns the data size and data stream if found, otherwise `undefined`. 29 | */ 30 | get(tenant: string, recordId: string, dataCid: string): Promise; 31 | 32 | /** 33 | * Deletes the specified data. No-op if the data does not exist. 34 | * @param recordId The logical ID of the record that references the data. 35 | * @param dataCid The IPFS CID of the data. 36 | */ 37 | delete(tenant: string, recordId: string, dataCid: string): Promise; 38 | 39 | /** 40 | * Clears the entire store. Mainly used for testing to cleaning up in test environments. 41 | */ 42 | clear(): Promise; 43 | } 44 | 45 | /** 46 | * Result of a data store `put()` method call. 47 | */ 48 | export type DataStorePutResult = { 49 | /** 50 | * The number of bytes of the data stored. 51 | */ 52 | dataSize: number; 53 | }; 54 | 55 | /** 56 | * Result of a data store `get()` method call if the data exists. 57 | */ 58 | export type DataStoreGetResult = { 59 | /** 60 | * The number of bytes of the data stored. 61 | */ 62 | dataSize: number; 63 | dataStream: Readable; 64 | }; 65 | -------------------------------------------------------------------------------- /src/types/event-log.ts: -------------------------------------------------------------------------------- 1 | import type { Filter, KeyValues, PaginationCursor } from './query-types.js'; 2 | 3 | export interface EventLog { 4 | /** 5 | * opens a connection to the underlying store 6 | */ 7 | open(): Promise; 8 | 9 | /** 10 | * closes the connection to the underlying store 11 | */ 12 | close(): Promise; 13 | 14 | /** 15 | * adds an event to a tenant's event log 16 | * @param tenant - the tenant's DID 17 | * @param messageCid - the CID of the message 18 | * @param indexes - (key-value pairs) to be included as part of indexing this event. 19 | */ 20 | append(tenant: string, messageCid: string, indexes: KeyValues): Promise 21 | 22 | /** 23 | * Retrieves all of a tenant's events that occurred after the cursor provided. 24 | * If no cursor is provided, all events for a given tenant will be returned. 25 | * 26 | * The cursor is a messageCid. 27 | * 28 | * Returns an array of messageCids that represent the events. 29 | */ 30 | getEvents(tenant: string, cursor?: PaginationCursor): Promise<{ events: string[], cursor?: PaginationCursor }> 31 | 32 | /** 33 | * retrieves a filtered set of events that occurred after a the cursor provided, accepts multiple filters. 34 | * 35 | * If no cursor is provided, all events for a given tenant and filter combo will be returned. 36 | * The cursor is a messageCid. 37 | * 38 | * Returns an array of messageCids that represent the events. 39 | */ 40 | queryEvents(tenant: string, filters: Filter[], cursor?: PaginationCursor): Promise<{ events: string[], cursor?: PaginationCursor }> 41 | 42 | /** 43 | * deletes any events that have any of the messageCids provided 44 | * @returns {Promise} the number of events deleted 45 | */ 46 | deleteEventsByCid(tenant: string, messageCids: Array): Promise 47 | 48 | /** 49 | * Clears the entire store. Mainly used for cleaning up in test environment. 50 | */ 51 | clear(): Promise; 52 | } -------------------------------------------------------------------------------- /src/types/jose-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains a public-private key pair and the associated key ID. 3 | */ 4 | export type KeyMaterial = { 5 | keyId: string, 6 | keyPair: { publicJwk: PublicJwk, privateJwk: PrivateJwk } 7 | }; 8 | 9 | export type Jwk = { 10 | /** The "alg" (algorithm) parameter identifies the algorithm intended for use with the key. */ 11 | alg?: string; 12 | /** The "alg" (algorithm) parameter identifies the algorithm intended for use with the key. */ 13 | kid?: string; 14 | /** identifies the cryptographic algorithm family used with the key, such "EC". */ 15 | kty: string; 16 | }; 17 | 18 | export type PublicJwk = Jwk & { 19 | /** The "crv" (curve) parameter identifies the cryptographic curve used with the key. 20 | * MUST be present for all EC public keys 21 | */ 22 | crv: 'Ed25519' | 'secp256k1' | 'P-256'; 23 | /** 24 | * the x coordinate for the Elliptic Curve point. 25 | * Represented as the base64url encoding of the octet string representation of the coordinate. 26 | * MUST be present for all EC public keys 27 | */ 28 | x: string; 29 | /** 30 | * the y coordinate for the Elliptic Curve point. 31 | * Represented as the base64url encoding of the octet string representation of the coordinate. 32 | */ 33 | y?: string; 34 | }; 35 | 36 | export type PrivateJwk = PublicJwk & { 37 | /** 38 | * the Elliptic Curve private key value. 39 | * It is represented as the base64url encoding of the octet string representation of the private key value 40 | * MUST be present to represent Elliptic Curve private keys. 41 | */ 42 | d: string; 43 | }; 44 | 45 | export interface SignatureAlgorithm { 46 | /** 47 | * signs the provided payload using the provided JWK 48 | * @param content - the content to sign 49 | * @param privateJwk - the key to sign with 50 | * @returns the signed content (aka signature) 51 | */ 52 | sign(content: Uint8Array, privateJwk: PrivateJwk): Promise; 53 | 54 | /** 55 | * Verifies a signature against the provided payload hash and public key. 56 | * @param content - the content to verify with 57 | * @param signature - the signature to verify against 58 | * @param publicJwk - the key to verify with 59 | * @returns a boolean indicating whether the signature matches 60 | */ 61 | verify(content: Uint8Array, signature: Uint8Array, publicJwk: PublicJwk): Promise; 62 | 63 | /** 64 | * generates a random key pair 65 | * @returns the public and private keys as JWKs 66 | */ 67 | generateKeyPair(): Promise<{ publicJwk: PublicJwk, privateJwk: PrivateJwk }> 68 | 69 | 70 | /** 71 | * converts public key in bytes into a JWK 72 | * @param publicKeyBytes - the public key to convert into JWK 73 | * @returns the public key in JWK format 74 | */ 75 | publicKeyToJwk(publicKeyBytes: Uint8Array): Promise 76 | } -------------------------------------------------------------------------------- /src/types/jws-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * General JWS definition. Payload is returned as an empty 3 | * string when JWS Unencoded Payload Option 4 | * [RFC7797](https://www.rfc-editor.org/rfc/rfc7797) is used. 5 | */ 6 | export type GeneralJws = { 7 | payload: string 8 | signatures: SignatureEntry[] 9 | }; 10 | 11 | /** 12 | * An entry of the `signatures` array in a general JWS. 13 | */ 14 | export type SignatureEntry = { 15 | /** 16 | * The "protected" member MUST be present and contain the value 17 | * BASE64URL(UTF8(JWS Protected Header)) when the JWS Protected 18 | * Header value is non-empty; otherwise, it MUST be absent. These 19 | * Header Parameter values are integrity protected. 20 | */ 21 | protected: string 22 | 23 | /** 24 | * The "signature" member MUST be present and contain the value 25 | * BASE64URL(JWS Signature). 26 | */ 27 | signature: string 28 | }; 29 | -------------------------------------------------------------------------------- /src/types/message-interface.ts: -------------------------------------------------------------------------------- 1 | import type { GenericMessage, GenericSignaturePayload } from './message-types.js'; 2 | 3 | /** 4 | * An generic interface that represents a DWN message and convenience methods for working with it. 5 | */ 6 | export interface MessageInterface { 7 | /** 8 | * Valid JSON message representing this DWN message. 9 | */ 10 | get message(): M; 11 | 12 | /** 13 | * Gets the signer of this message. 14 | * This is not to be confused with the logical author of the message. 15 | */ 16 | get signer(): string | undefined; 17 | 18 | /** 19 | * DID of the logical author of this message. 20 | * NOTE: we say "logical" author because a message can be signed by a delegate of the actual author, 21 | * in which case the author DID would not be the same as the signer/delegate DID, 22 | * but be the DID of the grantor (`grantedBy`) of the delegated grant presented. 23 | */ 24 | get author(): string | undefined; 25 | 26 | /** 27 | * Decoded payload of the signature of this message. 28 | */ 29 | get signaturePayload(): GenericSignaturePayload | undefined; 30 | } 31 | -------------------------------------------------------------------------------- /src/types/message-store.ts: -------------------------------------------------------------------------------- 1 | import type { Filter, KeyValues, PaginationCursor } from './query-types.js'; 2 | import type { GenericMessage, MessageSort, Pagination } from './message-types.js'; 3 | 4 | export interface MessageStoreOptions { 5 | signal?: AbortSignal; 6 | } 7 | 8 | export interface MessageStore { 9 | /** 10 | * opens a connection to the underlying store 11 | */ 12 | open(): Promise; 13 | 14 | /** 15 | * closes the connection to the underlying store 16 | */ 17 | close(): Promise; 18 | 19 | /** 20 | * adds a message to the underlying store. Uses the message's cid as the key 21 | * @param indexes indexes (key-value pairs) to be included as part of this put operation 22 | */ 23 | put( 24 | tenant: string, 25 | message: GenericMessage, 26 | indexes: KeyValues, 27 | options?: MessageStoreOptions 28 | ): Promise; 29 | 30 | /** 31 | * Fetches a single message by `cid` from the underlying store. 32 | * Returns `undefined` no message was found. 33 | */ 34 | get(tenant: string, cid: string, options?: MessageStoreOptions): Promise; 35 | 36 | /** 37 | * Queries the underlying store for messages that matches the provided filters. 38 | * Supplying multiple filters establishes an OR condition between the filters. 39 | */ 40 | query( 41 | tenant: string, 42 | filters: Filter[], 43 | messageSort?: MessageSort, 44 | pagination?: Pagination, 45 | options?: MessageStoreOptions 46 | ): Promise<{ messages: GenericMessage[], cursor?: PaginationCursor}>; 47 | 48 | /** 49 | * Deletes the message associated with the id provided. 50 | */ 51 | delete(tenant: string, cid: string, options?: MessageStoreOptions): Promise; 52 | 53 | /** 54 | * Clears the entire store. Mainly used for cleaning up in test environment. 55 | */ 56 | clear(): Promise; 57 | } -------------------------------------------------------------------------------- /src/types/messages-types.ts: -------------------------------------------------------------------------------- 1 | import type { MessageEvent } from './subscriptions.js'; 2 | import type { Readable } from 'readable-stream'; 3 | import type { AuthorizationModel, GenericMessage, GenericMessageReply, MessageSubscription } from './message-types.js'; 4 | import type { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js'; 5 | import type { PaginationCursor, RangeCriterion } from './query-types.js'; 6 | 7 | /** 8 | * filters used when filtering for any type of Message across interfaces 9 | */ 10 | export type MessagesFilter = { 11 | interface?: string; 12 | method?: string; 13 | protocol?: string; 14 | messageTimestamp?: RangeCriterion; 15 | }; 16 | 17 | export type MessagesReadDescriptor = { 18 | interface : DwnInterfaceName.Messages; 19 | method: DwnMethodName.Read; 20 | messageCid: string; 21 | messageTimestamp: string; 22 | }; 23 | 24 | export type MessagesReadMessage = GenericMessage & { 25 | authorization: AuthorizationModel; // overriding `GenericMessage` with `authorization` being required 26 | descriptor: MessagesReadDescriptor; 27 | }; 28 | 29 | export type MessagesReadReplyEntry = { 30 | messageCid: string; 31 | message: GenericMessage; 32 | data?: Readable; 33 | }; 34 | 35 | export type MessagesReadReply = GenericMessageReply & { 36 | entry?: MessagesReadReplyEntry; 37 | }; 38 | 39 | export type MessagesQueryDescriptor = { 40 | interface: DwnInterfaceName.Messages; 41 | method: DwnMethodName.Query; 42 | messageTimestamp: string; 43 | filters: MessagesFilter[]; 44 | cursor?: PaginationCursor; 45 | }; 46 | 47 | export type MessagesQueryMessage = GenericMessage & { 48 | authorization: AuthorizationModel; 49 | descriptor: MessagesQueryDescriptor; 50 | }; 51 | 52 | export type MessagesQueryReply = GenericMessageReply & { 53 | entries?: string[]; 54 | cursor?: PaginationCursor; 55 | }; 56 | 57 | export type MessageSubscriptionHandler = (event: MessageEvent) => void; 58 | 59 | export type MessagesSubscribeMessageOptions = { 60 | subscriptionHandler: MessageSubscriptionHandler; 61 | }; 62 | 63 | export type MessagesSubscribeMessage = { 64 | authorization: AuthorizationModel; 65 | descriptor: MessagesSubscribeDescriptor; 66 | }; 67 | 68 | export type MessagesSubscribeReply = GenericMessageReply & { 69 | subscription?: MessageSubscription; 70 | }; 71 | 72 | export type MessagesSubscribeDescriptor = { 73 | interface: DwnInterfaceName.Messages; 74 | method: DwnMethodName.Subscribe; 75 | messageTimestamp: string; 76 | filters: MessagesFilter[]; 77 | }; 78 | -------------------------------------------------------------------------------- /src/types/method-handler.ts: -------------------------------------------------------------------------------- 1 | import type { MessageSubscriptionHandler } from './messages-types.js'; 2 | import type { Readable } from 'readable-stream'; 3 | import type { RecordSubscriptionHandler } from './records-types.js'; 4 | import type { GenericMessage, GenericMessageReply } from './message-types.js'; 5 | 6 | /** 7 | * Interface that defines a message handler of a specific method. 8 | */ 9 | export interface MethodHandler { 10 | /** 11 | * Handles the given message and returns a `MessageReply` response. 12 | */ 13 | handle(input: { 14 | tenant: string; 15 | message: GenericMessage; 16 | dataStream?: Readable 17 | subscriptionHandler?: MessageSubscriptionHandler | RecordSubscriptionHandler; 18 | }): Promise; 19 | } -------------------------------------------------------------------------------- /src/types/query-types.ts: -------------------------------------------------------------------------------- 1 | export type QueryOptions = { 2 | sortProperty: string; 3 | sortDirection?: SortDirection; 4 | limit?: number; 5 | cursor?: PaginationCursor; 6 | }; 7 | 8 | export enum SortDirection { 9 | Descending = -1, 10 | Ascending = 1 11 | } 12 | 13 | export type KeyValues = { [key:string]: string | number | boolean | string[] | number[] }; 14 | 15 | export type EqualFilter = string | number | boolean; 16 | 17 | export type OneOfFilter = EqualFilter[]; 18 | 19 | export type RangeValue = string | number; 20 | 21 | /** 22 | * "greater than" or "greater than or equal to" range condition. `gt` and `gte` are mutually exclusive. 23 | */ 24 | export type GT = ({ gt: RangeValue } & { gte?: never }) | ({ gt?: never } & { gte: RangeValue }); 25 | 26 | /** 27 | * "less than" or "less than or equal to" range condition. `lt`, `lte` are mutually exclusive. 28 | */ 29 | export type LT = ({ lt: RangeValue } & { lte?: never }) | ({ lt?: never } & { lte: RangeValue }); 30 | 31 | /** 32 | * Ranger filter. 1 condition is required. 33 | */ 34 | export type RangeFilter = (GT | LT) & Partial & Partial; 35 | 36 | export type StartsWithFilter = { 37 | startsWith: string; 38 | }; 39 | 40 | export type FilterValue = EqualFilter | OneOfFilter | RangeFilter; 41 | 42 | export type Filter = { 43 | [property: string]: FilterValue; 44 | }; 45 | 46 | export type RangeCriterion = { 47 | /** 48 | * Inclusive starting date-time. 49 | */ 50 | from?: string; 51 | 52 | /** 53 | * Inclusive end date-time. 54 | */ 55 | to?: string; 56 | }; 57 | 58 | export type PaginationCursor = { 59 | messageCid: string; 60 | value: string | number; 61 | }; -------------------------------------------------------------------------------- /src/types/signer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A signer that is capable of generating a digital signature over any given bytes. 3 | */ 4 | export interface Signer { 5 | /** 6 | * The ID of the key used by this signer. 7 | * This needs to be a fully-qualified ID (ie. prefixed with DID) so that author can be parsed out for processing such as `recordId` computation. 8 | * Example: did:example:alice#key1 9 | * This value will be used as the "kid" parameter in JWS produced. 10 | * While this property is not a required property per JWS specification, it is required for DWN authentication. 11 | */ 12 | keyId: string 13 | 14 | /** 15 | * The name of the signature algorithm used by this signer. 16 | * This value will be used as the "alg" parameter in JWS produced. 17 | * This parameter is not used by the DWN but is unfortunately a required header property for a JWS as per: 18 | * https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1 19 | * Valid signature algorithm values can be found at https://www.iana.org/assignments/jose/jose.xhtml 20 | */ 21 | algorithm: string; 22 | 23 | /** 24 | * Signs the given content and returns the signature as bytes. 25 | */ 26 | sign (content: Uint8Array): Promise; 27 | } 28 | -------------------------------------------------------------------------------- /src/types/subscriptions.ts: -------------------------------------------------------------------------------- 1 | import type { GenericMessageReply } from '../types/message-types.js'; 2 | import type { KeyValues } from './query-types.js'; 3 | import type { RecordsWriteMessage } from './records-types.js'; 4 | import type { GenericMessage, MessageSubscription } from './message-types.js'; 5 | 6 | export type EventListener = (tenant: string, event: MessageEvent, indexes: KeyValues) => void; 7 | 8 | /** 9 | * MessageEvent contains the message being emitted and an optional initial write message. 10 | */ 11 | export type MessageEvent = { 12 | message: GenericMessage; 13 | /** the initial write of the RecordsWrite or RecordsDelete message */ 14 | initialWrite?: RecordsWriteMessage 15 | }; 16 | 17 | /** 18 | * The EventStream interface implements a pub/sub system based on Message filters. 19 | */ 20 | export interface EventStream { 21 | subscribe(tenant: string, id: string, listener: EventListener): Promise; 22 | emit(tenant: string, event: MessageEvent, indexes: KeyValues): void; 23 | open(): Promise; 24 | close(): Promise; 25 | } 26 | 27 | export interface EventSubscription { 28 | id: string; 29 | close: () => Promise; 30 | } 31 | 32 | export type SubscriptionReply = GenericMessageReply & { 33 | subscription?: MessageSubscription; 34 | }; 35 | -------------------------------------------------------------------------------- /src/utils/abort.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Wraps the given `AbortSignal` in a `Promise` that rejects if it is programmatically triggered, 3 | * otherwise the promise will remain in await state (will never resolve). 4 | */ 5 | function promisifySignal(signal: AbortSignal): Promise { 6 | return new Promise((resolve, reject) => { 7 | // immediately reject if the given is signal is already aborted 8 | if (signal.aborted) { 9 | reject(signal.reason); 10 | return; 11 | } 12 | 13 | signal.addEventListener('abort', () => { 14 | reject(signal.reason); 15 | }); 16 | }); 17 | } 18 | 19 | /** 20 | * Wraps the given `Promise` such that it will reject if the `AbortSignal` is triggered. 21 | */ 22 | export async function executeUnlessAborted(promise: Promise, signal: AbortSignal | undefined): Promise { 23 | if (!signal) { 24 | return promise; 25 | } 26 | 27 | return Promise.race([ 28 | promise, 29 | promisifySignal(signal), 30 | ]); 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Array utility methods. 3 | */ 4 | export class ArrayUtility { 5 | /** 6 | * Returns `true` if content of the two given byte arrays are equal; `false` otherwise. 7 | */ 8 | public static byteArraysEqual(array1: Uint8Array, array2:Uint8Array): boolean { 9 | const equal = array1.length === array2.length && array1.every((value, index) => value === array2[index]); 10 | return equal; 11 | } 12 | 13 | /** 14 | * Asynchronously iterates an {AsyncGenerator} to return all the values in an array. 15 | */ 16 | public static async fromAsyncGenerator(iterator: AsyncGenerator): Promise> { 17 | const array: Array = [ ]; 18 | for await (const value of iterator) { 19 | array.push(value); 20 | } 21 | return array; 22 | } 23 | 24 | /** 25 | * Generic asynchronous sort method. 26 | */ 27 | public static async asyncSort(array: T[], asyncComparer: (a: T, b: T) => Promise): Promise { 28 | // this is a bubble sort implementation 29 | for (let i = 0; i < array.length; i++) { 30 | for (let j = i + 1; j < array.length; j++) { 31 | const comparison = await asyncComparer(array[i], array[j]); 32 | if (comparison > 0) { 33 | [array[i], array[j]] = [array[j], array[i]]; // Swap 34 | } 35 | } 36 | } 37 | return array; 38 | } 39 | } -------------------------------------------------------------------------------- /src/utils/encoder.ts: -------------------------------------------------------------------------------- 1 | import { base64url } from 'multiformats/bases/base64'; 2 | 3 | const textEncoder = new TextEncoder(); 4 | const textDecoder = new TextDecoder(); 5 | 6 | /** 7 | * Utility class for encoding/converting data into various formats. 8 | */ 9 | export class Encoder { 10 | 11 | public static base64UrlToBytes(base64urlString: string): Uint8Array { 12 | const content = base64url.baseDecode(base64urlString); 13 | return content; 14 | } 15 | 16 | public static base64UrlToObject(base64urlString: string): any { 17 | const payloadBytes = base64url.baseDecode(base64urlString); 18 | const payloadObject = Encoder.bytesToObject(payloadBytes); 19 | return payloadObject; 20 | } 21 | 22 | public static bytesToBase64Url(bytes: Uint8Array): string { 23 | const base64UrlString = base64url.baseEncode(bytes); 24 | return base64UrlString; 25 | } 26 | 27 | public static bytesToString(content: Uint8Array): string { 28 | const bytes = textDecoder.decode(content); 29 | return bytes; 30 | } 31 | 32 | public static bytesToObject(content: Uint8Array): object { 33 | const contentString = Encoder.bytesToString(content); 34 | const contentObject = JSON.parse(contentString); 35 | return contentObject; 36 | } 37 | 38 | public static objectToBytes(obj: Record): Uint8Array { 39 | const objectString = JSON.stringify(obj); 40 | const objectBytes = textEncoder.encode(objectString); 41 | return objectBytes; 42 | } 43 | 44 | public static stringToBase64Url(content: string): string { 45 | const bytes = textEncoder.encode(content); 46 | const base64UrlString = base64url.baseEncode(bytes); 47 | return base64UrlString; 48 | } 49 | 50 | public static stringToBytes(content: string): Uint8Array { 51 | const bytes = textEncoder.encode(content); 52 | return bytes; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/memory-cache.ts: -------------------------------------------------------------------------------- 1 | import type { Cache } from '../types/cache.js'; 2 | import { LRUCache } from 'lru-cache'; 3 | 4 | /** 5 | * A cache using local memory. 6 | */ 7 | export class MemoryCache implements Cache { 8 | private cache: LRUCache; 9 | 10 | /** 11 | * @param timeToLiveInSeconds time-to-live for every key-value pair set in the cache 12 | */ 13 | public constructor (private timeToLiveInSeconds: number) { 14 | this.cache = new LRUCache({ 15 | max : 100_000, 16 | ttl : timeToLiveInSeconds * 1000 17 | }); 18 | } 19 | 20 | async set(key: string, value: any): Promise { 21 | try { 22 | this.cache.set(key, value); 23 | } catch { 24 | // let the code continue as this is a non-fatal error 25 | } 26 | } 27 | 28 | async get(key: string): Promise { 29 | return this.cache.get(key); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/object.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks whether the given object has any properties. 3 | */ 4 | export function isEmptyObject(obj: unknown): boolean { 5 | if (typeof(obj) !== 'object') { 6 | return false; 7 | } 8 | 9 | for (const _ in obj) { 10 | return false; 11 | } 12 | 13 | return true; 14 | } 15 | 16 | /** 17 | * Recursively removes all properties with an empty object or array as its value from the given object. 18 | */ 19 | export function removeEmptyObjects(obj: Record): void { 20 | Object.keys(obj).forEach(key => { 21 | if (typeof(obj[key]) === 'object') { 22 | // recursive remove empty object or array properties in nested objects 23 | removeEmptyObjects(obj[key] as Record); 24 | } 25 | 26 | if (isEmptyObject(obj[key])) { 27 | delete obj[key]; 28 | } 29 | }); 30 | } 31 | 32 | /** 33 | * Recursively removes all properties with `undefined` as its value from the given object. 34 | */ 35 | export function removeUndefinedProperties(obj: Record): void { 36 | Object.keys(obj).forEach(key => { 37 | if (obj[key] === undefined) { 38 | delete obj[key]; 39 | } else if (typeof(obj[key]) === 'object') { 40 | removeUndefinedProperties(obj[key] as Record); // recursive remove `undefined` properties in nested objects 41 | } 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/private-key-signer.ts: -------------------------------------------------------------------------------- 1 | import type { PrivateJwk } from '../types/jose-types.js'; 2 | import type { Signer } from '../types/signer.js'; 3 | 4 | import { signatureAlgorithms } from '../jose/algorithms/signing/signature-algorithms.js'; 5 | import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; 6 | 7 | /** 8 | * Input to `PrivateKeySigner` constructor. 9 | */ 10 | export type PrivateKeySignerOptions = { 11 | /** 12 | * Private JWK to create the signer from. 13 | */ 14 | privateJwk: PrivateJwk; 15 | 16 | /** 17 | * If not specified, the constructor will attempt to default/fall back to the `kid` value in the given `privateJwk`. 18 | */ 19 | keyId?: string; 20 | 21 | /** 22 | * If not specified, the constructor will attempt to default/fall back to the `alg` value in the given `privateJwk`. 23 | */ 24 | algorithm?: string; 25 | }; 26 | 27 | /** 28 | * A signer that signs using a private key. 29 | */ 30 | export class PrivateKeySigner implements Signer { 31 | public keyId; 32 | public algorithm; 33 | private privateJwk: PrivateJwk; 34 | private signatureAlgorithm; 35 | 36 | public constructor(options: PrivateKeySignerOptions) { 37 | if (options.keyId === undefined && options.privateJwk.kid === undefined) { 38 | throw new DwnError( 39 | DwnErrorCode.PrivateKeySignerUnableToDeduceKeyId, 40 | `Unable to deduce the key ID` 41 | ); 42 | } 43 | 44 | // NOTE: `alg` is optional for a JWK as specified in https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 45 | if (options.algorithm === undefined && options.privateJwk.alg === undefined) { 46 | throw new DwnError( 47 | DwnErrorCode.PrivateKeySignerUnableToDeduceAlgorithm, 48 | `Unable to deduce the signature algorithm` 49 | ); 50 | } 51 | 52 | this.keyId = options.keyId ?? options.privateJwk.kid!; 53 | this.algorithm = options.algorithm ?? options.privateJwk.alg!; 54 | this.privateJwk = options.privateJwk; 55 | this.signatureAlgorithm = signatureAlgorithms[options.privateJwk.crv]; 56 | 57 | if (!this.signatureAlgorithm) { 58 | throw new DwnError( 59 | DwnErrorCode.PrivateKeySignerUnsupportedCurve, 60 | `Unsupported crv ${options.privateJwk.crv}, crv must be one of ${Object.keys(signatureAlgorithms)}` 61 | ); 62 | } 63 | } 64 | 65 | /** 66 | * Signs the given content and returns the signature as bytes. 67 | */ 68 | public async sign (content: Uint8Array): Promise { 69 | const signatureBytes = await this.signatureAlgorithm.sign(content, this.privateJwk); 70 | return signatureBytes; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/protocols.ts: -------------------------------------------------------------------------------- 1 | import type { DerivedPrivateJwk } from '../utils/hd-key.js'; 2 | import type { PrivateJwk } from '../types/jose-types.js'; 3 | import type { ProtocolDefinition, ProtocolRuleSet } from '../types/protocols-types.js'; 4 | 5 | import { Secp256k1 } from './secp256k1.js'; 6 | import { HdKey, KeyDerivationScheme } from '../utils/hd-key.js'; 7 | 8 | /** 9 | * Class containing Protocol related utility methods. 10 | */ 11 | export class Protocols { 12 | /** 13 | * Derives public encryptions keys and inject it in the `$encryption` property for each protocol path segment of the given Protocol definition, 14 | * then returns the final encryption-enabled protocol definition. 15 | * NOTE: The original definition passed in is unmodified. 16 | */ 17 | public static async deriveAndInjectPublicEncryptionKeys( 18 | protocolDefinition: ProtocolDefinition, 19 | rootKeyId: string, 20 | privateJwk: PrivateJwk 21 | ): Promise { 22 | // clone before modify 23 | const encryptionEnabledProtocolDefinition = JSON.parse(JSON.stringify(protocolDefinition)) as ProtocolDefinition; 24 | 25 | // a function that recursively creates and adds `$encryption` property to every rule set 26 | async function addEncryptionProperty(ruleSet: ProtocolRuleSet, parentKey: DerivedPrivateJwk): Promise { 27 | for (const key in ruleSet) { 28 | // if we encounter a nested rule set (a property name that doesn't begin with '$'), recursively inject the `$encryption` property 29 | if (!key.startsWith('$')) { 30 | const derivedPrivateKey = await HdKey.derivePrivateKey(parentKey, [key]); 31 | const publicKeyJwk = await Secp256k1.getPublicJwk(derivedPrivateKey.derivedPrivateKey); 32 | 33 | ruleSet[key].$encryption = { rootKeyId, publicKeyJwk }; 34 | await addEncryptionProperty(ruleSet[key], derivedPrivateKey); 35 | } 36 | } 37 | } 38 | 39 | // inject encryption property starting from each root level record type 40 | const rootKey: DerivedPrivateJwk = { 41 | derivationScheme : KeyDerivationScheme.ProtocolPath, 42 | derivedPrivateKey : privateJwk, 43 | rootKeyId 44 | }; 45 | const protocolLevelDerivedKey = await HdKey.derivePrivateKey(rootKey, [KeyDerivationScheme.ProtocolPath, protocolDefinition.protocol]); 46 | await addEncryptionProperty(encryptionEnabledProtocolDefinition.structure, protocolLevelDerivedKey); 47 | 48 | return encryptionEnabledProtocolDefinition; 49 | } 50 | } -------------------------------------------------------------------------------- /src/utils/string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Compares two string given in lexicographical order. 3 | * @returns 1 if `a` is larger than `b`; -1 if `a` is smaller/older than `b`; 0 otherwise (same message) 4 | */ 5 | export function lexicographicalCompare(a: string, b: string): number { 6 | if (a > b) { 7 | return 1; 8 | } else if (a < b) { 9 | return -1; 10 | } else { 11 | return 0; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from '@js-temporal/polyfill'; 2 | import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; 3 | 4 | /** 5 | * Time related utilities. 6 | */ 7 | export class Time { 8 | /** 9 | * sleeps for the desired duration 10 | * @param durationInMillisecond the desired amount of sleep time 11 | * @returns when the provided duration has passed 12 | */ 13 | public static async sleep(durationInMillisecond: number): Promise { 14 | return new Promise(resolve => setTimeout(resolve, durationInMillisecond)); 15 | } 16 | 17 | /** 18 | * We must sleep for at least 2ms to avoid timestamp collisions during testing. 19 | * https://github.com/TBD54566975/dwn-sdk-js/issues/481 20 | */ 21 | public static async minimalSleep(): Promise { 22 | await Time.sleep(2); 23 | } 24 | 25 | /** 26 | * Returns an UTC ISO-8601 timestamp with microsecond precision accepted by DWN. 27 | * using @js-temporal/polyfill 28 | */ 29 | public static getCurrentTimestamp(): string { 30 | return Temporal.Now.instant().toString({ smallestUnit: 'microseconds' }); 31 | } 32 | 33 | /** 34 | * Creates a UTC ISO-8601 timestamp in microsecond precision accepted by DWN. 35 | * @param options - Options for creating the timestamp. 36 | * @returns string 37 | */ 38 | public static createTimestamp(options: { 39 | year?: number, month?: number, day?: number, hour?: number, minute?: number, second?: number, millisecond?: number, microsecond?: number 40 | }): string { 41 | const { year, month, day, hour, minute, second, millisecond, microsecond } = options; 42 | return Temporal.ZonedDateTime.from({ 43 | timeZone: 'UTC', 44 | year, 45 | month, 46 | day, 47 | hour, 48 | minute, 49 | second, 50 | millisecond, 51 | microsecond 52 | }).toInstant().toString({ smallestUnit: 'microseconds' }); 53 | } 54 | 55 | /** 56 | * Creates a UTC ISO-8601 timestamp offset from now or given timestamp accepted by DWN. 57 | * @param offset Negative number means offset into the past. 58 | */ 59 | public static createOffsetTimestamp(offset: { seconds: number }, timestamp?: string): string { 60 | const timestampInstant = timestamp ? Temporal.Instant.from(timestamp) : Temporal.Now.instant(); 61 | const offsetDuration = Temporal.Duration.from(offset); 62 | const offsetInstant = timestampInstant.add(offsetDuration); 63 | return offsetInstant.toString({ smallestUnit: 'microseconds' }); 64 | } 65 | 66 | /** 67 | * Validates that the provided timestamp is a valid number 68 | * @param timestamp the timestamp to validate 69 | * @throws DwnError if timestamp is not a valid number 70 | */ 71 | public static validateTimestamp(timestamp: string): void { 72 | try { 73 | Temporal.Instant.from(timestamp); 74 | } catch { 75 | throw new DwnError(DwnErrorCode.TimestampInvalid, `Invalid timestamp: ${timestamp}`); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/url.ts: -------------------------------------------------------------------------------- 1 | import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; 2 | 3 | export function validateProtocolUrlNormalized(url: string): void { 4 | let normalized: string | undefined; 5 | try { 6 | normalized = normalizeProtocolUrl(url); 7 | } catch { 8 | normalized = undefined; 9 | } 10 | 11 | if (url !== normalized) { 12 | throw new DwnError(DwnErrorCode.UrlProtocolNotNormalized, `Protocol URI ${url} must be normalized.`); 13 | } 14 | } 15 | 16 | export function normalizeProtocolUrl(url: string): string { 17 | // Keeping protocol normalization as a separate function in case 18 | // protocol and schema normalization diverge in the future 19 | return normalizeUrl(url); 20 | } 21 | 22 | export function validateSchemaUrlNormalized(url: string): void { 23 | let normalized: string | undefined; 24 | try { 25 | normalized = normalizeSchemaUrl(url); 26 | } catch { 27 | normalized = undefined; 28 | } 29 | 30 | if (url !== normalized) { 31 | throw new DwnError(DwnErrorCode.UrlSchemaNotNormalized, `Schema URI ${url} must be normalized.`); 32 | } 33 | } 34 | 35 | export function normalizeSchemaUrl(url: string): string { 36 | // Keeping schema normalization as a separate function in case 37 | // protocol and schema normalization diverge in the future 38 | return normalizeUrl(url); 39 | } 40 | 41 | function normalizeUrl(url: string): string { 42 | let fullUrl: string; 43 | if (/^[^:]+:(\/{2})?[^\/].*/.test(url)) { 44 | fullUrl = url; 45 | } else { 46 | fullUrl = `http://${url}`; 47 | } 48 | 49 | try { 50 | const result = new URL(fullUrl); 51 | result.search = ''; 52 | result.hash = ''; 53 | return removeTrailingSlash(result.href); 54 | } catch (e) { 55 | throw new DwnError(DwnErrorCode.UrlProtocolNotNormalizable, 'Could not normalize protocol URI'); 56 | } 57 | } 58 | 59 | function removeTrailingSlash(str: string): string { 60 | if (str.endsWith('/')) { 61 | return str.slice(0, -1); 62 | } else { 63 | return str; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/core/auth.spec.ts: -------------------------------------------------------------------------------- 1 | import { authenticate } from '../../src/core/auth.js'; 2 | import { DwnErrorCode } from '../../src/core/dwn-error.js'; 3 | import { expect } from 'chai'; 4 | import { DidDht, UniversalResolver } from '@web5/dids'; 5 | 6 | describe('Auth', () => { 7 | describe('authenticate()', () => { 8 | it('should throw if given JWS is `undefined`', async () => { 9 | const jws = undefined; 10 | await expect(authenticate(jws, new UniversalResolver({ didResolvers: [DidDht] }))).to.be.rejectedWith(DwnErrorCode.AuthenticateJwsMissing); 11 | }); 12 | }); 13 | }); -------------------------------------------------------------------------------- /tests/core/message-reply.spec.ts: -------------------------------------------------------------------------------- 1 | import type { GenericMessageReply } from '../../src/types/message-types.js'; 2 | 3 | import { expect } from 'chai'; 4 | import { messageReplyFromError } from '../../src/core/message-reply.js'; 5 | 6 | describe('Message Reply', () => { 7 | it('handles non-Errors being thrown', () => { 8 | let response: GenericMessageReply; 9 | try { 10 | throw 'Some error message'; 11 | } catch (e: unknown) { 12 | response = messageReplyFromError(e, 500); 13 | } 14 | expect(response.status.code).to.eq(500); 15 | expect(response.status.detail).to.eq('Error'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/core/protocol-authorization.spec.ts: -------------------------------------------------------------------------------- 1 | import { DwnErrorCode } from '../../src/core/dwn-error.js'; 2 | import { expect } from 'chai'; 3 | import { MessageStoreLevel } from '../../src/index.js'; 4 | import { ProtocolAuthorization } from '../../src/core/protocol-authorization.js'; 5 | import { TestDataGenerator } from '../utils/test-data-generator.js'; 6 | 7 | import sinon from 'sinon'; 8 | 9 | describe('ProtocolAuthorization', () => { 10 | beforeEach(() => { 11 | sinon.restore(); 12 | }); 13 | 14 | describe('authorizeWrite()', () => { 15 | it('should throw if message references non-existent parent', async () => { 16 | const alice = await TestDataGenerator.generateDidKeyPersona(); 17 | 18 | const { recordsWrite } = await TestDataGenerator.generateRecordsWrite({ 19 | author : alice, 20 | parentContextId : 'nonExistentParent', 21 | }); 22 | 23 | // stub the message store 24 | const messageStoreStub = sinon.createStubInstance(MessageStoreLevel); 25 | messageStoreStub.query.resolves({ messages: [] }); // simulate parent not in message store 26 | 27 | await expect(ProtocolAuthorization.authorizeWrite(alice.did, recordsWrite, messageStoreStub)).to.be.rejectedWith( 28 | DwnErrorCode.ProtocolAuthorizationParentNotFoundConstructingRecordChain 29 | ); 30 | }); 31 | }); 32 | 33 | describe('getActionsSeekingARuleMatch()', () => { 34 | it('should return empty array if unknown message method type is given', async () => { 35 | const alice = await TestDataGenerator.generateDidKeyPersona(); 36 | 37 | const deliberatelyCraftedInvalidMessage = { 38 | message: { 39 | descriptor: { 40 | method: 'invalid-method' 41 | }, 42 | } 43 | } as any; 44 | 45 | const messageStoreStub = sinon.createStubInstance(MessageStoreLevel); 46 | expect(ProtocolAuthorization['getActionsSeekingARuleMatch'](alice.did, deliberatelyCraftedInvalidMessage, messageStoreStub)).to.be.empty; 47 | }); 48 | }); 49 | }); -------------------------------------------------------------------------------- /tests/event-log/event-log-level.spec.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtility } from '../../src/utils/array.js'; 2 | import { EventLogLevel } from '../../src/event-log/event-log-level.js'; 3 | import { Message } from '../../src/core/message.js'; 4 | import { TestDataGenerator } from '../utils/test-data-generator.js'; 5 | 6 | import chaiAsPromised from 'chai-as-promised'; 7 | import sinon from 'sinon'; 8 | import chai, { expect } from 'chai'; 9 | 10 | chai.use(chaiAsPromised); 11 | 12 | let eventLog: EventLogLevel; 13 | 14 | describe('EventLogLevel Tests', () => { 15 | before(async () => { 16 | eventLog = new EventLogLevel({ location: 'TEST-EVENTLOG' }); 17 | await eventLog.open(); 18 | }); 19 | 20 | beforeEach(async () => { 21 | await eventLog.clear(); 22 | }); 23 | 24 | after(async () => { 25 | await eventLog.close(); 26 | }); 27 | 28 | describe('deleteEventsByCid', () => { 29 | it('deletes all index related data', async () => { 30 | const { author, message, recordsWrite } = await TestDataGenerator.generateRecordsWrite(); 31 | const messageCid = await Message.getCid(message); 32 | const index = await recordsWrite.constructIndexes(true); 33 | await eventLog.append(author.did, messageCid, index); 34 | 35 | const indexLevelDeleteSpy = sinon.spy(eventLog.index, 'delete'); 36 | 37 | await eventLog.deleteEventsByCid(author.did, [ messageCid ]); 38 | expect(indexLevelDeleteSpy.callCount).to.equal(1); 39 | 40 | const keysAfterDelete = await ArrayUtility.fromAsyncGenerator(eventLog.index.db.keys()); 41 | expect(keysAfterDelete.length).to.equal(0); 42 | }); 43 | }); 44 | }); -------------------------------------------------------------------------------- /tests/interfaces/messages-get.spec.ts: -------------------------------------------------------------------------------- 1 | import type { MessagesReadMessage } from '../../src/index.js'; 2 | 3 | import { expect } from 'chai'; 4 | import { Message } from '../../src/core/message.js'; 5 | import { MessagesRead } from '../../src/index.js'; 6 | import { TestDataGenerator } from '../utils/test-data-generator.js'; 7 | import { DwnErrorCode, Jws } from '../../src/index.js'; 8 | 9 | describe('MessagesRead Message', () => { 10 | describe('create', () => { 11 | it('creates a MessagesRead message', async () => { 12 | const { author, message } = await TestDataGenerator.generateRecordsWrite(); 13 | const messageCid = await Message.getCid(message); 14 | const messageTimestamp = TestDataGenerator.randomTimestamp(); 15 | 16 | const messagesRead = await MessagesRead.create({ 17 | signer : await Jws.createSigner(author), 18 | messageCid : messageCid, 19 | messageTimestamp, 20 | }); 21 | 22 | expect(messagesRead.message.authorization).to.exist; 23 | expect(messagesRead.message.descriptor).to.exist; 24 | expect(messagesRead.message.descriptor.messageCid).to.equal(messageCid); 25 | expect(messagesRead.message.descriptor.messageTimestamp).to.equal(messageTimestamp); 26 | }); 27 | 28 | it('throws an error if an invalid CID is provided', async () => { 29 | const alice = await TestDataGenerator.generatePersona(); 30 | 31 | try { 32 | await MessagesRead.create({ 33 | signer : await Jws.createSigner(alice), 34 | messageCid : 'abcd' 35 | }); 36 | 37 | expect.fail(); 38 | } catch (e: any) { 39 | expect(e.message).to.include(DwnErrorCode.MessagesReadInvalidCid); 40 | } 41 | }); 42 | }); 43 | 44 | describe('parse', () => { 45 | it('parses a message into a MessagesRead instance', async () => { 46 | const { author, message } = await TestDataGenerator.generateRecordsWrite(); 47 | let messageCid = await Message.getCid(message); 48 | 49 | const messagesRead = await MessagesRead.create({ 50 | signer : await Jws.createSigner(author), 51 | messageCid : messageCid 52 | }); 53 | 54 | const parsed = await MessagesRead.parse(messagesRead.message); 55 | expect(parsed).to.be.instanceof(MessagesRead); 56 | 57 | const expectedMessageCid = await Message.getCid(messagesRead.message); 58 | messageCid = await Message.getCid(parsed.message); 59 | 60 | expect(messageCid).to.equal(expectedMessageCid); 61 | }); 62 | 63 | it('throws an exception if messageCids contains an invalid cid', async () => { 64 | const { author, message: recordsWriteMessage } = await TestDataGenerator.generateRecordsWrite(); 65 | const messageCid = await Message.getCid(recordsWriteMessage); 66 | 67 | const messagesRead = await MessagesRead.create({ 68 | signer : await Jws.createSigner(author), 69 | messageCid : messageCid 70 | }); 71 | 72 | const message = messagesRead.toJSON() as MessagesReadMessage; 73 | message.descriptor.messageCid = 'abcd'; 74 | 75 | try { 76 | await MessagesRead.parse(message); 77 | 78 | expect.fail(); 79 | } catch (e: any) { 80 | expect(e.message).to.include('is not a valid CID'); 81 | } 82 | }); 83 | }); 84 | }); -------------------------------------------------------------------------------- /tests/interfaces/messages-subscribe.spec.ts: -------------------------------------------------------------------------------- 1 | import { MessagesSubscribe } from '../../src/interfaces/messages-subscribe.js'; 2 | import { DwnInterfaceName, DwnMethodName, Jws, TestDataGenerator, Time } from '../../src/index.js'; 3 | 4 | import { expect } from 'chai'; 5 | 6 | describe('MessagesSubscribe', () => { 7 | describe('create()', () => { 8 | it('should be able to create and authorize MessagesSubscribe', async () => { 9 | const alice = await TestDataGenerator.generateDidKeyPersona(); 10 | const timestamp = Time.getCurrentTimestamp(); 11 | const messagesSubscribe = await MessagesSubscribe.create({ 12 | signer : Jws.createSigner(alice), 13 | messageTimestamp : timestamp, 14 | }); 15 | 16 | const message = messagesSubscribe.message; 17 | expect(message.descriptor.interface).to.eql(DwnInterfaceName.Messages); 18 | expect(message.descriptor.method).to.eql(DwnMethodName.Subscribe); 19 | expect(message.authorization).to.exist; 20 | expect(message.descriptor.messageTimestamp).to.equal(timestamp); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/interfaces/protocols-query.spec.ts: -------------------------------------------------------------------------------- 1 | import chaiAsPromised from 'chai-as-promised'; 2 | import chai, { expect } from 'chai'; 3 | 4 | import type { ProtocolsQueryMessage } from '../../src/index.js'; 5 | 6 | import dexProtocolDefinition from '../vectors/protocol-definitions/dex.json' assert { type: 'json' }; 7 | import { Jws } from '../../src/index.js'; 8 | import { ProtocolsQuery } from '../../src/interfaces/protocols-query.js'; 9 | import { TestDataGenerator } from '../utils/test-data-generator.js'; 10 | import { Time } from '../../src/utils/time.js'; 11 | 12 | chai.use(chaiAsPromised); 13 | 14 | describe('ProtocolsQuery', () => { 15 | describe('create()', () => { 16 | it('should use `messageTimestamp` as is if given', async () => { 17 | const alice = await TestDataGenerator.generatePersona(); 18 | 19 | const currentTime = Time.getCurrentTimestamp(); 20 | const protocolsQuery = await ProtocolsQuery.create({ 21 | filter : { protocol: 'anyValue' }, 22 | messageTimestamp : currentTime, 23 | signer : Jws.createSigner(alice), 24 | }); 25 | 26 | expect(protocolsQuery.message.descriptor.messageTimestamp).to.equal(currentTime); 27 | }); 28 | 29 | 30 | it('should auto-normalize protocol URI', async () => { 31 | const alice = await TestDataGenerator.generatePersona(); 32 | 33 | const options = { 34 | recipient : alice.did, 35 | data : TestDataGenerator.randomBytes(10), 36 | dataFormat : 'application/json', 37 | signer : Jws.createSigner(alice), 38 | filter : { protocol: 'example.com/' }, 39 | definition : dexProtocolDefinition 40 | }; 41 | const protocolsConfig = await ProtocolsQuery.create(options); 42 | 43 | const message = protocolsConfig.message as ProtocolsQueryMessage; 44 | 45 | expect(message.descriptor.filter!.protocol).to.eq('http://example.com'); 46 | }); 47 | }); 48 | }); 49 | 50 | -------------------------------------------------------------------------------- /tests/interfaces/records-delete.spec.ts: -------------------------------------------------------------------------------- 1 | import chaiAsPromised from 'chai-as-promised'; 2 | import chai, { expect } from 'chai'; 3 | 4 | import { Jws } from '../../src/index.js'; 5 | import { RecordsDelete } from '../../src/interfaces/records-delete.js'; 6 | import { TestDataGenerator } from '../utils/test-data-generator.js'; 7 | import { Time } from '../../src/utils/time.js'; 8 | 9 | chai.use(chaiAsPromised); 10 | 11 | describe('RecordsDelete', () => { 12 | describe('create()', () => { 13 | it('should use `messageTimestamp` as is if given', async () => { 14 | const alice = await TestDataGenerator.generatePersona(); 15 | 16 | const currentTime = Time.getCurrentTimestamp(); 17 | const recordsDelete = await RecordsDelete.create({ 18 | recordId : 'anything', 19 | signer : Jws.createSigner(alice), 20 | messageTimestamp : currentTime 21 | }); 22 | 23 | expect(recordsDelete.message.descriptor.messageTimestamp).to.equal(currentTime); 24 | }); 25 | 26 | it('should auto-fill `messageTimestamp` if not given', async () => { 27 | const alice = await TestDataGenerator.generatePersona(); 28 | 29 | const recordsDelete = await RecordsDelete.create({ 30 | recordId : 'anything', 31 | signer : Jws.createSigner(alice) 32 | }); 33 | 34 | expect(recordsDelete.message.descriptor.messageTimestamp).to.exist; 35 | }); 36 | }); 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /tests/interfaces/records-read.spec.ts: -------------------------------------------------------------------------------- 1 | import chaiAsPromised from 'chai-as-promised'; 2 | import chai, { expect } from 'chai'; 3 | 4 | import dexProtocolDefinition from '../vectors/protocol-definitions/dex.json' assert { type: 'json' }; 5 | import { Jws } from '../../src/index.js'; 6 | import { RecordsRead } from '../../src/interfaces/records-read.js'; 7 | import { TestDataGenerator } from '../utils/test-data-generator.js'; 8 | import { Time } from '../../src/utils/time.js'; 9 | 10 | chai.use(chaiAsPromised); 11 | 12 | describe('RecordsRead', () => { 13 | describe('create()', () => { 14 | it('should use `messageTimestamp` as is if given', async () => { 15 | const alice = await TestDataGenerator.generatePersona(); 16 | 17 | const currentTime = Time.getCurrentTimestamp(); 18 | const recordsRead = await RecordsRead.create({ 19 | filter: { 20 | recordId: 'anything', 21 | }, 22 | signer : Jws.createSigner(alice), 23 | messageTimestamp : currentTime 24 | }); 25 | 26 | expect(recordsRead.message.descriptor.messageTimestamp).to.equal(currentTime); 27 | }); 28 | 29 | it('should auto-normalize protocol URL', async () => { 30 | const alice = await TestDataGenerator.generatePersona(); 31 | 32 | const options = { 33 | recipient : alice.did, 34 | data : TestDataGenerator.randomBytes(10), 35 | dataFormat : 'application/json', 36 | signer : Jws.createSigner(alice), 37 | filter : { protocol: 'example.com/' }, 38 | definition : dexProtocolDefinition 39 | }; 40 | const recordsQuery = await RecordsRead.create(options); 41 | 42 | const message = recordsQuery.message; 43 | 44 | expect(message.descriptor.filter!.protocol).to.eq('http://example.com'); 45 | }); 46 | 47 | it('should auto-normalize schema URL', async () => { 48 | const alice = await TestDataGenerator.generatePersona(); 49 | 50 | const options = { 51 | recipient : alice.did, 52 | data : TestDataGenerator.randomBytes(10), 53 | dataFormat : 'application/json', 54 | signer : Jws.createSigner(alice), 55 | filter : { schema: 'example.com/' }, 56 | definition : dexProtocolDefinition 57 | }; 58 | const recordsQuery = await RecordsRead.create(options); 59 | 60 | const message = recordsQuery.message; 61 | 62 | expect(message.descriptor.filter!.schema).to.eq('http://example.com'); 63 | }); 64 | }); 65 | }); 66 | 67 | -------------------------------------------------------------------------------- /tests/protocols/permission-request.spec.ts: -------------------------------------------------------------------------------- 1 | import type { RecordsPermissionScope } from '../../src/types/permission-types.js'; 2 | 3 | import chaiAsPromised from 'chai-as-promised'; 4 | import sinon from 'sinon'; 5 | import chai, { expect } from 'chai'; 6 | 7 | import { Jws } from '../../src/utils/jws.js'; 8 | import { DwnInterfaceName, DwnMethodName, PermissionRequest, PermissionsProtocol, TestDataGenerator } from '../../src/index.js'; 9 | 10 | chai.use(chaiAsPromised); 11 | 12 | describe('PermissionRequest', () => { 13 | afterEach(() => { 14 | // restores all fakes, stubs, spies etc. not restoring causes a memory leak. 15 | // more info here: https://sinonjs.org/releases/v13/general-setup/ 16 | sinon.restore(); 17 | }); 18 | 19 | it('should parse a permission request message into a PermissionRequest', async () => { 20 | const alice = await TestDataGenerator.generateDidKeyPersona(); 21 | const scope: RecordsPermissionScope = { 22 | interface : DwnInterfaceName.Records, 23 | method : DwnMethodName.Query, 24 | protocol : 'https://example.com/protocol/test' 25 | }; 26 | 27 | const permissionRequest = await PermissionsProtocol.createRequest({ 28 | signer : Jws.createSigner(alice), 29 | delegated : true, 30 | scope 31 | }); 32 | 33 | const parsedPermissionRequest = await PermissionRequest.parse(permissionRequest.dataEncodedMessage); 34 | expect (parsedPermissionRequest.id).to.equal(permissionRequest.dataEncodedMessage.recordId); 35 | expect (parsedPermissionRequest.delegated).to.equal(true); 36 | expect (parsedPermissionRequest.scope).to.deep.equal(scope); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/store-dependent-tests.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestSuite } from './test-suite.js'; 2 | 3 | describe('Store dependent tests', () => { 4 | TestSuite.runInjectableDependentTests(); 5 | }); -------------------------------------------------------------------------------- /tests/store/message-store-level.spec.ts: -------------------------------------------------------------------------------- 1 | import type { MessageStore } from '../../src/index.js'; 2 | import type { CreateLevelDatabaseOptions, LevelDatabase } from '../../src/store/level-wrapper.js'; 3 | 4 | import { createLevelDatabase } from '../../src/store/level-wrapper.js'; 5 | import { expect } from 'chai'; 6 | import { MessageStoreLevel } from '../../src/store/message-store-level.js'; 7 | 8 | let messageStore: MessageStore; 9 | 10 | describe('MessageStoreLevel Test Suite', () => { 11 | // important to follow the `before` and `after` pattern to initialize and clean the stores in tests 12 | // so that different test suites can reuse the same backend store for testing 13 | before(async () => { 14 | messageStore = new MessageStoreLevel({ 15 | blockstoreLocation : 'TEST-MESSAGESTORE', 16 | indexLocation : 'TEST-INDEX' 17 | }); 18 | await messageStore.open(); 19 | }); 20 | 21 | beforeEach(async () => { 22 | await messageStore.clear(); // clean up before each test rather than after so that a test does not depend on other tests to do the clean up 23 | }); 24 | 25 | after(async () => { 26 | await messageStore.close(); 27 | }); 28 | 29 | describe('createLevelDatabase', function () { 30 | it('should be called if provided', async () => { 31 | // need to close the message store instance first before creating a new one with the same name below 32 | await messageStore.close(); 33 | 34 | const locations = new Set; 35 | 36 | messageStore = new MessageStoreLevel({ 37 | blockstoreLocation : 'TEST-MESSAGESTORE', 38 | indexLocation : 'TEST-INDEX', 39 | createLevelDatabase(location: string, options?: CreateLevelDatabaseOptions): Promise> { 40 | locations.add(location); 41 | return createLevelDatabase(location, options); 42 | } 43 | }); 44 | await messageStore.open(); 45 | 46 | expect(locations).to.eql(new Set([ 'TEST-MESSAGESTORE', 'TEST-INDEX' ])); 47 | }); 48 | }); 49 | }); -------------------------------------------------------------------------------- /tests/test-event-stream.ts: -------------------------------------------------------------------------------- 1 | import type { EventStream } from '../src/index.js'; 2 | 3 | import { EventEmitterStream } from '../src/index.js'; 4 | 5 | /** 6 | * Class that manages the EventStream implementation for testing. 7 | * This is intended to be extended as the single point of configuration 8 | * that allows different EventStream implementations to be swapped in 9 | * to test compatibility with default/built-in implementation. 10 | */ 11 | export class TestEventStream { 12 | private static eventStream?: EventStream; 13 | 14 | /** 15 | * Overrides the event stream with a given implementation. 16 | * If not given, default implementation will be used. 17 | */ 18 | public static override(overrides?: { eventStream?: EventStream }): void { 19 | TestEventStream.eventStream = overrides?.eventStream; 20 | } 21 | 22 | /** 23 | * Initializes and returns the event stream used for running the test suite. 24 | */ 25 | public static get(): EventStream { 26 | TestEventStream.eventStream ??= new EventEmitterStream(); 27 | return TestEventStream.eventStream; 28 | } 29 | } -------------------------------------------------------------------------------- /tests/test-stores.ts: -------------------------------------------------------------------------------- 1 | import type { DataStore, EventLog, MessageStore, ResumableTaskStore } from '../src/index.js'; 2 | import { DataStoreLevel, EventLogLevel, MessageStoreLevel, ResumableTaskStoreLevel } from '../src/index.js'; 3 | 4 | /** 5 | * Class that manages store implementations for testing. 6 | * This is intended to be extended as the single point of configuration 7 | * that allows different store implementations to be swapped in 8 | * to test compatibility with default/built-in store implementations. 9 | */ 10 | export class TestStores { 11 | 12 | private static messageStore?: MessageStore; 13 | private static dataStore?: DataStore; 14 | private static eventLog?: EventLog; 15 | private static resumableTaskStore?: ResumableTaskStore; 16 | 17 | /** 18 | * Overrides test stores with given implementation. 19 | * If not given, default implementation will be used. 20 | */ 21 | public static override(overrides?:{ 22 | messageStore?: MessageStore, 23 | dataStore?: DataStore, 24 | eventLog?: EventLog, 25 | resumableTaskStore?: ResumableTaskStore, 26 | }): void { 27 | TestStores.messageStore = overrides?.messageStore; 28 | TestStores.dataStore = overrides?.dataStore; 29 | TestStores.eventLog = overrides?.eventLog; 30 | TestStores.resumableTaskStore = overrides?.resumableTaskStore; 31 | } 32 | 33 | /** 34 | * Initializes and return the stores used for running the test suite. 35 | */ 36 | public static get(): { 37 | messageStore: MessageStore, 38 | dataStore: DataStore, 39 | eventLog: EventLog, 40 | resumableTaskStore: ResumableTaskStore, 41 | } { 42 | TestStores.messageStore ??= new MessageStoreLevel({ 43 | blockstoreLocation : 'TEST-MESSAGESTORE', 44 | indexLocation : 'TEST-INDEX' 45 | }); 46 | 47 | TestStores.dataStore ??= new DataStoreLevel({ 48 | blockstoreLocation: 'TEST-DATASTORE' 49 | }); 50 | 51 | TestStores.eventLog ??= new EventLogLevel({ 52 | location: 'TEST-EVENTLOG' 53 | }); 54 | 55 | TestStores.resumableTaskStore ??= new ResumableTaskStoreLevel({ 56 | location: 'TEST-RESUMABLE-TASK-STORE' 57 | }); 58 | 59 | return { 60 | messageStore : TestStores.messageStore, 61 | dataStore : TestStores.dataStore, 62 | eventLog : TestStores.eventLog, 63 | resumableTaskStore : TestStores.resumableTaskStore, 64 | }; 65 | } 66 | } -------------------------------------------------------------------------------- /tests/utils/data-stream.spec.ts: -------------------------------------------------------------------------------- 1 | import chaiAsPromised from 'chai-as-promised'; 2 | import chai, { expect } from 'chai'; 3 | 4 | import { TestDataGenerator } from './test-data-generator.js'; 5 | import { DataStream, Encoder } from '../../src/index.js'; 6 | 7 | // extends chai to test promises 8 | chai.use(chaiAsPromised); 9 | 10 | describe('DataStream', () => { 11 | it('should be able to convert an object to a readable stream using `fromObject() and read back the bytes using `toBytes`', async () => { 12 | const originalObject = { 13 | a: TestDataGenerator.randomString(32) 14 | }; 15 | 16 | const stream = DataStream.fromObject(originalObject); 17 | const readBytes = await DataStream.toBytes(stream); 18 | const readObject = JSON.parse(Encoder.bytesToString(readBytes)); 19 | expect(readObject.a).to.equal(originalObject.a); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/utils/hd-key.spec.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtility } from '../../src/utils/array.js'; 2 | import { DwnErrorCode } from '../../src/core/dwn-error.js'; 3 | import { expect } from 'chai'; 4 | import { HdKey } from '../../src/utils/hd-key.js'; 5 | import { Secp256k1 } from '../../src/utils/secp256k1.js'; 6 | 7 | describe('HdKey', () => { 8 | describe('derivePrivateKeyBytes()', () => { 9 | it('should be able to derive same key using different ancestor along the chain path', async () => { 10 | const { privateKey } = await Secp256k1.generateKeyPairRaw(); 11 | 12 | const fullPathToG = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; 13 | const fullPathToD = ['a', 'b', 'c', 'd']; 14 | const relativePathFromDToG = ['e', 'f', 'g']; 15 | 16 | // testing private key derivation from different ancestor in the same chain 17 | const privateKeyG = await HdKey.derivePrivateKeyBytes(privateKey, fullPathToG); 18 | const privateKeyD = await HdKey.derivePrivateKeyBytes(privateKey, fullPathToD); 19 | const privateKeyGFromD = await HdKey.derivePrivateKeyBytes(privateKeyD, relativePathFromDToG); 20 | expect(ArrayUtility.byteArraysEqual(privateKeyG, privateKeyGFromD)).to.be.true; 21 | }); 22 | 23 | it('should throw if derivation path is invalid', async () => { 24 | const { privateKey } = await Secp256k1.generateKeyPairRaw(); 25 | 26 | const invalidPath = ['should not have segment with empty string', '']; 27 | 28 | await expect(HdKey.derivePrivateKeyBytes(privateKey, invalidPath)).to.be.rejectedWith(DwnErrorCode.HdKeyDerivationPathInvalid); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/utils/jws.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Jws', () => { 2 | describe('verifySignature', () => { 3 | xit('throws an exception if signature does not match', () => {}); 4 | xit('returns true if signature is successfully verified', () => {}); 5 | }); 6 | }); -------------------------------------------------------------------------------- /tests/utils/memory-cache.spec.ts: -------------------------------------------------------------------------------- 1 | import chaiAsPromised from 'chai-as-promised'; 2 | import chai, { expect } from 'chai'; 3 | 4 | import { MemoryCache } from '../../src/utils/memory-cache.js'; 5 | import sinon from 'sinon'; 6 | 7 | // extends chai to test promises 8 | chai.use(chaiAsPromised); 9 | 10 | describe('MemoryCache', () => { 11 | it('should return `undefined` when value expires', async () => { 12 | const memoryCache = new MemoryCache(0.01); // 0.01 second = 10 millisecond time-to-live 13 | 14 | await memoryCache.set('key', 'aValue'); 15 | let valueInCache = await memoryCache.get('key'); 16 | expect(valueInCache).to.equal('aValue'); 17 | 18 | await new Promise(resolve => setTimeout(resolve, 11)); // wait for 11 millisecond for value to expire 19 | valueInCache = await memoryCache.get('key'); 20 | expect(valueInCache).to.be.undefined; 21 | }); 22 | 23 | it('should continue if set() fails', async () => { 24 | const timeToLiveInSeconds = 1; 25 | const memoryCache = new MemoryCache(timeToLiveInSeconds); 26 | 27 | const setStub = sinon.stub(memoryCache['cache'], 'set'); 28 | setStub.throws('a simulated error'); 29 | 30 | await memoryCache.set('key', 'aValue'); 31 | expect(setStub.called).to.be.true; 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/utils/object.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { removeEmptyObjects, removeUndefinedProperties } from '../../src/utils/object.js'; 3 | 4 | describe('Object', () => { 5 | describe('removeUndefinedProperties', () => { 6 | it('should remove all `undefined` properties of a nested object', () => { 7 | const mockObject = { 8 | a : true, 9 | b : undefined, 10 | c : { 11 | a : 0, 12 | b : undefined, 13 | } 14 | }; 15 | const expectedResult = { 16 | a : true, 17 | c : { 18 | a: 0 19 | } 20 | }; 21 | 22 | removeUndefinedProperties(mockObject); 23 | 24 | expect(mockObject).to.deep.equal(expectedResult); 25 | }); 26 | }); 27 | 28 | describe('removeEmptyObjects', () => { 29 | it('should remove all empty objects', () => { 30 | const obj = { 31 | foo : {}, 32 | bar : { baz: {} }, 33 | buzz : 'hello' 34 | }; 35 | removeEmptyObjects(obj); 36 | 37 | expect(obj).to.deep.equal({ buzz: 'hello' }); 38 | }); 39 | }); 40 | }); -------------------------------------------------------------------------------- /tests/utils/poller.ts: -------------------------------------------------------------------------------- 1 | import { Time } from '../../src/utils/time.js'; 2 | 3 | export class Poller { 4 | 5 | /** 6 | * The interval in milliseconds to wait before retrying the delegate function. 7 | */ 8 | static pollRetrySleep: number = 20; 9 | 10 | /** 11 | * The maximum time in milliseconds to wait before timing out the delegate function. 12 | */ 13 | static pollTimeout: number = 2000; 14 | 15 | /** 16 | * Polls the delegate function until it succeeds or the timeout is exceeded. 17 | * 18 | * @param delegate a function that returns a promise and may throw. 19 | * @param retrySleep the interval in milliseconds to wait before retrying the delegate function. 20 | * @param timeout the maximum time in milliseconds to wait before timing out the delegate function. 21 | * 22 | * @throws {Error} `Operation timed out` if the timeout is exceeded. 23 | */ 24 | static async pollUntilSuccessOrTimeout( 25 | delegate: () => Promise, 26 | retrySleep: number = Poller.pollRetrySleep, 27 | timeout: number = Poller.pollTimeout, 28 | ): Promise { 29 | const startTime = Date.now(); 30 | 31 | while (true) { 32 | try { 33 | // Attempt to execute the delegate function 34 | return await delegate(); 35 | } catch (error) { 36 | // Check if the timeout has been exceeded 37 | if (Date.now() - startTime >= timeout) { 38 | throw new Error('Operation timed out'); 39 | } 40 | 41 | // Sleep for the retry interval before attempting again 42 | await Time.sleep(retrySleep); 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /tests/utils/private-key-signer.spec.ts: -------------------------------------------------------------------------------- 1 | import { DwnErrorCode } from '../../src/core/dwn-error.js'; 2 | import { expect } from 'chai'; 3 | import { PrivateKeySigner } from '../../src/index.js'; 4 | import { Secp256k1 } from '../../src/utils/secp256k1.js'; 5 | 6 | describe('PrivateKeySigner', () => { 7 | describe('constructor', () => { 8 | it('should use key ID found in the private JWK if no key ID is explicitly given', async () => { 9 | const { privateJwk } = await Secp256k1.generateKeyPair(); 10 | privateJwk.kid = 'awesome-key-id'; 11 | 12 | const signer = new PrivateKeySigner({ privateJwk }); 13 | expect(signer.keyId).to.equal(privateJwk.kid); 14 | }); 15 | 16 | it('should override signature algorithm found in the private JWK if a value is explicitly given', async () => { 17 | const { privateJwk } = await Secp256k1.generateKeyPair(); 18 | 19 | const explicitlySpecifiedAlgorithm = 'awesome-algorithm'; 20 | const signer = new PrivateKeySigner({ privateJwk, keyId: 'anyValue', algorithm: explicitlySpecifiedAlgorithm }); 21 | expect(signer.algorithm).to.equal(explicitlySpecifiedAlgorithm); 22 | }); 23 | 24 | it('should throw if key ID is not explicitly specified and not given in private JWK', async () => { 25 | const { privateJwk } = await Secp256k1.generateKeyPair(); 26 | 27 | expect(() => new PrivateKeySigner({ privateJwk })).to.throw(DwnErrorCode.PrivateKeySignerUnableToDeduceKeyId); 28 | }); 29 | 30 | it('should throw if signature algorithm is not explicitly specified and not given in private JWK', async () => { 31 | const { privateJwk } = await Secp256k1.generateKeyPair(); 32 | delete privateJwk.alg; // remove `alg` for this test 33 | 34 | expect(() => new PrivateKeySigner({ privateJwk, keyId: 'anyValue' })).to.throw(DwnErrorCode.PrivateKeySignerUnableToDeduceAlgorithm); 35 | }); 36 | 37 | it('should throw if crypto curve of the given private JWK is not supported', async () => { 38 | const { privateJwk } = await Secp256k1.generateKeyPair(); 39 | (privateJwk as any).crv = 'unknown'; // change `crv` to an unsupported value for this test 40 | 41 | expect(() => new PrivateKeySigner({ privateJwk, keyId: 'anyValue' })).to.throw(DwnErrorCode.PrivateKeySignerUnsupportedCurve); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/utils/records.spec.ts: -------------------------------------------------------------------------------- 1 | import type { DerivedPrivateJwk, RecordsWriteDescriptor } from '../../src/index.js'; 2 | 3 | import { expect } from 'chai'; 4 | 5 | import { DwnErrorCode } from '../../src/core/dwn-error.js'; 6 | import { ed25519 } from '../../src/jose/algorithms/signing/ed25519.js'; 7 | import { DwnInterfaceName, DwnMethodName, KeyDerivationScheme, Records } from '../../src/index.js'; 8 | 9 | describe('Records', () => { 10 | describe('deriveLeafPrivateKey()', () => { 11 | it('should throw if given private key is not supported', async () => { 12 | const derivedKey: DerivedPrivateJwk = { 13 | rootKeyId : 'unused', 14 | derivationScheme : KeyDerivationScheme.ProtocolPath, 15 | derivedPrivateKey : (await ed25519.generateKeyPair()).privateJwk 16 | }; 17 | await expect(Records.derivePrivateKey(derivedKey, ['a'])).to.be.rejectedWith(DwnErrorCode.RecordsDerivePrivateKeyUnSupportedCurve); 18 | }); 19 | }); 20 | 21 | describe('constructKeyDerivationPathUsingProtocolPathScheme()', () => { 22 | it('should throw if given a flat-space descriptor', async () => { 23 | const descriptor: RecordsWriteDescriptor = { 24 | interface : DwnInterfaceName.Records, 25 | method : DwnMethodName.Write, 26 | dataCid : 'anyCid', 27 | dataFormat : 'application/json', 28 | dataSize : 123, 29 | dateCreated : '2022-12-19T10:20:30.123456Z', 30 | messageTimestamp : '2022-12-19T10:20:30.123456Z', 31 | }; 32 | 33 | expect(() => Records.constructKeyDerivationPathUsingProtocolPathScheme(descriptor)) 34 | .to.throw(DwnErrorCode.RecordsProtocolPathDerivationSchemeMissingProtocol); 35 | }); 36 | }); 37 | 38 | describe('constructKeyDerivationPathUsingProtocolContextScheme()', () => { 39 | it('should throw if not given contextId', async () => { 40 | expect(() => Records.constructKeyDerivationPathUsingProtocolContextScheme(undefined)) 41 | .to.throw(DwnErrorCode.RecordsProtocolContextDerivationSchemeMissingContextId); 42 | }); 43 | }); 44 | 45 | describe('constructKeyDerivationPathUsingSchemasScheme()', () => { 46 | it('should throw if not given schema', async () => { 47 | expect(() => Records.constructKeyDerivationPathUsingSchemasScheme(undefined)) 48 | .to.throw(DwnErrorCode.RecordsSchemasDerivationSchemeMissingSchema); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/utils/secp256k1.spec.ts: -------------------------------------------------------------------------------- 1 | import { base64url } from 'multiformats/bases/base64'; 2 | import { DwnErrorCode } from '../../src/core/dwn-error.js'; 3 | import { expect } from 'chai'; 4 | import { Secp256k1 } from '../../src/utils/secp256k1.js'; 5 | import { TestDataGenerator } from './test-data-generator.js'; 6 | 7 | describe('Secp256k1', () => { 8 | describe('generateKeyPairRaw()', () => { 9 | it('should generate compressed publicKey', async () => { 10 | const { publicKey } = await Secp256k1.generateKeyPairRaw(); 11 | expect(publicKey.length).to.equal(33); 12 | }); 13 | }); 14 | 15 | describe('validateKey()', () => { 16 | it('should throw if key is not a valid SECP256K1 key', async () => { 17 | const validKey = (await Secp256k1.generateKeyPair()).publicJwk; 18 | 19 | expect(() => Secp256k1.validateKey({ ...validKey, kty: 'invalidKty' as any })).to.throw(DwnErrorCode.Secp256k1KeyNotValid); 20 | expect(() => Secp256k1.validateKey({ ...validKey, crv: 'invalidCrv' as any })).to.throw(DwnErrorCode.Secp256k1KeyNotValid); 21 | }); 22 | }); 23 | 24 | describe('publicKeyToJwk()', () => { 25 | it('should generate the same JWK regardless of compressed or compressed public key bytes given', async () => { 26 | const compressedPublicKeyBase64UrlString = 'A5roVr1J6MufaaBwweb5Q75PrZCbZpzC55kTCO68ylMs'; 27 | const uncompressedPublicKeyBase64UrlString = 'BJroVr1J6MufaaBwweb5Q75PrZCbZpzC55kTCO68ylMsyC3G4QfbKeDzIr2BwyMUQ3Na1mxPvwxJ8GBMO3jkGL0'; 28 | 29 | const compressedPublicKey = base64url.baseDecode(compressedPublicKeyBase64UrlString); 30 | const uncompressedPublicKey = base64url.baseDecode(uncompressedPublicKeyBase64UrlString); 31 | 32 | const publicJwk1 = await Secp256k1.publicKeyToJwk(compressedPublicKey); 33 | const publicJwk2 = await Secp256k1.publicKeyToJwk(uncompressedPublicKey); 34 | 35 | expect(publicJwk1.x).to.equal(publicJwk2.x); 36 | expect(publicJwk1.y).to.equal(publicJwk2.y); 37 | }); 38 | }); 39 | 40 | describe('sign()', () => { 41 | it('should generate the signature in compact format', async () => { 42 | const { privateJwk } = await Secp256k1.generateKeyPair(); 43 | 44 | const contentBytes = TestDataGenerator.randomBytes(16); 45 | const signatureBytes = await Secp256k1.sign(contentBytes, privateJwk); 46 | 47 | expect(signatureBytes.length).to.equal(64); // DER format would be 70 bytes 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/utils/secp256r1.spec.ts: -------------------------------------------------------------------------------- 1 | import { base64url } from 'multiformats/bases/base64'; 2 | import { DwnErrorCode } from '../../src/core/dwn-error.js'; 3 | import { expect } from 'chai'; 4 | import { p256 } from '@noble/curves/p256'; 5 | import { Secp256r1 } from '../../src/utils/secp256r1.js'; 6 | import { TestDataGenerator } from './test-data-generator.js'; 7 | 8 | describe('Secp256r1', () => { 9 | describe('validateKey()', () => { 10 | it('should throw if key is not a valid SECP256R1 key', async () => { 11 | const validKey = (await Secp256r1.generateKeyPair()).publicJwk; 12 | 13 | expect(() => 14 | Secp256r1.validateKey({ ...validKey, kty: 'invalidKty' as any }) 15 | ).to.throw(DwnErrorCode.Secp256r1KeyNotValid); 16 | expect(() => 17 | Secp256r1.validateKey({ ...validKey, crv: 'invalidCrv' as any }) 18 | ).to.throw(DwnErrorCode.Secp256r1KeyNotValid); 19 | }); 20 | }); 21 | 22 | describe('publicKeyToJwk()', () => { 23 | it('should generate the same JWK regardless of compressed or uncompressed public key bytes given', async () => { 24 | const compressedPublicKeyBase64UrlString = 25 | 'Aom0shYia6t0cNMRQDRzPgCxdMWQamrfX3UJfOroLHo_'; 26 | const uncompressedPublicKeyBase64UrlString = 27 | 'BIm0shYia6t0cNMRQDRzPgCxdMWQamrfX3UJfOroLHo_cSITyng0NN1lt2BtZVXH4PE9Gerxq_mw2_CpbBHsWUI'; 28 | 29 | const compressedPublicKey = base64url.baseDecode( 30 | compressedPublicKeyBase64UrlString 31 | ); 32 | 33 | const uncompressedPublicKey = base64url.baseDecode( 34 | uncompressedPublicKeyBase64UrlString 35 | ); 36 | 37 | const publicJwk1 = await Secp256r1.publicKeyToJwk(compressedPublicKey); 38 | const publicJwk2 = await Secp256r1.publicKeyToJwk(uncompressedPublicKey); 39 | 40 | expect(publicJwk1.x).to.equal(publicJwk2.x); 41 | expect(publicJwk1.y).to.equal(publicJwk2.y); 42 | }); 43 | }); 44 | 45 | describe('verify()', () => { 46 | it('should correctly handle DER formatted signatures', async () => { 47 | const { privateJwk, publicJwk } = await Secp256r1.generateKeyPair(); 48 | 49 | const content = TestDataGenerator.randomBytes(16); 50 | 51 | const signature = await Secp256r1.sign(content, privateJwk); 52 | 53 | // Convert the signature to DER format 54 | const derSignature = 55 | p256.Signature.fromCompact(signature).toDERRawBytes(); 56 | 57 | const result = await Secp256r1.verify(content, derSignature, publicJwk); 58 | 59 | expect(result).to.equal(true); 60 | }); 61 | }); 62 | 63 | describe('sign()', () => { 64 | it('should generate the signature in compact format', async () => { 65 | const { privateJwk } = await Secp256r1.generateKeyPair(); 66 | 67 | const contentBytes = TestDataGenerator.randomBytes(16); 68 | const signatureBytes = await Secp256r1.sign(contentBytes, privateJwk); 69 | 70 | expect(signatureBytes.length).to.equal(64); // DER format would be 70 bytes 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /tests/utils/test-stub-generator.ts: -------------------------------------------------------------------------------- 1 | import type { Persona } from './test-data-generator.js'; 2 | import type { DidResolutionResult, DidResolver } from '@web5/dids'; 3 | 4 | import sinon from 'sinon'; 5 | 6 | import { TestDataGenerator } from './test-data-generator.js'; 7 | import { UniversalResolver } from '@web5/dids'; 8 | 9 | /** 10 | * Utility class for generating stub for testing. 11 | */ 12 | export class TestStubGenerator { 13 | /** 14 | * Creates a {DidResolver} stub for testing. 15 | */ 16 | public static createDidResolverStub(persona: Persona): DidResolver { 17 | 18 | // setting up a stub did resolver & message store 19 | const didResolutionResult = TestDataGenerator.createDidResolutionResult(persona); 20 | const resolveStub = sinon.stub<[string], Promise>(); 21 | resolveStub.withArgs(persona.did).resolves(didResolutionResult); 22 | const didResolverStub = sinon.createStubInstance(UniversalResolver, { resolve: resolveStub }); 23 | 24 | return didResolverStub; 25 | } 26 | 27 | /** 28 | * Stubs resolution results for the given personas. 29 | */ 30 | public static stubDidResolver(didResolver: DidResolver, personas: Persona[]): void { 31 | const didToResolutionMap = new Map(); 32 | 33 | for (const persona of personas) { 34 | const mockResolution = TestDataGenerator.createDidResolutionResult(persona); 35 | 36 | didToResolutionMap.set(persona.did, mockResolution); 37 | } 38 | 39 | sinon.stub(didResolver, 'resolve').callsFake((did) => { 40 | const mockResolution = didToResolutionMap.get(did); 41 | 42 | return new Promise((resolve, _reject) => { 43 | if (mockResolution === undefined) { 44 | throw new Error('unexpected DID'); 45 | } 46 | 47 | resolve(mockResolution); 48 | }); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/utils/time.spec.ts: -------------------------------------------------------------------------------- 1 | import { DwnErrorCode } from '../../src/core/dwn-error.js'; 2 | import { expect } from 'chai'; 3 | import { TestDataGenerator } from '../utils/test-data-generator.js'; 4 | import { Time } from '../../src/utils/time.js'; 5 | 6 | 7 | describe('time', () => { 8 | describe('validateTimestamp', () => { 9 | describe('invalid timestamps', () => { 10 | const invalidTimestamps = [ 11 | '2022-02-31T10:20:30.405060Z', // invalid day 12 | '2022-01-36T90:20:30.405060Z', // invalid hour 13 | '2022-01-36T25:99:30.405060Z', // invalid minute 14 | '2022-14-18T10:30:00.123456Z', // invalid month 15 | ]; 16 | invalidTimestamps.forEach((timestamp) => { 17 | it(`should throw an exception if an invalid timestamp is passed: ${timestamp}`, () => { 18 | expect(() => Time.validateTimestamp(timestamp)).to.throw(DwnErrorCode.TimestampInvalid); 19 | }); 20 | }); 21 | }); 22 | 23 | describe('valid timestamps', () => { 24 | it('should pass if a valid timestamp is passed', () => { 25 | expect(() => Time.validateTimestamp('2022-04-29T10:30:00.123456Z')).to.not.throw(); 26 | expect(() => Time.validateTimestamp(TestDataGenerator.randomTimestamp())).to.not.throw(); 27 | }); 28 | }); 29 | }); 30 | 31 | describe('createTimestamp', () => { 32 | it('should create a valid timestamp', () => { 33 | const timestamp = Time.createTimestamp({ 34 | year : 2022, 35 | month : 4, 36 | day : 29, 37 | hour : 10, 38 | minute : 30, 39 | second : 0, 40 | millisecond : 123, 41 | microsecond : 456 42 | }); 43 | expect(timestamp).to.equal('2022-04-29T10:30:00.123456Z'); 44 | }); 45 | 46 | for (let i = 0; i < 5; i++) { 47 | const year = TestDataGenerator.randomInt(1900, 2500); 48 | const month = TestDataGenerator.randomInt(1, 12); 49 | const day = TestDataGenerator.randomInt(1, 28); 50 | const hour = TestDataGenerator.randomInt(0, 23); 51 | const minute = TestDataGenerator.randomInt(0, 59); 52 | const second = TestDataGenerator.randomInt(0, 59); 53 | const millisecond = TestDataGenerator.randomInt(0, 999); 54 | const microsecond = TestDataGenerator.randomInt(0, 999); 55 | 56 | it(`should create a valid timestamp for random values ${i}`, () => { 57 | const timestamp = Time.createTimestamp({ year, month, day, hour, minute, second, millisecond, microsecond }); 58 | expect(()=> Time.validateTimestamp(timestamp)).to.not.throw(); 59 | }); 60 | } 61 | }); 62 | 63 | describe('createOffsetTimestamp', () => { 64 | it('should use the given timestamp as the base timestamp to compute the offset timestamp', () => { 65 | const baseTimestamp = '2000-04-29T10:30:00.123456Z'; 66 | const offsetTimestamp = Time.createOffsetTimestamp({ seconds: 60 * 60 * 24 * 365 }, baseTimestamp); 67 | 68 | expect(offsetTimestamp).to.equal('2001-04-29T10:30:00.123456Z'); 69 | }); 70 | }); 71 | }); -------------------------------------------------------------------------------- /tests/utils/url.spec.ts: -------------------------------------------------------------------------------- 1 | import chaiAsPromised from 'chai-as-promised'; 2 | import chai, { expect } from 'chai'; 3 | 4 | import { normalizeProtocolUrl, validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../../src/utils/url.js'; 5 | 6 | chai.use(chaiAsPromised); 7 | 8 | describe('url', () => { 9 | describe('validateProtocolUrlNormalized', () => { 10 | it('errors when URI is not normalized', () => { 11 | expect(() => validateProtocolUrlNormalized('https://example.com')).to.not.throw(); 12 | expect(() => validateProtocolUrlNormalized('example.com')).to.throw(); 13 | expect(() => validateProtocolUrlNormalized(':foo:')).to.throw(); 14 | }); 15 | }); 16 | 17 | describe('validateSchemaUrlNormalized', () => { 18 | it('should throw when URI is not normalized', () => { 19 | expect(() => validateSchemaUrlNormalized('example.com')).to.throw(); 20 | expect(() => validateSchemaUrlNormalized(':foo:')).to.throw(); 21 | }); 22 | }); 23 | 24 | describe('normalizeProtocolUrl', () => { 25 | it('returns hostname and path with trailing slash removed', () => { 26 | expect(normalizeProtocolUrl('example.com')).to.equal('http://example.com'); 27 | expect(normalizeProtocolUrl('example.com/')).to.equal('http://example.com'); 28 | expect(normalizeProtocolUrl('http://example.com')).to.equal('http://example.com'); 29 | expect(normalizeProtocolUrl('http://example.com/')).to.equal('http://example.com'); 30 | expect(normalizeProtocolUrl('example.com?foo=bar')).to.equal('http://example.com'); 31 | expect(normalizeProtocolUrl('example.com/?foo=bar')).to.equal('http://example.com'); 32 | expect(normalizeProtocolUrl('foo:example?foo=bar')).to.equal('foo:example'); 33 | 34 | expect(normalizeProtocolUrl('example.com/path')).to.equal('http://example.com/path'); 35 | expect(normalizeProtocolUrl('example.com/path/')).to.equal('http://example.com/path'); 36 | expect(normalizeProtocolUrl('http://example.com/path')).to.equal('http://example.com/path'); 37 | expect(normalizeProtocolUrl('http://example.com/path')).to.equal('http://example.com/path'); 38 | expect(normalizeProtocolUrl('example.com/path?foo=bar')).to.equal('http://example.com/path'); 39 | expect(normalizeProtocolUrl('example.com/path/?foo=bar')).to.equal('http://example.com/path'); 40 | expect(normalizeProtocolUrl('example.com/path#baz')).to.equal('http://example.com/path'); 41 | expect(normalizeProtocolUrl('example.com/path/#baz')).to.equal('http://example.com/path'); 42 | 43 | expect(normalizeProtocolUrl('example')).to.equal('http://example'); 44 | expect(normalizeProtocolUrl('/example/')).to.equal('http://example'); 45 | 46 | expect(() => normalizeProtocolUrl('://http')).to.throw(Error); 47 | expect(() => normalizeProtocolUrl(':foo:')).to.throw(Error); 48 | }); 49 | }); 50 | }); -------------------------------------------------------------------------------- /tests/validation/json-schemas/definitions.spec.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv'; 2 | import definitions from '../../../json-schemas/definitions.json' assert { type: 'json' }; 3 | 4 | import { expect } from 'chai'; 5 | 6 | describe('date-time schema', async () => { 7 | 8 | const ajv = new Ajv.default(); 9 | const validateDateTime = ajv.compile(definitions.$defs['date-time']); 10 | 11 | it('should accept ISO 8601 date-time strings accepted by DWN', () => { 12 | expect(validateDateTime('2022-04-29T10:30:00.123456Z')).to.be.true; 13 | }); 14 | 15 | it('should reject ISO 8601 date-time strings not accepted by DWN', () => { 16 | const unacceptableDateTimeStrings = [ 17 | '2023-04-27T13:30:00.123456', 18 | '2023-04-27T13:30:00.123456z', 19 | '2023-04-27T13:30:00.1234Z', 20 | '2023-04-27T13:30:00Z', 21 | '2023-04-27T13:30:00.000000+00:00', 22 | '2023-04-27 13:30:00.000000Z' 23 | ]; 24 | 25 | for (const dateTime of unacceptableDateTimeStrings) { 26 | expect(validateDateTime(dateTime)).to.be.false; 27 | } 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/validation/json-schemas/jwk/general-jwk.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { signatureAlgorithms } from '../../../../src/jose/algorithms/signing/signature-algorithms.js'; 3 | import { validateJsonSchema } from '../../../../src/schema-validator.js'; 4 | 5 | const { Ed25519, secp256k1 } = signatureAlgorithms; 6 | 7 | describe('GeneralJwk Schema', async () => { 8 | const jwkSecp256k1 = await secp256k1.generateKeyPair(); 9 | const jwkEd25519 = await Ed25519.generateKeyPair(); 10 | 11 | const jwkRsa = { 12 | publicJwk: { 13 | 'kty' : 'RSA', 14 | 'e' : 'AQAB', 15 | 'use' : 'sig', 16 | 'alg' : 'RS256', 17 | 'n' : 'abcd1234' 18 | }, 19 | privateJwk: { 20 | 'p' : 'pProp', 21 | 'kty' : 'RSA', 22 | 'q' : 'qProp', 23 | 'd' : 'dProp', 24 | 'e' : 'eProp', 25 | 'use' : 'sig', 26 | 'qi' : 'qiProp', 27 | 'dp' : 'dpProp', 28 | 'alg' : 'RS256', 29 | 'dq' : 'dqProp', 30 | 'n' : 'nProp' 31 | } 32 | }; 33 | 34 | [ 35 | jwkEd25519, 36 | jwkSecp256k1, 37 | jwkRsa 38 | ].forEach((jwk): void => { 39 | it('should not throw an exception if properly formatted jwk', () => { 40 | expect( 41 | () => validateJsonSchema('GeneralJwk', jwk.publicJwk) 42 | ).to.not.throw(); 43 | expect( 44 | () => validateJsonSchema('GeneralJwk', jwk.privateJwk) 45 | ).to.not.throw(); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/validation/json-schemas/jwk/public-jwk.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { signatureAlgorithms } from '../../../../src/jose/algorithms/signing/signature-algorithms.js'; 3 | import { validateJsonSchema } from '../../../../src/schema-validator.js'; 4 | 5 | const { Ed25519, secp256k1 } = signatureAlgorithms; 6 | 7 | describe('PublicJwk Schema', async () => { 8 | const { publicJwk: publicJwkSecp256k1 } = await secp256k1.generateKeyPair(); 9 | const { publicJwk: publicJwkEd25519 } = await Ed25519.generateKeyPair(); 10 | 11 | const publicJwkRsa = { 12 | 'kty' : 'RSA', 13 | 'e' : 'AQAB', 14 | 'use' : 'sig', 15 | 'alg' : 'RS256', 16 | 'n' : 'abcd1234' 17 | }; 18 | it('should not throw an exception if properly formatted publicJwk', () => { 19 | expect( 20 | () => validateJsonSchema('PublicJwk', publicJwkSecp256k1) 21 | ).to.not.throw(); 22 | expect( 23 | () => validateJsonSchema('PublicJwk', publicJwkEd25519) 24 | ).to.not.throw(); 25 | expect( 26 | () => validateJsonSchema('PublicJwk', publicJwkRsa) 27 | ).to.not.throw(); 28 | }); 29 | 30 | it('should throw an exception if publicJwk has private property', () => { 31 | expect( 32 | () => validateJsonSchema('PublicJwk', { ...publicJwkSecp256k1, d: 'supersecret' }) 33 | ).to.throw(); 34 | expect( 35 | () => validateJsonSchema('PublicJwk', { ...publicJwkEd25519, d: 'supersecret' }) 36 | ).to.throw(); 37 | expect( 38 | () => validateJsonSchema('PublicJwk', { ...publicJwkRsa, oth: {} }) 39 | ).to.throw(); 40 | expect( 41 | () => validateJsonSchema('PublicJwk', { ...publicJwkRsa, d: 'supersecret', oth: {} }) 42 | ).to.throw(); 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /tests/validation/json-schemas/protocols/protocols-configure.spec.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolAction, type ProtocolDefinition, type ProtocolsConfigureMessage } from '../../../../src/types/protocols-types.js'; 2 | 3 | import { expect } from 'chai'; 4 | import { Message } from '../../../../src/core/message.js'; 5 | import { TestDataGenerator } from '../../../utils/test-data-generator.js'; 6 | import { validateJsonSchema } from '../../../../src/schema-validator.js'; 7 | import { DwnInterfaceName, DwnMethodName } from '../../../../src/index.js'; 8 | 9 | describe('ProtocolsConfigure schema definition', () => { 10 | it('should throw if unknown actor is encountered in action rule', async () => { 11 | const protocolDefinition: ProtocolDefinition = { 12 | protocol : 'email', 13 | published : true, 14 | types : { 15 | email: { 16 | schema : 'email', 17 | dataFormats : ['text/plain'] 18 | } 19 | }, 20 | structure: { 21 | email: { 22 | $actions: [ 23 | { 24 | who : 'unknown', 25 | can : [ProtocolAction.Create] 26 | } 27 | ] 28 | } 29 | } 30 | }; 31 | 32 | const message: ProtocolsConfigureMessage = { 33 | descriptor: { 34 | interface : DwnInterfaceName.Protocols, 35 | method : DwnMethodName.Configure, 36 | messageTimestamp : '2022-10-14T10:20:30.405060Z', 37 | definition : protocolDefinition 38 | }, 39 | authorization: TestDataGenerator.generateAuthorization() 40 | }; 41 | 42 | expect(() => { 43 | Message.validateJsonSchema(message); 44 | }).throws('/$actions/0'); 45 | }); 46 | 47 | describe('rule-set tests', () => { 48 | it('#183 - should throw if required `can` is missing in rule-set', async () => { 49 | const invalidRuleSet1 = { 50 | $actions: [{ 51 | who : 'author', 52 | of : 'thread', 53 | // can: ['read'] // intentionally missing 54 | }] 55 | }; 56 | 57 | const invalidRuleSet2 = { 58 | $actions: [{ 59 | who : 'recipient', 60 | of : 'thread', 61 | // can: ['read'] // intentionally missing 62 | }] 63 | }; 64 | 65 | for (const ruleSet of [invalidRuleSet1, invalidRuleSet2]) { 66 | expect(() => { 67 | validateJsonSchema('ProtocolRuleSet', ruleSet); 68 | }).throws('/$actions/0'); 69 | } 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/anyone-collaborate.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://anyone-collaborate-protocol.xyz", 3 | "published": true, 4 | "types": { 5 | "doc": {} 6 | }, 7 | "structure": { 8 | "doc": { 9 | "$actions": [ 10 | { 11 | "who": "anyone", 12 | "can": [ 13 | "read", 14 | "co-update", 15 | "co-delete" 16 | ] 17 | } 18 | ] 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/author-can.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://author-can-protocol.xyz", 3 | "published": true, 4 | "types": { 5 | "post": {}, 6 | "comment": {} 7 | }, 8 | "structure": { 9 | "post": { 10 | "$actions": [ 11 | { 12 | "who": "anyone", 13 | "can": [ 14 | "create", 15 | "update" 16 | ] 17 | } 18 | ], 19 | "comment": { 20 | "$actions": [ 21 | { 22 | "who": "author", 23 | "of": "post", 24 | "can": [ 25 | "co-update", 26 | "co-delete" 27 | ] 28 | } 29 | ] 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/chat.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://chat-protocol.xyz", 3 | "published": true, 4 | "types": { 5 | "thread": { 6 | "schema": "thread", 7 | "dataFormats": [ 8 | "application/json" 9 | ] 10 | }, 11 | "message": { 12 | "schema": "message", 13 | "dataFormats": [ 14 | "application/json" 15 | ] 16 | } 17 | }, 18 | "structure": { 19 | "thread": { 20 | "$actions": [ 21 | { 22 | "who": "anyone", 23 | "can": [ 24 | "create", 25 | "update" 26 | ] 27 | }, 28 | { 29 | "who": "author", 30 | "of": "thread", 31 | "can": [ 32 | "read" 33 | ] 34 | }, 35 | { 36 | "who": "recipient", 37 | "of": "thread", 38 | "can": [ 39 | "read" 40 | ] 41 | } 42 | ], 43 | "message": { 44 | "$actions": [ 45 | { 46 | "who": "anyone", 47 | "can": [ 48 | "create", 49 | "update" 50 | ] 51 | }, 52 | { 53 | "who": "author", 54 | "of": "thread/message", 55 | "can": [ 56 | "read" 57 | ] 58 | }, 59 | { 60 | "who": "recipient", 61 | "of": "thread/message", 62 | "can": [ 63 | "read" 64 | ] 65 | } 66 | ] 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/contribution-reward.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://contribution-reward-protocol.xyz", 3 | "published": true, 4 | "types": { 5 | "contribution": { 6 | "schema": "contribution", 7 | "dataFormats": [ 8 | "application/json" 9 | ] 10 | }, 11 | "reward": { 12 | "schema": "reward", 13 | "dataFormats": [ 14 | "application/json" 15 | ] 16 | } 17 | }, 18 | "structure": { 19 | "contribution": { 20 | "$actions": [ 21 | { 22 | "who": "anyone", 23 | "can": [ 24 | "create", 25 | "update" 26 | ] 27 | } 28 | ] 29 | }, 30 | "reward": { 31 | "$actions": [ 32 | { 33 | "who": "author", 34 | "of": "contribution", 35 | "can": [ 36 | "read" 37 | ] 38 | } 39 | ] 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/credential-issuance.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://credential-issuance-protocol.xyz", 3 | "published": true, 4 | "types": { 5 | "credentialApplication": { 6 | "schema": "https://identity.foundation/credential-manifest/schemas/credential-application", 7 | "dataFormats": [ 8 | "application/json" 9 | ] 10 | }, 11 | "credentialResponse": { 12 | "schema": "https://identity.foundation/credential-manifest/schemas/credential-response", 13 | "dataFormats": [ 14 | "application/json" 15 | ] 16 | } 17 | }, 18 | "structure": { 19 | "credentialApplication": { 20 | "$actions": [ 21 | { 22 | "who": "anyone", 23 | "can": [ 24 | "create" 25 | ] 26 | } 27 | ], 28 | "credentialResponse": { 29 | "$actions": [ 30 | { 31 | "who": "recipient", 32 | "of": "credentialApplication", 33 | "can": [ 34 | "create" 35 | ] 36 | } 37 | ] 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/dex.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://dex.xyz", 3 | "published": true, 4 | "types": { 5 | "ask": { 6 | "schema": "https://tbd/website/tbdex/ask", 7 | "dataFormats": [ 8 | "application/json" 9 | ] 10 | }, 11 | "offer": { 12 | "schema": "https://tbd/website/tbdex/offer", 13 | "dataFormats": [ 14 | "application/json" 15 | ] 16 | }, 17 | "fulfillment": { 18 | "schema": "https://tbd/website/tbdex/fulfillment", 19 | "dataFormats": [ 20 | "application/json" 21 | ] 22 | } 23 | }, 24 | "structure": { 25 | "ask": { 26 | "$actions": [ 27 | { 28 | "who": "anyone", 29 | "can": [ 30 | "create" 31 | ] 32 | } 33 | ], 34 | "offer": { 35 | "$actions": [ 36 | { 37 | "who": "recipient", 38 | "of": "ask", 39 | "can": [ 40 | "create" 41 | ] 42 | } 43 | ], 44 | "fulfillment": { 45 | "$actions": [ 46 | { 47 | "who": "recipient", 48 | "of": "ask/offer", 49 | "can": [ 50 | "create" 51 | ] 52 | } 53 | ] 54 | } 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/email.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://email-protocol.xyz", 3 | "published": true, 4 | "types": { 5 | "email": { 6 | "schema": "email", 7 | "dataFormats": [ 8 | "text/plain" 9 | ] 10 | } 11 | }, 12 | "structure": { 13 | "email": { 14 | "$actions": [ 15 | { 16 | "who": "anyone", 17 | "can": [ 18 | "create" 19 | ] 20 | }, 21 | { 22 | "who": "author", 23 | "of": "email", 24 | "can": [ 25 | "read" 26 | ] 27 | }, 28 | { 29 | "who": "recipient", 30 | "of": "email", 31 | "can": [ 32 | "read" 33 | ] 34 | } 35 | ], 36 | "email": { 37 | "$actions": [ 38 | { 39 | "who": "anyone", 40 | "can": [ 41 | "create" 42 | ] 43 | }, 44 | { 45 | "who": "author", 46 | "of": "email/email", 47 | "can": [ 48 | "read" 49 | ] 50 | }, 51 | { 52 | "who": "recipient", 53 | "of": "email/email", 54 | "can": [ 55 | "read" 56 | ] 57 | } 58 | ] 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/free-for-all.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://free-for-all-protocol.xyz", 3 | "published": true, 4 | "types": { 5 | "post": { 6 | "schema": "post", 7 | "dataFormats": [ 8 | "application/json" 9 | ] 10 | }, 11 | "attachment": { } 12 | }, 13 | "structure": { 14 | "post": { 15 | "$actions": [ 16 | { 17 | "who": "anyone", 18 | "can": [ 19 | "create", 20 | "update", 21 | "delete", 22 | "prune", 23 | "read", 24 | "co-delete", 25 | "co-prune" 26 | ] 27 | } 28 | ], 29 | "attachment": { 30 | "$actions": [ 31 | { 32 | "who": "anyone", 33 | "can": [ 34 | "create", 35 | "update", 36 | "delete", 37 | "read", 38 | "co-delete" 39 | ] 40 | } 41 | ] 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/friend-role.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://minimal.xyz", 3 | "published": false, 4 | "types": { 5 | "friend": {}, 6 | "admin": {}, 7 | "chat": {}, 8 | "fan": {} 9 | }, 10 | "structure": { 11 | "admin": { 12 | "$role": true 13 | }, 14 | "friend": { 15 | "$role": true 16 | }, 17 | "fan": { 18 | "$role": true 19 | }, 20 | "chat": { 21 | "$actions": [ 22 | { 23 | "role": "fan", 24 | "can": [ 25 | "read", "query", "subscribe" 26 | ] 27 | }, 28 | { 29 | "role": "friend", 30 | "can": [ 31 | "create", 32 | "update", 33 | "read", 34 | "query", 35 | "subscribe" 36 | ] 37 | }, 38 | { 39 | "role": "admin", 40 | "can": [ 41 | "co-update", 42 | "co-delete" 43 | ] 44 | } 45 | ] 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/message.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://message-protocol.xyz", 3 | "published": true, 4 | "types": { 5 | "message": { 6 | "schema": "http://message.me", 7 | "dataFormats": [ 8 | "text/plain" 9 | ] 10 | }, 11 | "attachment": { } 12 | }, 13 | "structure": { 14 | "message": { 15 | "$actions": [ 16 | { 17 | "who": "anyone", 18 | "can": [ 19 | "create", 20 | "update" 21 | ] 22 | } 23 | ], 24 | "attachment": { 25 | "$actions": [ 26 | { 27 | "who": "anyone", 28 | "can": [ 29 | "create", 30 | "update" 31 | ] 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/minimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://minimal.xyz", 3 | "published": false, 4 | "types": { 5 | "foo": {} 6 | }, 7 | "structure": { 8 | "foo": {} 9 | } 10 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/nested.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://nested.xyz", 3 | "published": false, 4 | "types": { 5 | "foo": { 6 | "schema": "foo", 7 | "dataFormats": [ 8 | "text/plain" 9 | ] 10 | }, 11 | "bar": { 12 | "schema": "bar", 13 | "dataFormats": [ 14 | "text/plain" 15 | ] 16 | }, 17 | "baz": { 18 | "schema": "baz", 19 | "dataFormats": [ 20 | "text/plain" 21 | ] 22 | } 23 | }, 24 | "structure": { 25 | "foo": { 26 | "bar": { 27 | "baz" : {} 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/post-comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://post-comment-protocol.xyz", 3 | "published": true, 4 | "types": { 5 | "post": { 6 | "schema": "post", 7 | "dataFormats": [ 8 | "application/json" 9 | ] 10 | }, 11 | "comment": { 12 | "schema": "comment", 13 | "dataFormats": [ 14 | "application/json" 15 | ] 16 | } 17 | }, 18 | "structure": { 19 | "post": { 20 | "$actions": [ 21 | { 22 | "who": "anyone", 23 | "can": [ 24 | "read" 25 | ] 26 | } 27 | ], 28 | "comment": { 29 | "$actions": [ 30 | { 31 | "who": "anyone", 32 | "can": [ 33 | "read", 34 | "create", 35 | "update" 36 | ] 37 | }, 38 | { 39 | "who": "author", 40 | "of": "post", 41 | "can": [ 42 | "co-delete" 43 | ] 44 | } 45 | ] 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/private-protocol.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://private-protocol.xyz", 3 | "published": false, 4 | "types": { 5 | "privateNote": { 6 | "schema": "private-note", 7 | "dataFormats": [ 8 | "text/plain" 9 | ] 10 | } 11 | }, 12 | "structure": { 13 | "privateNote": {} 14 | } 15 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/recipient-can.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://recipient-can-protocol.xyz", 3 | "published": true, 4 | "types": { 5 | "post": {}, 6 | "tag": {} 7 | }, 8 | "structure": { 9 | "post": { 10 | "$actions": [ 11 | { 12 | "who": "recipient", 13 | "can": [ 14 | "co-update", 15 | "co-delete" 16 | ] 17 | } 18 | ], 19 | "tag": { 20 | "$actions": [ 21 | { 22 | "who": "recipient", 23 | "of": "post", 24 | "can": [ 25 | "co-update", 26 | "co-delete" 27 | ] 28 | } 29 | ] 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/social-media.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://social-media.xyz", 3 | "published": true, 4 | "types": { 5 | "message": { 6 | "schema": "messageSchema", 7 | "dataFormats": [ 8 | "text/plain" 9 | ] 10 | }, 11 | "reply": { 12 | "schema": "replySchema", 13 | "dataFormats": [ 14 | "text/plain" 15 | ] 16 | }, 17 | "image": { 18 | "schema": "imageSchema", 19 | "dataFormats": [ 20 | "image/jpeg", 21 | "image/gif", 22 | "image/png" 23 | ] 24 | }, 25 | "caption": { 26 | "schema": "captionSchema", 27 | "dataFormats": [ 28 | "text/plain" 29 | ] 30 | } 31 | }, 32 | "structure": { 33 | "message": { 34 | "$actions": [ 35 | { 36 | "who": "anyone", 37 | "can": [ 38 | "create", 39 | "update" 40 | ] 41 | } 42 | ], 43 | "reply": { 44 | "$actions": [ 45 | { 46 | "who": "recipient", 47 | "of": "message", 48 | "can": [ 49 | "create", 50 | "update" 51 | ] 52 | } 53 | ] 54 | } 55 | }, 56 | "image": { 57 | "$actions": [ 58 | { 59 | "who": "anyone", 60 | "can": [ 61 | "read", 62 | "create", 63 | "update" 64 | ] 65 | } 66 | ], 67 | "caption": { 68 | "$actions": [ 69 | { 70 | "who": "anyone", 71 | "can": [ 72 | "read" 73 | ] 74 | }, 75 | { 76 | "who": "author", 77 | "of": "image", 78 | "can": [ 79 | "create", 80 | "update" 81 | ] 82 | } 83 | ] 84 | }, 85 | "reply": { 86 | "$actions": [ 87 | { 88 | "who": "author", 89 | "of": "image", 90 | "can": [ 91 | "read" 92 | ] 93 | }, 94 | { 95 | "who": "recipient", 96 | "of": "image", 97 | "can": [ 98 | "create", 99 | "update" 100 | ] 101 | } 102 | ] 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /tests/vectors/protocol-definitions/thread-role.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol": "http://thread-role.xyz", 3 | "published": true, 4 | "types": { 5 | "thread": {}, 6 | "participant": {}, 7 | "admin": {}, 8 | "globalAdmin": {}, 9 | "chat": {} 10 | }, 11 | "structure": { 12 | "globalAdmin": { 13 | "$role": true 14 | }, 15 | "thread": { 16 | "$actions": [ 17 | { 18 | "role": "thread/participant", 19 | "can": [ 20 | "read", "query", "subscribe" 21 | ] 22 | } 23 | ], 24 | "admin": { 25 | "$role": true 26 | }, 27 | "participant": { 28 | "$role": true, 29 | "$actions": [ 30 | { 31 | "role": "thread/participant", 32 | "can": [ 33 | "read", 34 | "query", 35 | "subscribe", 36 | "create" 37 | ] 38 | } 39 | ] 40 | }, 41 | "chat": { 42 | "$actions": [ 43 | { 44 | "role": "thread/participant", 45 | "can": [ 46 | "create", 47 | "update", 48 | "read", 49 | "query", 50 | "subscribe" 51 | ] 52 | }, 53 | { 54 | "role": "thread/admin", 55 | "can": [ 56 | "co-update", 57 | "co-delete" 58 | ] 59 | }, 60 | { 61 | "role": "globalAdmin", 62 | "can": [ 63 | "co-delete" 64 | ] 65 | } 66 | ] 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "noImplicitAny": true, 5 | "lib": [ 6 | "DOM", 7 | "ES6" 8 | ], 9 | "allowJs": true, 10 | "target": "ES6", 11 | "module": "NodeNext", 12 | "declaration": true, 13 | "declarationMap": true, 14 | "declarationDir": "dist/types", 15 | "outDir": "dist/esm", 16 | "sourceMap": true, 17 | // `NodeNext` will throw compilation errors if relative import paths are missing file extension 18 | // reference: https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#ecmascript-module-support-in-node-js 19 | "moduleResolution": "NodeNext", 20 | // allows us to import json files 21 | "resolveJsonModule": true, 22 | // required otherwise `ms` lib doesn't import 23 | "esModuleInterop": true 24 | }, 25 | "typedocOptions": { 26 | "entryPoints": [ 27 | "src/index.ts" 28 | ], 29 | "out": "documentation", 30 | "readme": "none", 31 | "excludePrivate": "true", 32 | "excludeProtected": "true", 33 | "excludeExternals": "true", 34 | "name": "DWN-SDK Documentation", 35 | "includeVersion": "true", 36 | "validation": { 37 | "notDocumented": false, 38 | "notExported": false 39 | } 40 | }, 41 | "include": [ 42 | "./src/index.ts", 43 | "tests" // building tests also so we can run tests directly without the need for ts-node, ts-mocha 44 | ], 45 | "exclude": [ 46 | "node_modules", 47 | "dist" 48 | ] 49 | } --------------------------------------------------------------------------------