diff --git a/.github/workflows/content-types.yml b/.github/workflows/content-types.yml new file mode 100644 index 000000000..9f5ded514 --- /dev/null +++ b/.github/workflows/content-types.yml @@ -0,0 +1,94 @@ +name: Content types + +on: + push: + branches: + - main + + pull_request: + paths: + - "content-types/**" + - ".github/workflows/content-types.yml" + - "dev/**" + - ".node-version" + - ".nvmrc" + - ".yarnrc.yml" + - "turbo.json" + - "yarn.lock" + +jobs: + typecheck: + name: Typecheck + runs-on: warp-ubuntu-latest-x64-8x + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "yarn" + env: + SKIP_YARN_COREPACK_CHECK: "1" + - name: Enable corepack + run: corepack enable + - name: Install dependencies + run: yarn + - name: Typecheck + run: yarn turbo run typecheck --filter='./content-types/*' + + lint: + name: Lint + runs-on: warp-ubuntu-latest-x64-8x + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "yarn" + env: + SKIP_YARN_COREPACK_CHECK: "1" + - name: Enable corepack + run: corepack enable + - name: Install dependencies + run: yarn + - name: Lint + run: yarn turbo run lint --filter='./content-types/*' + + test: + name: Test + runs-on: warp-ubuntu-latest-x64-8x + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "yarn" + env: + SKIP_YARN_COREPACK_CHECK: "1" + - name: Enable corepack + run: corepack enable + - name: Install dependencies + run: yarn + - name: Start dev environment + run: ./dev/up + - name: Sleep for 5 seconds + run: sleep 5s + - name: Run tests + run: yarn turbo run test --filter='./content-types/*' + + build: + name: Build + runs-on: warp-ubuntu-latest-x64-8x + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "yarn" + env: + SKIP_YARN_COREPACK_CHECK: "1" + - name: Enable corepack + run: corepack enable + - name: Install dependencies + run: yarn + - name: Build + run: yarn turbo run build --filter='./content-types/*' diff --git a/.github/workflows/js-sdk.yml b/.github/workflows/js-sdk.yml index 8d9575030..24240811f 100644 --- a/.github/workflows/js-sdk.yml +++ b/.github/workflows/js-sdk.yml @@ -7,16 +7,14 @@ on: pull_request: paths: - - 'packages/js-sdk/**' - - '.github/workflows/js-sdk.yml' - - 'dev/**' - - '.node-version' - - '.nvmrc' - - '.prettierignore' - - '.prettierrc.cjs' - - '.yarnrc.yml' - - 'turbo.json' - - 'yarn.lock' + - "packages/js-sdk/**" + - ".github/workflows/js-sdk.yml" + - "dev/**" + - ".node-version" + - ".nvmrc" + - ".yarnrc.yml" + - "turbo.json" + - "yarn.lock" jobs: typecheck: @@ -26,18 +24,16 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: ".nvmrc" + cache: "yarn" env: - SKIP_YARN_COREPACK_CHECK: '1' + SKIP_YARN_COREPACK_CHECK: "1" - name: Enable corepack run: corepack enable - name: Install dependencies run: yarn - name: Typecheck - run: | - cd packages/js-sdk - yarn typecheck + run: yarn turbo run typecheck --filter='./packages/js-sdk' lint: name: Lint @@ -46,38 +42,16 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: ".nvmrc" + cache: "yarn" env: - SKIP_YARN_COREPACK_CHECK: '1' + SKIP_YARN_COREPACK_CHECK: "1" - name: Enable corepack run: corepack enable - name: Install dependencies run: yarn - name: Lint - run: | - cd packages/js-sdk - yarn lint - - prettier: - name: Prettier - runs-on: warp-ubuntu-latest-x64-8x - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: 'yarn' - env: - SKIP_YARN_COREPACK_CHECK: '1' - - name: Enable corepack - run: corepack enable - - name: Install dependencies - run: yarn - - name: Format check - run: | - cd packages/js-sdk - yarn format:check + run: yarn turbo run lint --filter='./packages/js-sdk' test: name: Test @@ -86,10 +60,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: ".nvmrc" + cache: "yarn" env: - SKIP_YARN_COREPACK_CHECK: '1' + SKIP_YARN_COREPACK_CHECK: "1" - name: Enable corepack run: corepack enable - name: Install dependencies @@ -98,20 +72,8 @@ jobs: run: ./dev/up - name: Sleep for 5 seconds run: sleep 5s - - name: Run node tests - run: | - cd packages/js-sdk - yarn test:node - env: - NODE_OPTIONS: '-r dd-trace/ci/init' - DD_ENV: ci:node - DD_SERVICE: xmtp-js - DD_CIVISIBILITY_AGENTLESS_ENABLED: 'true' - DD_API_KEY: ${{ secrets.DD_API_KEY }} - - name: Run browser tests - run: | - cd packages/js-sdk - yarn test:browser + - name: Run tests + run: yarn turbo run test --filter='./packages/js-sdk' build: name: Build @@ -120,15 +82,13 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: ".nvmrc" + cache: "yarn" env: - SKIP_YARN_COREPACK_CHECK: '1' + SKIP_YARN_COREPACK_CHECK: "1" - name: Enable corepack run: corepack enable - name: Install dependencies run: yarn - name: Build - run: | - cd packages/js-sdk - yarn build + run: yarn turbo run build --filter='./packages/js-sdk' diff --git a/.github/workflows/mls-client.yml b/.github/workflows/mls-client.yml index 57fbd410f..9c2619185 100644 --- a/.github/workflows/mls-client.yml +++ b/.github/workflows/mls-client.yml @@ -7,16 +7,14 @@ on: pull_request: paths: - - 'packages/mls-client/**' - - '.github/workflows/mls-client.yml' - - 'dev/**' - - '.node-version' - - '.nvmrc' - - '.prettierignore' - - '.prettierrc.cjs' - - '.yarnrc.yml' - - 'turbo.json' - - 'yarn.lock' + - "packages/mls-client/**" + - ".github/workflows/mls-client.yml" + - "dev/**" + - ".node-version" + - ".nvmrc" + - ".yarnrc.yml" + - "turbo.json" + - "yarn.lock" jobs: typecheck: @@ -26,18 +24,16 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: ".nvmrc" + cache: "yarn" env: - SKIP_YARN_COREPACK_CHECK: '1' + SKIP_YARN_COREPACK_CHECK: "1" - name: Enable corepack run: corepack enable - name: Install dependencies run: yarn - name: Typecheck - run: | - cd packages/mls-client - yarn typecheck + run: yarn turbo run typecheck --filter='./packages/mls-client' lint: name: Lint @@ -46,38 +42,16 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: ".nvmrc" + cache: "yarn" env: - SKIP_YARN_COREPACK_CHECK: '1' + SKIP_YARN_COREPACK_CHECK: "1" - name: Enable corepack run: corepack enable - name: Install dependencies run: yarn - name: Lint - run: | - cd packages/mls-client - yarn lint - - prettier: - name: Prettier - runs-on: warp-ubuntu-latest-x64-8x - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: 'yarn' - env: - SKIP_YARN_COREPACK_CHECK: '1' - - name: Enable corepack - run: corepack enable - - name: Install dependencies - run: yarn - - name: Format check - run: | - cd packages/mls-client - yarn format:check + run: yarn turbo run lint --filter='./packages/mls-client' test: name: Test @@ -86,10 +60,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: ".nvmrc" + cache: "yarn" env: - SKIP_YARN_COREPACK_CHECK: '1' + SKIP_YARN_COREPACK_CHECK: "1" - name: Enable corepack run: corepack enable - name: Install dependencies @@ -99,9 +73,7 @@ jobs: - name: Sleep for 5 seconds run: sleep 5s - name: Run tests - run: | - cd packages/mls-client - yarn test + run: yarn turbo run test --filter='./packages/mls-client' build: name: Build @@ -110,15 +82,13 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: ".nvmrc" + cache: "yarn" env: - SKIP_YARN_COREPACK_CHECK: '1' + SKIP_YARN_COREPACK_CHECK: "1" - name: Enable corepack run: corepack enable - name: Install dependencies run: yarn - name: Build - run: | - cd packages/mls-client - yarn build + run: yarn turbo run build --filter='./packages/mls-client' diff --git a/.github/workflows/noop.yml b/.github/workflows/noop.yml index 4121c520c..4e0d4892a 100644 --- a/.github/workflows/noop.yml +++ b/.github/workflows/noop.yml @@ -7,20 +7,21 @@ on: pull_request: paths: - - '.changeset/config.json' - - '.github/**' - - '!.github/workflows/js-sdk.yml' - - '!.github/workflows/mls-client.yml' - - '.vscode/**' - - '.yarn/**' - - '*' - - '!.node-version' - - '!.nvmrc' - - '!.prettierignore' - - '!.prettierrc.cjs' - - '!.yarnrc.yml' - - '!turbo.json' - - '!yarn.lock' + - ".changeset/config.json" + - ".github/**" + - "!.github/workflows/js-sdk.yml" + - "!.github/workflows/mls-client.yml" + - "!.github/workflows/content-types.yml" + - ".vscode/**" + - ".yarn/**" + - "*" + - "!.node-version" + - "!.nvmrc" + - "!.prettierignore" + - "!.prettierrc.cjs" + - "!.yarnrc.yml" + - "!turbo.json" + - "!yarn.lock" jobs: typecheck: @@ -35,24 +36,6 @@ jobs: steps: - run: echo "Nothing to lint" - prettier: - name: Prettier - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: 'yarn' - env: - SKIP_YARN_COREPACK_CHECK: '1' - - name: Enable corepack - run: corepack enable - - name: Install dependencies - run: yarn - - name: Format check - run: yarn prettier -c . - test: name: Test runs-on: ubuntu-latest diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml new file mode 100644 index 000000000..9b3575ff7 --- /dev/null +++ b/.github/workflows/prettier.yml @@ -0,0 +1,27 @@ +name: Format check + +on: + push: + branches: + - main + + pull_request: + +jobs: + prettier: + name: Prettier + runs-on: warp-ubuntu-latest-x64-8x + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "yarn" + env: + SKIP_YARN_COREPACK_CHECK: "1" + - name: Enable corepack + run: corepack enable + - name: Install dependencies + run: yarn + - name: Format check + run: yarn format:check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d34dc08bb..e4d2d763b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,10 +26,10 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'yarn' + node-version-file: ".nvmrc" + cache: "yarn" env: - SKIP_YARN_COREPACK_CHECK: '1' + SKIP_YARN_COREPACK_CHECK: "1" - name: Enable corepack run: corepack enable - name: Update npm to latest @@ -41,8 +41,8 @@ jobs: - name: Publish uses: changesets/action@v1 with: - title: 'release: version packages' - commit: 'release: version packages' + title: "release: version packages" + commit: "release: version packages" publish: yarn publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.prettierrc.cjs b/.prettierrc.cjs index eeedeb8c0..d20f3fed0 100644 --- a/.prettierrc.cjs +++ b/.prettierrc.cjs @@ -1,20 +1,31 @@ module.exports = { - semi: false, - singleQuote: true, - trailingComma: 'es5', - arrowParens: 'always', + arrowParens: "always", + bracketSameLine: true, + bracketSpacing: true, + embeddedLanguageFormatting: "auto", + endOfLine: "lf", + htmlWhitespaceSensitivity: "css", + jsxSingleQuote: false, printWidth: 80, + proseWrap: "preserve", + quoteProps: "as-needed", + semi: true, + singleAttributePerLine: false, + singleQuote: false, + tabWidth: 2, + trailingComma: "all", + useTabs: false, plugins: [ - 'prettier-plugin-packagejson', - '@ianvs/prettier-plugin-sort-imports', + "prettier-plugin-packagejson", + "@ianvs/prettier-plugin-sort-imports", ], importOrder: [ - '', - '', - '^@(/.*)$', - '^@test(/.*)$', - '^@bench(/.*)$', - '^[.]', + "", + "", + "^@(/.*)$", + "^@test(/.*)$", + "^@bench(/.*)$", + "^[.]", ], - importOrderTypeScriptVersion: '5.4.2', -} + importOrderTypeScriptVersion: "5.6.3", +}; diff --git a/README.md b/README.md index b7a114db9..af06395af 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,25 @@ -# XMTP JavaScript SDKs +# XMTP TypeScript -This is the official repository for XMTP JavaScript SDKs, powered by [Turborepo](https://turbo.build/repo). +This is the official repository for XMTP TypeScript SDKs and content types, powered by [Turborepo](https://turbo.build/repo). All content types can be used with these SDKs. -To learn more about the contents of this repository, see this README and the READMEs provided for [packages](https://github.com/xmtp/xmtp-web/tree/main/packages). +To learn more about the contents of this repository, see this README and the READMEs provided in workspace directories. ## What's inside? -### Packages +### SDKs - [`js-sdk`](https://github.com/xmtp/xmtp-js/blob/main/packages/js-sdk): XMTP JS client SDK for Node and the browser +### Content types + +- [`content-type-primitives`](content-types/content-type-primitives): Primitives for building custom XMTP content types +- [`content-type-reaction`](content-types/content-type-reaction): Content type for reactions to messages +- [`content-type-read-receipt`](content-types/content-type-read-receipt): Content type for read receipts for messages +- [`content-type-remote-attachment`](content-types/content-type-remote-attachment): Content type for sending file attachments that are stored off-network +- [`content-type-reply`](content-types/content-type-reply): Content type for direct replies to messages +- [`content-type-text`](content-types/content-type-text): Content type for plain text messages +- [`content-type-transaction-reference`](content-types/content-type-transaction-reference): Content type for on-chain transaction references + ## Contributing See our [contribution guide](./CONTRIBUTING.md) to learn more about contributing to this project. diff --git a/content-types/content-type-primitives/.eslintrc.cjs b/content-types/content-type-primitives/.eslintrc.cjs new file mode 100644 index 000000000..d46b01d75 --- /dev/null +++ b/content-types/content-type-primitives/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ["custom"], + parserOptions: { + project: "./tsconfig.eslint.json", + }, +}; diff --git a/content-types/content-type-primitives/CHANGELOG.md b/content-types/content-type-primitives/CHANGELOG.md new file mode 100644 index 000000000..c3ca5bf8f --- /dev/null +++ b/content-types/content-type-primitives/CHANGELOG.md @@ -0,0 +1,11 @@ +# @xmtp/content-type-primitives + +## 1.0.1 + +### Patch Changes + +- [#71](https://github.com/xmtp/xmtp-js-content-types/pull/71) [`52bf31e`](https://github.com/xmtp/xmtp-js-content-types/commit/52bf31ec9d9b78da321727745d0a37bfa617362a) - Add more primitive types + +## 1.0.0 + +Initial release diff --git a/content-types/content-type-primitives/LICENSE b/content-types/content-type-primitives/LICENSE new file mode 100644 index 000000000..ae6695abd --- /dev/null +++ b/content-types/content-type-primitives/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 XMTP (xmtp.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/content-types/content-type-primitives/README.md b/content-types/content-type-primitives/README.md new file mode 100644 index 000000000..b1f0b6478 --- /dev/null +++ b/content-types/content-type-primitives/README.md @@ -0,0 +1,31 @@ +# Content type primitives + +This package provides primitives for building custom XMTP content types. + +## Install the package + +```bash +# npm +npm i @xmtp/content-type-primitives + +# yarn +yarn add @xmtp/content-type-primitives + +# pnpm +pnpm i @xmtp/content-type-primitives +``` + +## Developing + +Run `yarn dev` to build the content type primitives and watch for changes, which will trigger a rebuild. + +## Useful commands + +- `yarn build`: Builds the content type primitives +- `yarn clean`: Removes `node_modules`, `dist`, and `.turbo` folders +- `yarn dev`: Builds the content type and watches for changes, which will trigger a rebuild +- `yarn format`: Runs Prettier format and write changes +- `yarn format:check`: Runs Prettier format check +- `yarn lint`: Runs ESLint +- `yarn test`: Runs all unit tests +- `yarn typecheck`: Runs `tsc` diff --git a/content-types/content-type-primitives/package.json b/content-types/content-type-primitives/package.json new file mode 100644 index 000000000..0e1c3493e --- /dev/null +++ b/content-types/content-type-primitives/package.json @@ -0,0 +1,89 @@ +{ + "name": "@xmtp/content-type-primitives", + "version": "1.0.1", + "description": "Primitives for building custom XMTP content types", + "keywords": [ + "xmtp", + "messaging", + "web3", + "js", + "ts", + "javascript", + "typescript", + "content-types" + ], + "homepage": "https://github.com/xmtp/xmtp-js", + "bugs": { + "url": "https://github.com/xmtp/xmtp-js/issues" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/xmtp/xmtp-js.git", + "directory": "content-types/content-type-primitives" + }, + "license": "MIT", + "author": "XMTP Labs ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "browser": "./dist/browser/index.js", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "main": "dist/index.cjs", + "module": "dist/index.js", + "browser": "dist/browser/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn clean:dist && yarn rollup -c", + "clean": "yarn clean:dist && rimraf .turbo node_modules", + "clean:dist": "rimraf dist", + "dev": "yarn clean:dist && yarn rollup -c --watch", + "lint": "eslint . --ignore-path ../../.gitignore", + "test": "yarn test:node && yarn test:jsdom", + "test:jsdom": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment happy-dom", + "test:node": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment node", + "typecheck": "tsc --noEmit" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome versions", + "last 3 firefox versions", + "last 3 safari versions" + ] + }, + "dependencies": { + "@xmtp/proto": "^3.61.1" + }, + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.0", + "@types/node": "^18.19.22", + "eslint": "^8.57.0", + "eslint-config-custom": "workspace:*", + "happy-dom": "^13.7.3", + "rimraf": "^6.0.1", + "rollup": "^4.24.0", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-filesize": "^10.0.0", + "typescript": "^5.6.3", + "vite": "^5.1.6", + "vitest": "^2.1.2" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "registry": "https://registry.npmjs.org/" + } +} diff --git a/content-types/content-type-primitives/rollup.config.js b/content-types/content-type-primitives/rollup.config.js new file mode 100644 index 000000000..d56c26380 --- /dev/null +++ b/content-types/content-type-primitives/rollup.config.js @@ -0,0 +1,58 @@ +import terser from "@rollup/plugin-terser"; +import typescript from "@rollup/plugin-typescript"; +import { defineConfig } from "rollup"; +import { dts } from "rollup-plugin-dts"; +import filesize from "rollup-plugin-filesize"; + +const plugins = [ + typescript({ + declaration: false, + declarationMap: false, + }), + filesize({ + showMinifiedSize: false, + }), +]; + +const external = ["@xmtp/proto"]; + +export default defineConfig([ + { + input: "src/index.ts", + output: { + file: "dist/index.js", + format: "es", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/browser/index.js", + format: "es", + sourcemap: true, + }, + plugins: [...plugins, terser()], + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.cjs", + format: "cjs", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.d.ts", + format: "es", + }, + plugins: [dts()], + }, +]); diff --git a/content-types/content-type-primitives/src/index.test.ts b/content-types/content-type-primitives/src/index.test.ts new file mode 100644 index 000000000..1f6c9f998 --- /dev/null +++ b/content-types/content-type-primitives/src/index.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { ContentTypeId } from "."; + +describe("ContentTypeId", () => { + it("creates a new content type", () => { + const contentType = new ContentTypeId({ + authorityId: "foo", + typeId: "bar", + versionMajor: 1, + versionMinor: 0, + }); + expect(contentType.authorityId).toEqual("foo"); + expect(contentType.typeId).toEqual("bar"); + expect(contentType.versionMajor).toEqual(1); + expect(contentType.versionMinor).toEqual(0); + }); + + it("creates a string from a content type", () => { + const contentType = new ContentTypeId({ + authorityId: "foo", + typeId: "bar", + versionMajor: 1, + versionMinor: 0, + }); + expect(contentType.toString()).toEqual("foo/bar:1.0"); + }); + + it("creates a content type from a string", () => { + const contentType = ContentTypeId.fromString("foo/bar:1.0"); + expect(contentType.authorityId).toEqual("foo"); + expect(contentType.typeId).toEqual("bar"); + expect(contentType.versionMajor).toEqual(1); + expect(contentType.versionMinor).toEqual(0); + }); + + it("compares two content types", () => { + const contentType1 = new ContentTypeId({ + authorityId: "foo", + typeId: "bar", + versionMajor: 1, + versionMinor: 0, + }); + const contentType2 = new ContentTypeId({ + authorityId: "baz", + typeId: "qux", + versionMajor: 1, + versionMinor: 0, + }); + expect(contentType1.sameAs(contentType2)).toBe(false); + expect(contentType1.sameAs(contentType1)).toBe(true); + }); +}); diff --git a/content-types/content-type-primitives/src/index.ts b/content-types/content-type-primitives/src/index.ts new file mode 100644 index 000000000..2825c2768 --- /dev/null +++ b/content-types/content-type-primitives/src/index.ts @@ -0,0 +1,64 @@ +import type { content } from "@xmtp/proto"; + +export class ContentTypeId { + authorityId: string; + + typeId: string; + + versionMajor: number; + + versionMinor: number; + + constructor(obj: content.ContentTypeId) { + this.authorityId = obj.authorityId; + this.typeId = obj.typeId; + this.versionMajor = obj.versionMajor; + this.versionMinor = obj.versionMinor; + } + + toString(): string { + return `${this.authorityId}/${this.typeId}:${this.versionMajor}.${this.versionMinor}`; + } + + static fromString(contentTypeString: string): ContentTypeId { + const [idString, versionString] = contentTypeString.split(":"); + const [authorityId, typeId] = idString.split("/"); + const [major, minor] = versionString.split("."); + return new ContentTypeId({ + authorityId, + typeId, + versionMajor: Number(major), + versionMinor: Number(minor), + }); + } + + sameAs(id: ContentTypeId): boolean { + return this.authorityId === id.authorityId && this.typeId === id.typeId; + } +} + +export type EncodedContent> = { + type: ContentTypeId; + parameters: Parameters; + fallback?: string; + compression?: number; + content: Uint8Array; +}; + +export type ContentCodec = { + contentType: ContentTypeId; + encode(content: T, registry: CodecRegistry): EncodedContent; + decode(content: EncodedContent, registry: CodecRegistry): T; + fallback(content: T): string | undefined; + shouldPush: (content: T) => boolean; +}; + +/** + * An interface implemented for accessing codecs by content type. + * @deprecated + */ +export interface CodecRegistry { + codecFor(contentType: ContentTypeId): ContentCodec | undefined; +} + +export type CodecMap = Map>; diff --git a/content-types/content-type-primitives/tsconfig.eslint.json b/content-types/content-type-primitives/tsconfig.eslint.json new file mode 100644 index 000000000..5ee5f6f56 --- /dev/null +++ b/content-types/content-type-primitives/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": [".", ".eslintrc.cjs", "rollup.config.js"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-primitives/tsconfig.json b/content-types/content-type-primitives/tsconfig.json new file mode 100644 index 000000000..69f454187 --- /dev/null +++ b/content-types/content-type-primitives/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/build.json", + "include": ["src"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-reaction/.eslintrc.cjs b/content-types/content-type-reaction/.eslintrc.cjs new file mode 100644 index 000000000..d46b01d75 --- /dev/null +++ b/content-types/content-type-reaction/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ["custom"], + parserOptions: { + project: "./tsconfig.eslint.json", + }, +}; diff --git a/content-types/content-type-reaction/CHANGELOG.md b/content-types/content-type-reaction/CHANGELOG.md new file mode 100644 index 000000000..6311979a1 --- /dev/null +++ b/content-types/content-type-reaction/CHANGELOG.md @@ -0,0 +1,87 @@ +# @xmtp/content-type-reaction + +## 1.1.9 + +### Patch Changes + +- [#75](https://github.com/xmtp/xmtp-js-content-types/pull/75) [`da0bd85`](https://github.com/xmtp/xmtp-js-content-types/commit/da0bd8578d5f5032b221e25f02e8492b27929d6c) + - Use primitives package for types + +## 1.1.8 + +### Patch Changes + +- [#68](https://github.com/xmtp/xmtp-js-content-types/pull/68) [`8896b33`](https://github.com/xmtp/xmtp-js-content-types/commit/8896b33501b2860d68ea8be5e33a9cca5dd9315c) + - Add optional referenceInboxId + +## 1.1.7 + +### Patch Changes + +- [#65](https://github.com/xmtp/xmtp-js-content-types/pull/65) [`c4d43dc`](https://github.com/xmtp/xmtp-js-content-types/commit/c4d43dc948231de5c7f730e06f0931076de0673b) + - Add `shouldPush` to all content codecs + +## 1.1.6 + +### Patch Changes + +- [#60](https://github.com/xmtp/xmtp-js-content-types/pull/60) [`5b9310a`](https://github.com/xmtp/xmtp-js-content-types/commit/5b9310ac89fd23e5cfd74903894073b6ef8af7c3) + - Upgraded JS SDK to `11.3.12` + +## 1.1.5 + +### Patch Changes + +- [#54](https://github.com/xmtp/xmtp-js-content-types/pull/54) [`718cb9f`](https://github.com/xmtp/xmtp-js-content-types/commit/718cb9fec51f74bf2402f3f22160687cae35dda8) + - Updated Turbo config to remove `generate:types` command and instead rely on `build` + - Removed all `generate:types` commands from `package.json` files + - Updated shared ESLint config and local ESLint configs + - Updated `include` field of `tsconfig.json` and `tsconfig.eslint.json` files + - Replaced `tsup` with `rollup` + +## 1.1.4 + +### Patch Changes + +- [#51](https://github.com/xmtp/xmtp-js-content-types/pull/51) [`aeb6db7`](https://github.com/xmtp/xmtp-js-content-types/commit/aeb6db73a63409a33c7d3d3431e33682b0ce4c4d) + - Only publish files in the `/dist` directory + +## 1.1.3 + +### Patch Changes + +- Upgraded `@xmtp/proto` package +- Upgraded `@xmtp/xmtp-js` package + +## 1.1.2 + +### Patch Changes + +- Upgrade to JS SDK v11 +- Update client initialization for tests to use `codecs` option for proper types + +## 1.1.1 + +### Patch Changes + +- [#30](https://github.com/xmtp/xmtp-js-content-types/pull/30) [`41fe976`](https://github.com/xmtp/xmtp-js-content-types/commit/41fe976c009af8daa415e29b6820166675a8c77b) + - fix: update the copy for the default fallbacks + +## 1.1.0 + +### Minor Changes + +- [#25](https://github.com/xmtp/xmtp-js-content-types/pull/25) [`3c531b7`](https://github.com/xmtp/xmtp-js-content-types/commit/3c531b7dc057a9f7907a9289a0a35f0da3a48e44) + - Add dummy fallback field to all content types + +## 1.0.2 + +### Patch Changes + +- Normalize Reaction content type encoding, support legacy format + +## 1.0.1 + +### Patch Changes + +- Add schema to Reaction content type diff --git a/content-types/content-type-reaction/LICENSE b/content-types/content-type-reaction/LICENSE new file mode 100644 index 000000000..ae6695abd --- /dev/null +++ b/content-types/content-type-reaction/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 XMTP (xmtp.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/content-types/content-type-reaction/README.md b/content-types/content-type-reaction/README.md new file mode 100644 index 000000000..5e7cd4dc4 --- /dev/null +++ b/content-types/content-type-reaction/README.md @@ -0,0 +1,98 @@ +# Reaction content type + +![Status](https://img.shields.io/badge/Content_type_status-Standards--track-yellow) ![Status](https://img.shields.io/badge/Reference_implementation_status-Beta-yellow) + +This package provides an XMTP content type to support reactions to messages. + +> **Open for feedback** +> You are welcome to provide feedback on this implementation by commenting on the [Proposal for emoji reactions content type](https://github.com/orgs/xmtp/discussions/36). + +## What’s a reaction? + +A reaction is a quick and often emoji-based way to respond to a message. Reactions are usually limited to a predefined set of emojis or symbols provided by the messaging app. + +## Why reactions? + +Providing message reactions in your app enables users to easily express a general sentiment or emotion toward a message. It also provides a handy way to acknowledge a message or show a particular emotional reaction without engaging in a detailed response. + +## Install the package + +```bash +# npm +npm i @xmtp/content-type-reaction + +# yarn +yarn add @xmtp/content-type-reaction + +# pnpm +pnpm i @xmtp/content-type-reaction +``` + +## Create a reaction + +With XMTP, reactions are represented as objects with the following keys: + +- `reference`: The message ID for the message that is being reacted to +- `action`: The action of the reaction (`added` or `removed`) +- `content`: A string representation of a reaction (e.g. `smile`) to be interpreted by clients + +```tsx +const reaction: Reaction = { + reference: someMessageID, + action: "added", + content: "smile", +}; +``` + +## Send a reaction + +Now that you have a reaction, you can send it: + +```tsx +await conversation.messages.send(reaction, { + contentType: ContentTypeReaction, +}); +``` + +> **Note** +> `contentFallback` text is provided by the codec and gives clients that _don't_ support a content type the option to display some useful context. For cases where clients *do* support the content type, they can use the content fallback as alt text for accessibility purposes. + +## Receive a reaction + +Now that you can send a reaction, you need a way to receive a reaction. For example: + +```tsx +// Assume `loadLastMessage` is a thing you have +const message: DecodedMessage = await loadLastMessage(); + +if (!message.contentType.sameAs(ContentTypeReaction)) { + // We do not have a reaction. A topic for another blog post. + return; +} + +// We've got a reaction. +const reaction: Reaction = message.content; +``` + +## Display the reaction + +Generally, reactions should be interpreted as emoji. So, `smile` would translate to :smile: in UI clients. That being said, how you ultimately choose to render a reaction in your app is up to you. + +## Developing + +Run `yarn dev` to build the content type and watch for changes, which will trigger a rebuild. + +## Testing + +Before running unit tests, start the required Docker container at the root of this repository. For more info, see [Running tests](../../README.md#running-tests). + +## Useful commands + +- `yarn build`: Builds the content type +- `yarn clean`: Removes `node_modules`, `dist`, and `.turbo` folders +- `yarn dev`: Builds the content type and watches for changes, which will trigger a rebuild +- `yarn lint`: Runs ESLint +- `yarn test:setup`: Starts a necessary docker container for testing +- `yarn test:teardown`: Stops docker container for testing +- `yarn test`: Runs all unit tests +- `yarn typecheck`: Runs `tsc` diff --git a/content-types/content-type-reaction/package.json b/content-types/content-type-reaction/package.json new file mode 100644 index 000000000..9a69d7917 --- /dev/null +++ b/content-types/content-type-reaction/package.json @@ -0,0 +1,92 @@ +{ + "name": "@xmtp/content-type-reaction", + "version": "1.1.9", + "description": "An XMTP content type to support reactions to messages", + "keywords": [ + "xmtp", + "messaging", + "web3", + "js", + "ts", + "javascript", + "typescript", + "content-types" + ], + "homepage": "https://github.com/xmtp/xmtp-js", + "bugs": { + "url": "https://github.com/xmtp/xmtp-js/issues" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/xmtp/xmtp-js.git", + "directory": "content-types/content-type-reaction" + }, + "license": "MIT", + "author": "XMTP Labs ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "browser": "./dist/browser/index.js", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "main": "dist/index.cjs", + "module": "dist/index.js", + "browser": "dist/browser/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn clean:dist && yarn rollup -c", + "clean": "yarn clean:dist && rimraf .turbo node_modules", + "clean:dist": "rimraf dist", + "dev": "yarn clean:dist && yarn rollup -c --watch", + "lint": "eslint . --ignore-path ../../.gitignore", + "test": "yarn test:node && yarn test:jsdom", + "test:jsdom": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment happy-dom", + "test:node": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment node", + "typecheck": "tsc --noEmit" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome versions", + "last 3 firefox versions", + "last 3 safari versions" + ] + }, + "dependencies": { + "@xmtp/content-type-primitives": "^1.0.1" + }, + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.0", + "@types/node": "^18.19.22", + "@xmtp/xmtp-js": "^11.6.3", + "buffer": "^6.0.3", + "eslint": "^8.57.0", + "eslint-config-custom": "workspace:*", + "ethers": "^6.11.1", + "happy-dom": "^13.7.3", + "rimraf": "^6.0.1", + "rollup": "^4.24.0", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-filesize": "^10.0.0", + "typescript": "^5.6.3", + "vite": "^5.1.6", + "vitest": "^2.1.2" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "registry": "https://registry.npmjs.org/" + } +} diff --git a/content-types/content-type-reaction/rollup.config.js b/content-types/content-type-reaction/rollup.config.js new file mode 100644 index 000000000..099cfcf95 --- /dev/null +++ b/content-types/content-type-reaction/rollup.config.js @@ -0,0 +1,58 @@ +import terser from "@rollup/plugin-terser"; +import typescript from "@rollup/plugin-typescript"; +import { defineConfig } from "rollup"; +import { dts } from "rollup-plugin-dts"; +import filesize from "rollup-plugin-filesize"; + +const plugins = [ + typescript({ + declaration: false, + declarationMap: false, + }), + filesize({ + showMinifiedSize: false, + }), +]; + +const external = ["@xmtp/content-type-primitives"]; + +export default defineConfig([ + { + input: "src/index.ts", + output: { + file: "dist/index.js", + format: "es", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/browser/index.js", + format: "es", + sourcemap: true, + }, + plugins: [...plugins, terser()], + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.cjs", + format: "cjs", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.d.ts", + format: "es", + }, + plugins: [dts()], + }, +]); diff --git a/content-types/content-type-reaction/src/Reaction.test.ts b/content-types/content-type-reaction/src/Reaction.test.ts new file mode 100644 index 000000000..ada9c5d26 --- /dev/null +++ b/content-types/content-type-reaction/src/Reaction.test.ts @@ -0,0 +1,120 @@ +import { Client } from "@xmtp/xmtp-js"; +import { Wallet } from "ethers"; +import { ContentTypeReaction, ReactionCodec, type Reaction } from "./Reaction"; + +describe("ReactionContentType", () => { + it("has the right content type", () => { + expect(ContentTypeReaction.authorityId).toBe("xmtp.org"); + expect(ContentTypeReaction.typeId).toBe("reaction"); + expect(ContentTypeReaction.versionMajor).toBe(1); + expect(ContentTypeReaction.versionMinor).toBe(0); + }); + + it("supports canonical and legacy form", () => { + const codec = new ReactionCodec(); + + // This is how clients send reactions now. + const canonicalEncoded = { + type: ContentTypeReaction, + content: new TextEncoder().encode( + JSON.stringify({ + action: "added", + content: "smile", + reference: "abc123", + schema: "shortcode", + }), + ), + }; + + // Previously, some clients sent reactions like this. + // So we test here to make sure we can still decode them. + const legacyEncoded = { + type: ContentTypeReaction, + parameters: { + action: "added", + reference: "abc123", + schema: "shortcode", + }, + content: new TextEncoder().encode("smile"), + }; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const canonical = codec.decode(canonicalEncoded as any); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const legacy = codec.decode(legacyEncoded as any); + expect(canonical.action).toBe("added"); + expect(legacy.action).toBe("added"); + expect(canonical.content).toBe("smile"); + expect(legacy.content).toBe("smile"); + expect(canonical.reference).toBe("abc123"); + expect(legacy.reference).toBe("abc123"); + expect(canonical.schema).toBe("shortcode"); + expect(legacy.schema).toBe("shortcode"); + }); + + it("can send a reaction", async () => { + const aliceWallet = Wallet.createRandom(); + const aliceClient = await Client.create(aliceWallet, { + codecs: [new ReactionCodec()], + env: "local", + }); + await aliceClient.publishUserContact(); + + const bobWallet = Wallet.createRandom(); + const bobClient = await Client.create(bobWallet, { + codecs: [new ReactionCodec()], + env: "local", + }); + await bobClient.publishUserContact(); + + const conversation = await aliceClient.conversations.newConversation( + bobWallet.address, + ); + + const originalMessage = await conversation.send("test"); + + const reaction: Reaction = { + action: "added", + content: "smile", + reference: originalMessage.id, + schema: "shortcode", + }; + + await conversation.send(reaction, { contentType: ContentTypeReaction }); + + const bobConversation = await bobClient.conversations.newConversation( + aliceWallet.address, + ); + const messages = await bobConversation.messages(); + + expect(messages.length).toBe(2); + + const reactionMessage = messages[1]; + const messageContent = reactionMessage.content as Reaction; + expect(messageContent.action).toBe("added"); + expect(messageContent.content).toBe("smile"); + expect(messageContent.reference).toBe(originalMessage.id); + expect(messageContent.schema).toBe("shortcode"); + }); + + it("has a proper shouldPush value based on content", () => { + const codec = new ReactionCodec(); + + const addReaction: Reaction = { + action: "added", + content: "smile", + reference: "foo", + schema: "shortcode", + }; + + const removeReaction: Reaction = { + action: "removed", + content: "smile", + reference: "foo", + schema: "shortcode", + }; + + expect(codec.shouldPush(addReaction)).toBe(true); + expect(codec.shouldPush(removeReaction)).toBe(false); + }); +}); diff --git a/content-types/content-type-reaction/src/Reaction.ts b/content-types/content-type-reaction/src/Reaction.ts new file mode 100644 index 000000000..2fef90ae6 --- /dev/null +++ b/content-types/content-type-reaction/src/Reaction.ts @@ -0,0 +1,104 @@ +import { + ContentTypeId, + type ContentCodec, + type EncodedContent, +} from "@xmtp/content-type-primitives"; + +export const ContentTypeReaction = new ContentTypeId({ + authorityId: "xmtp.org", + typeId: "reaction", + versionMajor: 1, + versionMinor: 0, +}); + +export type Reaction = { + /** + * The message ID for the message that is being reacted to + */ + reference: string; + /** + * The inbox ID of the user who sent the message that is being reacted to + * + * This only applies to group messages + */ + referenceInboxId?: string; + /** + * The action of the reaction + */ + action: "added" | "removed"; + /** + * The content of the reaction + */ + content: string; + /** + * The schema of the content to provide guidance on how to display it + */ + schema: "unicode" | "shortcode" | "custom"; +}; + +type LegacyReactionParameters = Pick< + Reaction, + "action" | "reference" | "schema" +> & { + encoding: "UTF-8"; +}; + +export class ReactionCodec implements ContentCodec { + get contentType(): ContentTypeId { + return ContentTypeReaction; + } + + encode(reaction: Reaction): EncodedContent { + const { action, reference, referenceInboxId, schema, content } = reaction; + return { + type: this.contentType, + parameters: {}, + content: new TextEncoder().encode( + JSON.stringify({ + action, + reference, + referenceInboxId, + schema, + content, + }), + ), + }; + } + + decode(encodedContent: EncodedContent): Reaction { + const decodedContent = new TextDecoder().decode(encodedContent.content); + + // First try to decode it in the canonical form. + try { + const reaction = JSON.parse(decodedContent) as Reaction; + const { action, reference, referenceInboxId, schema, content } = reaction; + return { action, reference, referenceInboxId, schema, content }; + } catch (e) { + // ignore, fall through to legacy decoding + } + + // If that fails, try to decode it in the legacy form. + const parameters = encodedContent.parameters as LegacyReactionParameters; + return { + action: parameters.action, + reference: parameters.reference, + schema: parameters.schema, + content: decodedContent, + }; + } + + fallback(content: Reaction): string | undefined { + switch (content.action) { + case "added": + return `Reacted “${content.content}” to an earlier message`; + case "removed": + return `Removed “${content.content}” from an earlier message`; + default: + return undefined; + } + } + + shouldPush(content: Reaction): boolean { + return content.action === "added"; + } +} diff --git a/content-types/content-type-reaction/src/index.ts b/content-types/content-type-reaction/src/index.ts new file mode 100644 index 000000000..edc7e20f0 --- /dev/null +++ b/content-types/content-type-reaction/src/index.ts @@ -0,0 +1,2 @@ +export { ReactionCodec, ContentTypeReaction } from "./Reaction"; +export type { Reaction } from "./Reaction"; diff --git a/content-types/content-type-reaction/tsconfig.eslint.json b/content-types/content-type-reaction/tsconfig.eslint.json new file mode 100644 index 000000000..5ee5f6f56 --- /dev/null +++ b/content-types/content-type-reaction/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": [".", ".eslintrc.cjs", "rollup.config.js"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-reaction/tsconfig.json b/content-types/content-type-reaction/tsconfig.json new file mode 100644 index 000000000..69f454187 --- /dev/null +++ b/content-types/content-type-reaction/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/build.json", + "include": ["src"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-reaction/vitest.config.ts b/content-types/content-type-reaction/vitest.config.ts new file mode 100644 index 000000000..2ee901930 --- /dev/null +++ b/content-types/content-type-reaction/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + setupFiles: "./vitest.setup.ts", + }, +}); diff --git a/content-types/content-type-reaction/vitest.setup.ts b/content-types/content-type-reaction/vitest.setup.ts new file mode 100644 index 000000000..df951dbaa --- /dev/null +++ b/content-types/content-type-reaction/vitest.setup.ts @@ -0,0 +1,3 @@ +import { Buffer } from "buffer"; + +globalThis.Buffer = Buffer; diff --git a/content-types/content-type-read-receipt/.eslintrc.cjs b/content-types/content-type-read-receipt/.eslintrc.cjs new file mode 100644 index 000000000..d46b01d75 --- /dev/null +++ b/content-types/content-type-read-receipt/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ["custom"], + parserOptions: { + project: "./tsconfig.eslint.json", + }, +}; diff --git a/content-types/content-type-read-receipt/CHANGELOG.md b/content-types/content-type-read-receipt/CHANGELOG.md new file mode 100644 index 000000000..05ab4826a --- /dev/null +++ b/content-types/content-type-read-receipt/CHANGELOG.md @@ -0,0 +1,90 @@ +# @xmtp/content-type-read-receipt + +## 1.1.10 + +### Patch Changes + +- [#75](https://github.com/xmtp/xmtp-js-content-types/pull/75) [`da0bd85`](https://github.com/xmtp/xmtp-js-content-types/commit/da0bd8578d5f5032b221e25f02e8492b27929d6c) + - Use primitives package for types + +## 1.1.9 + +### Patch Changes + +- [#65](https://github.com/xmtp/xmtp-js-content-types/pull/65) [`c4d43dc`](https://github.com/xmtp/xmtp-js-content-types/commit/c4d43dc948231de5c7f730e06f0931076de0673b) + - Add `shouldPush` to all content codecs + +## 1.1.8 + +### Patch Changes + +- [#60](https://github.com/xmtp/xmtp-js-content-types/pull/60) [`5b9310a`](https://github.com/xmtp/xmtp-js-content-types/commit/5b9310ac89fd23e5cfd74903894073b6ef8af7c3) + - Upgraded JS SDK to `11.3.12` + +## 1.1.7 + +### Patch Changes + +- [#54](https://github.com/xmtp/xmtp-js-content-types/pull/54) [`718cb9f`](https://github.com/xmtp/xmtp-js-content-types/commit/718cb9fec51f74bf2402f3f22160687cae35dda8) + - Updated Turbo config to remove `generate:types` command and instead rely on `build` + - Removed all `generate:types` commands from `package.json` files + - Updated shared ESLint config and local ESLint configs + - Updated `include` field of `tsconfig.json` and `tsconfig.eslint.json` files + - Replaced `tsup` with `rollup` + +## 1.1.6 + +### Patch Changes + +- [#51](https://github.com/xmtp/xmtp-js-content-types/pull/51) [`aeb6db7`](https://github.com/xmtp/xmtp-js-content-types/commit/aeb6db73a63409a33c7d3d3431e33682b0ce4c4d) + - Only publish files in the `/dist` directory + +## 1.1.5 + +### Patch Changes + +- Upgraded `@xmtp/proto` package +- Upgraded `@xmtp/xmtp-js` package + +## 1.1.4 + +### Patch Changes + +- Update `@xmtp/proto` to latest version + +## 1.1.3 + +### Patch Changes + +- Upgrade to JS SDK v11 +- Update client initialization for tests to use `codecs` option for proper types + +## 1.1.2 + +### Patch Changes + +- fix: remove timestamp from the read receipt content type + +## 1.1.1 + +### Patch Changes + +- fix: update the copy for the default fallbacks + +## 1.1.0 + +### Minor Changes + +- Add dummy fallback field to all content types + +## 1.0.1 + +### Patch Changes + +- added readme + +## 1.0.0 + +### Major Changes + +- added read receipt codec diff --git a/content-types/content-type-read-receipt/LICENSE b/content-types/content-type-read-receipt/LICENSE new file mode 100644 index 000000000..ae6695abd --- /dev/null +++ b/content-types/content-type-read-receipt/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 XMTP (xmtp.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/content-types/content-type-read-receipt/README.md b/content-types/content-type-read-receipt/README.md new file mode 100644 index 000000000..60f22dc45 --- /dev/null +++ b/content-types/content-type-read-receipt/README.md @@ -0,0 +1,108 @@ +# Read receipt content type + +![Status](https://img.shields.io/badge/Content_type_status-Standards--track-yellow) ![Status](https://img.shields.io/badge/Reference_implementation_status-Alpha-orange) + +This package provides an XMTP content type to support read receipts to messages. + +> **Important** +> This standards-track content type is in **Alpha** status as this implementation doesn't work efficiently with the current protocol architecture. This inefficiency will be addressed in a future protocol release. + +Until then, if you must support read receipts, we recommend that you use this implementation and **not build your own custom content type.** + +> **Open for feedback** +> You are welcome to provide feedback on this implementation by commenting on the [Read Receipts content type proposal](https://github.com/orgs/xmtp/discussions/43). + +## What’s a read receipt? + +A read receipt is a message sent to confirm that a previously sent message has been read by the recipient. With XMTP, read receipts are special messages with the `ReadReceipt` content type. They contain a timestamp of when the original message was read. + +When someone receives a message using an app with read receipts enabled, their XMTP client can send a read receipt when they open that message. + +## Why read receipts? + +Read receipts give the sender confirmation that the recipient has read their message. This avoids uncertainty about whether a message was seen, without needing to rely on a manual response. + +## Install the package + +```bash +# npm +npm i @xmtp/content-type-read-receipt + +# yarn +yarn add @xmtp/content-type-read-receipt + +# pnpm +pnpm i @xmtp/content-type-read-receipt +``` + +## Provide an opt-out option + +While this is a per-app decision, the best practice is to provide users with the option to opt out of sending read receipts. If a user opts out, when they read a message, a read receipt will not be sent to the sender of the message. + +## Create a read receipt + +With XMTP, read receipts are represented as empty objects. + +```tsx +const readReceipt: ReadReceipt = {}; +``` + +## Send a read receipt + +If a sender has opened a conversation and has not yet sent a read receipt for its received messages (this can either be done with each message or the most recent message and is an individual app decision), you can send a read receipt like so: + +```tsx +await conversation.messages.send({}, ContentTypeReadReceipt); +``` + +## Receive a read receipt + +Now that you can send a read receipt, you can also receive a read receipt that was sent from another user. For example: + +```tsx +// Assume `loadLastMessage` is a thing you have +const message: DecodedMessage = await loadLastMessage(); + +if (message.contentType.sameAs(ContentTypeReadReceipt)) { + // We have a read receipt + return; +} +``` + +## Display the read receipt + +Generally, a read receipt indicator should be displayed under the message it's associated with. The indicator can include a timestamp. Ultimately, how you choose to display a read receipt indicator is completely up to you. + +> **Important** +> The read receipt is provided as an **empty message** whose timestamp provides the data needed for the indicators. **Be sure to filter out read receipt empty messages and not display them to users.** + +## Playground implementation + +In the XMTP React playground implementation, read receipts are stored in IndexedDB in their own table, separate from regular messages. + +A read receipt is sent when a user opens a conversation only if the most recent message was from the other party, and there is no read receipt after that last message timestamp in the read receipts table. The decision to do this for the last message instead of for all received messages has to do with not wanting to potentially double the number of messages by sending read receipts for every single message. + +To try it out, see the [XMTP React playground](https://github.com/xmtp/xmtp-react-playground). + +A read receipt indicator is shown if the most recent message was from the other party and a read receipt for that message exists. + +## Developing + +Run `yarn dev` to build the content type and watch for changes, which will trigger a rebuild. + +## Testing + +Before running unit tests, start the required Docker container at the root of this repository. For more info, see [Running tests](../../README.md#running-tests). + +## Useful commands + +- `yarn build`: Builds the content type +- `yarn clean`: Removes `node_modules`, `dist`, and `.turbo` folders +- `yarn dev`: Builds the content type and watches for changes, which will trigger a rebuild +- `yarn format`: Runs Prettier format and write changes +- `yarn format:check`: Runs Prettier format check +- `yarn lint`: Runs ESLint +- `yarn test:setup`: Starts a necessary Docker container for testing +- `yarn test:teardown`: Stops Docker container for testing +- `yarn test`: Runs all unit tests +- `yarn typecheck`: Runs `tsc` diff --git a/content-types/content-type-read-receipt/package.json b/content-types/content-type-read-receipt/package.json new file mode 100644 index 000000000..56c76092a --- /dev/null +++ b/content-types/content-type-read-receipt/package.json @@ -0,0 +1,92 @@ +{ + "name": "@xmtp/content-type-read-receipt", + "version": "1.1.10", + "description": "An XMTP content type to support read receipts", + "keywords": [ + "xmtp", + "messaging", + "web3", + "js", + "ts", + "javascript", + "typescript", + "content-types" + ], + "homepage": "https://github.com/xmtp/xmtp-js", + "bugs": { + "url": "https://github.com/xmtp/xmtp-js/issues" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/xmtp/xmtp-js.git", + "directory": "content-types/content-type-read-receipt" + }, + "license": "MIT", + "author": "XMTP Labs ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "browser": "./dist/browser/index.js", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "main": "dist/index.cjs", + "module": "dist/index.js", + "browser": "dist/browser/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn clean:dist && yarn rollup -c", + "clean": "yarn clean:dist && rimraf .turbo node_modules", + "clean:dist": "rimraf dist", + "dev": "yarn clean:dist && yarn rollup -c --watch", + "lint": "eslint . --ignore-path ../../.gitignore", + "test": "yarn test:node && yarn test:jsdom", + "test:jsdom": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment happy-dom", + "test:node": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment node", + "typecheck": "tsc --noEmit" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome versions", + "last 3 firefox versions", + "last 3 safari versions" + ] + }, + "dependencies": { + "@xmtp/content-type-primitives": "^1.0.1" + }, + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.0", + "@types/node": "^18.19.22", + "@xmtp/xmtp-js": "^11.6.3", + "buffer": "^6.0.3", + "eslint": "^8.57.0", + "eslint-config-custom": "workspace:*", + "ethers": "^6.11.1", + "happy-dom": "^13.7.3", + "rimraf": "^6.0.1", + "rollup": "^4.24.0", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-filesize": "^10.0.0", + "typescript": "^5.6.3", + "vite": "^5.1.6", + "vitest": "^2.1.2" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "registry": "https://registry.npmjs.org/" + } +} diff --git a/content-types/content-type-read-receipt/rollup.config.js b/content-types/content-type-read-receipt/rollup.config.js new file mode 100644 index 000000000..099cfcf95 --- /dev/null +++ b/content-types/content-type-read-receipt/rollup.config.js @@ -0,0 +1,58 @@ +import terser from "@rollup/plugin-terser"; +import typescript from "@rollup/plugin-typescript"; +import { defineConfig } from "rollup"; +import { dts } from "rollup-plugin-dts"; +import filesize from "rollup-plugin-filesize"; + +const plugins = [ + typescript({ + declaration: false, + declarationMap: false, + }), + filesize({ + showMinifiedSize: false, + }), +]; + +const external = ["@xmtp/content-type-primitives"]; + +export default defineConfig([ + { + input: "src/index.ts", + output: { + file: "dist/index.js", + format: "es", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/browser/index.js", + format: "es", + sourcemap: true, + }, + plugins: [...plugins, terser()], + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.cjs", + format: "cjs", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.d.ts", + format: "es", + }, + plugins: [dts()], + }, +]); diff --git a/content-types/content-type-read-receipt/src/ReadReceipt.test.ts b/content-types/content-type-read-receipt/src/ReadReceipt.test.ts new file mode 100644 index 000000000..9379b4fc1 --- /dev/null +++ b/content-types/content-type-read-receipt/src/ReadReceipt.test.ts @@ -0,0 +1,58 @@ +import { Client } from "@xmtp/xmtp-js"; +import { Wallet } from "ethers"; +import { + ContentTypeReadReceipt, + ReadReceiptCodec, + type ReadReceipt, +} from "./ReadReceipt"; + +describe("ReadReceiptContentType", () => { + it("has the right content type", () => { + expect(ContentTypeReadReceipt.authorityId).toBe("xmtp.org"); + expect(ContentTypeReadReceipt.typeId).toBe("readReceipt"); + expect(ContentTypeReadReceipt.versionMajor).toBe(1); + expect(ContentTypeReadReceipt.versionMinor).toBe(0); + }); + + it("can send a read receipt", async () => { + const aliceWallet = Wallet.createRandom(); + const aliceClient = await Client.create(aliceWallet, { + codecs: [new ReadReceiptCodec()], + env: "local", + }); + await aliceClient.publishUserContact(); + + const bobWallet = Wallet.createRandom(); + const bobClient = await Client.create(bobWallet, { + codecs: [new ReadReceiptCodec()], + env: "local", + }); + await bobClient.publishUserContact(); + + const conversation = await aliceClient.conversations.newConversation( + bobWallet.address, + ); + + const readReceipt: ReadReceipt = {}; + + await conversation.send(readReceipt, { + contentType: ContentTypeReadReceipt, + }); + + const bobConversation = await bobClient.conversations.newConversation( + aliceWallet.address, + ); + const messages = await bobConversation.messages(); + + expect(messages.length).toBe(1); + + const readReceiptMessage = messages[0]; + const messageContent = readReceiptMessage.contentType; + expect(messageContent.typeId).toBe("readReceipt"); + }); + + it("has a proper shouldPush value", () => { + const codec = new ReadReceiptCodec(); + expect(codec.shouldPush()).toBe(false); + }); +}); diff --git a/content-types/content-type-read-receipt/src/ReadReceipt.ts b/content-types/content-type-read-receipt/src/ReadReceipt.ts new file mode 100644 index 000000000..970a8865e --- /dev/null +++ b/content-types/content-type-read-receipt/src/ReadReceipt.ts @@ -0,0 +1,40 @@ +import { + ContentTypeId, + type ContentCodec, + type EncodedContent, +} from "@xmtp/content-type-primitives"; + +export const ContentTypeReadReceipt = new ContentTypeId({ + authorityId: "xmtp.org", + typeId: "readReceipt", + versionMajor: 1, + versionMinor: 0, +}); + +export type ReadReceipt = Record; + +export class ReadReceiptCodec implements ContentCodec { + get contentType(): ContentTypeId { + return ContentTypeReadReceipt; + } + + encode(): EncodedContent { + return { + type: ContentTypeReadReceipt, + parameters: {}, + content: new Uint8Array(), + }; + } + + decode(): ReadReceipt { + return {}; + } + + fallback(): string | undefined { + return undefined; + } + + shouldPush() { + return false; + } +} diff --git a/content-types/content-type-read-receipt/src/index.ts b/content-types/content-type-read-receipt/src/index.ts new file mode 100644 index 000000000..977b5587f --- /dev/null +++ b/content-types/content-type-read-receipt/src/index.ts @@ -0,0 +1,2 @@ +export { ReadReceiptCodec, ContentTypeReadReceipt } from "./ReadReceipt"; +export type { ReadReceipt } from "./ReadReceipt"; diff --git a/content-types/content-type-read-receipt/tsconfig.eslint.json b/content-types/content-type-read-receipt/tsconfig.eslint.json new file mode 100644 index 000000000..5ee5f6f56 --- /dev/null +++ b/content-types/content-type-read-receipt/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": [".", ".eslintrc.cjs", "rollup.config.js"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-read-receipt/tsconfig.json b/content-types/content-type-read-receipt/tsconfig.json new file mode 100644 index 000000000..69f454187 --- /dev/null +++ b/content-types/content-type-read-receipt/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/build.json", + "include": ["src"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-read-receipt/vitest.config.ts b/content-types/content-type-read-receipt/vitest.config.ts new file mode 100644 index 000000000..2ee901930 --- /dev/null +++ b/content-types/content-type-read-receipt/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + setupFiles: "./vitest.setup.ts", + }, +}); diff --git a/content-types/content-type-read-receipt/vitest.setup.ts b/content-types/content-type-read-receipt/vitest.setup.ts new file mode 100644 index 000000000..df951dbaa --- /dev/null +++ b/content-types/content-type-read-receipt/vitest.setup.ts @@ -0,0 +1,3 @@ +import { Buffer } from "buffer"; + +globalThis.Buffer = Buffer; diff --git a/content-types/content-type-remote-attachment/.eslintrc.cjs b/content-types/content-type-remote-attachment/.eslintrc.cjs new file mode 100644 index 000000000..d46b01d75 --- /dev/null +++ b/content-types/content-type-remote-attachment/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ["custom"], + parserOptions: { + project: "./tsconfig.eslint.json", + }, +}; diff --git a/content-types/content-type-remote-attachment/CHANGELOG.md b/content-types/content-type-remote-attachment/CHANGELOG.md new file mode 100644 index 000000000..e4b51ba0e --- /dev/null +++ b/content-types/content-type-remote-attachment/CHANGELOG.md @@ -0,0 +1,72 @@ +# @xmtp/content-type-remote-attachment + +## 1.1.9 + +### Patch Changes + +- [#75](https://github.com/xmtp/xmtp-js-content-types/pull/75) [`da0bd85`](https://github.com/xmtp/xmtp-js-content-types/commit/da0bd8578d5f5032b221e25f02e8492b27929d6c) + - Use primitives package for types + +## 1.1.8 + +### Patch Changes + +- [#65](https://github.com/xmtp/xmtp-js-content-types/pull/65) [`c4d43dc`](https://github.com/xmtp/xmtp-js-content-types/commit/c4d43dc948231de5c7f730e06f0931076de0673b) + - Add `shouldPush` to all content codecs + +## 1.1.7 + +### Patch Changes + +- [#60](https://github.com/xmtp/xmtp-js-content-types/pull/60) [`5b9310a`](https://github.com/xmtp/xmtp-js-content-types/commit/5b9310ac89fd23e5cfd74903894073b6ef8af7c3) + - Upgraded JS SDK to `11.3.12` + +## 1.1.6 + +### Patch Changes + +- [#54](https://github.com/xmtp/xmtp-js-content-types/pull/54) [`718cb9f`](https://github.com/xmtp/xmtp-js-content-types/commit/718cb9fec51f74bf2402f3f22160687cae35dda8) + - Updated Turbo config to remove `generate:types` command and instead rely on `build` + - Removed all `generate:types` commands from `package.json` files + - Updated shared ESLint config and local ESLint configs + - Updated `include` field of `tsconfig.json` and `tsconfig.eslint.json` files + - Replaced `tsup` with `rollup` + +## 1.1.5 + +### Patch Changes + +- [#51](https://github.com/xmtp/xmtp-js-content-types/pull/51) [`aeb6db7`](https://github.com/xmtp/xmtp-js-content-types/commit/aeb6db73a63409a33c7d3d3431e33682b0ce4c4d) + - Only publish files in the `/dist` directory + +## 1.1.4 + +### Patch Changes + +- Upgraded `@xmtp/proto` package +- Upgraded `@xmtp/xmtp-js` package + +## 1.1.3 + +### Patch Changes + +- Update `@xmtp/proto` to latest version + +## 1.1.2 + +### Patch Changes + +- Upgrade to JS SDK v11 +- Update client initialization for tests to use `codecs` option for proper types + +## 1.1.1 + +### Patch Changes + +- fix: update the copy for the default fallbacks + +## 1.1.0 + +### Minor Changes + +- Add dummy fallback field to all content types diff --git a/content-types/content-type-remote-attachment/LICENSE b/content-types/content-type-remote-attachment/LICENSE new file mode 100644 index 000000000..ae6695abd --- /dev/null +++ b/content-types/content-type-remote-attachment/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 XMTP (xmtp.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/content-types/content-type-remote-attachment/README.md b/content-types/content-type-remote-attachment/README.md new file mode 100644 index 000000000..d3d3ad333 --- /dev/null +++ b/content-types/content-type-remote-attachment/README.md @@ -0,0 +1,212 @@ +# Remote attachment content type + +![Status](https://img.shields.io/badge/Content_type_status-Standards--track-yellow) ![Status](https://img.shields.io/badge/Reference_implementation_status-Stable-brightgreen) + +The [@xmtp/content-type-remote-attachment](https://github.com/xmtp/xmtp-js-content-types/tree/main/content-types/content-type-remote-attachment) package provides an XMTP content type to support sending file attachments that are stored off-network. Use it to enable your app to send and receive message attachments. + +> **Open for feedback** +> You are welcome to provide feedback on this implementation by commenting on the [Remote Attachment Content Type XIP](https://github.com/xmtp/XIPs/blob/main/XIPs/xip-17-remote-attachment-content-type-proposal.md) (XMTP Improvement Proposal). + +## What’s an attachment? + +Attachments are files. More specifically, attachments are objects that have: + +- `filename` Most files have names, at least the most common file types. +- `mimeType` What kind of file is it? You can often assume this from the file extension, but it's nice to have a specific field for it. [Here's a list of common mime types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types). +- `data` What is this file's data? Most files have data. If the file doesn't have data, then it's probably not the most interesting thing to send. + +## Why remote attachments? + +Because XMTP messages can only be up to 1MB in size, we need to store the attachment somewhere other than the XMTP network. In other words, we need to store it in a remote location. + +## What about encryption? + +End-to-end encryption must apply not only to XMTP messages, but to message attachments as well. For this reason, we need to encrypt the attachment before we store it. + +## Install the package + +```bash +# npm +npm i @xmtp/content-type-remote-attachment + +# yarn +yarn add @xmtp/content-type-remote-attachment + +# pnpm +pnpm i @xmtp/content-type-remote-attachment +``` + +## Create an attachment object + +```tsx +const attachment: Attachment = { + filename: "screenshot.png", + mimeType: "image/png", + data: [the PNG data] +} +``` + +## Create a preview attachment object + +Once you have the attachment object created, you can also create a preview for what to show in a message input before sending: + +```tsx +URL.createObjectURL( + new Blob([Buffer.from(somePNGData)], { + type: attachment.mimeType, + }), +), +``` + +## Encrypt the attachment + +Use the `RemoteAttachmentCodec.encodeEncrypted` to encrypt the attachment: + +```tsx +// Import the codecs we're going to use +import { + AttachmentCodec, + RemoteAttachmentCodec, +} from "@xmtp/content-type-remote-attachment"; + +// Encode the attachment and encrypt that encoded content +const encryptedAttachment = await RemoteAttachmentCodec.encodeEncrypted( + attachment, + new AttachmentCodec(), +); +``` + +## Upload the encrypted attachment + +Upload the encrypted attachment anywhere where it will be accessible via an HTTPS GET request. For example, you can use web3.storage: + +```tsx +const web3Storage = new Web3Storage({ + token: "your web3.storage token here", +}); + +const upload = new Upload("XMTPEncryptedContent", encryptedEncoded.payload); +const cid = await web3Storage.put([upload]); +const url = `https://${cid}.ipfs.w3s.link/XMTPEncryptedContent`; +``` + +_([Upload](https://github.com/xmtp-labs/xmtp-inbox-web/blob/5b45e05efbe0b0f49c17d66d7547be2c13a51eab/hooks/useSendMessage.ts#L15-L33) is a small class that implements Web3Storage's `Filelike` interface for uploading)_ + +## Create a remote attachment + +Now that you have a `url`, you can create a `RemoteAttachment`. + +```tsx +const remoteAttachment: RemoteAttachment = { + // This is the URL string where clients can download the encrypted + // encoded content + url: url, + + // We hash the encrypted encoded payload and send that along with the + // remote attachment. On the recipient side, clients can verify that the + // encrypted encoded payload they've downloaded matches what was uploaded. + // This is to prevent tampering with the content once it's been uploaded. + contentDigest: encryptedAttachment.digest, + + // These are the encryption keys that will be used by the recipient to + // decrypt the remote payload + salt: encryptedAttachment.salt, + nonce: encryptedAttachment.nonce, + secret: encryptedAttachment.secret, + + // For now, all remote attachments MUST be fetchable via HTTPS GET requests. + // We're investigating IPFS here among other options. + scheme: "https://", + + // These fields are used by clients to display some information about + // the remote attachment before it is downloaded and decrypted. + filename: attachment.filename, + contentLength: attachment.data.byteLength, +}; +``` + +## Send a remote attachment + +Now that you have a remote attachment, you can send it: + +```tsx +await conversation.messages.send(remoteAttachment, { + contentType: ContentTypeRemoteAttachment, +}); +``` + +> **Note** +> `contentFallback` text is provided by the codec and gives clients that _don't_ support a content type the option to display some useful context. For cases where clients *do* support the content type, they can use the content fallback as alt text for accessibility purposes. + +## Receive a remote attachment + +Now that you can send a remote attachment, you need a way to receive a remote attachment. For example: + +```tsx +// Assume `loadLastMessage` is a thing you have +const message: DecodedMessage = await loadLastMessage(); + +if (!message.contentType.sameAs(ContentTypeRemoteAttachment)) { + // We do not have a remote attachment. A topic for another blog post. + return; +} + +// We've got a remote attachment. +const remoteAttachment: RemoteAttachment = message.content; +``` + +## Download, decrypt, and decode the attachment + +Now that you can receive a remote attachment, you need to download, decrypt, and decode it so your app can display it. For example: + +```tsx +const attachment: Attachment = await RemoteAttachmentCodec.load( + remoteAttachment, + client, // <- Your XMTP Client instance +); +``` + +You now have the original attachment: + +```tsx +attachment.filename; // => "screenshot.png" +attachment.mimeType; // => "image/png", +attachment.data; // => [the PNG data] +``` + +## Display the attachment + +Display the attachment in your app as you please. For example, you can display it as an image: + +```tsx +const objectURL = URL.createObjectURL( + new Blob([Buffer.from(attachment.data)], { + type: attachment.mimeType, + }), +); + +const img = document.createElement("img"); +img.src = objectURL; +img.title = attachment.filename; +``` + +To learn more, see [Introducing remote media attachments](https://xmtp.org/blog/attachments-and-remote-attachments). + +## Developing + +Run `yarn dev` to build the content type and watch for changes, which will trigger a rebuild. + +## Testing + +Before running unit tests, start the required Docker container at the root of this repository. For more info, see [Running tests](../../README.md#running-tests). + +## Useful commands + +- `yarn build`: Builds the content type +- `yarn clean`: Removes `node_modules`, `dist`, and `.turbo` folders +- `yarn dev`: Builds the content type and watches for changes, which will trigger a rebuild +- `yarn lint`: Runs ESLint +- `yarn test:setup`: Starts a necessary docker container for testing +- `yarn test:teardown`: Stops docker container for testing +- `yarn test`: Runs all unit tests +- `yarn typecheck`: Runs `tsc` diff --git a/content-types/content-type-remote-attachment/package.json b/content-types/content-type-remote-attachment/package.json new file mode 100644 index 000000000..2dda6959a --- /dev/null +++ b/content-types/content-type-remote-attachment/package.json @@ -0,0 +1,96 @@ +{ + "name": "@xmtp/content-type-remote-attachment", + "version": "1.1.9", + "description": "An XMTP content type to support sending file attachments that are stored off network", + "keywords": [ + "xmtp", + "messaging", + "web3", + "js", + "ts", + "javascript", + "typescript", + "content-types" + ], + "homepage": "https://github.com/xmtp/xmtp-js", + "bugs": { + "url": "https://github.com/xmtp/xmtp-js/issues" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/xmtp/xmtp-js.git", + "directory": "content-types/content-type-remote-attachment" + }, + "license": "MIT", + "author": "XMTP Labs ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "browser": "./dist/browser/index.js", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "main": "dist/index.cjs", + "module": "dist/index.js", + "browser": "dist/browser/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn clean:dist && yarn rollup -c", + "clean": "yarn clean:dist && rimraf .turbo node_modules", + "clean:dist": "rimraf dist", + "dev": "yarn clean:dist && yarn rollup -c --watch", + "lint": "eslint . --ignore-path ../../.gitignore", + "test": "yarn test:node && yarn test:jsdom", + "test:jsdom": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment happy-dom", + "test:node": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment node", + "typecheck": "tsc --noEmit" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome versions", + "last 3 firefox versions", + "last 3 safari versions" + ] + }, + "dependencies": { + "@noble/secp256k1": "^1.7.1", + "@xmtp/content-type-primitives": "^1.0.1", + "@xmtp/proto": "^3.61.1", + "@xmtp/xmtp-js": "^11.6.3" + }, + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.0", + "@types/node": "^18.19.22", + "@xmtp/rollup-plugin-resolve-extensions": "^1.0.1", + "@xmtp/xmtp-js": "^11.6.3", + "buffer": "^6.0.3", + "eslint": "^8.57.0", + "eslint-config-custom": "workspace:*", + "ethers": "^6.11.1", + "happy-dom": "^13.7.3", + "rimraf": "^6.0.1", + "rollup": "^4.24.0", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-filesize": "^10.0.0", + "typescript": "^5.6.3", + "vite": "^5.1.6", + "vitest": "^2.1.2" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "registry": "https://registry.npmjs.org/" + } +} diff --git a/content-types/content-type-remote-attachment/rollup.config.js b/content-types/content-type-remote-attachment/rollup.config.js new file mode 100644 index 000000000..74e5cdfed --- /dev/null +++ b/content-types/content-type-remote-attachment/rollup.config.js @@ -0,0 +1,69 @@ +import terser from "@rollup/plugin-terser"; +import typescript from "@rollup/plugin-typescript"; +import { resolveExtensions } from "@xmtp/rollup-plugin-resolve-extensions"; +import { defineConfig } from "rollup"; +import { dts } from "rollup-plugin-dts"; +import filesize from "rollup-plugin-filesize"; + +const plugins = [ + typescript({ + declaration: false, + declarationMap: false, + }), + filesize({ + showMinifiedSize: false, + }), +]; + +const external = [ + "@noble/secp256k1", + "@xmtp/proto", + "@xmtp/content-type-primitives", + "@xmtp/xmtp-js", + "node:crypto", +]; + +export default defineConfig([ + { + input: "src/index.ts", + output: { + file: "dist/index.js", + format: "es", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/browser/index.js", + format: "es", + sourcemap: true, + }, + plugins: [ + resolveExtensions({ extensions: [".browser"] }), + terser(), + ...plugins, + ], + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.cjs", + format: "cjs", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.d.ts", + format: "es", + }, + plugins: [dts()], + }, +]); diff --git a/content-types/content-type-remote-attachment/src/Attachment.test.ts b/content-types/content-type-remote-attachment/src/Attachment.test.ts new file mode 100644 index 000000000..8cdff8180 --- /dev/null +++ b/content-types/content-type-remote-attachment/src/Attachment.test.ts @@ -0,0 +1,60 @@ +import { Client } from "@xmtp/xmtp-js"; +import { Wallet } from "ethers"; +import { + AttachmentCodec, + ContentTypeAttachment, + type Attachment, +} from "./Attachment"; + +test("content type exists", () => { + expect(ContentTypeAttachment.authorityId).toBe("xmtp.org"); + expect(ContentTypeAttachment.typeId).toBe("attachment"); + expect(ContentTypeAttachment.versionMajor).toBe(1); + expect(ContentTypeAttachment.versionMinor).toBe(0); +}); + +test("can send an attachment", async () => { + const aliceWallet = Wallet.createRandom(); + const aliceClient = await Client.create(aliceWallet, { + codecs: [new AttachmentCodec()], + env: "local", + }); + await aliceClient.publishUserContact(); + + const bobWallet = Wallet.createRandom(); + const bobClient = await Client.create(bobWallet, { + codecs: [new AttachmentCodec()], + env: "local", + }); + await bobClient.publishUserContact(); + + const conversation = await aliceClient.conversations.newConversation( + bobWallet.address, + ); + + const attachment: Attachment = { + filename: "test.png", + mimeType: "image/png", + data: Uint8Array.from([5, 4, 3, 2, 1]), + }; + + await conversation.send(attachment, { contentType: ContentTypeAttachment }); + + const bobConversation = await bobClient.conversations.newConversation( + aliceWallet.address, + ); + const messages = await bobConversation.messages(); + + expect(messages.length).toBe(1); + + const message = messages[0]; + const messageContent = message.content as Attachment; + expect(messageContent.filename).toBe("test.png"); + expect(messageContent.mimeType).toBe("image/png"); + expect(messageContent.data).toStrictEqual(Uint8Array.from([5, 4, 3, 2, 1])); +}); + +test("has a proper shouldPush value", () => { + const codec = new AttachmentCodec(); + expect(codec.shouldPush()).toBe(true); +}); diff --git a/content-types/content-type-remote-attachment/src/Attachment.ts b/content-types/content-type-remote-attachment/src/Attachment.ts new file mode 100644 index 000000000..e25d1e237 --- /dev/null +++ b/content-types/content-type-remote-attachment/src/Attachment.ts @@ -0,0 +1,51 @@ +import { + ContentTypeId, + type ContentCodec, + type EncodedContent, +} from "@xmtp/content-type-primitives"; + +export const ContentTypeAttachment = new ContentTypeId({ + authorityId: "xmtp.org", + typeId: "attachment", + versionMajor: 1, + versionMinor: 0, +}); + +export type Attachment = { + filename: string; + mimeType: string; + data: Uint8Array; +}; + +export class AttachmentCodec implements ContentCodec { + get contentType(): ContentTypeId { + return ContentTypeAttachment; + } + + encode(content: Attachment): EncodedContent { + return { + type: ContentTypeAttachment, + parameters: { + filename: content.filename, + mimeType: content.mimeType, + }, + content: content.data, + }; + } + + decode(content: EncodedContent): Attachment { + return { + filename: content.parameters.filename, + mimeType: content.parameters.mimeType, + data: content.content, + }; + } + + fallback(content: Attachment): string | undefined { + return `Can’t display "${content.filename}". This app doesn’t support attachments.`; + } + + shouldPush() { + return true; + } +} diff --git a/content-types/content-type-remote-attachment/src/RemoteAttachment.test.ts b/content-types/content-type-remote-attachment/src/RemoteAttachment.test.ts new file mode 100644 index 000000000..a1b71328b --- /dev/null +++ b/content-types/content-type-remote-attachment/src/RemoteAttachment.test.ts @@ -0,0 +1,224 @@ +import { Client } from "@xmtp/xmtp-js"; +import { Wallet } from "ethers"; +import { AttachmentCodec, type Attachment } from "./Attachment"; +import { + ContentTypeRemoteAttachment, + RemoteAttachmentCodec, + type RemoteAttachment, +} from "./RemoteAttachment"; + +test("content type exists", () => { + expect(ContentTypeRemoteAttachment.authorityId).toBe("xmtp.org"); + expect(ContentTypeRemoteAttachment.typeId).toBe("remoteStaticAttachment"); + expect(ContentTypeRemoteAttachment.versionMajor).toBe(1); + expect(ContentTypeRemoteAttachment.versionMinor).toBe(0); +}); + +test("can create a remote attachment", async () => { + const aliceWallet = Wallet.createRandom(); + const aliceClient = await Client.create(aliceWallet, { + codecs: [new AttachmentCodec(), new RemoteAttachmentCodec()], + env: "local", + }); + await aliceClient.publishUserContact(); + + const bobWallet = Wallet.createRandom(); + const bobClient = await Client.create(bobWallet, { + codecs: [new AttachmentCodec(), new RemoteAttachmentCodec()], + env: "local", + }); + await bobClient.publishUserContact(); + + const conversation = await aliceClient.conversations.newConversation( + bobWallet.address, + ); + + const attachment: Attachment = { + filename: "test.txt", + mimeType: "text/plain", + data: new TextEncoder().encode("hello world"), + }; + const encryptedEncodedContent = await RemoteAttachmentCodec.encodeEncrypted( + attachment, + new AttachmentCodec(), + ); + + try { + await fetch("https://localhost:3000/test", { + method: "POST", + body: encryptedEncodedContent.payload, + headers: { + "Content-Type": "application/octet-stream", + }, + }); + } catch (e) { + console.error("error fetch", e); + } + + const remoteAttachment: RemoteAttachment = { + url: "https://localhost:3000/test", + contentDigest: encryptedEncodedContent.digest, + salt: encryptedEncodedContent.salt, + nonce: encryptedEncodedContent.nonce, + secret: encryptedEncodedContent.secret, + scheme: "https", + contentLength: encryptedEncodedContent.payload.length, + filename: "test.txt", + }; + + await conversation.send(remoteAttachment, { + contentType: ContentTypeRemoteAttachment, + }); + + const bobConversation = await bobClient.conversations.newConversation( + aliceWallet.address, + ); + const messages = await bobConversation.messages(); + + expect(messages.length).toBe(1); + + const message = messages[0]; + const messageContent = message.content as RemoteAttachment; + expect(messageContent.url).toBe("https://localhost:3000/test"); + expect(messageContent.filename).toBe("test.txt"); + expect(messageContent.contentDigest).toBe(encryptedEncodedContent.digest); + + const content = await RemoteAttachmentCodec.load( + messageContent, + bobClient, + ); + expect(content.filename).toBe("test.txt"); + expect(content.mimeType).toBe("text/plain"); + expect(content.data).toStrictEqual(new TextEncoder().encode("hello world")); +}); + +test("fails if url is not https", async () => { + const aliceWallet = Wallet.createRandom(); + const aliceClient = await Client.create(aliceWallet, { + codecs: [new AttachmentCodec(), new RemoteAttachmentCodec()], + env: "local", + }); + await aliceClient.publishUserContact(); + + const bobWallet = Wallet.createRandom(); + const bobClient = await Client.create(bobWallet, { + codecs: [new AttachmentCodec(), new RemoteAttachmentCodec()], + env: "local", + }); + await bobClient.publishUserContact(); + + const conversation = await aliceClient.conversations.newConversation( + bobWallet.address, + ); + + const attachment: Attachment = { + filename: "test.txt", + mimeType: "text/plain", + data: new TextEncoder().encode("hello world"), + }; + const encryptedEncodedContent = await RemoteAttachmentCodec.encodeEncrypted( + attachment, + new AttachmentCodec(), + ); + + const remoteAttachment: RemoteAttachment = { + url: "http://localhost/test", // We didn't upload this, but it doesn't matter + contentDigest: encryptedEncodedContent.digest, + salt: encryptedEncodedContent.salt, + nonce: encryptedEncodedContent.nonce, + secret: encryptedEncodedContent.secret, + scheme: "https", + contentLength: encryptedEncodedContent.payload.length, + filename: "test.txt", + }; + + await expect( + conversation.send(remoteAttachment, { + contentType: ContentTypeRemoteAttachment, + }), + ).rejects.toThrow("scheme must be https"); +}); + +test("fails if content digest does not match", async () => { + const aliceWallet = Wallet.createRandom(); + const aliceClient = await Client.create(aliceWallet, { + codecs: [new AttachmentCodec(), new RemoteAttachmentCodec()], + env: "local", + }); + await aliceClient.publishUserContact(); + + const bobWallet = Wallet.createRandom(); + const bobClient = await Client.create(bobWallet, { + codecs: [new AttachmentCodec(), new RemoteAttachmentCodec()], + env: "local", + }); + await bobClient.publishUserContact(); + + const conversation = await aliceClient.conversations.newConversation( + bobWallet.address, + ); + + const attachment: Attachment = { + filename: "test.txt", + mimeType: "text/plain", + data: new TextEncoder().encode("hello world"), + }; + const encryptedEncodedContent = await RemoteAttachmentCodec.encodeEncrypted( + attachment, + new AttachmentCodec(), + ); + + try { + await fetch("https://localhost:3000/test", { + method: "POST", + body: encryptedEncodedContent.payload, + headers: { + "Content-Type": "application/octet-stream", + }, + }); + } catch (e) { + console.error("error fetch", e); + } + + const remoteAttachment: RemoteAttachment = { + url: "https://localhost:3000/test", + contentDigest: encryptedEncodedContent.digest, + salt: encryptedEncodedContent.salt, + nonce: encryptedEncodedContent.nonce, + secret: encryptedEncodedContent.secret, + scheme: "https", + contentLength: encryptedEncodedContent.payload.length, + filename: "test.txt", + }; + + await conversation.send(remoteAttachment, { + contentType: ContentTypeRemoteAttachment, + }); + + const bobConversation = await bobClient.conversations.newConversation( + aliceWallet.address, + ); + const messages = await bobConversation.messages(); + const message = messages[0]; + + const encryptedEncoded2 = await RemoteAttachmentCodec.encodeEncrypted( + attachment, + new AttachmentCodec(), + ); + await fetch("https://localhost:3000/test", { + method: "POST", + body: encryptedEncoded2.payload, + headers: { + "Content-Type": "application/octet-stream", + }, + }); + + await expect( + RemoteAttachmentCodec.load(message.content as RemoteAttachment, bobClient), + ).rejects.toThrow("content digest does not match"); +}); + +test("has a proper shouldPush value", () => { + const codec = new RemoteAttachmentCodec(); + expect(codec.shouldPush()).toBe(true); +}); diff --git a/content-types/content-type-remote-attachment/src/RemoteAttachment.ts b/content-types/content-type-remote-attachment/src/RemoteAttachment.ts new file mode 100644 index 000000000..e40616a2e --- /dev/null +++ b/content-types/content-type-remote-attachment/src/RemoteAttachment.ts @@ -0,0 +1,172 @@ +import * as secp from "@noble/secp256k1"; +import { + ContentTypeId, + type CodecRegistry, + type ContentCodec, + type EncodedContent, +} from "@xmtp/content-type-primitives"; +import { content as proto } from "@xmtp/proto"; +import { Ciphertext, decrypt, encrypt } from "@xmtp/xmtp-js"; +import { crypto } from "./encryption"; + +export const ContentTypeRemoteAttachment = new ContentTypeId({ + authorityId: "xmtp.org", + typeId: "remoteStaticAttachment", + versionMajor: 1, + versionMinor: 0, +}); + +export type EncryptedEncodedContent = { + digest: string; + salt: Uint8Array; + nonce: Uint8Array; + secret: Uint8Array; + payload: Uint8Array; +}; + +export type RemoteAttachment = { + url: string; + contentDigest: string; + salt: Uint8Array; + nonce: Uint8Array; + secret: Uint8Array; + scheme: string; + contentLength: number; + filename: string; +}; + +export class RemoteAttachmentCodec implements ContentCodec { + static async load( + remoteAttachment: RemoteAttachment, + codecRegistry: CodecRegistry, + ): Promise { + const response = await fetch(remoteAttachment.url); + const payload = new Uint8Array(await response.arrayBuffer()); + + if (!payload) { + throw new Error( + `no payload for remote attachment at ${remoteAttachment.url}`, + ); + } + + const digestBytes = new Uint8Array( + await crypto.subtle.digest("SHA-256", payload), + ); + const digest = secp.utils.bytesToHex(digestBytes); + + if (digest !== remoteAttachment.contentDigest) { + throw new Error("content digest does not match"); + } + + const ciphertext = new Ciphertext({ + aes256GcmHkdfSha256: { + hkdfSalt: remoteAttachment.salt, + gcmNonce: remoteAttachment.nonce, + payload, + }, + }); + + const encodedContentData = await decrypt( + ciphertext, + remoteAttachment.secret, + ); + const encodedContent = proto.EncodedContent.decode(encodedContentData); + + if (!encodedContent || !encodedContent.type) { + throw new Error("no encoded content"); + } + + const contentType = encodedContent.type; + if (!contentType) { + throw new Error("no content type"); + } + + const codec = codecRegistry.codecFor(new ContentTypeId(contentType)); + + if (!codec) { + throw new Error(`no codec found for ${encodedContent.type?.typeId}`); + } + + return codec.decode(encodedContent as EncodedContent, codecRegistry); + } + + static async encodeEncrypted( + content: T, + codec: ContentCodec, + ): Promise { + const secret = crypto.getRandomValues(new Uint8Array(32)); + const encodedContent = proto.EncodedContent.encode( + codec.encode(content, { + codecFor() { + return undefined; + }, + }), + ).finish(); + const ciphertext = await encrypt(encodedContent, secret); + const salt = ciphertext.aes256GcmHkdfSha256?.hkdfSalt; + const nonce = ciphertext.aes256GcmHkdfSha256?.gcmNonce; + const payload = ciphertext.aes256GcmHkdfSha256?.payload; + + if (!salt || !nonce || !payload) { + throw new Error("missing encryption key"); + } + + const digestBytes = new Uint8Array( + await crypto.subtle.digest("SHA-256", payload), + ); + const digest = secp.utils.bytesToHex(digestBytes); + + return { + digest, + secret, + salt, + nonce, + payload, + }; + } + + get contentType(): ContentTypeId { + return ContentTypeRemoteAttachment; + } + + encode(content: RemoteAttachment): EncodedContent { + if (!content.url.startsWith("https")) { + throw new Error("scheme must be https"); + } + + return { + type: ContentTypeRemoteAttachment, + parameters: { + contentDigest: content.contentDigest, + salt: secp.utils.bytesToHex(content.salt), + nonce: secp.utils.bytesToHex(content.nonce), + secret: secp.utils.bytesToHex(content.secret), + scheme: content.scheme, + contentLength: String(content.contentLength), + filename: content.filename, + }, + content: new TextEncoder().encode(content.url), + }; + } + + decode(content: EncodedContent): RemoteAttachment { + return { + url: new TextDecoder().decode(content.content), + contentDigest: content.parameters.contentDigest, + salt: secp.utils.hexToBytes(content.parameters.salt), + nonce: secp.utils.hexToBytes(content.parameters.nonce), + secret: secp.utils.hexToBytes(content.parameters.secret), + scheme: content.parameters.scheme, + contentLength: parseInt(content.parameters.contentLength, 10), + filename: content.parameters.filename, + }; + } + + fallback(content: RemoteAttachment): string | undefined { + return `Can’t display "${content.filename}". This app doesn’t support attachments.`; + } + + shouldPush() { + return true; + } +} diff --git a/content-types/content-type-remote-attachment/src/encryption.browser.ts b/content-types/content-type-remote-attachment/src/encryption.browser.ts new file mode 100644 index 000000000..edc514afb --- /dev/null +++ b/content-types/content-type-remote-attachment/src/encryption.browser.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line prefer-destructuring +export const crypto = window.crypto; diff --git a/content-types/content-type-remote-attachment/src/encryption.ts b/content-types/content-type-remote-attachment/src/encryption.ts new file mode 100644 index 000000000..bd4ac7fea --- /dev/null +++ b/content-types/content-type-remote-attachment/src/encryption.ts @@ -0,0 +1,3 @@ +import { webcrypto } from "node:crypto"; + +export const crypto = webcrypto; diff --git a/content-types/content-type-remote-attachment/src/index.ts b/content-types/content-type-remote-attachment/src/index.ts new file mode 100644 index 000000000..ae339d9d9 --- /dev/null +++ b/content-types/content-type-remote-attachment/src/index.ts @@ -0,0 +1,10 @@ +export { AttachmentCodec, ContentTypeAttachment } from "./Attachment"; +export type { Attachment } from "./Attachment"; +export { + RemoteAttachmentCodec, + ContentTypeRemoteAttachment, +} from "./RemoteAttachment"; +export type { + RemoteAttachment, + EncryptedEncodedContent, +} from "./RemoteAttachment"; diff --git a/content-types/content-type-remote-attachment/src/utils.ts b/content-types/content-type-remote-attachment/src/utils.ts new file mode 100644 index 000000000..acb5d3dd4 --- /dev/null +++ b/content-types/content-type-remote-attachment/src/utils.ts @@ -0,0 +1,6 @@ +// This is a variation of https://github.com/paulmillr/noble-secp256k1/blob/main/index.ts#L1378-L1388 +// that uses `digest('SHA-256', bytes)` instead of `digest('SHA-256', bytes.buffer)` +// which seems to produce different results. +export async function sha256(bytes: Uint8Array): Promise { + return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); +} diff --git a/content-types/content-type-remote-attachment/tsconfig.eslint.json b/content-types/content-type-remote-attachment/tsconfig.eslint.json new file mode 100644 index 000000000..5ee5f6f56 --- /dev/null +++ b/content-types/content-type-remote-attachment/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": [".", ".eslintrc.cjs", "rollup.config.js"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-remote-attachment/tsconfig.json b/content-types/content-type-remote-attachment/tsconfig.json new file mode 100644 index 000000000..69f454187 --- /dev/null +++ b/content-types/content-type-remote-attachment/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/build.json", + "include": ["src"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-remote-attachment/vitest.config.ts b/content-types/content-type-remote-attachment/vitest.config.ts new file mode 100644 index 000000000..2ee901930 --- /dev/null +++ b/content-types/content-type-remote-attachment/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + setupFiles: "./vitest.setup.ts", + }, +}); diff --git a/content-types/content-type-remote-attachment/vitest.setup.ts b/content-types/content-type-remote-attachment/vitest.setup.ts new file mode 100644 index 000000000..df951dbaa --- /dev/null +++ b/content-types/content-type-remote-attachment/vitest.setup.ts @@ -0,0 +1,3 @@ +import { Buffer } from "buffer"; + +globalThis.Buffer = Buffer; diff --git a/content-types/content-type-reply/.eslintrc.cjs b/content-types/content-type-reply/.eslintrc.cjs new file mode 100644 index 000000000..d46b01d75 --- /dev/null +++ b/content-types/content-type-reply/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ["custom"], + parserOptions: { + project: "./tsconfig.eslint.json", + }, +}; diff --git a/content-types/content-type-reply/CHANGELOG.md b/content-types/content-type-reply/CHANGELOG.md new file mode 100644 index 000000000..dfa532929 --- /dev/null +++ b/content-types/content-type-reply/CHANGELOG.md @@ -0,0 +1,85 @@ +# @xmtp/content-type-reply + +## 1.1.11 + +### Patch Changes + +- [#75](https://github.com/xmtp/xmtp-js-content-types/pull/75) [`da0bd85`](https://github.com/xmtp/xmtp-js-content-types/commit/da0bd8578d5f5032b221e25f02e8492b27929d6c) + - Use primitives package for types + +## 1.1.10 + +### Patch Changes + +- [#68](https://github.com/xmtp/xmtp-js-content-types/pull/68) [`8896b33`](https://github.com/xmtp/xmtp-js-content-types/commit/8896b33501b2860d68ea8be5e33a9cca5dd9315c) + - Add optional referenceInboxId + +## 1.1.9 + +### Patch Changes + +- [#65](https://github.com/xmtp/xmtp-js-content-types/pull/65) [`c4d43dc`](https://github.com/xmtp/xmtp-js-content-types/commit/c4d43dc948231de5c7f730e06f0931076de0673b) + - Add `shouldPush` to all content codecs + +## 1.1.8 + +### Patch Changes + +- [#60](https://github.com/xmtp/xmtp-js-content-types/pull/60) [`5b9310a`](https://github.com/xmtp/xmtp-js-content-types/commit/5b9310ac89fd23e5cfd74903894073b6ef8af7c3) + - Upgraded JS SDK to `11.3.12` + +## 1.1.7 + +### Patch Changes + +- [#54](https://github.com/xmtp/xmtp-js-content-types/pull/54) [`718cb9f`](https://github.com/xmtp/xmtp-js-content-types/commit/718cb9fec51f74bf2402f3f22160687cae35dda8) + - Updated Turbo config to remove `generate:types` command and instead rely on `build` + - Removed all `generate:types` commands from `package.json` files + - Updated shared ESLint config and local ESLint configs + - Updated `include` field of `tsconfig.json` and `tsconfig.eslint.json` files + - Replaced `tsup` with `rollup` + +## 1.1.6 + +### Patch Changes + +- [#51](https://github.com/xmtp/xmtp-js-content-types/pull/51) [`aeb6db7`](https://github.com/xmtp/xmtp-js-content-types/commit/aeb6db73a63409a33c7d3d3431e33682b0ce4c4d) + - Only publish files in the `/dist` directory + +## 1.1.5 + +### Patch Changes + +- Upgraded `@xmtp/proto` package +- Upgraded `@xmtp/xmtp-js` package + +## 1.1.4 + +### Patch Changes + +- Update `@xmtp/proto` to latest version + +## 1.1.3 + +### Patch Changes + +- Upgrade to JS SDK v11 +- Update client initialization for tests to use `codecs` option for proper types + +## 1.1.2 + +### Patch Changes + +- fix: update the copy for the default fallbacks + +## 1.1.1 + +### Patch Changes + +- Gets the nested type of a reply from the deserialized EncodedContent instead of inspecting the parameter map + +## 1.1.0 + +### Minor Changes + +- Add dummy fallback field to all content types diff --git a/content-types/content-type-reply/LICENSE b/content-types/content-type-reply/LICENSE new file mode 100644 index 000000000..ae6695abd --- /dev/null +++ b/content-types/content-type-reply/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 XMTP (xmtp.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/content-types/content-type-reply/README.md b/content-types/content-type-reply/README.md new file mode 100644 index 000000000..874b19eac --- /dev/null +++ b/content-types/content-type-reply/README.md @@ -0,0 +1,98 @@ +# Reply content type + +![Status](https://img.shields.io/badge/Content_type_status-Standards--track-yellow) ![Status](https://img.shields.io/badge/Reference_implementation_status-Beta-yellow) + +This package provides an XMTP content type to support direct replies to messages. + +> **Open for feedback** +> You are welcome to provide feedback on this implementation by commenting on the [Proposal for Reply content type](https://github.com/orgs/xmtp/discussions/35). + +## What’s a reply? + +A reply action is a way to respond directly to a specific message in a conversation. Instead of sending a new message, users can select and reply to a particular message. + +## Why replies? + +Providing replies in your app enables users to maintain context and clarity in their conversations. Replies can also help organize messages, making messages easier to find and reference in the future. This user experience can help make your app a great tool for collaboration. + +## Install the package + +```bash +# npm +npm i @xmtp/content-type-reply + +# yarn +yarn add @xmtp/content-type-reply + +# pnpm +pnpm i @xmtp/content-type-reply +``` + +## Create a reply + +With XMTP, replies are represented as objects with the following keys: + +- `reference`: The message ID for the message that is being reacted to +- `content`: A string representation of the reply + +```tsx +const reply: Reply = { + reference: someMessageID, + content: "I concur", +}; +``` + +## Send a reply + +Now that you have a reply, you can send it: + +```tsx +await conversation.messages.send(reply, { + contentType: ContentTypeReply, +}); +``` + +> **Note** +> `contentFallback` text is provided by the codec and gives clients that _don't_ support a content type the option to display some useful context. For cases where clients *do* support the content type, they can use the content fallback as alt text for accessibility purposes. + +## Receive a reply + +Now that you can send a reply, you need a way to receive a reply. For example: + +```tsx +// Assume `loadLastMessage` is a thing you have +const message: DecodedMessage = await loadLastMessage(); + +if (!message.contentType.sameAs(ContentTypeReply)) { + // We do not have a reply. A topic for another blog post. + return; +} + +// We've got a reply. +const reply: Reply = message.content; +``` + +## Display the reply + +Generally, replies should be displayed alongside the original message to provide context. Ultimately, how you choose to display replies is completely up to you. + +## Developing + +Run `yarn dev` to build the content type and watch for changes, which will trigger a rebuild. + +## Testing + +Before running unit tests, start the required Docker container at the root of this repository. For more info, see [Running tests](../../README.md#running-tests). + +## Useful commands + +- `yarn build`: Builds the content type +- `yarn clean`: Removes `node_modules`, `dist`, and `.turbo` folders +- `yarn dev`: Builds the content type and watches for changes, which will trigger a rebuild +- `yarn format`: Runs Prettier format and write changes +- `yarn format:check`: Runs Prettier format check +- `yarn lint`: Runs ESLint +- `yarn test:setup`: Starts a necessary Docker container for testing +- `yarn test:teardown`: Stops Docker container for testing +- `yarn test`: Runs all unit tests +- `yarn typecheck`: Runs `tsc` diff --git a/content-types/content-type-reply/package.json b/content-types/content-type-reply/package.json new file mode 100644 index 000000000..a5650625c --- /dev/null +++ b/content-types/content-type-reply/package.json @@ -0,0 +1,94 @@ +{ + "name": "@xmtp/content-type-reply", + "version": "1.1.11", + "description": "An XMTP content type to support replying to a message", + "keywords": [ + "xmtp", + "messaging", + "web3", + "js", + "ts", + "javascript", + "typescript", + "content-types" + ], + "homepage": "https://github.com/xmtp/xmtp-js", + "bugs": { + "url": "https://github.com/xmtp/xmtp-js/issues" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/xmtp/xmtp-js.git", + "directory": "content-types/content-type-reply" + }, + "license": "MIT", + "author": "XMTP Labs ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "browser": "./dist/browser/index.js", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "main": "dist/index.cjs", + "module": "dist/index.js", + "browser": "dist/browser/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn clean:dist && yarn rollup -c", + "clean": "yarn clean:dist && rimraf .turbo node_modules", + "clean:dist": "rimraf dist", + "dev": "yarn clean:dist && yarn rollup -c --watch", + "lint": "eslint . --ignore-path ../../.gitignore", + "test": "yarn test:node && yarn test:jsdom", + "test:jsdom": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment happy-dom", + "test:node": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment node", + "typecheck": "tsc --noEmit" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome versions", + "last 3 firefox versions", + "last 3 safari versions" + ] + }, + "dependencies": { + "@xmtp/content-type-primitives": "^1.0.1", + "@xmtp/proto": "^3.61.1" + }, + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.0", + "@types/node": "^18.19.22", + "@xmtp/content-type-remote-attachment": "workspace:*", + "@xmtp/xmtp-js": "^11.6.3", + "buffer": "^6.0.3", + "eslint": "^8.57.0", + "eslint-config-custom": "workspace:*", + "ethers": "^6.11.1", + "happy-dom": "^13.7.3", + "rimraf": "^6.0.1", + "rollup": "^4.24.0", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-filesize": "^10.0.0", + "typescript": "^5.6.3", + "vite": "^5.1.6", + "vitest": "^2.1.2" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "registry": "https://registry.npmjs.org/" + } +} diff --git a/content-types/content-type-reply/rollup.config.js b/content-types/content-type-reply/rollup.config.js new file mode 100644 index 000000000..92d393a56 --- /dev/null +++ b/content-types/content-type-reply/rollup.config.js @@ -0,0 +1,58 @@ +import terser from "@rollup/plugin-terser"; +import typescript from "@rollup/plugin-typescript"; +import { defineConfig } from "rollup"; +import { dts } from "rollup-plugin-dts"; +import filesize from "rollup-plugin-filesize"; + +const plugins = [ + typescript({ + declaration: false, + declarationMap: false, + }), + filesize({ + showMinifiedSize: false, + }), +]; + +const external = ["@xmtp/proto", "@xmtp/content-type-primitives"]; + +export default defineConfig([ + { + input: "src/index.ts", + output: { + file: "dist/index.js", + format: "es", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/browser/index.js", + format: "es", + sourcemap: true, + }, + plugins: [...plugins, terser()], + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.cjs", + format: "cjs", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.d.ts", + format: "es", + }, + plugins: [dts()], + }, +]); diff --git a/content-types/content-type-reply/src/Reply.test.ts b/content-types/content-type-reply/src/Reply.test.ts new file mode 100644 index 000000000..089304991 --- /dev/null +++ b/content-types/content-type-reply/src/Reply.test.ts @@ -0,0 +1,117 @@ +import { + AttachmentCodec, + ContentTypeAttachment, + type Attachment, +} from "@xmtp/content-type-remote-attachment"; +import { Client, ContentTypeText } from "@xmtp/xmtp-js"; +import { Wallet } from "ethers"; +import { ContentTypeReply, ReplyCodec, type Reply } from "./Reply"; + +describe("ReplyContentType", () => { + it("has the right content type", () => { + expect(ContentTypeReply.authorityId).toBe("xmtp.org"); + expect(ContentTypeReply.typeId).toBe("reply"); + expect(ContentTypeReply.versionMajor).toBe(1); + expect(ContentTypeReply.versionMinor).toBe(0); + }); + + it("can send a text reply", async () => { + const aliceWallet = Wallet.createRandom(); + const aliceClient = await Client.create(aliceWallet, { + codecs: [new ReplyCodec()], + env: "local", + }); + await aliceClient.publishUserContact(); + + const bobWallet = Wallet.createRandom(); + const bobClient = await Client.create(bobWallet, { + codecs: [new ReplyCodec()], + env: "local", + }); + await bobClient.publishUserContact(); + + const conversation = await aliceClient.conversations.newConversation( + bobWallet.address, + ); + + const originalMessage = await conversation.send("test"); + + const reply: Reply = { + content: "LGTM", + contentType: ContentTypeText, + reference: originalMessage.id, + }; + + await conversation.send(reply, { contentType: ContentTypeReply }); + + const bobConversation = await bobClient.conversations.newConversation( + aliceWallet.address, + ); + const messages = await bobConversation.messages(); + + expect(messages.length).toBe(2); + + const replyMessage = messages[1]; + const messageContent = replyMessage.content as Reply; + expect(messageContent.content).toBe("LGTM"); + expect(messageContent.reference).toBe(originalMessage.id); + }); + + it("can send an attachment reply", async () => { + const aliceWallet = Wallet.createRandom(); + const aliceClient = await Client.create(aliceWallet, { + codecs: [new ReplyCodec(), new AttachmentCodec()], + env: "local", + }); + await aliceClient.publishUserContact(); + + const bobWallet = Wallet.createRandom(); + const bobClient = await Client.create(bobWallet, { + codecs: [new ReplyCodec(), new AttachmentCodec()], + env: "local", + }); + await bobClient.publishUserContact(); + + const conversation = await aliceClient.conversations.newConversation( + bobWallet.address, + ); + + const originalMessage = await conversation.send("test"); + + const attachment: Attachment = { + filename: "test.png", + mimeType: "image/png", + data: Uint8Array.from([5, 4, 3, 2, 1]), + }; + + const reply: Reply = { + content: attachment, + contentType: ContentTypeAttachment, + reference: originalMessage.id, + }; + + await conversation.send(reply, { contentType: ContentTypeReply }); + + const bobConversation = await bobClient.conversations.newConversation( + aliceWallet.address, + ); + const messages = await bobConversation.messages(); + + expect(messages.length).toBe(2); + + const replyMessage = messages[1]; + const messageContent = replyMessage.content as Reply; + expect(ContentTypeAttachment.sameAs(messageContent.contentType)).toBe(true); + expect(messageContent.content).toEqual({ + filename: "test.png", + mimeType: "image/png", + data: Uint8Array.from([5, 4, 3, 2, 1]), + }); + expect(messageContent.reference).toBe(originalMessage.id); + }); + + it("has a proper shouldPush value", () => { + const codec = new ReplyCodec(); + expect(codec.shouldPush()).toBe(true); + }); +}); diff --git a/content-types/content-type-reply/src/Reply.ts b/content-types/content-type-reply/src/Reply.ts new file mode 100644 index 000000000..ab2e0f85f --- /dev/null +++ b/content-types/content-type-reply/src/Reply.ts @@ -0,0 +1,103 @@ +import { + ContentTypeId, + type CodecRegistry, + type ContentCodec, + type EncodedContent, +} from "@xmtp/content-type-primitives"; +import { content as proto } from "@xmtp/proto"; + +export const ContentTypeReply = new ContentTypeId({ + authorityId: "xmtp.org", + typeId: "reply", + versionMajor: 1, + versionMinor: 0, +}); + +export type Reply = { + /** + * The message ID for the message that is being replied to + */ + reference: string; + /** + * The inbox ID of the user who sent the message that is being replied to + * + * This only applies to group messages + */ + referenceInboxId?: string; + /** + * The content of the reply + */ + content: any; + /** + * The content type of the reply + */ + contentType: ContentTypeId; +}; + +export class ReplyCodec implements ContentCodec { + get contentType(): ContentTypeId { + return ContentTypeReply; + } + + encode(content: Reply, registry: CodecRegistry): EncodedContent { + const codec = registry.codecFor(content.contentType); + if (!codec) { + throw new Error( + `missing codec for content type "${content.contentType.toString()}"`, + ); + } + + const encodedContent = codec.encode(content.content, registry); + const bytes = proto.EncodedContent.encode(encodedContent).finish(); + + const parameters: Record = { + // TODO: cut when we're certain no one is looking for "contentType" here. + contentType: content.contentType.toString(), + reference: content.reference, + }; + + // add referenceInboxId if it's present + if (content.referenceInboxId) { + parameters.referenceInboxId = content.referenceInboxId; + } + + return { + type: this.contentType, + parameters, + content: bytes, + }; + } + + decode(content: EncodedContent, registry: CodecRegistry): Reply { + const decodedContent = proto.EncodedContent.decode(content.content); + if (!decodedContent.type) { + throw new Error("missing content type"); + } + const contentType = new ContentTypeId(decodedContent.type); + const codec = registry.codecFor(contentType); + if (!codec) { + throw new Error( + `missing codec for content type "${contentType.toString()}"`, + ); + } + + return { + reference: content.parameters.reference, + referenceInboxId: content.parameters.referenceInboxId, + contentType, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + content: codec.decode(decodedContent as EncodedContent, registry), + }; + } + + fallback(content: Reply): string | undefined { + if (typeof content.content === "string") { + return `Replied with “${content.content}” to an earlier message`; + } + return "Replied to an earlier message"; + } + + shouldPush() { + return true; + } +} diff --git a/content-types/content-type-reply/src/index.ts b/content-types/content-type-reply/src/index.ts new file mode 100644 index 000000000..649645531 --- /dev/null +++ b/content-types/content-type-reply/src/index.ts @@ -0,0 +1,2 @@ +export { ReplyCodec, ContentTypeReply } from "./Reply"; +export type { Reply } from "./Reply"; diff --git a/content-types/content-type-reply/tsconfig.eslint.json b/content-types/content-type-reply/tsconfig.eslint.json new file mode 100644 index 000000000..5ee5f6f56 --- /dev/null +++ b/content-types/content-type-reply/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": [".", ".eslintrc.cjs", "rollup.config.js"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-reply/tsconfig.json b/content-types/content-type-reply/tsconfig.json new file mode 100644 index 000000000..69f454187 --- /dev/null +++ b/content-types/content-type-reply/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/build.json", + "include": ["src"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-reply/vitest.config.ts b/content-types/content-type-reply/vitest.config.ts new file mode 100644 index 000000000..2ee901930 --- /dev/null +++ b/content-types/content-type-reply/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + setupFiles: "./vitest.setup.ts", + }, +}); diff --git a/content-types/content-type-reply/vitest.setup.ts b/content-types/content-type-reply/vitest.setup.ts new file mode 100644 index 000000000..df951dbaa --- /dev/null +++ b/content-types/content-type-reply/vitest.setup.ts @@ -0,0 +1,3 @@ +import { Buffer } from "buffer"; + +globalThis.Buffer = Buffer; diff --git a/content-types/content-type-text/.eslintrc.cjs b/content-types/content-type-text/.eslintrc.cjs new file mode 100644 index 000000000..d46b01d75 --- /dev/null +++ b/content-types/content-type-text/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ["custom"], + parserOptions: { + project: "./tsconfig.eslint.json", + }, +}; diff --git a/content-types/content-type-text/LICENSE b/content-types/content-type-text/LICENSE new file mode 100644 index 000000000..ae6695abd --- /dev/null +++ b/content-types/content-type-text/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 XMTP (xmtp.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/content-types/content-type-text/README.md b/content-types/content-type-text/README.md new file mode 100644 index 000000000..f3643886d --- /dev/null +++ b/content-types/content-type-text/README.md @@ -0,0 +1,43 @@ +# Text content type + +This package provides an XMTP content type to support text messages. + +## Install the package + +```bash +# npm +npm i @xmtp/content-type-text + +# yarn +yarn add @xmtp/content-type-text + +# pnpm +pnpm i @xmtp/content-type-text +``` + +## Send a text message + +Use a string to send a text message. It's not required to specify a content type in the send options for text messages. + +```tsx +await conversation.send("gm"); +``` + +## Developing + +Run `yarn dev` to build the content type and watch for changes, which will trigger a rebuild. + +## Testing + +Before running unit tests, start the required Docker container at the root of this repository. For more info, see [Running tests](../../README.md#running-tests). + +## Useful commands + +- `yarn build`: Builds the content type +- `yarn clean`: Removes `node_modules`, `dist`, and `.turbo` folders +- `yarn dev`: Builds the content type and watches for changes, which will trigger a rebuild +- `yarn lint`: Runs ESLint +- `yarn test:setup`: Starts a necessary docker container for testing +- `yarn test:teardown`: Stops docker container for testing +- `yarn test`: Runs all unit tests +- `yarn typecheck`: Runs `tsc` diff --git a/content-types/content-type-text/package.json b/content-types/content-type-text/package.json new file mode 100644 index 000000000..15db5526d --- /dev/null +++ b/content-types/content-type-text/package.json @@ -0,0 +1,92 @@ +{ + "name": "@xmtp/content-type-text", + "version": "1.0.0", + "description": "An XMTP content type to support text messages", + "keywords": [ + "xmtp", + "messaging", + "web3", + "js", + "ts", + "javascript", + "typescript", + "content-types" + ], + "homepage": "https://github.com/xmtp/xmtp-js", + "bugs": { + "url": "https://github.com/xmtp/xmtp-js/issues" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/xmtp/xmtp-js.git", + "directory": "content-types/content-type-text" + }, + "license": "MIT", + "author": "XMTP Labs ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "browser": "./dist/browser/index.js", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "main": "dist/index.cjs", + "module": "dist/index.js", + "browser": "dist/browser/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn clean:dist && yarn rollup -c", + "clean": "yarn clean:dist && rimraf .turbo node_modules", + "clean:dist": "rimraf dist", + "dev": "yarn clean:dist && yarn rollup -c --watch", + "lint": "eslint . --ignore-path ../../.gitignore", + "test": "yarn test:node && yarn test:jsdom", + "test:jsdom": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment happy-dom", + "test:node": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment node", + "typecheck": "tsc --noEmit" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome versions", + "last 3 firefox versions", + "last 3 safari versions" + ] + }, + "dependencies": { + "@xmtp/content-type-primitives": "^1.0.1" + }, + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.0", + "@types/node": "^18.19.22", + "@xmtp/xmtp-js": "^11.6.3", + "buffer": "^6.0.3", + "eslint": "^8.57.0", + "eslint-config-custom": "workspace:*", + "ethers": "^6.11.1", + "happy-dom": "^13.7.3", + "rimraf": "^6.0.1", + "rollup": "^4.24.0", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-filesize": "^10.0.0", + "typescript": "^5.6.3", + "vite": "^5.1.6", + "vitest": "^2.1.2" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "registry": "https://registry.npmjs.org/" + } +} diff --git a/content-types/content-type-text/rollup.config.js b/content-types/content-type-text/rollup.config.js new file mode 100644 index 000000000..099cfcf95 --- /dev/null +++ b/content-types/content-type-text/rollup.config.js @@ -0,0 +1,58 @@ +import terser from "@rollup/plugin-terser"; +import typescript from "@rollup/plugin-typescript"; +import { defineConfig } from "rollup"; +import { dts } from "rollup-plugin-dts"; +import filesize from "rollup-plugin-filesize"; + +const plugins = [ + typescript({ + declaration: false, + declarationMap: false, + }), + filesize({ + showMinifiedSize: false, + }), +]; + +const external = ["@xmtp/content-type-primitives"]; + +export default defineConfig([ + { + input: "src/index.ts", + output: { + file: "dist/index.js", + format: "es", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/browser/index.js", + format: "es", + sourcemap: true, + }, + plugins: [...plugins, terser()], + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.cjs", + format: "cjs", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.d.ts", + format: "es", + }, + plugins: [dts()], + }, +]); diff --git a/content-types/content-type-text/src/Text.test.ts b/content-types/content-type-text/src/Text.test.ts new file mode 100644 index 000000000..8f98d7d1c --- /dev/null +++ b/content-types/content-type-text/src/Text.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { ContentTypeText, Encoding, TextCodec } from "./Text"; + +describe("ContentTypeText", () => { + it("can encode/decode text", () => { + const text = "Hey"; + const codec = new TextCodec(); + const ec = codec.encode(text); + expect(ec.type.sameAs(ContentTypeText)).toBe(true); + expect(ec.parameters.encoding).toEqual(Encoding.utf8); + const text2 = codec.decode(ec); + expect(text2).toEqual(text); + }); + + it("defaults to utf-8", () => { + const text = "Hey"; + const codec = new TextCodec(); + const ec = codec.encode(text); + expect(ec.type.sameAs(ContentTypeText)).toBe(true); + expect(ec.parameters.encoding).toEqual(Encoding.utf8); + const text2 = codec.decode(ec); + expect(text2).toEqual(text); + }); + + it("throws on invalid input", () => { + const codec = new TextCodec(); + const ec = { + type: ContentTypeText, + parameters: {}, + content: {} as Uint8Array, + }; + expect(() => codec.decode(ec)).toThrow(); + }); + + it("throws on unknown encoding", () => { + const codec = new TextCodec(); + const ec = { + type: ContentTypeText, + parameters: { encoding: "UTF-16" }, + content: new Uint8Array(0), + }; + expect(() => codec.decode(ec)).toThrow("unrecognized encoding UTF-16"); + }); +}); diff --git a/content-types/content-type-text/src/Text.ts b/content-types/content-type-text/src/Text.ts new file mode 100644 index 000000000..12f747147 --- /dev/null +++ b/content-types/content-type-text/src/Text.ts @@ -0,0 +1,46 @@ +import { + ContentTypeId, + type ContentCodec, + type EncodedContent, +} from "@xmtp/content-type-primitives"; + +export const ContentTypeText = new ContentTypeId({ + authorityId: "xmtp.org", + typeId: "text", + versionMajor: 1, + versionMinor: 0, +}); + +export enum Encoding { + utf8 = "UTF-8", +} + +export class TextCodec implements ContentCodec { + get contentType(): ContentTypeId { + return ContentTypeText; + } + + encode(content: string): EncodedContent { + return { + type: ContentTypeText, + parameters: { encoding: Encoding.utf8 }, + content: new TextEncoder().encode(content), + }; + } + + decode(content: EncodedContent) { + const { encoding } = content.parameters; + if ((encoding as Encoding) !== Encoding.utf8) { + throw new Error(`unrecognized encoding ${encoding}`); + } + return new TextDecoder().decode(content.content); + } + + fallback() { + return undefined; + } + + shouldPush() { + return true; + } +} diff --git a/content-types/content-type-text/src/index.ts b/content-types/content-type-text/src/index.ts new file mode 100644 index 000000000..41b613ef9 --- /dev/null +++ b/content-types/content-type-text/src/index.ts @@ -0,0 +1 @@ +export { ContentTypeText, Encoding, TextCodec } from "./Text"; diff --git a/content-types/content-type-text/tsconfig.eslint.json b/content-types/content-type-text/tsconfig.eslint.json new file mode 100644 index 000000000..5ee5f6f56 --- /dev/null +++ b/content-types/content-type-text/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": [".", ".eslintrc.cjs", "rollup.config.js"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-text/tsconfig.json b/content-types/content-type-text/tsconfig.json new file mode 100644 index 000000000..69f454187 --- /dev/null +++ b/content-types/content-type-text/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/build.json", + "include": ["src"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-transaction-reference/.eslintrc.cjs b/content-types/content-type-transaction-reference/.eslintrc.cjs new file mode 100644 index 000000000..d46b01d75 --- /dev/null +++ b/content-types/content-type-transaction-reference/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ["custom"], + parserOptions: { + project: "./tsconfig.eslint.json", + }, +}; diff --git a/content-types/content-type-transaction-reference/CHANGELOG.md b/content-types/content-type-transaction-reference/CHANGELOG.md new file mode 100644 index 000000000..43d8ae7de --- /dev/null +++ b/content-types/content-type-transaction-reference/CHANGELOG.md @@ -0,0 +1,29 @@ +# @xmtp/content-type-transaction-reference + +## 1.0.4 + +### Patch Changes + +- [#75](https://github.com/xmtp/xmtp-js-content-types/pull/75) [`da0bd85`](https://github.com/xmtp/xmtp-js-content-types/commit/da0bd8578d5f5032b221e25f02e8492b27929d6c) + - Use primitives package for types + +## 1.0.3 + +### Patch Changes + +- [#65](https://github.com/xmtp/xmtp-js-content-types/pull/65) [`c4d43dc`](https://github.com/xmtp/xmtp-js-content-types/commit/c4d43dc948231de5c7f730e06f0931076de0673b) + - Add `shouldPush` to all content codecs + +## 1.0.2 + +### Patch Changes + +- [#60](https://github.com/xmtp/xmtp-js-content-types/pull/60) [`5b9310a`](https://github.com/xmtp/xmtp-js-content-types/commit/5b9310ac89fd23e5cfd74903894073b6ef8af7c3) + - Upgraded JS SDK to `11.3.12` + +## 1.0.1 + +### Patch Changes + +- [#51](https://github.com/xmtp/xmtp-js-content-types/pull/51) [`aeb6db7`](https://github.com/xmtp/xmtp-js-content-types/commit/aeb6db73a63409a33c7d3d3431e33682b0ce4c4d) + - Only publish files in the `/dist` directory diff --git a/content-types/content-type-transaction-reference/LICENSE b/content-types/content-type-transaction-reference/LICENSE new file mode 100644 index 000000000..ae6695abd --- /dev/null +++ b/content-types/content-type-transaction-reference/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 XMTP (xmtp.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/content-types/content-type-transaction-reference/README.md b/content-types/content-type-transaction-reference/README.md new file mode 100644 index 000000000..584aeede1 --- /dev/null +++ b/content-types/content-type-transaction-reference/README.md @@ -0,0 +1,117 @@ +# Transaction reference content type + +![Status](https://img.shields.io/badge/Content_type_status-Standards--track-yellow) ![Status](https://img.shields.io/badge/Reference_implementation_status-Beta-yellow) + +This package provides an XMTP content type to support on-chain transaction references. + +> **Open for feedback** +> You are welcome to provide feedback on this implementation by commenting on the [Proposal for on-chain transaction reference content type](https://github.com/orgs/xmtp/discussions/37). + +## What’s a transaction reference? + +It is a reference to an on-chain transaction sent as a message. This content type facilitates sharing transaction hashes or IDs, thereby providing a direct link to on-chain activities. + +## Why transaction references? + +Transaction references serve to display transaction details, facilitating the sharing of on-chain activities, such as token transfers, between users. + +## Install the package + +```bash +# npm +npm i @xmtp/content-type-transaction-reference + +# yarn +yarn add @xmtp/content-type-transaction-reference + +# pnpm +pnpm i @xmtp/content-type-transaction-reference +``` + +## Create a transaction reference + +With XMTP, a transaction reference is represented as an object with the following keys: + +```tsx +const transactionReference: TransactionReference = { + /** + * Optional namespace for the networkId + */ + namespace: "eip155"; + /** + * The networkId for the transaction, in decimal or hexidecimal format + */ + networkId: 1; + /** + * The transaction hash + */ + reference: "0x123...abc"; + /** + * Optional metadata object + */ + metadata: { + transactionType: "transfer", + currency: "USDC", + amount: 100000, // In integer format, this represents 1 USDC (100000/10^6) + decimals: 6, // Specifies that the currency uses 6 decimal places + fromAddress: "0x456...def", + toAddress: "0x789...ghi" + }; +}; +``` + +## Send a transaction reference + +Once you have a transaction reference, you can send it as part of your conversation: + +```tsx +await conversation.messages.send(transactionReference, { + contentType: ContentTypeTransactionReference, +}); +``` + +## Receive a transaction reference + +To receive and process a transaction reference: + +```tsx +// Assume `loadLastMessage` is a thing you have +const message: DecodedMessage = await loadLastMessage(); + +if (!message.contentType.sameAs(ContentTypeTransactionReference)) { + // Handle non-transaction reference message + return; +} + +const transactionRef: TransactionReference = message.content; +// Process the transaction reference here +``` + +## Display the transaction reference + +Displaying a transaction reference typically involves rendering details such as the transaction hash, network ID, and any relevant metadata. The exact UI representation can vary based on your application's design, you might want to fetch on-chain data before showing them to the user. + +## Note on Metadata + +The optional metadata within a transaction reference, such as transaction type, currency, amount, and addresses, are provided for informational purposes only. These details should not be solely relied upon for verifying transaction specifics. Developers are responsible for ensuring the accuracy of transaction data, either by directing users to the appropriate block explorer or by fetching and displaying verified transaction data from the blockchain. + +## Developing + +Run `yarn dev` to build the content type and watch for changes, which will trigger a rebuild. + +## Testing + +Before running unit tests, start the required Docker container at the root of this repository. For more info, see [Running tests](../../README.md#running-tests). + +## Useful commands + +- `yarn build`: Builds the content type +- `yarn clean`: Removes `node_modules`, `dist`, and `.turbo` folders +- `yarn dev`: Builds the content type and watches for changes, which will trigger a rebuild +- `yarn format`: Runs Prettier format and write changes +- `yarn format:check`: Runs Prettier format check +- `yarn lint`: Runs ESLint +- `yarn test:setup`: Starts a necessary Docker container for testing +- `yarn test:teardown`: Stops Docker container for testing +- `yarn test`: Runs all unit tests +- `yarn typecheck`: Runs `tsc` diff --git a/content-types/content-type-transaction-reference/package.json b/content-types/content-type-transaction-reference/package.json new file mode 100644 index 000000000..7c7ddadb4 --- /dev/null +++ b/content-types/content-type-transaction-reference/package.json @@ -0,0 +1,92 @@ +{ + "name": "@xmtp/content-type-transaction-reference", + "version": "1.0.4", + "description": "An XMTP content type to support on-chain transaction references", + "keywords": [ + "xmtp", + "messaging", + "web3", + "js", + "ts", + "javascript", + "typescript", + "content-types" + ], + "homepage": "https://github.com/xmtp/xmtp-js", + "bugs": { + "url": "https://github.com/xmtp/xmtp-js/issues" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/xmtp/xmtp-js.git", + "directory": "content-types/content-type-transaction-reference" + }, + "license": "MIT", + "author": "XMTP Labs ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "browser": "./dist/browser/index.js", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "main": "dist/index.cjs", + "module": "dist/index.js", + "browser": "dist/browser/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn clean:dist && yarn rollup -c", + "clean": "yarn clean:dist && rimraf .turbo node_modules", + "clean:dist": "rimraf dist", + "dev": "yarn clean:dist && yarn rollup -c --watch", + "lint": "eslint . --ignore-path ../../.gitignore", + "test": "yarn test:node && yarn test:jsdom", + "test:jsdom": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment happy-dom", + "test:node": "NODE_TLS_REJECT_UNAUTHORIZED=0 vitest run --environment node", + "typecheck": "tsc --noEmit" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome versions", + "last 3 firefox versions", + "last 3 safari versions" + ] + }, + "dependencies": { + "@xmtp/content-type-primitives": "^1.0.1" + }, + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.0", + "@types/node": "^18.19.22", + "@xmtp/xmtp-js": "^11.6.3", + "buffer": "^6.0.3", + "eslint": "^8.57.0", + "eslint-config-custom": "workspace:*", + "ethers": "^6.11.1", + "happy-dom": "^13.7.3", + "rimraf": "^6.0.1", + "rollup": "^4.24.0", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-filesize": "^10.0.0", + "typescript": "^5.6.3", + "vite": "^5.1.6", + "vitest": "^2.1.2" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "registry": "https://registry.npmjs.org/" + } +} diff --git a/content-types/content-type-transaction-reference/rollup.config.js b/content-types/content-type-transaction-reference/rollup.config.js new file mode 100644 index 000000000..099cfcf95 --- /dev/null +++ b/content-types/content-type-transaction-reference/rollup.config.js @@ -0,0 +1,58 @@ +import terser from "@rollup/plugin-terser"; +import typescript from "@rollup/plugin-typescript"; +import { defineConfig } from "rollup"; +import { dts } from "rollup-plugin-dts"; +import filesize from "rollup-plugin-filesize"; + +const plugins = [ + typescript({ + declaration: false, + declarationMap: false, + }), + filesize({ + showMinifiedSize: false, + }), +]; + +const external = ["@xmtp/content-type-primitives"]; + +export default defineConfig([ + { + input: "src/index.ts", + output: { + file: "dist/index.js", + format: "es", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/browser/index.js", + format: "es", + sourcemap: true, + }, + plugins: [...plugins, terser()], + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.cjs", + format: "cjs", + sourcemap: true, + }, + plugins, + external, + }, + { + input: "src/index.ts", + output: { + file: "dist/index.d.ts", + format: "es", + }, + plugins: [dts()], + }, +]); diff --git a/content-types/content-type-transaction-reference/src/TransactionReference.test.ts b/content-types/content-type-transaction-reference/src/TransactionReference.test.ts new file mode 100644 index 000000000..f2c0d4744 --- /dev/null +++ b/content-types/content-type-transaction-reference/src/TransactionReference.test.ts @@ -0,0 +1,74 @@ +import { Client } from "@xmtp/xmtp-js"; +import { Wallet } from "ethers"; +import { + ContentTypeTransactionReference, + TransactionReferenceCodec, + type TransactionReference, +} from "./TransactionReference"; + +test("content type exists", () => { + expect(ContentTypeTransactionReference.authorityId).toBe("xmtp.org"); + expect(ContentTypeTransactionReference.typeId).toBe("transactionReference"); + expect(ContentTypeTransactionReference.versionMajor).toBe(1); + expect(ContentTypeTransactionReference.versionMinor).toBe(0); +}); + +test("should successfully send and receive a TransactionReference message", async () => { + const aliceWallet = Wallet.createRandom(); + const aliceClient = await Client.create(aliceWallet, { + codecs: [new TransactionReferenceCodec()], + env: "local", + }); + await aliceClient.publishUserContact(); + + const bobWallet = Wallet.createRandom(); + const bobClient = await Client.create(bobWallet, { + codecs: [new TransactionReferenceCodec()], + env: "local", + }); + await bobClient.publishUserContact(); + + const conversation = await aliceClient.conversations.newConversation( + bobWallet.address, + ); + + const transactionRefToSend: TransactionReference = { + namespace: "eip155", + networkId: "0x14a33", + reference: + "0xa7cd32b79204559e46b4ef9b519fce58cedb25246f48d0c00bd628e873a81f2f", + metadata: { + transactionType: "transfer", + currency: "USDC", + amount: 1337, + decimals: 6, + fromAddress: aliceWallet.address, + toAddress: bobWallet.address, + }, + }; + + await conversation.send(transactionRefToSend, { + contentType: ContentTypeTransactionReference, + }); + + const bobConversation = await bobClient.conversations.newConversation( + aliceWallet.address, + ); + + const messages = await bobConversation.messages(); + + expect(messages.length).toBe(1); + + const message = messages[0]; + const messageContent = message.content as TransactionReference; + + expect(messageContent.namespace).toBe(transactionRefToSend.namespace); + expect(messageContent.networkId).toBe(transactionRefToSend.networkId); + expect(messageContent.reference).toBe(transactionRefToSend.reference); + expect(messageContent.metadata).toEqual(transactionRefToSend.metadata); +}); + +test("has a proper shouldPush value", () => { + const codec = new TransactionReferenceCodec(); + expect(codec.shouldPush()).toBe(true); +}); diff --git a/content-types/content-type-transaction-reference/src/TransactionReference.ts b/content-types/content-type-transaction-reference/src/TransactionReference.ts new file mode 100644 index 000000000..58c4a8d51 --- /dev/null +++ b/content-types/content-type-transaction-reference/src/TransactionReference.ts @@ -0,0 +1,74 @@ +import { + ContentTypeId, + type ContentCodec, + type EncodedContent, +} from "@xmtp/content-type-primitives"; + +export const ContentTypeTransactionReference = new ContentTypeId({ + authorityId: "xmtp.org", + typeId: "transactionReference", + versionMajor: 1, + versionMinor: 0, +}); + +export type TransactionReference = { + /** + * The namespace for the networkId + */ + namespace?: string; + /** + * The networkId for the transaction, in decimal or hexidecimal format + */ + networkId: number | string; + /** + * The transaction hash + */ + reference: string; + /** + * Optional metadata object + */ + metadata?: { + transactionType: string; + currency: string; + amount: number; + decimals: number; + fromAddress: string; + toAddress: string; + }; +}; + +export class TransactionReferenceCodec + implements ContentCodec +{ + get contentType(): ContentTypeId { + return ContentTypeTransactionReference; + } + + encode(content: TransactionReference): EncodedContent { + const encoded = { + type: ContentTypeTransactionReference, + parameters: {}, + content: new TextEncoder().encode(JSON.stringify(content)), + }; + return encoded; + } + + decode(encodedContent: EncodedContent): TransactionReference { + const uint8Array = encodedContent.content; + const contentReceived = JSON.parse( + new TextDecoder().decode(uint8Array), + ) as TransactionReference; + return contentReceived; + } + + fallback(content: TransactionReference): string | undefined { + if (content.reference) { + return `[Crypto transaction] Use a blockchain explorer to learn more using the transaction hash: ${content.reference}`; + } + return `Crypto transaction`; + } + + shouldPush() { + return true; + } +} diff --git a/content-types/content-type-transaction-reference/src/index.ts b/content-types/content-type-transaction-reference/src/index.ts new file mode 100644 index 000000000..bb18ff2e0 --- /dev/null +++ b/content-types/content-type-transaction-reference/src/index.ts @@ -0,0 +1,5 @@ +export { + TransactionReferenceCodec, + ContentTypeTransactionReference, +} from "./TransactionReference"; +export type { TransactionReference } from "./TransactionReference"; diff --git a/content-types/content-type-transaction-reference/tsconfig.eslint.json b/content-types/content-type-transaction-reference/tsconfig.eslint.json new file mode 100644 index 000000000..5ee5f6f56 --- /dev/null +++ b/content-types/content-type-transaction-reference/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": [".", ".eslintrc.cjs", "rollup.config.js"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-transaction-reference/tsconfig.json b/content-types/content-type-transaction-reference/tsconfig.json new file mode 100644 index 000000000..69f454187 --- /dev/null +++ b/content-types/content-type-transaction-reference/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/build.json", + "include": ["src"], + "exclude": ["dist", "node_modules"] +} diff --git a/content-types/content-type-transaction-reference/vitest.config.ts b/content-types/content-type-transaction-reference/vitest.config.ts new file mode 100644 index 000000000..2ee901930 --- /dev/null +++ b/content-types/content-type-transaction-reference/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + setupFiles: "./vitest.setup.ts", + }, +}); diff --git a/content-types/content-type-transaction-reference/vitest.setup.ts b/content-types/content-type-transaction-reference/vitest.setup.ts new file mode 100644 index 000000000..df951dbaa --- /dev/null +++ b/content-types/content-type-transaction-reference/vitest.setup.ts @@ -0,0 +1,3 @@ +import { Buffer } from "buffer"; + +globalThis.Buffer = Buffer; diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 72530e426..af0986dd4 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -34,3 +34,8 @@ services: image: postgres:13 environment: POSTGRES_PASSWORD: xmtp + + upload-service: + build: ./uploadService + ports: + - 3000:3000 diff --git a/dev/uploadService/Dockerfile b/dev/uploadService/Dockerfile new file mode 100644 index 000000000..44c8489a0 --- /dev/null +++ b/dev/uploadService/Dockerfile @@ -0,0 +1,15 @@ +# Fetching the minified node image on apline linux +FROM node:18.19-slim + +WORKDIR /uploadService +COPY . . + +RUN apt-get update && \ + apt-get install -y openssl && \ + openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -sha256 -days 3650 -subj /CN=localhost -out cert.pem + +RUN npm install + +CMD ["node", "index.js"] + +EXPOSE 3000 diff --git a/dev/uploadService/index.js b/dev/uploadService/index.js new file mode 100644 index 000000000..05c66a33b --- /dev/null +++ b/dev/uploadService/index.js @@ -0,0 +1,36 @@ +const fs = require("fs"); +const https = require("https"); +const express = require("express"); +const bodyParser = require("body-parser"); +const app = express(); +const port = 3000; + +const key = fs.readFileSync("key.pem", "utf-8"); +const cert = fs.readFileSync("cert.pem", "utf-8"); + +const UPLOADS = {}; + +app.use(bodyParser.raw({ type: "application/octet-stream" })); + +app.get("/:path", (req, res) => { + const path = req.params.path; + console.log(`GET /${path}`); + const file = UPLOADS[path]; + if (file) { + res.header("Content-Type", "application/octet-stream"); + res.send(file); + } else { + console.log(`Upload path found: ${path}`); + } +}); + +app.post("/:path", (req, res) => { + const path = req.params.path; + console.log(`POST /${path}`); + UPLOADS[path] = req.body; + res.sendStatus(200); +}); + +https.createServer({ key, cert }, app).listen(port, () => { + console.log(`Upload service listening on port ${port}`); +}); diff --git a/dev/uploadService/package.json b/dev/uploadService/package.json new file mode 100644 index 000000000..1839affca --- /dev/null +++ b/dev/uploadService/package.json @@ -0,0 +1,13 @@ +{ + "name": "uploadservice", + "version": "0.0.0", + "private": true, + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "body-parser": "^1.20.2", + "express": "^4.18.2" + } +} diff --git a/package.json b/package.json index 6e182d688..eca78757f 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,15 @@ "version": "0.0.0", "private": true, "workspaces": [ - "packages/*" + "content-types/*", + "packages/*", + "shared/*" ], "scripts": { - "build": "turbo run build --filter='./packages/*'", - "clean": "turbo run clean && rm -rf node_modules && rm -rf .turbo && yarn cache clean", - "format": "turbo run format", - "format:check": "turbo run format:check", + "build": "turbo run build", + "clean": "turbo run clean && rimraf .turbo node_modules && yarn cache clean", + "format": "prettier -w .", + "format:check": "prettier -c .", "lint": "FORCE_COLOR=1 turbo run lint", "publish": "yarn build && changeset publish", "test": "FORCE_COLOR=1 turbo run test", @@ -22,8 +24,10 @@ "@changesets/cli": "^2.27.9" }, "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.3.1", "prettier": "^3.3.3", - "prettier-plugin-packagejson": "^2.5.2", + "prettier-plugin-packagejson": "^2.5.3", + "rimraf": "^6.0.1", "turbo": "^2.1.3" }, "packageManager": "yarn@4.5.0", diff --git a/packages/js-sdk/.eslintrc.cjs b/packages/js-sdk/.eslintrc.cjs index f01c92e3a..afc7f8b0e 100644 --- a/packages/js-sdk/.eslintrc.cjs +++ b/packages/js-sdk/.eslintrc.cjs @@ -1,57 +1,57 @@ module.exports = { - parser: '@typescript-eslint/parser', + parser: "@typescript-eslint/parser", extends: [ - 'eslint:recommended', - 'standard', - 'prettier', - 'plugin:@typescript-eslint/recommended', - 'eslint-config-prettier', - 'plugin:jsdoc/recommended', + "eslint:recommended", + "standard", + "prettier", + "plugin:@typescript-eslint/recommended", + "eslint-config-prettier", + "plugin:jsdoc/recommended", ], parserOptions: { - sourceType: 'module', + sourceType: "module", warnOnUnsupportedTypeScriptVersion: false, - project: 'tsconfig.json', + project: "tsconfig.json", }, rules: { - '@typescript-eslint/consistent-type-exports': [ - 'error', + "@typescript-eslint/consistent-type-exports": [ + "error", { fixMixedExportsWithInlineTypeSpecifier: false, }, ], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/consistent-type-imports': 'error', - '@typescript-eslint/no-unused-vars': [ - 'error', + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/consistent-type-imports": "error", + "@typescript-eslint/no-unused-vars": [ + "error", { - argsIgnorePattern: '^_', - destructuredArrayIgnorePattern: '^_', + argsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", ignoreRestSiblings: true, - varsIgnorePattern: '^_', + varsIgnorePattern: "^_", }, ], - 'prettier/prettier': 'error', - 'jsdoc/require-jsdoc': 'off', - 'jsdoc/require-description': 'off', - 'jsdoc/require-param': 'off', - 'jsdoc/require-param-type': 'off', - 'jsdoc/require-returns': 'off', + "prettier/prettier": "error", + "jsdoc/require-jsdoc": "off", + "jsdoc/require-description": "off", + "jsdoc/require-param": "off", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "off", // this is necessary to ensure that the crypto library is available // in node and the browser - 'no-restricted-syntax': [ - 'error', + "no-restricted-syntax": [ + "error", { - selector: 'ImportDeclaration[source.value=/^(node:)?crypto$/]', + selector: "ImportDeclaration[source.value=/^(node:)?crypto$/]", message: - 'Do not import directly from `crypto`, use `src/crypto/crypto` instead.', + "Do not import directly from `crypto`, use `src/crypto/crypto` instead.", }, { - selector: 'ImportDeclaration[source.value=/^\\.\\./]', + selector: "ImportDeclaration[source.value=/^\\.\\./]", message: - 'Relative parent imports are not allowed, use path aliases instead.', + "Relative parent imports are not allowed, use path aliases instead.", }, ], }, - plugins: ['@typescript-eslint', 'prettier', 'jsdoc'], -} + plugins: ["@typescript-eslint", "prettier", "jsdoc"], +}; diff --git a/packages/js-sdk/README.md b/packages/js-sdk/README.md index a98b323a9..d712de8ba 100644 --- a/packages/js-sdk/README.md +++ b/packages/js-sdk/README.md @@ -66,13 +66,13 @@ This SDK uses WebAssembly, which may require additional configuration in your en **vite.config.js** ```js -import { defineConfig } from 'vite' +import { defineConfig } from "vite"; export default defineConfig({ optimizeDeps: { - exclude: ['@xmtp/user-preferences-bindings-wasm'], + exclude: ["@xmtp/user-preferences-bindings-wasm"], }, -}) +}); ``` #### Next.js @@ -86,21 +86,21 @@ Next.js < 15 ```js const nextConfig = { experimental: { - serverComponentsExternalPackages: ['@xmtp/user-preferences-bindings-wasm'], + serverComponentsExternalPackages: ["@xmtp/user-preferences-bindings-wasm"], }, -} +}; -export default nextConfig +export default nextConfig; ``` Next.js >= 15 ```js const nextConfig = { - serverExternalPackages: ['@xmtp/user-preferences-bindings-wasm'], -} + serverExternalPackages: ["@xmtp/user-preferences-bindings-wasm"], +}; -export default nextConfig +export default nextConfig; ``` ### BigInt polyfill diff --git a/packages/js-sdk/bench/decode.ts b/packages/js-sdk/bench/decode.ts index ab3c84f17..7e4c28396 100644 --- a/packages/js-sdk/bench/decode.ts +++ b/packages/js-sdk/bench/decode.ts @@ -1,98 +1,98 @@ -import { fetcher } from '@xmtp/proto' -import { add } from 'benny' -import { ConversationV1, ConversationV2 } from '@/conversations/Conversation' -import { SignedPublicKeyBundle } from '@/crypto/PublicKeyBundle' -import { MessageV1 } from '@/Message' -import { dateToNs } from '@/utils/date' -import { newLocalHostClient } from '@test/helpers' +import { fetcher } from "@xmtp/proto"; +import { add } from "benny"; +import { ConversationV1, ConversationV2 } from "@/conversations/Conversation"; +import { SignedPublicKeyBundle } from "@/crypto/PublicKeyBundle"; +import { MessageV1 } from "@/Message"; +import { dateToNs } from "@/utils/date"; +import { newLocalHostClient } from "@test/helpers"; import { MESSAGE_SIZES, newPrivateKeyBundle, randomBytes, wrapSuite, -} from './helpers' +} from "./helpers"; const decodeV1 = () => { return MESSAGE_SIZES.map((size) => add(`decode and decrypt a ${size} byte v1 message`, async () => { - const alice = await newLocalHostClient() - const bob = await newPrivateKeyBundle() + const alice = await newLocalHostClient(); + const bob = await newPrivateKeyBundle(); - const message = randomBytes(size) - const { payload } = await alice.encodeContent(message) + const message = randomBytes(size); + const { payload } = await alice.encodeContent(message); const encodedMessage = await MessageV1.encode( alice.keystore, payload, alice.publicKeyBundle, bob.getPublicKeyBundle(), - new Date() - ) + new Date(), + ); - const messageBytes = encodedMessage.toBytes() + const messageBytes = encodedMessage.toBytes(); const convo = new ConversationV1( alice, bob.identityKey.publicKey.walletSignatureAddress(), - new Date() - ) + new Date(), + ); const envelope = { contentTopic: convo.topic, message: fetcher.b64Encode( messageBytes, 0, - messageBytes.length + messageBytes.length, ) as unknown as Uint8Array, - } + }; return async () => { - await convo.decodeMessage(envelope) - } - }) - ) -} + await convo.decodeMessage(envelope); + }; + }), + ); +}; const decodeV2 = () => { return MESSAGE_SIZES.map((size) => add(`decode and decrypt a ${size} byte v2 message`, async () => { - const alice = await newLocalHostClient() - const bob = await newPrivateKeyBundle() + const alice = await newLocalHostClient(); + const bob = await newPrivateKeyBundle(); - const message = randomBytes(size) + const message = randomBytes(size); const invite = await alice.keystore.createInvite({ recipient: SignedPublicKeyBundle.fromLegacyBundle( - bob.getPublicKeyBundle() + bob.getPublicKeyBundle(), ), createdNs: dateToNs(new Date()), context: undefined, consentProof: undefined, - }) + }); const convo = new ConversationV2( alice, - invite.conversation?.topic ?? '', + invite.conversation?.topic ?? "", bob.identityKey.publicKey.walletSignatureAddress(), new Date(), undefined, - undefined - ) - const { payload, shouldPush } = await alice.encodeContent(message) - const encodedMessage = await convo.createMessage(payload, shouldPush) - const messageBytes = encodedMessage.toBytes() + undefined, + ); + const { payload, shouldPush } = await alice.encodeContent(message); + const encodedMessage = await convo.createMessage(payload, shouldPush); + const messageBytes = encodedMessage.toBytes(); const envelope = { contentTopic: convo.topic, message: fetcher.b64Encode( messageBytes, 0, - messageBytes.length + messageBytes.length, ) as unknown as Uint8Array, - } + }; return async () => { - await convo.decodeMessage(envelope) - } - }) - ) -} + await convo.decodeMessage(envelope); + }; + }), + ); +}; -export default wrapSuite('decode', ...decodeV1(), ...decodeV2()) +export default wrapSuite("decode", ...decodeV1(), ...decodeV2()); diff --git a/packages/js-sdk/bench/encode.ts b/packages/js-sdk/bench/encode.ts index 8691e5af1..16d9a8062 100644 --- a/packages/js-sdk/bench/encode.ts +++ b/packages/js-sdk/bench/encode.ts @@ -1,73 +1,73 @@ -import { add } from 'benny' -import Client from '@/Client' -import { ConversationV2 } from '@/conversations/Conversation' -import { SignedPublicKeyBundle } from '@/crypto/PublicKeyBundle' -import { MessageV1 } from '@/Message' -import { dateToNs } from '@/utils/date' -import { newLocalHostClient, newWallet } from '@test/helpers' +import { add } from "benny"; +import Client from "@/Client"; +import { ConversationV2 } from "@/conversations/Conversation"; +import { SignedPublicKeyBundle } from "@/crypto/PublicKeyBundle"; +import { MessageV1 } from "@/Message"; +import { dateToNs } from "@/utils/date"; +import { newLocalHostClient, newWallet } from "@test/helpers"; import { MESSAGE_SIZES, newPrivateKeyBundle, randomBytes, wrapSuite, -} from './helpers' +} from "./helpers"; const encodeV1 = () => { return MESSAGE_SIZES.map((size) => add(`encode and encrypt a ${size} byte v1 message`, async () => { - const alice = await Client.create(newWallet(), { env: 'local' }) - const bobKeys = (await newPrivateKeyBundle()).getPublicKeyBundle() + const alice = await Client.create(newWallet(), { env: "local" }); + const bobKeys = (await newPrivateKeyBundle()).getPublicKeyBundle(); - const message = randomBytes(size).toString() - const timestamp = new Date() + const message = randomBytes(size).toString(); + const timestamp = new Date(); // The returned function is the actual benchmark. Everything above is setup return async () => { - const { payload: encodedMessage } = await alice.encodeContent(message) + const { payload: encodedMessage } = await alice.encodeContent(message); await MessageV1.encode( alice.keystore, encodedMessage, alice.publicKeyBundle, bobKeys, - timestamp - ) - } - }) - ) -} + timestamp, + ); + }; + }), + ); +}; const encodeV2 = () => { // All these sizes should take roughly the same amount of time return MESSAGE_SIZES.map((size) => add(`encode and encrypt a ${size} byte v2 message`, async () => { - const alice = await newLocalHostClient() - const bob = await newPrivateKeyBundle() + const alice = await newLocalHostClient(); + const bob = await newPrivateKeyBundle(); const invite = await alice.keystore.createInvite({ recipient: SignedPublicKeyBundle.fromLegacyBundle( - bob.getPublicKeyBundle() + bob.getPublicKeyBundle(), ), createdNs: dateToNs(new Date()), context: undefined, consentProof: undefined, - }) + }); const convo = new ConversationV2( alice, - invite.conversation?.topic ?? '', + invite.conversation?.topic ?? "", bob.identityKey.publicKey.walletSignatureAddress(), new Date(), undefined, - undefined - ) - const message = randomBytes(size) - const { payload, shouldPush } = await alice.encodeContent(message) + undefined, + ); + const message = randomBytes(size); + const { payload, shouldPush } = await alice.encodeContent(message); // The returned function is the actual benchmark. Everything above is setup return async () => { - await convo.createMessage(payload, shouldPush) - } - }) - ) -} + await convo.createMessage(payload, shouldPush); + }; + }), + ); +}; -export default wrapSuite('encode', ...encodeV1(), ...encodeV2()) +export default wrapSuite("encode", ...encodeV1(), ...encodeV2()); diff --git a/packages/js-sdk/bench/helpers.ts b/packages/js-sdk/bench/helpers.ts index abad46c2d..1e673da5d 100644 --- a/packages/js-sdk/bench/helpers.ts +++ b/packages/js-sdk/bench/helpers.ts @@ -1,31 +1,31 @@ -import type Benchmark from 'benchmark' -import { cycle, save, suite } from 'benny' -import type { Config } from 'benny/lib/internal/common-types' -import crypto from '@/crypto/crypto' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import { newWallet } from '@test/helpers' +import type Benchmark from "benchmark"; +import { cycle, save, suite } from "benny"; +import type { Config } from "benny/lib/internal/common-types"; +import crypto from "@/crypto/crypto"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import { newWallet } from "@test/helpers"; -const MAX_RANDOM_BYTES_SIZE = 65536 +const MAX_RANDOM_BYTES_SIZE = 65536; export const randomBytes = (size: number) => { - const out = new Uint8Array(size) - let remaining = size + const out = new Uint8Array(size); + let remaining = size; while (remaining > 0) { const chunkSize = - remaining < MAX_RANDOM_BYTES_SIZE ? remaining : MAX_RANDOM_BYTES_SIZE - const chunk = crypto.getRandomValues(new Uint8Array(chunkSize)) - out.set(chunk, size - remaining) - remaining -= MAX_RANDOM_BYTES_SIZE + remaining < MAX_RANDOM_BYTES_SIZE ? remaining : MAX_RANDOM_BYTES_SIZE; + const chunk = crypto.getRandomValues(new Uint8Array(chunkSize)); + out.set(chunk, size - remaining); + remaining -= MAX_RANDOM_BYTES_SIZE; } - return out -} + return out; +}; export const newPrivateKeyBundle = () => - PrivateKeyBundleV1.generate(newWallet()) + PrivateKeyBundleV1.generate(newWallet()); type BenchGenerator = ( - config: Config -) => Promise<(suiteObj: Benchmark.Suite) => Benchmark.Suite> + config: Config, +) => Promise<(suiteObj: Benchmark.Suite) => Benchmark.Suite>; // Async test suites should be wrapped in a function so that they can be run one at a time export const wrapSuite = @@ -35,9 +35,9 @@ export const wrapSuite = name, ...tests, cycle(), - save({ file: name, folder: 'bench/results', format: 'json' }), - save({ file: name, folder: 'bench/results', format: 'table.html' }), - save({ file: name, folder: 'bench/results', format: 'chart.html' }) - ) + save({ file: name, folder: "bench/results", format: "json" }), + save({ file: name, folder: "bench/results", format: "table.html" }), + save({ file: name, folder: "bench/results", format: "chart.html" }), + ); -export const MESSAGE_SIZES = [128, 65536, 524288] +export const MESSAGE_SIZES = [128, 65536, 524288]; diff --git a/packages/js-sdk/bench/index.ts b/packages/js-sdk/bench/index.ts index 1fc4f5672..d0de643b1 100644 --- a/packages/js-sdk/bench/index.ts +++ b/packages/js-sdk/bench/index.ts @@ -1,9 +1,9 @@ -import decodeSuite from './decode' -import encodeSuite from './encode' +import decodeSuite from "./decode"; +import encodeSuite from "./encode"; const main = async () => { - await encodeSuite() - await decodeSuite() -} + await encodeSuite(); + await decodeSuite(); +}; -main() +main(); diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index 4d1394325..94b612657 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -66,18 +66,15 @@ "bench": "yarn build:bench && node dist/bench/index.cjs", "build": "yarn clean:dist && rollup -c", "build:bench": "rollup -c rollup.config.bench.js", - "build:docs": "yarn clean:docs && mkdir -p tmp && cp README.md tmp/ && sed -i.bak '/badge.svg/d' tmp/README.md && typedoc", - "clean": "yarn clean:artifacts && yarn clean:dist && yarn clean:docs && yarn clean:deps", - "clean:artifacts": "rm -rf docs tmp package.tgz", - "clean:deps": "rm -rf node_modules", - "clean:dist": "rm -rf dist", - "clean:docs": "rm -rf docs", - "format": "yarn format:base -w .", - "format:base": "prettier --ignore-path ../../.gitignore", - "format:check": "yarn format:base -c .", + "build:docs": "cd ../../ && yarn build && cd packages/js-sdk && yarn clean:docs && mkdir -p tmp && cp README.md tmp/ && sed -i.bak '/badge.svg/d' tmp/README.md && typedoc", + "clean": "rimraf .turbo &&yarn clean:artifacts && yarn clean:dist && yarn clean:docs && yarn clean:deps", + "clean:artifacts": "rimraf docs tmp package.tgz", + "clean:deps": "rimraf node_modules", + "clean:dist": "rimraf dist", + "clean:docs": "rimraf docs", "lint": "eslint . --ignore-path ../../.gitignore", "package": "yarn pack", - "test": "yarn test:node", + "test": "yarn test:node && yarn test:browser", "test:browser": "vitest run --environment happy-dom", "test:cov": "vitest run --coverage", "test:node": "vitest run", @@ -109,7 +106,6 @@ "viem": "2.7.15" }, "devDependencies": { - "@ianvs/prettier-plugin-sort-imports": "^4.3.1", "@metamask/providers": "^17.1.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-terser": "^0.4.4", @@ -124,7 +120,6 @@ "@vitest/coverage-v8": "^2.1.2", "@xmtp/rollup-plugin-resolve-extensions": "1.0.1", "benny": "^3.7.1", - "dd-trace": "5.5.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-config-standard": "^17.1.0", @@ -136,14 +131,13 @@ "eslint-plugin-promise": "^6.4.0", "ethers": "^5.7.2", "happy-dom": "^15.7.4", - "prettier": "^3.3.3", - "prettier-plugin-packagejson": "^2.5.2", + "rimraf": "^6.0.1", "rollup": "^4.24.0", "rollup-plugin-dts": "^6.1.1", "rollup-plugin-filesize": "^10.0.0", "rollup-plugin-tsconfig-paths": "^1.5.2", "typedoc": "^0.26.8", - "typescript": "^5.6.2", + "typescript": "^5.6.3", "vite": "5.4.8", "vite-tsconfig-paths": "^5.0.1", "vitest": "^2.1.2" diff --git a/packages/js-sdk/rollup.config.bench.js b/packages/js-sdk/rollup.config.bench.js index a485194b6..e399ae95c 100644 --- a/packages/js-sdk/rollup.config.bench.js +++ b/packages/js-sdk/rollup.config.bench.js @@ -1,21 +1,21 @@ -import json from '@rollup/plugin-json' -import typescript from '@rollup/plugin-typescript' -import { defineConfig } from 'rollup' -import tsConfigPaths from 'rollup-plugin-tsconfig-paths' +import json from "@rollup/plugin-json"; +import typescript from "@rollup/plugin-typescript"; +import { defineConfig } from "rollup"; +import tsConfigPaths from "rollup-plugin-tsconfig-paths"; const external = [ - '@noble/secp256k1', - '@xmtp/proto', - '@xmtp/user-preferences-bindings-wasm', - 'assert', - 'async-mutex', - 'benny', - 'crypto', - 'elliptic', - 'ethers', - 'long', - 'viem', -] + "@noble/secp256k1", + "@xmtp/proto", + "@xmtp/user-preferences-bindings-wasm", + "assert", + "async-mutex", + "benny", + "crypto", + "elliptic", + "ethers", + "long", + "viem", +]; const plugins = [ tsConfigPaths(), @@ -26,16 +26,16 @@ const plugins = [ json({ preferConst: true, }), -] +]; export default defineConfig([ { - input: 'bench/index.ts', + input: "bench/index.ts", output: { - file: 'dist/bench/index.cjs', - format: 'cjs', + file: "dist/bench/index.cjs", + format: "cjs", }, plugins, external, }, -]) +]); diff --git a/packages/js-sdk/rollup.config.js b/packages/js-sdk/rollup.config.js index 3415d65d6..8d19c44b9 100644 --- a/packages/js-sdk/rollup.config.js +++ b/packages/js-sdk/rollup.config.js @@ -1,27 +1,27 @@ -import json from '@rollup/plugin-json' -import terser from '@rollup/plugin-terser' -import typescript from '@rollup/plugin-typescript' -import { resolveExtensions } from '@xmtp/rollup-plugin-resolve-extensions' -import { defineConfig } from 'rollup' -import { dts } from 'rollup-plugin-dts' -import filesize from 'rollup-plugin-filesize' -import tsConfigPaths from 'rollup-plugin-tsconfig-paths' +import json from "@rollup/plugin-json"; +import terser from "@rollup/plugin-terser"; +import typescript from "@rollup/plugin-typescript"; +import { resolveExtensions } from "@xmtp/rollup-plugin-resolve-extensions"; +import { defineConfig } from "rollup"; +import { dts } from "rollup-plugin-dts"; +import filesize from "rollup-plugin-filesize"; +import tsConfigPaths from "rollup-plugin-tsconfig-paths"; const external = [ - '@noble/secp256k1', - '@xmtp/consent-proof-signature', - '@xmtp/content-type-text', - '@xmtp/content-type-primitives', - '@xmtp/proto', - '@xmtp/user-preferences-bindings-wasm', - '@xmtp/user-preferences-bindings-wasm/web', - '@xmtp/user-preferences-bindings-wasm/bundler', - 'async-mutex', - 'crypto', - 'elliptic', - 'long', - 'viem', -] + "@noble/secp256k1", + "@xmtp/consent-proof-signature", + "@xmtp/content-type-text", + "@xmtp/content-type-primitives", + "@xmtp/proto", + "@xmtp/user-preferences-bindings-wasm", + "@xmtp/user-preferences-bindings-wasm/web", + "@xmtp/user-preferences-bindings-wasm/bundler", + "async-mutex", + "crypto", + "elliptic", + "long", + "viem", +]; const plugins = [ tsConfigPaths(), @@ -35,63 +35,63 @@ const plugins = [ json({ preferConst: true, }), -] +]; export default defineConfig([ { - input: 'src/index.ts', + input: "src/index.ts", output: { - file: 'dist/index.js', - format: 'es', + file: "dist/index.js", + format: "es", sourcemap: true, }, plugins, external, }, { - input: 'src/index.ts', + input: "src/index.ts", output: { - file: 'dist/index.cjs', - format: 'cjs', + file: "dist/index.cjs", + format: "cjs", sourcemap: true, }, plugins, external, }, { - input: 'src/index.ts', + input: "src/index.ts", output: { - file: 'dist/index.d.ts', - format: 'es', + file: "dist/index.d.ts", + format: "es", }, plugins: [tsConfigPaths(), dts()], }, { - input: 'src/index.ts', + input: "src/index.ts", output: { - file: 'dist/browser/index.js', - format: 'es', + file: "dist/browser/index.js", + format: "es", sourcemap: true, }, plugins: [ - resolveExtensions({ extensions: ['.browser'] }), + resolveExtensions({ extensions: [".browser"] }), terser(), ...plugins, ], external, }, { - input: 'src/index.ts', + input: "src/index.ts", output: { - file: 'dist/bundler/index.js', - format: 'es', + file: "dist/bundler/index.js", + format: "es", sourcemap: true, }, plugins: [ - resolveExtensions({ extensions: ['.bundler', '.browser'] }), + resolveExtensions({ extensions: [".bundler", ".browser"] }), terser(), ...plugins, ], external, }, -]) +]); diff --git a/packages/js-sdk/src/ApiClient.ts b/packages/js-sdk/src/ApiClient.ts index b64eb4c36..9b59580ab 100644 --- a/packages/js-sdk/src/ApiClient.ts +++ b/packages/js-sdk/src/ApiClient.ts @@ -1,28 +1,28 @@ -import { messageApi } from '@xmtp/proto' -import type { NotifyStreamEntityArrival } from '@xmtp/proto/ts/dist/types/fetch.pb' -import type { Authenticator } from '@/authn/interfaces' -import { retry, sleep } from '@/utils/async' -import { b64Decode } from '@/utils/bytes' -import { toNanoString } from '@/utils/date' +import { messageApi } from "@xmtp/proto"; +import type { NotifyStreamEntityArrival } from "@xmtp/proto/ts/dist/types/fetch.pb"; +import type { Authenticator } from "@/authn/interfaces"; +import { retry, sleep } from "@/utils/async"; +import { b64Decode } from "@/utils/bytes"; +import { toNanoString } from "@/utils/date"; // eslint-disable-next-line no-restricted-syntax -import { version } from '../package.json' -import AuthCache from './authn/AuthCache' -import { XMTP_DEV_WARNING } from './constants' -import type { Flatten } from './utils/typedefs' +import { version } from "../package.json"; +import AuthCache from "./authn/AuthCache"; +import { XMTP_DEV_WARNING } from "./constants"; +import type { Flatten } from "./utils/typedefs"; -export const { MessageApi, SortDirection } = messageApi +export const { MessageApi, SortDirection } = messageApi; -const RETRY_SLEEP_TIME = 100 -const ERR_CODE_UNAUTHENTICATED = 16 +const RETRY_SLEEP_TIME = 100; +const ERR_CODE_UNAUTHENTICATED = 16; -const clientVersionHeaderKey = 'X-Client-Version' -const appVersionHeaderKey = 'X-App-Version' +const clientVersionHeaderKey = "X-Client-Version"; +const appVersionHeaderKey = "X-App-Version"; export const ApiUrls = { - local: 'http://localhost:5555', - dev: 'https://dev.xmtp.network', - production: 'https://production.xmtp.network', -} as const + local: "http://localhost:5555", + dev: "https://dev.xmtp.network", + production: "https://production.xmtp.network", +} as const; export enum GrpcStatus { OK = 0, @@ -45,147 +45,147 @@ export enum GrpcStatus { } export class GrpcError extends Error { - code: GrpcStatus + code: GrpcStatus; constructor(message: string, code: GrpcStatus) { - super(message) - this.code = code + super(message); + this.code = code; } static fromObject(err: { code: GrpcStatus; message: string }): GrpcError { - return new GrpcError(err.message, err.code) + return new GrpcError(err.message, err.code); } } export type QueryParams = { - startTime?: Date - endTime?: Date - contentTopic: string -} + startTime?: Date; + endTime?: Date; + contentTopic: string; +}; export type QueryAllOptions = { - direction?: messageApi.SortDirection - limit?: number - pageSize?: number -} + direction?: messageApi.SortDirection; + limit?: number; + pageSize?: number; +}; export type QueryStreamOptions = Flatten< - Omit & { - pageSize?: number + Omit & { + pageSize?: number; } -> +>; // All of the fields in both QueryParams and QueryStreamOptions -export type Query = Flatten +export type Query = Flatten; export type PublishParams = { - contentTopic: string - message: Uint8Array - timestamp?: Date -} + contentTopic: string; + message: Uint8Array; + timestamp?: Date; +}; export type SubscribeParams = { - contentTopics: string[] -} + contentTopics: string[]; +}; export type ApiClientOptions = { - maxRetries?: number - appVersion?: string -} + maxRetries?: number; + appVersion?: string; +}; -export type SubscribeCallback = NotifyStreamEntityArrival +export type SubscribeCallback = NotifyStreamEntityArrival; -export type UnsubscribeFn = () => Promise +export type UnsubscribeFn = () => Promise; -export type UpdateContentTopics = (topics: string[]) => Promise +export type UpdateContentTopics = (topics: string[]) => Promise; export type SubscriptionManager = { - unsubscribe: UnsubscribeFn - updateContentTopics?: UpdateContentTopics -} + unsubscribe: UnsubscribeFn; + updateContentTopics?: UpdateContentTopics; +}; -export type OnConnectionLostCallback = () => void +export type OnConnectionLostCallback = () => void; const isAbortError = (err?: Error): boolean => { if (!err) { - return false + return false; } - if (err.name === 'AbortError' || err.message.includes('aborted')) { - return true + if (err.name === "AbortError" || err.message.includes("aborted")) { + return true; } - return false -} + return false; +}; const isAuthError = (err?: GrpcError | Error): boolean => { - if (err && 'code' in err && err.code === ERR_CODE_UNAUTHENTICATED) { - return true + if (err && "code" in err && err.code === ERR_CODE_UNAUTHENTICATED) { + return true; } - return false -} + return false; +}; -const isNotAuthError = (err?: Error): boolean => !isAuthError(err) +const isNotAuthError = (err?: Error): boolean => !isAuthError(err); export interface ApiClient { query( params: QueryParams, - options: QueryAllOptions - ): Promise + options: QueryAllOptions, + ): Promise; queryIterator( params: QueryParams, - options: QueryStreamOptions - ): AsyncGenerator + options: QueryStreamOptions, + ): AsyncGenerator; queryIteratePages( params: QueryParams, - options: QueryStreamOptions - ): AsyncGenerator + options: QueryStreamOptions, + ): AsyncGenerator; subscribe( params: SubscribeParams, callback: SubscribeCallback, - onConnectionLost?: OnConnectionLostCallback - ): SubscriptionManager - publish(messages: PublishParams[]): ReturnType - batchQuery(queries: Query[]): Promise + onConnectionLost?: OnConnectionLostCallback, + ): SubscriptionManager; + publish(messages: PublishParams[]): ReturnType; + batchQuery(queries: Query[]): Promise; setAuthenticator( authenticator: Authenticator, - cacheExpirySeconds?: number - ): void + cacheExpirySeconds?: number, + ): void; } const normalizeEnvelope = (env: messageApi.Envelope): messageApi.Envelope => { if (!env.message || !env.message.length) { - return env + return env; } - if (typeof env.message === 'string') { - env.message = b64Decode(env.message) + if (typeof env.message === "string") { + env.message = b64Decode(env.message); } - return env -} + return env; +}; /** * ApiClient provides a wrapper for calling the GRPC Gateway generated code. * It adds some helpers for dealing with paginated data and automatically retries idempotent calls */ export default class HttpApiClient implements ApiClient { - pathPrefix: string - maxRetries: number - private authCache?: AuthCache - appVersion: string | undefined - version: string + pathPrefix: string; + maxRetries: number; + private authCache?: AuthCache; + appVersion: string | undefined; + version: string; constructor(pathPrefix: string, opts?: ApiClientOptions) { - this.pathPrefix = pathPrefix - this.maxRetries = opts?.maxRetries || 5 - this.appVersion = opts?.appVersion - this.version = 'xmtp-js/' + version + this.pathPrefix = pathPrefix; + this.maxRetries = opts?.maxRetries || 5; + this.appVersion = opts?.appVersion; + this.version = "xmtp-js/" + version; if (pathPrefix === ApiUrls.dev) { - console.info(XMTP_DEV_WARNING) + console.info(XMTP_DEV_WARNING); } } // Raw method for querying the API private async _query( - req: messageApi.QueryRequest + req: messageApi.QueryRequest, ): ReturnType { try { return await retry( @@ -194,22 +194,22 @@ export default class HttpApiClient implements ApiClient { req, { pathPrefix: this.pathPrefix, - mode: 'cors', + mode: "cors", headers: this.headers(), }, ], this.maxRetries, - RETRY_SLEEP_TIME - ) + RETRY_SLEEP_TIME, + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { - throw GrpcError.fromObject(e) + throw GrpcError.fromObject(e); } } // Raw method for batch-querying the API private _batchQuery( - req: messageApi.BatchQueryRequest + req: messageApi.BatchQueryRequest, ): ReturnType { return retry( MessageApi.BatchQuery, @@ -217,23 +217,23 @@ export default class HttpApiClient implements ApiClient { req, { pathPrefix: this.pathPrefix, - mode: 'cors', + mode: "cors", headers: this.headers(), }, ], this.maxRetries, - RETRY_SLEEP_TIME - ) + RETRY_SLEEP_TIME, + ); } // Raw method for publishing to the API private async _publish( req: messageApi.PublishRequest, - attemptNumber = 0 + attemptNumber = 0, ): ReturnType { - const authToken = await this.getToken() - const headers = this.headers() - headers.set('Authorization', `Bearer ${authToken}`) + const authToken = await this.getToken(); + const headers = this.headers(); + headers.set("Authorization", `Bearer ${authToken}`); try { return await retry( MessageApi.Publish, @@ -241,23 +241,23 @@ export default class HttpApiClient implements ApiClient { req, { pathPrefix: this.pathPrefix, - mode: 'cors', + mode: "cors", headers, }, ], this.maxRetries, RETRY_SLEEP_TIME, // Do not retry UnauthenticatedErrors - isNotAuthError - ) + isNotAuthError, + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { // Try at most 2X. If refreshing the auth token doesn't work the first time, it won't work the second time if (isNotAuthError(e) || attemptNumber >= 1) { - throw GrpcError.fromObject(e) + throw GrpcError.fromObject(e); } - await this.authCache?.refresh() - return this._publish(req, attemptNumber + 1) + await this.authCache?.refresh(); + return this._publish(req, attemptNumber + 1); } } @@ -265,51 +265,51 @@ export default class HttpApiClient implements ApiClient { private _subscribe( req: messageApi.SubscribeRequest, cb: NotifyStreamEntityArrival, - onConnectionLost?: OnConnectionLostCallback + onConnectionLost?: OnConnectionLostCallback, ): SubscriptionManager { - const abortController = new AbortController() + const abortController = new AbortController(); const doSubscribe = async () => { while (true) { - const startTime = new Date().getTime() + const startTime = new Date().getTime(); try { await MessageApi.Subscribe(req, cb, { pathPrefix: this.pathPrefix, signal: abortController.signal, - mode: 'cors', + mode: "cors", headers: this.headers(), - }) + }); if (abortController.signal.aborted) { - return + return; } - console.info('Stream connection closed. Resubscribing') + console.info("Stream connection closed. Resubscribing"); if (new Date().getTime() - startTime < 1000) { - await sleep(1000) + await sleep(1000); } - onConnectionLost?.() + onConnectionLost?.(); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { if (isAbortError(err) || abortController.signal.aborted) { - return + return; } - console.info('Stream connection closed. Resubscribing') + console.info("Stream connection closed. Resubscribing"); if (new Date().getTime() - startTime < 1000) { - await sleep(1000) + await sleep(1000); } - onConnectionLost?.() + onConnectionLost?.(); } } - } - doSubscribe() + }; + doSubscribe(); return { unsubscribe: async () => { - abortController?.abort() + abortController?.abort(); }, - } + }; } // Use the Query API to return the full contents of any specified topics @@ -319,12 +319,12 @@ export default class HttpApiClient implements ApiClient { direction = SortDirection.SORT_DIRECTION_ASCENDING, limit, pageSize, - }: QueryAllOptions + }: QueryAllOptions, ): Promise { - const out: messageApi.Envelope[] = [] - const maxPageSize = params.contentTopic.startsWith('userpreferences-') + const out: messageApi.Envelope[] = []; + const maxPageSize = params.contentTopic.startsWith("userpreferences-") ? 500 - : 100 + : 100; // Use queryIteratePages for better performance. 1/100th the number of Promises to resolve compared to queryStream for await (const page of this.queryIteratePages(params, { @@ -333,24 +333,24 @@ export default class HttpApiClient implements ApiClient { pageSize: pageSize ? Math.min(pageSize, maxPageSize) : maxPageSize, })) { for (const envelope of page) { - out.push(envelope) + out.push(envelope); if (limit && out.length === limit) { - return out + return out; } } } - return out + return out; } // Will produce an AsyncGenerator of Envelopes // Uses queryStreamPages under the hood async *queryIterator( params: QueryParams, - options: QueryStreamOptions + options: QueryStreamOptions, ): AsyncGenerator { for await (const page of this.queryIteratePages(params, options)) { for (const envelope of page) { - yield envelope + yield envelope; } } } @@ -359,40 +359,40 @@ export default class HttpApiClient implements ApiClient { // Will yield each page of results as needed async *queryIteratePages( { contentTopic, startTime, endTime }: QueryParams, - { direction, pageSize = 10 }: QueryStreamOptions + { direction, pageSize = 10 }: QueryStreamOptions, ): AsyncGenerator { if (!contentTopic || !contentTopic.length) { - throw new Error('Must specify content topics') + throw new Error("Must specify content topics"); } - const startTimeNs = toNanoString(startTime) - const endTimeNs = toNanoString(endTime) - let cursor: messageApi.Cursor | undefined + const startTimeNs = toNanoString(startTime); + const endTimeNs = toNanoString(endTime); + let cursor: messageApi.Cursor | undefined; while (true) { const pagingInfo: messageApi.PagingInfo = { limit: pageSize, direction, cursor, - } + }; const result = await this._query({ contentTopics: [contentTopic], startTimeNs, endTimeNs, pagingInfo, - }) + }); if (result.envelopes?.length) { - yield result.envelopes.map(normalizeEnvelope) + yield result.envelopes.map(normalizeEnvelope); } else { - return + return; } if (result.pagingInfo?.cursor) { - cursor = result.pagingInfo?.cursor + cursor = result.pagingInfo?.cursor; } else { - return + return; } } } @@ -400,16 +400,16 @@ export default class HttpApiClient implements ApiClient { // Take a list of queries and execute them in batches async batchQuery(queries: Query[]): Promise { // Group queries into batches of 50 (implicit server-side limit) and then perform BatchQueries - const BATCH_SIZE = 50 + const BATCH_SIZE = 50; // Keep a list of BatchQueryRequests to execute all at once later - const batchRequests: messageApi.BatchQueryRequest[] = [] + const batchRequests: messageApi.BatchQueryRequest[] = []; // Assemble batches for (let i = 0; i < queries.length; i += BATCH_SIZE) { - const queriesInBatch = queries.slice(i, i + BATCH_SIZE) + const queriesInBatch = queries.slice(i, i + BATCH_SIZE); // Perform batch query by first compiling a list of repeated individual QueryRequests // then populating a BatchQueryRequest with that list - const constructedQueries: messageApi.QueryRequest[] = [] + const constructedQueries: messageApi.QueryRequest[] = []; for (const queryParams of queriesInBatch) { constructedQueries.push({ @@ -421,64 +421,64 @@ export default class HttpApiClient implements ApiClient { direction: queryParams.direction || SortDirection.SORT_DIRECTION_ASCENDING, }, - }) + }); } const batchQueryRequest = { requests: constructedQueries, - } - batchRequests.push(batchQueryRequest) + }; + batchRequests.push(batchQueryRequest); } // Execute batches const batchQueryResponses = await Promise.all( - batchRequests.map(async (batch) => this._batchQuery(batch)) - ) + batchRequests.map(async (batch) => this._batchQuery(batch)), + ); // For every batch, read all responses within the batch, and add to a list of lists of envelopes // one top-level list for every original query - const allEnvelopes: messageApi.Envelope[][] = [] + const allEnvelopes: messageApi.Envelope[][] = []; for (const batchResponse of batchQueryResponses) { if (!batchResponse.responses) { // An error on any of the batch query is propagated to the caller // for simplicity, rather than trying to return partial results - throw new Error('BatchQueryResponse missing responses') + throw new Error("BatchQueryResponse missing responses"); } for (const queryResponse of batchResponse.responses) { if (queryResponse.envelopes) { - allEnvelopes.push(queryResponse.envelopes.map(normalizeEnvelope)) + allEnvelopes.push(queryResponse.envelopes.map(normalizeEnvelope)); } else { // If no envelopes provided, then add an empty list - allEnvelopes.push([]) + allEnvelopes.push([]); } } } - return allEnvelopes + return allEnvelopes; } // Publish a message to the network // Will convert timestamps to the appropriate format expected by the network async publish( - messages: PublishParams[] + messages: PublishParams[], ): ReturnType { - const toSend: messageApi.Envelope[] = [] + const toSend: messageApi.Envelope[] = []; for (const { contentTopic, message, timestamp } of messages) { if (!contentTopic.length) { - throw new Error('Content topic cannot be empty string') + throw new Error("Content topic cannot be empty string"); } if (!message.length) { - throw new Error('0 length messages not allowed') + throw new Error("0 length messages not allowed"); } - const dt = timestamp || new Date() + const dt = timestamp || new Date(); toSend.push({ contentTopic, timestampNs: toNanoString(dt), message: Uint8Array.from(message), - }) + }); } - return this._publish({ envelopes: toSend }) + return this._publish({ envelopes: toSend }); } // Subscribe to a list of topics. @@ -487,39 +487,39 @@ export default class HttpApiClient implements ApiClient { subscribe( params: SubscribeParams, callback: SubscribeCallback, - onConnectionLost?: OnConnectionLostCallback + onConnectionLost?: OnConnectionLostCallback, ): SubscriptionManager { if (!params.contentTopics.length) { - throw new Error('Must provide list of contentTopics to subscribe to') + throw new Error("Must provide list of contentTopics to subscribe to"); } return this._subscribe( params, (env) => callback(normalizeEnvelope(env)), - onConnectionLost - ) + onConnectionLost, + ); } private getToken(): Promise { if (!this.authCache) { - throw new Error('AuthCache is not set on API Client') + throw new Error("AuthCache is not set on API Client"); } - return this.authCache.getToken() + return this.authCache.getToken(); } setAuthenticator( authenticator: Authenticator, - cacheExpirySeconds?: number + cacheExpirySeconds?: number, ): void { - this.authCache = new AuthCache(authenticator, cacheExpirySeconds) + this.authCache = new AuthCache(authenticator, cacheExpirySeconds); } headers(): Headers { - const headers = new Headers() - headers.set(clientVersionHeaderKey, this.version) + const headers = new Headers(); + headers.set(clientVersionHeaderKey, this.version); if (this.appVersion) { - headers.set(appVersionHeaderKey, this.appVersion) + headers.set(appVersionHeaderKey, this.appVersion); } - return headers + return headers; } } diff --git a/packages/js-sdk/src/Client.ts b/packages/js-sdk/src/Client.ts index ed5cac34e..f5aa64a5a 100644 --- a/packages/js-sdk/src/Client.ts +++ b/packages/js-sdk/src/Client.ts @@ -2,91 +2,91 @@ import { ContentTypeId, type ContentCodec, type EncodedContent, -} from '@xmtp/content-type-primitives' -import { ContentTypeText, TextCodec } from '@xmtp/content-type-text' -import { messageApi, content as proto } from '@xmtp/proto' -import { getAddress, type WalletClient } from 'viem' -import KeystoreAuthenticator from '@/authn/KeystoreAuthenticator' -import Conversations from '@/conversations/Conversations' +} from "@xmtp/content-type-primitives"; +import { ContentTypeText, TextCodec } from "@xmtp/content-type-text"; +import { messageApi, content as proto } from "@xmtp/proto"; +import { getAddress, type WalletClient } from "viem"; +import KeystoreAuthenticator from "@/authn/KeystoreAuthenticator"; +import Conversations from "@/conversations/Conversations"; import { PublicKeyBundle, SignedPublicKeyBundle, -} from '@/crypto/PublicKeyBundle' -import BrowserStoragePersistence from '@/keystore/persistence/BrowserStoragePersistence' -import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' -import type { Persistence } from '@/keystore/persistence/interface' -import { KeystoreProviderUnavailableError } from '@/keystore/providers/errors' -import KeyGeneratorKeystoreProvider from '@/keystore/providers/KeyGeneratorKeystoreProvider' -import NetworkKeystoreProvider from '@/keystore/providers/NetworkKeystoreProvider' -import SnapProvider from '@/keystore/providers/SnapProvider' -import StaticKeystoreProvider from '@/keystore/providers/StaticKeystoreProvider' +} from "@/crypto/PublicKeyBundle"; +import BrowserStoragePersistence from "@/keystore/persistence/BrowserStoragePersistence"; +import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; +import type { Persistence } from "@/keystore/persistence/interface"; +import { KeystoreProviderUnavailableError } from "@/keystore/providers/errors"; +import KeyGeneratorKeystoreProvider from "@/keystore/providers/KeyGeneratorKeystoreProvider"; +import NetworkKeystoreProvider from "@/keystore/providers/NetworkKeystoreProvider"; +import SnapProvider from "@/keystore/providers/SnapProvider"; +import StaticKeystoreProvider from "@/keystore/providers/StaticKeystoreProvider"; import { mapPaginatedStream, type EnvelopeMapper, type EnvelopeMapperWithMessage, type EnvelopeWithMessage, -} from '@/utils/async' -import { isBrowser } from '@/utils/browser' -import { buildUserContactTopic, buildUserInviteTopic } from '@/utils/topic' -import { getSigner } from '@/utils/viem' +} from "@/utils/async"; +import { isBrowser } from "@/utils/browser"; +import { buildUserContactTopic, buildUserInviteTopic } from "@/utils/topic"; +import { getSigner } from "@/utils/viem"; import HttpApiClient, { ApiUrls, SortDirection, type ApiClient, type PublishParams, -} from './ApiClient' -import { compress, decompress } from './Compression' -import { decodeContactBundle, encodeContactBundle } from './ContactBundle' -import { Contacts } from './Contacts' -import { PrivateKeyBundleV1 } from './crypto/PrivateKeyBundle' -import type { KeystoreProvider } from './keystore/providers/interfaces' -import type { KeystoreInterfaces } from './keystore/rpcDefinitions' -import { hasMetamaskWithSnaps } from './keystore/snapHelpers' -import type BackupClient from './message-backup/BackupClient' -import { BackupType } from './message-backup/BackupClient' -import { createBackupClient } from './message-backup/BackupClientFactory' -import { packageName, version } from './snapInfo.json' -import type { ExtractDecodedType } from './types/client' -import type { Signer } from './types/Signer' -import type { Flatten } from './utils/typedefs' - -const { Compression } = proto +} from "./ApiClient"; +import { compress, decompress } from "./Compression"; +import { decodeContactBundle, encodeContactBundle } from "./ContactBundle"; +import { Contacts } from "./Contacts"; +import { PrivateKeyBundleV1 } from "./crypto/PrivateKeyBundle"; +import type { KeystoreProvider } from "./keystore/providers/interfaces"; +import type { KeystoreInterfaces } from "./keystore/rpcDefinitions"; +import { hasMetamaskWithSnaps } from "./keystore/snapHelpers"; +import type BackupClient from "./message-backup/BackupClient"; +import { BackupType } from "./message-backup/BackupClient"; +import { createBackupClient } from "./message-backup/BackupClientFactory"; +import { packageName, version } from "./snapInfo.json"; +import type { ExtractDecodedType } from "./types/client"; +import type { Signer } from "./types/Signer"; +import type { Flatten } from "./utils/typedefs"; + +const { Compression } = proto; // eslint-disable @typescript-eslint/explicit-module-boundary-types // eslint-disable @typescript-eslint/no-explicit-any // Default maximum allowed content size -const MaxContentSize = 100 * 1024 * 1024 // 100M +const MaxContentSize = 100 * 1024 * 1024; // 100M // Parameters for the listMessages functions export type ListMessagesOptions = { - checkAddresses?: boolean - startTime?: Date - endTime?: Date - limit?: number - direction?: messageApi.SortDirection - pageSize?: number -} + checkAddresses?: boolean; + startTime?: Date; + endTime?: Date; + limit?: number; + direction?: messageApi.SortDirection; + pageSize?: number; +}; export type ListMessagesPaginatedOptions = { - startTime?: Date - endTime?: Date - pageSize?: number - direction?: messageApi.SortDirection -} + startTime?: Date; + endTime?: Date; + pageSize?: number; + direction?: messageApi.SortDirection; +}; // Parameters for the send functions export type SendOptions = { - contentType?: ContentTypeId - compression?: proto.Compression - timestamp?: Date - ephemeral?: boolean -} + contentType?: ContentTypeId; + compression?: proto.Compression; + timestamp?: Date; + ephemeral?: boolean; +}; -export { Compression } +export { Compression }; -export type XmtpEnv = keyof typeof ApiUrls -export type PreEventCallback = () => Promise +export type XmtpEnv = keyof typeof ApiUrls; +export type PreEventCallback = () => Promise; /** * Network startup options @@ -95,12 +95,12 @@ export type NetworkOptions = { /** * Specify which XMTP environment to connect to. (default: `dev`) */ - env: XmtpEnv + env: XmtpEnv; /** * apiUrl can be used to override the `env` flag and connect to a * specific endpoint */ - apiUrl: string | undefined + apiUrl: string | undefined; /** * identifier that's included with API requests. * @@ -111,7 +111,7 @@ export type NetworkOptions = { * provide app support, especially around communicating important * SDK updates, including deprecations and required upgrades. */ - appVersion?: string + appVersion?: string; /** * Skip publishing the user's contact bundle as part of Client startup. * @@ -126,24 +126,24 @@ export type NetworkOptions = { * instance is very short-lived. For example, spinning up a Client to decrypt * a push notification. */ - skipContactPublishing: boolean + skipContactPublishing: boolean; - apiClientFactory: (options: NetworkOptions) => ApiClient -} + apiClientFactory: (options: NetworkOptions) => ApiClient; +}; export type ContentOptions = { /** * Allow configuring codecs for additional content types */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - codecs: ContentCodec[] + codecs: ContentCodec[]; /** * Set the maximum content size in bytes that is allowed by the Client. * Currently only checked when decompressing compressed content. */ - maxContentSize: number -} + maxContentSize: number; +}; export type KeyStoreOptions = { /** @@ -151,36 +151,36 @@ export type KeyStoreOptions = { * The client will attempt to use each one in sequence until one successfully * returns a Keystore instance */ - keystoreProviders: KeystoreProvider[] + keystoreProviders: KeystoreProvider[]; /** * Enable the Keystore to persist conversations in the provided storage interface */ - persistConversations: boolean + persistConversations: boolean; /** * Provide a XMTP PrivateKeyBundle encoded as a Uint8Array. * A bundle can be retried using `Client.getKeys(...)` */ - privateKeyOverride?: Uint8Array + privateKeyOverride?: Uint8Array; /** * Override the base persistence provider. * Defaults to LocalStoragePersistence, which is fine for most implementations */ - basePersistence: Persistence + basePersistence: Persistence; /** * Whether or not the persistence provider should encrypt the values. * Only disable if you are using a secure datastore that already has encryption */ - disablePersistenceEncryption: boolean + disablePersistenceEncryption: boolean; /** * A single option to allow Metamask Snaps to be used as a keystore provider */ - useSnaps: boolean -} + useSnaps: boolean; +}; export type LegacyOptions = { - publishLegacyContact?: boolean -} + publishLegacyContact?: boolean; +}; export type PreEventCallbackOptions = { /** @@ -190,7 +190,7 @@ export type PreEventCallbackOptions = { * The provided function must return a Promise and will be awaited, allowing the * developer to update the UI or insert a required delay before requesting a signature. */ - preCreateIdentityCallback?: PreEventCallback + preCreateIdentityCallback?: PreEventCallback; /** * preEnableIdentityCallback will be called immediately before an Enable Identity * wallet signature is requested from the user. @@ -198,8 +198,8 @@ export type PreEventCallbackOptions = { * The provided function must return a Promise and will be awaited, allowing the * developer to update the UI or insert a required delay before requesting a signature. */ - preEnableIdentityCallback?: PreEventCallback -} + preEnableIdentityCallback?: PreEventCallback; +}; /** * Aggregate type for client options. Optional properties are used when the default value is calculated on invocation, and are computed @@ -211,7 +211,7 @@ export type ClientOptions = Flatten< ContentOptions & LegacyOptions & PreEventCallbackOptions -> +>; /** * Provide a default client configuration. These settings can be used on their own, or as a starting point for custom configurations @@ -220,7 +220,7 @@ export type ClientOptions = Flatten< export function defaultOptions(opts?: Partial): ClientOptions { const _defaultOptions: ClientOptions = { privateKeyOverride: undefined, - env: 'dev', + env: "dev", apiUrl: undefined, codecs: [new TextCodec()], maxContentSize: MaxContentSize, @@ -233,20 +233,20 @@ export function defaultOptions(opts?: Partial): ClientOptions { disablePersistenceEncryption: false, keystoreProviders: defaultKeystoreProviders(), apiClientFactory: createHttpApiClientFromOptions, - } + }; if (opts?.codecs) { - opts.codecs = _defaultOptions.codecs.concat(opts.codecs) + opts.codecs = _defaultOptions.codecs.concat(opts.codecs); } if (opts?.useSnaps) { opts.keystoreProviders = [ new SnapProvider(`npm:${packageName}`, version), ..._defaultOptions.keystoreProviders, - ] + ]; } - return { ..._defaultOptions, ...opts } as ClientOptions + return { ..._defaultOptions, ...opts } as ClientOptions; } /** @@ -255,57 +255,57 @@ export function defaultOptions(opts?: Partial): ClientOptions { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export default class Client { - address: string - keystore: KeystoreInterfaces - apiClient: ApiClient - contacts: Contacts - publicKeyBundle: PublicKeyBundle + address: string; + keystore: KeystoreInterfaces; + apiClient: ApiClient; + contacts: Contacts; + publicKeyBundle: PublicKeyBundle; private knownPublicKeyBundles: Map< string, PublicKeyBundle | SignedPublicKeyBundle - > // addresses and key bundles that we have witnessed + >; // addresses and key bundles that we have witnessed - private _backupClient: BackupClient - private readonly _conversations: Conversations + private _backupClient: BackupClient; + private readonly _conversations: Conversations; // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _codecs: Map> - private _maxContentSize: number + private _codecs: Map>; + private _maxContentSize: number; constructor( publicKeyBundle: PublicKeyBundle, apiClient: ApiClient, backupClient: BackupClient, - keystore: KeystoreInterfaces + keystore: KeystoreInterfaces, ) { this.knownPublicKeyBundles = new Map< string, PublicKeyBundle | SignedPublicKeyBundle - >() + >(); // TODO: Remove keys and legacyKeys - this.keystore = keystore - this.publicKeyBundle = publicKeyBundle - this.address = publicKeyBundle.walletSignatureAddress() - this._conversations = new Conversations(this) - this._codecs = new Map() - this._maxContentSize = MaxContentSize - this.apiClient = apiClient - this._backupClient = backupClient - this.contacts = new Contacts(this) + this.keystore = keystore; + this.publicKeyBundle = publicKeyBundle; + this.address = publicKeyBundle.walletSignatureAddress(); + this._conversations = new Conversations(this); + this._codecs = new Map(); + this._maxContentSize = MaxContentSize; + this.apiClient = apiClient; + this._backupClient = backupClient; + this.contacts = new Contacts(this); } /** * @type {Conversations} */ get conversations(): Conversations { - return this._conversations + return this._conversations; } get backupType(): BackupType { - return this._backupClient.backupType + return this._backupClient.backupType; } get signedPublicKeyBundle(): SignedPublicKeyBundle { - return SignedPublicKeyBundle.fromLegacyBundle(this.publicKeyBundle) + return SignedPublicKeyBundle.fromLegacyBundle(this.publicKeyBundle); } /** @@ -317,27 +317,27 @@ export default class Client { // eslint-disable-next-line @typescript-eslint/no-explicit-any static async create[] = []>( wallet: Signer | WalletClient | null, - opts?: Partial & { codecs?: ContentCodecs } + opts?: Partial & { codecs?: ContentCodecs }, ): Promise< Client< ExtractDecodedType<[...ContentCodecs, TextCodec][number]> | undefined > > { - const signer = getSigner(wallet) - const options = defaultOptions(opts) - const apiClient = options.apiClientFactory(options) - const keystore = await bootstrapKeystore(options, apiClient, signer) + const signer = getSigner(wallet); + const options = defaultOptions(opts); + const apiClient = options.apiClientFactory(options); + const keystore = await bootstrapKeystore(options, apiClient, signer); const publicKeyBundle = new PublicKeyBundle( - await keystore.getPublicKeyBundle() - ) - const address = publicKeyBundle.walletSignatureAddress() - apiClient.setAuthenticator(new KeystoreAuthenticator(keystore)) - const backupClient = await Client.setupBackupClient(address, options.env) + await keystore.getPublicKeyBundle(), + ); + const address = publicKeyBundle.walletSignatureAddress(); + apiClient.setAuthenticator(new KeystoreAuthenticator(keystore)); + const backupClient = await Client.setupBackupClient(address, options.env); const client = new Client< ExtractDecodedType<[...ContentCodecs, TextCodec][number]> | undefined - >(publicKeyBundle, apiClient, backupClient, keystore) - await client.init(options) - return client + >(publicKeyBundle, apiClient, backupClient, keystore); + await client.init(options); + return client; } /** @@ -352,76 +352,79 @@ export default class Client { */ static async getKeys( wallet: Signer | WalletClient | null, - opts?: Partial & { codecs?: U } + opts?: Partial & { codecs?: U }, ): Promise { - const client = await Client.create(getSigner(wallet), opts) - const keys = await client.keystore.getPrivateKeyBundle() - return new PrivateKeyBundleV1(keys).encode() + const client = await Client.create(getSigner(wallet), opts); + const keys = await client.keystore.getPrivateKeyBundle(); + return new PrivateKeyBundleV1(keys).encode(); } /** * Tells the caller whether the browser has a Snaps-compatible version of MetaMask installed */ static isSnapsReady() { - return hasMetamaskWithSnaps() + return hasMetamaskWithSnaps(); } private static async setupBackupClient( walletAddress: string, - env: keyof typeof ApiUrls + env: keyof typeof ApiUrls, ): Promise { // Hard-code the provider to use for now const selectBackupProvider = async () => { return Promise.resolve({ - type: env === 'local' ? BackupType.xmtpTopicStore : BackupType.none, - }) - } - return createBackupClient(walletAddress, selectBackupProvider) + type: env === "local" ? BackupType.xmtpTopicStore : BackupType.none, + }); + }; + return createBackupClient(walletAddress, selectBackupProvider); } private async init(options: ClientOptions): Promise { options.codecs.forEach((codec) => { - this.registerCodec(codec) - }) - this._maxContentSize = options.maxContentSize + this.registerCodec(codec); + }); + this._maxContentSize = options.maxContentSize; if (!options.skipContactPublishing) { - await this.ensureUserContactPublished(options.publishLegacyContact) + await this.ensureUserContactPublished(options.publishLegacyContact); } } // gracefully shut down the client async close(): Promise { - return undefined + return undefined; } private async ensureUserContactPublished(legacy = false): Promise { - const bundle = await getUserContactFromNetwork(this.apiClient, this.address) + const bundle = await getUserContactFromNetwork( + this.apiClient, + this.address, + ); if ( bundle && bundle instanceof SignedPublicKeyBundle && this.signedPublicKeyBundle.equals(bundle) ) { - return + return; } // TEMPORARY: publish V1 contact to make sure there is one in the topic // in order to preserve compatibility with pre-v7 clients. // Remove when pre-v7 clients are deprecated - await this.publishUserContact(true) + await this.publishUserContact(true); if (!legacy) { - await this.publishUserContact(legacy) + await this.publishUserContact(legacy); } } // PRIVATE: publish the key bundle into the contact topic // left public for testing purposes async publishUserContact(legacy = false): Promise { - const bundle = legacy ? this.publicKeyBundle : this.signedPublicKeyBundle + const bundle = legacy ? this.publicKeyBundle : this.signedPublicKeyBundle; await this.publishEnvelopes([ { contentTopic: buildUserContactTopic(this.address), message: encodeContactBundle(bundle), }, - ]) + ]); } /** @@ -432,157 +435,157 @@ export default class Client { * See also [#canMessage]. */ async getUserContact( - peerAddress: string + peerAddress: string, ): Promise { - peerAddress = getAddress(peerAddress) // EIP55 normalize the address case. - const existingBundle = this.knownPublicKeyBundles.get(peerAddress) + peerAddress = getAddress(peerAddress); // EIP55 normalize the address case. + const existingBundle = this.knownPublicKeyBundles.get(peerAddress); if (existingBundle) { - return existingBundle + return existingBundle; } const newBundle = await getUserContactFromNetwork( this.apiClient, - peerAddress - ) + peerAddress, + ); if (newBundle) { - this.knownPublicKeyBundles.set(peerAddress, newBundle) + this.knownPublicKeyBundles.set(peerAddress, newBundle); } - return newBundle + return newBundle; } /** * Identical to getUserContact but for multiple peer addresses */ async getUserContacts( - peerAddresses: string[] + peerAddresses: string[], ): Promise<(PublicKeyBundle | SignedPublicKeyBundle | undefined)[]> { // EIP55 normalize all peer addresses const normalizedAddresses = peerAddresses.map((address) => - getAddress(address) - ) + getAddress(address), + ); // The logic here is tricky because we need to do a batch query for any uncached bundles, // then interleave back into an ordered array. So we create a map // and fill it with cached values, then take any undefined entries and form a BatchQuery from those. const addressToBundle = new Map< string, PublicKeyBundle | SignedPublicKeyBundle | undefined - >() - const uncachedAddresses = [] + >(); + const uncachedAddresses = []; for (const address of normalizedAddresses) { - const existingBundle = this.knownPublicKeyBundles.get(address) + const existingBundle = this.knownPublicKeyBundles.get(address); if (existingBundle) { - addressToBundle.set(address, existingBundle) + addressToBundle.set(address, existingBundle); } else { - addressToBundle.set(address, undefined) - uncachedAddresses.push(address) + addressToBundle.set(address, undefined); + uncachedAddresses.push(address); } } // Now do a getUserContactsFromNetwork call const newBundles = await getUserContactsFromNetwork( this.apiClient, - uncachedAddresses - ) + uncachedAddresses, + ); // Now merge the newBundles into the addressToBundle map for (let i = 0; i < newBundles.length; i++) { - const address = uncachedAddresses[i] - const bundle = newBundles[i] - addressToBundle.set(address, bundle) + const address = uncachedAddresses[i]; + const bundle = newBundles[i]; + addressToBundle.set(address, bundle); // If the bundle is not undefined, cache it if (bundle) { - this.knownPublicKeyBundles.set(address, bundle) + this.knownPublicKeyBundles.set(address, bundle); } } // Finally return the bundles in the same order as the input addresses - return normalizedAddresses.map((address) => addressToBundle.get(address)) + return normalizedAddresses.map((address) => addressToBundle.get(address)); } /** * Used to force getUserContact fetch contact from the network. */ forgetContact(peerAddress: string) { - peerAddress = getAddress(peerAddress) // EIP55 normalize the address case. - this.knownPublicKeyBundles.delete(peerAddress) + peerAddress = getAddress(peerAddress); // EIP55 normalize the address case. + this.knownPublicKeyBundles.delete(peerAddress); } - public async canMessage(peerAddress: string): Promise - public async canMessage(peerAddress: string[]): Promise + public async canMessage(peerAddress: string): Promise; + public async canMessage(peerAddress: string[]): Promise; /** * Check if @peerAddress can be messaged, specifically * it checks that a PublicKeyBundle can be found for the given address */ public async canMessage( - peerAddress: string | string[] + peerAddress: string | string[], ): Promise { try { if (Array.isArray(peerAddress)) { - const contacts = await this.getUserContacts(peerAddress) - return contacts.map((contact) => !!contact) + const contacts = await this.getUserContacts(peerAddress); + return contacts.map((contact) => !!contact); } // Else do the single address case - const keyBundle = await this.getUserContact(peerAddress) - return keyBundle !== undefined + const keyBundle = await this.getUserContact(peerAddress); + return keyBundle !== undefined; } catch (e) { // Instead of throwing, a bad address should just return false. - return false + return false; } } static async canMessage( peerAddress: string, - opts?: Partial - ): Promise + opts?: Partial, + ): Promise; static async canMessage( peerAddress: string[], - opts?: Partial - ): Promise + opts?: Partial, + ): Promise; static async canMessage( peerAddress: string | string[], - opts?: Partial + opts?: Partial, ): Promise { - const apiUrl = opts?.apiUrl || ApiUrls[opts?.env || 'dev'] + const apiUrl = opts?.apiUrl || ApiUrls[opts?.env || "dev"]; const apiClient = new HttpApiClient(apiUrl, { appVersion: opts?.appVersion, - }) + }); if (Array.isArray(peerAddress)) { - const rawPeerAddresses: string[] = peerAddress + const rawPeerAddresses: string[] = peerAddress; // Try to normalize each of the peer addresses const normalizedPeerAddresses = rawPeerAddresses.map((address) => - getAddress(address) - ) + getAddress(address), + ); // The getUserContactsFromNetwork will return false instead of throwing // on invalid envelopes const contacts = await getUserContactsFromNetwork( apiClient, - normalizedPeerAddresses - ) - return contacts.map((contact) => !!contact) + normalizedPeerAddresses, + ); + return contacts.map((contact) => !!contact); } try { - peerAddress = getAddress(peerAddress) // EIP55 normalize the address case. + peerAddress = getAddress(peerAddress); // EIP55 normalize the address case. } catch (e) { - return false + return false; } - const keyBundle = await getUserContactFromNetwork(apiClient, peerAddress) - return keyBundle !== undefined + const keyBundle = await getUserContactFromNetwork(apiClient, peerAddress); + return keyBundle !== undefined; } private validateEnvelope(env: PublishParams): void { - const bytes = env.message + const bytes = env.message; if (!env.contentTopic) { - throw new Error('Missing content topic') + throw new Error("Missing content topic"); } if (!bytes || !bytes.length) { - throw new Error('Cannot publish empty message') + throw new Error("Cannot publish empty message"); } } @@ -595,10 +598,10 @@ export default class Client { */ async publishEnvelopes(envelopes: PublishParams[]): Promise { for (const env of envelopes) { - this.validateEnvelope(env) + this.validateEnvelope(env); } - await this.apiClient.publish(envelopes) + await this.apiClient.publish(envelopes); } /** @@ -607,12 +610,12 @@ export default class Client { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any registerCodec>( - codec: Codec + codec: Codec, ): Client> { - const id = codec.contentType - const key = `${id.authorityId}/${id.typeId}` - this._codecs.set(key, codec) - return this + const id = codec.contentType; + const key = `${id.authorityId}/${id.typeId}`; + this._codecs.set(key, codec); + return this; } /** @@ -621,15 +624,15 @@ export default class Client { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any codecFor(contentType: ContentTypeId): ContentCodec | undefined { - const key = `${contentType.authorityId}/${contentType.typeId}` - const codec = this._codecs.get(key) + const key = `${contentType.authorityId}/${contentType.typeId}`; + const codec = this._codecs.get(key); if (!codec) { - return undefined + return undefined; } if (contentType.versionMajor > codec.contentType.versionMajor) { - return undefined + return undefined; } - return codec + return codec; } /** @@ -638,59 +641,59 @@ export default class Client { */ async encodeContent( content: ContentTypes, - options?: SendOptions + options?: SendOptions, ): Promise<{ - payload: Uint8Array - shouldPush: boolean + payload: Uint8Array; + shouldPush: boolean; }> { - const contentType = options?.contentType || ContentTypeText - const codec = this.codecFor(contentType) + const contentType = options?.contentType || ContentTypeText; + const codec = this.codecFor(contentType); if (!codec) { - throw new Error('unknown content type ' + contentType) + throw new Error("unknown content type " + contentType); } - const encoded = codec.encode(content, this) + const encoded = codec.encode(content, this); - const fallback = codec.fallback(content) + const fallback = codec.fallback(content); if (fallback) { - encoded.fallback = fallback + encoded.fallback = fallback; } if ( - typeof options?.compression === 'number' && + typeof options?.compression === "number" && // do not compress content less than 10 bytes encoded.content.length >= 10 ) { - encoded.compression = options.compression + encoded.compression = options.compression; } - await compress(encoded) + await compress(encoded); return { payload: proto.EncodedContent.encode(encoded).finish(), shouldPush: codec.shouldPush(content), - } + }; } async decodeContent(contentBytes: Uint8Array): Promise<{ - content: ContentTypes - contentType: ContentTypeId - error?: Error - contentFallback?: string + content: ContentTypes; + contentType: ContentTypeId; + error?: Error; + contentFallback?: string; }> { - const encodedContent = proto.EncodedContent.decode(contentBytes) + const encodedContent = proto.EncodedContent.decode(contentBytes); if (!encodedContent.type) { - throw new Error('missing content type') + throw new Error("missing content type"); } - let content: any // eslint-disable-line @typescript-eslint/no-explicit-any - const contentType = new ContentTypeId(encodedContent.type) - let error: Error | undefined + let content: any; // eslint-disable-line @typescript-eslint/no-explicit-any + const contentType = new ContentTypeId(encodedContent.type); + let error: Error | undefined; - await decompress(encodedContent, 1000) + await decompress(encodedContent, 1000); - const codec = this.codecFor(contentType) + const codec = this.codecFor(contentType); if (codec) { - content = codec.decode(encodedContent as EncodedContent, this) + content = codec.decode(encodedContent as EncodedContent, this); } else { - error = new Error('unknown content type ' + contentType) + error = new Error("unknown content type " + contentType); } return { @@ -698,15 +701,15 @@ export default class Client { contentType, error, contentFallback: encodedContent.fallback, - } + }; } listInvitations(opts?: ListMessagesOptions): Promise { return this.listEnvelopes( buildUserInviteTopic(this.address), async (env) => env, - opts - ) + opts, + ); } /** @@ -719,12 +722,12 @@ export default class Client { async listEnvelopes( topic: string, mapper: EnvelopeMapperWithMessage, - opts?: ListMessagesOptions + opts?: ListMessagesOptions, ): Promise { if (!opts) { - opts = {} + opts = {}; } - const { startTime, endTime, limit, pageSize } = opts + const { startTime, endTime, limit, pageSize } = opts; const envelopes = await this.apiClient.query( { contentTopic: topic, startTime, endTime }, @@ -733,19 +736,19 @@ export default class Client { opts.direction || messageApi.SortDirection.SORT_DIRECTION_ASCENDING, limit, pageSize, - } - ) - const results: Out[] = [] + }, + ); + const results: Out[] = []; for (const env of envelopes) { - if (!env.message) continue + if (!env.message) continue; try { - const res = await mapper(env as EnvelopeWithMessage) - results.push(res) + const res = await mapper(env as EnvelopeWithMessage); + results.push(res); } catch (e) { - console.warn('Error in listEnvelopes mapper', e) + console.warn("Error in listEnvelopes mapper", e); } } - return results + return results; } /** @@ -754,7 +757,7 @@ export default class Client { listEnvelopesPaginated( contentTopic: string, mapper: EnvelopeMapper, - opts?: ListMessagesPaginatedOptions + opts?: ListMessagesPaginatedOptions, ): AsyncGenerator { return mapPaginatedStream( this.apiClient.queryIteratePages( @@ -763,16 +766,16 @@ export default class Client { startTime: opts?.startTime, endTime: opts?.endTime, }, - { direction: opts?.direction, pageSize: opts?.pageSize || 100 } + { direction: opts?.direction, pageSize: opts?.pageSize || 100 }, ), - mapper - ) + mapper, + ); } } function createHttpApiClientFromOptions(options: NetworkOptions): ApiClient { - const apiUrl = options.apiUrl || ApiUrls[options.env] - return new HttpApiClient(apiUrl, { appVersion: options.appVersion }) + const apiUrl = options.apiUrl || ApiUrls[options.env]; + return new HttpApiClient(apiUrl, { appVersion: options.appVersion }); } /** @@ -780,28 +783,28 @@ function createHttpApiClientFromOptions(options: NetworkOptions): ApiClient { */ async function getUserContactFromNetwork( apiClient: ApiClient, - peerAddress: string + peerAddress: string, ): Promise { const stream = apiClient.queryIterator( { contentTopic: buildUserContactTopic(peerAddress) }, - { pageSize: 5, direction: SortDirection.SORT_DIRECTION_DESCENDING } - ) + { pageSize: 5, direction: SortDirection.SORT_DIRECTION_DESCENDING }, + ); for await (const env of stream) { - if (!env.message) continue - const keyBundle = decodeContactBundle(env.message) - let address: string | undefined + if (!env.message) continue; + const keyBundle = decodeContactBundle(env.message); + let address: string | undefined; try { - address = await keyBundle?.walletSignatureAddress() + address = await keyBundle?.walletSignatureAddress(); } catch (e) { - address = undefined + address = undefined; } if (address?.toLowerCase() === peerAddress.toLowerCase()) { - return keyBundle + return keyBundle; } } - return undefined + return undefined; } /** @@ -809,42 +812,42 @@ async function getUserContactFromNetwork( */ async function getUserContactsFromNetwork( apiClient: ApiClient, - peerAddresses: string[] + peerAddresses: string[], ): Promise<(PublicKeyBundle | SignedPublicKeyBundle | undefined)[]> { - const userContactTopics = peerAddresses.map(buildUserContactTopic) + const userContactTopics = peerAddresses.map(buildUserContactTopic); const topicToEnvelopes = await apiClient.batchQuery( userContactTopics.map((topic) => ({ contentTopic: topic, pageSize: 5, direction: SortDirection.SORT_DIRECTION_DESCENDING, - })) - ) + })), + ); // Transform topicToEnvelopes into a list of PublicKeyBundles or undefined // by going through each message and attempting to decode return Promise.all( peerAddresses.map(async (address: string, index: number) => { - const envelopes = topicToEnvelopes[index] + const envelopes = topicToEnvelopes[index]; if (!envelopes) { - return undefined + return undefined; } for (const env of envelopes) { - if (!env.message) continue + if (!env.message) continue; try { - const keyBundle = decodeContactBundle(env.message) - const signingAddress = await keyBundle?.walletSignatureAddress() + const keyBundle = decodeContactBundle(env.message); + const signingAddress = await keyBundle?.walletSignatureAddress(); if (address.toLowerCase() === signingAddress.toLowerCase()) { - return keyBundle + return keyBundle; } else { - console.info('Received contact bundle with incorrect address') + console.info("Received contact bundle with incorrect address"); } } catch (e) { - console.info('Invalid contact bundle', e) + console.info("Invalid contact bundle", e); } } - return undefined - }) - ) + return undefined; + }), + ); } /** @@ -862,7 +865,7 @@ export function defaultKeystoreProviders(): KeystoreProvider[] { new NetworkKeystoreProvider(), // If the first two failed with `KeystoreProviderUnavailableError`, then generate a new key and write it to the network new KeyGeneratorKeystoreProvider(), - ] + ]; } /** @@ -871,17 +874,17 @@ export function defaultKeystoreProviders(): KeystoreProvider[] { async function bootstrapKeystore( opts: ClientOptions, apiClient: ApiClient, - wallet: Signer | null + wallet: Signer | null, ) { for (const provider of opts.keystoreProviders) { try { - return await provider.newKeystore(opts, apiClient, wallet ?? undefined) + return await provider.newKeystore(opts, apiClient, wallet ?? undefined); } catch (err) { if (err instanceof KeystoreProviderUnavailableError) { - continue + continue; } - throw err + throw err; } } - throw new Error('No keystore providers available') + throw new Error("No keystore providers available"); } diff --git a/packages/js-sdk/src/Compression.ts b/packages/js-sdk/src/Compression.ts index 80354cab9..782426bae 100644 --- a/packages/js-sdk/src/Compression.ts +++ b/packages/js-sdk/src/Compression.ts @@ -1,99 +1,99 @@ // This import has to come first so that the polyfills are registered before the stream polyfills -import { content as proto } from '@xmtp/proto' +import { content as proto } from "@xmtp/proto"; // // Compression // export async function decompress( encoded: proto.EncodedContent, - maxSize: number + maxSize: number, ): Promise { if (encoded.compression === undefined) { - return + return; } - const sink = { bytes: new Uint8Array(encoded.content.length) } + const sink = { bytes: new Uint8Array(encoded.content.length) }; await readStreamFromBytes(encoded.content) .pipeThrough( - new DecompressionStream(compressionIdFromCode(encoded.compression)) + new DecompressionStream(compressionIdFromCode(encoded.compression)), ) - .pipeTo(writeStreamToBytes(sink, maxSize)) - encoded.content = sink.bytes + .pipeTo(writeStreamToBytes(sink, maxSize)); + encoded.content = sink.bytes; } export async function compress(encoded: proto.EncodedContent): Promise { if (encoded.compression === undefined) { - return + return; } - const sink = { bytes: new Uint8Array(encoded.content.length / 10) } + const sink = { bytes: new Uint8Array(encoded.content.length / 10) }; await readStreamFromBytes(encoded.content) .pipeThrough( - new CompressionStream(compressionIdFromCode(encoded.compression)) + new CompressionStream(compressionIdFromCode(encoded.compression)), ) - .pipeTo(writeStreamToBytes(sink, encoded.content.length + 1000)) - encoded.content = sink.bytes + .pipeTo(writeStreamToBytes(sink, encoded.content.length + 1000)); + encoded.content = sink.bytes; } function compressionIdFromCode(code: proto.Compression) { if (code === proto.Compression.COMPRESSION_GZIP) { - return 'gzip' + return "gzip"; } if (code === proto.Compression.COMPRESSION_DEFLATE) { - return 'deflate' + return "deflate"; } - throw new Error('unrecognized compression algorithm') + throw new Error("unrecognized compression algorithm"); } export function readStreamFromBytes( bytes: Uint8Array, - chunkSize = 1024 + chunkSize = 1024, ): ReadableStream { - let position = 0 + let position = 0; return new ReadableStream({ pull(controller) { if (position >= bytes.length) { - return controller.close() + return controller.close(); } - let end = position + chunkSize - end = end <= bytes.length ? end : bytes.length - controller.enqueue(bytes.subarray(position, end)) - position = end + let end = position + chunkSize; + end = end <= bytes.length ? end : bytes.length; + controller.enqueue(bytes.subarray(position, end)); + position = end; }, - }) + }); } export function writeStreamToBytes( sink: { - bytes: Uint8Array + bytes: Uint8Array; }, - maxSize: number + maxSize: number, ): WritableStream { - let position = 0 + let position = 0; return new WritableStream({ write(chunk: Uint8Array) { - const end = position + chunk.length + const end = position + chunk.length; if (end > maxSize) { - throw new Error('maximum output size exceeded') + throw new Error("maximum output size exceeded"); } while (sink.bytes.length < end) { - sink.bytes = growBytes(sink.bytes, maxSize) + sink.bytes = growBytes(sink.bytes, maxSize); } - sink.bytes.set(chunk, position) - position = end + sink.bytes.set(chunk, position); + position = end; }, close() { if (position < sink.bytes.length) { - sink.bytes = sink.bytes.subarray(0, position) + sink.bytes = sink.bytes.subarray(0, position); } }, - }) + }); } function growBytes(bytes: Uint8Array, maxSize: number): Uint8Array { - let newSize = bytes.length * 2 + let newSize = bytes.length * 2; if (newSize > maxSize) { - newSize = maxSize + newSize = maxSize; } - const bigger = new Uint8Array(newSize) - bigger.set(bytes) - return bigger + const bigger = new Uint8Array(newSize); + bigger.set(bytes); + return bigger; } diff --git a/packages/js-sdk/src/ContactBundle.ts b/packages/js-sdk/src/ContactBundle.ts index 3d2648a1b..3be2c30a3 100644 --- a/packages/js-sdk/src/ContactBundle.ts +++ b/packages/js-sdk/src/ContactBundle.ts @@ -1,42 +1,42 @@ -import { contact, publicKey } from '@xmtp/proto' +import { contact, publicKey } from "@xmtp/proto"; import { PublicKeyBundle, SignedPublicKeyBundle, -} from '@/crypto/PublicKeyBundle' +} from "@/crypto/PublicKeyBundle"; // Decodes contact bundles from the contact topic. export function decodeContactBundle( - bytes: Uint8Array + bytes: Uint8Array, ): PublicKeyBundle | SignedPublicKeyBundle { - let cb: contact.ContactBundle + let cb: contact.ContactBundle; try { - cb = contact.ContactBundle.decode(bytes) + cb = contact.ContactBundle.decode(bytes); } catch (e) { - const pb = publicKey.PublicKeyBundle.decode(bytes) - cb = { v1: { keyBundle: new PublicKeyBundle(pb) }, v2: undefined } + const pb = publicKey.PublicKeyBundle.decode(bytes); + cb = { v1: { keyBundle: new PublicKeyBundle(pb) }, v2: undefined }; } if (cb.v1?.keyBundle) { - return new PublicKeyBundle(cb.v1.keyBundle) + return new PublicKeyBundle(cb.v1.keyBundle); } if (cb.v2?.keyBundle) { - return new SignedPublicKeyBundle(cb.v2.keyBundle) + return new SignedPublicKeyBundle(cb.v2.keyBundle); } - throw new Error('unknown or invalid contact bundle') + throw new Error("unknown or invalid contact bundle"); } // Encodes public key bundle for the contact topic. export function encodeContactBundle( - bundle: PublicKeyBundle | SignedPublicKeyBundle + bundle: PublicKeyBundle | SignedPublicKeyBundle, ): Uint8Array { if (bundle instanceof PublicKeyBundle) { return contact.ContactBundle.encode({ v1: { keyBundle: bundle }, v2: undefined, - }).finish() + }).finish(); } else { return contact.ContactBundle.encode({ v1: undefined, v2: { keyBundle: bundle }, - }).finish() + }).finish(); } } diff --git a/packages/js-sdk/src/Contacts.ts b/packages/js-sdk/src/Contacts.ts index 749db20d0..b8524b3e9 100644 --- a/packages/js-sdk/src/Contacts.ts +++ b/packages/js-sdk/src/Contacts.ts @@ -1,167 +1,167 @@ -import { createConsentMessage } from '@xmtp/consent-proof-signature' -import { messageApi, privatePreferences, type invitation } from '@xmtp/proto' +import { createConsentMessage } from "@xmtp/consent-proof-signature"; +import { messageApi, privatePreferences, type invitation } from "@xmtp/proto"; // eslint-disable-next-line camelcase -import type { DecryptResponse_Response } from '@xmtp/proto/ts/dist/types/keystore_api/v1/keystore.pb' -import { hashMessage, hexToBytes } from 'viem' -import { ecdsaSignerKey } from '@/crypto/Signature' -import { splitSignature } from '@/crypto/utils' -import type { ActionsMap } from '@/keystore/privatePreferencesStore' -import type { EnvelopeWithMessage } from '@/utils/async' -import type { OnConnectionLostCallback } from './ApiClient' -import type Client from './Client' -import JobRunner from './conversations/JobRunner' -import Stream from './Stream' +import type { DecryptResponse_Response } from "@xmtp/proto/ts/dist/types/keystore_api/v1/keystore.pb"; +import { hashMessage, hexToBytes } from "viem"; +import { ecdsaSignerKey } from "@/crypto/Signature"; +import { splitSignature } from "@/crypto/utils"; +import type { ActionsMap } from "@/keystore/privatePreferencesStore"; +import type { EnvelopeWithMessage } from "@/utils/async"; +import type { OnConnectionLostCallback } from "./ApiClient"; +import type Client from "./Client"; +import JobRunner from "./conversations/JobRunner"; +import Stream from "./Stream"; -export type ConsentState = 'allowed' | 'denied' | 'unknown' +export type ConsentState = "allowed" | "denied" | "unknown"; -export type ConsentListEntryType = 'address' | 'groupId' | 'inboxId' +export type ConsentListEntryType = "address" | "groupId" | "inboxId"; export type PrivatePreferencesAction = - privatePreferences.PrivatePreferencesAction + privatePreferences.PrivatePreferencesAction; -type PrivatePreferencesActionKey = keyof PrivatePreferencesAction +type PrivatePreferencesActionKey = keyof PrivatePreferencesAction; type PrivatePreferencesActionValueKey = { [K in PrivatePreferencesActionKey]: keyof NonNullable< PrivatePreferencesAction[K] - > -}[PrivatePreferencesActionKey] + >; +}[PrivatePreferencesActionKey]; export class ConsentListEntry { - value: string - entryType: ConsentListEntryType - permissionType: ConsentState + value: string; + entryType: ConsentListEntryType; + permissionType: ConsentState; constructor( value: string, entryType: ConsentListEntryType, - permissionType: ConsentState + permissionType: ConsentState, ) { - this.value = value - this.entryType = entryType - this.permissionType = permissionType + this.value = value; + this.entryType = entryType; + this.permissionType = permissionType; } get key(): string { - return `${this.entryType}-${this.value}` + return `${this.entryType}-${this.value}`; } static fromAddress( address: string, - permissionType: ConsentState = 'unknown' + permissionType: ConsentState = "unknown", ): ConsentListEntry { - return new ConsentListEntry(address, 'address', permissionType) + return new ConsentListEntry(address, "address", permissionType); } static fromGroupId( groupId: string, - permissionType: ConsentState = 'unknown' + permissionType: ConsentState = "unknown", ): ConsentListEntry { - return new ConsentListEntry(groupId, 'groupId', permissionType) + return new ConsentListEntry(groupId, "groupId", permissionType); } static fromInboxId( inboxId: string, - permissionType: ConsentState = 'unknown' + permissionType: ConsentState = "unknown", ): ConsentListEntry { - return new ConsentListEntry(inboxId, 'inboxId', permissionType) + return new ConsentListEntry(inboxId, "inboxId", permissionType); } } export class ConsentList { - client: Client - entries: Map + client: Client; + entries: Map; constructor(client: Client) { - this.entries = new Map() - this.client = client + this.entries = new Map(); + this.client = client; } allow(address: string) { - const entry = ConsentListEntry.fromAddress(address, 'allowed') - this.entries.set(entry.key, 'allowed') - return entry + const entry = ConsentListEntry.fromAddress(address, "allowed"); + this.entries.set(entry.key, "allowed"); + return entry; } deny(address: string) { - const entry = ConsentListEntry.fromAddress(address, 'denied') - this.entries.set(entry.key, 'denied') - return entry + const entry = ConsentListEntry.fromAddress(address, "denied"); + this.entries.set(entry.key, "denied"); + return entry; } allowGroup(groupId: string) { - const entry = ConsentListEntry.fromGroupId(groupId, 'allowed') - this.entries.set(entry.key, 'allowed') - return entry + const entry = ConsentListEntry.fromGroupId(groupId, "allowed"); + this.entries.set(entry.key, "allowed"); + return entry; } denyGroup(groupId: string) { - const entry = ConsentListEntry.fromGroupId(groupId, 'denied') - this.entries.set(entry.key, 'denied') - return entry + const entry = ConsentListEntry.fromGroupId(groupId, "denied"); + this.entries.set(entry.key, "denied"); + return entry; } allowInboxId(inboxId: string) { - const entry = ConsentListEntry.fromInboxId(inboxId, 'allowed') - this.entries.set(entry.key, 'allowed') - return entry + const entry = ConsentListEntry.fromInboxId(inboxId, "allowed"); + this.entries.set(entry.key, "allowed"); + return entry; } denyInboxId(inboxId: string) { - const entry = ConsentListEntry.fromInboxId(inboxId, 'denied') - this.entries.set(entry.key, 'denied') - return entry + const entry = ConsentListEntry.fromInboxId(inboxId, "denied"); + this.entries.set(entry.key, "denied"); + return entry; } state(address: string) { - const entry = ConsentListEntry.fromAddress(address) - return this.entries.get(entry.key) ?? 'unknown' + const entry = ConsentListEntry.fromAddress(address); + return this.entries.get(entry.key) ?? "unknown"; } groupState(groupId: string) { - const entry = ConsentListEntry.fromGroupId(groupId) - return this.entries.get(entry.key) ?? 'unknown' + const entry = ConsentListEntry.fromGroupId(groupId); + return this.entries.get(entry.key) ?? "unknown"; } inboxIdState(inboxId: string) { - const entry = ConsentListEntry.fromInboxId(inboxId) - return this.entries.get(entry.key) ?? 'unknown' + const entry = ConsentListEntry.fromInboxId(inboxId); + return this.entries.get(entry.key) ?? "unknown"; } /** * Decode messages and save them to the keystore */ async decodeMessages(messageMap: Map) { - const messages = Array.from(messageMap.values()) + const messages = Array.from(messageMap.values()); // decrypt messages const { responses } = await this.client.keystore.selfDecrypt({ requests: messages.map((message) => ({ payload: message })), - }) + }); const decryptedMessageEntries = Array.from(messageMap.keys()).map( (key, index) => // eslint-disable-next-line camelcase - [key, responses[index]] as [string, DecryptResponse_Response] - ) + [key, responses[index]] as [string, DecryptResponse_Response], + ); // decode decrypted messages into actions, convert to map const actionsMap = decryptedMessageEntries.reduce( (result, [key, response]) => { if (response.result?.decrypted) { const action = privatePreferences.PrivatePreferencesAction.decode( - response.result.decrypted - ) - result.set(key, action) + response.result.decrypted, + ); + result.set(key, action); } - return result + return result; }, - new Map() - ) + new Map(), + ); // save actions to keystore - await this.client.keystore.savePrivatePreferences(actionsMap) + await this.client.keystore.savePrivatePreferences(actionsMap); - return actionsMap + return actionsMap; } /* @@ -169,33 +169,34 @@ export class ConsentList { */ processActions(actionsMap: ActionsMap) { // actions to process - const actions = Array.from(actionsMap.values()) + const actions = Array.from(actionsMap.values()); // update the consent list actions.forEach((action) => { action.allowAddress?.walletAddresses.forEach((address) => { - this.allow(address) - }) + this.allow(address); + }); action.denyAddress?.walletAddresses.forEach((address) => { - this.deny(address) - }) + this.deny(address); + }); action.allowGroup?.groupIds.forEach((groupId) => { - this.allowGroup(groupId) - }) + this.allowGroup(groupId); + }); action.denyGroup?.groupIds.forEach((groupId) => { - this.denyGroup(groupId) - }) + this.denyGroup(groupId); + }); action.allowInboxId?.inboxIds.forEach((inboxId) => { - this.allowInboxId(inboxId) - }) + this.allowInboxId(inboxId); + }); action.denyInboxId?.inboxIds.forEach((inboxId) => { - this.denyInboxId(inboxId) - }) - }) + this.denyInboxId(inboxId); + }); + }); } async stream(onConnectionLost?: OnConnectionLostCallback) { - const contentTopic = await this.client.keystore.getPrivatePreferencesTopic() + const contentTopic = + await this.client.keystore.getPrivatePreferencesTopic(); return Stream.create( this.client, @@ -203,32 +204,33 @@ export class ConsentList { async (envelope) => { // ignore envelopes without message or timestamp if (!envelope.message || !envelope.timestampNs) { - return undefined + return undefined; } // decode message and save to keystore const actionsMap = await this.decodeMessages( - new Map([[envelope.timestampNs, envelope.message]]) - ) + new Map([[envelope.timestampNs, envelope.message]]), + ); // update consent list - this.processActions(actionsMap) + this.processActions(actionsMap); // return the action - return actionsMap.get(envelope.timestampNs) + return actionsMap.get(envelope.timestampNs); }, undefined, - onConnectionLost - ) + onConnectionLost, + ); } reset() { // clear existing entries - this.entries.clear() + this.entries.clear(); } async load(startTime?: Date) { - const contentTopic = await this.client.keystore.getPrivatePreferencesTopic() + const contentTopic = + await this.client.keystore.getPrivatePreferencesTopic(); // get private preferences from the network const messageEntries = ( @@ -242,85 +244,89 @@ export class ConsentList { // ensure messages are in ascending order direction: messageApi.SortDirection.SORT_DIRECTION_ASCENDING, startTime, - } + }, ) ) // filter out messages with no timestamp - .filter(([timestampNs]) => Boolean(timestampNs)) as [string, Uint8Array][] + .filter(([timestampNs]) => Boolean(timestampNs)) as [ + string, + Uint8Array, + ][]; // decode messages and save them to keystore - await this.decodeMessages(new Map(messageEntries)) + await this.decodeMessages(new Map(messageEntries)); // get all actions from keystore - const actionsMap = this.client.keystore.getPrivatePreferences() + const actionsMap = this.client.keystore.getPrivatePreferences(); // reset consent list - this.reset() + this.reset(); // process actions and update consent list - this.processActions(actionsMap) + this.processActions(actionsMap); - return this.entries + return this.entries; } async publish(entries: ConsentListEntry[]) { // this reduce is purposefully verbose for type safety const action = entries.reduce((result, entry) => { - let actionKey: PrivatePreferencesActionKey - let valueKey: PrivatePreferencesActionValueKey - let values: string[] + let actionKey: PrivatePreferencesActionKey; + let valueKey: PrivatePreferencesActionValueKey; + let values: string[]; // ignore unknown permission types - if (entry.permissionType === 'unknown') { - return result + if (entry.permissionType === "unknown") { + return result; } switch (entry.entryType) { - case 'address': { + case "address": { actionKey = - entry.permissionType === 'allowed' ? 'allowAddress' : 'denyAddress' - valueKey = 'walletAddresses' - values = result[actionKey]?.[valueKey] ?? [] - break + entry.permissionType === "allowed" ? "allowAddress" : "denyAddress"; + valueKey = "walletAddresses"; + values = result[actionKey]?.[valueKey] ?? []; + break; } - case 'groupId': { + case "groupId": { actionKey = - entry.permissionType === 'allowed' ? 'allowGroup' : 'denyGroup' - valueKey = 'groupIds' - values = result[actionKey]?.[valueKey] ?? [] - break + entry.permissionType === "allowed" ? "allowGroup" : "denyGroup"; + valueKey = "groupIds"; + values = result[actionKey]?.[valueKey] ?? []; + break; } - case 'inboxId': { + case "inboxId": { actionKey = - entry.permissionType === 'allowed' ? 'allowInboxId' : 'denyInboxId' - valueKey = 'inboxIds' - values = result[actionKey]?.[valueKey] ?? [] - break + entry.permissionType === "allowed" ? "allowInboxId" : "denyInboxId"; + valueKey = "inboxIds"; + values = result[actionKey]?.[valueKey] ?? []; + break; } default: - return result + return result; } return { ...result, [actionKey]: { [valueKey]: [...values, entry.value], }, - } - }, {} as PrivatePreferencesAction) + }; + }, {} as PrivatePreferencesAction); // get envelopes to publish (there should only be one) - const envelopes = await this.client.keystore.createPrivatePreference(action) + const envelopes = + await this.client.keystore.createPrivatePreference(action); // publish private preferences update - await this.client.publishEnvelopes(envelopes) + await this.client.publishEnvelopes(envelopes); // persist newly published private preference to keystore this.client.keystore.savePrivatePreferences( - new Map([[envelopes[0].timestamp!.getTime().toString(), action]]) - ) + new Map([[envelopes[0].timestamp!.getTime().toString(), action]]), + ); // update local entries after publishing entries.forEach((entry) => { - this.entries.set(entry.key, entry.permissionType) - }) + this.entries.set(entry.key, entry.permissionType); + }); } } @@ -328,19 +334,19 @@ export class Contacts { /** * Addresses that the client has connected to */ - addresses: Set + addresses: Set; /** * XMTP client */ - client: Client - #consentList: ConsentList - #jobRunner: JobRunner + client: Client; + #consentList: ConsentList; + #jobRunner: JobRunner; constructor(client: Client) { - this.addresses = new Set() - this.client = client - this.#consentList = new ConsentList(client) - this.#jobRunner = new JobRunner('user-preferences', client.keystore) + this.addresses = new Set(); + this.client = client; + this.#consentList = new ConsentList(client); + this.#jobRunner = new JobRunner("user-preferences", client.keystore); } /** @@ -348,166 +354,170 @@ export class Contacts { */ #validateConsentSignature( { signature, timestamp }: invitation.ConsentProofPayload, - peerAddress: string + peerAddress: string, ): boolean { - const timestampMs = Number(timestamp) + const timestampMs = Number(timestamp); if (!signature || !timestampMs) { - return false + return false; } // timestamp should be in the past if (timestampMs > Date.now()) { - return false + return false; } // timestamp should be within the last 30 days if (timestampMs < Date.now() - 1000 * 60 * 60 * 24 * 30) { - return false + return false; } - const signatureData = splitSignature(signature as `0x${string}`) - const message = createConsentMessage(peerAddress, timestampMs) - const digest = hexToBytes(hashMessage(message)) + const signatureData = splitSignature(signature as `0x${string}`); + const message = createConsentMessage(peerAddress, timestampMs); + const digest = hexToBytes(hashMessage(message)); // Recover public key - const publicKey = ecdsaSignerKey(digest, signatureData) - return publicKey?.getEthereumAddress() === this.client.address + const publicKey = ecdsaSignerKey(digest, signatureData); + return publicKey?.getEthereumAddress() === this.client.address; } async loadConsentList(startTime?: Date) { return this.#jobRunner.run(async (lastRun) => { // allow for override of startTime - const entries = await this.#consentList.load(startTime ?? lastRun) + const entries = await this.#consentList.load(startTime ?? lastRun); try { - const conversations = await this.client.conversations.list() + const conversations = await this.client.conversations.list(); const validConsentProofAddresses: string[] = conversations.reduce( (result, conversation) => { if ( conversation.consentProof && - this.consentState(conversation.peerAddress) === 'unknown' && + this.consentState(conversation.peerAddress) === "unknown" && this.#validateConsentSignature( conversation.consentProof, - conversation.peerAddress + conversation.peerAddress, ) ) { - return result.concat(conversation.peerAddress) + return result.concat(conversation.peerAddress); } else { - return result + return result; } }, - [] as string[] - ) + [] as string[], + ); if (validConsentProofAddresses.length) { - await this.client.contacts.allow(validConsentProofAddresses) + await this.client.contacts.allow(validConsentProofAddresses); } } catch (err) { - console.log(err) + console.log(err); } - return entries - }) + return entries; + }); } async refreshConsentList() { // clear existing consent list - this.#consentList.reset() + this.#consentList.reset(); // reset last run time to the epoch - await this.#jobRunner.resetLastRunTime() + await this.#jobRunner.resetLastRunTime(); // reload the consent list - return this.loadConsentList() + return this.loadConsentList(); } async streamConsentList(onConnectionLost?: OnConnectionLostCallback) { - return this.#consentList.stream(onConnectionLost) + return this.#consentList.stream(onConnectionLost); } setConsentListEntries(entries: ConsentListEntry[]) { if (!entries.length) { - return + return; } - this.#consentList.reset() + this.#consentList.reset(); entries.forEach((entry) => { - if (entry.permissionType === 'allowed') { - this.#consentList.allow(entry.value) + if (entry.permissionType === "allowed") { + this.#consentList.allow(entry.value); } - if (entry.permissionType === 'denied') { - this.#consentList.deny(entry.value) + if (entry.permissionType === "denied") { + this.#consentList.deny(entry.value); } - }) + }); } isAllowed(address: string) { - return this.#consentList.state(address) === 'allowed' + return this.#consentList.state(address) === "allowed"; } isDenied(address: string) { - return this.#consentList.state(address) === 'denied' + return this.#consentList.state(address) === "denied"; } isGroupAllowed(groupId: string) { - return this.#consentList.groupState(groupId) === 'allowed' + return this.#consentList.groupState(groupId) === "allowed"; } isGroupDenied(groupId: string) { - return this.#consentList.groupState(groupId) === 'denied' + return this.#consentList.groupState(groupId) === "denied"; } isInboxAllowed(inboxId: string) { - return this.#consentList.inboxIdState(inboxId) === 'allowed' + return this.#consentList.inboxIdState(inboxId) === "allowed"; } isInboxDenied(inboxId: string) { - return this.#consentList.inboxIdState(inboxId) === 'denied' + return this.#consentList.inboxIdState(inboxId) === "denied"; } consentState(address: string) { - return this.#consentList.state(address) + return this.#consentList.state(address); } groupConsentState(groupId: string) { - return this.#consentList.groupState(groupId) + return this.#consentList.groupState(groupId); } inboxConsentState(inboxId: string) { - return this.#consentList.inboxIdState(inboxId) + return this.#consentList.inboxIdState(inboxId); } async allow(addresses: string[]) { await this.#consentList.publish( addresses.map((address) => - ConsentListEntry.fromAddress(address, 'allowed') - ) - ) + ConsentListEntry.fromAddress(address, "allowed"), + ), + ); } async deny(addresses: string[]) { await this.#consentList.publish( addresses.map((address) => - ConsentListEntry.fromAddress(address, 'denied') - ) - ) + ConsentListEntry.fromAddress(address, "denied"), + ), + ); } async allowGroups(groupIds: string[]) { await this.#consentList.publish( groupIds.map((groupId) => - ConsentListEntry.fromGroupId(groupId, 'allowed') - ) - ) + ConsentListEntry.fromGroupId(groupId, "allowed"), + ), + ); } async denyGroups(groupIds: string[]) { await this.#consentList.publish( - groupIds.map((groupId) => ConsentListEntry.fromGroupId(groupId, 'denied')) - ) + groupIds.map((groupId) => + ConsentListEntry.fromGroupId(groupId, "denied"), + ), + ); } async allowInboxes(inboxIds: string[]) { await this.#consentList.publish( inboxIds.map((inboxId) => - ConsentListEntry.fromInboxId(inboxId, 'allowed') - ) - ) + ConsentListEntry.fromInboxId(inboxId, "allowed"), + ), + ); } async denyInboxes(inboxIds: string[]) { await this.#consentList.publish( - inboxIds.map((inboxId) => ConsentListEntry.fromInboxId(inboxId, 'denied')) - ) + inboxIds.map((inboxId) => + ConsentListEntry.fromInboxId(inboxId, "denied"), + ), + ); } } diff --git a/packages/js-sdk/src/Invitation.ts b/packages/js-sdk/src/Invitation.ts index 3a38bac43..7fa85442b 100644 --- a/packages/js-sdk/src/Invitation.ts +++ b/packages/js-sdk/src/Invitation.ts @@ -1,26 +1,26 @@ -import { invitation, type messageApi } from '@xmtp/proto' -import Long from 'long' -import { dateToNs } from '@/utils/date' -import { buildDirectMessageTopicV2 } from '@/utils/topic' -import Ciphertext from './crypto/Ciphertext' -import crypto from './crypto/crypto' -import { decrypt, encrypt } from './crypto/encryption' -import type { PrivateKeyBundleV2 } from './crypto/PrivateKeyBundle' -import { SignedPublicKeyBundle } from './crypto/PublicKeyBundle' +import { invitation, type messageApi } from "@xmtp/proto"; +import Long from "long"; +import { dateToNs } from "@/utils/date"; +import { buildDirectMessageTopicV2 } from "@/utils/topic"; +import Ciphertext from "./crypto/Ciphertext"; +import crypto from "./crypto/crypto"; +import { decrypt, encrypt } from "./crypto/encryption"; +import type { PrivateKeyBundleV2 } from "./crypto/PrivateKeyBundle"; +import { SignedPublicKeyBundle } from "./crypto/PublicKeyBundle"; export type InvitationContext = { - conversationId: string - metadata: { [k: string]: string } -} + conversationId: string; + metadata: { [k: string]: string }; +}; /** * InvitationV1 is a protobuf message to be encrypted and used as the ciphertext in a SealedInvitationV1 message */ export class InvitationV1 implements invitation.InvitationV1 { - topic: string - context: InvitationContext | undefined - aes256GcmHkdfSha256: invitation.InvitationV1_Aes256gcmHkdfsha256 // eslint-disable-line camelcase - consentProof: invitation.ConsentProofPayload | undefined + topic: string; + context: InvitationContext | undefined; + aes256GcmHkdfSha256: invitation.InvitationV1_Aes256gcmHkdfsha256; // eslint-disable-line camelcase + consentProof: invitation.ConsentProofPayload | undefined; constructor({ topic, @@ -29,49 +29,49 @@ export class InvitationV1 implements invitation.InvitationV1 { consentProof, }: invitation.InvitationV1) { if (!topic || !topic.length) { - throw new Error('Missing topic') + throw new Error("Missing topic"); } if ( !aes256GcmHkdfSha256 || !aes256GcmHkdfSha256.keyMaterial || !aes256GcmHkdfSha256.keyMaterial.length ) { - throw new Error('Missing key material') + throw new Error("Missing key material"); } - this.topic = topic - this.context = context - this.aes256GcmHkdfSha256 = aes256GcmHkdfSha256 - this.consentProof = consentProof + this.topic = topic; + this.context = context; + this.aes256GcmHkdfSha256 = aes256GcmHkdfSha256; + this.consentProof = consentProof; } static createRandom( context?: invitation.InvitationV1_Context, - consentProof?: invitation.ConsentProofPayload + consentProof?: invitation.ConsentProofPayload, ): InvitationV1 { const topic = buildDirectMessageTopicV2( Buffer.from(crypto.getRandomValues(new Uint8Array(32))) - .toString('base64') - .replace(/=*$/g, '') + .toString("base64") + .replace(/=*$/g, "") // Replace slashes with dashes so that the topic is still easily split by / // We do not treat this as needing to be valid Base64 anywhere - .replace(/\//g, '-') - ) - const keyMaterial = crypto.getRandomValues(new Uint8Array(32)) + .replace(/\//g, "-"), + ); + const keyMaterial = crypto.getRandomValues(new Uint8Array(32)); return new InvitationV1({ topic, aes256GcmHkdfSha256: { keyMaterial }, context, consentProof, - }) + }); } toBytes(): Uint8Array { - return invitation.InvitationV1.encode(this).finish() + return invitation.InvitationV1.encode(this).finish(); } static fromBytes(bytes: Uint8Array): InvitationV1 { - return new InvitationV1(invitation.InvitationV1.decode(bytes)) + return new InvitationV1(invitation.InvitationV1.decode(bytes)); } } @@ -81,9 +81,9 @@ export class InvitationV1 implements invitation.InvitationV1 { export class SealedInvitationHeaderV1 implements invitation.SealedInvitationHeaderV1 { - sender: SignedPublicKeyBundle - recipient: SignedPublicKeyBundle - createdNs: Long + sender: SignedPublicKeyBundle; + recipient: SignedPublicKeyBundle; + createdNs: Long; constructor({ sender, @@ -91,42 +91,42 @@ export class SealedInvitationHeaderV1 createdNs, }: invitation.SealedInvitationHeaderV1) { if (!sender) { - throw new Error('Missing sender') + throw new Error("Missing sender"); } if (!recipient) { - throw new Error('Missing recipient') + throw new Error("Missing recipient"); } - this.sender = new SignedPublicKeyBundle(sender) - this.recipient = new SignedPublicKeyBundle(recipient) - this.createdNs = createdNs + this.sender = new SignedPublicKeyBundle(sender); + this.recipient = new SignedPublicKeyBundle(recipient); + this.createdNs = createdNs; } toBytes(): Uint8Array { - return invitation.SealedInvitationHeaderV1.encode(this).finish() + return invitation.SealedInvitationHeaderV1.encode(this).finish(); } static fromBytes(bytes: Uint8Array): SealedInvitationHeaderV1 { return new SealedInvitationHeaderV1( - invitation.SealedInvitationHeaderV1.decode(bytes) - ) + invitation.SealedInvitationHeaderV1.decode(bytes), + ); } } export class SealedInvitationV1 implements invitation.SealedInvitationV1 { - headerBytes: Uint8Array - ciphertext: Ciphertext - private _header?: SealedInvitationHeaderV1 - private _invitation?: InvitationV1 + headerBytes: Uint8Array; + ciphertext: Ciphertext; + private _header?: SealedInvitationHeaderV1; + private _invitation?: InvitationV1; constructor({ headerBytes, ciphertext }: invitation.SealedInvitationV1) { if (!headerBytes || !headerBytes.length) { - throw new Error('Missing header bytes') + throw new Error("Missing header bytes"); } if (!ciphertext) { - throw new Error('Missing ciphertext') + throw new Error("Missing ciphertext"); } - this.headerBytes = headerBytes - this.ciphertext = new Ciphertext(ciphertext) + this.headerBytes = headerBytes; + this.ciphertext = new Ciphertext(ciphertext); } /** @@ -135,10 +135,10 @@ export class SealedInvitationV1 implements invitation.SealedInvitationV1 { get header(): SealedInvitationHeaderV1 { // Use cached value if already exists if (this._header) { - return this._header + return this._header; } - this._header = SealedInvitationHeaderV1.fromBytes(this.headerBytes) - return this._header + this._header = SealedInvitationHeaderV1.fromBytes(this.headerBytes); + return this._header; } /** @@ -147,40 +147,40 @@ export class SealedInvitationV1 implements invitation.SealedInvitationV1 { async getInvitation(viewer: PrivateKeyBundleV2): Promise { // Use cached value if already exists if (this._invitation) { - return this._invitation + return this._invitation; } // The constructors for child classes will validate that this is complete - const header = this.header - let secret: Uint8Array + const header = this.header; + let secret: Uint8Array; if (viewer.identityKey.matches(this.header.sender.identityKey)) { secret = await viewer.sharedSecret( header.recipient, header.sender.preKey, - false - ) + false, + ); } else { secret = await viewer.sharedSecret( header.sender, header.recipient.preKey, - true - ) + true, + ); } const decryptedBytes = await decrypt( this.ciphertext, secret, - this.headerBytes - ) - this._invitation = InvitationV1.fromBytes(decryptedBytes) - return this._invitation + this.headerBytes, + ); + this._invitation = InvitationV1.fromBytes(decryptedBytes); + return this._invitation; } toBytes(): Uint8Array { - return invitation.SealedInvitationV1.encode(this).finish() + return invitation.SealedInvitationV1.encode(this).finish(); } static fromBytes(bytes: Uint8Array): SealedInvitationV1 { - return new SealedInvitationV1(invitation.SealedInvitationV1.decode(bytes)) + return new SealedInvitationV1(invitation.SealedInvitationV1.decode(bytes)); } } @@ -188,37 +188,37 @@ export class SealedInvitationV1 implements invitation.SealedInvitationV1 { * Wrapper class for SealedInvitationV1 and any future iterations of SealedInvitation */ export class SealedInvitation implements invitation.SealedInvitation { - v1: SealedInvitationV1 | undefined + v1: SealedInvitationV1 | undefined; constructor({ v1 }: invitation.SealedInvitation) { if (v1) { - this.v1 = new SealedInvitationV1(v1) + this.v1 = new SealedInvitationV1(v1); } else { - throw new Error('Missing v1 or v2 invitation') + throw new Error("Missing v1 or v2 invitation"); } } toBytes(): Uint8Array { - return invitation.SealedInvitation.encode(this).finish() + return invitation.SealedInvitation.encode(this).finish(); } static fromBytes(bytes: Uint8Array): SealedInvitation { - return new SealedInvitation(invitation.SealedInvitation.decode(bytes)) + return new SealedInvitation(invitation.SealedInvitation.decode(bytes)); } static async fromEnvelope( - env: messageApi.Envelope + env: messageApi.Envelope, ): Promise { if (!env.message || !env.timestampNs) { - throw new Error('invalid invitation envelope') + throw new Error("invalid invitation envelope"); } - const sealed = SealedInvitation.fromBytes(env.message) - const envelopeTime = Long.fromString(env.timestampNs) - const headerTime = sealed.v1?.header.createdNs + const sealed = SealedInvitation.fromBytes(env.message); + const envelopeTime = Long.fromString(env.timestampNs); + const headerTime = sealed.v1?.header.createdNs; if (!headerTime || !headerTime.equals(envelopeTime)) { - throw new Error('envelope and header timestamp mistmatch') + throw new Error("envelope and header timestamp mistmatch"); } - return sealed + return sealed; } /** @@ -231,28 +231,28 @@ export class SealedInvitation implements invitation.SealedInvitation { created, invitation, }: { - sender: PrivateKeyBundleV2 - recipient: SignedPublicKeyBundle - created: Date - invitation: InvitationV1 + sender: PrivateKeyBundleV2; + recipient: SignedPublicKeyBundle; + created: Date; + invitation: InvitationV1; }): Promise { const headerBytes = new SealedInvitationHeaderV1({ sender: sender.getPublicKeyBundle(), recipient, createdNs: dateToNs(created), - }).toBytes() + }).toBytes(); const secret = await sender.sharedSecret( recipient, sender.getCurrentPreKey().publicKey, - false - ) + false, + ); - const invitationBytes = invitation.toBytes() - const ciphertext = await encrypt(invitationBytes, secret, headerBytes) + const invitationBytes = invitation.toBytes(); + const ciphertext = await encrypt(invitationBytes, secret, headerBytes); return new SealedInvitation({ v1: { headerBytes, ciphertext }, - }) + }); } } diff --git a/packages/js-sdk/src/Message.ts b/packages/js-sdk/src/Message.ts index e92390d20..8fe8d1e61 100644 --- a/packages/js-sdk/src/Message.ts +++ b/packages/js-sdk/src/Message.ts @@ -1,154 +1,154 @@ -import type { ContentTypeId } from '@xmtp/content-type-primitives' -import { message as proto, type conversationReference } from '@xmtp/proto' -import Long from 'long' -import { PublicKey } from '@/crypto/PublicKey' -import { PublicKeyBundle } from '@/crypto/PublicKeyBundle' -import type Client from './Client' +import type { ContentTypeId } from "@xmtp/content-type-primitives"; +import { message as proto, type conversationReference } from "@xmtp/proto"; +import Long from "long"; +import { PublicKey } from "@/crypto/PublicKey"; +import { PublicKeyBundle } from "@/crypto/PublicKeyBundle"; +import type Client from "./Client"; import { ConversationV1, ConversationV2, type Conversation, -} from './conversations/Conversation' -import Ciphertext from './crypto/Ciphertext' -import { sha256 } from './crypto/encryption' -import { bytesToHex } from './crypto/utils' -import type { KeystoreInterfaces } from './keystore/rpcDefinitions' -import { dateToNs, nsToDate } from './utils/date' -import { buildDecryptV1Request, getResultOrThrow } from './utils/keystore' +} from "./conversations/Conversation"; +import Ciphertext from "./crypto/Ciphertext"; +import { sha256 } from "./crypto/encryption"; +import { bytesToHex } from "./crypto/utils"; +import type { KeystoreInterfaces } from "./keystore/rpcDefinitions"; +import { dateToNs, nsToDate } from "./utils/date"; +import { buildDecryptV1Request, getResultOrThrow } from "./utils/keystore"; const headerBytesAndCiphertext = ( - msg: proto.Message + msg: proto.Message, ): [Uint8Array, Ciphertext] => { if (msg.v1?.ciphertext) { - return [msg.v1.headerBytes, new Ciphertext(msg.v1.ciphertext)] + return [msg.v1.headerBytes, new Ciphertext(msg.v1.ciphertext)]; } if (msg.v2?.ciphertext) { - return [msg.v2.headerBytes, new Ciphertext(msg.v2.ciphertext)] + return [msg.v2.headerBytes, new Ciphertext(msg.v2.ciphertext)]; } - throw new Error('unknown message version') -} + throw new Error("unknown message version"); +}; // Message is basic unit of communication on the network. // Message timestamp is set by the sender. class MessageBase { - headerBytes: Uint8Array // encoded header bytes - ciphertext: Ciphertext + headerBytes: Uint8Array; // encoded header bytes + ciphertext: Ciphertext; // content allows attaching decoded content to the Message // the message receiving APIs need to return a Message to provide access to the header fields like sender/recipient - contentType?: ContentTypeId - error?: Error + contentType?: ContentTypeId; + error?: Error; /** * Identifier that is deterministically derived from the bytes of the message * header and ciphertext, where all those bytes are authenticated. This can * be used in determining uniqueness of messages. */ - id: string - private bytes: Uint8Array + id: string; + private bytes: Uint8Array; constructor(id: string, bytes: Uint8Array, obj: proto.Message) { - ;[this.headerBytes, this.ciphertext] = headerBytesAndCiphertext(obj) - this.id = id - this.bytes = bytes + [this.headerBytes, this.ciphertext] = headerBytesAndCiphertext(obj); + this.id = id; + this.bytes = bytes; } toBytes(): Uint8Array { - return this.bytes + return this.bytes; } } // Message header carries the sender and recipient keys used to protect message. // Message timestamp is set by the sender. export class MessageV1 extends MessageBase implements proto.MessageV1 { - header: proto.MessageHeaderV1 // eslint-disable-line camelcase + header: proto.MessageHeaderV1; // eslint-disable-line camelcase // wallet address derived from the signature of the message recipient - senderAddress: string | undefined - conversation = undefined + senderAddress: string | undefined; + conversation = undefined; constructor( id: string, bytes: Uint8Array, obj: proto.Message, header: proto.MessageHeaderV1, - senderAddress: string | undefined + senderAddress: string | undefined, ) { - super(id, bytes, obj) - this.senderAddress = senderAddress - this.header = header + super(id, bytes, obj); + this.senderAddress = senderAddress; + this.header = header; } static async create( obj: proto.Message, header: proto.MessageHeaderV1, - bytes: Uint8Array + bytes: Uint8Array, ): Promise { if (!header.sender) { - throw new Error('missing message sender') + throw new Error("missing message sender"); } const senderAddress = new PublicKeyBundle( - header.sender - ).walletSignatureAddress() - const id = bytesToHex(await sha256(bytes)) - return new MessageV1(id, bytes, obj, header, senderAddress) + header.sender, + ).walletSignatureAddress(); + const id = bytesToHex(await sha256(bytes)); + return new MessageV1(id, bytes, obj, header, senderAddress); } get sent(): Date { - return new Date(this.header.timestamp.toNumber()) + return new Date(this.header.timestamp.toNumber()); } // wallet address derived from the signature of the message recipient get recipientAddress(): string | undefined { if (!this.header?.recipient?.identityKey) { - return undefined + return undefined; } return new PublicKey( - this.header.recipient.identityKey - ).walletSignatureAddress() + this.header.recipient.identityKey, + ).walletSignatureAddress(); } async decrypt( keystore: KeystoreInterfaces, - myPublicKeyBundle: PublicKeyBundle + myPublicKeyBundle: PublicKeyBundle, ): Promise { const responses = ( await keystore.decryptV1(buildDecryptV1Request([this], myPublicKeyBundle)) - ).responses + ).responses; if (!responses.length) { - throw new Error('No response from Keystore') + throw new Error("No response from Keystore"); } - const { decrypted } = getResultOrThrow(responses[0]) + const { decrypted } = getResultOrThrow(responses[0]); - return decrypted + return decrypted; } static fromBytes(bytes: Uint8Array): Promise { - const message = proto.Message.decode(bytes) - const [headerBytes] = headerBytesAndCiphertext(message) - const header = proto.MessageHeaderV1.decode(headerBytes) + const message = proto.Message.decode(bytes); + const [headerBytes] = headerBytesAndCiphertext(message); + const header = proto.MessageHeaderV1.decode(headerBytes); if (!header) { - throw new Error('missing message header') + throw new Error("missing message header"); } if (!header.sender) { - throw new Error('missing message sender') + throw new Error("missing message sender"); } if (!header.sender.identityKey) { - throw new Error('missing message sender identity key') + throw new Error("missing message sender identity key"); } if (!header.sender.preKey) { - throw new Error('missing message sender pre-key') + throw new Error("missing message sender pre-key"); } if (!header.recipient) { - throw new Error('missing message recipient') + throw new Error("missing message recipient"); } if (!header.recipient.identityKey) { - throw new Error('missing message recipient identity-key') + throw new Error("missing message recipient identity-key"); } if (!header.recipient.preKey) { - throw new Error('missing message recipient pre-key') + throw new Error("missing message recipient pre-key"); } - return MessageV1.create(message, header, bytes) + return MessageV1.create(message, header, bytes); } static async encode( @@ -156,14 +156,14 @@ export class MessageV1 extends MessageBase implements proto.MessageV1 { payload: Uint8Array, sender: PublicKeyBundle, recipient: PublicKeyBundle, - timestamp: Date + timestamp: Date, ): Promise { const header: proto.MessageHeaderV1 = { sender, recipient, timestamp: Long.fromNumber(timestamp.getTime()), - } - const headerBytes = proto.MessageHeaderV1.encode(header).finish() + }; + const headerBytes = proto.MessageHeaderV1.encode(header).finish(); const results = await keystore.encryptV1({ requests: [ { @@ -172,28 +172,28 @@ export class MessageV1 extends MessageBase implements proto.MessageV1 { payload, }, ], - }) + }); if (!results.responses.length) { - throw new Error('No response from Keystore') + throw new Error("No response from Keystore"); } - const { encrypted: ciphertext } = getResultOrThrow(results.responses[0]) + const { encrypted: ciphertext } = getResultOrThrow(results.responses[0]); const protoMsg = { v1: { headerBytes, ciphertext }, v2: undefined, - } - const bytes = proto.Message.encode(protoMsg).finish() - return MessageV1.create(protoMsg, header, bytes) + }; + const bytes = proto.Message.encode(protoMsg).finish(); + return MessageV1.create(protoMsg, header, bytes); } } export class MessageV2 extends MessageBase implements proto.MessageV2 { - senderAddress: string | undefined - private header: proto.MessageHeaderV2 - senderHmac?: Uint8Array - shouldPush?: boolean + senderAddress: string | undefined; + private header: proto.MessageHeaderV2; + senderHmac?: Uint8Array; + shouldPush?: boolean; constructor( id: string, @@ -201,12 +201,12 @@ export class MessageV2 extends MessageBase implements proto.MessageV2 { obj: proto.Message, header: proto.MessageHeaderV2, senderHmac?: Uint8Array, - shouldPush?: boolean + shouldPush?: boolean, ) { - super(id, bytes, obj) - this.header = header - this.senderHmac = senderHmac - this.shouldPush = shouldPush + super(id, bytes, obj); + this.header = header; + this.senderHmac = senderHmac; + this.shouldPush = shouldPush; } static async create( @@ -214,34 +214,34 @@ export class MessageV2 extends MessageBase implements proto.MessageV2 { header: proto.MessageHeaderV2, bytes: Uint8Array, senderHmac?: Uint8Array, - shouldPush?: boolean + shouldPush?: boolean, ): Promise { - const id = bytesToHex(await sha256(bytes)) + const id = bytesToHex(await sha256(bytes)); - return new MessageV2(id, bytes, obj, header, senderHmac, shouldPush) + return new MessageV2(id, bytes, obj, header, senderHmac, shouldPush); } get sent(): Date { - return nsToDate(this.header.createdNs) + return nsToDate(this.header.createdNs); } } -export type Message = MessageV1 | MessageV2 +export type Message = MessageV1 | MessageV2; // eslint-disable-next-line @typescript-eslint/no-explicit-any export class DecodedMessage { - id: string - messageVersion: 'v1' | 'v2' - senderAddress: string - recipientAddress?: string - sent: Date - contentTopic: string - conversation: Conversation - contentType: ContentTypeId - content: ContentTypes - error?: Error - contentBytes: Uint8Array - contentFallback?: string + id: string; + messageVersion: "v1" | "v2"; + senderAddress: string; + recipientAddress?: string; + sent: Date; + contentTopic: string; + conversation: Conversation; + contentType: ContentTypeId; + content: ContentTypes; + error?: Error; + contentBytes: Uint8Array; + contentFallback?: string; constructor({ id, @@ -256,19 +256,19 @@ export class DecodedMessage { sent, error, contentFallback, - }: Omit, 'toBytes'>) { - this.id = id - this.messageVersion = messageVersion - this.senderAddress = senderAddress - this.recipientAddress = recipientAddress - this.conversation = conversation - this.contentType = contentType - this.sent = sent - this.error = error - this.content = content - this.contentTopic = contentTopic - this.contentBytes = contentBytes - this.contentFallback = contentFallback + }: Omit, "toBytes">) { + this.id = id; + this.messageVersion = messageVersion; + this.senderAddress = senderAddress; + this.recipientAddress = recipientAddress; + this.conversation = conversation; + this.contentType = contentType; + this.sent = sent; + this.error = error; + this.content = content; + this.contentTopic = contentTopic; + this.contentBytes = contentBytes; + this.contentFallback = contentFallback; } toBytes(): Uint8Array { @@ -282,26 +282,26 @@ export class DecodedMessage { consentProofPayload: this.conversation.consentProof ?? undefined, }, sentNs: dateToNs(this.sent), - }).finish() + }).finish(); } static async fromBytes( data: Uint8Array, - client: Client + client: Client, ): Promise> { - const protoVal = proto.DecodedMessage.decode(data) - const messageVersion = protoVal.messageVersion + const protoVal = proto.DecodedMessage.decode(data); + const messageVersion = protoVal.messageVersion; - if (messageVersion !== 'v1' && messageVersion !== 'v2') { - throw new Error('Invalid message version') + if (messageVersion !== "v1" && messageVersion !== "v2") { + throw new Error("Invalid message version"); } if (!protoVal.conversation) { - throw new Error('No conversation reference found') + throw new Error("No conversation reference found"); } const { content, contentType, error, contentFallback } = - await client.decodeContent(protoVal.contentBytes) + await client.decodeContent(protoVal.contentBytes); return new DecodedMessage({ ...protoVal, @@ -313,10 +313,10 @@ export class DecodedMessage { conversation: conversationReferenceToConversation( protoVal.conversation, client, - messageVersion + messageVersion, ), contentFallback, - }) + }); } static fromV1Message( @@ -327,15 +327,15 @@ export class DecodedMessage { contentTopic: string, conversation: Conversation, error?: Error, - contentFallback?: string + contentFallback?: string, ): DecodedMessage { - const { id, senderAddress, recipientAddress, sent } = message + const { id, senderAddress, recipientAddress, sent } = message; if (!senderAddress) { - throw new Error('Sender address is required') + throw new Error("Sender address is required"); } return new DecodedMessage({ id, - messageVersion: 'v1', + messageVersion: "v1", senderAddress, recipientAddress, sent, @@ -346,7 +346,7 @@ export class DecodedMessage { conversation, error, contentFallback, - }) + }); } static fromV2Message( @@ -358,13 +358,13 @@ export class DecodedMessage { conversation: Conversation, senderAddress: string, error?: Error, - contentFallback?: string + contentFallback?: string, ): DecodedMessage { - const { id, sent } = message + const { id, sent } = message; return new DecodedMessage({ id, - messageVersion: 'v2', + messageVersion: "v2", senderAddress, sent, content, @@ -374,38 +374,38 @@ export class DecodedMessage { conversation, error, contentFallback, - }) + }); } } function conversationReferenceToConversation( reference: conversationReference.ConversationReference, client: Client, - version: DecodedMessage['messageVersion'] + version: DecodedMessage["messageVersion"], ): Conversation { - if (version === 'v1') { + if (version === "v1") { return new ConversationV1( client, reference.peerAddress, - nsToDate(reference.createdNs) - ) + nsToDate(reference.createdNs), + ); } - if (version === 'v2') { + if (version === "v2") { return new ConversationV2( client, reference.topic, reference.peerAddress, nsToDate(reference.createdNs), reference.context, - reference.consentProofPayload - ) + reference.consentProofPayload, + ); } - throw new Error(`Unknown conversation version ${version}`) + throw new Error(`Unknown conversation version ${version}`); } export function decodeContent( contentBytes: Uint8Array, - client: Client + client: Client, ) { - return client.decodeContent(contentBytes) + return client.decodeContent(contentBytes); } diff --git a/packages/js-sdk/src/PreparedMessage.ts b/packages/js-sdk/src/PreparedMessage.ts index 3d0b8cae0..fb614e0b6 100644 --- a/packages/js-sdk/src/PreparedMessage.ts +++ b/packages/js-sdk/src/PreparedMessage.ts @@ -1,29 +1,29 @@ -import type { Envelope } from '@xmtp/proto/ts/dist/types/message_api/v1/message_api.pb' -import { sha256 } from './crypto/encryption' -import { bytesToHex } from './crypto/utils' -import type { DecodedMessage } from './Message' +import type { Envelope } from "@xmtp/proto/ts/dist/types/message_api/v1/message_api.pb"; +import { sha256 } from "./crypto/encryption"; +import { bytesToHex } from "./crypto/utils"; +import type { DecodedMessage } from "./Message"; export class PreparedMessage { - messageEnvelope: Envelope - onSend: () => Promise + messageEnvelope: Envelope; + onSend: () => Promise; constructor( messageEnvelope: Envelope, - onSend: () => Promise + onSend: () => Promise, ) { - this.messageEnvelope = messageEnvelope - this.onSend = onSend + this.messageEnvelope = messageEnvelope; + this.onSend = onSend; } async messageID(): Promise { if (!this.messageEnvelope.message) { - throw new Error('no envelope message') + throw new Error("no envelope message"); } - return bytesToHex(await sha256(this.messageEnvelope.message)) + return bytesToHex(await sha256(this.messageEnvelope.message)); } async send() { - return this.onSend() + return this.onSend(); } } diff --git a/packages/js-sdk/src/Stream.ts b/packages/js-sdk/src/Stream.ts index e7bbff50c..5553ed648 100644 --- a/packages/js-sdk/src/Stream.ts +++ b/packages/js-sdk/src/Stream.ts @@ -1,12 +1,15 @@ -import type { messageApi } from '@xmtp/proto' -import type { OnConnectionLostCallback, SubscriptionManager } from './ApiClient' -import type Client from './Client' +import type { messageApi } from "@xmtp/proto"; +import type { + OnConnectionLostCallback, + SubscriptionManager, +} from "./ApiClient"; +import type Client from "./Client"; export type MessageDecoder = ( - env: messageApi.Envelope -) => Promise + env: messageApi.Envelope, +) => Promise; -export type ContentTopicUpdater = (msg: M) => string[] | undefined +export type ContentTopicUpdater = (msg: M) => string[] | undefined; /** * Stream implements an Asynchronous Iterable over messages received from a topic. @@ -14,75 +17,75 @@ export type ContentTopicUpdater = (msg: M) => string[] | undefined */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export default class Stream { - topics: string[] - client: Client + topics: string[]; + client: Client; // queue of incoming Waku messages - messages: T[] + messages: T[]; // queue of already pending Promises - resolvers: ((value: IteratorResult) => void)[] + resolvers: ((value: IteratorResult) => void)[]; // cache the callback so that it can be properly deregistered in Waku // if callback is undefined the stream is closed - callback: ((env: messageApi.Envelope) => Promise) | undefined + callback: ((env: messageApi.Envelope) => Promise) | undefined; - subscriptionManager?: SubscriptionManager + subscriptionManager?: SubscriptionManager; - onConnectionLost?: OnConnectionLostCallback + onConnectionLost?: OnConnectionLostCallback; constructor( client: Client, topics: string[], decoder: MessageDecoder, contentTopicUpdater?: ContentTopicUpdater, - onConnectionLost?: OnConnectionLostCallback + onConnectionLost?: OnConnectionLostCallback, ) { - this.messages = [] - this.resolvers = [] - this.topics = topics - this.client = client - this.callback = this.newMessageCallback(decoder, contentTopicUpdater) - this.onConnectionLost = onConnectionLost + this.messages = []; + this.resolvers = []; + this.topics = topics; + this.client = client; + this.callback = this.newMessageCallback(decoder, contentTopicUpdater); + this.onConnectionLost = onConnectionLost; } // returns new closure to handle incoming messages private newMessageCallback( decoder: MessageDecoder, - contentTopicUpdater?: ContentTopicUpdater + contentTopicUpdater?: ContentTopicUpdater, ): (env: messageApi.Envelope) => Promise { return async (env: messageApi.Envelope) => { if (!env.message) { - return + return; } try { - const msg = await decoder(env) + const msg = await decoder(env); // decoder can return undefined to signal a message to ignore/skip. if (!msg) { - return + return; } // Check to see if we should update the stream's content topic subscription if (contentTopicUpdater) { - const topics = contentTopicUpdater(msg) + const topics = contentTopicUpdater(msg); if (topics) { - this.resubscribeToTopics(topics) + this.resubscribeToTopics(topics); } } // is there a Promise already pending? - const resolver = this.resolvers.pop() + const resolver = this.resolvers.pop(); if (resolver) { // yes, resolve it - resolver({ value: msg }) + resolver({ value: msg }); } else { // no, push the message into the queue - this.messages.unshift(msg) + this.messages.unshift(msg); } } catch (e) { - console.warn(e) + console.warn(e); } - } + }; } private async start(): Promise { if (!this.callback) { - throw new Error('Missing callback for stream') + throw new Error("Missing callback for stream"); } this.subscriptionManager = this.client.apiClient.subscribe( @@ -90,11 +93,11 @@ export default class Stream { contentTopics: this.topics, }, async (env: messageApi.Envelope) => { - if (!this.callback) return - await this?.callback(env) + if (!this.callback) return; + await this?.callback(env); }, - this.onConnectionLost - ) + this.onConnectionLost, + ); } static async create( @@ -102,22 +105,22 @@ export default class Stream { topics: string[], decoder: MessageDecoder, contentTopicUpdater?: ContentTopicUpdater, - onConnectionLost?: OnConnectionLostCallback + onConnectionLost?: OnConnectionLostCallback, ): Promise> { const stream = new Stream( client, topics, decoder, contentTopicUpdater, - onConnectionLost - ) - await stream.start() - return stream + onConnectionLost, + ); + await stream.start(); + return stream; } // To make Stream proper Async Iterable [Symbol.asyncIterator](): AsyncIterableIterator { - return this + return this; } // return should be called if the interpreter detects that the stream won't be used anymore, @@ -126,16 +129,16 @@ export default class Stream { // Note that this means the Stream will be closed after it was used in a for-await-of or yield* or similar. async return(): Promise> { if (this.subscriptionManager) { - await this.subscriptionManager.unsubscribe() + await this.subscriptionManager.unsubscribe(); } if (!this.callback) { - return { value: undefined, done: true } + return { value: undefined, done: true }; } - this.callback = undefined + this.callback = undefined; this.resolvers.forEach((resolve) => - resolve({ value: undefined, done: true }) - ) - return { value: undefined, done: true } + resolve({ value: undefined, done: true }), + ); + return { value: undefined, done: true }; } // To make Stream proper Async Iterator @@ -143,39 +146,39 @@ export default class Stream { // even after the stream was closed via return(). next(): Promise> { // Is there a message already pending? - const msg = this.messages.pop() + const msg = this.messages.pop(); if (msg) { // yes, return resolved promise - return Promise.resolve({ value: msg }) + return Promise.resolve({ value: msg }); } if (!this.callback) { - return Promise.resolve({ value: undefined, done: true }) + return Promise.resolve({ value: undefined, done: true }); } // otherwise return empty Promise and queue its resolver - return new Promise((resolve) => this.resolvers.unshift(resolve)) + return new Promise((resolve) => this.resolvers.unshift(resolve)); } // Unsubscribe from the existing content topics and resubscribe to the given topics. private async resubscribeToTopics(topics: string[]): Promise { if (!this.callback || !this.subscriptionManager) { - throw new Error('Missing callback for stream') + throw new Error("Missing callback for stream"); } - if (typeof this.subscriptionManager?.updateContentTopics === 'function') { - return this.subscriptionManager.updateContentTopics(topics) + if (typeof this.subscriptionManager?.updateContentTopics === "function") { + return this.subscriptionManager.updateContentTopics(topics); } - await this.subscriptionManager.unsubscribe() - this.topics = topics + await this.subscriptionManager.unsubscribe(); + this.topics = topics; this.subscriptionManager = this.client.apiClient.subscribe( { contentTopics: this.topics, }, async (env: messageApi.Envelope) => { - if (!this.callback) return - await this?.callback(env) + if (!this.callback) return; + await this?.callback(env); }, - this.onConnectionLost - ) + this.onConnectionLost, + ); } } diff --git a/packages/js-sdk/src/authn/AuthCache.ts b/packages/js-sdk/src/authn/AuthCache.ts index 397efdc9c..abafd098c 100644 --- a/packages/js-sdk/src/authn/AuthCache.ts +++ b/packages/js-sdk/src/authn/AuthCache.ts @@ -1,32 +1,32 @@ -import type { Authenticator } from './interfaces' -import type Token from './Token' +import type { Authenticator } from "./interfaces"; +import type Token from "./Token"; // Default to 10 seconds less than expected expiry to give some wiggle room near the end // https://github.com/xmtp/xmtp-node-go/blob/main/pkg/api/authentication.go#L18 -const DEFAULT_MAX_AGE_SECONDS = 60 * 60 - 10 +const DEFAULT_MAX_AGE_SECONDS = 60 * 60 - 10; export default class AuthCache { - private authenticator: Authenticator - private token?: Token - maxAgeMs: number + private authenticator: Authenticator; + private token?: Token; + maxAgeMs: number; constructor( authenticator: Authenticator, - cacheExpirySeconds = DEFAULT_MAX_AGE_SECONDS + cacheExpirySeconds = DEFAULT_MAX_AGE_SECONDS, ) { - this.authenticator = authenticator - this.maxAgeMs = cacheExpirySeconds * 1000 + this.authenticator = authenticator; + this.maxAgeMs = cacheExpirySeconds * 1000; } async getToken(): Promise { if (!this.token || this.token.ageMs > this.maxAgeMs) { - await this.refresh() + await this.refresh(); } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this.token!.toBase64() + return this.token!.toBase64(); } async refresh(): Promise { - this.token = await this.authenticator.createToken() + this.token = await this.authenticator.createToken(); } } diff --git a/packages/js-sdk/src/authn/AuthData.ts b/packages/js-sdk/src/authn/AuthData.ts index 492d1af37..02554decb 100644 --- a/packages/js-sdk/src/authn/AuthData.ts +++ b/packages/js-sdk/src/authn/AuthData.ts @@ -1,30 +1,30 @@ -import { authn as authnProto } from '@xmtp/proto' -import type Long from 'long' -import { dateToNs } from '@/utils/date' +import { authn as authnProto } from "@xmtp/proto"; +import type Long from "long"; +import { dateToNs } from "@/utils/date"; export default class AuthData implements authnProto.AuthData { - walletAddr: string - createdNs: Long + walletAddr: string; + createdNs: Long; public constructor({ walletAddr, createdNs }: authnProto.AuthData) { - this.walletAddr = walletAddr - this.createdNs = createdNs + this.walletAddr = walletAddr; + this.createdNs = createdNs; } static create(walletAddr: string, timestamp?: Date): AuthData { - timestamp = timestamp || new Date() + timestamp = timestamp || new Date(); return new AuthData({ walletAddr, createdNs: dateToNs(timestamp), - }) + }); } static fromBytes(bytes: Uint8Array): AuthData { - const res = authnProto.AuthData.decode(bytes) - return new AuthData(res) + const res = authnProto.AuthData.decode(bytes); + return new AuthData(res); } toBytes(): Uint8Array { - return authnProto.AuthData.encode(this).finish() + return authnProto.AuthData.encode(this).finish(); } } diff --git a/packages/js-sdk/src/authn/KeystoreAuthenticator.ts b/packages/js-sdk/src/authn/KeystoreAuthenticator.ts index c8c62bed1..4d40df9df 100644 --- a/packages/js-sdk/src/authn/KeystoreAuthenticator.ts +++ b/packages/js-sdk/src/authn/KeystoreAuthenticator.ts @@ -1,32 +1,32 @@ -import type { authn } from '@xmtp/proto' +import type { authn } from "@xmtp/proto"; import type { KeystoreInterface, KeystoreInterfaces, -} from '@/keystore/rpcDefinitions' -import { dateToNs } from '@/utils/date' -import Token from './Token' +} from "@/keystore/rpcDefinitions"; +import { dateToNs } from "@/utils/date"; +import Token from "./Token"; const wrapToken = (token: authn.Token): Token => { if (token instanceof Token) { - return token + return token; } - return new Token(token) -} + return new Token(token); +}; export default class KeystoreAuthenticator< T extends KeystoreInterfaces = KeystoreInterface, > { - private keystore: T + private keystore: T; constructor(keystore: T) { - this.keystore = keystore + this.keystore = keystore; } async createToken(timestamp?: Date): Promise { const token = await this.keystore.createAuthToken({ timestampNs: timestamp ? dateToNs(timestamp) : undefined, - }) + }); - return wrapToken(token) + return wrapToken(token); } } diff --git a/packages/js-sdk/src/authn/LocalAuthenticator.ts b/packages/js-sdk/src/authn/LocalAuthenticator.ts index 6072834a7..d79603437 100644 --- a/packages/js-sdk/src/authn/LocalAuthenticator.ts +++ b/packages/js-sdk/src/authn/LocalAuthenticator.ts @@ -1,28 +1,28 @@ -import { authn, publicKey, signature } from '@xmtp/proto' -import { hexToBytes, keccak256 } from 'viem' -import type { PrivateKey } from '@/crypto/PrivateKey' -import AuthData from './AuthData' -import Token from './Token' +import { authn, publicKey, signature } from "@xmtp/proto"; +import { hexToBytes, keccak256 } from "viem"; +import type { PrivateKey } from "@/crypto/PrivateKey"; +import AuthData from "./AuthData"; +import Token from "./Token"; export default class LocalAuthenticator { - private identityKey: PrivateKey + private identityKey: PrivateKey; constructor(identityKey: PrivateKey) { if (!identityKey.publicKey.signature) { - throw new Error('Provided public key is not signed') + throw new Error("Provided public key is not signed"); } - this.identityKey = identityKey + this.identityKey = identityKey; } async createToken(timestamp?: Date): Promise { const authData = AuthData.create( this.identityKey.publicKey.walletSignatureAddress(), - timestamp || new Date() - ) - const authDataBytes = authData.toBytes() - const digest = keccak256(authDataBytes) - const authSig = await this.identityKey.sign(hexToBytes(digest)) + timestamp || new Date(), + ); + const authDataBytes = authData.toBytes(); + const digest = keccak256(authDataBytes); + const authSig = await this.identityKey.sign(hexToBytes(digest)); return new Token( authn.Token.fromPartial({ @@ -30,14 +30,14 @@ export default class LocalAuthenticator { // The generated types are overly strict and don't like our additional methods // eslint-disable-next-line // @ts-ignore - this.identityKey.publicKey + this.identityKey.publicKey, ), authDataBytes, // The generated types are overly strict and don't like our additional methods // eslint-disable-next-line // @ts-ignore authDataSignature: signature.Signature.fromPartial(authSig), - }) - ) + }), + ); } } diff --git a/packages/js-sdk/src/authn/Token.ts b/packages/js-sdk/src/authn/Token.ts index a133e25e3..23843bfa0 100644 --- a/packages/js-sdk/src/authn/Token.ts +++ b/packages/js-sdk/src/authn/Token.ts @@ -1,49 +1,49 @@ -import { authn, type publicKey, type signature } from '@xmtp/proto' -import AuthData from './AuthData' +import { authn, type publicKey, type signature } from "@xmtp/proto"; +import AuthData from "./AuthData"; export default class Token implements authn.Token { - identityKey: publicKey.PublicKey - authDataBytes: Uint8Array - authDataSignature: signature.Signature - private _authData?: AuthData + identityKey: publicKey.PublicKey; + authDataBytes: Uint8Array; + authDataSignature: signature.Signature; + private _authData?: AuthData; constructor({ identityKey, authDataBytes, authDataSignature }: authn.Token) { if (!identityKey) { - throw new Error('Missing identity key in token') + throw new Error("Missing identity key in token"); } if (!authDataSignature) { - throw new Error('Missing authDataSignature in token') + throw new Error("Missing authDataSignature in token"); } - this.identityKey = identityKey - this.authDataBytes = authDataBytes - this.authDataSignature = authDataSignature + this.identityKey = identityKey; + this.authDataBytes = authDataBytes; + this.authDataSignature = authDataSignature; } // Get AuthData, generating from bytes and cacheing the first time it is accessed get authData(): AuthData { if (!this._authData) { - this._authData = AuthData.fromBytes(this.authDataBytes) + this._authData = AuthData.fromBytes(this.authDataBytes); } - return this._authData + return this._authData; } get ageMs(): number { - const now = new Date().valueOf() - const authData = this.authData - const createdAt = authData.createdNs.div(1_000_000).toNumber() - return now - createdAt + const now = new Date().valueOf(); + const authData = this.authData; + const createdAt = authData.createdNs.div(1_000_000).toNumber(); + return now - createdAt; } toBytes(): Uint8Array { - return authn.Token.encode(this).finish() + return authn.Token.encode(this).finish(); } static fromBytes(bytes: Uint8Array): Token { - return new Token(authn.Token.decode(bytes)) + return new Token(authn.Token.decode(bytes)); } toBase64(): string { - return Buffer.from(this.toBytes()).toString('base64') + return Buffer.from(this.toBytes()).toString("base64"); } } diff --git a/packages/js-sdk/src/authn/interfaces.ts b/packages/js-sdk/src/authn/interfaces.ts index 53ee2f0d1..e6b8c6bea 100644 --- a/packages/js-sdk/src/authn/interfaces.ts +++ b/packages/js-sdk/src/authn/interfaces.ts @@ -1,5 +1,5 @@ -import type Token from './Token' +import type Token from "./Token"; export interface Authenticator { - createToken(timestamp?: Date): Promise + createToken(timestamp?: Date): Promise; } diff --git a/packages/js-sdk/src/constants.ts b/packages/js-sdk/src/constants.ts index 3b1e7b03c..f9f2c3a5e 100644 --- a/packages/js-sdk/src/constants.ts +++ b/packages/js-sdk/src/constants.ts @@ -7,4 +7,4 @@ XX XX MM MM TT PP DDDDDD EEEEEEE VVV Connected to the XMTP 'dev' network. Use 'production' for production messages. https://github.com/xmtp/xmtp-js#xmtp-production-and-dev-network-environments -` +`; diff --git a/packages/js-sdk/src/conversations/Conversation.ts b/packages/js-sdk/src/conversations/Conversation.ts index a6bc7655a..b51cbd9f7 100644 --- a/packages/js-sdk/src/conversations/Conversation.ts +++ b/packages/js-sdk/src/conversations/Conversation.ts @@ -1,95 +1,95 @@ -import { ContentTypeText } from '@xmtp/content-type-text' +import { ContentTypeText } from "@xmtp/content-type-text"; import { message, content as proto, type invitation, type keystore, type messageApi, -} from '@xmtp/proto' -import { getAddress } from 'viem' -import type { OnConnectionLostCallback } from '@/ApiClient' +} from "@xmtp/proto"; +import { getAddress } from "viem"; +import type { OnConnectionLostCallback } from "@/ApiClient"; import type { ListMessagesOptions, ListMessagesPaginatedOptions, SendOptions, -} from '@/Client' -import type Client from '@/Client' -import type { ConsentState } from '@/Contacts' -import { sha256 } from '@/crypto/encryption' -import { SignedPublicKey } from '@/crypto/PublicKey' +} from "@/Client"; +import type Client from "@/Client"; +import type { ConsentState } from "@/Contacts"; +import { sha256 } from "@/crypto/encryption"; +import { SignedPublicKey } from "@/crypto/PublicKey"; import { PublicKeyBundle, SignedPublicKeyBundle, -} from '@/crypto/PublicKeyBundle' -import Signature from '@/crypto/Signature' -import type { InvitationContext } from '@/Invitation' -import { DecodedMessage, MessageV1, MessageV2 } from '@/Message' -import { PreparedMessage } from '@/PreparedMessage' -import Stream from '@/Stream' -import { concat } from '@/utils/bytes' -import { dateToNs, toNanoString } from '@/utils/date' -import { buildDecryptV1Request, getResultOrThrow } from '@/utils/keystore' -import { buildDirectMessageTopic, buildUserIntroTopic } from '@/utils/topic' +} from "@/crypto/PublicKeyBundle"; +import Signature from "@/crypto/Signature"; +import type { InvitationContext } from "@/Invitation"; +import { DecodedMessage, MessageV1, MessageV2 } from "@/Message"; +import { PreparedMessage } from "@/PreparedMessage"; +import Stream from "@/Stream"; +import { concat } from "@/utils/bytes"; +import { dateToNs, toNanoString } from "@/utils/date"; +import { buildDecryptV1Request, getResultOrThrow } from "@/utils/keystore"; +import { buildDirectMessageTopic, buildUserIntroTopic } from "@/utils/topic"; /** * Conversation represents either a V1 or V2 conversation with a common set of methods. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface Conversation { - conversationVersion: 'v1' | 'v2' + conversationVersion: "v1" | "v2"; /** * The wallet address connected to the client */ - clientAddress: string + clientAddress: string; /** * A unique identifier for a conversation. Each conversation is stored on the network on one topic */ - topic: string + topic: string; /** * A unique identifier for ephemeral envelopes for a conversation. */ - ephemeralTopic: string + ephemeralTopic: string; /** * The wallet address of the other party in the conversation */ - peerAddress: string + peerAddress: string; /** * Timestamp the conversation was created at */ - createdAt: Date + createdAt: Date; /** * Optional field containing the `conversationId` and `metadata` for V2 conversations. * Will always be undefined on V1 conversations */ - context?: InvitationContext | undefined + context?: InvitationContext | undefined; /** * Add conversation peer address to allow list */ - allow(): Promise + allow(): Promise; /** * Add conversation peer address to deny list */ - deny(): Promise + deny(): Promise; /** * Returns true if conversation peer address is on the allow list */ - isAllowed: boolean + isAllowed: boolean; /** * Returns true if conversation peer address is on the deny list */ - isDenied: boolean + isDenied: boolean; /** * Returns the consent state of the conversation peer address */ - consentState: ConsentState + consentState: ConsentState; /** * Proof of consent for the conversation, used when a user has pre-consented to a conversation */ - consentProof?: invitation.ConsentProofPayload + consentProof?: invitation.ConsentProofPayload; /** * Retrieve messages in this conversation. Default to returning all messages. @@ -104,18 +104,20 @@ export interface Conversation { * }) * ``` */ - messages(opts?: ListMessagesOptions): Promise[]> + messages(opts?: ListMessagesOptions): Promise[]>; /** * @deprecated */ messagesPaginated( - opts?: ListMessagesPaginatedOptions - ): AsyncGenerator[]> + opts?: ListMessagesPaginatedOptions, + ): AsyncGenerator[]>; /** * Takes a XMTP envelope as input and will decrypt and decode it * returning a `DecodedMessage` instance. */ - decodeMessage(env: messageApi.Envelope): Promise> + decodeMessage( + env: messageApi.Envelope, + ): Promise>; /** * Return a `Stream` of new messages in this conversation. * @@ -128,7 +130,7 @@ export interface Conversation { * } * ``` */ - streamMessages(): Promise, ContentTypes>> + streamMessages(): Promise, ContentTypes>>; /** * Send a message into the conversation * @@ -139,8 +141,8 @@ export interface Conversation { */ send( content: Exclude, - options?: SendOptions - ): Promise> + options?: SendOptions, + ): Promise>; /** * Return a `PreparedMessage` that has contains the message ID @@ -148,8 +150,8 @@ export interface Conversation { */ prepareMessage( content: any, // eslint-disable-line @typescript-eslint/no-explicit-any - options?: SendOptions - ): Promise + options?: SendOptions, + ): Promise; /** * Return a `Stream` of new ephemeral messages from this conversation's @@ -164,7 +166,9 @@ export interface Conversation { * } * ``` */ - streamEphemeral(): Promise, ContentTypes>> + streamEphemeral(): Promise< + Stream, ContentTypes> + >; } /** @@ -173,134 +177,141 @@ export interface Conversation { export class ConversationV1 implements Conversation { - conversationVersion = 'v1' as const - peerAddress: string - createdAt: Date - context = undefined - private client: Client + conversationVersion = "v1" as const; + peerAddress: string; + createdAt: Date; + context = undefined; + private client: Client; constructor(client: Client, address: string, createdAt: Date) { - this.peerAddress = getAddress(address) - this.client = client - this.createdAt = createdAt + this.peerAddress = getAddress(address); + this.client = client; + this.createdAt = createdAt; } get clientAddress() { - return this.client.address + return this.client.address; } async allow() { - await this.client.contacts.allow([this.peerAddress]) + await this.client.contacts.allow([this.peerAddress]); } async deny() { - await this.client.contacts.deny([this.peerAddress]) + await this.client.contacts.deny([this.peerAddress]); } get isAllowed() { - return this.client.contacts.isAllowed(this.peerAddress) + return this.client.contacts.isAllowed(this.peerAddress); } get isDenied() { - return this.client.contacts.isDenied(this.peerAddress) + return this.client.contacts.isDenied(this.peerAddress); } get consentState() { - return this.client.contacts.consentState(this.peerAddress) + return this.client.contacts.consentState(this.peerAddress); } get topic(): string { - return buildDirectMessageTopic(this.peerAddress, this.client.address) + return buildDirectMessageTopic(this.peerAddress, this.client.address); } get ephemeralTopic(): string { return buildDirectMessageTopic( this.peerAddress, - this.client.address - ).replace('/xmtp/0/dm-', '/xmtp/0/dmE-') + this.client.address, + ).replace("/xmtp/0/dm-", "/xmtp/0/dmE-"); } /** * Returns a list of all messages to/from the peerAddress */ async messages( - opts?: ListMessagesOptions + opts?: ListMessagesOptions, ): Promise[]> { - const topic = buildDirectMessageTopic(this.peerAddress, this.client.address) + const topic = buildDirectMessageTopic( + this.peerAddress, + this.client.address, + ); const messages = await this.client.listEnvelopes( topic, this.processEnvelope.bind(this), - opts - ) + opts, + ); - return this.decryptBatch(messages, topic, false) + return this.decryptBatch(messages, topic, false); } messagesPaginated( - opts?: ListMessagesPaginatedOptions + opts?: ListMessagesPaginatedOptions, ): AsyncGenerator[]> { return this.client.listEnvelopesPaginated( this.topic, // This won't be performant once we start supporting a remote keystore // TODO: Either better batch support or we ditch this under-utilized feature this.decodeMessage.bind(this), - opts - ) + opts, + ); } // decodeMessage takes an envelope and either returns a `DecodedMessage` or throws if an error occurs async decodeMessage( - env: messageApi.Envelope + env: messageApi.Envelope, ): Promise> { if (!env.contentTopic) { - throw new Error('Missing content topic') + throw new Error("Missing content topic"); } - const msg = await this.processEnvelope(env) + const msg = await this.processEnvelope(env); const decryptResults = await this.decryptBatch( [msg], env.contentTopic, - true - ) + true, + ); if (!decryptResults.length) { - throw new Error('No results') + throw new Error("No results"); } - return decryptResults[0] + return decryptResults[0]; } async prepareMessage( content: any, // eslint-disable-line @typescript-eslint/no-explicit-any - options?: SendOptions + options?: SendOptions, ): Promise { - let topics: string[] - let recipient = await this.client.getUserContact(this.peerAddress) + let topics: string[]; + let recipient = await this.client.getUserContact(this.peerAddress); if (!recipient) { - throw new Error(`recipient ${this.peerAddress} is not registered`) + throw new Error(`recipient ${this.peerAddress} is not registered`); } if (!(recipient instanceof PublicKeyBundle)) { - recipient = recipient.toLegacyBundle() + recipient = recipient.toLegacyBundle(); } - const topic = options?.ephemeral ? this.ephemeralTopic : this.topic + const topic = options?.ephemeral ? this.ephemeralTopic : this.topic; if (!this.client.contacts.addresses.has(this.peerAddress)) { topics = [ buildUserIntroTopic(this.peerAddress), buildUserIntroTopic(this.client.address), topic, - ] - this.client.contacts.addresses.add(this.peerAddress) + ]; + this.client.contacts.addresses.add(this.peerAddress); } else { - topics = [topic] + topics = [topic]; } - const { payload } = await this.client.encodeContent(content, options) - const msg = await this.createMessage(payload, recipient, options?.timestamp) - const msgBytes = msg.toBytes() + const { payload } = await this.client.encodeContent(content, options); + const msg = await this.createMessage( + payload, + recipient, + options?.timestamp, + ); + const msgBytes = msg.toBytes(); const env: messageApi.Envelope = { contentTopic: topic, message: msgBytes, timestampNs: toNanoString(msg.sent), - } + }; return new PreparedMessage(env, async () => { await this.client.publishEnvelopes( @@ -308,8 +319,8 @@ export class ConversationV1 contentTopic: topic, message: msgBytes, timestamp: msg.sent, - })) - ) + })), + ); return DecodedMessage.fromV1Message( msg, @@ -317,24 +328,24 @@ export class ConversationV1 options?.contentType || ContentTypeText, payload, topic, - this - ) - }) + this, + ); + }); } /** * Returns a Stream of any new messages to/from the peerAddress */ streamMessages( - onConnectionLost?: OnConnectionLostCallback + onConnectionLost?: OnConnectionLostCallback, ): Promise, ContentTypes>> { return Stream.create, ContentTypes>( this.client, [this.topic], async (env: messageApi.Envelope) => this.decodeMessage(env), undefined, - onConnectionLost - ) + onConnectionLost, + ); } async processEnvelope({ @@ -342,10 +353,10 @@ export class ConversationV1 contentTopic, }: messageApi.Envelope): Promise { if (!message || !message.length) { - throw new Error('empty envelope') + throw new Error("empty envelope"); } - const decoded = await MessageV1.fromBytes(message) - const { senderAddress, recipientAddress } = decoded + const decoded = await MessageV1.fromBytes(message); + const { senderAddress, recipientAddress } = decoded; // Filter for topics if ( @@ -354,22 +365,22 @@ export class ConversationV1 !contentTopic || buildDirectMessageTopic(senderAddress, recipientAddress) !== this.topic ) { - throw new Error('Headers do not match intended recipient') + throw new Error("Headers do not match intended recipient"); } - return decoded + return decoded; } streamEphemeral( - onConnectionLost?: OnConnectionLostCallback + onConnectionLost?: OnConnectionLostCallback, ): Promise, ContentTypes>> { return Stream.create, ContentTypes>( this.client, [this.ephemeralTopic], this.decodeMessage.bind(this), undefined, - onConnectionLost - ) + onConnectionLost, + ); } /** @@ -377,46 +388,50 @@ export class ConversationV1 */ async send( content: Exclude, - options?: SendOptions + options?: SendOptions, ): Promise> { - let topics: string[] - let recipient = await this.client.getUserContact(this.peerAddress) + let topics: string[]; + let recipient = await this.client.getUserContact(this.peerAddress); if (!recipient) { - throw new Error(`recipient ${this.peerAddress} is not registered`) + throw new Error(`recipient ${this.peerAddress} is not registered`); } if (!(recipient instanceof PublicKeyBundle)) { - recipient = recipient.toLegacyBundle() + recipient = recipient.toLegacyBundle(); } - const topic = options?.ephemeral ? this.ephemeralTopic : this.topic + const topic = options?.ephemeral ? this.ephemeralTopic : this.topic; if (!this.client.contacts.addresses.has(this.peerAddress)) { topics = [ buildUserIntroTopic(this.peerAddress), buildUserIntroTopic(this.client.address), topic, - ] - this.client.contacts.addresses.add(this.peerAddress) + ]; + this.client.contacts.addresses.add(this.peerAddress); } else { - topics = [topic] + topics = [topic]; } - const contentType = options?.contentType || ContentTypeText - const { payload } = await this.client.encodeContent(content, options) - const msg = await this.createMessage(payload, recipient, options?.timestamp) + const contentType = options?.contentType || ContentTypeText; + const { payload } = await this.client.encodeContent(content, options); + const msg = await this.createMessage( + payload, + recipient, + options?.timestamp, + ); await this.client.publishEnvelopes( topics.map((topic) => ({ contentTopic: topic, message: msg.toBytes(), timestamp: msg.sent, - })) - ) + })), + ); // if the conversation consent state is unknown, we assume the user has // consented to the conversation by sending a message into it - if (this.consentState === 'unknown') { + if (this.consentState === "unknown") { // add conversation to the allow list - await this.allow() + await this.allow(); } return DecodedMessage.fromV1Message( @@ -425,46 +440,46 @@ export class ConversationV1 contentType, payload, topic, - this - ) + this, + ); } async decryptBatch( messages: MessageV1[], topic: string, - throwOnError = false + throwOnError = false, ): Promise[]> { const responses = ( await this.client.keystore.decryptV1( - buildDecryptV1Request(messages, this.client.publicKeyBundle) + buildDecryptV1Request(messages, this.client.publicKeyBundle), ) - ).responses + ).responses; - const out: DecodedMessage[] = [] + const out: DecodedMessage[] = []; for (let i = 0; i < responses.length; i++) { - const result = responses[i] - const message = messages[i] + const result = responses[i]; + const message = messages[i]; try { - const { decrypted } = getResultOrThrow(result) - out.push(await this.buildDecodedMessage(message, decrypted, topic)) + const { decrypted } = getResultOrThrow(result); + out.push(await this.buildDecodedMessage(message, decrypted, topic)); } catch (e) { if (throwOnError) { - throw e + throw e; } - console.warn('Error decoding content', e) + console.warn("Error decoding content", e); } } - return out + return out; } private async buildDecodedMessage( message: MessageV1, decrypted: Uint8Array, - topic: string + topic: string, ): Promise> { const { content, contentType, error, contentFallback } = - await this.client.decodeContent(decrypted) + await this.client.decodeContent(decrypted); return DecodedMessage.fromV1Message( message, @@ -474,25 +489,25 @@ export class ConversationV1 topic, this, error, - contentFallback - ) + contentFallback, + ); } async createMessage( // Payload is expected to be the output of `client.encodeContent` payload: Uint8Array, recipient: PublicKeyBundle, - timestamp?: Date + timestamp?: Date, ): Promise { - timestamp = timestamp || new Date() + timestamp = timestamp || new Date(); return MessageV1.encode( this.client.keystore, payload, this.client.publicKeyBundle, recipient, - timestamp - ) + timestamp, + ); } } @@ -502,13 +517,13 @@ export class ConversationV1 export class ConversationV2 implements Conversation { - conversationVersion = 'v2' as const - client: Client - topic: string - peerAddress: string - createdAt: Date - context?: InvitationContext - consentProof?: invitation.ConsentProofPayload + conversationVersion = "v2" as const; + client: Client; + topic: string; + peerAddress: string; + createdAt: Date; + context?: InvitationContext; + consentProof?: invitation.ConsentProofPayload; constructor( client: Client, @@ -516,98 +531,98 @@ export class ConversationV2 peerAddress: string, createdAt: Date, context: InvitationContext | undefined, - consentProof: invitation.ConsentProofPayload | undefined + consentProof: invitation.ConsentProofPayload | undefined, ) { - this.topic = topic - this.createdAt = createdAt - this.context = context - this.client = client - this.peerAddress = peerAddress - this.consentProof = consentProof + this.topic = topic; + this.createdAt = createdAt; + this.context = context; + this.client = client; + this.peerAddress = peerAddress; + this.consentProof = consentProof; } get clientAddress() { - return this.client.address + return this.client.address; } async allow() { - await this.client.contacts.allow([this.peerAddress]) + await this.client.contacts.allow([this.peerAddress]); } async deny() { - await this.client.contacts.deny([this.peerAddress]) + await this.client.contacts.deny([this.peerAddress]); } get isAllowed() { - return this.client.contacts.isAllowed(this.peerAddress) + return this.client.contacts.isAllowed(this.peerAddress); } get isDenied() { - return this.client.contacts.isDenied(this.peerAddress) + return this.client.contacts.isDenied(this.peerAddress); } get consentState() { - return this.client.contacts.consentState(this.peerAddress) + return this.client.contacts.consentState(this.peerAddress); } get consentProofPayload(): invitation.ConsentProofPayload | undefined { - return this.consentProof + return this.consentProof; } /** * Returns a list of all messages to/from the peerAddress */ async messages( - opts?: ListMessagesOptions + opts?: ListMessagesOptions, ): Promise[]> { const messages = await this.client.listEnvelopes( this.topic, this.processEnvelope.bind(this), - opts - ) + opts, + ); - return this.decryptBatch(messages, false) + return this.decryptBatch(messages, false); } messagesPaginated( - opts?: ListMessagesPaginatedOptions + opts?: ListMessagesPaginatedOptions, ): AsyncGenerator[]> { return this.client.listEnvelopesPaginated( this.topic, this.decodeMessage.bind(this), - opts - ) + opts, + ); } get ephemeralTopic(): string { - return this.topic.replace('/xmtp/0/m', '/xmtp/0/mE') + return this.topic.replace("/xmtp/0/m", "/xmtp/0/mE"); } streamEphemeral( - onConnectionLost?: OnConnectionLostCallback + onConnectionLost?: OnConnectionLostCallback, ): Promise, ContentTypes>> { return Stream.create, ContentTypes>( this.client, [this.ephemeralTopic], this.decodeMessage.bind(this), undefined, - onConnectionLost - ) + onConnectionLost, + ); } /** * Returns a Stream of any new messages to/from the peerAddress */ streamMessages( - onConnectionLost?: OnConnectionLostCallback + onConnectionLost?: OnConnectionLostCallback, ): Promise, ContentTypes>> { return Stream.create, ContentTypes>( this.client, [this.topic], this.decodeMessage.bind(this), undefined, - onConnectionLost - ) + onConnectionLost, + ); } /** @@ -615,19 +630,19 @@ export class ConversationV2 */ async send( content: Exclude, - options?: SendOptions + options?: SendOptions, ): Promise> { const { payload, shouldPush } = await this.client.encodeContent( content, - options - ) + options, + ); const msg = await this.createMessage( payload, shouldPush, - options?.timestamp - ) + options?.timestamp, + ); - const topic = options?.ephemeral ? this.ephemeralTopic : this.topic + const topic = options?.ephemeral ? this.ephemeralTopic : this.topic; await this.client.publishEnvelopes([ { @@ -635,14 +650,14 @@ export class ConversationV2 message: msg.toBytes(), timestamp: msg.sent, }, - ]) - const contentType = options?.contentType || ContentTypeText + ]); + const contentType = options?.contentType || ContentTypeText; // if the conversation consent state is unknown, we assume the user has // consented to the conversation by sending a message into it - if (this.consentState === 'unknown') { + if (this.consentState === "unknown") { // add conversation to the allow list - await this.allow() + await this.allow(); } return DecodedMessage.fromV2Message( @@ -652,22 +667,22 @@ export class ConversationV2 topic, payload, this, - this.client.address - ) + this.client.address, + ); } async createMessage( // Payload is expected to have already gone through `client.encodeContent` payload: Uint8Array, shouldPush: boolean, - timestamp?: Date + timestamp?: Date, ): Promise { const header: message.MessageHeaderV2 = { topic: this.topic, createdNs: dateToNs(timestamp || new Date()), - } - const headerBytes = message.MessageHeaderV2.encode(header).finish() - const digest = await sha256(concat(headerBytes, payload)) + }; + const headerBytes = message.MessageHeaderV2.encode(header).finish(); + const digest = await sha256(concat(headerBytes, payload)); const signed = { payload, sender: this.client.signedPublicKeyBundle, @@ -676,52 +691,52 @@ export class ConversationV2 prekeyIndex: 0, identityKey: undefined, }), - } - const signedBytes = proto.SignedContent.encode(signed).finish() + }; + const signedBytes = proto.SignedContent.encode(signed).finish(); const { encrypted: ciphertext, senderHmac } = await this.encryptMessage( signedBytes, - headerBytes - ) + headerBytes, + ); const protoMsg = { v1: undefined, v2: { headerBytes, ciphertext, senderHmac, shouldPush }, - } - const bytes = message.Message.encode(protoMsg).finish() + }; + const bytes = message.Message.encode(protoMsg).finish(); - return MessageV2.create(protoMsg, header, bytes, senderHmac, shouldPush) + return MessageV2.create(protoMsg, header, bytes, senderHmac, shouldPush); } private async decryptBatch( messages: MessageV2[], - throwOnError = false + throwOnError = false, ): Promise[]> { const responses = ( await this.client.keystore.decryptV2(this.buildDecryptRequest(messages)) - ).responses + ).responses; - const out: DecodedMessage[] = [] + const out: DecodedMessage[] = []; for (let i = 0; i < responses.length; i++) { - const result = responses[i] - const message = messages[i] + const result = responses[i]; + const message = messages[i]; try { - const { decrypted } = getResultOrThrow(result) - out.push(await this.buildDecodedMessage(message, decrypted)) + const { decrypted } = getResultOrThrow(result); + out.push(await this.buildDecodedMessage(message, decrypted)); } catch (e) { if (throwOnError) { - throw e + throw e; } - console.warn('Error decoding content', e) + console.warn("Error decoding content", e); } } - return out + return out; } private buildDecryptRequest( - messages: message.MessageV2[] + messages: message.MessageV2[], ): keystore.DecryptV2Request { return { requests: messages.map((m) => { @@ -729,9 +744,9 @@ export class ConversationV2 payload: m.ciphertext, headerBytes: m.headerBytes, contentTopic: this.topic, - } + }; }), - } + }; } private async encryptMessage(payload: Uint8Array, headerBytes: Uint8Array) { @@ -743,48 +758,48 @@ export class ConversationV2 contentTopic: this.topic, }, ], - }) + }); if (responses.length !== 1) { - throw new Error('Invalid response length') + throw new Error("Invalid response length"); } - const { encrypted, senderHmac } = getResultOrThrow(responses[0]) - return { encrypted, senderHmac } + const { encrypted, senderHmac } = getResultOrThrow(responses[0]); + return { encrypted, senderHmac }; } private async buildDecodedMessage( msg: MessageV2, - decrypted: Uint8Array + decrypted: Uint8Array, ): Promise> { // Decode the decrypted bytes into SignedContent - const signed = proto.SignedContent.decode(decrypted) + const signed = proto.SignedContent.decode(decrypted); if ( !signed.sender?.identityKey || !signed.sender?.preKey || !signed.signature ) { - throw new Error('incomplete signed content') + throw new Error("incomplete signed content"); } - await validatePrekeys(signed) + await validatePrekeys(signed); // Verify the signature - const digest = await sha256(concat(msg.headerBytes, signed.payload)) + const digest = await sha256(concat(msg.headerBytes, signed.payload)); if ( !new SignedPublicKey(signed.sender?.preKey).verify( new Signature(signed.signature), - digest + digest, ) ) { - throw new Error('invalid signature') + throw new Error("invalid signature"); } // Derive the sender address from the valid signature const senderAddress = await new SignedPublicKeyBundle( - signed.sender - ).walletSignatureAddress() + signed.sender, + ).walletSignatureAddress(); const { content, contentType, error, contentFallback } = - await this.client.decodeContent(signed.payload) + await this.client.decodeContent(signed.payload); return DecodedMessage.fromV2Message( msg, @@ -795,32 +810,32 @@ export class ConversationV2 this, senderAddress, error, - contentFallback - ) + contentFallback, + ); } async prepareMessage( content: any, // eslint-disable-line @typescript-eslint/no-explicit-any - options?: SendOptions + options?: SendOptions, ): Promise { const { payload, shouldPush } = await this.client.encodeContent( content, - options - ) + options, + ); const msg = await this.createMessage( payload, shouldPush, - options?.timestamp - ) - const msgBytes = msg.toBytes() + options?.timestamp, + ); + const msgBytes = msg.toBytes(); - const topic = options?.ephemeral ? this.ephemeralTopic : this.topic + const topic = options?.ephemeral ? this.ephemeralTopic : this.topic; const env: messageApi.Envelope = { contentTopic: topic, message: msgBytes, timestampNs: toNanoString(msg.sent), - } + }; return new PreparedMessage(env, async () => { await this.client.publishEnvelopes([ @@ -829,7 +844,7 @@ export class ConversationV2 message: msgBytes, timestamp: msg.sent, }, - ]) + ]); return DecodedMessage.fromV2Message( msg, @@ -838,24 +853,24 @@ export class ConversationV2 topic, payload, this, - this.client.address - ) - }) + this.client.address, + ); + }); } async processEnvelope(env: messageApi.Envelope): Promise { if (!env.message || !env.contentTopic) { - throw new Error('empty envelope') + throw new Error("empty envelope"); } - const msg = message.Message.decode(env.message) + const msg = message.Message.decode(env.message); if (!msg.v2) { - throw new Error('unknown message version') + throw new Error("unknown message version"); } - const header = message.MessageHeaderV2.decode(msg.v2.headerBytes) + const header = message.MessageHeaderV2.decode(msg.v2.headerBytes); if (header.topic !== this.topic) { - throw new Error('topic mismatch') + throw new Error("topic mismatch"); } return MessageV2.create( @@ -863,22 +878,22 @@ export class ConversationV2 header, env.message, msg.v2.senderHmac, - msg.v2.shouldPush - ) + msg.v2.shouldPush, + ); } async decodeMessage( - env: messageApi.Envelope + env: messageApi.Envelope, ): Promise> { if (!env.contentTopic) { - throw new Error('Missing content topic') + throw new Error("Missing content topic"); } - const msg = await this.processEnvelope(env) - const decryptResults = await this.decryptBatch([msg], true) + const msg = await this.processEnvelope(env); + const decryptResults = await this.decryptBatch([msg], true); if (!decryptResults.length) { - throw new Error('No results') + throw new Error("No results"); } - return decryptResults[0] + return decryptResults[0]; } } @@ -886,18 +901,18 @@ async function validatePrekeys(signed: proto.SignedContent) { // Check that the pre key is signed by the identity key // this is required to chain the prekey-signed message to the identity key // and finally to the user's wallet address - const senderPreKey = signed.sender?.preKey + const senderPreKey = signed.sender?.preKey; if (!senderPreKey || !senderPreKey.signature || !senderPreKey.keyBytes) { - throw new Error('missing pre-key or pre-key signature') + throw new Error("missing pre-key or pre-key signature"); } - const senderIdentityKey = signed.sender?.identityKey + const senderIdentityKey = signed.sender?.identityKey; if (!senderIdentityKey) { - throw new Error('missing identity key in bundle') + throw new Error("missing identity key in bundle"); } const isValidPrekey = await new SignedPublicKey(senderIdentityKey).verifyKey( - new SignedPublicKey(senderPreKey) - ) + new SignedPublicKey(senderPreKey), + ); if (!isValidPrekey) { - throw new Error('pre key not signed by identity key') + throw new Error("pre key not signed by identity key"); } } diff --git a/packages/js-sdk/src/conversations/Conversations.ts b/packages/js-sdk/src/conversations/Conversations.ts index ddc3aae7f..2440b467e 100644 --- a/packages/js-sdk/src/conversations/Conversations.ts +++ b/packages/js-sdk/src/conversations/Conversations.ts @@ -3,48 +3,48 @@ import type { invitation, keystore, messageApi, -} from '@xmtp/proto' -import Long from 'long' -import { SortDirection, type OnConnectionLostCallback } from '@/ApiClient' -import type { ListMessagesOptions } from '@/Client' -import type Client from '@/Client' +} from "@xmtp/proto"; +import Long from "long"; +import { SortDirection, type OnConnectionLostCallback } from "@/ApiClient"; +import type { ListMessagesOptions } from "@/Client"; +import type Client from "@/Client"; import { PublicKeyBundle, SignedPublicKeyBundle, -} from '@/crypto/PublicKeyBundle' -import type { InvitationContext } from '@/Invitation' -import { DecodedMessage, MessageV1 } from '@/Message' -import Stream from '@/Stream' -import { dateToNs, nsToDate } from '@/utils/date' +} from "@/crypto/PublicKeyBundle"; +import type { InvitationContext } from "@/Invitation"; +import { DecodedMessage, MessageV1 } from "@/Message"; +import Stream from "@/Stream"; +import { dateToNs, nsToDate } from "@/utils/date"; import { buildDirectMessageTopic, buildUserIntroTopic, buildUserInviteTopic, isValidTopic, -} from '@/utils/topic' +} from "@/utils/topic"; import { ConversationV1, ConversationV2, type Conversation, -} from './Conversation' -import JobRunner from './JobRunner' +} from "./Conversation"; +import JobRunner from "./JobRunner"; const messageHasHeaders = (msg: MessageV1): boolean => { - return Boolean(msg.recipientAddress && msg.senderAddress) -} + return Boolean(msg.recipientAddress && msg.senderAddress); +}; /** * Conversations allows you to view ongoing 1:1 messaging sessions with another wallet */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export default class Conversations { - private client: Client - private v1JobRunner: JobRunner - private v2JobRunner: JobRunner + private client: Client; + private v1JobRunner: JobRunner; + private v2JobRunner: JobRunner; constructor(client: Client) { - this.client = client - this.v1JobRunner = new JobRunner('v1', client.keystore) - this.v2JobRunner = new JobRunner('v2', client.keystore) + this.client = client; + this.v1JobRunner = new JobRunner("v1", client.keystore); + this.v2JobRunner = new JobRunner("v2", client.keystore); } /** @@ -54,12 +54,12 @@ export default class Conversations { const [v1Convos, v2Convos] = await Promise.all([ this.listV1Conversations(), this.listV2Conversations(), - ]) + ]); - const conversations = v1Convos.concat(v2Convos) + const conversations = v1Convos.concat(v2Convos); - conversations.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()) - return conversations + conversations.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); + return conversations; } /** @@ -71,11 +71,11 @@ export default class Conversations { await Promise.all([ this.getV1ConversationsFromKeystore(), this.getV2ConversationsFromKeystore(), - ]) - const conversations = v1Convos.concat(v2Convos) + ]); + const conversations = v1Convos.concat(v2Convos); - conversations.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()) - return conversations + conversations.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); + return conversations; } private async listV1Conversations(): Promise[]> { @@ -83,7 +83,7 @@ export default class Conversations { const seenPeers = await this.getIntroductionPeers({ startTime: latestSeen, direction: SortDirection.SORT_DIRECTION_ASCENDING, - }) + }); await this.client.keystore.saveV1Conversations({ conversations: Array.from(seenPeers) @@ -95,12 +95,12 @@ export default class Conversations { consentProofPayload: undefined, })) .filter((c) => isValidTopic(c.topic)), - }) + }); return ( await this.client.keystore.getV1Conversations() - ).conversations.map(this.conversationReferenceToV1.bind(this)) - }) + ).conversations.map(this.conversationReferenceToV1.bind(this)); + }); } /** @@ -109,56 +109,56 @@ export default class Conversations { private async listV2Conversations(): Promise[]> { return this.v2JobRunner.run(async (lastRun) => { // Get all conversations already in the KeyStore - const existing = await this.getV2ConversationsFromKeystore() + const existing = await this.getV2ConversationsFromKeystore(); // Load all conversations started after the newest conversation found - const newConversations = await this.updateV2Conversations(lastRun) + const newConversations = await this.updateV2Conversations(lastRun); // Create a Set of all the existing topics to ensure no duplicates are added - const existingTopics = new Set(existing.map((c) => c.topic)) + const existingTopics = new Set(existing.map((c) => c.topic)); // Add all new conversations to the existing list for (const convo of newConversations) { if (!existingTopics.has(convo.topic)) { - existing.push(convo) - existingTopics.add(convo.topic) + existing.push(convo); + existingTopics.add(convo.topic); } } // Sort the result set by creation time in ascending order - existing.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()) - return existing - }) + existing.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); + return existing; + }); } private async getV2ConversationsFromKeystore(): Promise< ConversationV2[] > { return (await this.client.keystore.getV2Conversations()).conversations.map( - this.conversationReferenceToV2.bind(this) - ) + this.conversationReferenceToV2.bind(this), + ); } private async getV1ConversationsFromKeystore(): Promise< ConversationV1[] > { return (await this.client.keystore.getV1Conversations()).conversations.map( - this.conversationReferenceToV1.bind(this) - ) + this.conversationReferenceToV1.bind(this), + ); } // Called in listV2Conversations and in newConversation async updateV2Conversations( - startTime?: Date + startTime?: Date, ): Promise[]> { const envelopes = await this.client.listInvitations({ startTime, direction: SortDirection.SORT_DIRECTION_ASCENDING, - }) - return this.decodeInvites(envelopes) + }); + return this.decodeInvites(envelopes); } private async decodeInvites( envelopes: messageApi.Envelope[], - shouldThrow = false + shouldThrow = false, ): Promise[]> { const { responses } = await this.client.keystore.saveInvites({ requests: envelopes @@ -168,20 +168,20 @@ export default class Conversations { contentTopic: env.contentTopic as string, })) .filter((req) => isValidTopic(req.contentTopic)), - }) + }); - const out: ConversationV2[] = [] + const out: ConversationV2[] = []; for (const response of responses) { try { - out.push(this.saveInviteResponseToConversation(response)) + out.push(this.saveInviteResponseToConversation(response)); } catch (e) { - console.warn('Error saving invite response to conversation: ', e) + console.warn("Error saving invite response to conversation: ", e); if (shouldThrow) { - throw e + throw e; } } } - return out + return out; } private saveInviteResponseToConversation({ @@ -189,13 +189,13 @@ export default class Conversations { error, }: keystore.SaveInvitesResponse_Response): ConversationV2 { if (error || !result || !result.conversation) { - throw new Error(`Error from keystore: ${error?.code} ${error?.message}}`) + throw new Error(`Error from keystore: ${error?.code} ${error?.message}}`); } - return this.conversationReferenceToV2(result.conversation) + return this.conversationReferenceToV2(result.conversation); } private conversationReferenceToV2( - convoRef: conversationReference.ConversationReference + convoRef: conversationReference.ConversationReference, ): ConversationV2 { return new ConversationV2( this.client, @@ -203,18 +203,18 @@ export default class Conversations { convoRef.peerAddress, nsToDate(convoRef.createdNs), convoRef.context, - convoRef.consentProofPayload - ) + convoRef.consentProofPayload, + ); } private conversationReferenceToV1( - convoRef: conversationReference.ConversationReference + convoRef: conversationReference.ConversationReference, ): ConversationV1 { return new ConversationV1( this.client, convoRef.peerAddress, - nsToDate(convoRef.createdNs) - ) + nsToDate(convoRef.createdNs), + ); } /** @@ -223,53 +223,53 @@ export default class Conversations { * Does not dedupe any other previously seen conversations */ async stream( - onConnectionLost?: OnConnectionLostCallback + onConnectionLost?: OnConnectionLostCallback, ): Promise, ContentTypes>> { - const seenPeers: Set = new Set() - const introTopic = buildUserIntroTopic(this.client.address) - const inviteTopic = buildUserInviteTopic(this.client.address) + const seenPeers: Set = new Set(); + const introTopic = buildUserIntroTopic(this.client.address); + const inviteTopic = buildUserInviteTopic(this.client.address); const newPeer = (peerAddress: string): boolean => { // Check if we have seen the peer already in this stream if (seenPeers.has(peerAddress)) { - return false + return false; } - seenPeers.add(peerAddress) - return true - } + seenPeers.add(peerAddress); + return true; + }; const decodeConversation = async (env: messageApi.Envelope) => { if (env.contentTopic === introTopic) { if (!env.message) { - throw new Error('empty envelope') + throw new Error("empty envelope"); } - const msg = await MessageV1.fromBytes(env.message) - const peerAddress = this.getPeerAddress(msg) + const msg = await MessageV1.fromBytes(env.message); + const peerAddress = this.getPeerAddress(msg); if (!newPeer(peerAddress)) { - return undefined + return undefined; } - await msg.decrypt(this.client.keystore, this.client.publicKeyBundle) - return new ConversationV1(this.client, peerAddress, msg.sent) + await msg.decrypt(this.client.keystore, this.client.publicKeyBundle); + return new ConversationV1(this.client, peerAddress, msg.sent); } if (env.contentTopic === inviteTopic) { - const results = await this.decodeInvites([env], true) + const results = await this.decodeInvites([env], true); if (results.length) { - return results[0] + return results[0]; } } - throw new Error('unrecognized invite topic') - } + throw new Error("unrecognized invite topic"); + }; - const topics = [introTopic, inviteTopic] + const topics = [introTopic, inviteTopic]; return Stream.create, ContentTypes>( this.client, topics, decodeConversation.bind(this), undefined, - onConnectionLost - ) + onConnectionLost, + ); } /** @@ -280,87 +280,87 @@ export default class Conversations { * */ async streamAllMessages( - onConnectionLost?: OnConnectionLostCallback + onConnectionLost?: OnConnectionLostCallback, ): Promise>> { - const introTopic = buildUserIntroTopic(this.client.address) - const inviteTopic = buildUserInviteTopic(this.client.address) + const introTopic = buildUserIntroTopic(this.client.address); + const inviteTopic = buildUserInviteTopic(this.client.address); - const topics = new Set([introTopic, inviteTopic]) + const topics = new Set([introTopic, inviteTopic]); - const convoMap = new Map>() + const convoMap = new Map>(); for (const conversation of await this.list()) { - topics.add(conversation.topic) - convoMap.set(conversation.topic, conversation) + topics.add(conversation.topic); + convoMap.set(conversation.topic, conversation); } const decodeMessage = async ( - env: messageApi.Envelope + env: messageApi.Envelope, ): Promise< Conversation | DecodedMessage | null > => { - const contentTopic = env.contentTopic + const contentTopic = env.contentTopic; if (!contentTopic || !env.message) { - return null + return null; } if (contentTopic === introTopic) { - const msg = await MessageV1.fromBytes(env.message) + const msg = await MessageV1.fromBytes(env.message); if (!messageHasHeaders(msg)) { - return null + return null; } - const peerAddress = this.getPeerAddress(msg) + const peerAddress = this.getPeerAddress(msg); // Temporarily create a convo to decrypt the message const convo = new ConversationV1( this.client, peerAddress as string, - msg.sent - ) + msg.sent, + ); // TODO: This duplicates the proto deserialization unnecessarily // Refactor to avoid duplicate work - return convo.decodeMessage(env) + return convo.decodeMessage(env); } // Decode as an invite and return the envelope // This gives the contentTopicUpdater everything it needs to add to the topic list if (contentTopic === inviteTopic) { - const results = await this.decodeInvites([env], true) - return results[0] + const results = await this.decodeInvites([env], true); + return results[0]; } - const convo = convoMap.get(contentTopic) + const convo = convoMap.get(contentTopic); // Decode as a V1 message if the topic matches a V1 convo if (convo instanceof ConversationV1) { - return convo.decodeMessage(env) + return convo.decodeMessage(env); } // Decode as a V2 message if the topic matches a V2 convo if (convo instanceof ConversationV2) { - return convo.decodeMessage(env) + return convo.decodeMessage(env); } - console.log('Unknown topic') + console.log("Unknown topic"); - throw new Error('Unknown topic') - } + throw new Error("Unknown topic"); + }; const addConvo = ( topic: string, - conversation: Conversation + conversation: Conversation, ): boolean => { if (topics.has(topic)) { - return false + return false; } - convoMap.set(topic, conversation) - topics.add(topic) - return true - } + convoMap.set(topic, conversation); + topics.add(topic); + return true; + }; const contentTopicUpdater = ( - msg: Conversation | DecodedMessage | null + msg: Conversation | DecodedMessage | null, ) => { // If we have a V1 message from the introTopic, store the conversation in our mapping if (msg instanceof DecodedMessage && msg.contentTopic === introTopic) { @@ -370,21 +370,21 @@ export default class Conversations { this.client.address.toLowerCase() ? (msg.senderAddress as string) : (msg.recipientAddress as string), - msg.sent - ) - const isNew = addConvo(convo.topic, convo) + msg.sent, + ); + const isNew = addConvo(convo.topic, convo); - return isNew ? Array.from(topics.values()) : undefined + return isNew ? Array.from(topics.values()) : undefined; } if (msg instanceof ConversationV2) { - const isNew = addConvo(msg.topic, msg) + const isNew = addConvo(msg.topic, msg); - return isNew ? Array.from(topics.values()) : undefined + return isNew ? Array.from(topics.values()) : undefined; } - return undefined - } + return undefined; + }; const str = await Stream.create< DecodedMessage | Conversation | null, @@ -394,23 +394,23 @@ export default class Conversations { Array.from(topics.values()), decodeMessage, contentTopicUpdater, - onConnectionLost - ) + onConnectionLost, + ); const gen = (async function* generate() { for await (const val of str) { if (val instanceof DecodedMessage) { - yield val + yield val; } // For conversation V2, we may have messages in the new topic before we started streaming. // To be safe, we fetch all messages if (val instanceof ConversationV2) { for (const convoMessage of await val.messages()) { - yield convoMessage + yield convoMessage; } } } - })() + })(); // Overwrite the generator's return method to close the underlying stream // Generators by default need to wait until the next yield to return. @@ -418,55 +418,55 @@ export default class Conversations { gen.return = async () => { // Returning the stream will cause the iteration to end inside the generator // The generator will then return on its own - await str?.return() - return { value: undefined, done: true } - } + await str?.return(); + return { value: undefined, done: true }; + }; - return gen + return gen; } private async getIntroductionPeers( - opts?: ListMessagesOptions + opts?: ListMessagesOptions, ): Promise> { - const topic = buildUserIntroTopic(this.client.address) + const topic = buildUserIntroTopic(this.client.address); const messages = await this.client.listEnvelopes( topic, (env) => { if (!env.message) { - throw new Error('empty envelope') + throw new Error("empty envelope"); } - return MessageV1.fromBytes(env.message) + return MessageV1.fromBytes(env.message); }, - opts - ) - const seenPeers: Map = new Map() + opts, + ); + const seenPeers: Map = new Map(); for (const message of messages) { // Ignore all messages without sender or recipient address headers // Makes getPeerAddress safe if (!messageHasHeaders(message)) { - continue + continue; } - const peerAddress = this.getPeerAddress(message) + const peerAddress = this.getPeerAddress(message); if (peerAddress) { - const have = seenPeers.get(peerAddress) + const have = seenPeers.get(peerAddress); if (!have || have > message.sent) { try { // Verify that the message can be decrypted before treating the intro as valid await message.decrypt( this.client.keystore, - this.client.publicKeyBundle - ) - seenPeers.set(peerAddress, message.sent) + this.client.publicKeyBundle, + ); + seenPeers.set(peerAddress, message.sent); } catch (e) { - continue + continue; } } } } - return seenPeers + return seenPeers; } /** @@ -475,96 +475,97 @@ export default class Conversations { async newConversation( peerAddress: string, context?: InvitationContext, - consentProof?: invitation.ConsentProofPayload + consentProof?: invitation.ConsentProofPayload, ): Promise> { // Define a function for matching V2 conversations const matcherFn = (convo: Conversation) => convo.peerAddress.toLowerCase() === peerAddress.toLowerCase() && - isMatchingContext(context, convo.context ?? undefined) + isMatchingContext(context, convo.context ?? undefined); // Check if we already have a V2 conversation with the peer in keystore - const existing = await this.getV2ConversationsFromKeystore() - const existingMatch = existing.find(matcherFn) + const existing = await this.getV2ConversationsFromKeystore(); + const existingMatch = existing.find(matcherFn); if (existingMatch) { - return existingMatch + return existingMatch; } - let contact = await this.client.getUserContact(peerAddress) + let contact = await this.client.getUserContact(peerAddress); if (!contact) { - throw new Error(`Recipient ${peerAddress} is not on the XMTP network`) + throw new Error(`Recipient ${peerAddress} is not on the XMTP network`); } if (peerAddress.toLowerCase() === this.client.address.toLowerCase()) { - throw new Error('self messaging not supported') + throw new Error("self messaging not supported"); } // If this is a V1 conversation continuation if (contact instanceof PublicKeyBundle && !context?.conversationId) { - return new ConversationV1(this.client, peerAddress, new Date()) + return new ConversationV1(this.client, peerAddress, new Date()); } // If no conversationId, check and see if we have an existing V1 conversation if (!context?.conversationId) { - const v1Convos = await this.listV1Conversations() + const v1Convos = await this.listV1Conversations(); const matchingConvo = v1Convos.find( - (convo) => convo.peerAddress.toLowerCase() === peerAddress.toLowerCase() - ) + (convo) => + convo.peerAddress.toLowerCase() === peerAddress.toLowerCase(), + ); // If intro already exists, return V1 conversation // if both peers have V1 compatible key bundles if (matchingConvo) { if (!this.client.signedPublicKeyBundle.isFromLegacyBundle()) { throw new Error( - 'cannot resume pre-existing V1 conversation; client keys not compatible' - ) + "cannot resume pre-existing V1 conversation; client keys not compatible", + ); } if ( !(contact instanceof PublicKeyBundle) && !contact.isFromLegacyBundle() ) { throw new Error( - 'cannot resume pre-existing V1 conversation; peer keys not compatible' - ) + "cannot resume pre-existing V1 conversation; peer keys not compatible", + ); } - return matchingConvo + return matchingConvo; } } // Coerce the contact into a V2 bundle if (contact instanceof PublicKeyBundle) { - contact = SignedPublicKeyBundle.fromLegacyBundle(contact) + contact = SignedPublicKeyBundle.fromLegacyBundle(contact); } return this.v2JobRunner.run(async (lastRun) => { - const newItems = await this.updateV2Conversations(lastRun) - const newItemMatch = newItems.find(matcherFn) + const newItems = await this.updateV2Conversations(lastRun); + const newItemMatch = newItems.find(matcherFn); // If one of those matches, return it to update the cache if (newItemMatch) { - return newItemMatch + return newItemMatch; } return this.createV2Convo( contact as SignedPublicKeyBundle, context, - consentProof - ) - }) + consentProof, + ); + }); } private async createV2Convo( recipient: SignedPublicKeyBundle, context?: InvitationContext, - consentProof?: invitation.ConsentProofPayload + consentProof?: invitation.ConsentProofPayload, ): Promise> { - const timestamp = new Date() + const timestamp = new Date(); const { payload, conversation } = await this.client.keystore.createInvite({ recipient, context, createdNs: dateToNs(timestamp), consentProof, - }) + }); if (!payload || !conversation) { - throw new Error('Required field not returned from Keystore') + throw new Error("Required field not returned from Keystore"); } - const peerAddress = await recipient.walletSignatureAddress() + const peerAddress = await recipient.walletSignatureAddress(); await this.client.publishEnvelopes([ { @@ -577,12 +578,12 @@ export default class Conversations { message: payload, timestamp, }, - ]) + ]); // add peer address to allow list - await this.client.contacts.allow([peerAddress]) + await this.client.contacts.allow([peerAddress]); - return this.conversationReferenceToV2(conversation) + return this.conversationReferenceToV2(conversation); } private getPeerAddress(message: MessageV1): string { @@ -590,17 +591,17 @@ export default class Conversations { message.recipientAddress?.toLowerCase() === this.client.address.toLowerCase() ? message.senderAddress - : message.recipientAddress + : message.recipientAddress; // This assertion is safe, so long as messages have been through the filter - return peerAddress as string + return peerAddress as string; } } function isMatchingContext( contextA?: InvitationContext, - contextB?: InvitationContext + contextB?: InvitationContext, ): boolean { // Use == to allow null and undefined to be equivalent - return contextA?.conversationId === contextB?.conversationId + return contextA?.conversationId === contextB?.conversationId; } diff --git a/packages/js-sdk/src/conversations/JobRunner.ts b/packages/js-sdk/src/conversations/JobRunner.ts index af5abe1af..4739c7267 100644 --- a/packages/js-sdk/src/conversations/JobRunner.ts +++ b/packages/js-sdk/src/conversations/JobRunner.ts @@ -1,71 +1,71 @@ -import { keystore } from '@xmtp/proto' -import { Mutex } from 'async-mutex' -import Long from 'long' -import type { KeystoreInterfaces } from '@/keystore/rpcDefinitions' -import { dateToNs, nsToDate } from '@/utils/date' +import { keystore } from "@xmtp/proto"; +import { Mutex } from "async-mutex"; +import Long from "long"; +import type { KeystoreInterfaces } from "@/keystore/rpcDefinitions"; +import { dateToNs, nsToDate } from "@/utils/date"; -const CLOCK_SKEW_OFFSET_MS = 10000 +const CLOCK_SKEW_OFFSET_MS = 10000; -type JobType = 'v1' | 'v2' | 'user-preferences' +type JobType = "v1" | "v2" | "user-preferences"; -type UpdateJob = (lastRun: Date | undefined) => Promise +type UpdateJob = (lastRun: Date | undefined) => Promise; export default class JobRunner { - readonly jobType: JobType - readonly mutex: Mutex - readonly keystore: KeystoreInterfaces - disableOffset: boolean = false + readonly jobType: JobType; + readonly mutex: Mutex; + readonly keystore: KeystoreInterfaces; + disableOffset: boolean = false; constructor(jobType: JobType, keystore: KeystoreInterfaces) { - this.jobType = jobType - this.mutex = new Mutex() - this.keystore = keystore + this.jobType = jobType; + this.mutex = new Mutex(); + this.keystore = keystore; } get protoJobType(): keystore.JobType { - return getProtoJobType(this.jobType) + return getProtoJobType(this.jobType); } async run(callback: UpdateJob): Promise { return this.mutex.runExclusive(async () => { - const lastRun = await this.getLastRunTime() - const startTime = new Date() + const lastRun = await this.getLastRunTime(); + const startTime = new Date(); const result = await callback( lastRun ? !this.disableOffset ? new Date(lastRun.getTime() - CLOCK_SKEW_OFFSET_MS) : lastRun - : undefined - ) - await this.setLastRunTime(startTime) - return result - }) + : undefined, + ); + await this.setLastRunTime(startTime); + return result; + }); } async resetLastRunTime() { await this.keystore.setRefreshJob({ jobType: this.protoJobType, lastRunNs: dateToNs(new Date(0)), - }) + }); } private async getLastRunTime(): Promise { const { lastRunNs } = await this.keystore.getRefreshJob( keystore.GetRefreshJobRequest.fromPartial({ jobType: this.protoJobType, - }) - ) + }), + ); if (lastRunNs.equals(Long.fromNumber(0))) { - return undefined + return undefined; } - return nsToDate(lastRunNs) + return nsToDate(lastRunNs); } private async setLastRunTime(lastRun: Date): Promise { await this.keystore.setRefreshJob({ jobType: this.protoJobType, lastRunNs: dateToNs(lastRun), - }) + }); } } @@ -73,12 +73,12 @@ function getProtoJobType(jobType: JobType): keystore.JobType { const protoJobType = { v1: keystore.JobType.JOB_TYPE_REFRESH_V1, v2: keystore.JobType.JOB_TYPE_REFRESH_V2, - 'user-preferences': keystore.JobType.JOB_TYPE_REFRESH_PPPP, - }[jobType] + "user-preferences": keystore.JobType.JOB_TYPE_REFRESH_PPPP, + }[jobType]; if (!protoJobType) { - throw new Error(`unknown job type: ${jobType}`) + throw new Error(`unknown job type: ${jobType}`); } - return protoJobType + return protoJobType; } diff --git a/packages/js-sdk/src/crypto/Ciphertext.ts b/packages/js-sdk/src/crypto/Ciphertext.ts index 941ebbc14..293961c8a 100644 --- a/packages/js-sdk/src/crypto/Ciphertext.ts +++ b/packages/js-sdk/src/crypto/Ciphertext.ts @@ -1,43 +1,43 @@ -import { ciphertext } from '@xmtp/proto' +import { ciphertext } from "@xmtp/proto"; -export const AESKeySize = 32 // bytes -export const KDFSaltSize = 32 // bytes +export const AESKeySize = 32; // bytes +export const KDFSaltSize = 32; // bytes // AES-GCM defaults from https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams -export const AESGCMNonceSize = 12 // property iv -export const AESGCMTagLength = 16 // property tagLength +export const AESGCMNonceSize = 12; // property iv +export const AESGCMTagLength = 16; // property tagLength // Ciphertext packages the encrypted ciphertext with the salt and nonce used to produce it. // salt and nonce are not secret, and should be transmitted/stored along with the encrypted ciphertext. export default class Ciphertext implements ciphertext.Ciphertext { - aes256GcmHkdfSha256: ciphertext.Ciphertext_Aes256gcmHkdfsha256 | undefined // eslint-disable-line camelcase + aes256GcmHkdfSha256: ciphertext.Ciphertext_Aes256gcmHkdfsha256 | undefined; // eslint-disable-line camelcase constructor(obj: ciphertext.Ciphertext) { if (!obj.aes256GcmHkdfSha256) { - throw new Error('invalid ciphertext') + throw new Error("invalid ciphertext"); } if (obj.aes256GcmHkdfSha256.payload.length < AESGCMTagLength) { throw new Error( - `invalid ciphertext ciphertext length: ${obj.aes256GcmHkdfSha256.payload.length}` - ) + `invalid ciphertext ciphertext length: ${obj.aes256GcmHkdfSha256.payload.length}`, + ); } if (obj.aes256GcmHkdfSha256.hkdfSalt.length !== KDFSaltSize) { throw new Error( - `invalid ciphertext salt length: ${obj.aes256GcmHkdfSha256.hkdfSalt.length}` - ) + `invalid ciphertext salt length: ${obj.aes256GcmHkdfSha256.hkdfSalt.length}`, + ); } if (obj.aes256GcmHkdfSha256.gcmNonce.length !== AESGCMNonceSize) { throw new Error( - `invalid ciphertext nonce length: ${obj.aes256GcmHkdfSha256.gcmNonce.length}` - ) + `invalid ciphertext nonce length: ${obj.aes256GcmHkdfSha256.gcmNonce.length}`, + ); } - this.aes256GcmHkdfSha256 = obj.aes256GcmHkdfSha256 + this.aes256GcmHkdfSha256 = obj.aes256GcmHkdfSha256; } toBytes(): Uint8Array { - return ciphertext.Ciphertext.encode(this).finish() + return ciphertext.Ciphertext.encode(this).finish(); } static fromBytes(bytes: Uint8Array): Ciphertext { - return new Ciphertext(ciphertext.Ciphertext.decode(bytes)) + return new Ciphertext(ciphertext.Ciphertext.decode(bytes)); } } diff --git a/packages/js-sdk/src/crypto/PrivateKey.ts b/packages/js-sdk/src/crypto/PrivateKey.ts index 0c6ba5ed1..1e9a6f5c5 100644 --- a/packages/js-sdk/src/crypto/PrivateKey.ts +++ b/packages/js-sdk/src/crypto/PrivateKey.ts @@ -1,25 +1,25 @@ -import * as secp from '@noble/secp256k1' -import { privateKey } from '@xmtp/proto' -import Long from 'long' -import type Ciphertext from './Ciphertext' -import { decrypt, encrypt, sha256 } from './encryption' -import { PublicKey, SignedPublicKey, UnsignedPublicKey } from './PublicKey' +import * as secp from "@noble/secp256k1"; +import { privateKey } from "@xmtp/proto"; +import Long from "long"; +import type Ciphertext from "./Ciphertext"; +import { decrypt, encrypt, sha256 } from "./encryption"; +import { PublicKey, SignedPublicKey, UnsignedPublicKey } from "./PublicKey"; import Signature, { ecdsaSignerKey, type ECDSACompactWithRecovery, type KeySigner, -} from './Signature' -import { equalBytes } from './utils' +} from "./Signature"; +import { equalBytes } from "./utils"; // SECP256k1 private key type secp256k1 = { - bytes: Uint8Array // D big-endian, 32 bytes -} + bytes: Uint8Array; // D big-endian, 32 bytes +}; // Validate SECP256k1 private key function secp256k1Check(key: secp256k1): void { if (key.bytes.length !== 32) { - throw new Error(`invalid private key length: ${key.bytes.length}`) + throw new Error(`invalid private key length: ${key.bytes.length}`); } } @@ -27,46 +27,46 @@ function secp256k1Check(key: secp256k1): void { export class SignedPrivateKey implements privateKey.SignedPrivateKey, KeySigner { - createdNs: Long // time the key was generated, ns since epoch - secp256k1: secp256k1 // eslint-disable-line camelcase - publicKey: SignedPublicKey // caches corresponding PublicKey + createdNs: Long; // time the key was generated, ns since epoch + secp256k1: secp256k1; // eslint-disable-line camelcase + publicKey: SignedPublicKey; // caches corresponding PublicKey constructor(obj: privateKey.SignedPrivateKey) { if (!obj.secp256k1) { - throw new Error('invalid private key') + throw new Error("invalid private key"); } - secp256k1Check(obj.secp256k1) - this.secp256k1 = obj.secp256k1 - this.createdNs = obj.createdNs + secp256k1Check(obj.secp256k1); + this.secp256k1 = obj.secp256k1; + this.createdNs = obj.createdNs; if (!obj.publicKey) { - throw new Error('missing public key') + throw new Error("missing public key"); } - this.publicKey = new SignedPublicKey(obj.publicKey) + this.publicKey = new SignedPublicKey(obj.publicKey); } // Create a random key pair signed by the signer. static async generate(signer: KeySigner): Promise { const secp256k1 = { bytes: secp.utils.randomPrivateKey(), - } - const createdNs = Long.fromNumber(new Date().getTime()).mul(1000000) + }; + const createdNs = Long.fromNumber(new Date().getTime()).mul(1000000); const unsigned = new UnsignedPublicKey({ secp256k1Uncompressed: { bytes: secp.getPublicKey(secp256k1.bytes), }, createdNs, - }) - const signed = await signer.signKey(unsigned) + }); + const signed = await signer.signKey(unsigned); return new SignedPrivateKey({ secp256k1, createdNs, publicKey: signed, - }) + }); } // Time the key was generated. generated(): Date | undefined { - return new Date(this.createdNs.div(1000000).toNumber()) + return new Date(this.createdNs.div(1000000).toNumber()); } // Sign provided digest. @@ -77,31 +77,31 @@ export class SignedPrivateKey { recovered: true, der: false, - } - ) + }, + ); return new Signature({ ecdsaCompact: { bytes: signature, recovery }, - }) + }); } // Sign provided public key. async signKey(pub: UnsignedPublicKey): Promise { - const keyBytes = pub.toBytes() - const digest = await sha256(keyBytes) - const signature = await this.sign(digest) + const keyBytes = pub.toBytes(); + const digest = await sha256(keyBytes); + const signature = await this.sign(digest); return new SignedPublicKey({ keyBytes, signature, - }) + }); } // Return public key of the signer of the provided signed key. static async signerKey( key: SignedPublicKey, - signature: ECDSACompactWithRecovery + signature: ECDSACompactWithRecovery, ): Promise { - const digest = await sha256(key.bytesToSign()) - return ecdsaSignerKey(digest, signature) + const digest = await sha256(key.bytesToSign()); + return ecdsaSignerKey(digest, signature); } // Derive shared secret from peer's PublicKey; @@ -110,8 +110,8 @@ export class SignedPrivateKey return secp.getSharedSecret( this.secp256k1.bytes, peer.secp256k1Uncompressed.bytes, - false - ) + false, + ); } // encrypt plain bytes using a shared secret derived from peer's PublicKey; @@ -120,10 +120,10 @@ export class SignedPrivateKey encrypt( plain: Uint8Array, peer: UnsignedPublicKey, - additionalData?: Uint8Array + additionalData?: Uint8Array, ): Promise { - const secret = this.sharedSecret(peer) - return encrypt(plain, secret, additionalData) + const secret = this.sharedSecret(peer); + return encrypt(plain, secret, additionalData); } // decrypt Ciphertext using a shared secret derived from peer's PublicKey; @@ -131,15 +131,15 @@ export class SignedPrivateKey decrypt( encrypted: Ciphertext, peer: UnsignedPublicKey, - additionalData?: Uint8Array + additionalData?: Uint8Array, ): Promise { - const secret = this.sharedSecret(peer) - return decrypt(encrypted, secret, additionalData) + const secret = this.sharedSecret(peer); + return decrypt(encrypted, secret, additionalData); } // Does the provided PublicKey correspond to this PrivateKey? matches(key: SignedPublicKey): boolean { - return this.publicKey.equals(key) + return this.publicKey.equals(key); } // Is other the same/equivalent key? @@ -147,64 +147,64 @@ export class SignedPrivateKey return ( equalBytes(this.secp256k1.bytes, other.secp256k1.bytes) && this.publicKey.equals(other.publicKey) - ) + ); } // Encode this key into bytes. toBytes(): Uint8Array { - return privateKey.SignedPrivateKey.encode(this).finish() + return privateKey.SignedPrivateKey.encode(this).finish(); } validatePublicKey(): boolean { - const generatedPublicKey = secp.getPublicKey(this.secp256k1.bytes) + const generatedPublicKey = secp.getPublicKey(this.secp256k1.bytes); return equalBytes( generatedPublicKey, - this.publicKey.secp256k1Uncompressed.bytes - ) + this.publicKey.secp256k1Uncompressed.bytes, + ); } // Decode key from bytes. static fromBytes(bytes: Uint8Array): SignedPrivateKey { - return new SignedPrivateKey(privateKey.SignedPrivateKey.decode(bytes)) + return new SignedPrivateKey(privateKey.SignedPrivateKey.decode(bytes)); } static fromLegacyKey( key: PrivateKey, - signedByWallet?: boolean + signedByWallet?: boolean, ): SignedPrivateKey { return new SignedPrivateKey({ createdNs: key.timestamp.mul(1000000), secp256k1: key.secp256k1, publicKey: SignedPublicKey.fromLegacyKey(key.publicKey, signedByWallet), - }) + }); } } // LEGACY: PrivateKey represents a secp256k1 private key. export class PrivateKey implements privateKey.PrivateKey { - timestamp: Long - secp256k1: secp256k1 // eslint-disable-line camelcase - publicKey: PublicKey // caches corresponding PublicKey + timestamp: Long; + secp256k1: secp256k1; // eslint-disable-line camelcase + publicKey: PublicKey; // caches corresponding PublicKey constructor(obj: privateKey.PrivateKey) { if (!obj.secp256k1) { - throw new Error('invalid private key') + throw new Error("invalid private key"); } - secp256k1Check(obj.secp256k1) - this.timestamp = obj.timestamp - this.secp256k1 = obj.secp256k1 + secp256k1Check(obj.secp256k1); + this.timestamp = obj.timestamp; + this.secp256k1 = obj.secp256k1; if (!obj.publicKey) { - throw new Error('missing public key') + throw new Error("missing public key"); } - this.publicKey = new PublicKey(obj.publicKey) + this.publicKey = new PublicKey(obj.publicKey); } // create a random PrivateKey/PublicKey pair. static generate(): PrivateKey { const secp256k1 = { bytes: secp.utils.randomPrivateKey(), - } - const timestamp = Long.fromNumber(new Date().getTime()) + }; + const timestamp = Long.fromNumber(new Date().getTime()); return new PrivateKey({ secp256k1, timestamp, @@ -214,11 +214,11 @@ export class PrivateKey implements privateKey.PrivateKey { }, timestamp, }), - }) + }); } generated(): Date | undefined { - return new Date(this.timestamp.toNumber()) + return new Date(this.timestamp.toNumber()); } // sign provided digest @@ -229,18 +229,18 @@ export class PrivateKey implements privateKey.PrivateKey { { recovered: true, der: false, - } - ) + }, + ); return new Signature({ ecdsaCompact: { bytes: signature, recovery }, - }) + }); } // sign provided public key async signKey(pub: PublicKey): Promise { - const digest = await sha256(pub.bytesToSign()) - pub.signature = await this.sign(digest) - return pub + const digest = await sha256(pub.bytesToSign()); + pub.signature = await this.sign(digest); + return pub; } // derive shared secret from peer's PublicKey; @@ -249,8 +249,8 @@ export class PrivateKey implements privateKey.PrivateKey { return secp.getSharedSecret( this.secp256k1.bytes, peer.secp256k1Uncompressed.bytes, - false - ) + false, + ); } // encrypt plain bytes using a shared secret derived from peer's PublicKey; @@ -259,10 +259,10 @@ export class PrivateKey implements privateKey.PrivateKey { encrypt( plain: Uint8Array, peer: PublicKey, - additionalData?: Uint8Array + additionalData?: Uint8Array, ): Promise { - const secret = this.sharedSecret(peer) - return encrypt(plain, secret, additionalData) + const secret = this.sharedSecret(peer); + return encrypt(plain, secret, additionalData); } // decrypt Ciphertext using a shared secret derived from peer's PublicKey; @@ -270,32 +270,32 @@ export class PrivateKey implements privateKey.PrivateKey { decrypt( encrypted: Ciphertext, peer: PublicKey, - additionalData?: Uint8Array + additionalData?: Uint8Array, ): Promise { - const secret = this.sharedSecret(peer) - return decrypt(encrypted, secret, additionalData) + const secret = this.sharedSecret(peer); + return decrypt(encrypted, secret, additionalData); } // Does the provided PublicKey correspond to this PrivateKey? matches(key: PublicKey): boolean { - return this.publicKey.equals(key) + return this.publicKey.equals(key); } validatePublicKey(): boolean { - const generatedPublicKey = secp.getPublicKey(this.secp256k1.bytes) + const generatedPublicKey = secp.getPublicKey(this.secp256k1.bytes); return equalBytes( generatedPublicKey, - this.publicKey.secp256k1Uncompressed.bytes - ) + this.publicKey.secp256k1Uncompressed.bytes, + ); } // Encode this key into bytes. toBytes(): Uint8Array { - return privateKey.PrivateKey.encode(this).finish() + return privateKey.PrivateKey.encode(this).finish(); } // Decode key from bytes. static fromBytes(bytes: Uint8Array): PrivateKey { - return new PrivateKey(privateKey.PrivateKey.decode(bytes)) + return new PrivateKey(privateKey.PrivateKey.decode(bytes)); } } diff --git a/packages/js-sdk/src/crypto/PrivateKeyBundle.ts b/packages/js-sdk/src/crypto/PrivateKeyBundle.ts index 0bb16f625..4efd6ce43 100644 --- a/packages/js-sdk/src/crypto/PrivateKeyBundle.ts +++ b/packages/js-sdk/src/crypto/PrivateKeyBundle.ts @@ -1,61 +1,61 @@ -import { privateKey as proto } from '@xmtp/proto' -import type { Signer } from '@/types/Signer' -import { NoMatchingPreKeyError } from './errors' -import { PrivateKey, SignedPrivateKey } from './PrivateKey' -import type { PublicKey, SignedPublicKey } from './PublicKey' -import { PublicKeyBundle, SignedPublicKeyBundle } from './PublicKeyBundle' -import { WalletSigner } from './Signature' +import { privateKey as proto } from "@xmtp/proto"; +import type { Signer } from "@/types/Signer"; +import { NoMatchingPreKeyError } from "./errors"; +import { PrivateKey, SignedPrivateKey } from "./PrivateKey"; +import type { PublicKey, SignedPublicKey } from "./PublicKey"; +import { PublicKeyBundle, SignedPublicKeyBundle } from "./PublicKeyBundle"; +import { WalletSigner } from "./Signature"; // PrivateKeyBundle bundles the private keys corresponding to a PublicKeyBundle for convenience. // This bundle must not be shared with anyone, although will have to be persisted // somehow so that older messages can be decrypted again. export class PrivateKeyBundleV2 implements proto.PrivateKeyBundleV2 { - identityKey: SignedPrivateKey - preKeys: SignedPrivateKey[] - version = 2 - private _publicKeyBundle?: SignedPublicKeyBundle + identityKey: SignedPrivateKey; + preKeys: SignedPrivateKey[]; + version = 2; + private _publicKeyBundle?: SignedPublicKeyBundle; constructor(bundle: proto.PrivateKeyBundleV2) { if (!bundle.identityKey) { - throw new Error('missing identity key') + throw new Error("missing identity key"); } - this.identityKey = new SignedPrivateKey(bundle.identityKey) - this.preKeys = (bundle.preKeys || []).map((k) => new SignedPrivateKey(k)) + this.identityKey = new SignedPrivateKey(bundle.identityKey); + this.preKeys = (bundle.preKeys || []).map((k) => new SignedPrivateKey(k)); } // Generate a new key bundle with the preKey signed byt the identityKey. // Optionally sign the identityKey with the provided wallet as well. static async generate(wallet: Signer): Promise { const identityKey = await SignedPrivateKey.generate( - new WalletSigner(wallet) - ) + new WalletSigner(wallet), + ); const bundle = new PrivateKeyBundleV2({ identityKey, preKeys: [], - }) - await bundle.addPreKey() - return bundle + }); + await bundle.addPreKey(); + return bundle; } // Return the current (latest) pre-key (to be advertised). getCurrentPreKey(): SignedPrivateKey { - return this.preKeys[0] + return this.preKeys[0]; } // Find pre-key matching the provided public key. findPreKey(which: SignedPublicKey): SignedPrivateKey { - const preKey = this.preKeys.find((key) => key.matches(which)) + const preKey = this.preKeys.find((key) => key.matches(which)); if (!preKey) { - throw new NoMatchingPreKeyError(which) + throw new NoMatchingPreKeyError(which); } - return preKey + return preKey; } // Generate a new pre-key to be used as the current pre-key. async addPreKey(): Promise { - this._publicKeyBundle = undefined - const preKey = await SignedPrivateKey.generate(this.identityKey) - this.preKeys.unshift(preKey) + this._publicKeyBundle = undefined; + const preKey = await SignedPrivateKey.generate(this.identityKey); + this.preKeys.unshift(preKey); } // Return a key bundle with the current pre-key. @@ -64,9 +64,9 @@ export class PrivateKeyBundleV2 implements proto.PrivateKeyBundleV2 { this._publicKeyBundle = new SignedPublicKeyBundle({ identityKey: this.identityKey.publicKey, preKey: this.getCurrentPreKey().publicKey, - }) + }); } - return this._publicKeyBundle + return this._publicKeyBundle; } // sharedSecret derives a secret from peer's key bundles using a variation of X3DH protocol @@ -77,69 +77,69 @@ export class PrivateKeyBundleV2 implements proto.PrivateKeyBundleV2 { async sharedSecret( peer: SignedPublicKeyBundle, myPreKey: SignedPublicKey, - isRecipient: boolean + isRecipient: boolean, ): Promise { if (!peer.identityKey || !peer.preKey) { - throw new Error('invalid peer key bundle') + throw new Error("invalid peer key bundle"); } if (!(await peer.identityKey.verifyKey(peer.preKey))) { - throw new Error('peer preKey signature invalid') + throw new Error("peer preKey signature invalid"); } if (!this.identityKey) { - throw new Error('missing identity key') + throw new Error("missing identity key"); } - let dh1: Uint8Array, dh2: Uint8Array, preKey: SignedPrivateKey + let dh1: Uint8Array, dh2: Uint8Array, preKey: SignedPrivateKey; if (isRecipient) { - preKey = this.findPreKey(myPreKey) - dh1 = preKey.sharedSecret(peer.identityKey) - dh2 = this.identityKey.sharedSecret(peer.preKey) + preKey = this.findPreKey(myPreKey); + dh1 = preKey.sharedSecret(peer.identityKey); + dh2 = this.identityKey.sharedSecret(peer.preKey); } else { - preKey = this.findPreKey(myPreKey) - dh1 = this.identityKey.sharedSecret(peer.preKey) - dh2 = preKey.sharedSecret(peer.identityKey) + preKey = this.findPreKey(myPreKey); + dh1 = this.identityKey.sharedSecret(peer.preKey); + dh2 = preKey.sharedSecret(peer.identityKey); } - const dh3 = preKey.sharedSecret(peer.preKey) - const secret = new Uint8Array(dh1.length + dh2.length + dh3.length) - secret.set(dh1, 0) - secret.set(dh2, dh1.length) - secret.set(dh3, dh1.length + dh2.length) - return secret + const dh3 = preKey.sharedSecret(peer.preKey); + const secret = new Uint8Array(dh1.length + dh2.length + dh3.length); + secret.set(dh1, 0); + secret.set(dh2, dh1.length); + secret.set(dh3, dh1.length + dh2.length); + return secret; } encode(): Uint8Array { return proto.PrivateKeyBundle.encode({ v1: undefined, v2: this, - }).finish() + }).finish(); } validatePublicKeys(): boolean { if (!this.identityKey.validatePublicKey()) { - return false + return false; } - return this.preKeys.every((key) => key.validatePublicKey()) + return this.preKeys.every((key) => key.validatePublicKey()); } equals(other: this): boolean { if (this.preKeys.length !== other.preKeys.length) { - return false + return false; } for (let i = 0; i < this.preKeys.length; i++) { if (!this.preKeys[i].equals(other.preKeys[i])) { - return false + return false; } } - return this.identityKey.equals(other.identityKey) + return this.identityKey.equals(other.identityKey); } static fromLegacyBundle(bundle: PrivateKeyBundleV1): PrivateKeyBundleV2 { return new PrivateKeyBundleV2({ identityKey: SignedPrivateKey.fromLegacyKey(bundle.identityKey, true), preKeys: bundle.preKeys.map((k: PrivateKey) => - SignedPrivateKey.fromLegacyKey(k) + SignedPrivateKey.fromLegacyKey(k), ), - }) + }); } } @@ -147,54 +147,54 @@ export class PrivateKeyBundleV2 implements proto.PrivateKeyBundleV2 { // This bundle must not be shared with anyone, although will have to be persisted // somehow so that older messages can be decrypted again. export class PrivateKeyBundleV1 implements proto.PrivateKeyBundleV1 { - identityKey: PrivateKey - preKeys: PrivateKey[] - version = 1 - private _publicKeyBundle?: PublicKeyBundle + identityKey: PrivateKey; + preKeys: PrivateKey[]; + version = 1; + private _publicKeyBundle?: PublicKeyBundle; constructor(bundle: proto.PrivateKeyBundleV1) { if (!bundle.identityKey) { - throw new Error('missing identity key') + throw new Error("missing identity key"); } - this.identityKey = new PrivateKey(bundle.identityKey) - this.preKeys = (bundle.preKeys || []).map((k) => new PrivateKey(k)) + this.identityKey = new PrivateKey(bundle.identityKey); + this.preKeys = (bundle.preKeys || []).map((k) => new PrivateKey(k)); } // Generate a new key bundle with the preKey signed byt the identityKey. // Optionally sign the identityKey with the provided wallet as well. static async generate(wallet?: Signer): Promise { - const identityKey = PrivateKey.generate() + const identityKey = PrivateKey.generate(); if (wallet) { - await identityKey.publicKey.signWithWallet(wallet) + await identityKey.publicKey.signWithWallet(wallet); } const bundle = new PrivateKeyBundleV1({ identityKey, preKeys: [], - }) - await bundle.addPreKey() - return bundle + }); + await bundle.addPreKey(); + return bundle; } // Return the current (latest) pre-key (to be advertised). getCurrentPreKey(): PrivateKey { - return this.preKeys[0] + return this.preKeys[0]; } // Find pre-key matching the provided public key. findPreKey(which: PublicKey): PrivateKey { - const preKey = this.preKeys.find((key) => key.matches(which)) + const preKey = this.preKeys.find((key) => key.matches(which)); if (!preKey) { - throw new NoMatchingPreKeyError(which) + throw new NoMatchingPreKeyError(which); } - return preKey + return preKey; } // Generate a new pre-key to be used as the current pre-key. async addPreKey(): Promise { - this._publicKeyBundle = undefined - const preKey = PrivateKey.generate() - await this.identityKey.signKey(preKey.publicKey) - this.preKeys.unshift(preKey) + this._publicKeyBundle = undefined; + const preKey = PrivateKey.generate(); + await this.identityKey.signKey(preKey.publicKey); + this.preKeys.unshift(preKey); } // Return a key bundle with the current pre-key. @@ -203,17 +203,17 @@ export class PrivateKeyBundleV1 implements proto.PrivateKeyBundleV1 { this._publicKeyBundle = new PublicKeyBundle({ identityKey: this.identityKey.publicKey, preKey: this.getCurrentPreKey().publicKey, - }) + }); } - return this._publicKeyBundle + return this._publicKeyBundle; } validatePublicKeys(): boolean { if (!this.identityKey.validatePublicKey()) { - return false + return false; } - return this.preKeys.every((key) => key.validatePublicKey()) + return this.preKeys.every((key) => key.validatePublicKey()); } // sharedSecret derives a secret from peer's key bundles using a variation of X3DH protocol @@ -224,52 +224,52 @@ export class PrivateKeyBundleV1 implements proto.PrivateKeyBundleV1 { async sharedSecret( peer: PublicKeyBundle | SignedPublicKeyBundle, myPreKey: PublicKey, - isRecipient: boolean + isRecipient: boolean, ): Promise { if (!peer.identityKey || !peer.preKey) { - throw new Error('invalid peer key bundle') + throw new Error("invalid peer key bundle"); } if (!(await peer.identityKey.verifyKey(peer.preKey))) { - throw new Error('peer preKey signature invalid') + throw new Error("peer preKey signature invalid"); } if (!this.identityKey) { - throw new Error('missing identity key') + throw new Error("missing identity key"); } - let dh1: Uint8Array, dh2: Uint8Array, preKey: PrivateKey + let dh1: Uint8Array, dh2: Uint8Array, preKey: PrivateKey; if (isRecipient) { - preKey = this.findPreKey(myPreKey) - dh1 = preKey.sharedSecret(peer.identityKey) - dh2 = this.identityKey.sharedSecret(peer.preKey) + preKey = this.findPreKey(myPreKey); + dh1 = preKey.sharedSecret(peer.identityKey); + dh2 = this.identityKey.sharedSecret(peer.preKey); } else { - preKey = this.findPreKey(myPreKey) - dh1 = this.identityKey.sharedSecret(peer.preKey) - dh2 = preKey.sharedSecret(peer.identityKey) + preKey = this.findPreKey(myPreKey); + dh1 = this.identityKey.sharedSecret(peer.preKey); + dh2 = preKey.sharedSecret(peer.identityKey); } - const dh3 = preKey.sharedSecret(peer.preKey) - const secret = new Uint8Array(dh1.length + dh2.length + dh3.length) - secret.set(dh1, 0) - secret.set(dh2, dh1.length) - secret.set(dh3, dh1.length + dh2.length) - return secret + const dh3 = preKey.sharedSecret(peer.preKey); + const secret = new Uint8Array(dh1.length + dh2.length + dh3.length); + secret.set(dh1, 0); + secret.set(dh2, dh1.length); + secret.set(dh3, dh1.length + dh2.length); + return secret; } encode(): Uint8Array { return proto.PrivateKeyBundle.encode({ v1: this, v2: undefined, - }).finish() + }).finish(); } } -export type PrivateKeyBundle = PrivateKeyBundleV1 | PrivateKeyBundleV2 +export type PrivateKeyBundle = PrivateKeyBundleV1 | PrivateKeyBundleV2; export function decodePrivateKeyBundle(bytes: Uint8Array): PrivateKeyBundle { - const b = proto.PrivateKeyBundle.decode(bytes) + const b = proto.PrivateKeyBundle.decode(bytes); if (b.v1) { - return new PrivateKeyBundleV1(b.v1) + return new PrivateKeyBundleV1(b.v1); } if (b.v2) { - return new PrivateKeyBundleV2(b.v2) + return new PrivateKeyBundleV2(b.v2); } - throw new Error('unknown private key bundle version') + throw new Error("unknown private key bundle version"); } diff --git a/packages/js-sdk/src/crypto/PublicKey.ts b/packages/js-sdk/src/crypto/PublicKey.ts index 84f61e6a9..18fbdc59b 100644 --- a/packages/js-sdk/src/crypto/PublicKey.ts +++ b/packages/js-sdk/src/crypto/PublicKey.ts @@ -1,29 +1,29 @@ -import * as secp from '@noble/secp256k1' -import { publicKey } from '@xmtp/proto' -import Long from 'long' -import { hashMessage, hexToBytes, type Hex } from 'viem' -import type { Signer } from '@/types/Signer' -import { sha256 } from './encryption' -import Signature, { WalletSigner } from './Signature' -import { computeAddress, equalBytes, splitSignature } from './utils' +import * as secp from "@noble/secp256k1"; +import { publicKey } from "@xmtp/proto"; +import Long from "long"; +import { hashMessage, hexToBytes, type Hex } from "viem"; +import type { Signer } from "@/types/Signer"; +import { sha256 } from "./encryption"; +import Signature, { WalletSigner } from "./Signature"; +import { computeAddress, equalBytes, splitSignature } from "./utils"; // SECP256k1 public key in uncompressed format with prefix type secp256k1Uncompressed = { // uncompressed point with prefix (0x04) [ P || X || Y ], 65 bytes - bytes: Uint8Array -} + bytes: Uint8Array; +}; // Validate a key. function secp256k1UncompressedCheck(key: secp256k1Uncompressed): void { if (key.bytes.length !== 65) { - throw new Error(`invalid public key length: ${key.bytes.length}`) + throw new Error(`invalid public key length: ${key.bytes.length}`); } if (key.bytes[0] !== 4) { - throw new Error(`unrecognized public key prefix: ${key.bytes[0]}`) + throw new Error(`unrecognized public key prefix: ${key.bytes[0]}`); } } -const MS_NS_TIMESTAMP_THRESHOLD = new Long(10 ** 9).mul(10 ** 9) +const MS_NS_TIMESTAMP_THRESHOLD = new Long(10 ** 9).mul(10 ** 9); // Basic public key without a signature. export class UnsignedPublicKey implements publicKey.UnsignedPublicKey { @@ -31,76 +31,76 @@ export class UnsignedPublicKey implements publicKey.UnsignedPublicKey { // to allow transparent conversion of pre-existing signed PublicKey to SignedPublicKey // it can also be ms since epoch; use MS_NS_TIMESTAMP_THRESHOLD to distinguish // the two cases. - createdNs: Long - secp256k1Uncompressed: secp256k1Uncompressed // eslint-disable-line camelcase + createdNs: Long; + secp256k1Uncompressed: secp256k1Uncompressed; // eslint-disable-line camelcase constructor(obj: publicKey.UnsignedPublicKey) { if (!obj?.secp256k1Uncompressed) { - throw new Error('invalid public key') + throw new Error("invalid public key"); } - secp256k1UncompressedCheck(obj.secp256k1Uncompressed) - this.secp256k1Uncompressed = obj.secp256k1Uncompressed - this.createdNs = obj.createdNs.toUnsigned() + secp256k1UncompressedCheck(obj.secp256k1Uncompressed); + this.secp256k1Uncompressed = obj.secp256k1Uncompressed; + this.createdNs = obj.createdNs.toUnsigned(); } // The time the key was generated. generated(): Date | undefined { - return new Date(this.timestamp.toNumber()) + return new Date(this.timestamp.toNumber()); } isFromLegacyKey(): boolean { - return this.createdNs.lessThan(MS_NS_TIMESTAMP_THRESHOLD) + return this.createdNs.lessThan(MS_NS_TIMESTAMP_THRESHOLD); } // creation time in milliseconds get timestamp(): Long { return ( this.isFromLegacyKey() ? this.createdNs : this.createdNs.div(1000000) - ).toUnsigned() + ).toUnsigned(); } // Verify that signature was created from the digest using matching private key. verify(signature: Signature, digest: Uint8Array): boolean { if (!signature.ecdsaCompact) { - return false + return false; } return secp.verify( signature.ecdsaCompact.bytes, digest, - this.secp256k1Uncompressed.bytes - ) + this.secp256k1Uncompressed.bytes, + ); } // Verify that the provided public key was signed by matching private key. async verifyKey(pub: PublicKey | SignedPublicKey): Promise { if (!pub.signature) { - return false + return false; } - const digest = await sha256(pub.bytesToSign()) - return this.verify(pub.signature, digest) + const digest = await sha256(pub.bytesToSign()); + return this.verify(pub.signature, digest); } // Is other the same/equivalent public key? equals(other: this): boolean { return equalBytes( this.secp256k1Uncompressed.bytes, - other.secp256k1Uncompressed.bytes - ) + other.secp256k1Uncompressed.bytes, + ); } // Derive Ethereum address from this public key. getEthereumAddress(): string { - return computeAddress(this.secp256k1Uncompressed.bytes) + return computeAddress(this.secp256k1Uncompressed.bytes); } // Encode public key into bytes. toBytes(): Uint8Array { - return publicKey.UnsignedPublicKey.encode(this).finish() + return publicKey.UnsignedPublicKey.encode(this).finish(); } // Decode public key from bytes. static fromBytes(bytes: Uint8Array): UnsignedPublicKey { - return new UnsignedPublicKey(publicKey.UnsignedPublicKey.decode(bytes)) + return new UnsignedPublicKey(publicKey.UnsignedPublicKey.decode(bytes)); } } @@ -109,19 +109,19 @@ export class SignedPublicKey extends UnsignedPublicKey implements publicKey.SignedPublicKey { - keyBytes: Uint8Array // caches the bytes of the encoded unsigned key - signature: Signature + keyBytes: Uint8Array; // caches the bytes of the encoded unsigned key + signature: Signature; constructor(obj: publicKey.SignedPublicKey) { if (!obj.keyBytes) { - throw new Error('missing key bytes') + throw new Error("missing key bytes"); } - super(publicKey.UnsignedPublicKey.decode(obj.keyBytes)) - this.keyBytes = obj.keyBytes + super(publicKey.UnsignedPublicKey.decode(obj.keyBytes)); + this.keyBytes = obj.keyBytes; if (!obj.signature) { - throw new Error('missing key signature') + throw new Error("missing key signature"); } - this.signature = new Signature(obj.signature) + this.signature = new Signature(obj.signature); } // Return the key without the signature. @@ -129,12 +129,12 @@ export class SignedPublicKey return new UnsignedPublicKey({ createdNs: this.createdNs, secp256k1Uncompressed: this.secp256k1Uncompressed, - }) + }); } // Return public key of the signer of this key. signerKey(): Promise { - return this.signature.signerKey(this) + return this.signature.signerKey(this); } // Assume the key was signed by a wallet and @@ -142,13 +142,13 @@ export class SignedPublicKey // the signature of this key. async walletSignatureAddress(): Promise { if (!this.signature.walletEcdsaCompact) { - throw new Error('key was not signed by a wallet') + throw new Error("key was not signed by a wallet"); } - const pk = await this.signerKey() + const pk = await this.signerKey(); if (!pk) { - throw new Error('key signature not valid') + throw new Error("key signature not valid"); } - return pk.getEthereumAddress() + return pk.getEthereumAddress(); } // Is other the same/equivalent public key? @@ -156,58 +156,58 @@ export class SignedPublicKey return ( this.unsignedKey.equals(other.unsignedKey) && this.signature.equals(other.signature) - ) + ); } // Return bytes of the encoded unsigned key. bytesToSign(): Uint8Array { - return this.keyBytes + return this.keyBytes; } // Encode signed key into bytes. toBytes(): Uint8Array { - return publicKey.SignedPublicKey.encode(this).finish() + return publicKey.SignedPublicKey.encode(this).finish(); } // Decode signed key from bytes. static fromBytes(bytes: Uint8Array): SignedPublicKey { - return new SignedPublicKey(publicKey.SignedPublicKey.decode(bytes)) + return new SignedPublicKey(publicKey.SignedPublicKey.decode(bytes)); } toLegacyKey(): PublicKey { if (!this.isFromLegacyKey()) { - throw new Error('cannot be converted to legacy key') + throw new Error("cannot be converted to legacy key"); } - let signature = this.signature + let signature = this.signature; if (signature.walletEcdsaCompact) { signature = new Signature({ ecdsaCompact: signature.walletEcdsaCompact, - }) + }); } return new PublicKey({ timestamp: this.timestamp, secp256k1Uncompressed: this.secp256k1Uncompressed, signature, - }) + }); } static fromLegacyKey( legacyKey: PublicKey, - signedByWallet?: boolean + signedByWallet?: boolean, ): SignedPublicKey { if (!legacyKey.signature) { - throw new Error('key is not signed') + throw new Error("key is not signed"); } - let signature = legacyKey.signature + let signature = legacyKey.signature; if (signedByWallet) { signature = new Signature({ walletEcdsaCompact: signature.ecdsaCompact, - }) + }); } return new SignedPublicKey({ keyBytes: legacyKey.bytesToSign(), signature, - }) + }); } } @@ -217,13 +217,13 @@ export class PublicKey extends UnsignedPublicKey implements publicKey.PublicKey { - signature?: Signature + signature?: Signature; constructor(obj: publicKey.PublicKey) { super({ createdNs: obj.timestamp.mul(1000000), secp256k1Uncompressed: obj.secp256k1Uncompressed, - }) + }); if (obj.signature) { // Handle a case where Flutter was publishing signatures with walletEcdsaCompact // instead of ecdsaCompact for v1 keys. @@ -233,36 +233,36 @@ export class PublicKey bytes: obj.signature.walletEcdsaCompact.bytes, recovery: obj.signature.walletEcdsaCompact.recovery, }, - }) + }); } else { - this.signature = new Signature(obj.signature) + this.signature = new Signature(obj.signature); } } } get timestamp(): Long { - return this.createdNs.div(1000000) + return this.createdNs.div(1000000); } bytesToSign(): Uint8Array { return publicKey.PublicKey.encode({ timestamp: this.timestamp, secp256k1Uncompressed: this.secp256k1Uncompressed, - }).finish() + }).finish(); } // sign the key using a wallet async signWithWallet(wallet: Signer): Promise { const sigString = await wallet.signMessage( - WalletSigner.identitySigRequestText(this.bytesToSign()) - ) - const { bytes, recovery } = splitSignature(sigString as Hex) + WalletSigner.identitySigRequestText(this.bytesToSign()), + ); + const { bytes, recovery } = splitSignature(sigString as Hex); this.signature = new Signature({ ecdsaCompact: { bytes, recovery, }, - }) + }); } // Assume the key was signed by a wallet and @@ -270,23 +270,23 @@ export class PublicKey // the signature for this key. walletSignatureAddress(): string { if (!this.signature) { - throw new Error('key is not signed') + throw new Error("key is not signed"); } const digest = hexToBytes( - hashMessage(WalletSigner.identitySigRequestText(this.bytesToSign())) - ) - const pk = this.signature.getPublicKey(digest) + hashMessage(WalletSigner.identitySigRequestText(this.bytesToSign())), + ); + const pk = this.signature.getPublicKey(digest); if (!pk) { - throw new Error('key signature is malformed') + throw new Error("key signature is malformed"); } - return pk.getEthereumAddress() + return pk.getEthereumAddress(); } toBytes(): Uint8Array { - return publicKey.PublicKey.encode(this).finish() + return publicKey.PublicKey.encode(this).finish(); } static fromBytes(bytes: Uint8Array): PublicKey { - return new PublicKey(publicKey.PublicKey.decode(bytes)) + return new PublicKey(publicKey.PublicKey.decode(bytes)); } } diff --git a/packages/js-sdk/src/crypto/PublicKeyBundle.ts b/packages/js-sdk/src/crypto/PublicKeyBundle.ts index 2c94ee884..06f4978ff 100644 --- a/packages/js-sdk/src/crypto/PublicKeyBundle.ts +++ b/packages/js-sdk/src/crypto/PublicKeyBundle.ts @@ -1,53 +1,53 @@ -import { publicKey } from '@xmtp/proto' -import { PublicKey, SignedPublicKey } from './PublicKey' +import { publicKey } from "@xmtp/proto"; +import { PublicKey, SignedPublicKey } from "./PublicKey"; // LEGACY: PublicKeyBundle packages all the keys that a participant should advertise. // The PreKey must be signed by the IdentityKey. // The IdentityKey must be signed by the wallet to authenticate it. export class SignedPublicKeyBundle implements publicKey.SignedPublicKeyBundle { - identityKey: SignedPublicKey - preKey: SignedPublicKey + identityKey: SignedPublicKey; + preKey: SignedPublicKey; constructor(bundle: publicKey.SignedPublicKeyBundle) { if (!bundle.identityKey) { - throw new Error('missing identity key') + throw new Error("missing identity key"); } if (!bundle.preKey) { - throw new Error('missing pre-key') + throw new Error("missing pre-key"); } - this.identityKey = new SignedPublicKey(bundle.identityKey) - this.preKey = new SignedPublicKey(bundle.preKey) + this.identityKey = new SignedPublicKey(bundle.identityKey); + this.preKey = new SignedPublicKey(bundle.preKey); } walletSignatureAddress(): Promise { - return this.identityKey.walletSignatureAddress() + return this.identityKey.walletSignatureAddress(); } equals(other: this): boolean { return ( this.identityKey.equals(other.identityKey) && this.preKey.equals(other.preKey) - ) + ); } toBytes(): Uint8Array { - return publicKey.SignedPublicKeyBundle.encode(this).finish() + return publicKey.SignedPublicKeyBundle.encode(this).finish(); } isFromLegacyBundle(): boolean { - return this.identityKey.isFromLegacyKey() && this.preKey.isFromLegacyKey() + return this.identityKey.isFromLegacyKey() && this.preKey.isFromLegacyKey(); } toLegacyBundle(): PublicKeyBundle { return new PublicKeyBundle({ identityKey: this.identityKey.toLegacyKey(), preKey: this.preKey.toLegacyKey(), - }) + }); } static fromBytes(bytes: Uint8Array): SignedPublicKeyBundle { - const decoded = publicKey.SignedPublicKeyBundle.decode(bytes) - return new SignedPublicKeyBundle(decoded) + const decoded = publicKey.SignedPublicKeyBundle.decode(bytes); + return new SignedPublicKeyBundle(decoded); } static fromLegacyBundle(bundle: PublicKeyBundle): SignedPublicKeyBundle { @@ -56,7 +56,7 @@ export class SignedPublicKeyBundle implements publicKey.SignedPublicKeyBundle { // Maybe that is not universally true in the future identityKey: SignedPublicKey.fromLegacyKey(bundle.identityKey, true), preKey: SignedPublicKey.fromLegacyKey(bundle.preKey), - }) + }); } } @@ -64,37 +64,37 @@ export class SignedPublicKeyBundle implements publicKey.SignedPublicKeyBundle { // The PreKey must be signed by the IdentityKey. // The IdentityKey can be signed by the wallet to authenticate it. export class PublicKeyBundle implements publicKey.PublicKeyBundle { - identityKey: PublicKey - preKey: PublicKey + identityKey: PublicKey; + preKey: PublicKey; constructor(bundle: publicKey.PublicKeyBundle) { if (!bundle.identityKey) { - throw new Error('missing identity key') + throw new Error("missing identity key"); } if (!bundle.preKey) { - throw new Error('missing pre-key') + throw new Error("missing pre-key"); } - this.identityKey = new PublicKey(bundle.identityKey) - this.preKey = new PublicKey(bundle.preKey) + this.identityKey = new PublicKey(bundle.identityKey); + this.preKey = new PublicKey(bundle.preKey); } equals(other: this): boolean { return ( this.identityKey.equals(other.identityKey) && this.preKey.equals(other.preKey) - ) + ); } walletSignatureAddress(): string { - return this.identityKey.walletSignatureAddress() + return this.identityKey.walletSignatureAddress(); } toBytes(): Uint8Array { - return publicKey.PublicKeyBundle.encode(this).finish() + return publicKey.PublicKeyBundle.encode(this).finish(); } static fromBytes(bytes: Uint8Array): PublicKeyBundle { - const decoded = publicKey.PublicKeyBundle.decode(bytes) - return new PublicKeyBundle(decoded) + const decoded = publicKey.PublicKeyBundle.decode(bytes); + return new PublicKeyBundle(decoded); } } diff --git a/packages/js-sdk/src/crypto/README.md b/packages/js-sdk/src/crypto/README.md index 54ba89af8..aa3c72cd2 100644 --- a/packages/js-sdk/src/crypto/README.md +++ b/packages/js-sdk/src/crypto/README.md @@ -12,18 +12,18 @@ Following snippet shows the API for managing key bundles (assuming a connected w ```js // generate new wallet keys -let pri = await PrivateKeyBundleV1.generate(wallet) -let pub = pri.getPublicKeyBundle() +let pri = await PrivateKeyBundleV1.generate(wallet); +let pub = pri.getPublicKeyBundle(); // serialize the public bundle for advertisement on the network -let bytes = pub.toBytes() +let bytes = pub.toBytes(); // serialize/encrypt the private bundle for secure storage -store = EncryptedKeyStore(wallet, new LocalStorageStore()) -await store.storePrivateKeyBundle(pri) +store = EncryptedKeyStore(wallet, new LocalStorageStore()); +await store.storePrivateKeyBundle(pri); // deserialize/decrypt private key bundle from storage -let pri2 = await store.loadPrivateKeyBundle() +let pri2 = await store.loadPrivateKeyBundle(); ``` ## Sending a message @@ -32,7 +32,7 @@ The sender must obtain the advertized public key bundle of the recipient and use ```js // deserializing recipient's public key bundle (bytes obtained from the network) -recipientPublic = PublicKeyBundle.fromBytes(bytes) +recipientPublic = PublicKeyBundle.fromBytes(bytes); // encrypting binary `payload` for submission to the network // @sender is sender's PrivateKeyBundle @@ -40,9 +40,9 @@ recipientPublic = PublicKeyBundle.fromBytes(bytes) let secret = await sender.sharedSecret( recipientPublic, senderPublic.preKey, - false -) -let bytes = await encrypt(payload, secret) + false, +); +let bytes = await encrypt(payload, secret); ``` ## Receiving a message diff --git a/packages/js-sdk/src/crypto/Signature.ts b/packages/js-sdk/src/crypto/Signature.ts index f83601df0..48f44d237 100644 --- a/packages/js-sdk/src/crypto/Signature.ts +++ b/packages/js-sdk/src/crypto/Signature.ts @@ -1,136 +1,136 @@ -import * as secp from '@noble/secp256k1' -import { signature } from '@xmtp/proto' -import Long from 'long' -import { hashMessage, hexToBytes, type Hex } from 'viem' -import type { Signer } from '@/types/Signer' -import { SignedPrivateKey } from './PrivateKey' -import { PublicKey, SignedPublicKey, UnsignedPublicKey } from './PublicKey' -import { bytesToHex, equalBytes, splitSignature } from './utils' +import * as secp from "@noble/secp256k1"; +import { signature } from "@xmtp/proto"; +import Long from "long"; +import { hashMessage, hexToBytes, type Hex } from "viem"; +import type { Signer } from "@/types/Signer"; +import { SignedPrivateKey } from "./PrivateKey"; +import { PublicKey, SignedPublicKey, UnsignedPublicKey } from "./PublicKey"; +import { bytesToHex, equalBytes, splitSignature } from "./utils"; // ECDSA signature with recovery bit. export type ECDSACompactWithRecovery = { - bytes: Uint8Array // compact representation [ R || S ], 64 bytes - recovery: number // recovery bit -} + bytes: Uint8Array; // compact representation [ R || S ], 64 bytes + recovery: number; // recovery bit +}; // Validate signature. function ecdsaCheck(sig: ECDSACompactWithRecovery): void { if (sig.bytes.length !== 64) { - throw new Error(`invalid signature length: ${sig.bytes.length}`) + throw new Error(`invalid signature length: ${sig.bytes.length}`); } if (sig.recovery !== 0 && sig.recovery !== 1) { - throw new Error(`invalid recovery bit: ${sig.recovery}`) + throw new Error(`invalid recovery bit: ${sig.recovery}`); } } // Compare signatures. function ecdsaEqual( a: ECDSACompactWithRecovery, - b: ECDSACompactWithRecovery + b: ECDSACompactWithRecovery, ): boolean { - return a.recovery === b.recovery && equalBytes(a.bytes, b.bytes) + return a.recovery === b.recovery && equalBytes(a.bytes, b.bytes); } // Derive public key of the signer from the digest and the signature. export function ecdsaSignerKey( digest: Uint8Array, - signature: ECDSACompactWithRecovery + signature: ECDSACompactWithRecovery, ): UnsignedPublicKey | undefined { const bytes = secp.recoverPublicKey( digest, signature.bytes, - signature.recovery - ) + signature.recovery, + ); return bytes ? new UnsignedPublicKey({ secp256k1Uncompressed: { bytes }, createdNs: Long.fromNumber(0), }) - : undefined + : undefined; } export default class Signature implements signature.Signature { // SECP256k1/SHA256 ECDSA signature - ecdsaCompact: ECDSACompactWithRecovery | undefined // eslint-disable-line camelcase + ecdsaCompact: ECDSACompactWithRecovery | undefined; // eslint-disable-line camelcase // SECP256k1/keccak256 ECDSA signature created with Signer.signMessage (see WalletSigner) - walletEcdsaCompact: ECDSACompactWithRecovery | undefined // eslint-disable-line camelcase + walletEcdsaCompact: ECDSACompactWithRecovery | undefined; // eslint-disable-line camelcase constructor(obj: Partial) { if (obj.ecdsaCompact) { - ecdsaCheck(obj.ecdsaCompact) - this.ecdsaCompact = obj.ecdsaCompact + ecdsaCheck(obj.ecdsaCompact); + this.ecdsaCompact = obj.ecdsaCompact; } else if (obj.walletEcdsaCompact) { - ecdsaCheck(obj.walletEcdsaCompact) - this.walletEcdsaCompact = obj.walletEcdsaCompact + ecdsaCheck(obj.walletEcdsaCompact); + this.walletEcdsaCompact = obj.walletEcdsaCompact; } else { - throw new Error('invalid signature') + throw new Error("invalid signature"); } } // Return the public key that validates provided key's signature. async signerKey( - key: SignedPublicKey + key: SignedPublicKey, ): Promise { if (this.ecdsaCompact) { - return SignedPrivateKey.signerKey(key, this.ecdsaCompact) + return SignedPrivateKey.signerKey(key, this.ecdsaCompact); } else if (this.walletEcdsaCompact) { - return WalletSigner.signerKey(key, this.walletEcdsaCompact) + return WalletSigner.signerKey(key, this.walletEcdsaCompact); } else { - return undefined + return undefined; } } // LEGACY: Return the public key that validates this signature given the provided digest. // Return undefined if the signature is malformed. getPublicKey(digest: Uint8Array): PublicKey | undefined { - let bytes: Uint8Array | undefined + let bytes: Uint8Array | undefined; if (this.ecdsaCompact) { bytes = secp.recoverPublicKey( digest, this.ecdsaCompact.bytes, - this.ecdsaCompact.recovery - ) + this.ecdsaCompact.recovery, + ); } else if (this.walletEcdsaCompact) { bytes = secp.recoverPublicKey( digest, this.walletEcdsaCompact.bytes, - this.walletEcdsaCompact.recovery - ) + this.walletEcdsaCompact.recovery, + ); } else { - throw new Error('invalid v1 signature') + throw new Error("invalid v1 signature"); } return bytes ? new PublicKey({ secp256k1Uncompressed: { bytes }, timestamp: Long.fromNumber(0), }) - : undefined + : undefined; } // Is this the same/equivalent signature as other? equals(other: Signature): boolean { if (this.ecdsaCompact && other.ecdsaCompact) { - return ecdsaEqual(this.ecdsaCompact, other.ecdsaCompact) + return ecdsaEqual(this.ecdsaCompact, other.ecdsaCompact); } if (this.walletEcdsaCompact && other.walletEcdsaCompact) { - return ecdsaEqual(this.walletEcdsaCompact, other.walletEcdsaCompact) + return ecdsaEqual(this.walletEcdsaCompact, other.walletEcdsaCompact); } - return false + return false; } toBytes(): Uint8Array { - return signature.Signature.encode(this).finish() + return signature.Signature.encode(this).finish(); } static fromBytes(bytes: Uint8Array): Signature { - return new Signature(signature.Signature.decode(bytes)) + return new Signature(signature.Signature.decode(bytes)); } } // Deprecation in progress // A signer that can be used to sign public keys. export interface KeySigner { - signKey(key: UnsignedPublicKey): Promise + signKey(key: UnsignedPublicKey): Promise; } export enum AccountLinkedRole { @@ -140,10 +140,10 @@ export enum AccountLinkedRole { // A wallet based KeySigner. export class WalletSigner implements KeySigner { - wallet: Signer + wallet: Signer; constructor(wallet: Signer) { - this.wallet = wallet + this.wallet = wallet; } static identitySigRequestText(keyBytes: Uint8Array): string { @@ -152,35 +152,35 @@ export class WalletSigner implements KeySigner { // and/or a migration; otherwise clients will fail to verify previously // signed keys. return ( - 'XMTP : Create Identity\n' + + "XMTP : Create Identity\n" + `${bytesToHex(keyBytes)}\n` + - '\n' + - 'For more info: https://xmtp.org/signatures/' - ) + "\n" + + "For more info: https://xmtp.org/signatures/" + ); } static signerKey( key: SignedPublicKey, - signature: ECDSACompactWithRecovery + signature: ECDSACompactWithRecovery, ): UnsignedPublicKey | undefined { const digest = hexToBytes( - hashMessage(this.identitySigRequestText(key.bytesToSign())) - ) - return ecdsaSignerKey(digest, signature) + hashMessage(this.identitySigRequestText(key.bytesToSign())), + ); + return ecdsaSignerKey(digest, signature); } async signKey(key: UnsignedPublicKey): Promise { - const keyBytes = key.toBytes() + const keyBytes = key.toBytes(); const sigString = await this.wallet.signMessage( - WalletSigner.identitySigRequestText(keyBytes) - ) - const { bytes, recovery } = splitSignature(sigString as Hex) + WalletSigner.identitySigRequestText(keyBytes), + ); + const { bytes, recovery } = splitSignature(sigString as Hex); const signature = new Signature({ walletEcdsaCompact: { bytes, recovery, }, - }) - return new SignedPublicKey({ keyBytes, signature }) + }); + return new SignedPublicKey({ keyBytes, signature }); } } diff --git a/packages/js-sdk/src/crypto/SignedEciesCiphertext.ts b/packages/js-sdk/src/crypto/SignedEciesCiphertext.ts index 992428b39..b4dbd0334 100644 --- a/packages/js-sdk/src/crypto/SignedEciesCiphertext.ts +++ b/packages/js-sdk/src/crypto/SignedEciesCiphertext.ts @@ -1,77 +1,77 @@ -import { ciphertext } from '@xmtp/proto' -import { sha256 } from './encryption' -import type { PrivateKey, SignedPrivateKey } from './PrivateKey' -import type { PublicKey, SignedPublicKey } from './PublicKey' -import Signature from './Signature' +import { ciphertext } from "@xmtp/proto"; +import { sha256 } from "./encryption"; +import type { PrivateKey, SignedPrivateKey } from "./PrivateKey"; +import type { PublicKey, SignedPublicKey } from "./PublicKey"; +import Signature from "./Signature"; -const IV_LENGTH = 16 -const EPHEMERAL_PUBLIC_KEY_LENGTH = 65 -const MAC_LENGTH = 32 -const AES_BLOCK_SIZE = 16 +const IV_LENGTH = 16; +const EPHEMERAL_PUBLIC_KEY_LENGTH = 65; +const MAC_LENGTH = 32; +const AES_BLOCK_SIZE = 16; const assertEciesLengths = ( - ecies: ciphertext.SignedEciesCiphertext_Ecies + ecies: ciphertext.SignedEciesCiphertext_Ecies, ): void => { if (ecies.iv.length !== IV_LENGTH) { - throw new Error('Invalid iv length') + throw new Error("Invalid iv length"); } if (ecies.ephemeralPublicKey.length !== EPHEMERAL_PUBLIC_KEY_LENGTH) { - throw new Error('Invalid ephemPublicKey length') + throw new Error("Invalid ephemPublicKey length"); } if ( ecies.ciphertext.length < 1 || ecies.ciphertext.length % AES_BLOCK_SIZE !== 0 ) { - throw new Error('Invalid ciphertext length') + throw new Error("Invalid ciphertext length"); } if (ecies.mac.length !== MAC_LENGTH) { - throw new Error('Invalid mac length') + throw new Error("Invalid mac length"); } -} +}; export default class SignedEciesCiphertext implements ciphertext.SignedEciesCiphertext { - eciesBytes: Uint8Array - signature: Signature - ciphertext: ciphertext.SignedEciesCiphertext_Ecies + eciesBytes: Uint8Array; + signature: Signature; + ciphertext: ciphertext.SignedEciesCiphertext_Ecies; constructor({ eciesBytes, signature }: ciphertext.SignedEciesCiphertext) { if (!eciesBytes || !eciesBytes.length) { - throw new Error('eciesBytes is empty') + throw new Error("eciesBytes is empty"); } if (!signature) { - throw new Error('signature is undefined') + throw new Error("signature is undefined"); } - this.eciesBytes = eciesBytes - this.signature = new Signature(signature) - this.ciphertext = ciphertext.SignedEciesCiphertext_Ecies.decode(eciesBytes) + this.eciesBytes = eciesBytes; + this.signature = new Signature(signature); + this.ciphertext = ciphertext.SignedEciesCiphertext_Ecies.decode(eciesBytes); } toBytes(): Uint8Array { - return ciphertext.SignedEciesCiphertext.encode(this).finish() + return ciphertext.SignedEciesCiphertext.encode(this).finish(); } async verify(pubKey: PublicKey | SignedPublicKey): Promise { - return pubKey.verify(this.signature, await sha256(this.eciesBytes)) + return pubKey.verify(this.signature, await sha256(this.eciesBytes)); } static fromBytes(data: Uint8Array): SignedEciesCiphertext { - const obj = ciphertext.SignedEciesCiphertext.decode(data) + const obj = ciphertext.SignedEciesCiphertext.decode(data); - return new SignedEciesCiphertext(obj) + return new SignedEciesCiphertext(obj); } static async create( ecies: ciphertext.SignedEciesCiphertext_Ecies, - signer: PrivateKey | SignedPrivateKey + signer: PrivateKey | SignedPrivateKey, ): Promise { - assertEciesLengths(ecies) + assertEciesLengths(ecies); const eciesBytes = - ciphertext.SignedEciesCiphertext_Ecies.encode(ecies).finish() - const signature = await signer.sign(await sha256(eciesBytes)) + ciphertext.SignedEciesCiphertext_Ecies.encode(ecies).finish(); + const signature = await signer.sign(await sha256(eciesBytes)); - return new SignedEciesCiphertext({ eciesBytes, signature }) + return new SignedEciesCiphertext({ eciesBytes, signature }); } } diff --git a/packages/js-sdk/src/crypto/crypto.browser.ts b/packages/js-sdk/src/crypto/crypto.browser.ts index 037947be4..e34bab874 100644 --- a/packages/js-sdk/src/crypto/crypto.browser.ts +++ b/packages/js-sdk/src/crypto/crypto.browser.ts @@ -1,5 +1,5 @@ /*********************************************************************************************** * DO NOT IMPORT THIS FILE DIRECTLY ***********************************************************************************************/ -const crypto = window.crypto -export default crypto +const crypto = window.crypto; +export default crypto; diff --git a/packages/js-sdk/src/crypto/crypto.ts b/packages/js-sdk/src/crypto/crypto.ts index 8447a2446..38329179b 100644 --- a/packages/js-sdk/src/crypto/crypto.ts +++ b/packages/js-sdk/src/crypto/crypto.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line no-restricted-syntax -import { webcrypto } from 'crypto' +import { webcrypto } from "crypto"; -const crypto = webcrypto -export default crypto +const crypto = webcrypto; +export default crypto; diff --git a/packages/js-sdk/src/crypto/ecies.ts b/packages/js-sdk/src/crypto/ecies.ts index fb234696d..6e3ce3320 100644 --- a/packages/js-sdk/src/crypto/ecies.ts +++ b/packages/js-sdk/src/crypto/ecies.ts @@ -3,109 +3,109 @@ * `elliptic` is a CommonJS module and has issues with named imports * DO NOT CHANGE THIS TO A NAMED IMPORT */ -import elliptic from 'elliptic' -import crypto from './crypto' +import elliptic from "elliptic"; +import crypto from "./crypto"; -const EC = elliptic.ec -const ec = new EC('secp256k1') +const EC = elliptic.ec; +const ec = new EC("secp256k1"); -const subtle = crypto.subtle +const subtle = crypto.subtle; const EC_GROUP_ORDER = Buffer.from( - 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', - 'hex' -) -const ZERO32 = Buffer.alloc(32, 0) + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + "hex", +); +const ZERO32 = Buffer.alloc(32, 0); export type Ecies = { - iv: Buffer - ephemeralPublicKey: Buffer - ciphertext: Buffer - mac: Buffer -} + iv: Buffer; + ephemeralPublicKey: Buffer; + ciphertext: Buffer; + mac: Buffer; +}; function assert(condition: boolean, message: string) { if (!condition) { - throw new Error(message || 'Assertion failed') + throw new Error(message || "Assertion failed"); } } function isScalar(x: unknown) { - return Buffer.isBuffer(x) && x.length === 32 + return Buffer.isBuffer(x) && x.length === 32; } function isValidPrivateKey(privateKey: Buffer) { if (!isScalar(privateKey)) { - return false + return false; } return ( privateKey.compare(ZERO32) > 0 && // > 0 privateKey.compare(EC_GROUP_ORDER) < 0 - ) // < G + ); // < G } // Compare two buffers in constant time to prevent timing attacks. function equalConstTime(b1: Buffer, b2: Buffer) { if (b1.length !== b2.length) { - return false + return false; } - let res = 0 + let res = 0; for (let i = 0; i < b1.length; i++) { - res |= b1[i] ^ b2[i] // jshint ignore:line + res |= b1[i] ^ b2[i]; // jshint ignore:line } - return res === 0 + return res === 0; } function randomBytes(size: number): Buffer { - const arr = new Uint8Array(size) - crypto.getRandomValues(arr) - return Buffer.from(arr) + const arr = new Uint8Array(size); + crypto.getRandomValues(arr); + return Buffer.from(arr); } async function sha512(msg: Buffer) { - const digest = await subtle.digest('SHA-512', msg) - return Buffer.from(digest) + const digest = await subtle.digest("SHA-512", msg); + return Buffer.from(digest); } function getAes( - op: 'encrypt' | 'decrypt' + op: "encrypt" | "decrypt", ): (iv: Buffer, key: Buffer, data: Buffer) => Promise { return function (iv: Buffer, key: Uint8Array, data: Uint8Array) { return new Promise(function (resolve) { - const importAlgorithm = { name: 'AES-CBC' } - const keyp = subtle.importKey('raw', key, importAlgorithm, false, [op]) + const importAlgorithm = { name: "AES-CBC" }; + const keyp = subtle.importKey("raw", key, importAlgorithm, false, [op]); return keyp .then(function (cryptoKey) { - const encAlgorithm = { name: 'AES-CBC', iv } - return subtle[op](encAlgorithm, cryptoKey, data) + const encAlgorithm = { name: "AES-CBC", iv }; + return subtle[op](encAlgorithm, cryptoKey, data); }) .then(function (result) { - resolve(Buffer.from(new Uint8Array(result))) - }) - }) - } + resolve(Buffer.from(new Uint8Array(result))); + }); + }); + }; } -const aesCbcEncrypt = getAes('encrypt') -const aesCbcDecrypt = getAes('decrypt') +const aesCbcEncrypt = getAes("encrypt"); +const aesCbcDecrypt = getAes("decrypt"); export async function hmacSha256Sign(key: Buffer, msg: Buffer) { const newKey = await subtle.importKey( - 'raw', + "raw", key, - { name: 'HMAC', hash: { name: 'SHA-256' } }, + { name: "HMAC", hash: { name: "SHA-256" } }, false, - ['sign'] - ) + ["sign"], + ); return Buffer.from( - await subtle.sign({ name: 'HMAC', hash: 'SHA-256' }, newKey, msg) - ) + await subtle.sign({ name: "HMAC", hash: "SHA-256" }, newKey, msg), + ); } async function hmacSha256Verify(key: Buffer, msg: Buffer, sig: Buffer) { - const expectedSig = await hmacSha256Sign(key, msg) - return equalConstTime(expectedSig, sig) + const expectedSig = await hmacSha256Sign(key, msg); + return equalConstTime(expectedSig, sig); } /** @@ -115,20 +115,20 @@ async function hmacSha256Verify(key: Buffer, msg: Buffer, sig: Buffer) { * @function */ export function generatePrivate() { - let privateKey = randomBytes(32) + let privateKey = randomBytes(32); while (!isValidPrivateKey(privateKey)) { - privateKey = randomBytes(32) + privateKey = randomBytes(32); } - return privateKey + return privateKey; } export function getPublic(privateKey: Buffer) { // This function has sync API so we throw an error immediately. - assert(privateKey.length === 32, 'Bad private key') - assert(isValidPrivateKey(privateKey), 'Bad private key') + assert(privateKey.length === 32, "Bad private key"); + assert(isValidPrivateKey(privateKey), "Bad private key"); // XXX(Kagami): `elliptic.utils.encode` returns array for every // encoding except `hex`. - return Buffer.from(ec.keyFromPrivate(privateKey).getPublic('array')) + return Buffer.from(ec.keyFromPrivate(privateKey).getPublic("array")); } /** @@ -136,13 +136,13 @@ export function getPublic(privateKey: Buffer) { */ export function getPublicCompressed(privateKey: Buffer) { // jshint ignore:line - assert(privateKey.length === 32, 'Bad private key') - assert(isValidPrivateKey(privateKey), 'Bad private key') + assert(privateKey.length === 32, "Bad private key"); + assert(isValidPrivateKey(privateKey), "Bad private key"); // See https://github.com/wanderer/secp256k1-node/issues/46 - const compressed = true + const compressed = true; return Buffer.from( - ec.keyFromPrivate(privateKey).getPublic(compressed, 'array') - ) + ec.keyFromPrivate(privateKey).getPublic(compressed, "array"), + ); } // NOTE(Kagami): We don't use promise shim in Browser implementation @@ -152,86 +152,89 @@ export function getPublicCompressed(privateKey: Buffer) { // ). export function sign(privateKey: Buffer, msg: Buffer) { return new Promise(function (resolve) { - assert(privateKey.length === 32, 'Bad private key') - assert(isValidPrivateKey(privateKey), 'Bad private key') - assert(msg.length > 0, 'Message should not be empty') - assert(msg.length <= 32, 'Message is too long') - resolve(Buffer.from(ec.sign(msg, privateKey, { canonical: true }).toDER())) - }) + assert(privateKey.length === 32, "Bad private key"); + assert(isValidPrivateKey(privateKey), "Bad private key"); + assert(msg.length > 0, "Message should not be empty"); + assert(msg.length <= 32, "Message is too long"); + resolve(Buffer.from(ec.sign(msg, privateKey, { canonical: true }).toDER())); + }); } export function verify(publicKey: Buffer, msg: Buffer, sig: Buffer) { return new Promise(function (resolve, reject) { - assert(publicKey.length === 65 || publicKey.length === 33, 'Bad public key') + assert( + publicKey.length === 65 || publicKey.length === 33, + "Bad public key", + ); if (publicKey.length === 65) { - assert(publicKey[0] === 4, 'Bad public key') + assert(publicKey[0] === 4, "Bad public key"); } if (publicKey.length === 33) { - assert(publicKey[0] === 2 || publicKey[0] === 3, 'Bad public key') + assert(publicKey[0] === 2 || publicKey[0] === 3, "Bad public key"); } - assert(msg.length > 0, 'Message should not be empty') - assert(msg.length <= 32, 'Message is too long') + assert(msg.length > 0, "Message should not be empty"); + assert(msg.length <= 32, "Message is too long"); if (ec.verify(msg, sig, publicKey)) { - resolve(null) + resolve(null); } else { - reject(new Error('Bad signature')) + reject(new Error("Bad signature")); } - }) + }); } export function derive( privateKeyA: Buffer, - publicKeyB: Buffer + publicKeyB: Buffer, ): Promise { return new Promise(function (resolve) { - assert(Buffer.isBuffer(privateKeyA), 'Bad private key') - assert(Buffer.isBuffer(publicKeyB), 'Bad public key') - assert(privateKeyA.length === 32, 'Bad private key') - assert(isValidPrivateKey(privateKeyA), 'Bad private key') + assert(Buffer.isBuffer(privateKeyA), "Bad private key"); + assert(Buffer.isBuffer(publicKeyB), "Bad public key"); + assert(privateKeyA.length === 32, "Bad private key"); + assert(isValidPrivateKey(privateKeyA), "Bad private key"); assert( publicKeyB.length === 65 || publicKeyB.length === 33, - 'Bad public key' - ) + "Bad public key", + ); if (publicKeyB.length === 65) { - assert(publicKeyB[0] === 4, 'Bad public key') + assert(publicKeyB[0] === 4, "Bad public key"); } if (publicKeyB.length === 33) { - assert(publicKeyB[0] === 2 || publicKeyB[0] === 3, 'Bad public key') + assert(publicKeyB[0] === 2 || publicKeyB[0] === 3, "Bad public key"); } - const keyA = ec.keyFromPrivate(privateKeyA) - const keyB = ec.keyFromPublic(publicKeyB) - const Px = keyA.derive(keyB.getPublic()) // BN instance - resolve(Buffer.from(Px.toArray())) - }) + const keyA = ec.keyFromPrivate(privateKeyA); + const keyB = ec.keyFromPublic(publicKeyB); + const Px = keyA.derive(keyB.getPublic()); // BN instance + resolve(Buffer.from(Px.toArray())); + }); } export async function encrypt( publicKeyTo: Buffer, msg: Buffer, - opts?: { ephemPrivateKey?: Buffer; iv?: Buffer } | undefined + opts?: { ephemPrivateKey?: Buffer; iv?: Buffer } | undefined, ) { - opts = opts || {} + opts = opts || {}; // Take IV from opts or generate randomly - const iv = opts?.iv || randomBytes(16) - let ephemPrivateKey = opts?.ephemPrivateKey || randomBytes(32) + const iv = opts?.iv || randomBytes(16); + let ephemPrivateKey = opts?.ephemPrivateKey || randomBytes(32); // There is a very unlikely possibility that it is not a valid key while (!isValidPrivateKey(ephemPrivateKey)) { if (opts?.ephemPrivateKey) { - throw new Error('ephemPrivateKey is not valid') + throw new Error("ephemPrivateKey is not valid"); } - ephemPrivateKey = randomBytes(32) + ephemPrivateKey = randomBytes(32); } // Get the public key from the ephemeral private key - const ephemeralPublicKey = getPublic(ephemPrivateKey) + const ephemeralPublicKey = getPublic(ephemPrivateKey); - const hash = await sha512(await derive(ephemPrivateKey, publicKeyTo)) - const encryptionKey = hash.slice(0, 32) - const macKey = hash.slice(32) - const ciphertext = await aesCbcEncrypt(iv, encryptionKey, msg) + const hash = await sha512(await derive(ephemPrivateKey, publicKeyTo)); + const encryptionKey = hash.slice(0, 32); + const macKey = hash.slice(32); + const ciphertext = await aesCbcEncrypt(iv, encryptionKey, msg); // Get a MAC - const dataToMac = Buffer.concat([iv, ephemeralPublicKey, ciphertext]) - const mac = await hmacSha256Sign(macKey, dataToMac) + const dataToMac = Buffer.concat([iv, ephemeralPublicKey, ciphertext]); + const mac = await hmacSha256Sign(macKey, dataToMac); // Return the payload return { @@ -239,20 +242,20 @@ export async function encrypt( ephemeralPublicKey, ciphertext, mac, - } + }; } export async function decrypt(privateKey: Buffer, opts: Ecies) { - const px = await derive(privateKey, opts.ephemeralPublicKey) - const hash = await sha512(px) - const encryptionKey = hash.slice(0, 32) - const macKey = hash.slice(32) + const px = await derive(privateKey, opts.ephemeralPublicKey); + const hash = await sha512(px); + const encryptionKey = hash.slice(0, 32); + const macKey = hash.slice(32); const dataToMac = Buffer.concat([ opts.iv, opts.ephemeralPublicKey, opts.ciphertext, - ]) - assert(await hmacSha256Verify(macKey, dataToMac, opts.mac), 'Bad mac') + ]); + assert(await hmacSha256Verify(macKey, dataToMac, opts.mac), "Bad mac"); - return aesCbcDecrypt(opts.iv, encryptionKey, opts.ciphertext) + return aesCbcDecrypt(opts.iv, encryptionKey, opts.ciphertext); } diff --git a/packages/js-sdk/src/crypto/encryption.ts b/packages/js-sdk/src/crypto/encryption.ts index a4b1c6d08..3574b4f22 100644 --- a/packages/js-sdk/src/crypto/encryption.ts +++ b/packages/js-sdk/src/crypto/encryption.ts @@ -1,15 +1,15 @@ -import type { ciphertext } from '@xmtp/proto' -import Ciphertext, { AESGCMNonceSize, KDFSaltSize } from './Ciphertext' -import crypto from './crypto' +import type { ciphertext } from "@xmtp/proto"; +import Ciphertext, { AESGCMNonceSize, KDFSaltSize } from "./Ciphertext"; +import crypto from "./crypto"; -const hkdfNoInfo = new Uint8Array().buffer -const hkdfNoSalt = new Uint8Array().buffer +const hkdfNoInfo = new Uint8Array().buffer; +const hkdfNoSalt = new Uint8Array().buffer; // This is a variation of https://github.com/paulmillr/noble-secp256k1/blob/main/index.ts#L1378-L1388 // that uses `digest('SHA-256', bytes)` instead of `digest('SHA-256', bytes.buffer)` // which seems to produce different results. export async function sha256(bytes: Uint8Array): Promise { - return new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)) + return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); } // symmetric authenticated encryption of plaintext using the secret; @@ -18,118 +18,118 @@ export async function sha256(bytes: Uint8Array): Promise { export async function encrypt( plain: Uint8Array, secret: Uint8Array, - additionalData?: Uint8Array + additionalData?: Uint8Array, ): Promise { - const salt = crypto.getRandomValues(new Uint8Array(KDFSaltSize)) - const nonce = crypto.getRandomValues(new Uint8Array(AESGCMNonceSize)) - const key = await hkdf(secret, salt) + const salt = crypto.getRandomValues(new Uint8Array(KDFSaltSize)); + const nonce = crypto.getRandomValues(new Uint8Array(AESGCMNonceSize)); + const key = await hkdf(secret, salt); const encrypted: ArrayBuffer = await crypto.subtle.encrypt( aesGcmParams(nonce, additionalData), key, - plain - ) + plain, + ); return new Ciphertext({ aes256GcmHkdfSha256: { payload: new Uint8Array(encrypted), hkdfSalt: salt, gcmNonce: nonce, }, - }) + }); } // symmetric authenticated decryption of the encrypted ciphertext using the secret and additionalData export async function decrypt( encrypted: Ciphertext | ciphertext.Ciphertext, secret: Uint8Array, - additionalData?: Uint8Array + additionalData?: Uint8Array, ): Promise { if (!encrypted.aes256GcmHkdfSha256) { - throw new Error('invalid payload ciphertext') + throw new Error("invalid payload ciphertext"); } - const key = await hkdf(secret, encrypted.aes256GcmHkdfSha256.hkdfSalt) + const key = await hkdf(secret, encrypted.aes256GcmHkdfSha256.hkdfSalt); const decrypted: ArrayBuffer = await crypto.subtle.decrypt( aesGcmParams(encrypted.aes256GcmHkdfSha256.gcmNonce, additionalData), key, - encrypted.aes256GcmHkdfSha256.payload - ) - return new Uint8Array(decrypted) + encrypted.aes256GcmHkdfSha256.payload, + ); + return new Uint8Array(decrypted); } // helper for building Web Crypto API encryption parameter structure function aesGcmParams( nonce: Uint8Array, - additionalData?: Uint8Array + additionalData?: Uint8Array, ): AesGcmParams { const spec: AesGcmParams = { - name: 'AES-GCM', + name: "AES-GCM", iv: nonce, - } + }; if (additionalData) { - spec.additionalData = additionalData + spec.additionalData = additionalData; } - return spec + return spec; } // Derive AES-256-GCM key from a shared secret and salt. // Returns crypto.CryptoKey suitable for the encrypt/decrypt API async function hkdf(secret: Uint8Array, salt: Uint8Array): Promise { - const key = await crypto.subtle.importKey('raw', secret, 'HKDF', false, [ - 'deriveKey', - ]) + const key = await crypto.subtle.importKey("raw", secret, "HKDF", false, [ + "deriveKey", + ]); return crypto.subtle.deriveKey( - { name: 'HKDF', hash: 'SHA-256', salt, info: hkdfNoInfo }, + { name: "HKDF", hash: "SHA-256", salt, info: hkdfNoInfo }, key, - { name: 'AES-GCM', length: 256 }, + { name: "AES-GCM", length: 256 }, false, - ['encrypt', 'decrypt'] - ) + ["encrypt", "decrypt"], + ); } export async function hkdfHmacKey( secret: Uint8Array, - info: Uint8Array + info: Uint8Array, ): Promise { - const key = await crypto.subtle.importKey('raw', secret, 'HKDF', false, [ - 'deriveKey', - ]) + const key = await crypto.subtle.importKey("raw", secret, "HKDF", false, [ + "deriveKey", + ]); return crypto.subtle.deriveKey( - { name: 'HKDF', hash: 'SHA-256', salt: hkdfNoSalt, info }, + { name: "HKDF", hash: "SHA-256", salt: hkdfNoSalt, info }, key, - { name: 'HMAC', hash: 'SHA-256', length: 256 }, + { name: "HMAC", hash: "SHA-256", length: 256 }, true, - ['sign', 'verify'] - ) + ["sign", "verify"], + ); } export async function generateHmacSignature( secret: Uint8Array, info: Uint8Array, - message: Uint8Array + message: Uint8Array, ): Promise { - const key = await hkdfHmacKey(secret, info) - const signed = await crypto.subtle.sign('HMAC', key, message) - return new Uint8Array(signed) + const key = await hkdfHmacKey(secret, info); + const signed = await crypto.subtle.sign("HMAC", key, message); + return new Uint8Array(signed); } export async function verifyHmacSignature( key: CryptoKey, signature: Uint8Array, - message: Uint8Array + message: Uint8Array, ): Promise { - return await crypto.subtle.verify('HMAC', key, signature, message) + return await crypto.subtle.verify("HMAC", key, signature, message); } export async function exportHmacKey(key: CryptoKey): Promise { - const exported = await crypto.subtle.exportKey('raw', key) - return new Uint8Array(exported) + const exported = await crypto.subtle.exportKey("raw", key); + return new Uint8Array(exported); } export async function importHmacKey(key: Uint8Array): Promise { return crypto.subtle.importKey( - 'raw', + "raw", key, - { name: 'HMAC', hash: 'SHA-256', length: 256 }, + { name: "HMAC", hash: "SHA-256", length: 256 }, true, - ['sign', 'verify'] - ) + ["sign", "verify"], + ); } diff --git a/packages/js-sdk/src/crypto/errors.ts b/packages/js-sdk/src/crypto/errors.ts index dca646580..f8f03f7fe 100644 --- a/packages/js-sdk/src/crypto/errors.ts +++ b/packages/js-sdk/src/crypto/errors.ts @@ -1,10 +1,10 @@ -import type { PublicKey, SignedPublicKey } from './PublicKey' -import { bytesToHex } from './utils' +import type { PublicKey, SignedPublicKey } from "./PublicKey"; +import { bytesToHex } from "./utils"; export class NoMatchingPreKeyError extends Error { constructor(preKey: PublicKey | SignedPublicKey) { super( - `no pre-key matches: ${bytesToHex(preKey.secp256k1Uncompressed.bytes)}` - ) + `no pre-key matches: ${bytesToHex(preKey.secp256k1Uncompressed.bytes)}`, + ); } } diff --git a/packages/js-sdk/src/crypto/selfEncryption.browser.ts b/packages/js-sdk/src/crypto/selfEncryption.browser.ts index f3f5e3f26..aecaeebd7 100644 --- a/packages/js-sdk/src/crypto/selfEncryption.browser.ts +++ b/packages/js-sdk/src/crypto/selfEncryption.browser.ts @@ -9,37 +9,37 @@ import init, { user_preferences_decrypt, // eslint-disable-next-line camelcase user_preferences_encrypt, -} from '@xmtp/user-preferences-bindings-wasm/web' -import type { PrivateKey } from './PrivateKey' +} from "@xmtp/user-preferences-bindings-wasm/web"; +import type { PrivateKey } from "./PrivateKey"; export async function userPreferencesEncrypt( identityKey: PrivateKey, - payload: Uint8Array + payload: Uint8Array, ) { // wait for WASM to be initialized - await init() - const publicKey = identityKey.publicKey.secp256k1Uncompressed.bytes - const privateKey = identityKey.secp256k1.bytes + await init(); + const publicKey = identityKey.publicKey.secp256k1Uncompressed.bytes; + const privateKey = identityKey.secp256k1.bytes; // eslint-disable-next-line camelcase - return user_preferences_encrypt(publicKey, privateKey, payload) + return user_preferences_encrypt(publicKey, privateKey, payload); } export async function userPreferencesDecrypt( identityKey: PrivateKey, - payload: Uint8Array + payload: Uint8Array, ) { // wait for WASM to be initialized - await init() - const publicKey = identityKey.publicKey.secp256k1Uncompressed.bytes - const privateKey = identityKey.secp256k1.bytes + await init(); + const publicKey = identityKey.publicKey.secp256k1Uncompressed.bytes; + const privateKey = identityKey.secp256k1.bytes; // eslint-disable-next-line camelcase - return user_preferences_decrypt(publicKey, privateKey, payload) + return user_preferences_decrypt(publicKey, privateKey, payload); } export async function generateUserPreferencesTopic(identityKey: PrivateKey) { // wait for WASM to be initialized - await init() - const privateKey = identityKey.secp256k1.bytes + await init(); + const privateKey = identityKey.secp256k1.bytes; // eslint-disable-next-line camelcase - return generate_private_preferences_topic(privateKey) + return generate_private_preferences_topic(privateKey); } diff --git a/packages/js-sdk/src/crypto/selfEncryption.bundler.ts b/packages/js-sdk/src/crypto/selfEncryption.bundler.ts index aff53ddf3..56a6ffc82 100644 --- a/packages/js-sdk/src/crypto/selfEncryption.bundler.ts +++ b/packages/js-sdk/src/crypto/selfEncryption.bundler.ts @@ -9,31 +9,31 @@ import { user_preferences_decrypt, // eslint-disable-next-line camelcase user_preferences_encrypt, -} from '@xmtp/user-preferences-bindings-wasm/bundler' -import type { PrivateKey } from './PrivateKey' +} from "@xmtp/user-preferences-bindings-wasm/bundler"; +import type { PrivateKey } from "./PrivateKey"; export async function userPreferencesEncrypt( identityKey: PrivateKey, - payload: Uint8Array + payload: Uint8Array, ) { - const publicKey = identityKey.publicKey.secp256k1Uncompressed.bytes - const privateKey = identityKey.secp256k1.bytes + const publicKey = identityKey.publicKey.secp256k1Uncompressed.bytes; + const privateKey = identityKey.secp256k1.bytes; // eslint-disable-next-line camelcase - return user_preferences_encrypt(publicKey, privateKey, payload) + return user_preferences_encrypt(publicKey, privateKey, payload); } export async function userPreferencesDecrypt( identityKey: PrivateKey, - payload: Uint8Array + payload: Uint8Array, ) { - const publicKey = identityKey.publicKey.secp256k1Uncompressed.bytes - const privateKey = identityKey.secp256k1.bytes + const publicKey = identityKey.publicKey.secp256k1Uncompressed.bytes; + const privateKey = identityKey.secp256k1.bytes; // eslint-disable-next-line camelcase - return user_preferences_decrypt(publicKey, privateKey, payload) + return user_preferences_decrypt(publicKey, privateKey, payload); } export async function generateUserPreferencesTopic(identityKey: PrivateKey) { - const privateKey = identityKey.secp256k1.bytes + const privateKey = identityKey.secp256k1.bytes; // eslint-disable-next-line camelcase - return generate_private_preferences_topic(privateKey) + return generate_private_preferences_topic(privateKey); } diff --git a/packages/js-sdk/src/crypto/selfEncryption.ts b/packages/js-sdk/src/crypto/selfEncryption.ts index 743078c37..d37b3acbf 100644 --- a/packages/js-sdk/src/crypto/selfEncryption.ts +++ b/packages/js-sdk/src/crypto/selfEncryption.ts @@ -5,31 +5,31 @@ import { user_preferences_decrypt, // eslint-disable-next-line camelcase user_preferences_encrypt, -} from '@xmtp/user-preferences-bindings-wasm' -import type { PrivateKey } from '@/crypto/PrivateKey' +} from "@xmtp/user-preferences-bindings-wasm"; +import type { PrivateKey } from "@/crypto/PrivateKey"; export async function userPreferencesEncrypt( identityKey: PrivateKey, - payload: Uint8Array + payload: Uint8Array, ) { - const publicKey = identityKey.publicKey.secp256k1Uncompressed.bytes - const privateKey = identityKey.secp256k1.bytes + const publicKey = identityKey.publicKey.secp256k1Uncompressed.bytes; + const privateKey = identityKey.secp256k1.bytes; // eslint-disable-next-line camelcase - return user_preferences_encrypt(publicKey, privateKey, payload) + return user_preferences_encrypt(publicKey, privateKey, payload); } export async function userPreferencesDecrypt( identityKey: PrivateKey, - payload: Uint8Array + payload: Uint8Array, ) { - const publicKey = identityKey.publicKey.secp256k1Uncompressed.bytes - const privateKey = identityKey.secp256k1.bytes + const publicKey = identityKey.publicKey.secp256k1Uncompressed.bytes; + const privateKey = identityKey.secp256k1.bytes; // eslint-disable-next-line camelcase - return user_preferences_decrypt(publicKey, privateKey, payload) + return user_preferences_decrypt(publicKey, privateKey, payload); } export async function generateUserPreferencesTopic(identityKey: PrivateKey) { - const privateKey = identityKey.secp256k1.bytes + const privateKey = identityKey.secp256k1.bytes; // eslint-disable-next-line camelcase - return generate_private_preferences_topic(privateKey) + return generate_private_preferences_topic(privateKey); } diff --git a/packages/js-sdk/src/crypto/utils.ts b/packages/js-sdk/src/crypto/utils.ts index 4161eab06..93bc5c0a3 100644 --- a/packages/js-sdk/src/crypto/utils.ts +++ b/packages/js-sdk/src/crypto/utils.ts @@ -1,4 +1,4 @@ -import * as secp from '@noble/secp256k1' +import * as secp from "@noble/secp256k1"; import { getAddress, hexToBytes, @@ -6,50 +6,50 @@ import { keccak256, bytesToHex as viemBytesToHex, type Hex, -} from 'viem' +} from "viem"; -export const bytesToHex = secp.utils.bytesToHex +export const bytesToHex = secp.utils.bytesToHex; export function bytesToBase64(bytes: Uint8Array): string { - return Buffer.from(bytes).toString('base64') + return Buffer.from(bytes).toString("base64"); } export function equalBytes(b1: Uint8Array, b2: Uint8Array): boolean { if (b1.length !== b2.length) { - return false + return false; } for (let i = 0; i < b1.length; i++) { if (b1[i] !== b2[i]) { - return false + return false; } } - return true + return true; } /** * Compute the Ethereum address from uncompressed PublicKey bytes */ export function computeAddress(bytes: Uint8Array) { - const publicKey = viemBytesToHex(bytes.slice(1)) as Hex - const hash = keccak256(publicKey) - const address = hash.substring(hash.length - 40) - return getAddress(`0x${address}`) + const publicKey = viemBytesToHex(bytes.slice(1)) as Hex; + const hash = keccak256(publicKey); + const address = hash.substring(hash.length - 40); + return getAddress(`0x${address}`); } /** * Split an Ethereum signature hex string into bytes and a recovery bit */ export function splitSignature(signature: Hex) { - const eSig = hexToSignature(signature) - const r = hexToBytes(eSig.r) - const s = hexToBytes(eSig.s) - let v = Number(eSig.v) + const eSig = hexToSignature(signature); + const r = hexToBytes(eSig.r); + const s = hexToBytes(eSig.s); + let v = Number(eSig.v); if (v === 0 || v === 1) { - v += 27 + v += 27; } - const recovery = 1 - (v % 2) - const bytes = new Uint8Array(64) - bytes.set(r) - bytes.set(s, r.length) - return { bytes, recovery } + const recovery = 1 - (v % 2); + const bytes = new Uint8Array(64); + bytes.set(r); + bytes.set(s, r.length); + return { bytes, recovery }; } diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index f9e1d0cee..17a763eb6 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -1,18 +1,18 @@ -export type { Message } from './Message' -export { DecodedMessage, MessageV1, MessageV2, decodeContent } from './Message' -export type { PrivateKeyBundle } from './crypto/PrivateKeyBundle' -export { PrivateKey } from './crypto/PrivateKey' +export type { Message } from "./Message"; +export { DecodedMessage, MessageV1, MessageV2, decodeContent } from "./Message"; +export type { PrivateKeyBundle } from "./crypto/PrivateKeyBundle"; +export { PrivateKey } from "./crypto/PrivateKey"; export { PrivateKeyBundleV1, PrivateKeyBundleV2, -} from './crypto/PrivateKeyBundle' -export { default as Ciphertext } from './crypto/Ciphertext' -export { PublicKey, SignedPublicKey } from './crypto/PublicKey' +} from "./crypto/PrivateKeyBundle"; +export { default as Ciphertext } from "./crypto/Ciphertext"; +export { PublicKey, SignedPublicKey } from "./crypto/PublicKey"; export { PublicKeyBundle, SignedPublicKeyBundle, -} from './crypto/PublicKeyBundle' -export { default as Signature } from './crypto/Signature' +} from "./crypto/PublicKeyBundle"; +export { default as Signature } from "./crypto/Signature"; export { encrypt, decrypt, @@ -21,9 +21,9 @@ export { hkdfHmacKey, importHmacKey, verifyHmacSignature, -} from './crypto/encryption' -export { default as Stream } from './Stream' -export type { Signer } from './types/Signer' +} from "./crypto/encryption"; +export { default as Stream } from "./Stream"; +export type { Signer } from "./types/Signer"; export type { ClientOptions, ListMessagesOptions, @@ -34,15 +34,15 @@ export type { KeyStoreOptions, LegacyOptions, XmtpEnv, -} from './Client' +} from "./Client"; export { default as Client, defaultKeystoreProviders, Compression, -} from './Client' -export type { Conversation } from '@/conversations/Conversation' -export { ConversationV1, ConversationV2 } from '@/conversations/Conversation' -export { default as Conversations } from '@/conversations/Conversations' +} from "./Client"; +export type { Conversation } from "@/conversations/Conversation"; +export { ConversationV1, ConversationV2 } from "@/conversations/Conversation"; +export { default as Conversations } from "@/conversations/Conversations"; export type { ApiClient, QueryParams, @@ -55,12 +55,12 @@ export type { SubscribeCallback, UnsubscribeFn, OnConnectionLostCallback, -} from './ApiClient' -export { default as HttpApiClient, ApiUrls, SortDirection } from './ApiClient' -export type { Authenticator } from '@/authn/interfaces' -export { default as LocalAuthenticator } from '@/authn/LocalAuthenticator' -export { default as AuthCache } from '@/authn/AuthCache' -export { retry, mapPaginatedStream } from './utils/async' +} from "./ApiClient"; +export { default as HttpApiClient, ApiUrls, SortDirection } from "./ApiClient"; +export type { Authenticator } from "@/authn/interfaces"; +export { default as LocalAuthenticator } from "@/authn/LocalAuthenticator"; +export { default as AuthCache } from "@/authn/AuthCache"; +export { retry, mapPaginatedStream } from "./utils/async"; export { buildContentTopic, buildDirectMessageTopic, @@ -69,10 +69,10 @@ export { buildUserIntroTopic, buildUserInviteTopic, buildUserPrivateStoreTopic, -} from './utils/topic' -export { nsToDate, dateToNs, fromNanoString, toNanoString } from './utils/date' -export type { Keystore, TopicData } from './keystore/interfaces' -export { default as InMemoryKeystore } from './keystore/InMemoryKeystore' +} from "./utils/topic"; +export { nsToDate, dateToNs, fromNanoString, toNanoString } from "./utils/date"; +export type { Keystore, TopicData } from "./keystore/interfaces"; +export { default as InMemoryKeystore } from "./keystore/InMemoryKeystore"; export type { KeystoreApiDefs, KeystoreApiEntries, @@ -93,31 +93,31 @@ export type { SnapKeystoreApiResponseDecoders, SnapKeystoreInterface, SnapKeystoreInterfaceRequestValues, -} from './keystore/rpcDefinitions' +} from "./keystore/rpcDefinitions"; export { apiDefs as keystoreApiDefs, snapApiDefs as snapKeystoreApiDefs, -} from './keystore/rpcDefinitions' -export type { KeystoreProvider } from './keystore/providers/interfaces' -export { default as KeyGeneratorKeystoreProvider } from './keystore/providers/KeyGeneratorKeystoreProvider' -export { default as NetworkKeystoreProvider } from './keystore/providers/NetworkKeystoreProvider' -export { default as StaticKeystoreProvider } from './keystore/providers/StaticKeystoreProvider' -export { default as SnapProvider } from './keystore/providers/SnapProvider' -export type { Persistence } from './keystore/persistence/interface' -export { default as EncryptedPersistence } from './keystore/persistence/EncryptedPersistence' -export { default as BrowserStoragePersistence } from './keystore/persistence/BrowserStoragePersistence' -export { default as InMemoryPersistence } from './keystore/persistence/InMemoryPersistence' -export { default as PrefixedPersistence } from './keystore/persistence/PrefixedPersistence' -export type { InvitationContext } from './Invitation' -export { SealedInvitation, InvitationV1 } from './Invitation' -export { decodeContactBundle } from './ContactBundle' +} from "./keystore/rpcDefinitions"; +export type { KeystoreProvider } from "./keystore/providers/interfaces"; +export { default as KeyGeneratorKeystoreProvider } from "./keystore/providers/KeyGeneratorKeystoreProvider"; +export { default as NetworkKeystoreProvider } from "./keystore/providers/NetworkKeystoreProvider"; +export { default as StaticKeystoreProvider } from "./keystore/providers/StaticKeystoreProvider"; +export { default as SnapProvider } from "./keystore/providers/SnapProvider"; +export type { Persistence } from "./keystore/persistence/interface"; +export { default as EncryptedPersistence } from "./keystore/persistence/EncryptedPersistence"; +export { default as BrowserStoragePersistence } from "./keystore/persistence/BrowserStoragePersistence"; +export { default as InMemoryPersistence } from "./keystore/persistence/InMemoryPersistence"; +export { default as PrefixedPersistence } from "./keystore/persistence/PrefixedPersistence"; +export type { InvitationContext } from "./Invitation"; +export { SealedInvitation, InvitationV1 } from "./Invitation"; +export { decodeContactBundle } from "./ContactBundle"; export type { GetMessageContentTypeFromClient, ExtractDecodedType, -} from './types/client' +} from "./types/client"; export type { ConsentState, ConsentListEntryType, PrivatePreferencesAction, -} from './Contacts' -export { ConsentListEntry } from './Contacts' +} from "./Contacts"; +export { ConsentListEntry } from "./Contacts"; diff --git a/packages/js-sdk/src/keystore/InMemoryKeystore.ts b/packages/js-sdk/src/keystore/InMemoryKeystore.ts index f7e91d9f2..d56d8fb01 100644 --- a/packages/js-sdk/src/keystore/InMemoryKeystore.ts +++ b/packages/js-sdk/src/keystore/InMemoryKeystore.ts @@ -4,46 +4,46 @@ import { type authn, type privateKey, type signature, -} from '@xmtp/proto' -import Long from 'long' -import type { PublishParams } from '@/ApiClient' -import LocalAuthenticator from '@/authn/LocalAuthenticator' -import crypto from '@/crypto/crypto' -import { hmacSha256Sign } from '@/crypto/ecies' +} from "@xmtp/proto"; +import Long from "long"; +import type { PublishParams } from "@/ApiClient"; +import LocalAuthenticator from "@/authn/LocalAuthenticator"; +import crypto from "@/crypto/crypto"; +import { hmacSha256Sign } from "@/crypto/ecies"; import { exportHmacKey, generateHmacSignature, hkdfHmacKey, -} from '@/crypto/encryption' -import type { PrivateKey } from '@/crypto/PrivateKey' +} from "@/crypto/encryption"; +import type { PrivateKey } from "@/crypto/PrivateKey"; import { PrivateKeyBundleV2, type PrivateKeyBundleV1, -} from '@/crypto/PrivateKeyBundle' -import type { PublicKeyBundle } from '@/crypto/PublicKeyBundle' +} from "@/crypto/PrivateKeyBundle"; +import type { PublicKeyBundle } from "@/crypto/PublicKeyBundle"; import { generateUserPreferencesTopic, userPreferencesDecrypt, userPreferencesEncrypt, -} from '@/crypto/selfEncryption' -import { bytesToHex } from '@/crypto/utils' -import { InvitationV1, SealedInvitation } from '@/Invitation' +} from "@/crypto/selfEncryption"; +import { bytesToHex } from "@/crypto/utils"; +import { InvitationV1, SealedInvitation } from "@/Invitation"; import { PrivatePreferencesStore, type ActionsMap, -} from '@/keystore/privatePreferencesStore' -import type { KeystoreInterface } from '@/keystore/rpcDefinitions' -import { nsToDate } from '@/utils/date' +} from "@/keystore/privatePreferencesStore"; +import type { KeystoreInterface } from "@/keystore/rpcDefinitions"; +import { nsToDate } from "@/utils/date"; import { buildDirectMessageTopic, buildDirectMessageTopicV2, buildUserPrivatePreferencesTopic, -} from '@/utils/topic' -import { V1Store, V2Store, type AddRequest } from './conversationStores' -import { decryptV1, decryptV2, encryptV1, encryptV2 } from './encryption' -import { KeystoreError } from './errors' -import type { TopicData } from './interfaces' -import type { Persistence } from './persistence/interface' +} from "@/utils/topic"; +import { V1Store, V2Store, type AddRequest } from "./conversationStores"; +import { decryptV1, decryptV2, encryptV1, encryptV2 } from "./encryption"; +import { KeystoreError } from "./errors"; +import type { TopicData } from "./interfaces"; +import type { Persistence } from "./persistence/interface"; import { convertError, getKeyMaterial, @@ -52,55 +52,57 @@ import { toPublicKeyBundle, toSignedPublicKeyBundle, validateObject, -} from './utils' +} from "./utils"; -const { ErrorCode } = keystore +const { ErrorCode } = keystore; // Constant, 32 byte salt // DO NOT CHANGE -const INVITE_SALT = new TextEncoder().encode('__XMTP__INVITATION__SALT__XMTP__') +const INVITE_SALT = new TextEncoder().encode( + "__XMTP__INVITATION__SALT__XMTP__", +); async function deriveKey( secret: Uint8Array, - info: Uint8Array + info: Uint8Array, ): Promise { - const key = await crypto.subtle.importKey('raw', secret, 'HKDF', false, [ - 'deriveKey', - ]) + const key = await crypto.subtle.importKey("raw", secret, "HKDF", false, [ + "deriveKey", + ]); return crypto.subtle.deriveKey( - { name: 'HKDF', hash: 'SHA-256', salt: INVITE_SALT, info }, + { name: "HKDF", hash: "SHA-256", salt: INVITE_SALT, info }, key, - { name: 'AES-GCM', length: 256 }, + { name: "AES-GCM", length: 256 }, true, - ['encrypt', 'decrypt'] - ) + ["encrypt", "decrypt"], + ); } export default class InMemoryKeystore implements KeystoreInterface { - private v1Keys: PrivateKeyBundleV1 - private v2Keys: PrivateKeyBundleV2 // Do I need this? - private v1Store: V1Store - private v2Store: V2Store - private privatePreferencesStore: PrivatePreferencesStore - private authenticator: LocalAuthenticator - private accountAddress: string | undefined - private jobStatePersistence: Persistence - #privatePreferencesTopic: string | undefined + private v1Keys: PrivateKeyBundleV1; + private v2Keys: PrivateKeyBundleV2; // Do I need this? + private v1Store: V1Store; + private v2Store: V2Store; + private privatePreferencesStore: PrivatePreferencesStore; + private authenticator: LocalAuthenticator; + private accountAddress: string | undefined; + private jobStatePersistence: Persistence; + #privatePreferencesTopic: string | undefined; constructor( keys: PrivateKeyBundleV1, v1Store: V1Store, v2Store: V2Store, privatePreferencesStore: PrivatePreferencesStore, - persistence: Persistence + persistence: Persistence, ) { - this.v1Keys = keys - this.v2Keys = PrivateKeyBundleV2.fromLegacyBundle(keys) - this.v1Store = v1Store - this.v2Store = v2Store - this.privatePreferencesStore = privatePreferencesStore - this.authenticator = new LocalAuthenticator(keys.identityKey) - this.jobStatePersistence = persistence + this.v1Keys = keys; + this.v2Keys = PrivateKeyBundleV2.fromLegacyBundle(keys); + this.v1Store = v1Store; + this.v2Store = v2Store; + this.privatePreferencesStore = privatePreferencesStore; + this.authenticator = new LocalAuthenticator(keys.identityKey); + this.jobStatePersistence = persistence; } static async create(keys: PrivateKeyBundleV1, persistence: Persistence) { @@ -109,391 +111,394 @@ export default class InMemoryKeystore implements KeystoreInterface { await V1Store.create(persistence), await V2Store.create(persistence), await PrivatePreferencesStore.create(persistence), - persistence - ) + persistence, + ); } get walletAddress(): string { - return this.v1Keys.identityKey.publicKey.walletSignatureAddress() + return this.v1Keys.identityKey.publicKey.walletSignatureAddress(); } async decryptV1( - req: keystore.DecryptV1Request + req: keystore.DecryptV1Request, ): Promise { const responses = await mapAndConvertErrors( req.requests, async (req) => { - if (!validateObject(req, ['payload', 'peerKeys'], ['headerBytes'])) { - throw new KeystoreError(ErrorCode.ERROR_CODE_INVALID_INPUT, 'invalid') + if (!validateObject(req, ["payload", "peerKeys"], ["headerBytes"])) { + throw new KeystoreError( + ErrorCode.ERROR_CODE_INVALID_INPUT, + "invalid", + ); } - const { payload, peerKeys, headerBytes, isSender } = req + const { payload, peerKeys, headerBytes, isSender } = req; const decrypted = await decryptV1( this.v1Keys, toPublicKeyBundle(peerKeys), payload, headerBytes, - isSender - ) + isSender, + ); return { decrypted, - } + }; }, - keystore.ErrorCode.ERROR_CODE_UNSPECIFIED - ) + keystore.ErrorCode.ERROR_CODE_UNSPECIFIED, + ); return keystore.DecryptResponse.fromPartial({ responses, - }) + }); } async decryptV2( - req: keystore.DecryptV2Request + req: keystore.DecryptV2Request, ): Promise { const responses = await mapAndConvertErrors( req.requests, async (req) => { - if (!validateObject(req, ['payload'], ['headerBytes'])) { + if (!validateObject(req, ["payload"], ["headerBytes"])) { throw new KeystoreError( keystore.ErrorCode.ERROR_CODE_INVALID_INPUT, - 'missing required field' - ) + "missing required field", + ); } - const { payload, headerBytes, contentTopic } = req - const topicData = this.v2Store.lookup(contentTopic) + const { payload, headerBytes, contentTopic } = req; + const topicData = this.v2Store.lookup(contentTopic); if (!topicData) { // This is the wrong error type. Will add to the proto repo later throw new KeystoreError( keystore.ErrorCode.ERROR_CODE_NO_MATCHING_PREKEY, - 'no topic key' - ) + "no topic key", + ); } const decrypted = await decryptV2( payload, getKeyMaterial(topicData.invitation), - headerBytes - ) + headerBytes, + ); - return { decrypted } + return { decrypted }; }, - ErrorCode.ERROR_CODE_UNSPECIFIED - ) + ErrorCode.ERROR_CODE_UNSPECIFIED, + ); return keystore.DecryptResponse.fromPartial({ responses, - }) + }); } async encryptV1( - req: keystore.EncryptV1Request + req: keystore.EncryptV1Request, ): Promise { const responses = await mapAndConvertErrors( req.requests, async (req) => { - if (!validateObject(req, ['payload', 'recipient'], ['headerBytes'])) { + if (!validateObject(req, ["payload", "recipient"], ["headerBytes"])) { throw new KeystoreError( ErrorCode.ERROR_CODE_INVALID_INPUT, - 'missing required field' - ) + "missing required field", + ); } - const { recipient, payload, headerBytes } = req + const { recipient, payload, headerBytes } = req; return { encrypted: await encryptV1( this.v1Keys, toPublicKeyBundle(recipient), payload, - headerBytes + headerBytes, ), - } + }; }, - ErrorCode.ERROR_CODE_UNSPECIFIED - ) + ErrorCode.ERROR_CODE_UNSPECIFIED, + ); return keystore.EncryptResponse.fromPartial({ responses, - }) + }); } async createAuthToken({ timestampNs, }: keystore.CreateAuthTokenRequest): Promise { return this.authenticator.createToken( - timestampNs ? nsToDate(timestampNs) : undefined - ) + timestampNs ? nsToDate(timestampNs) : undefined, + ); } async selfEncrypt( - req: keystore.SelfEncryptRequest + req: keystore.SelfEncryptRequest, ): Promise { const responses = await mapAndConvertErrors( req.requests, async (req) => { - const { payload } = req + const { payload } = req; if (!payload) { throw new KeystoreError( ErrorCode.ERROR_CODE_INVALID_INPUT, - 'Missing field payload' - ) + "Missing field payload", + ); } return { encrypted: await userPreferencesEncrypt( this.v1Keys.identityKey, - payload + payload, ), - } + }; }, - ErrorCode.ERROR_CODE_INVALID_INPUT - ) + ErrorCode.ERROR_CODE_INVALID_INPUT, + ); return keystore.SelfEncryptResponse.fromPartial({ responses, - }) + }); } async selfDecrypt( - req: keystore.SelfDecryptRequest + req: keystore.SelfDecryptRequest, ): Promise { const responses = await mapAndConvertErrors( req.requests, async (req) => { - const { payload } = req + const { payload } = req; if (!payload) { throw new KeystoreError( ErrorCode.ERROR_CODE_INVALID_INPUT, - 'Missing field payload' - ) + "Missing field payload", + ); } return { decrypted: await userPreferencesDecrypt( this.v1Keys.identityKey, - payload + payload, ), - } + }; }, - ErrorCode.ERROR_CODE_INVALID_INPUT - ) + ErrorCode.ERROR_CODE_INVALID_INPUT, + ); return keystore.DecryptResponse.fromPartial({ responses, - }) + }); } async getPrivatePreferencesTopicIdentifier(): Promise { const identifier = await generateUserPreferencesTopic( - this.v1Keys.identityKey - ) + this.v1Keys.identityKey, + ); return keystore.GetPrivatePreferencesTopicIdentifierResponse.fromPartial({ identifier, - }) + }); } async encryptV2( - req: keystore.EncryptV2Request + req: keystore.EncryptV2Request, ): Promise { const responses = await mapAndConvertErrors( req.requests, async (req) => { - if (!validateObject(req, ['payload'], ['headerBytes'])) { + if (!validateObject(req, ["payload"], ["headerBytes"])) { throw new KeystoreError( ErrorCode.ERROR_CODE_INVALID_INPUT, - 'missing required field' - ) + "missing required field", + ); } - const { payload, headerBytes, contentTopic } = req + const { payload, headerBytes, contentTopic } = req; - const topicData = this.v2Store.lookup(contentTopic) + const topicData = this.v2Store.lookup(contentTopic); if (!topicData) { throw new KeystoreError( ErrorCode.ERROR_CODE_NO_MATCHING_PREKEY, - 'no topic key' - ) + "no topic key", + ); } - const keyMaterial = getKeyMaterial(topicData.invitation) - const ciphertext = await encryptV2(payload, keyMaterial, headerBytes) + const keyMaterial = getKeyMaterial(topicData.invitation); + const ciphertext = await encryptV2(payload, keyMaterial, headerBytes); const thirtyDayPeriodsSinceEpoch = Math.floor( - Date.now() / 1000 / 60 / 60 / 24 / 30 - ) - const info = `${thirtyDayPeriodsSinceEpoch}-${await this.getAccountAddress()}` + Date.now() / 1000 / 60 / 60 / 24 / 30, + ); + const info = `${thirtyDayPeriodsSinceEpoch}-${await this.getAccountAddress()}`; const hmac = await generateHmacSignature( keyMaterial, new TextEncoder().encode(info), - headerBytes - ) + headerBytes, + ); return { encrypted: ciphertext, senderHmac: hmac, - } + }; }, - ErrorCode.ERROR_CODE_INVALID_INPUT - ) + ErrorCode.ERROR_CODE_INVALID_INPUT, + ); return keystore.EncryptResponse.fromPartial({ responses, - }) + }); } async saveInvites( - req: keystore.SaveInvitesRequest + req: keystore.SaveInvitesRequest, ): Promise { - const toAdd: AddRequest[] = [] + const toAdd: AddRequest[] = []; const responses = await mapAndConvertErrors( req.requests, async ({ payload, timestampNs }) => { - const sealed = SealedInvitation.fromBytes(payload) + const sealed = SealedInvitation.fromBytes(payload); if (sealed.v1) { - const headerTime = sealed.v1.header.createdNs + const headerTime = sealed.v1.header.createdNs; if (!headerTime.equals(timestampNs)) { - throw new Error('envelope and header timestamp mismatch') + throw new Error("envelope and header timestamp mismatch"); } const isSender = sealed.v1.header.sender.equals( - this.v2Keys.getPublicKeyBundle() - ) + this.v2Keys.getPublicKeyBundle(), + ); - const invitation = await sealed.v1.getInvitation(this.v2Keys) + const invitation = await sealed.v1.getInvitation(this.v2Keys); const topicData = { invitation, createdNs: sealed.v1.header.createdNs, peerAddress: isSender ? await sealed.v1.header.recipient.walletSignatureAddress() : await sealed.v1.header.sender.walletSignatureAddress(), - } - toAdd.push({ ...topicData, topic: invitation.topic }) + }; + toAdd.push({ ...topicData, topic: invitation.topic }); return { conversation: topicDataToV2ConversationReference(topicData), - } + }; } }, - ErrorCode.ERROR_CODE_INVALID_INPUT - ) + ErrorCode.ERROR_CODE_INVALID_INPUT, + ); - await this.v2Store.add(toAdd) + await this.v2Store.add(toAdd); return keystore.SaveInvitesResponse.fromPartial({ responses, - }) + }); } async createInvite( - req: keystore.CreateInviteRequest + req: keystore.CreateInviteRequest, ): Promise { try { - if (!validateObject(req, ['recipient'], [])) { + if (!validateObject(req, ["recipient"], [])) { throw new KeystoreError( ErrorCode.ERROR_CODE_INVALID_INPUT, - 'missing recipient' - ) + "missing recipient", + ); } - const created = nsToDate(req.createdNs) - const recipient = toSignedPublicKeyBundle(req.recipient) - const myAddress = await this.getAccountAddress() - const theirAddress = await recipient.walletSignatureAddress() + const created = nsToDate(req.createdNs); + const recipient = toSignedPublicKeyBundle(req.recipient); + const myAddress = await this.getAccountAddress(); + const theirAddress = await recipient.walletSignatureAddress(); const secret = await this.v2Keys.sharedSecret( recipient, this.v2Keys.getCurrentPreKey().publicKey, - myAddress < theirAddress - ) + myAddress < theirAddress, + ); - const sortedAddresses = [myAddress, theirAddress].sort() + const sortedAddresses = [myAddress, theirAddress].sort(); const msgString = - (req.context?.conversationId || '') + sortedAddresses.join() + (req.context?.conversationId || "") + sortedAddresses.join(); - const msgBytes = new TextEncoder().encode(msgString) + const msgBytes = new TextEncoder().encode(msgString); const topic = bytesToHex( - await hmacSha256Sign(Buffer.from(secret), Buffer.from(msgBytes)) - ) + await hmacSha256Sign(Buffer.from(secret), Buffer.from(msgBytes)), + ); const infoString = [ - '0', // sequence number + "0", // sequence number ...sortedAddresses, - ].join('|') - const info = new TextEncoder().encode(infoString) - const derivedKey = await deriveKey(secret, info) + ].join("|"); + const info = new TextEncoder().encode(infoString); + const derivedKey = await deriveKey(secret, info); const keyMaterial = new Uint8Array( - await crypto.subtle.exportKey('raw', derivedKey) - ) + await crypto.subtle.exportKey("raw", derivedKey), + ); const invitation = new InvitationV1({ topic: buildDirectMessageTopicV2(topic), aes256GcmHkdfSha256: { keyMaterial }, context: req.context, consentProof: req.consentProof, - }) + }); const sealed = await SealedInvitation.createV1({ sender: this.v2Keys, recipient, created, invitation, - }) + }); const topicData = { invitation, topic: invitation.topic, createdNs: req.createdNs, peerAddress: await recipient.walletSignatureAddress(), - } + }; - await this.v2Store.add([topicData]) + await this.v2Store.add([topicData]); return keystore.CreateInviteResponse.fromPartial({ conversation: topicDataToV2ConversationReference(topicData), payload: sealed.toBytes(), - }) + }); } catch (e) { - throw convertError(e as Error, ErrorCode.ERROR_CODE_INVALID_INPUT) + throw convertError(e as Error, ErrorCode.ERROR_CODE_INVALID_INPUT); } } async signDigest( - req: keystore.SignDigestRequest + req: keystore.SignDigestRequest, ): Promise { - if (!validateObject(req, ['digest'], [])) { + if (!validateObject(req, ["digest"], [])) { throw new KeystoreError( ErrorCode.ERROR_CODE_INVALID_INPUT, - 'missing required field' - ) + "missing required field", + ); } - const { digest, identityKey, prekeyIndex } = req - let key: PrivateKey + const { digest, identityKey, prekeyIndex } = req; + let key: PrivateKey; if (identityKey) { - key = this.v1Keys.identityKey + key = this.v1Keys.identityKey; } else if ( - typeof prekeyIndex !== 'undefined' && + typeof prekeyIndex !== "undefined" && Number.isInteger(prekeyIndex) ) { - key = this.v1Keys.preKeys[prekeyIndex] + key = this.v1Keys.preKeys[prekeyIndex]; if (!key) { throw new KeystoreError( ErrorCode.ERROR_CODE_NO_MATCHING_PREKEY, - 'no prekey found' - ) + "no prekey found", + ); } } else { throw new KeystoreError( ErrorCode.ERROR_CODE_INVALID_INPUT, - 'must specifify identityKey or prekeyIndex' - ) + "must specifify identityKey or prekeyIndex", + ); } - return key.sign(digest) + return key.sign(digest); } async saveV1Conversations({ @@ -505,49 +510,49 @@ export default class InMemoryKeystore implements KeystoreInterface { peerAddress: convo.peerAddress, createdNs: convo.createdNs, invitation: undefined, - })) - ) + })), + ); - return {} + return {}; } async getV1Conversations(): Promise { const convos = this.v1Store.topics.map( - this.topicDataToV1ConversationReference.bind(this) - ) + this.topicDataToV1ConversationReference.bind(this), + ); - return { conversations: convos } + return { conversations: convos }; } async getV2Conversations(): Promise { const convos = this.v2Store.topics.map((invite) => - topicDataToV2ConversationReference(invite as TopicData) - ) + topicDataToV2ConversationReference(invite as TopicData), + ); convos.sort((a, b) => - a.createdNs.div(1_000_000).sub(b.createdNs.div(1_000_000)).toNumber() - ) + a.createdNs.div(1_000_000).sub(b.createdNs.div(1_000_000)).toNumber(), + ); return keystore.GetConversationsResponse.fromPartial({ conversations: convos, - }) + }); } async getPublicKeyBundle(): Promise { - return this.v1Keys.getPublicKeyBundle() + return this.v1Keys.getPublicKeyBundle(); } async getPrivateKeyBundle(): Promise { - return this.v1Keys + return this.v1Keys; } async getAccountAddress(): Promise { if (!this.accountAddress) { this.accountAddress = await this.v2Keys .getPublicKeyBundle() - .walletSignatureAddress() + .walletSignatureAddress(); } - return this.accountAddress + return this.accountAddress; } async getRefreshJob({ @@ -556,32 +561,32 @@ export default class InMemoryKeystore implements KeystoreInterface { if (jobType === keystore.JobType.JOB_TYPE_UNSPECIFIED) { throw new KeystoreError( ErrorCode.ERROR_CODE_INVALID_INPUT, - 'invalid job type' - ) + "invalid job type", + ); } - const lastRunTime = await this.getLastRunTime(jobType) + const lastRunTime = await this.getLastRunTime(jobType); return keystore.GetRefreshJobResponse.fromPartial({ lastRunNs: lastRunTime || Long.fromNumber(0), - }) + }); } async setRefreshJob({ jobType, lastRunNs, }: keystore.SetRefeshJobRequest): Promise { - const key = await this.buildJobStorageKey(jobType) + const key = await this.buildJobStorageKey(jobType); await this.jobStatePersistence.setItem( key, - Uint8Array.from(lastRunNs.toBytes()) - ) + Uint8Array.from(lastRunNs.toBytes()), + ); - return {} + return {}; } private topicDataToV1ConversationReference( - data: keystore.TopicMap_TopicData + data: keystore.TopicMap_TopicData, ) { return { peerAddress: data.peerAddress, @@ -589,96 +594,96 @@ export default class InMemoryKeystore implements KeystoreInterface { topic: buildDirectMessageTopic(data.peerAddress, this.walletAddress), context: undefined, consentProofPayload: undefined, - } + }; } private buildJobStorageKey(jobType: keystore.JobType): string { - return `refreshJob/${jobType.toString()}` + return `refreshJob/${jobType.toString()}`; } private async getLastRunTime( - jobType: keystore.JobType + jobType: keystore.JobType, ): Promise { const bytes = await this.jobStatePersistence.getItem( - this.buildJobStorageKey(jobType) - ) + this.buildJobStorageKey(jobType), + ); if (!bytes || !bytes.length) { - return + return; } - return Long.fromBytes([...bytes]) + return Long.fromBytes([...bytes]); } // This method is not defined as part of the standard Keystore API, but is available // on the InMemoryKeystore to support legacy use-cases. lookupTopic(topic: string) { - return this.v2Store.lookup(topic) + return this.v2Store.lookup(topic); } async getV2ConversationHmacKeys( - req?: keystore.GetConversationHmacKeysRequest + req?: keystore.GetConversationHmacKeysRequest, ): Promise { const thirtyDayPeriodsSinceEpoch = Math.floor( - Date.now() / 1000 / 60 / 60 / 24 / 30 - ) + Date.now() / 1000 / 60 / 60 / 24 / 30, + ); - const hmacKeys: keystore.GetConversationHmacKeysResponse['hmacKeys'] = {} + const hmacKeys: keystore.GetConversationHmacKeysResponse["hmacKeys"] = {}; - let topics = this.v2Store.topics + let topics = this.v2Store.topics; // if specific topics are requested, only include those topics if (req?.topics) { topics = topics.filter( (topicData) => topicData.invitation !== undefined && - req.topics.includes(topicData.invitation.topic) - ) + req.topics.includes(topicData.invitation.topic), + ); } await Promise.all( topics.map(async (topicData) => { if (topicData.invitation?.topic) { - const keyMaterial = getKeyMaterial(topicData.invitation) + const keyMaterial = getKeyMaterial(topicData.invitation); const values = await Promise.all( [ thirtyDayPeriodsSinceEpoch - 1, thirtyDayPeriodsSinceEpoch, thirtyDayPeriodsSinceEpoch + 1, ].map(async (value) => { - const info = `${value}-${await this.getAccountAddress()}` + const info = `${value}-${await this.getAccountAddress()}`; const hmacKey = await hkdfHmacKey( keyMaterial, - new TextEncoder().encode(info) - ) + new TextEncoder().encode(info), + ); return { thirtyDayPeriodsSinceEpoch: value, // convert CryptoKey to Uint8Array to match the proto hmacKey: await exportHmacKey(hmacKey), - } - }) - ) + }; + }), + ); hmacKeys[topicData.invitation.topic] = { values, - } + }; } - }) - ) + }), + ); - return { hmacKeys } + return { hmacKeys }; } async getPrivatePreferencesTopic(): Promise { if (!this.#privatePreferencesTopic) { - const { identifier } = await this.getPrivatePreferencesTopicIdentifier() + const { identifier } = await this.getPrivatePreferencesTopicIdentifier(); this.#privatePreferencesTopic = - buildUserPrivatePreferencesTopic(identifier) + buildUserPrivatePreferencesTopic(identifier); } - return this.#privatePreferencesTopic + return this.#privatePreferencesTopic; } async createPrivatePreference( - action: privatePreferences.PrivatePreferencesAction + action: privatePreferences.PrivatePreferencesAction, ) { // encrypt action payload // there should only be one response @@ -689,31 +694,31 @@ export default class InMemoryKeystore implements KeystoreInterface { privatePreferences.PrivatePreferencesAction.encode(action).finish(), }, ], - }) + }); // encrypted message const messages = responses.reduce((result, response) => { return response.result?.encrypted ? result.concat(response.result?.encrypted) - : result - }, [] as Uint8Array[]) + : result; + }, [] as Uint8Array[]); - const contentTopic = await this.getPrivatePreferencesTopic() - const timestamp = new Date() + const contentTopic = await this.getPrivatePreferencesTopic(); + const timestamp = new Date(); // return envelopes to publish return messages.map((message) => ({ contentTopic, message, timestamp, - })) as PublishParams[] + })) as PublishParams[]; } getPrivatePreferences() { - return this.privatePreferencesStore.actions + return this.privatePreferencesStore.actions; } savePrivatePreferences(data: ActionsMap) { - return this.privatePreferencesStore.add(data) + return this.privatePreferencesStore.add(data); } } diff --git a/packages/js-sdk/src/keystore/SnapKeystore.ts b/packages/js-sdk/src/keystore/SnapKeystore.ts index 33af6dba1..840947210 100644 --- a/packages/js-sdk/src/keystore/SnapKeystore.ts +++ b/packages/js-sdk/src/keystore/SnapKeystore.ts @@ -1,42 +1,42 @@ -import type { XmtpEnv } from '@/Client' +import type { XmtpEnv } from "@/Client"; import { snapApiDefs, type SnapKeystoreApiEntries, type SnapKeystoreApiRequestValues, type SnapKeystoreInterface, -} from './rpcDefinitions' -import { snapRPC, type SnapMeta } from './snapHelpers' +} from "./rpcDefinitions"; +import { snapRPC, type SnapMeta } from "./snapHelpers"; export function SnapKeystore( walletAddress: string, env: XmtpEnv, - snapId: string + snapId: string, ) { - const generatedMethods: Partial = {} + const generatedMethods: Partial = {}; const snapMeta: SnapMeta = { walletAddress, env, - } + }; for (const [method, rpc] of Object.entries( - snapApiDefs + snapApiDefs, ) as SnapKeystoreApiEntries) { generatedMethods[method] = async (req?: SnapKeystoreApiRequestValues) => { if (!rpc.req) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - return snapRPC(method, rpc, undefined, snapMeta, snapId) as any + return snapRPC(method, rpc, undefined, snapMeta, snapId) as any; } // eslint-disable-next-line @typescript-eslint/no-explicit-any - return snapRPC(method, rpc, req as any, snapMeta, snapId) as any - } + return snapRPC(method, rpc, req as any, snapMeta, snapId) as any; + }; } return { ...generatedMethods, // Don't bother calling the keystore, since we already have the wallet address async getAccountAddress() { - return walletAddress + return walletAddress; }, - } as SnapKeystoreInterface + } as SnapKeystoreInterface; } diff --git a/packages/js-sdk/src/keystore/conversationStores.ts b/packages/js-sdk/src/keystore/conversationStores.ts index c28d69bc6..b82212f2b 100644 --- a/packages/js-sdk/src/keystore/conversationStores.ts +++ b/packages/js-sdk/src/keystore/conversationStores.ts @@ -1,52 +1,52 @@ -import { keystore, type invitation } from '@xmtp/proto' -import { Mutex } from 'async-mutex' -import type Long from 'long' -import { numberToUint8Array, uint8ArrayToNumber } from '@/utils/bytes' -import type { Persistence } from './persistence/interface' -import { isCompleteTopicData, topicDataToMap } from './utils' +import { keystore, type invitation } from "@xmtp/proto"; +import { Mutex } from "async-mutex"; +import type Long from "long"; +import { numberToUint8Array, uint8ArrayToNumber } from "@/utils/bytes"; +import type { Persistence } from "./persistence/interface"; +import { isCompleteTopicData, topicDataToMap } from "./utils"; export type AddRequest = { - topic: string - createdNs: Long - peerAddress: string - invitation: invitation.InvitationV1 | undefined -} + topic: string; + createdNs: Long; + peerAddress: string; + invitation: invitation.InvitationV1 | undefined; +}; -const INVITE_STORAGE_KEY = 'invitations/v1' -const V1_STORAGE_KEY = 'conversation-v1/v1' +const INVITE_STORAGE_KEY = "invitations/v1"; +const V1_STORAGE_KEY = "conversation-v1/v1"; /** * V2Store holds a simple map of topic -> TopicData and writes to the persistence layer on changes */ export class V2Store { - private readonly persistence: Persistence - private readonly persistenceKey: string - private readonly mutex: Mutex - private readonly topicMap: Map - private revision: number + private readonly persistence: Persistence; + private readonly persistenceKey: string; + private readonly mutex: Mutex; + private readonly topicMap: Map; + private revision: number; constructor( persistence: Persistence, persistenceKey: string, - initialData: Map = new Map() + initialData: Map = new Map(), ) { - this.persistenceKey = persistenceKey - this.persistence = persistence - this.revision = 0 - this.mutex = new Mutex() - this.topicMap = initialData + this.persistenceKey = persistenceKey; + this.persistence = persistence; + this.revision = 0; + this.mutex = new Mutex(); + this.topicMap = initialData; } get revisionKey(): string { - return this.persistenceKey + '/revision' + return this.persistenceKey + "/revision"; } static async create(persistence: Persistence): Promise { - const persistenceKey = INVITE_STORAGE_KEY + const persistenceKey = INVITE_STORAGE_KEY; - const v2Store = new V2Store(persistence, persistenceKey) - await v2Store.refresh() - return v2Store + const v2Store = new V2Store(persistence, persistenceKey); + await v2Store.refresh(); + return v2Store; } protected validate(topicData: AddRequest): boolean { @@ -54,93 +54,96 @@ export class V2Store { !!topicData.topic && topicData.topic.length > 0 && isCompleteTopicData(topicData) - ) + ); } async refresh() { - const currentRevision = await this.getRevision() + const currentRevision = await this.getRevision(); if (currentRevision > this.revision) { for (const [topic, data] of await this.loadFromPersistence()) { - this.topicMap.set(topic, data) + this.topicMap.set(topic, data); } } - this.revision = currentRevision + this.revision = currentRevision; } async getRevision(): Promise { - const data = await this.persistence.getItem(this.revisionKey) + const data = await this.persistence.getItem(this.revisionKey); if (!data) { - return 0 + return 0; } - return uint8ArrayToNumber(data) + return uint8ArrayToNumber(data); } async setRevision(number: number) { - await this.persistence.setItem(this.revisionKey, numberToUint8Array(number)) + await this.persistence.setItem( + this.revisionKey, + numberToUint8Array(number), + ); } async loadFromPersistence(): Promise< Map > { - const rawData = await this.persistence.getItem(this.persistenceKey) + const rawData = await this.persistence.getItem(this.persistenceKey); if (!rawData) { - return new Map() + return new Map(); } - return topicDataToMap(keystore.TopicMap.decode(rawData)) + return topicDataToMap(keystore.TopicMap.decode(rawData)); } async store() { - await this.persistence.setItem(this.persistenceKey, this.toBytes()) - this.revision++ - await this.setRevision(this.revision) + await this.persistence.setItem(this.persistenceKey, this.toBytes()); + this.revision++; + await this.setRevision(this.revision); } async add(topicData: AddRequest[]): Promise { await this.mutex.runExclusive(async () => { - await this.refresh() - let isDirty = false + await this.refresh(); + let isDirty = false; for (const row of topicData) { if (!this.validate(row)) { - console.warn('Invalid topic data', row.topic) - continue + console.warn("Invalid topic data", row.topic); + continue; } - const { topic, ...data } = row + const { topic, ...data } = row; // This will not overwrite any existing values. First invite found in the store for a given topic will always be used // Duplicates do not throw errors if (!this.topicMap.has(topic)) { - this.topicMap.set(topic, data) - isDirty = true + this.topicMap.set(topic, data); + isDirty = true; } } // Only write to persistence once, and only if we have added new invites if (isDirty) { - await this.store() + await this.store(); } - }) + }); } get topics(): keystore.TopicMap_TopicData[] { - return [...this.topicMap.values()] + return [...this.topicMap.values()]; } lookup(topic: string): keystore.TopicMap_TopicData | undefined { - return this.topicMap.get(topic) + return this.topicMap.get(topic); } private toBytes(): Uint8Array { return keystore.TopicMap.encode({ topics: Object.fromEntries(this.topicMap), - }).finish() + }).finish(); } } export class V1Store extends V2Store { static async create(persistence: Persistence): Promise { - const persistenceKey = V1_STORAGE_KEY - const v1Store = new V1Store(persistence, persistenceKey) - await v1Store.refresh() + const persistenceKey = V1_STORAGE_KEY; + const v1Store = new V1Store(persistence, persistenceKey); + await v1Store.refresh(); - return v1Store + return v1Store; } protected override validate(topicData: AddRequest) { @@ -148,6 +151,6 @@ export class V1Store extends V2Store { topicData.topic && topicData.topic.length && topicData.peerAddress?.length > 0 - ) + ); } } diff --git a/packages/js-sdk/src/keystore/encryption.ts b/packages/js-sdk/src/keystore/encryption.ts index e461f729a..dbcd747d8 100644 --- a/packages/js-sdk/src/keystore/encryption.ts +++ b/packages/js-sdk/src/keystore/encryption.ts @@ -1,47 +1,47 @@ -import type { ciphertext } from '@xmtp/proto' -import { decrypt, encrypt } from '@/crypto/encryption' -import type { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import type { PublicKeyBundle } from '@/crypto/PublicKeyBundle' +import type { ciphertext } from "@xmtp/proto"; +import { decrypt, encrypt } from "@/crypto/encryption"; +import type { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import type { PublicKeyBundle } from "@/crypto/PublicKeyBundle"; export const decryptV1 = async ( myKeys: PrivateKeyBundleV1, peerKeys: PublicKeyBundle, ciphertext: ciphertext.Ciphertext, headerBytes: Uint8Array, - isSender: boolean + isSender: boolean, ): Promise => { const secret = await myKeys.sharedSecret( peerKeys, myKeys.getCurrentPreKey().publicKey, // assumes that the current preKey is what was used to encrypt - !isSender - ) + !isSender, + ); - return decrypt(ciphertext, secret, headerBytes) -} + return decrypt(ciphertext, secret, headerBytes); +}; export const encryptV1 = async ( keys: PrivateKeyBundleV1, recipient: PublicKeyBundle, message: Uint8Array, - headerBytes: Uint8Array + headerBytes: Uint8Array, ): Promise => { const secret = await keys.sharedSecret( recipient, keys.getCurrentPreKey().publicKey, - false // assumes that the sender is the party doing the encrypting - ) + false, // assumes that the sender is the party doing the encrypting + ); - return encrypt(message, secret, headerBytes) -} + return encrypt(message, secret, headerBytes); +}; export const decryptV2 = ( ciphertext: ciphertext.Ciphertext, secret: Uint8Array, - headerBytes: Uint8Array -) => decrypt(ciphertext, secret, headerBytes) + headerBytes: Uint8Array, +) => decrypt(ciphertext, secret, headerBytes); export const encryptV2 = ( payload: Uint8Array, secret: Uint8Array, - headerBytes: Uint8Array -) => encrypt(payload, secret, headerBytes) + headerBytes: Uint8Array, +) => encrypt(payload, secret, headerBytes); diff --git a/packages/js-sdk/src/keystore/errors.ts b/packages/js-sdk/src/keystore/errors.ts index e870f86e1..7dbb2a8b0 100644 --- a/packages/js-sdk/src/keystore/errors.ts +++ b/packages/js-sdk/src/keystore/errors.ts @@ -1,10 +1,10 @@ -import type { keystore } from '@xmtp/proto' +import type { keystore } from "@xmtp/proto"; export class KeystoreError extends Error implements keystore.KeystoreError { - code: keystore.ErrorCode + code: keystore.ErrorCode; constructor(code: keystore.ErrorCode, message: string) { - super(message) - this.code = code + super(message); + this.code = code; } } diff --git a/packages/js-sdk/src/keystore/interfaces.ts b/packages/js-sdk/src/keystore/interfaces.ts index 6817d224c..718c98b8d 100644 --- a/packages/js-sdk/src/keystore/interfaces.ts +++ b/packages/js-sdk/src/keystore/interfaces.ts @@ -4,8 +4,8 @@ import type { privateKey, publicKey, signature, -} from '@xmtp/proto' -import type { WithoutUndefined } from '@/utils/typedefs' +} from "@xmtp/proto"; +import type { WithoutUndefined } from "@/utils/typedefs"; /** * A Keystore is responsible for holding the user's XMTP private keys and using them to encrypt/decrypt/sign messages. @@ -16,99 +16,99 @@ export interface Keystore { /** * Decrypt a batch of V1 messages */ - decryptV1(req: keystore.DecryptV1Request): Promise + decryptV1(req: keystore.DecryptV1Request): Promise; /** * Decrypt a batch of V2 messages */ - decryptV2(req: keystore.DecryptV2Request): Promise + decryptV2(req: keystore.DecryptV2Request): Promise; /** * Encrypt a batch of V1 messages */ - encryptV1(req: keystore.EncryptV1Request): Promise + encryptV1(req: keystore.EncryptV1Request): Promise; /** * Encrypt a batch of V2 messages */ - encryptV2(req: keystore.EncryptV2Request): Promise + encryptV2(req: keystore.EncryptV2Request): Promise; /** * Take a batch of invite messages and store the `TopicKeys` for later use in decrypting messages */ saveInvites( - req: keystore.SaveInvitesRequest - ): Promise + req: keystore.SaveInvitesRequest, + ): Promise; /** * Create a sealed/encrypted invite and store the Topic keys in the Keystore for later use. * The returned invite payload must be sent to the network for the other party to be able to communicate. */ createInvite( - req: keystore.CreateInviteRequest - ): Promise + req: keystore.CreateInviteRequest, + ): Promise; /** * Create an XMTP auth token to be used as a header on XMTP API requests */ - createAuthToken(req: keystore.CreateAuthTokenRequest): Promise + createAuthToken(req: keystore.CreateAuthTokenRequest): Promise; /** * Sign the provided digest with either the `IdentityKey` or a specified `PreKey` */ - signDigest(req: keystore.SignDigestRequest): Promise + signDigest(req: keystore.SignDigestRequest): Promise; /** * Get a refresh job from the persistence */ getRefreshJob( - req: keystore.GetRefreshJobRequest - ): Promise + req: keystore.GetRefreshJobRequest, + ): Promise; /** * Sets the time of a refresh job */ setRefreshJob( - req: keystore.SetRefeshJobRequest - ): Promise + req: keystore.SetRefeshJobRequest, + ): Promise; /** * Save V1 Conversations */ saveV1Conversations( - req: keystore.SaveV1ConversationsRequest - ): Promise + req: keystore.SaveV1ConversationsRequest, + ): Promise; /** * Get a list of V1 conversations */ - getV1Conversations(): Promise + getV1Conversations(): Promise; /** * Get a list of V2 conversations */ - getV2Conversations(): Promise + getV2Conversations(): Promise; /** * Get the `PublicKeyBundle` associated with the Keystore's private keys */ - getPublicKeyBundle(): Promise + getPublicKeyBundle(): Promise; /** * Export the private keys. May throw an error if the keystore implementation does not allow this operation */ - getPrivateKeyBundle(): Promise + getPrivateKeyBundle(): Promise; /** * Get the account address of the wallet used to create the Keystore */ - getAccountAddress(): Promise + getAccountAddress(): Promise; /** * Encrypt a batch of messages to yourself */ selfEncrypt( - req: keystore.SelfEncryptRequest - ): Promise + req: keystore.SelfEncryptRequest, + ): Promise; /** * Decrypt a batch of messages to yourself */ selfDecrypt( - req: keystore.SelfDecryptRequest - ): Promise + req: keystore.SelfDecryptRequest, + ): Promise; /** * Get the private preferences topic identifier */ - getPrivatePreferencesTopicIdentifier(): Promise + getPrivatePreferencesTopicIdentifier(): Promise; /** * Returns the conversation HMAC keys for the current, previous, and next * 30 day periods since the epoch */ - getV2ConversationHmacKeys(): Promise + getV2ConversationHmacKeys(): Promise; } -export type TopicData = WithoutUndefined +export type TopicData = WithoutUndefined; diff --git a/packages/js-sdk/src/keystore/persistence/BrowserStoragePersistence.ts b/packages/js-sdk/src/keystore/persistence/BrowserStoragePersistence.ts index fc0492de5..a52d835ad 100644 --- a/packages/js-sdk/src/keystore/persistence/BrowserStoragePersistence.ts +++ b/packages/js-sdk/src/keystore/persistence/BrowserStoragePersistence.ts @@ -1,27 +1,27 @@ -import type { Persistence } from './interface' +import type { Persistence } from "./interface"; export default class BrowserStoragePersistence implements Persistence { - storage: Storage + storage: Storage; constructor(storage: Storage) { - this.storage = storage + this.storage = storage; } static create(): BrowserStoragePersistence { - if (typeof localStorage === 'undefined') { - throw new Error('Missing LocalStorage. Use ephemeralPersistence instead') + if (typeof localStorage === "undefined") { + throw new Error("Missing LocalStorage. Use ephemeralPersistence instead"); } - return new BrowserStoragePersistence(localStorage) + return new BrowserStoragePersistence(localStorage); } async getItem(key: string): Promise { - const value = this.storage.getItem(key) + const value = this.storage.getItem(key); if (value === null) { - return null + return null; } - return Uint8Array.from(Buffer.from(value, 'binary')) + return Uint8Array.from(Buffer.from(value, "binary")); } async setItem(key: string, value: Uint8Array): Promise { - this.storage.setItem(key, Buffer.from(value).toString('binary')) + this.storage.setItem(key, Buffer.from(value).toString("binary")); } } diff --git a/packages/js-sdk/src/keystore/persistence/EncryptedPersistence.ts b/packages/js-sdk/src/keystore/persistence/EncryptedPersistence.ts index 917b429ef..04985510e 100644 --- a/packages/js-sdk/src/keystore/persistence/EncryptedPersistence.ts +++ b/packages/js-sdk/src/keystore/persistence/EncryptedPersistence.ts @@ -1,7 +1,7 @@ -import { decrypt, encrypt, getPublic, type Ecies } from '@/crypto/ecies' -import type { PrivateKey, SignedPrivateKey } from '@/crypto/PrivateKey' -import SignedEciesCiphertext from '@/crypto/SignedEciesCiphertext' -import type { Persistence } from './interface' +import { decrypt, encrypt, getPublic, type Ecies } from "@/crypto/ecies"; +import type { PrivateKey, SignedPrivateKey } from "@/crypto/PrivateKey"; +import SignedEciesCiphertext from "@/crypto/SignedEciesCiphertext"; +import type { Persistence } from "./interface"; /** * EncryptedPersistence is a Persistence implementation that uses ECIES to encrypt all values @@ -9,64 +9,64 @@ import type { Persistence } from './interface' * A third party with access to the underlying store could write malicious data using the public key of the owner */ export default class EncryptedPersistence implements Persistence { - private persistence: Persistence - private privateKey: PrivateKey | SignedPrivateKey - private privateKeyBytes: Buffer - private publicKey: Buffer + private persistence: Persistence; + private privateKey: PrivateKey | SignedPrivateKey; + private privateKeyBytes: Buffer; + private publicKey: Buffer; constructor( persistence: Persistence, - privateKey: PrivateKey | SignedPrivateKey + privateKey: PrivateKey | SignedPrivateKey, ) { - this.persistence = persistence - this.privateKey = privateKey - this.privateKeyBytes = Buffer.from(privateKey.secp256k1.bytes) - this.publicKey = getPublic(this.privateKeyBytes) + this.persistence = persistence; + this.privateKey = privateKey; + this.privateKeyBytes = Buffer.from(privateKey.secp256k1.bytes); + this.publicKey = getPublic(this.privateKeyBytes); } async getItem(key: string): Promise { - const encrypted = await this.persistence.getItem(key) + const encrypted = await this.persistence.getItem(key); if (encrypted) { - return this.decrypt(encrypted) + return this.decrypt(encrypted); } - return null + return null; } async setItem(key: string, value: Uint8Array): Promise { - const encrypted = await this.encrypt(value) - await this.persistence.setItem(key, encrypted) + const encrypted = await this.encrypt(value); + await this.persistence.setItem(key, encrypted); } private async encrypt(value: Uint8Array): Promise { - const ecies = await encrypt(this.publicKey, Buffer.from(value)) - return this.serializeEcies(ecies) + const ecies = await encrypt(this.publicKey, Buffer.from(value)); + return this.serializeEcies(ecies); } private async decrypt(value: Uint8Array): Promise { - const ecies = await this.deserializeEcies(value) - const result = await decrypt(this.privateKeyBytes, ecies) - return Uint8Array.from(result) + const ecies = await this.deserializeEcies(value); + const result = await decrypt(this.privateKeyBytes, ecies); + return Uint8Array.from(result); } private async serializeEcies(data: Ecies): Promise { // This will create and sign a `SignedEciesCiphertext` payload based on the provided data - const protoVal = await SignedEciesCiphertext.create(data, this.privateKey) - return protoVal.toBytes() + const protoVal = await SignedEciesCiphertext.create(data, this.privateKey); + return protoVal.toBytes(); } private async deserializeEcies(data: Uint8Array): Promise { - const protoVal = SignedEciesCiphertext.fromBytes(data) + const protoVal = SignedEciesCiphertext.fromBytes(data); // Verify the signature upon deserializing if (!(await protoVal.verify(this.privateKey.publicKey))) { - throw new Error('signature validation failed') + throw new Error("signature validation failed"); } - const ecies = protoVal.ciphertext + const ecies = protoVal.ciphertext; return { ciphertext: Buffer.from(ecies.ciphertext), mac: Buffer.from(ecies.mac), iv: Buffer.from(ecies.iv), ephemeralPublicKey: Buffer.from(ecies.ephemeralPublicKey), - } + }; } } diff --git a/packages/js-sdk/src/keystore/persistence/InMemoryPersistence.ts b/packages/js-sdk/src/keystore/persistence/InMemoryPersistence.ts index c16c0f317..9b66875d9 100644 --- a/packages/js-sdk/src/keystore/persistence/InMemoryPersistence.ts +++ b/packages/js-sdk/src/keystore/persistence/InMemoryPersistence.ts @@ -1,8 +1,8 @@ -import BrowserStoragePersistence from './BrowserStoragePersistence' -import LocalStoragePonyfill from './LocalStoragePonyfill' +import BrowserStoragePersistence from "./BrowserStoragePersistence"; +import LocalStoragePonyfill from "./LocalStoragePonyfill"; export default class InMemoryPersistence extends BrowserStoragePersistence { static create() { - return new BrowserStoragePersistence(new LocalStoragePonyfill()) + return new BrowserStoragePersistence(new LocalStoragePonyfill()); } } diff --git a/packages/js-sdk/src/keystore/persistence/LocalStoragePonyfill.ts b/packages/js-sdk/src/keystore/persistence/LocalStoragePonyfill.ts index 07c341c7c..75f059d0a 100644 --- a/packages/js-sdk/src/keystore/persistence/LocalStoragePonyfill.ts +++ b/packages/js-sdk/src/keystore/persistence/LocalStoragePonyfill.ts @@ -2,27 +2,27 @@ // Borrowed from https://github.com/MitchellCash/node-storage-polyfill but implemented as a ponyfill instead of a polyfill export default class LocalStoragePonyfill implements Storage { - store: Map + store: Map; constructor() { - this.store = new Map() + this.store = new Map(); } get length(): number { - return this.store.size + return this.store.size; } clear(): void { - this.store = new Map() + this.store = new Map(); } getItem(key: string): string | null { - this.validateString(key) + this.validateString(key); if (this.store.has(key)) { - return String(this.store.get(key)) + return String(this.store.get(key)); } - return null + return null; } key(index: number): string | null { @@ -30,33 +30,33 @@ export default class LocalStoragePonyfill implements Storage { // This is the TypeError implemented in Chrome, Firefox throws "Storage.key: At least 1 // argument required, but only 0 passed". throw new TypeError( - "Failed to execute 'key' on 'Storage': 1 argument required, but only 0 present." - ) + "Failed to execute 'key' on 'Storage': 1 argument required, but only 0 present.", + ); } - const keys = [...this.store.keys()] + const keys = [...this.store.keys()]; if (index >= keys.length) { - return null + return null; } - return keys[index] + return keys[index]; } removeItem(key: string): void { - this.validateString(key) - this.store.delete(key) + this.validateString(key); + this.store.delete(key); } setItem(key: string, value: string): void { - this.validateString(key) - this.validateString(value) - this.store.set(String(key), String(value)) + this.validateString(key); + this.validateString(value); + this.store.set(String(key), String(value)); } private validateString(val: string): void { - if (!(typeof val === 'string')) { - throw new TypeError('Key must be a string') + if (!(typeof val === "string")) { + throw new TypeError("Key must be a string"); } } } diff --git a/packages/js-sdk/src/keystore/persistence/PrefixedPersistence.ts b/packages/js-sdk/src/keystore/persistence/PrefixedPersistence.ts index 01c854dc4..b55ec12e7 100644 --- a/packages/js-sdk/src/keystore/persistence/PrefixedPersistence.ts +++ b/packages/js-sdk/src/keystore/persistence/PrefixedPersistence.ts @@ -1,23 +1,23 @@ -import type { Persistence } from './interface' +import type { Persistence } from "./interface"; export default class PrefixedPersistence { - prefix: string - persistence: Persistence + prefix: string; + persistence: Persistence; constructor(prefix: string, persistence: Persistence) { - this.prefix = prefix - this.persistence = persistence + this.prefix = prefix; + this.persistence = persistence; } getItem(key: string) { - return this.persistence.getItem(this.buildKey(key)) + return this.persistence.getItem(this.buildKey(key)); } setItem(key: string, value: Uint8Array) { - return this.persistence.setItem(this.buildKey(key), value) + return this.persistence.setItem(this.buildKey(key), value); } private buildKey(key: string) { - return this.prefix + key + return this.prefix + key; } } diff --git a/packages/js-sdk/src/keystore/persistence/TopicPersistence.ts b/packages/js-sdk/src/keystore/persistence/TopicPersistence.ts index 2f0711234..1f0ed76f3 100644 --- a/packages/js-sdk/src/keystore/persistence/TopicPersistence.ts +++ b/packages/js-sdk/src/keystore/persistence/TopicPersistence.ts @@ -1,13 +1,13 @@ -import { messageApi } from '@xmtp/proto' -import type { ApiClient } from '@/ApiClient' -import type { Authenticator } from '@/authn/interfaces' -import { buildUserPrivateStoreTopic } from '@/utils/topic' -import type { Persistence } from './interface' +import { messageApi } from "@xmtp/proto"; +import type { ApiClient } from "@/ApiClient"; +import type { Authenticator } from "@/authn/interfaces"; +import { buildUserPrivateStoreTopic } from "@/utils/topic"; +import type { Persistence } from "./interface"; export default class TopicPersistence implements Persistence { - apiClient: ApiClient + apiClient: ApiClient; constructor(apiClient: ApiClient) { - this.apiClient = apiClient + this.apiClient = apiClient; } // Returns the first record in a topic if it is present. @@ -17,34 +17,34 @@ export default class TopicPersistence implements Persistence { { pageSize: 1, direction: messageApi.SortDirection.SORT_DIRECTION_DESCENDING, - } + }, )) { - if (!env.message) continue + if (!env.message) continue; try { - return Uint8Array.from(env.message) + return Uint8Array.from(env.message); } catch (e) { - console.log(e) + console.log(e); } } - return null + return null; } async setItem(key: string, value: Uint8Array): Promise { - const keys = Uint8Array.from(value) + const keys = Uint8Array.from(value); await this.apiClient.publish([ { contentTopic: this.buildTopic(key), message: keys, }, - ]) + ]); } setAuthenticator(authenticator: Authenticator): void { - this.apiClient.setAuthenticator(authenticator) + this.apiClient.setAuthenticator(authenticator); } private buildTopic(key: string): string { - return buildUserPrivateStoreTopic(key) + return buildUserPrivateStoreTopic(key); } } diff --git a/packages/js-sdk/src/keystore/persistence/interface.ts b/packages/js-sdk/src/keystore/persistence/interface.ts index 41ba5bad4..32f4350ee 100644 --- a/packages/js-sdk/src/keystore/persistence/interface.ts +++ b/packages/js-sdk/src/keystore/persistence/interface.ts @@ -1,4 +1,4 @@ export interface Persistence { - getItem(key: string): Promise - setItem(key: string, value: Uint8Array): Promise + getItem(key: string): Promise; + setItem(key: string, value: Uint8Array): Promise; } diff --git a/packages/js-sdk/src/keystore/privatePreferencesStore.ts b/packages/js-sdk/src/keystore/privatePreferencesStore.ts index bed2657e8..389564453 100644 --- a/packages/js-sdk/src/keystore/privatePreferencesStore.ts +++ b/packages/js-sdk/src/keystore/privatePreferencesStore.ts @@ -1,115 +1,115 @@ -import { keystore, type privatePreferences } from '@xmtp/proto' -import { Mutex } from 'async-mutex' -import { numberToUint8Array, uint8ArrayToNumber } from '@/utils/bytes' -import { fromNanoString } from '@/utils/date' -import type { Persistence } from './persistence/interface' +import { keystore, type privatePreferences } from "@xmtp/proto"; +import { Mutex } from "async-mutex"; +import { numberToUint8Array, uint8ArrayToNumber } from "@/utils/bytes"; +import { fromNanoString } from "@/utils/date"; +import type { Persistence } from "./persistence/interface"; -const PRIVATE_PREFERENCES_ACTIONS_STORAGE_KEY = 'private-preferences/actions' +const PRIVATE_PREFERENCES_ACTIONS_STORAGE_KEY = "private-preferences/actions"; export type ActionsMap = Map< string, privatePreferences.PrivatePreferencesAction -> +>; /** * PrivatePreferencesStore holds a mapping of message timestamp -> private * preference action and writes to the persistence layer on changes */ export class PrivatePreferencesStore { - #persistence: Persistence - #persistenceKey: string - #mutex: Mutex - #revision: number - actionsMap: ActionsMap + #persistence: Persistence; + #persistenceKey: string; + #mutex: Mutex; + #revision: number; + actionsMap: ActionsMap; constructor( persistence: Persistence, persistenceKey: string, - initialData: ActionsMap = new Map() + initialData: ActionsMap = new Map(), ) { - this.#persistenceKey = persistenceKey - this.#persistence = persistence - this.#revision = 0 - this.#mutex = new Mutex() - this.actionsMap = initialData + this.#persistenceKey = persistenceKey; + this.#persistence = persistence; + this.#revision = 0; + this.#mutex = new Mutex(); + this.actionsMap = initialData; } get revisionKey(): string { - return this.#persistenceKey + '/revision' + return this.#persistenceKey + "/revision"; } static async create( - persistence: Persistence + persistence: Persistence, ): Promise { const store = new PrivatePreferencesStore( persistence, - PRIVATE_PREFERENCES_ACTIONS_STORAGE_KEY - ) - await store.refresh() - return store + PRIVATE_PREFERENCES_ACTIONS_STORAGE_KEY, + ); + await store.refresh(); + return store; } async refresh() { - const currentRevision = await this.getRevision() + const currentRevision = await this.getRevision(); if (currentRevision > this.#revision) { - this.actionsMap = await this.loadFromPersistence() + this.actionsMap = await this.loadFromPersistence(); } - this.#revision = currentRevision + this.#revision = currentRevision; } async getRevision(): Promise { - const data = await this.#persistence.getItem(this.revisionKey) + const data = await this.#persistence.getItem(this.revisionKey); if (!data) { - return 0 + return 0; } - return uint8ArrayToNumber(data) + return uint8ArrayToNumber(data); } async setRevision(number: number) { await this.#persistence.setItem( this.revisionKey, - numberToUint8Array(number) - ) + numberToUint8Array(number), + ); } async loadFromPersistence(): Promise { - const rawData = await this.#persistence.getItem(this.#persistenceKey) + const rawData = await this.#persistence.getItem(this.#persistenceKey); if (!rawData) { - return new Map() + return new Map(); } - const data = keystore.PrivatePreferencesActionMap.decode(rawData) - const actionsMap: ActionsMap = new Map() - const entries = Object.entries(data.actions) + const data = keystore.PrivatePreferencesActionMap.decode(rawData); + const actionsMap: ActionsMap = new Map(); + const entries = Object.entries(data.actions); for (let i = 0; i < entries.length; i++) { - actionsMap.set(entries[i][0], entries[i][1]) + actionsMap.set(entries[i][0], entries[i][1]); } - return actionsMap + return actionsMap; } async store() { - await this.#persistence.setItem(this.#persistenceKey, this.#toBytes()) - this.#revision++ - await this.setRevision(this.#revision) + await this.#persistence.setItem(this.#persistenceKey, this.#toBytes()); + this.#revision++; + await this.setRevision(this.#revision); } async add(actionsMap: ActionsMap): Promise { await this.#mutex.runExclusive(async () => { - await this.refresh() - let isDirty = false - const keys = Array.from(actionsMap.keys()) + await this.refresh(); + let isDirty = false; + const keys = Array.from(actionsMap.keys()); for (let i = 0; i < keys.length; i++) { // ignore duplicate actions if (!this.actionsMap.has(keys[i])) { - this.actionsMap.set(keys[i], actionsMap.get(keys[i])!) + this.actionsMap.set(keys[i], actionsMap.get(keys[i])!); // indicate new value added - isDirty = true + isDirty = true; } } // only write to persistence if new values were added if (isDirty) { - await this.store() + await this.store(); } - }) + }); } get actions(): ActionsMap { @@ -117,19 +117,19 @@ export class PrivatePreferencesStore { const sortedActions = new Map( [...this.actionsMap.entries()].sort( (a, b) => - fromNanoString(a[0])!.getTime() - fromNanoString(b[0])!.getTime() - ) - ) - return sortedActions + fromNanoString(a[0])!.getTime() - fromNanoString(b[0])!.getTime(), + ), + ); + return sortedActions; } lookup(key: string): privatePreferences.PrivatePreferencesAction | undefined { - return this.actionsMap.get(key) + return this.actionsMap.get(key); } #toBytes(): Uint8Array { return keystore.PrivatePreferencesActionMap.encode({ actions: Object.fromEntries(this.actionsMap), - }).finish() + }).finish(); } } diff --git a/packages/js-sdk/src/keystore/providers/KeyGeneratorKeystoreProvider.ts b/packages/js-sdk/src/keystore/providers/KeyGeneratorKeystoreProvider.ts index 08f6633ca..23ff4e5b3 100644 --- a/packages/js-sdk/src/keystore/providers/KeyGeneratorKeystoreProvider.ts +++ b/packages/js-sdk/src/keystore/providers/KeyGeneratorKeystoreProvider.ts @@ -1,13 +1,13 @@ -import type { ApiClient } from '@/ApiClient' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import InMemoryKeystore from '@/keystore/InMemoryKeystore' -import TopicPersistence from '@/keystore/persistence/TopicPersistence' -import type { KeystoreInterface } from '@/keystore/rpcDefinitions' -import type { Signer } from '@/types/Signer' -import { KeystoreProviderUnavailableError } from './errors' -import { buildPersistenceFromOptions } from './helpers' -import type { KeystoreProvider, KeystoreProviderOptions } from './interfaces' -import NetworkKeyManager from './NetworkKeyManager' +import type { ApiClient } from "@/ApiClient"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import InMemoryKeystore from "@/keystore/InMemoryKeystore"; +import TopicPersistence from "@/keystore/persistence/TopicPersistence"; +import type { KeystoreInterface } from "@/keystore/rpcDefinitions"; +import type { Signer } from "@/types/Signer"; +import { KeystoreProviderUnavailableError } from "./errors"; +import { buildPersistenceFromOptions } from "./helpers"; +import type { KeystoreProvider, KeystoreProviderOptions } from "./interfaces"; +import NetworkKeyManager from "./NetworkKeyManager"; /** * KeyGeneratorKeystoreProvider will create a new XMTP `PrivateKeyBundle` and persist it to the network @@ -18,27 +18,27 @@ export default class KeyGeneratorKeystoreProvider implements KeystoreProvider { async newKeystore( opts: KeystoreProviderOptions, apiClient: ApiClient, - wallet?: Signer + wallet?: Signer, ): Promise { if (!wallet) { throw new KeystoreProviderUnavailableError( - 'Wallet required to generate new keys' - ) + "Wallet required to generate new keys", + ); } if (opts.preCreateIdentityCallback) { - await opts.preCreateIdentityCallback() + await opts.preCreateIdentityCallback(); } - const bundle = await PrivateKeyBundleV1.generate(wallet) + const bundle = await PrivateKeyBundleV1.generate(wallet); const manager = new NetworkKeyManager( wallet, new TopicPersistence(apiClient), - opts.preEnableIdentityCallback - ) - await manager.storePrivateKeyBundle(bundle) + opts.preEnableIdentityCallback, + ); + await manager.storePrivateKeyBundle(bundle); return InMemoryKeystore.create( bundle, - await buildPersistenceFromOptions(opts, bundle) - ) + await buildPersistenceFromOptions(opts, bundle), + ); } } diff --git a/packages/js-sdk/src/keystore/providers/NetworkKeyManager.ts b/packages/js-sdk/src/keystore/providers/NetworkKeyManager.ts index 4918149e5..c82420006 100644 --- a/packages/js-sdk/src/keystore/providers/NetworkKeyManager.ts +++ b/packages/js-sdk/src/keystore/providers/NetworkKeyManager.ts @@ -1,96 +1,96 @@ -import { privateKey as proto } from '@xmtp/proto' -import { getAddress, hexToBytes, verifyMessage, type Hex } from 'viem' -import LocalAuthenticator from '@/authn/LocalAuthenticator' -import type { PreEventCallback } from '@/Client' -import Ciphertext from '@/crypto/Ciphertext' -import crypto from '@/crypto/crypto' -import { decrypt, encrypt } from '@/crypto/encryption' +import { privateKey as proto } from "@xmtp/proto"; +import { getAddress, hexToBytes, verifyMessage, type Hex } from "viem"; +import LocalAuthenticator from "@/authn/LocalAuthenticator"; +import type { PreEventCallback } from "@/Client"; +import Ciphertext from "@/crypto/Ciphertext"; +import crypto from "@/crypto/crypto"; +import { decrypt, encrypt } from "@/crypto/encryption"; import { decodePrivateKeyBundle, PrivateKeyBundleV1, PrivateKeyBundleV2, -} from '@/crypto/PrivateKeyBundle' -import { bytesToHex } from '@/crypto/utils' -import type TopicPersistence from '@/keystore/persistence/TopicPersistence' -import type { Signer } from '@/types/Signer' +} from "@/crypto/PrivateKeyBundle"; +import { bytesToHex } from "@/crypto/utils"; +import type TopicPersistence from "@/keystore/persistence/TopicPersistence"; +import type { Signer } from "@/types/Signer"; -const KEY_BUNDLE_NAME = 'key_bundle' +const KEY_BUNDLE_NAME = "key_bundle"; /** * EncryptedKeyStore wraps Store to enable encryption of private key bundles * using a wallet signature. */ export default class NetworkKeyManager { - private persistence: TopicPersistence - private signer: Signer - private preEnableIdentityCallback?: PreEventCallback + private persistence: TopicPersistence; + private signer: Signer; + private preEnableIdentityCallback?: PreEventCallback; constructor( signer: Signer, persistence: TopicPersistence, - preEnableIdentityCallback?: PreEventCallback + preEnableIdentityCallback?: PreEventCallback, ) { - this.signer = signer - this.persistence = persistence - this.preEnableIdentityCallback = preEnableIdentityCallback + this.signer = signer; + this.persistence = persistence; + this.preEnableIdentityCallback = preEnableIdentityCallback; } private async getStorageAddress(name: string): Promise { // I think we want to namespace the storage address by wallet // This will allow us to support switching between multiple wallets in the same browser - let walletAddress = await this.signer.getAddress() - walletAddress = getAddress(walletAddress) - return `${walletAddress}/${name}` + let walletAddress = await this.signer.getAddress(); + walletAddress = getAddress(walletAddress); + return `${walletAddress}/${name}`; } // Retrieve a private key bundle for the active wallet address in the signer async loadPrivateKeyBundle(): Promise { const storageBuffer = await this.persistence.getItem( - await this.getStorageAddress(KEY_BUNDLE_NAME) - ) + await this.getStorageAddress(KEY_BUNDLE_NAME), + ); if (!storageBuffer) { - return null + return null; } const [bundle, needsUpdate] = await this.fromEncryptedBytes( this.signer, - Uint8Array.from(storageBuffer) - ) + Uint8Array.from(storageBuffer), + ); // If a versioned bundle is not found, the legacy bundle needs to be resaved to the store in // the new format. Once all bundles have been upgraded, this migration code can be removed. if (needsUpdate) { - await this.storePrivateKeyBundle(bundle) + await this.storePrivateKeyBundle(bundle); } - return bundle + return bundle; } // Store the private key bundle at an address generated based on the active wallet in the signer async storePrivateKeyBundle(bundle: PrivateKeyBundleV1): Promise { - const keyAddress = await this.getStorageAddress(KEY_BUNDLE_NAME) - const encodedBundle = await this.toEncryptedBytes(bundle, this.signer) + const keyAddress = await this.getStorageAddress(KEY_BUNDLE_NAME); + const encodedBundle = await this.toEncryptedBytes(bundle, this.signer); // We need to setup the Authenticator so that the underlying store can publish messages without error - if (typeof this.persistence.setAuthenticator === 'function') { + if (typeof this.persistence.setAuthenticator === "function") { this.persistence.setAuthenticator( - new LocalAuthenticator(bundle.identityKey) - ) + new LocalAuthenticator(bundle.identityKey), + ); } - await this.persistence.setItem(keyAddress, encodedBundle) + await this.persistence.setItem(keyAddress, encodedBundle); } // encrypts/serializes the bundle for storage async toEncryptedBytes( bundle: PrivateKeyBundleV1, - wallet: Signer + wallet: Signer, ): Promise { // serialize the contents - const bytes = bundle.encode() - const wPreKey = crypto.getRandomValues(new Uint8Array(32)) - const input = storageSigRequestText(wPreKey) - const walletAddr = await wallet.getAddress() + const bytes = bundle.encode(); + const wPreKey = crypto.getRandomValues(new Uint8Array(32)); + const input = storageSigRequestText(wPreKey); + const walletAddr = await wallet.getAddress(); if (this.preEnableIdentityCallback) { - await this.preEnableIdentityCallback() + await this.preEnableIdentityCallback(); } - const sig = await wallet.signMessage(input) + const sig = await wallet.signMessage(input); // Check that the signature is correct, was created using the expected // input, and retry if not. This mitigates a bug in interacting with @@ -100,44 +100,44 @@ export default class NetworkKeyManager { address: walletAddr as `0x${string}`, message: input, signature: sig as Hex, - }) + }); if (!valid) { - throw new Error('invalid signature') + throw new Error("invalid signature"); } - const secret = hexToBytes(sig as Hex) - const ciphertext = await encrypt(bytes, secret) + const secret = hexToBytes(sig as Hex); + const ciphertext = await encrypt(bytes, secret); return proto.EncryptedPrivateKeyBundle.encode({ v1: { walletPreKey: wPreKey, ciphertext, }, - }).finish() + }).finish(); } // decrypts/deserializes the bundle from storage bytes async fromEncryptedBytes( wallet: Signer, - bytes: Uint8Array + bytes: Uint8Array, ): Promise<[PrivateKeyBundleV1, boolean]> { - const [eBundle, needsUpdate] = getEncryptedBundle(bytes) + const [eBundle, needsUpdate] = getEncryptedBundle(bytes); if (!eBundle.walletPreKey) { - throw new Error('missing wallet pre-key') + throw new Error("missing wallet pre-key"); } if (!eBundle.ciphertext?.aes256GcmHkdfSha256) { - throw new Error('missing bundle ciphertext') + throw new Error("missing bundle ciphertext"); } if (this.preEnableIdentityCallback) { - await this.preEnableIdentityCallback() + await this.preEnableIdentityCallback(); } const secret = hexToBytes( (await wallet.signMessage( - storageSigRequestText(eBundle.walletPreKey) - )) as Hex - ) + storageSigRequestText(eBundle.walletPreKey), + )) as Hex, + ); // Ledger uses the last byte = v=[0,1,...] but Metamask and other wallets generate with // v+27 as the last byte. We need to support both for interoperability. Doing this @@ -146,31 +146,31 @@ export default class NetworkKeyManager { // https://github.com/ethereum/go-ethereum/issues/19751#issuecomment-504900739 try { // Try the original version of the signature first - const ciphertext = new Ciphertext(eBundle.ciphertext) - const decrypted = await decrypt(ciphertext, secret) - const [bundle, needsUpdate2] = getPrivateBundle(decrypted) - return [bundle, needsUpdate || needsUpdate2] + const ciphertext = new Ciphertext(eBundle.ciphertext); + const decrypted = await decrypt(ciphertext, secret); + const [bundle, needsUpdate2] = getPrivateBundle(decrypted); + return [bundle, needsUpdate || needsUpdate2]; } catch (e) { // Assert that the secret is length 65 (encoded signature + recovery byte) if (secret.length !== 65) { throw new Error( - 'Expected 65 bytes before trying a different recovery byte' - ) + "Expected 65 bytes before trying a different recovery byte", + ); } // Try the other version of recovery byte, either +27 or -27 - const lastByte = secret[secret.length - 1] - let newSecret = secret.slice(0, secret.length - 1) + const lastByte = secret[secret.length - 1]; + let newSecret = secret.slice(0, secret.length - 1); if (lastByte < 27) { // This is a canonical signature, so we need to add 27 to the recovery byte and try again - newSecret = new Uint8Array([...newSecret, lastByte + 27]) + newSecret = new Uint8Array([...newSecret, lastByte + 27]); } else { // This canocalizes v to 0 or 1 (or maybe 2 or 3 but very unlikely) - newSecret = new Uint8Array([...newSecret, lastByte - 27]) + newSecret = new Uint8Array([...newSecret, lastByte - 27]); } - const ciphertext = new Ciphertext(eBundle.ciphertext) - const decrypted = await decrypt(ciphertext, newSecret) - const [bundle, needsUpdate2] = getPrivateBundle(decrypted) - return [bundle, needsUpdate || needsUpdate2] + const ciphertext = new Ciphertext(eBundle.ciphertext); + const decrypted = await decrypt(ciphertext, newSecret); + const [bundle, needsUpdate2] = getPrivateBundle(decrypted); + return [bundle, needsUpdate || needsUpdate2]; } } } @@ -178,17 +178,17 @@ export default class NetworkKeyManager { // getEncryptedV1Bundle returns the decoded bundle from the provided bytes. If there is an error decoding the bundle it attempts // to decode the bundle as a legacy bundle. Additionally return whether the bundle is in the expected format. function getEncryptedBundle( - bytes: Uint8Array + bytes: Uint8Array, ): [proto.EncryptedPrivateKeyBundleV1, boolean] { try { - const b = proto.EncryptedPrivateKeyBundle.decode(bytes) + const b = proto.EncryptedPrivateKeyBundle.decode(bytes); if (b.v1) { - return [b.v1, false] + return [b.v1, false]; } } catch (e) { - return [proto.EncryptedPrivateKeyBundleV1.decode(bytes), true] + return [proto.EncryptedPrivateKeyBundleV1.decode(bytes), true]; } - throw new Error('unrecognized encrypted private key bundle version') + throw new Error("unrecognized encrypted private key bundle version"); } // getPrivateV1Bundle returns the decoded bundle from the provided bytes. If there is an error decoding the bundle it attempts @@ -196,15 +196,15 @@ function getEncryptedBundle( function getPrivateBundle(bytes: Uint8Array): [PrivateKeyBundleV1, boolean] { try { // TODO: add support for V2 - const b = decodePrivateKeyBundle(bytes) + const b = decodePrivateKeyBundle(bytes); if (b instanceof PrivateKeyBundleV2) { - throw new Error('V2 bundles not supported yet') + throw new Error("V2 bundles not supported yet"); } - return [b, false] + return [b, false]; } catch (e) { // Adds a default fallback for older versions of the proto - const b = proto.PrivateKeyBundleV1.decode(bytes) - return [new PrivateKeyBundleV1(b), true] + const b = proto.PrivateKeyBundleV1.decode(bytes); + return [new PrivateKeyBundleV1(b), true]; } } @@ -214,9 +214,9 @@ export function storageSigRequestText(preKey: Uint8Array): string { // and/or a migration; otherwise clients will no longer be able to // decrypt those bundles. return ( - 'XMTP : Enable Identity\n' + + "XMTP : Enable Identity\n" + `${bytesToHex(preKey)}\n` + - '\n' + - 'For more info: https://xmtp.org/signatures/' - ) + "\n" + + "For more info: https://xmtp.org/signatures/" + ); } diff --git a/packages/js-sdk/src/keystore/providers/NetworkKeystoreProvider.ts b/packages/js-sdk/src/keystore/providers/NetworkKeystoreProvider.ts index 91051a14b..4830451a3 100644 --- a/packages/js-sdk/src/keystore/providers/NetworkKeystoreProvider.ts +++ b/packages/js-sdk/src/keystore/providers/NetworkKeystoreProvider.ts @@ -1,12 +1,12 @@ -import type { ApiClient } from '@/ApiClient' -import InMemoryKeystore from '@/keystore/InMemoryKeystore' -import TopicPersistence from '@/keystore/persistence/TopicPersistence' -import type { KeystoreInterface } from '@/keystore/rpcDefinitions' -import type { Signer } from '@/types/Signer' -import { KeystoreProviderUnavailableError } from './errors' -import { buildPersistenceFromOptions } from './helpers' -import type { KeystoreProvider, KeystoreProviderOptions } from './interfaces' -import NetworkKeyLoader from './NetworkKeyManager' +import type { ApiClient } from "@/ApiClient"; +import InMemoryKeystore from "@/keystore/InMemoryKeystore"; +import TopicPersistence from "@/keystore/persistence/TopicPersistence"; +import type { KeystoreInterface } from "@/keystore/rpcDefinitions"; +import type { Signer } from "@/types/Signer"; +import { KeystoreProviderUnavailableError } from "./errors"; +import { buildPersistenceFromOptions } from "./helpers"; +import type { KeystoreProvider, KeystoreProviderOptions } from "./interfaces"; +import NetworkKeyLoader from "./NetworkKeyManager"; /** * NetworkKeystoreProvider will look on the XMTP network for an `EncryptedPrivateKeyBundle` @@ -17,25 +17,25 @@ export default class NetworkKeystoreProvider implements KeystoreProvider { async newKeystore( opts: KeystoreProviderOptions, apiClient: ApiClient, - wallet?: Signer + wallet?: Signer, ): Promise { if (!wallet) { - throw new KeystoreProviderUnavailableError('No wallet provided') + throw new KeystoreProviderUnavailableError("No wallet provided"); } const loader = new NetworkKeyLoader( wallet, new TopicPersistence(apiClient), - opts.preEnableIdentityCallback - ) - const keys = await loader.loadPrivateKeyBundle() + opts.preEnableIdentityCallback, + ); + const keys = await loader.loadPrivateKeyBundle(); if (!keys) { - throw new KeystoreProviderUnavailableError('No keys found') + throw new KeystoreProviderUnavailableError("No keys found"); } return InMemoryKeystore.create( keys, - await buildPersistenceFromOptions(opts, keys) - ) + await buildPersistenceFromOptions(opts, keys), + ); } } diff --git a/packages/js-sdk/src/keystore/providers/SnapProvider.ts b/packages/js-sdk/src/keystore/providers/SnapProvider.ts index ff2d78db9..beae1b48e 100644 --- a/packages/js-sdk/src/keystore/providers/SnapProvider.ts +++ b/packages/js-sdk/src/keystore/providers/SnapProvider.ts @@ -1,29 +1,29 @@ -import { keystore } from '@xmtp/proto' -import type { ApiClient } from '@/ApiClient' -import type { XmtpEnv } from '@/Client' +import { keystore } from "@xmtp/proto"; +import type { ApiClient } from "@/ApiClient"; +import type { XmtpEnv } from "@/Client"; import { decodePrivateKeyBundle, PrivateKeyBundleV1, -} from '@/crypto/PrivateKeyBundle' -import type { SnapKeystoreInterface } from '@/keystore/rpcDefinitions' +} from "@/crypto/PrivateKeyBundle"; +import type { SnapKeystoreInterface } from "@/keystore/rpcDefinitions"; import { connectSnap, getSnap, getWalletStatus, hasMetamaskWithSnaps, initSnap, -} from '@/keystore/snapHelpers' -import { SnapKeystore } from '@/keystore/SnapKeystore' -import type { Signer } from '@/types/Signer' -import { semverGreaterThan } from '@/utils/semver' -import { KeystoreProviderUnavailableError } from './errors' -import type { KeystoreProvider, KeystoreProviderOptions } from './interfaces' -import KeyGeneratorKeystoreProvider from './KeyGeneratorKeystoreProvider' -import NetworkKeystoreProvider from './NetworkKeystoreProvider' +} from "@/keystore/snapHelpers"; +import { SnapKeystore } from "@/keystore/SnapKeystore"; +import type { Signer } from "@/types/Signer"; +import { semverGreaterThan } from "@/utils/semver"; +import { KeystoreProviderUnavailableError } from "./errors"; +import type { KeystoreProvider, KeystoreProviderOptions } from "./interfaces"; +import KeyGeneratorKeystoreProvider from "./KeyGeneratorKeystoreProvider"; +import NetworkKeystoreProvider from "./NetworkKeystoreProvider"; -const { GetKeystoreStatusResponse_KeystoreStatus: KeystoreStatus } = keystore +const { GetKeystoreStatusResponse_KeystoreStatus: KeystoreStatus } = keystore; -export const SNAP_LOCAL_ORIGIN = 'local:http://localhost:8080' +export const SNAP_LOCAL_ORIGIN = "local:http://localhost:8080"; /** * The Snap keystore provider will: @@ -34,109 +34,109 @@ export const SNAP_LOCAL_ORIGIN = 'local:http://localhost:8080' export default class SnapKeystoreProvider implements KeystoreProvider { - snapId: string - snapVersion?: string + snapId: string; + snapVersion?: string; constructor(snapId = SNAP_LOCAL_ORIGIN, snapVersion?: string) { - this.snapId = snapId - this.snapVersion = snapVersion + this.snapId = snapId; + this.snapVersion = snapVersion; } async newKeystore( opts: KeystoreProviderOptions, apiClient: ApiClient, - wallet?: Signer + wallet?: Signer, ) { if (!wallet) { - throw new KeystoreProviderUnavailableError('No wallet provided') + throw new KeystoreProviderUnavailableError("No wallet provided"); } if (!(await hasMetamaskWithSnaps())) { throw new KeystoreProviderUnavailableError( - 'MetaMask with Snaps not detected' - ) + "MetaMask with Snaps not detected", + ); } - const walletAddress = await wallet.getAddress() - const env = opts.env - const hasSnap = await getSnap(this.snapId, this.snapVersion) + const walletAddress = await wallet.getAddress(); + const env = opts.env; + const hasSnap = await getSnap(this.snapId, this.snapVersion); if (!hasSnap || semverGreaterThan(this.snapVersion, hasSnap.version)) { await connectSnap( this.snapId, - this.snapVersion ? { version: this.snapVersion } : {} - ) + this.snapVersion ? { version: this.snapVersion } : {}, + ); } if (!(await checkSnapLoaded(walletAddress, env, this.snapId))) { - const bundle = await bundleFromOptions(opts, apiClient, wallet) - await initSnap(bundle, env, this.snapId) + const bundle = await bundleFromOptions(opts, apiClient, wallet); + await initSnap(bundle, env, this.snapId); } - return SnapKeystore(walletAddress, env, this.snapId) + return SnapKeystore(walletAddress, env, this.snapId); } } async function createBundle( opts: KeystoreProviderOptions, apiClient: ApiClient, - wallet: Signer + wallet: Signer, ) { - const tmpProvider = new KeyGeneratorKeystoreProvider() - const tmpKeystore = await tmpProvider.newKeystore(opts, apiClient, wallet) - return new PrivateKeyBundleV1(await tmpKeystore.getPrivateKeyBundle()) + const tmpProvider = new KeyGeneratorKeystoreProvider(); + const tmpKeystore = await tmpProvider.newKeystore(opts, apiClient, wallet); + return new PrivateKeyBundleV1(await tmpKeystore.getPrivateKeyBundle()); } async function bundleFromOptions( opts: KeystoreProviderOptions, apiClient: ApiClient, - wallet?: Signer + wallet?: Signer, ) { if (opts.privateKeyOverride) { - const bundle = decodePrivateKeyBundle(opts.privateKeyOverride) + const bundle = decodePrivateKeyBundle(opts.privateKeyOverride); if (!(bundle instanceof PrivateKeyBundleV1)) { - throw new Error('Unsupported private key bundle version') + throw new Error("Unsupported private key bundle version"); } - return bundle + return bundle; } if (!wallet) { - throw new Error('No privateKeyOverride or wallet') + throw new Error("No privateKeyOverride or wallet"); } - return getOrCreateBundle(opts, apiClient, wallet) + return getOrCreateBundle(opts, apiClient, wallet); } async function getOrCreateBundle( opts: KeystoreProviderOptions, apiClient: ApiClient, - wallet: Signer + wallet: Signer, ): Promise { // I really don't love using other providers inside a provider. Feels like too much indirection // TODO: Refactor keystore providers to better support the weird Snap flow - const networkProvider = new NetworkKeystoreProvider() + const networkProvider = new NetworkKeystoreProvider(); try { const tmpKeystore = await networkProvider.newKeystore( opts, apiClient, - wallet - ) - return new PrivateKeyBundleV1(await tmpKeystore.getPrivateKeyBundle()) + wallet, + ); + return new PrivateKeyBundleV1(await tmpKeystore.getPrivateKeyBundle()); } catch (e) { if (e instanceof KeystoreProviderUnavailableError) { - return createBundle(opts, apiClient, wallet) + return createBundle(opts, apiClient, wallet); } - throw e + throw e; } } async function checkSnapLoaded( walletAddress: string, env: XmtpEnv, - snapId: string + snapId: string, ) { - const status = await getWalletStatus({ walletAddress, env }, snapId) + const status = await getWalletStatus({ walletAddress, env }, snapId); if (status === KeystoreStatus.KEYSTORE_STATUS_INITIALIZED) { - return true + return true; } - return false + return false; } diff --git a/packages/js-sdk/src/keystore/providers/StaticKeystoreProvider.ts b/packages/js-sdk/src/keystore/providers/StaticKeystoreProvider.ts index 49f7e91c1..5a0af3985 100644 --- a/packages/js-sdk/src/keystore/providers/StaticKeystoreProvider.ts +++ b/packages/js-sdk/src/keystore/providers/StaticKeystoreProvider.ts @@ -1,12 +1,12 @@ import { decodePrivateKeyBundle, PrivateKeyBundleV2, -} from '@/crypto/PrivateKeyBundle' -import InMemoryKeystore from '@/keystore/InMemoryKeystore' -import type { KeystoreInterface } from '@/keystore/rpcDefinitions' -import { KeystoreProviderUnavailableError } from './errors' -import { buildPersistenceFromOptions } from './helpers' -import type { KeystoreProvider, KeystoreProviderOptions } from './interfaces' +} from "@/crypto/PrivateKeyBundle"; +import InMemoryKeystore from "@/keystore/InMemoryKeystore"; +import type { KeystoreInterface } from "@/keystore/rpcDefinitions"; +import { KeystoreProviderUnavailableError } from "./errors"; +import { buildPersistenceFromOptions } from "./helpers"; +import type { KeystoreProvider, KeystoreProviderOptions } from "./interfaces"; /** * StaticKeystoreProvider will look for a `privateKeyOverride` in the provided options, @@ -17,21 +17,21 @@ import type { KeystoreProvider, KeystoreProviderOptions } from './interfaces' */ export default class StaticKeystoreProvider implements KeystoreProvider { async newKeystore(opts: KeystoreProviderOptions): Promise { - const { privateKeyOverride } = opts + const { privateKeyOverride } = opts; if (!privateKeyOverride) { throw new KeystoreProviderUnavailableError( - 'No private key override provided' - ) + "No private key override provided", + ); } - const bundle = decodePrivateKeyBundle(privateKeyOverride) + const bundle = decodePrivateKeyBundle(privateKeyOverride); if (bundle instanceof PrivateKeyBundleV2) { - throw new Error('V2 private key bundle found. Only V1 supported') + throw new Error("V2 private key bundle found. Only V1 supported"); } return InMemoryKeystore.create( bundle, - await buildPersistenceFromOptions(opts, bundle) - ) + await buildPersistenceFromOptions(opts, bundle), + ); } } diff --git a/packages/js-sdk/src/keystore/providers/helpers.ts b/packages/js-sdk/src/keystore/providers/helpers.ts index 071e93917..5e2a3a7ba 100644 --- a/packages/js-sdk/src/keystore/providers/helpers.ts +++ b/packages/js-sdk/src/keystore/providers/helpers.ts @@ -1,29 +1,29 @@ import type { PrivateKeyBundleV1, PrivateKeyBundleV2, -} from '@/crypto/PrivateKeyBundle' -import EncryptedPersistence from '@/keystore/persistence/EncryptedPersistence' -import EphemeralPersistence from '@/keystore/persistence/InMemoryPersistence' -import PrefixedPersistence from '@/keystore/persistence/PrefixedPersistence' -import { buildPersistenceKey } from '@/keystore/utils' -import type { KeystoreProviderOptions } from './interfaces' +} from "@/crypto/PrivateKeyBundle"; +import EncryptedPersistence from "@/keystore/persistence/EncryptedPersistence"; +import EphemeralPersistence from "@/keystore/persistence/InMemoryPersistence"; +import PrefixedPersistence from "@/keystore/persistence/PrefixedPersistence"; +import { buildPersistenceKey } from "@/keystore/utils"; +import type { KeystoreProviderOptions } from "./interfaces"; export const buildPersistenceFromOptions = async ( opts: KeystoreProviderOptions, - keys: PrivateKeyBundleV1 | PrivateKeyBundleV2 + keys: PrivateKeyBundleV1 | PrivateKeyBundleV2, ) => { if (!opts.persistConversations) { - return EphemeralPersistence.create() + return EphemeralPersistence.create(); } - const address = await keys.identityKey.publicKey.walletSignatureAddress() - const prefix = buildPersistenceKey(opts.env, address) - const basePersistence = opts.basePersistence - const shouldEncrypt = !opts.disablePersistenceEncryption + const address = await keys.identityKey.publicKey.walletSignatureAddress(); + const prefix = buildPersistenceKey(opts.env, address); + const basePersistence = opts.basePersistence; + const shouldEncrypt = !opts.disablePersistenceEncryption; return new PrefixedPersistence( prefix, shouldEncrypt ? new EncryptedPersistence(basePersistence, keys.identityKey) - : basePersistence - ) -} + : basePersistence, + ); +}; diff --git a/packages/js-sdk/src/keystore/providers/interfaces.ts b/packages/js-sdk/src/keystore/providers/interfaces.ts index afe1d5ec7..21da771e5 100644 --- a/packages/js-sdk/src/keystore/providers/interfaces.ts +++ b/packages/js-sdk/src/keystore/providers/interfaces.ts @@ -1,19 +1,19 @@ -import type { ApiClient } from '@/ApiClient' -import type { PreEventCallbackOptions, XmtpEnv } from '@/Client' -import type { Persistence } from '@/keystore/persistence/interface' +import type { ApiClient } from "@/ApiClient"; +import type { PreEventCallbackOptions, XmtpEnv } from "@/Client"; +import type { Persistence } from "@/keystore/persistence/interface"; import type { KeystoreInterface, KeystoreInterfaces, -} from '@/keystore/rpcDefinitions' -import type { Signer } from '@/types/Signer' +} from "@/keystore/rpcDefinitions"; +import type { Signer } from "@/types/Signer"; export type KeystoreProviderOptions = { - env: XmtpEnv - persistConversations: boolean - privateKeyOverride?: Uint8Array - basePersistence: Persistence - disablePersistenceEncryption: boolean -} & PreEventCallbackOptions + env: XmtpEnv; + persistConversations: boolean; + privateKeyOverride?: Uint8Array; + basePersistence: Persistence; + disablePersistenceEncryption: boolean; +} & PreEventCallbackOptions; /** * A Keystore Provider is responsible for either creating a Keystore instance or throwing a KeystoreUnavailableError @@ -25,6 +25,6 @@ export interface KeystoreProvider< newKeystore( opts: KeystoreProviderOptions, apiClient: ApiClient, - wallet?: Signer - ): Promise + wallet?: Signer, + ): Promise; } diff --git a/packages/js-sdk/src/keystore/rpcDefinitions.ts b/packages/js-sdk/src/keystore/rpcDefinitions.ts index 630d7a715..1f9c81c2d 100644 --- a/packages/js-sdk/src/keystore/rpcDefinitions.ts +++ b/packages/js-sdk/src/keystore/rpcDefinitions.ts @@ -5,86 +5,86 @@ import { publicKey, signature, type privatePreferences, -} from '@xmtp/proto' -import type { Reader, Writer } from 'protobufjs/minimal' -import type { PublishParams } from '@/ApiClient' -import type { ActionsMap } from '@/keystore/privatePreferencesStore' -import type { Flatten } from '@/utils/typedefs' +} from "@xmtp/proto"; +import type { Reader, Writer } from "protobufjs/minimal"; +import type { PublishParams } from "@/ApiClient"; +import type { ActionsMap } from "@/keystore/privatePreferencesStore"; +import type { Flatten } from "@/utils/typedefs"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type KeystoreRPCCodec = { - decode(input: Reader | Uint8Array, length?: number): T - encode(message: T, writer?: Writer): Writer -} + decode(input: Reader | Uint8Array, length?: number): T; + encode(message: T, writer?: Writer): Writer; +}; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type KeystoreRPC = { - req: KeystoreRPCCodec | null - res: KeystoreRPCCodec -} + req: KeystoreRPCCodec | null; + res: KeystoreRPCCodec; +}; type Entries = { - [K in keyof T]: [K, T[K]] -}[keyof T][] + [K in keyof T]: [K, T[K]]; +}[keyof T][]; type Values = { - [K in keyof T]: T[K] -}[keyof T] + [K in keyof T]: T[K]; +}[keyof T]; type ApiDefs = { - [key: string]: KeystoreRPC -} + [key: string]: KeystoreRPC; +}; type ApiInterface = { // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: (...args: any[]) => any -} + [key: string]: (...args: any[]) => any; +}; type PrivatePreferenceKeystoreMethods = { createPrivatePreference: ( - action: privatePreferences.PrivatePreferencesAction - ) => Promise - getPrivatePreferences: () => ActionsMap - getPrivatePreferencesTopic: () => Promise - savePrivatePreferences: (data: ActionsMap) => Promise -} + action: privatePreferences.PrivatePreferencesAction, + ) => Promise; + getPrivatePreferences: () => ActionsMap; + getPrivatePreferencesTopic: () => Promise; + savePrivatePreferences: (data: ActionsMap) => Promise; +}; type OtherKeyStoreMethods = { /** * Get the account address of the wallet used to create the Keystore */ - getAccountAddress(): Promise -} + getAccountAddress(): Promise; +}; type ExtractInterface = Flatten< { [K in keyof T]: T[K] extends KeystoreRPC - ? T[K]['req'] extends null + ? T[K]["req"] extends null ? () => Promise : (req: Req) => Promise - : never + : never; } & OtherKeyStoreMethods & PrivatePreferenceKeystoreMethods -> +>; type ExtractInterfaceRequestEncoders = { - [K in keyof T]: T[K]['req'] extends KeystoreRPCCodec - ? T[K]['req']['encode'] - : never -} + [K in keyof T]: T[K]["req"] extends KeystoreRPCCodec + ? T[K]["req"]["encode"] + : never; +}; type ExtractInterfaceResponseDecoders = { - [K in keyof T]: T[K]['res'] extends KeystoreRPCCodec - ? T[K]['res']['decode'] - : never -} + [K in keyof T]: T[K]["res"] extends KeystoreRPCCodec + ? T[K]["res"]["decode"] + : never; +}; type ExtractInterfaceRequestValues = { // eslint-disable-next-line @typescript-eslint/no-explicit-any [K in keyof T]: T[K] extends (...args: any[]) => any ? Parameters[0] - : never -} + : never; +}; export const apiDefs = { /** @@ -226,19 +226,19 @@ export const apiDefs = { req: keystore.GetConversationHmacKeysRequest, res: keystore.GetConversationHmacKeysResponse, }, -} +}; -export type KeystoreApiDefs = typeof apiDefs -export type KeystoreApiMethods = keyof KeystoreApiDefs -export type KeystoreInterface = ExtractInterface -export type KeystoreApiEntries = Entries +export type KeystoreApiDefs = typeof apiDefs; +export type KeystoreApiMethods = keyof KeystoreApiDefs; +export type KeystoreInterface = ExtractInterface; +export type KeystoreApiEntries = Entries; export type KeystoreApiRequestEncoders = - ExtractInterfaceRequestEncoders + ExtractInterfaceRequestEncoders; export type KeystoreApiResponseDecoders = - ExtractInterfaceResponseDecoders + ExtractInterfaceResponseDecoders; export type KeystoreInterfaceRequestValues = - ExtractInterfaceRequestValues -export type KeystoreApiRequestValues = Values + ExtractInterfaceRequestValues; +export type KeystoreApiRequestValues = Values; export const snapApiDefs = { getKeystoreStatus: { @@ -249,23 +249,23 @@ export const snapApiDefs = { req: keystore.InitKeystoreRequest, res: keystore.InitKeystoreResponse, }, -} +}; -export type SnapKeystoreApiDefs = typeof snapApiDefs & KeystoreApiDefs -export type SnapKeystoreApiMethods = keyof SnapKeystoreApiDefs -export type SnapKeystoreInterface = ExtractInterface -export type SnapKeystoreApiEntries = Entries +export type SnapKeystoreApiDefs = typeof snapApiDefs & KeystoreApiDefs; +export type SnapKeystoreApiMethods = keyof SnapKeystoreApiDefs; +export type SnapKeystoreInterface = ExtractInterface; +export type SnapKeystoreApiEntries = Entries; export type SnapKeystoreApiRequestEncoders = - ExtractInterfaceRequestEncoders + ExtractInterfaceRequestEncoders; export type SnapKeystoreApiResponseDecoders = - ExtractInterfaceResponseDecoders + ExtractInterfaceResponseDecoders; export type SnapKeystoreInterfaceRequestValues = - ExtractInterfaceRequestValues + ExtractInterfaceRequestValues; export type SnapKeystoreApiRequestValues = - Values + Values; /** * A Keystore is responsible for holding the user's XMTP private keys and using them to encrypt/decrypt/sign messages. * Keystores are instantiated using a `KeystoreProvider` */ -export type KeystoreInterfaces = KeystoreInterface | SnapKeystoreInterface +export type KeystoreInterfaces = KeystoreInterface | SnapKeystoreInterface; diff --git a/packages/js-sdk/src/keystore/snapHelpers.ts b/packages/js-sdk/src/keystore/snapHelpers.ts index 7fc8e819b..f758dcf3a 100644 --- a/packages/js-sdk/src/keystore/snapHelpers.ts +++ b/packages/js-sdk/src/keystore/snapHelpers.ts @@ -1,17 +1,17 @@ -import { keystore } from '@xmtp/proto' -import type { XmtpEnv } from '@/Client' -import type { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import { b64Decode, b64Encode } from '@/utils/bytes' -import { getEthereum } from '@/utils/ethereum' -import { isSameMajorVersion } from '@/utils/semver' -import { KeystoreError } from './errors' +import { keystore } from "@xmtp/proto"; +import type { XmtpEnv } from "@/Client"; +import type { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import { b64Decode, b64Encode } from "@/utils/bytes"; +import { getEthereum } from "@/utils/ethereum"; +import { isSameMajorVersion } from "@/utils/semver"; +import { KeystoreError } from "./errors"; import type { SnapKeystoreApiDefs, SnapKeystoreApiMethods, SnapKeystoreApiRequestEncoders, SnapKeystoreApiResponseDecoders, SnapKeystoreInterfaceRequestValues, -} from './rpcDefinitions' +} from "./rpcDefinitions"; const { GetKeystoreStatusResponse_KeystoreStatus: KeystoreStatus, @@ -19,58 +19,58 @@ const { InitKeystoreResponse, GetKeystoreStatusRequest, GetKeystoreStatusResponse, -} = keystore +} = keystore; export type SnapMeta = { - walletAddress: string - env: XmtpEnv -} + walletAddress: string; + env: XmtpEnv; +}; type SnapParams = { - meta: SnapMeta - req?: string -} + meta: SnapMeta; + req?: string; +}; type SnapResponse = { - res: string | string[] -} + res: string | string[]; +}; export async function snapRPC( method: T, rpc: SnapKeystoreApiDefs[T], req: SnapKeystoreInterfaceRequestValues[T], meta: SnapMeta, - snapId: string + snapId: string, ) { - let reqParam = null + let reqParam = null; if (rpc.req) { - const encoder = rpc.req.encode as SnapKeystoreApiRequestEncoders[T] - const reqBytes = encoder(req).finish() - reqParam = b64Encode(reqBytes, 0, reqBytes.length) + const encoder = rpc.req.encode as SnapKeystoreApiRequestEncoders[T]; + const reqBytes = encoder(req).finish(); + reqParam = b64Encode(reqBytes, 0, reqBytes.length); } - const responseString = await snapRequest(method, reqParam, meta, snapId) + const responseString = await snapRequest(method, reqParam, meta, snapId); if (Array.isArray(responseString)) { - throw new Error('Unexpected array response') + throw new Error("Unexpected array response"); } return rpc.res.decode(b64Decode(responseString)) as ReturnType< SnapKeystoreApiResponseDecoders[T] - > + >; } export async function snapRequest( method: SnapKeystoreApiMethods, req: string | null, meta: SnapMeta, - snapId: string + snapId: string, ) { - const params: SnapParams = { meta } - if (typeof req === 'string') { - params.req = req + const params: SnapParams = { meta }; + if (typeof req === "string") { + params.req = req; } const response = await getEthereum()?.request({ - method: 'wallet_invokeSnap', + method: "wallet_invokeSnap", params: { snapId, request: { @@ -78,55 +78,55 @@ export async function snapRequest( params, }, }, - }) + }); - if (!response || typeof response !== 'object') { - throw new Error('No response value') + if (!response || typeof response !== "object") { + throw new Error("No response value"); } - return (response as SnapResponse).res + return (response as SnapResponse).res; } export type Snap = { - permissionName: string - id: string - version: string - initialPermissions: Record -} + permissionName: string; + id: string; + version: string; + initialPermissions: Record; +}; -export type GetSnapsResponse = Record +export type GetSnapsResponse = Record; // If a browser has multiple providers, but one of them supports MetaMask flask // this function will ensure that Flask is being used and return true. // Designed to be resistant to provider clobbering by Phantom and CBW // Inspired by https://github.com/Montoya/snap-connect-test/blob/main/index.html export async function hasMetamaskWithSnaps() { - const ethereum = getEthereum() + const ethereum = getEthereum(); // Naive way of detecting snaps support if (ethereum?.isMetaMask) { try { await ethereum.request({ - method: 'wallet_getSnaps', - }) - return true + method: "wallet_getSnaps", + }); + return true; } catch { // no-op } } if ( - typeof ethereum?.detected !== 'undefined' && + typeof ethereum?.detected !== "undefined" && Array.isArray(ethereum.detected) ) { for (const provider of ethereum.detected) { try { // Detect snaps support await provider.request({ - method: 'wallet_getSnaps', - }) + method: "wallet_getSnaps", + }); // enforces MetaMask as provider - ethereum?.setProvider?.(provider) + ethereum?.setProvider?.(provider); - return true + return true; } catch { // no-op } @@ -134,84 +134,84 @@ export async function hasMetamaskWithSnaps() { } if ( - typeof ethereum?.providers !== 'undefined' && + typeof ethereum?.providers !== "undefined" && Array.isArray(ethereum.providers) ) { for (const provider of ethereum.providers) { try { // Detect snaps support await provider.request({ - method: 'wallet_getSnaps', - }) + method: "wallet_getSnaps", + }); - window.ethereum = provider + window.ethereum = provider; - return true + return true; } catch { // no-op } } } - return false + return false; } export async function getSnaps() { return await getEthereum()?.request({ - method: 'wallet_getSnaps', - }) + method: "wallet_getSnaps", + }); } export async function getSnap( snapId: string, - version?: string + version?: string, ): Promise { try { - const snaps = await getSnaps() + const snaps = await getSnaps(); if (snaps) { return Object.values(snaps).find( (snap) => snap && snap.id === snapId && - (!version || isSameMajorVersion(snap.version, version)) - ) + (!version || isSameMajorVersion(snap.version, version)), + ); } - return undefined + return undefined; } catch (e) { - console.warn('Failed to obtain installed snap', e) - return undefined + console.warn("Failed to obtain installed snap", e); + return undefined; } } export async function connectSnap( snapId: string, - params: Record<'version' | string, unknown> = {} + params: Record<"version" | string, unknown> = {}, ) { await getEthereum()?.request({ - method: 'wallet_requestSnaps', + method: "wallet_requestSnaps", params: { [snapId]: params, }, - }) + }); } const getWalletStatusCodec = { req: GetKeystoreStatusRequest, res: GetKeystoreStatusResponse, -} +}; export async function getWalletStatus(meta: SnapMeta, snapId: string) { const response = await snapRPC( - 'getKeystoreStatus', + "getKeystoreStatus", getWalletStatusCodec, { walletAddress: meta.walletAddress, }, meta, - snapId - ) + snapId, + ); if ( [ @@ -219,33 +219,33 @@ export async function getWalletStatus(meta: SnapMeta, snapId: string) { KeystoreStatus.UNRECOGNIZED, ].includes(response.status) ) { - throw new Error('No status specified in response') + throw new Error("No status specified in response"); } - return response.status + return response.status; } const initKeystoreCodec = { req: InitKeystoreRequest, res: InitKeystoreResponse, -} +}; export async function initSnap( bundle: PrivateKeyBundleV1, env: XmtpEnv, - snapId: string + snapId: string, ) { - const walletAddress = bundle.identityKey.publicKey.walletSignatureAddress() + const walletAddress = bundle.identityKey.publicKey.walletSignatureAddress(); const response = await snapRPC( - 'initKeystore', + "initKeystore", initKeystoreCodec, { v1: bundle, }, { walletAddress, env }, - snapId - ) + snapId, + ); if (response.error) { - throw new KeystoreError(response.error.code, response.error.message) + throw new KeystoreError(response.error.code, response.error.message); } } diff --git a/packages/js-sdk/src/keystore/utils.ts b/packages/js-sdk/src/keystore/utils.ts index 21087bcb6..093bc7441 100644 --- a/packages/js-sdk/src/keystore/utils.ts +++ b/packages/js-sdk/src/keystore/utils.ts @@ -3,31 +3,31 @@ import { type conversationReference, type invitation, type publicKey, -} from '@xmtp/proto' -import type { XmtpEnv } from '@/Client' +} from "@xmtp/proto"; +import type { XmtpEnv } from "@/Client"; import { PublicKeyBundle, SignedPublicKeyBundle, -} from '@/crypto/PublicKeyBundle' -import type { WithoutUndefined } from '@/utils/typedefs' -import { KeystoreError } from './errors' -import type { TopicData } from './interfaces' +} from "@/crypto/PublicKeyBundle"; +import type { WithoutUndefined } from "@/utils/typedefs"; +import { KeystoreError } from "./errors"; +import type { TopicData } from "./interfaces"; export const convertError = ( e: Error, // Default error code to apply to errors that don't have one - errorCode: keystore.ErrorCode + errorCode: keystore.ErrorCode, ) => { if (e instanceof KeystoreError) { - return e + return e; } - return new KeystoreError(errorCode, e.message) -} + return new KeystoreError(errorCode, e.message); +}; -export const wrapResult = (result: T): { result: T } => ({ result }) +export const wrapResult = (result: T): { result: T } => ({ result }); -type ResultOrError = { result: T } | { error: KeystoreError } +type ResultOrError = { result: T } | { error: KeystoreError }; // Map an array of items to an array of results or errors // Transform any errors thrown into `KeystoreError`s @@ -35,79 +35,79 @@ export const mapAndConvertErrors = ( input: Input[], mapper: (input: Input) => Promise | Output, // Default error code to apply to errors that don't have one - errorCode: keystore.ErrorCode + errorCode: keystore.ErrorCode, ): Promise[]> => { return Promise.all( input.map(async (item: Input) => { try { // Be sure to await mapper result to catch errors - return wrapResult(await mapper(item)) + return wrapResult(await mapper(item)); } catch (e) { - return { error: convertError(e as Error, errorCode) } + return { error: convertError(e as Error, errorCode) }; } - }) - ) -} + }), + ); +}; // Wrap the bundle in our class if not already wrapped export const toPublicKeyBundle = (bundle: publicKey.PublicKeyBundle) => { if (bundle instanceof PublicKeyBundle) { - return bundle + return bundle; } - return new PublicKeyBundle(bundle) -} + return new PublicKeyBundle(bundle); +}; // Wrap the bundle in our class if not already wrapped export const toSignedPublicKeyBundle = ( - bundle: publicKey.SignedPublicKeyBundle + bundle: publicKey.SignedPublicKeyBundle, ) => { if (bundle instanceof SignedPublicKeyBundle) { - return bundle + return bundle; } - return new SignedPublicKeyBundle(bundle) -} + return new SignedPublicKeyBundle(bundle); +}; // Takes object and returns true if none of the `objectFields` are null or undefined and none of the `arrayFields` are empty export const validateObject = ( obj: T, objectFields: (keyof T)[], - arrayFields: (keyof T)[] + arrayFields: (keyof T)[], ): obj is WithoutUndefined => { for (const field of objectFields) { if (!obj[field]) { throw new KeystoreError( keystore.ErrorCode.ERROR_CODE_INVALID_INPUT, - `Missing field ${String(field)}` - ) + `Missing field ${String(field)}`, + ); } } for (const field of arrayFields) { - const val = obj[field] + const val = obj[field]; // @ts-expect-error does not know it's an array if (!val || !val?.length) { throw new KeystoreError( keystore.ErrorCode.ERROR_CODE_INVALID_INPUT, - `Missing field ${String(field)}` - ) + `Missing field ${String(field)}`, + ); } } - return true -} + return true; +}; export const getKeyMaterial = ( - invite: invitation.InvitationV1 | undefined + invite: invitation.InvitationV1 | undefined, ): Uint8Array => { if (!invite?.aes256GcmHkdfSha256?.keyMaterial) { throw new KeystoreError( keystore.ErrorCode.ERROR_CODE_INVALID_INPUT, - 'Missing key material' - ) + "Missing key material", + ); } - return invite.aes256GcmHkdfSha256.keyMaterial -} + return invite.aes256GcmHkdfSha256.keyMaterial; +}; export const topicDataToV2ConversationReference = ({ invitation, @@ -119,19 +119,19 @@ export const topicDataToV2ConversationReference = ({ peerAddress, createdNs, consentProofPayload: invitation.consentProof, -}) +}); export const isCompleteTopicData = ( - obj: keystore.TopicMap_TopicData -): obj is TopicData => !!obj.invitation + obj: keystore.TopicMap_TopicData, +): obj is TopicData => !!obj.invitation; export const topicDataToMap = (topicMap: keystore.TopicMap) => { - const out = new Map() + const out = new Map(); for (const [k, v] of Object.entries(topicMap.topics)) { - out.set(k, v) + out.set(k, v); } - return out -} + return out; +}; export const buildPersistenceKey = (env: XmtpEnv, walletAddress: string) => - `xmtp/${env}/${walletAddress}/` + `xmtp/${env}/${walletAddress}/`; diff --git a/packages/js-sdk/src/message-backup/BackupClient.ts b/packages/js-sdk/src/message-backup/BackupClient.ts index db77f5b1b..986d7f9e3 100644 --- a/packages/js-sdk/src/message-backup/BackupClient.ts +++ b/packages/js-sdk/src/message-backup/BackupClient.ts @@ -6,26 +6,26 @@ export enum BackupType { xmtpTopicStore, } export interface BackupProvider { - type: BackupType + type: BackupType; } -export type SelectBackupProvider = () => Promise +export type SelectBackupProvider = () => Promise; export interface NoBackupConfiguration { - type: BackupType.none - version: number + type: BackupType.none; + version: number; } export interface TopicStoreBackupConfiguration { - type: BackupType.xmtpTopicStore - version: number + type: BackupType.xmtpTopicStore; + version: number; // The location where the backup will be stored - topic: string + topic: string; // The symmetric encryption key used to encrypt/decrypt backups (optional for now) - secret?: Uint8Array + secret?: Uint8Array; } export type BackupConfiguration = | NoBackupConfiguration - | TopicStoreBackupConfiguration + | TopicStoreBackupConfiguration; export default interface BackupClient { - get backupType(): BackupType + get backupType(): BackupType; } diff --git a/packages/js-sdk/src/message-backup/BackupClientFactory.ts b/packages/js-sdk/src/message-backup/BackupClientFactory.ts index bd46eac86..ab56e2b71 100644 --- a/packages/js-sdk/src/message-backup/BackupClientFactory.ts +++ b/packages/js-sdk/src/message-backup/BackupClientFactory.ts @@ -1,11 +1,11 @@ -import type BackupClient from './BackupClient' +import type BackupClient from "./BackupClient"; import { BackupType, type BackupConfiguration, type SelectBackupProvider, -} from './BackupClient' -import NoBackupClient from './NoBackupClient' -import TopicStoreBackupClient from './TopicStoreBackupClient' +} from "./BackupClient"; +import NoBackupClient from "./NoBackupClient"; +import TopicStoreBackupClient from "./TopicStoreBackupClient"; /** * Creates a backup client of the correct provider type (e.g. xmtp backup, no backup, etc). @@ -19,36 +19,36 @@ import TopicStoreBackupClient from './TopicStoreBackupClient' */ export async function createBackupClient( walletAddress: string, - selectBackupProvider: SelectBackupProvider + selectBackupProvider: SelectBackupProvider, ): Promise { const configuration = await fetchOrCreateConfiguration( walletAddress, - selectBackupProvider - ) + selectBackupProvider, + ); switch (configuration.type) { case BackupType.none: - return new NoBackupClient(configuration) + return new NoBackupClient(configuration); case BackupType.xmtpTopicStore: - return new TopicStoreBackupClient(configuration) + return new TopicStoreBackupClient(configuration); } } export async function fetchOrCreateConfiguration( walletAddress: string, - selectBackupProvider: SelectBackupProvider + selectBackupProvider: SelectBackupProvider, ): Promise { // TODO: return existing configuration from the backend if it exists - let backupConfiguration: BackupConfiguration - const provider = await selectBackupProvider() + let backupConfiguration: BackupConfiguration; + const provider = await selectBackupProvider(); switch (provider.type) { case BackupType.none: - backupConfiguration = NoBackupClient.createConfiguration() - break + backupConfiguration = NoBackupClient.createConfiguration(); + break; case BackupType.xmtpTopicStore: backupConfiguration = - TopicStoreBackupClient.createConfiguration(walletAddress) - break + TopicStoreBackupClient.createConfiguration(walletAddress); + break; } // TODO: Persist new configuration to backend - return backupConfiguration + return backupConfiguration; } diff --git a/packages/js-sdk/src/message-backup/NoBackupClient.ts b/packages/js-sdk/src/message-backup/NoBackupClient.ts index df1567b2f..246e923ab 100644 --- a/packages/js-sdk/src/message-backup/NoBackupClient.ts +++ b/packages/js-sdk/src/message-backup/NoBackupClient.ts @@ -1,22 +1,22 @@ -import type BackupClient from './BackupClient' -import { BackupType, type NoBackupConfiguration } from './BackupClient' +import type BackupClient from "./BackupClient"; +import { BackupType, type NoBackupConfiguration } from "./BackupClient"; -const BACKUP_TYPE = BackupType.none +const BACKUP_TYPE = BackupType.none; export default class NoBackupClient implements BackupClient { - private configuration: NoBackupConfiguration + private configuration: NoBackupConfiguration; public static createConfiguration(): NoBackupConfiguration { return { type: BACKUP_TYPE, version: 0, - } + }; } constructor(configuration: NoBackupConfiguration) { - this.configuration = configuration + this.configuration = configuration; } public get backupType(): BackupType { - return BACKUP_TYPE + return BACKUP_TYPE; } } diff --git a/packages/js-sdk/src/message-backup/TopicStoreBackupClient.ts b/packages/js-sdk/src/message-backup/TopicStoreBackupClient.ts index d1ff14554..bc0200c43 100644 --- a/packages/js-sdk/src/message-backup/TopicStoreBackupClient.ts +++ b/packages/js-sdk/src/message-backup/TopicStoreBackupClient.ts @@ -1,26 +1,26 @@ -import type BackupClient from './BackupClient' -import { BackupType, type TopicStoreBackupConfiguration } from './BackupClient' +import type BackupClient from "./BackupClient"; +import { BackupType, type TopicStoreBackupConfiguration } from "./BackupClient"; -const BACKUP_TYPE = BackupType.xmtpTopicStore +const BACKUP_TYPE = BackupType.xmtpTopicStore; export default class TopicStoreBackupClient implements BackupClient { - private configuration: TopicStoreBackupConfiguration + private configuration: TopicStoreBackupConfiguration; public static createConfiguration( - walletAddress: string + walletAddress: string, ): TopicStoreBackupConfiguration { // TODO: randomly generate topic and encryption key return { type: BACKUP_TYPE, version: 0, - topic: 'history-v0:' + walletAddress, - } + topic: "history-v0:" + walletAddress, + }; } constructor(configuration: TopicStoreBackupConfiguration) { - this.configuration = configuration + this.configuration = configuration; } public get backupType(): BackupType { - return BACKUP_TYPE + return BACKUP_TYPE; } } diff --git a/packages/js-sdk/src/types/Signer.ts b/packages/js-sdk/src/types/Signer.ts index 4638efa3e..57fcb6a2e 100644 --- a/packages/js-sdk/src/types/Signer.ts +++ b/packages/js-sdk/src/types/Signer.ts @@ -1,4 +1,4 @@ export interface Signer { - getAddress(): Promise - signMessage(message: ArrayLike | string): Promise + getAddress(): Promise; + signMessage(message: ArrayLike | string): Promise; } diff --git a/packages/js-sdk/src/types/client.ts b/packages/js-sdk/src/types/client.ts index 8f7dd9af1..6a7ff3b90 100644 --- a/packages/js-sdk/src/types/client.ts +++ b/packages/js-sdk/src/types/client.ts @@ -1,7 +1,7 @@ -import type { ContentCodec } from '@xmtp/content-type-primitives' -import type Client from '@/Client' +import type { ContentCodec } from "@xmtp/content-type-primitives"; +import type Client from "@/Client"; export type GetMessageContentTypeFromClient = - C extends Client ? T : never + C extends Client ? T : never; -export type ExtractDecodedType = C extends ContentCodec ? T : never +export type ExtractDecodedType = C extends ContentCodec ? T : never; diff --git a/packages/js-sdk/src/types/metamask.ts b/packages/js-sdk/src/types/metamask.ts index 7cb22e5f0..8df06b378 100644 --- a/packages/js-sdk/src/types/metamask.ts +++ b/packages/js-sdk/src/types/metamask.ts @@ -1,20 +1,20 @@ -import type { MetaMaskInpageProvider } from '@metamask/providers' +import type { MetaMaskInpageProvider } from "@metamask/providers"; type EthereumType = MetaMaskInpageProvider & { - setProvider?: (provider: MetaMaskInpageProvider) => void - detected?: MetaMaskInpageProvider[] - providers?: MetaMaskInpageProvider[] -} + setProvider?: (provider: MetaMaskInpageProvider) => void; + detected?: MetaMaskInpageProvider[]; + providers?: MetaMaskInpageProvider[]; +}; /* * Window type extension to support ethereum */ declare global { interface Window { - ethereum: EthereumType + ethereum: EthereumType; } interface globalThis { - ethereum: EthereumType + ethereum: EthereumType; } } diff --git a/packages/js-sdk/src/types/time-cache/index.d.ts b/packages/js-sdk/src/types/time-cache/index.d.ts index 0687977ae..acf2029ea 100644 --- a/packages/js-sdk/src/types/time-cache/index.d.ts +++ b/packages/js-sdk/src/types/time-cache/index.d.ts @@ -1,14 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ declare class TimeCache { - constructor(options: any) + constructor(options: any); - put(key: string, value: any): void - prune(): void - has(key: string): boolean - get(key: string): any - clear(): void + put(key: string, value: any): void; + prune(): void; + has(key: string): boolean; + get(key: string): any; + clear(): void; } -declare module 'time-cache' { - export = TimeCache +declare module "time-cache" { + export = TimeCache; } diff --git a/packages/js-sdk/src/utils/async.ts b/packages/js-sdk/src/utils/async.ts index 9e3d208bc..7a867fed7 100644 --- a/packages/js-sdk/src/utils/async.ts +++ b/packages/js-sdk/src/utils/async.ts @@ -1,31 +1,31 @@ -import type { messageApi } from '@xmtp/proto' -import type { Flatten } from './typedefs' +import type { messageApi } from "@xmtp/proto"; +import type { Flatten } from "./typedefs"; -export type IsRetryable = (err?: Error) => boolean +export type IsRetryable = (err?: Error) => boolean; export const sleep = (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)) + new Promise((resolve) => setTimeout(resolve, ms)); export const promiseWithTimeout = ( timeoutMs: number, promise: () => Promise, - failureMessage?: string + failureMessage?: string, ): Promise => { - let timeoutHandle: NodeJS.Timeout + let timeoutHandle: NodeJS.Timeout; const timeoutPromise = new Promise((_resolve, reject) => { timeoutHandle = setTimeout( () => reject(new Error(failureMessage)), - timeoutMs - ) - }) + timeoutMs, + ); + }); return Promise.race([promise(), timeoutPromise]).then((result) => { - clearTimeout(timeoutHandle) - return result - }) -} + clearTimeout(timeoutHandle); + return result; + }); +}; -const defaultIsRetryableFn = (err?: Error) => !!err +const defaultIsRetryableFn = (err?: Error) => !!err; // Implements type safe retries of arbitrary async functions // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -35,50 +35,50 @@ export async function retry any>( maxRetries: number, sleepTime: number, isRetryableFn: IsRetryable = defaultIsRetryableFn, - retryCount = 1 + retryCount = 1, ): Promise>> { - const currRetry = typeof retryCount === 'number' ? retryCount : 1 + const currRetry = typeof retryCount === "number" ? retryCount : 1; try { - const result = await fn(...args) - return result + const result = await fn(...args); + return result; } catch (e) { if (!isRetryableFn(e as Error) || currRetry > maxRetries) { - throw e + throw e; } - await sleep(sleepTime) - return retry(fn, args, maxRetries, sleepTime, isRetryableFn, currRetry + 1) + await sleep(sleepTime); + return retry(fn, args, maxRetries, sleepTime, isRetryableFn, currRetry + 1); } } export type EnvelopeWithMessage = Flatten< - messageApi.Envelope & Required> -> + messageApi.Envelope & Required> +>; export type EnvelopeMapperWithMessage = ( - env: EnvelopeWithMessage -) => Promise + env: EnvelopeWithMessage, +) => Promise; -export type EnvelopeMapper = (env: messageApi.Envelope) => Promise +export type EnvelopeMapper = (env: messageApi.Envelope) => Promise; // Takes an async generator returning pages of envelopes and converts to an async // generator returning pages of an arbitrary type using a mapper function export async function* mapPaginatedStream( gen: AsyncGenerator, - mapper: EnvelopeMapper + mapper: EnvelopeMapper, ): AsyncGenerator { for await (const page of gen) { - const results = await Promise.allSettled(page.map(mapper)) - const out: Out[] = [] + const results = await Promise.allSettled(page.map(mapper)); + const out: Out[] = []; for (const result of results) { - if (result.status === 'fulfilled') { - out.push(result.value) + if (result.status === "fulfilled") { + out.push(result.value); } else { console.warn( - 'Failed to process envelope due to reason: ', - result.reason - ) + "Failed to process envelope due to reason: ", + result.reason, + ); } } - yield out + yield out; } } diff --git a/packages/js-sdk/src/utils/browser.ts b/packages/js-sdk/src/utils/browser.ts index 8e6049af6..f6d9ef10f 100644 --- a/packages/js-sdk/src/utils/browser.ts +++ b/packages/js-sdk/src/utils/browser.ts @@ -1,2 +1,2 @@ export const isBrowser = () => - typeof window !== 'undefined' && typeof window.document !== 'undefined' + typeof window !== "undefined" && typeof window.document !== "undefined"; diff --git a/packages/js-sdk/src/utils/bytes.ts b/packages/js-sdk/src/utils/bytes.ts index 3c1425ab3..3c3876a77 100644 --- a/packages/js-sdk/src/utils/bytes.ts +++ b/packages/js-sdk/src/utils/bytes.ts @@ -1,30 +1,30 @@ -import { fetcher } from '@xmtp/proto' +import { fetcher } from "@xmtp/proto"; -export const { b64Decode, b64Encode } = fetcher +export const { b64Decode, b64Encode } = fetcher; export function concat(a: Uint8Array, b: Uint8Array): Uint8Array { - const ab = new Uint8Array(a.length + b.length) - ab.set(a) - ab.set(b, a.length) - return ab + const ab = new Uint8Array(a.length + b.length); + ab.set(a); + ab.set(b, a.length); + return ab; } export function numberToUint8Array(num: number) { // Create a buffer for a 32-bit integer - const buffer = new ArrayBuffer(4) - const view = new DataView(buffer) + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); // Set the number in the buffer - view.setInt32(0, num, true) // true for little-endian + view.setInt32(0, num, true); // true for little-endian // Create Uint8Array from buffer - return new Uint8Array(buffer) + return new Uint8Array(buffer); } export function uint8ArrayToNumber(arr: Uint8Array) { - const buffer = arr.buffer - const view = new DataView(buffer) + const buffer = arr.buffer; + const view = new DataView(buffer); // Read the number from the buffer - return view.getInt32(0, true) // true for little-endian + return view.getInt32(0, true); // true for little-endian } diff --git a/packages/js-sdk/src/utils/date.ts b/packages/js-sdk/src/utils/date.ts index 32b2abc52..dbb870fb0 100644 --- a/packages/js-sdk/src/utils/date.ts +++ b/packages/js-sdk/src/utils/date.ts @@ -1,20 +1,20 @@ -import Long from 'long' +import Long from "long"; export function dateToNs(date: Date): Long { - return Long.fromNumber(date.valueOf()).multiply(1_000_000) + return Long.fromNumber(date.valueOf()).multiply(1_000_000); } export function nsToDate(ns: Long): Date { - return new Date(ns.divide(1_000_000).toNumber()) + return new Date(ns.divide(1_000_000).toNumber()); } export const toNanoString = (d: Date | undefined): undefined | string => { - return d && dateToNs(d).toString() -} + return d && dateToNs(d).toString(); +}; export const fromNanoString = (s: string | undefined): undefined | Date => { if (!s) { - return undefined + return undefined; } - return nsToDate(Long.fromString(s)) -} + return nsToDate(Long.fromString(s)); +}; diff --git a/packages/js-sdk/src/utils/ethereum.ts b/packages/js-sdk/src/utils/ethereum.ts index d6673ba2c..e7309b52d 100644 --- a/packages/js-sdk/src/utils/ethereum.ts +++ b/packages/js-sdk/src/utils/ethereum.ts @@ -1,3 +1,3 @@ export function getEthereum() { - return window.ethereum + return window.ethereum; } diff --git a/packages/js-sdk/src/utils/keystore.ts b/packages/js-sdk/src/utils/keystore.ts index ff1a9ea00..a90c0766e 100644 --- a/packages/js-sdk/src/utils/keystore.ts +++ b/packages/js-sdk/src/utils/keystore.ts @@ -1,14 +1,14 @@ -import { keystore } from '@xmtp/proto' -import { PublicKeyBundle } from '@/crypto/PublicKeyBundle' -import { KeystoreError } from '@/keystore/errors' -import type { MessageV1 } from '@/Message' -import type { WithoutUndefined } from './typedefs' +import { keystore } from "@xmtp/proto"; +import { PublicKeyBundle } from "@/crypto/PublicKeyBundle"; +import { KeystoreError } from "@/keystore/errors"; +import type { MessageV1 } from "@/Message"; +import type { WithoutUndefined } from "./typedefs"; type EncryptionResponseResult< T extends | keystore.DecryptResponse_Response | keystore.EncryptResponse_Response, -> = WithoutUndefined['result'] +> = WithoutUndefined["result"]; // Validates the Keystore response. Throws on errors or missing fields. // Returns a type with all possibly undefined fields required to be defined @@ -17,42 +17,42 @@ export const getResultOrThrow = < | keystore.DecryptResponse_Response | keystore.EncryptResponse_Response, >( - response: T + response: T, ) => { if (response.error) { - throw new KeystoreError(response.error.code, response.error.message) + throw new KeystoreError(response.error.code, response.error.message); } if (!response.result) { throw new KeystoreError( keystore.ErrorCode.ERROR_CODE_UNSPECIFIED, - 'No result from Keystore' - ) + "No result from Keystore", + ); } - if ('encrypted' in response.result && !response.result.encrypted) { - throw new Error('Missing ciphertext') + if ("encrypted" in response.result && !response.result.encrypted) { + throw new Error("Missing ciphertext"); } - if ('decrypted' in response.result && !response.result.decrypted) { - throw new Error('Missing decrypted result') + if ("decrypted" in response.result && !response.result.decrypted) { + throw new Error("Missing decrypted result"); } - return response.result as EncryptionResponseResult -} + return response.result as EncryptionResponseResult; +}; export const buildDecryptV1Request = ( messages: MessageV1[], - myPublicKeyBundle: PublicKeyBundle + myPublicKeyBundle: PublicKeyBundle, ): keystore.DecryptV1Request => { return { requests: messages.map((m: MessageV1) => { const sender = new PublicKeyBundle({ identityKey: m.header.sender?.identityKey, preKey: m.header.sender?.preKey, - }) + }); - const isSender = myPublicKeyBundle.equals(sender) + const isSender = myPublicKeyBundle.equals(sender); return { payload: m.ciphertext, @@ -64,7 +64,7 @@ export const buildDecryptV1Request = ( : sender, headerBytes: m.headerBytes, isSender, - } + }; }), - } -} + }; +}; diff --git a/packages/js-sdk/src/utils/semver.ts b/packages/js-sdk/src/utils/semver.ts index 2f24d2e9a..19ec90216 100644 --- a/packages/js-sdk/src/utils/semver.ts +++ b/packages/js-sdk/src/utils/semver.ts @@ -1,62 +1,62 @@ export function semverParse(version: string) { - const [major, minor, ...patch] = version.split('.') + const [major, minor, ...patch] = version.split("."); return { major: Number(major), minor: Number(minor), // Keep patch as a string so that it can support prerelease versions - patch: patch.join('.'), - } + patch: patch.join("."), + }; } export function isSameMajorVersion(a?: string, b?: string): boolean { // If no version is provided, assume it is the same if (!a || !b) { - return true + return true; } - return semverParse(a).major === semverParse(b).major + return semverParse(a).major === semverParse(b).major; } // Checks if A semver is greater than B semver export function semverGreaterThan(a?: string, b?: string): boolean { if (!a || !b) { - return false + return false; } - const aSemver = semverParse(a) - const bSemver = semverParse(b) + const aSemver = semverParse(a); + const bSemver = semverParse(b); if (aSemver.major !== bSemver.major) { - return aSemver.major > bSemver.major + return aSemver.major > bSemver.major; } if (aSemver.minor !== bSemver.minor) { - return aSemver.minor > bSemver.minor + return aSemver.minor > bSemver.minor; } if (!aSemver.patch || !bSemver.patch) { - return false + return false; } - return patchGreaterThan(aSemver.patch, bSemver.patch) + return patchGreaterThan(aSemver.patch, bSemver.patch); } // Home-brewed attempt at comparing patch versions so we don't have to import semver package. // Example full version might be "2.0.1-alpha.1", and this will be operating on the "1-alpha.1" portion function patchGreaterThan(a: string, b: string): boolean { - const [aVersion, aExtra] = a.split('-') - const [bVersion, bExtra] = b.split('-') + const [aVersion, aExtra] = a.split("-"); + const [bVersion, bExtra] = b.split("-"); if (Number(aVersion) !== Number(bVersion)) { - return Number(aVersion) > Number(bVersion) + return Number(aVersion) > Number(bVersion); } if (!aExtra || !bExtra) { - return false + return false; } - const [aTag, aTagVersion] = aExtra.split('.') - const [bTag, bTagVersion] = bExtra.split('.') + const [aTag, aTagVersion] = aExtra.split("."); + const [bTag, bTagVersion] = bExtra.split("."); if (aTag !== bTag) { - return true + return true; } - return Number(aTagVersion) > Number(bTagVersion) + return Number(aTagVersion) > Number(bTagVersion); } diff --git a/packages/js-sdk/src/utils/topic.ts b/packages/js-sdk/src/utils/topic.ts index f706d321f..5fe55ff81 100644 --- a/packages/js-sdk/src/utils/topic.ts +++ b/packages/js-sdk/src/utils/topic.ts @@ -1,56 +1,56 @@ -import { getAddress } from 'viem' +import { getAddress } from "viem"; export const buildContentTopic = (name: string): string => - `/xmtp/0/${name}/proto` + `/xmtp/0/${name}/proto`; export const buildDirectMessageTopic = ( sender: string, - recipient: string + recipient: string, ): string => { // EIP55 normalize the address case. - const members = [getAddress(sender), getAddress(recipient)] - members.sort() - return buildContentTopic(`dm-${members.join('-')}`) -} + const members = [getAddress(sender), getAddress(recipient)]; + members.sort(); + return buildContentTopic(`dm-${members.join("-")}`); +}; export const buildDirectMessageTopicV2 = (randomString: string): string => { - return buildContentTopic(`m-${randomString}`) -} + return buildContentTopic(`m-${randomString}`); +}; export const buildUserContactTopic = (walletAddr: string): string => { // EIP55 normalize the address case. - return buildContentTopic(`contact-${getAddress(walletAddr)}`) -} + return buildContentTopic(`contact-${getAddress(walletAddr)}`); +}; export const buildUserIntroTopic = (walletAddr: string): string => { // EIP55 normalize the address case. - return buildContentTopic(`intro-${getAddress(walletAddr)}`) -} + return buildContentTopic(`intro-${getAddress(walletAddr)}`); +}; export const buildUserInviteTopic = (walletAddr: string): string => { // EIP55 normalize the address case. - return buildContentTopic(`invite-${getAddress(walletAddr)}`) -} + return buildContentTopic(`invite-${getAddress(walletAddr)}`); +}; export const buildUserPrivateStoreTopic = (addrPrefixedKey: string): string => { // e.g. "0x1111111111222222222233333333334444444444/key_bundle" - return buildContentTopic(`privatestore-${addrPrefixedKey}`) -} + return buildContentTopic(`privatestore-${addrPrefixedKey}`); +}; export const buildUserPrivatePreferencesTopic = (identifier: string) => - buildContentTopic(`userpreferences-${identifier}`) + buildContentTopic(`userpreferences-${identifier}`); // validate that a topic only contains ASCII characters 33-127 export const isValidTopic = (topic: string): boolean => { // eslint-disable-next-line no-control-regex - const regex = /^[\x21-\x7F]+$/ - const index = topic.indexOf('0/') + const regex = /^[\x21-\x7F]+$/; + const index = topic.indexOf("0/"); if (index !== -1) { const unwrappedTopic = topic.substring( index + 2, - topic.lastIndexOf('/proto') - ) - return regex.test(unwrappedTopic) + topic.lastIndexOf("/proto"), + ); + return regex.test(unwrappedTopic); } - return false -} + return false; +}; diff --git a/packages/js-sdk/src/utils/typedefs.ts b/packages/js-sdk/src/utils/typedefs.ts index 317892b71..de0038857 100644 --- a/packages/js-sdk/src/utils/typedefs.ts +++ b/packages/js-sdk/src/utils/typedefs.ts @@ -1,5 +1,5 @@ export type Flatten = { - [K in keyof T]: T[K] -} + [K in keyof T]: T[K]; +}; -export type WithoutUndefined = { [P in keyof T]: NonNullable } +export type WithoutUndefined = { [P in keyof T]: NonNullable }; diff --git a/packages/js-sdk/src/utils/viem.ts b/packages/js-sdk/src/utils/viem.ts index 1fd8f1d6d..a931d0a73 100644 --- a/packages/js-sdk/src/utils/viem.ts +++ b/packages/js-sdk/src/utils/viem.ts @@ -1,40 +1,40 @@ -import type { WalletClient } from 'viem' -import type { Signer } from '@/types/Signer' +import type { WalletClient } from "viem"; +import type { Signer } from "@/types/Signer"; export function getSigner(wallet: Signer | WalletClient | null): Signer | null { if (!wallet) { - return null + return null; } if (isWalletClient(wallet)) { - return convertWalletClientToSigner(wallet) + return convertWalletClientToSigner(wallet); } - if (typeof wallet.getAddress !== 'function') { - throw new Error('Unknown wallet type') + if (typeof wallet.getAddress !== "function") { + throw new Error("Unknown wallet type"); } - return wallet + return wallet; } function isWalletClient(wallet: Signer | WalletClient): wallet is WalletClient { return ( - 'type' in wallet && - (wallet.type === 'walletClient' || wallet.type === 'base') - ) + "type" in wallet && + (wallet.type === "walletClient" || wallet.type === "base") + ); } export function convertWalletClientToSigner( - walletClient: WalletClient + walletClient: WalletClient, ): Signer { - const { account } = walletClient + const { account } = walletClient; if (!account || !account.address) { - throw new Error('WalletClient is not configured') + throw new Error("WalletClient is not configured"); } return { getAddress: async () => account.address, signMessage: async (message: string | Uint8Array) => walletClient.signMessage({ - message: typeof message === 'string' ? message : { raw: message }, + message: typeof message === "string" ? message : { raw: message }, account, }), - } + }; } diff --git a/packages/js-sdk/test/ApiClient.test.ts b/packages/js-sdk/test/ApiClient.test.ts index ed049078d..4d056d4f2 100644 --- a/packages/js-sdk/test/ApiClient.test.ts +++ b/packages/js-sdk/test/ApiClient.test.ts @@ -1,139 +1,139 @@ -import { messageApi } from '@xmtp/proto' +import { messageApi } from "@xmtp/proto"; import type { InitReq, NotifyStreamEntityArrival, -} from '@xmtp/proto/ts/dist/types/fetch.pb' -import { vi } from 'vitest' +} from "@xmtp/proto/ts/dist/types/fetch.pb"; +import { vi } from "vitest"; import ApiClient, { GrpcError, GrpcStatus, type PublishParams, -} from '@/ApiClient' -import LocalAuthenticator from '@/authn/LocalAuthenticator' -import { PrivateKey } from '@/crypto/PrivateKey' -import { dateToNs } from '@/utils/date' +} from "@/ApiClient"; +import LocalAuthenticator from "@/authn/LocalAuthenticator"; +import { PrivateKey } from "@/crypto/PrivateKey"; +import { dateToNs } from "@/utils/date"; // eslint-disable-next-line no-restricted-syntax -import packageJson from '../package.json' -import { sleep } from './helpers' +import packageJson from "../package.json"; +import { sleep } from "./helpers"; -const { MessageApi } = messageApi +const { MessageApi } = messageApi; -const PATH_PREFIX = 'http://fake:5050' +const PATH_PREFIX = "http://fake:5050"; const CURSOR: messageApi.Cursor = { index: { digest: Uint8Array.from([1, 2, 3]), - senderTimeNs: '3', + senderTimeNs: "3", }, -} -const CONTENT_TOPIC = 'foo' -const AUTH_TOKEN = 'foo' +}; +const CONTENT_TOPIC = "foo"; +const AUTH_TOKEN = "foo"; -const client = new ApiClient(PATH_PREFIX) +const client = new ApiClient(PATH_PREFIX); const mockGetToken = vi.hoisted(() => vi.fn().mockReturnValue( Promise.resolve({ toBase64: () => AUTH_TOKEN, age: 10, - }) - ) -) -vi.mock('@/authn/LocalAuthenticator', () => { + }), + ), +); +vi.mock("@/authn/LocalAuthenticator", () => { return { default: vi.fn().mockImplementation(() => { - return { createToken: mockGetToken } + return { createToken: mockGetToken }; }), - } -}) + }; +}); -describe('Query', () => { +describe("Query", () => { beforeEach(() => { - vi.clearAllMocks() - }) - - it('stops when receiving empty results', async () => { - const apiMock = createQueryMock([], 1) - const result = await client.query({ contentTopic: CONTENT_TOPIC }, {}) - expect(result).toHaveLength(0) - expect(apiMock).toHaveBeenCalledTimes(1) + vi.clearAllMocks(); + }); + + it("stops when receiving empty results", async () => { + const apiMock = createQueryMock([], 1); + const result = await client.query({ contentTopic: CONTENT_TOPIC }, {}); + expect(result).toHaveLength(0); + expect(apiMock).toHaveBeenCalledTimes(1); const expectedReq: messageApi.QueryRequest = { - contentTopics: ['foo'], + contentTopics: ["foo"], pagingInfo: { direction: messageApi.SortDirection.SORT_DIRECTION_ASCENDING, limit: 100, }, - } + }; expect(apiMock).toHaveBeenCalledWith(expectedReq, { pathPrefix: PATH_PREFIX, - mode: 'cors', + mode: "cors", headers: new Headers({ - 'X-Client-Version': 'xmtp-js/' + packageJson.version, + "X-Client-Version": "xmtp-js/" + packageJson.version, }), - }) - }) + }); + }); - it('stops when limit is used', async () => { - const apiMock = createQueryMock([createEnvelope()], 3) + it("stops when limit is used", async () => { + const apiMock = createQueryMock([createEnvelope()], 3); const result = await client.query( { contentTopic: CONTENT_TOPIC }, - { limit: 2 } - ) - expect(result).toHaveLength(2) - expect(apiMock).toHaveBeenCalledTimes(2) - }) - - it('stops when receiving some results and a null cursor', async () => { - const apiMock = createQueryMock([createEnvelope()], 1) - const result = await client.query({ contentTopic: CONTENT_TOPIC }, {}) - expect(result).toHaveLength(1) - expect(apiMock).toHaveBeenCalledTimes(1) - }) - - it('gets multiple pages of results', async () => { - const apiMock = createQueryMock([createEnvelope(), createEnvelope()], 2) - const result = await client.query({ contentTopic: CONTENT_TOPIC }, {}) - expect(result).toHaveLength(4) - expect(apiMock).toHaveBeenCalledTimes(2) - }) - - it('streams a single page of results', async () => { - const apiMock = createQueryMock([createEnvelope(), createEnvelope()], 1) - let count = 0 + { limit: 2 }, + ); + expect(result).toHaveLength(2); + expect(apiMock).toHaveBeenCalledTimes(2); + }); + + it("stops when receiving some results and a null cursor", async () => { + const apiMock = createQueryMock([createEnvelope()], 1); + const result = await client.query({ contentTopic: CONTENT_TOPIC }, {}); + expect(result).toHaveLength(1); + expect(apiMock).toHaveBeenCalledTimes(1); + }); + + it("gets multiple pages of results", async () => { + const apiMock = createQueryMock([createEnvelope(), createEnvelope()], 2); + const result = await client.query({ contentTopic: CONTENT_TOPIC }, {}); + expect(result).toHaveLength(4); + expect(apiMock).toHaveBeenCalledTimes(2); + }); + + it("streams a single page of results", async () => { + const apiMock = createQueryMock([createEnvelope(), createEnvelope()], 1); + let count = 0; for await (const _envelope of client.queryIterator( - { contentTopic: 'foo' }, - { pageSize: 5 } + { contentTopic: "foo" }, + { pageSize: 5 }, )) { - count++ + count++; } - expect(count).toBe(2) - expect(apiMock).toHaveBeenCalledTimes(1) + expect(count).toBe(2); + expect(apiMock).toHaveBeenCalledTimes(1); const expectedReq: messageApi.QueryRequest = { - contentTopics: ['foo'], + contentTopics: ["foo"], pagingInfo: { limit: 5, }, - } + }; expect(apiMock).toHaveBeenCalledWith(expectedReq, { pathPrefix: PATH_PREFIX, - mode: 'cors', + mode: "cors", headers: new Headers({ - 'X-Client-Version': 'xmtp-js/' + packageJson.version, + "X-Client-Version": "xmtp-js/" + packageJson.version, }), - }) - }) + }); + }); - it('streams multiple pages of results', async () => { - const apiMock = createQueryMock([createEnvelope(), createEnvelope()], 2) - let count = 0 + it("streams multiple pages of results", async () => { + const apiMock = createQueryMock([createEnvelope(), createEnvelope()], 2); + let count = 0; for await (const _envelope of client.queryIterator( { contentTopic: CONTENT_TOPIC }, - { pageSize: 5 } + { pageSize: 5 }, )) { - count++ + count++; } - expect(count).toBe(4) - expect(apiMock).toHaveBeenCalledTimes(2) + expect(count).toBe(4); + expect(apiMock).toHaveBeenCalledTimes(2); const expectedReq: messageApi.QueryRequest = { contentTopics: [CONTENT_TOPIC], @@ -141,41 +141,41 @@ describe('Query', () => { limit: 5, cursor: CURSOR, }, - } + }; expect(apiMock).toHaveBeenLastCalledWith(expectedReq, { pathPrefix: PATH_PREFIX, - mode: 'cors', + mode: "cors", headers: new Headers({ - 'X-Client-Version': 'xmtp-js/' + packageJson.version, + "X-Client-Version": "xmtp-js/" + packageJson.version, }), - }) - }) -}) + }); + }); +}); -describe('Publish', () => { - const publishMock = createPublishMock() - let publishClient: ApiClient +describe("Publish", () => { + const publishMock = createPublishMock(); + let publishClient: ApiClient; beforeEach(() => { - publishMock.mockClear() - publishClient = new ApiClient(PATH_PREFIX, { appVersion: 'test/0.0.0' }) - }) + publishMock.mockClear(); + publishClient = new ApiClient(PATH_PREFIX, { appVersion: "test/0.0.0" }); + }); - it('publishes valid messages', async () => { + it("publishes valid messages", async () => { // This Authenticator will not actually be used by the mock publishClient.setAuthenticator( - new LocalAuthenticator(PrivateKey.generate()) - ) + new LocalAuthenticator(PrivateKey.generate()), + ); - const now = new Date() + const now = new Date(); const msg: PublishParams = { timestamp: now, message: Uint8Array.from([1, 2, 3]), contentTopic: CONTENT_TOPIC, - } + }; - await publishClient.publish([msg]) - expect(publishMock).toHaveBeenCalledTimes(1) + await publishClient.publish([msg]); + expect(publishMock).toHaveBeenCalledTimes(1); const expectedRequest: messageApi.PublishRequest = { envelopes: [ { @@ -184,249 +184,249 @@ describe('Publish', () => { timestampNs: dateToNs(now).toString(), }, ], - } + }; expect(publishMock).toHaveBeenCalledWith(expectedRequest, { pathPrefix: PATH_PREFIX, - mode: 'cors', + mode: "cors", headers: new Headers({ - 'X-Client-Version': 'xmtp-js/' + packageJson.version, - 'X-App-Version': 'test/0.0.0', + "X-Client-Version": "xmtp-js/" + packageJson.version, + "X-App-Version": "test/0.0.0", Authorization: `Bearer ${AUTH_TOKEN}`, }), - }) - }) + }); + }); - it('throws on invalid message', () => { + it("throws on invalid message", () => { const promise = client.publish([ { - contentTopic: '', + contentTopic: "", message: Uint8Array.from([]), }, - ]) - expect(promise).rejects.toThrow('Content topic cannot be empty string') - }) -}) + ]); + expect(promise).rejects.toThrow("Content topic cannot be empty string"); + }); +}); -describe('Publish authn', () => { - let publishClient: ApiClient +describe("Publish authn", () => { + let publishClient: ApiClient; beforeEach(() => { - publishClient = new ApiClient(PATH_PREFIX) - }) + publishClient = new ApiClient(PATH_PREFIX); + }); - it('retries on invalid message', async () => { - const publishMock = createAuthErrorPublishMock(1) + it("retries on invalid message", async () => { + const publishMock = createAuthErrorPublishMock(1); publishClient.setAuthenticator( - new LocalAuthenticator(PrivateKey.generate()) - ) + new LocalAuthenticator(PrivateKey.generate()), + ); - const now = new Date() + const now = new Date(); const msg: PublishParams = { timestamp: now, message: Uint8Array.from([1, 2, 3]), contentTopic: CONTENT_TOPIC, - } - await publishClient.publish([msg]) - expect(publishMock).toHaveBeenCalledTimes(2) - }) + }; + await publishClient.publish([msg]); + expect(publishMock).toHaveBeenCalledTimes(2); + }); - it('gives up after a second auth error', async () => { - const publishMock = createAuthErrorPublishMock(5) + it("gives up after a second auth error", async () => { + const publishMock = createAuthErrorPublishMock(5); publishClient.setAuthenticator( - new LocalAuthenticator(PrivateKey.generate()) - ) + new LocalAuthenticator(PrivateKey.generate()), + ); - const now = new Date() + const now = new Date(); const msg: PublishParams = { timestamp: now, message: Uint8Array.from([1, 2, 3]), contentTopic: CONTENT_TOPIC, - } + }; try { - await publishClient.publish([msg]) + await publishClient.publish([msg]); } catch (e: any) { - expect(e.code).toBe(GrpcStatus.UNAUTHENTICATED) + expect(e.code).toBe(GrpcStatus.UNAUTHENTICATED); } - expect(publishMock).toHaveBeenCalledTimes(2) - }) -}) + expect(publishMock).toHaveBeenCalledTimes(2); + }); +}); -describe('Subscribe', () => { +describe("Subscribe", () => { beforeEach(() => { - vi.clearAllMocks() - }) + vi.clearAllMocks(); + }); - it('can subscribe', async () => { - const subscribeMock = createSubscribeMock(2) - let numEnvelopes = 0 + it("can subscribe", async () => { + const subscribeMock = createSubscribeMock(2); + let numEnvelopes = 0; const cb = () => { - numEnvelopes++ - } - const req = { contentTopics: [CONTENT_TOPIC] } - const subscriptionManager = client.subscribe(req, cb) - await sleep(10) - expect(numEnvelopes).toBe(2) + numEnvelopes++; + }; + const req = { contentTopics: [CONTENT_TOPIC] }; + const subscriptionManager = client.subscribe(req, cb); + await sleep(10); + expect(numEnvelopes).toBe(2); expect(subscribeMock).toBeCalledWith(req, expect.anything(), { pathPrefix: PATH_PREFIX, signal: expect.anything(), - mode: 'cors', + mode: "cors", headers: new Headers({ - 'X-Client-Version': 'xmtp-js/' + packageJson.version, + "X-Client-Version": "xmtp-js/" + packageJson.version, }), - }) - await subscriptionManager.unsubscribe() - }) + }); + await subscriptionManager.unsubscribe(); + }); - it('should resubscribe on error', async () => { - let called = 0 + it("should resubscribe on error", async () => { + let called = 0; const subscribeMock = vi - .spyOn(MessageApi, 'Subscribe') + .spyOn(MessageApi, "Subscribe") .mockImplementation( async ( req: messageApi.SubscribeRequest, cb: NotifyStreamEntityArrival | undefined, - initReq?: InitReq + initReq?: InitReq, ): Promise => { // We mock a connection stream that immediately errors the first time // it is called. The second time it is called, it behaves as expected (the connection // stays open, and two messages are received over the subscription) - called++ + called++; if (called === 1) { - throw new Error('error') + throw new Error("error"); } - const nonErroringSubscribe = subscribeMockImplementation(2) - return await nonErroringSubscribe(req, cb, initReq) - } - ) - const consoleInfo = vi.spyOn(console, 'info').mockImplementation(() => {}) - let numEnvelopes = 0 + const nonErroringSubscribe = subscribeMockImplementation(2); + return await nonErroringSubscribe(req, cb, initReq); + }, + ); + const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {}); + let numEnvelopes = 0; const cb = () => { - numEnvelopes++ - } - let numDisconnects = 0 + numEnvelopes++; + }; + let numDisconnects = 0; const onDisconnect = () => { - numDisconnects++ - } - const req = { contentTopics: [CONTENT_TOPIC] } - const subscriptionManager = client.subscribe(req, cb, onDisconnect) - await sleep(1200) - expect(numEnvelopes).toBe(2) - expect(numDisconnects).toBe(1) + numDisconnects++; + }; + const req = { contentTopics: [CONTENT_TOPIC] }; + const subscriptionManager = client.subscribe(req, cb, onDisconnect); + await sleep(1200); + expect(numEnvelopes).toBe(2); + expect(numDisconnects).toBe(1); // Resubscribing triggers an info log - expect(consoleInfo).toBeCalledTimes(1) - expect(subscribeMock).toBeCalledTimes(2) + expect(consoleInfo).toBeCalledTimes(1); + expect(subscribeMock).toBeCalledTimes(2); expect(subscribeMock).toBeCalledWith(req, expect.anything(), { pathPrefix: PATH_PREFIX, signal: expect.anything(), - mode: 'cors', + mode: "cors", headers: new Headers({ - 'X-Client-Version': 'xmtp-js/' + packageJson.version, + "X-Client-Version": "xmtp-js/" + packageJson.version, }), - }) - consoleInfo.mockRestore() - await subscriptionManager.unsubscribe() - }) + }); + consoleInfo.mockRestore(); + await subscriptionManager.unsubscribe(); + }); - it('should resubscribe on completion', async () => { - let called = 0 + it("should resubscribe on completion", async () => { + let called = 0; const subscribeMock = vi - .spyOn(MessageApi, 'Subscribe') + .spyOn(MessageApi, "Subscribe") .mockImplementation( async ( req: messageApi.SubscribeRequest, cb: NotifyStreamEntityArrival | undefined, - initReq?: InitReq + initReq?: InitReq, ): Promise => { // We mock a connection stream that immediately terminates the first time // it is called. The second time it is called, it behaves as expected (the connection // stays open, and two messages are received over the subscription) - called++ + called++; if (called === 1) { - return + return; } - const nonAbortingSubscribe = subscribeMockImplementation(2) - return await nonAbortingSubscribe(req, cb, initReq) - } - ) - const consoleInfo = vi.spyOn(console, 'info').mockImplementation(() => {}) - let numEnvelopes = 0 + const nonAbortingSubscribe = subscribeMockImplementation(2); + return await nonAbortingSubscribe(req, cb, initReq); + }, + ); + const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {}); + let numEnvelopes = 0; const cb = () => { - numEnvelopes++ - } - const req = { contentTopics: [CONTENT_TOPIC] } - const subscriptionManager = client.subscribe(req, cb) - await sleep(1200) - expect(numEnvelopes).toBe(2) + numEnvelopes++; + }; + const req = { contentTopics: [CONTENT_TOPIC] }; + const subscriptionManager = client.subscribe(req, cb); + await sleep(1200); + expect(numEnvelopes).toBe(2); // Resubscribing triggers an info log - expect(consoleInfo).toBeCalledTimes(1) - expect(subscribeMock).toBeCalledTimes(2) + expect(consoleInfo).toBeCalledTimes(1); + expect(subscribeMock).toBeCalledTimes(2); expect(subscribeMock).toBeCalledWith(req, expect.anything(), { pathPrefix: PATH_PREFIX, signal: expect.anything(), - mode: 'cors', + mode: "cors", headers: new Headers({ - 'X-Client-Version': 'xmtp-js/' + packageJson.version, + "X-Client-Version": "xmtp-js/" + packageJson.version, }), - }) - consoleInfo.mockRestore() - await subscriptionManager.unsubscribe() - }) - - it('throws when no content topics returned', async () => { - createSubscribeMock(2) - const cb = () => {} - const req = { contentTopics: [] } - const t = () => client.subscribe(req, cb) + }); + consoleInfo.mockRestore(); + await subscriptionManager.unsubscribe(); + }); + + it("throws when no content topics returned", async () => { + createSubscribeMock(2); + const cb = () => {}; + const req = { contentTopics: [] }; + const t = () => client.subscribe(req, cb); expect(t).toThrow( - new Error('Must provide list of contentTopics to subscribe to') - ) - }) -}) + new Error("Must provide list of contentTopics to subscribe to"), + ); + }); +}); function createQueryMock(envelopes: messageApi.Envelope[], numPages = 1) { - let numCalls = 0 + let numCalls = 0; return vi - .spyOn(MessageApi, 'Query') + .spyOn(MessageApi, "Query") .mockImplementation(async (): Promise => { - numCalls++ + numCalls++; return { envelopes, pagingInfo: { cursor: numCalls >= numPages ? undefined : CURSOR, }, - } - }) + }; + }); } function createPublishMock() { return vi - .spyOn(MessageApi, 'Publish') - .mockImplementation(async (): Promise => ({})) + .spyOn(MessageApi, "Publish") + .mockImplementation(async (): Promise => ({})); } function createAuthErrorPublishMock(rejectTimes = 1) { - let numRejections = 0 + let numRejections = 0; return vi - .spyOn(MessageApi, 'Publish') + .spyOn(MessageApi, "Publish") .mockImplementation(async (): Promise => { if (numRejections < rejectTimes) { - numRejections++ + numRejections++; throw GrpcError.fromObject({ code: 16, - message: 'UNAUTHENTICATED', - }) + message: "UNAUTHENTICATED", + }); } - return {} - }) + return {}; + }); } function createSubscribeMock(numMessages: number) { return vi - .spyOn(MessageApi, 'Subscribe') - .mockImplementation(subscribeMockImplementation(numMessages)) + .spyOn(MessageApi, "Subscribe") + .mockImplementation(subscribeMockImplementation(numMessages)); } // Subscribes to a connection stream that pushes down the number of messages specified. @@ -436,26 +436,26 @@ function subscribeMockImplementation(numMessages: number) { const subscribe = async ( req: messageApi.SubscribeRequest, cb: NotifyStreamEntityArrival | undefined, - initReq?: InitReq + initReq?: InitReq, ): Promise => { for (let i = 0; i < numMessages; i++) { if (cb) { - cb(createEnvelope()) + cb(createEnvelope()); } } // Connection stream is expected to stay open until terminated const connectionClosePromise = new Promise((resolve) => { - initReq!.signal!.onabort = () => resolve(null) - }) - await connectionClosePromise - } - return subscribe + initReq!.signal!.onabort = () => resolve(null); + }); + await connectionClosePromise; + }; + return subscribe; } function createEnvelope(): messageApi.Envelope { return { contentTopic: CONTENT_TOPIC, - timestampNs: '1', + timestampNs: "1", message: Uint8Array.from([1, 2, 3]), - } + }; } diff --git a/packages/js-sdk/test/ApiClientE2E.test.ts b/packages/js-sdk/test/ApiClientE2E.test.ts index 8ab304c78..67c6cdcc1 100644 --- a/packages/js-sdk/test/ApiClientE2E.test.ts +++ b/packages/js-sdk/test/ApiClientE2E.test.ts @@ -1,63 +1,63 @@ -import type { Wallet } from 'ethers' -import ApiClient, { ApiUrls, GrpcStatus } from '@/ApiClient' -import LocalAuthenticator from '@/authn/LocalAuthenticator' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import { buildUserPrivateStoreTopic } from '@/utils/topic' -import { newWallet } from './helpers' +import type { Wallet } from "ethers"; +import ApiClient, { ApiUrls, GrpcStatus } from "@/ApiClient"; +import LocalAuthenticator from "@/authn/LocalAuthenticator"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import { buildUserPrivateStoreTopic } from "@/utils/topic"; +import { newWallet } from "./helpers"; -type TestCase = { name: string; api: string } +type TestCase = { name: string; api: string }; -describe('e2e tests', () => { +describe("e2e tests", () => { const tests: TestCase[] = [ { - name: 'local docker node', + name: "local docker node", api: ApiUrls.local, }, - ] + ]; if (process.env.CI || process.env.TESTNET) { tests.push({ - name: 'dev', + name: "dev", api: ApiUrls.dev, - }) + }); } tests.forEach((testCase) => { describe(testCase.name, () => { - let client: ApiClient - let wallet: Wallet - let keys: PrivateKeyBundleV1 + let client: ApiClient; + let wallet: Wallet; + let keys: PrivateKeyBundleV1; beforeEach(async () => { - wallet = newWallet() - client = new ApiClient(testCase.api) - keys = await PrivateKeyBundleV1.generate(wallet) - client.setAuthenticator(new LocalAuthenticator(keys.identityKey)) - }) + wallet = newWallet(); + client = new ApiClient(testCase.api); + keys = await PrivateKeyBundleV1.generate(wallet); + client.setAuthenticator(new LocalAuthenticator(keys.identityKey)); + }); - it('publish success', async () => { + it("publish success", async () => { await expect( client.publish([ { contentTopic: buildUserPrivateStoreTopic(wallet.address), message: new Uint8Array(5), }, - ]) - ).resolves - }) + ]), + ).resolves; + }); - it('publish restricted topic', () => { + it("publish restricted topic", () => { expect( client.publish([ { contentTopic: buildUserPrivateStoreTopic( - keys.getPublicKeyBundle().preKey.getEthereumAddress() + keys.getPublicKeyBundle().preKey.getEthereumAddress(), ), message: new Uint8Array(5), }, - ]) + ]), ).rejects.toMatchObject({ code: GrpcStatus.PERMISSION_DENIED, - message: 'publishing to restricted topic', - }) - }) - }) - }) -}) + message: "publishing to restricted topic", + }); + }); + }); + }); +}); diff --git a/packages/js-sdk/test/BackupClient.test.ts b/packages/js-sdk/test/BackupClient.test.ts index 7c81ee36d..8fc27e542 100644 --- a/packages/js-sdk/test/BackupClient.test.ts +++ b/packages/js-sdk/test/BackupClient.test.ts @@ -1,15 +1,15 @@ -import { BackupType } from '@/message-backup/BackupClient' -import { newDevClient, newLocalHostClient } from './helpers' +import { BackupType } from "@/message-backup/BackupClient"; +import { newDevClient, newLocalHostClient } from "./helpers"; -describe('Backup configuration', () => { - it('Uses XMTP backup for localhost', async function () { - const c = await newLocalHostClient() - expect(c.backupType).toEqual(BackupType.xmtpTopicStore) - }) +describe("Backup configuration", () => { + it("Uses XMTP backup for localhost", async function () { + const c = await newLocalHostClient(); + expect(c.backupType).toEqual(BackupType.xmtpTopicStore); + }); if (process.env.CI || process.env.TESTNET) { - it('Uses no backup for dev', async function () { - const c = await newDevClient() - expect(c.backupType).toEqual(BackupType.none) - }) + it("Uses no backup for dev", async function () { + const c = await newDevClient(); + expect(c.backupType).toEqual(BackupType.none); + }); } -}) +}); diff --git a/packages/js-sdk/test/Client.test.ts b/packages/js-sdk/test/Client.test.ts index 4903c1c1b..bd4e5a041 100644 --- a/packages/js-sdk/test/Client.test.ts +++ b/packages/js-sdk/test/Client.test.ts @@ -2,191 +2,193 @@ import { ContentTypeId, type ContentCodec, type EncodedContent, -} from '@xmtp/content-type-primitives' -import { ContentTypeText, TextCodec } from '@xmtp/content-type-text' -import { message } from '@xmtp/proto' -import type { PublishResponse } from '@xmtp/proto/ts/dist/types/message_api/v1/message_api.pb' -import { Wallet } from 'ethers' -import { createWalletClient, http } from 'viem' -import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' -import { mainnet } from 'viem/chains' -import { assert, vi } from 'vitest' -import HttpApiClient, { ApiUrls } from '@/ApiClient' -import Client, { Compression, type ClientOptions } from '@/Client' -import { PrivateKey } from '@/crypto/PrivateKey' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' -import LocalStoragePonyfill from '@/keystore/persistence/LocalStoragePonyfill' -import TopicPersistence from '@/keystore/persistence/TopicPersistence' -import NetworkKeyManager from '@/keystore/providers/NetworkKeyManager' -import NetworkKeystoreProvider from '@/keystore/providers/NetworkKeystoreProvider' -import type { EnvelopeWithMessage } from '@/utils/async' -import { buildUserContactTopic } from '@/utils/topic' -import { ContentTypeTestKey, TestKeyCodec } from './ContentTypeTestKey' +} from "@xmtp/content-type-primitives"; +import { ContentTypeText, TextCodec } from "@xmtp/content-type-text"; +import { message } from "@xmtp/proto"; +import type { PublishResponse } from "@xmtp/proto/ts/dist/types/message_api/v1/message_api.pb"; +import { Wallet } from "ethers"; +import { createWalletClient, http } from "viem"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { mainnet } from "viem/chains"; +import { assert, vi } from "vitest"; +import HttpApiClient, { ApiUrls } from "@/ApiClient"; +import Client, { Compression, type ClientOptions } from "@/Client"; +import { PrivateKey } from "@/crypto/PrivateKey"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; +import LocalStoragePonyfill from "@/keystore/persistence/LocalStoragePonyfill"; +import TopicPersistence from "@/keystore/persistence/TopicPersistence"; +import NetworkKeyManager from "@/keystore/providers/NetworkKeyManager"; +import NetworkKeystoreProvider from "@/keystore/providers/NetworkKeystoreProvider"; +import type { EnvelopeWithMessage } from "@/utils/async"; +import { buildUserContactTopic } from "@/utils/topic"; +import { ContentTypeTestKey, TestKeyCodec } from "./ContentTypeTestKey"; import { newDevClient, newLocalHostClient, newLocalHostClientWithCustomWallet, newWallet, waitForUserContact, -} from './helpers' +} from "./helpers"; type TestCase = { - name: string - newClient: (opts?: Partial) => Promise> -} + name: string; + newClient: (opts?: Partial) => Promise>; +}; -const mockEthRequest = vi.hoisted(() => vi.fn()) -vi.mock('@/utils/ethereum', () => { +const mockEthRequest = vi.hoisted(() => vi.fn()); +vi.mock("@/utils/ethereum", () => { return { __esModule: true, getEthereum: vi.fn(() => { const ethereum: any = { request: mockEthRequest, - } - ethereum.providers = [ethereum] - ethereum.detected = [ethereum] - ethereum.isMetaMask = true - return ethereum + }; + ethereum.providers = [ethereum]; + ethereum.detected = [ethereum]; + ethereum.isMetaMask = true; + return ethereum; }), - } -}) + }; +}); -describe('Client', () => { +describe("Client", () => { const tests: TestCase[] = [ { - name: 'local host node', + name: "local host node", newClient: newLocalHostClient, }, { - name: 'local host node with non-ethers wallet', + name: "local host node with non-ethers wallet", newClient: newLocalHostClientWithCustomWallet, }, - ] + ]; if (process.env.CI || process.env.TESTNET) { tests.push({ - name: 'dev', + name: "dev", newClient: newDevClient, - }) + }); } tests.forEach((testCase) => { describe(testCase.name, () => { - let alice: Client, bob: Client + let alice: Client, bob: Client; beforeEach(async () => { - alice = await testCase.newClient({ publishLegacyContact: true }) - bob = await testCase.newClient({ publishLegacyContact: true }) - await waitForUserContact(alice, alice) - await waitForUserContact(bob, bob) - }) - - it('user contacts published', async () => { - const alicePublic = await alice.getUserContact(alice.address) - expect(alicePublic).toEqual(alice.publicKeyBundle) - const bobPublic = await bob.getUserContact(bob.address) - expect(bobPublic).toEqual(bob.publicKeyBundle) - }) - - it('user contacts are filtered to valid contacts', async () => { + alice = await testCase.newClient({ publishLegacyContact: true }); + bob = await testCase.newClient({ publishLegacyContact: true }); + await waitForUserContact(alice, alice); + await waitForUserContact(bob, bob); + }); + + it("user contacts published", async () => { + const alicePublic = await alice.getUserContact(alice.address); + expect(alicePublic).toEqual(alice.publicKeyBundle); + const bobPublic = await bob.getUserContact(bob.address); + expect(bobPublic).toEqual(bob.publicKeyBundle); + }); + + it("user contacts are filtered to valid contacts", async () => { // publish bob's keys to alice's contact topic - const bobPublic = bob.publicKeyBundle + const bobPublic = bob.publicKeyBundle; await alice.publishEnvelopes([ { message: bobPublic.toBytes(), contentTopic: buildUserContactTopic(alice.address), }, - ]) - const alicePublic = await alice.getUserContact(alice.address) - expect(alicePublic).toEqual(alice.publicKeyBundle) - }) + ]); + const alicePublic = await alice.getUserContact(alice.address); + expect(alicePublic).toEqual(alice.publicKeyBundle); + }); - it('Check address can be sent to', async () => { - const canMessageA = await alice.canMessage('NOT AN ADDRESS') - expect(canMessageA).toBe(false) + it("Check address can be sent to", async () => { + const canMessageA = await alice.canMessage("NOT AN ADDRESS"); + expect(canMessageA).toBe(false); - const canMessageB = await alice.canMessage(bob.address) - expect(canMessageB).toBe(true) + const canMessageB = await alice.canMessage(bob.address); + expect(canMessageB).toBe(true); - const lower = await alice.canMessage(bob.address.toLowerCase()) - expect(lower).toBe(true) - }) - }) - }) -}) + const lower = await alice.canMessage(bob.address.toLowerCase()); + expect(lower).toBe(true); + }); + }); + }); +}); -describe('bootstrapping', () => { - let alice: Wallet +describe("bootstrapping", () => { + let alice: Wallet; beforeEach(async () => { - alice = newWallet() - }) + alice = newWallet(); + }); - it('can bootstrap with a new wallet and persist the private key bundle', async () => { - const client = await Client.create(alice, { env: 'local' }) + it("can bootstrap with a new wallet and persist the private key bundle", async () => { + const client = await Client.create(alice, { env: "local" }); const manager = new NetworkKeyManager( alice, - new TopicPersistence(client.apiClient) - ) - const loadedBundle = await manager.loadPrivateKeyBundle() - expect(loadedBundle).toBeInstanceOf(PrivateKeyBundleV1) + new TopicPersistence(client.apiClient), + ); + const loadedBundle = await manager.loadPrivateKeyBundle(); + expect(loadedBundle).toBeInstanceOf(PrivateKeyBundleV1); expect( - loadedBundle?.identityKey.publicKey.walletSignatureAddress() - ).toEqual(alice.address) - }) + loadedBundle?.identityKey.publicKey.walletSignatureAddress(), + ).toEqual(alice.address); + }); - it('fails to load if no valid keystore provider is available', async () => { + it("fails to load if no valid keystore provider is available", async () => { expect( - Client.create(alice, { env: 'local', keystoreProviders: [] }) - ).rejects.toThrow('No keystore providers available') - }) + Client.create(alice, { env: "local", keystoreProviders: [] }), + ).rejects.toThrow("No keystore providers available"); + }); - it('is able to bootstrap from the network', async () => { - const opts: Partial = { env: 'local' } + it("is able to bootstrap from the network", async () => { + const opts: Partial = { env: "local" }; // Create with the default keystore providers to ensure bootstrapping - const firstClient = await Client.create(alice, opts) + const firstClient = await Client.create(alice, opts); const secondClient = await Client.create(alice, { ...opts, keystoreProviders: [new NetworkKeystoreProvider()], - }) - expect(secondClient).toBeInstanceOf(Client) - expect(secondClient.address).toEqual(firstClient.address) - }) + }); + expect(secondClient).toBeInstanceOf(Client); + expect(secondClient.address).toEqual(firstClient.address); + }); - it('is able to bootstrap from a predefined private key', async () => { - const opts: Partial = { env: 'local' } - const keys = await Client.getKeys(alice, opts) + it("is able to bootstrap from a predefined private key", async () => { + const opts: Partial = { env: "local" }; + const keys = await Client.getKeys(alice, opts); const client = await Client.create(null, { ...opts, privateKeyOverride: keys, - }) - expect(client.address).toEqual(alice.address) - }) -}) - -describe('skipContactPublishing', () => { - it('skips publishing when flag is set to true', async () => { - const alice = newWallet() - await Client.create(alice, { skipContactPublishing: true, env: 'local' }) - expect(await Client.canMessage(alice.address, { env: 'local' })).toBeFalsy() - }) - - it('publishes contact when flag is false', async () => { - const alice = newWallet() - await Client.create(alice, { skipContactPublishing: false, env: 'local' }) + }); + expect(client.address).toEqual(alice.address); + }); +}); + +describe("skipContactPublishing", () => { + it("skips publishing when flag is set to true", async () => { + const alice = newWallet(); + await Client.create(alice, { skipContactPublishing: true, env: "local" }); expect( - await Client.canMessage(alice.address, { env: 'local' }) - ).toBeTruthy() - }) -}) + await Client.canMessage(alice.address, { env: "local" }), + ).toBeFalsy(); + }); -describe('encodeContent', () => { - it('passes deflate compression option through properly', async function () { - const c = await newLocalHostClient() - const utf8Encode = new TextEncoder() - const uncompressed = utf8Encode.encode('hello world '.repeat(20)) + it("publishes contact when flag is false", async () => { + const alice = newWallet(); + await Client.create(alice, { skipContactPublishing: false, env: "local" }); + expect( + await Client.canMessage(alice.address, { env: "local" }), + ).toBeTruthy(); + }); +}); + +describe("encodeContent", () => { + it("passes deflate compression option through properly", async function () { + const c = await newLocalHostClient(); + const utf8Encode = new TextEncoder(); + const uncompressed = utf8Encode.encode("hello world ".repeat(20)); const compressed = Uint8Array.from([ 10, 18, 10, 8, 120, 109, 116, 112, 46, 111, 114, 103, 18, 4, 116, 101, @@ -195,244 +197,244 @@ describe('encodeContent', () => { 48, 212, 49, 52, 176, 128, 96, 67, 67, 29, 99, 35, 29, 67, 67, 75, 48, 211, 208, 208, 4, 42, 101, 0, 22, 30, 85, 61, 170, 122, 84, 53, 237, 85, 3, 0, 139, 43, 173, 229, - ]) + ]); const { payload } = await c.encodeContent(uncompressed, { compression: Compression.COMPRESSION_DEFLATE, - }) - expect(Uint8Array.from(payload)).toEqual(compressed) - }) + }); + expect(Uint8Array.from(payload)).toEqual(compressed); + }); - it('returns shouldPush based on content codec', async () => { - const alice = await newLocalHostClient() - alice.registerCodec(new TestKeyCodec()) + it("returns shouldPush based on content codec", async () => { + const alice = await newLocalHostClient(); + alice.registerCodec(new TestKeyCodec()); - const { shouldPush: result1 } = await alice.encodeContent('gm') - expect(result1).toBe(true) + const { shouldPush: result1 } = await alice.encodeContent("gm"); + expect(result1).toBe(true); - const key = PrivateKey.generate().publicKey + const key = PrivateKey.generate().publicKey; const { shouldPush: result2 } = await alice.encodeContent(key, { contentType: ContentTypeTestKey, - }) - expect(result2).toBe(false) - }) -}) + }); + expect(result2).toBe(false); + }); +}); -describe('canMessage', () => { - it('can confirm a user is on the network statically', async () => { +describe("canMessage", () => { + it("can confirm a user is on the network statically", async () => { const registeredClient = await newLocalHostClient({ codecs: [new TextCodec()], - }) - await waitForUserContact(registeredClient, registeredClient) + }); + await waitForUserContact(registeredClient, registeredClient); const canMessageRegisteredClient = await Client.canMessage( registeredClient.address, { - env: 'local', - } - ) - expect(canMessageRegisteredClient).toBeTruthy() + env: "local", + }, + ); + expect(canMessageRegisteredClient).toBeTruthy(); const canMessageUnregisteredClient = await Client.canMessage( newWallet().address, - { env: 'local' } - ) - expect(canMessageUnregisteredClient).toBeFalsy() - }) -}) - -describe('canMessageBatch', () => { - it('can confirm multiple users are on the network statically', async () => { + { env: "local" }, + ); + expect(canMessageUnregisteredClient).toBeFalsy(); + }); +}); + +describe("canMessageBatch", () => { + it("can confirm multiple users are on the network statically", async () => { // Create 10 registered clients const registeredClients = await Promise.all( - Array.from({ length: 10 }, () => newLocalHostClient()) - ) + Array.from({ length: 10 }, () => newLocalHostClient()), + ); // Wait for all clients to be registered await Promise.all( - registeredClients.map((client) => waitForUserContact(client, client)) - ) + registeredClients.map((client) => waitForUserContact(client, client)), + ); // Now call canMessage with all of the peerAddresses const canMessageRegisteredClients = await Client.canMessage( registeredClients.map((client) => client.address), { - env: 'local', - } - ) + env: "local", + }, + ); // Expect all of the clients to be registered, so response should be all True expect(canMessageRegisteredClients).toEqual( - registeredClients.map(() => true) - ) + registeredClients.map(() => true), + ); const canMessageUnregisteredClient = await Client.canMessage( [newWallet().address], - { env: 'local' } - ) - expect(canMessageUnregisteredClient).toEqual([false]) - }) -}) - -describe('canMessageMultipleBatches', () => { - it('can confirm many multiple users are on the network statically', async () => { + { env: "local" }, + ); + expect(canMessageUnregisteredClient).toEqual([false]); + }); +}); + +describe("canMessageMultipleBatches", () => { + it("can confirm many multiple users are on the network statically", async () => { const registeredClients = await Promise.all( - Array.from({ length: 10 }, () => newLocalHostClient()) - ) + Array.from({ length: 10 }, () => newLocalHostClient()), + ); // Wait for all clients to be registered await Promise.all( - registeredClients.map((client) => waitForUserContact(client, client)) - ) + registeredClients.map((client) => waitForUserContact(client, client)), + ); // Repeat registeredClients 8 times to arrive at 80 addresses const initialPeerAddresses = registeredClients.map( - (client) => client.address - ) - const repeatedPeerAddresses: string[] = [] + (client) => client.address, + ); + const repeatedPeerAddresses: string[] = []; for (let i = 0; i < 8; i++) { - repeatedPeerAddresses.push(...initialPeerAddresses) + repeatedPeerAddresses.push(...initialPeerAddresses); } // Add 5 fake addresses repeatedPeerAddresses.push( ...Array.from( { length: 5 }, - () => '0x0000000000000000000000000000000000000000' - ) - ) + () => "0x0000000000000000000000000000000000000000", + ), + ); // Now call canMessage with all of the peerAddresses const canMessageRegisteredClients = await Client.canMessage( repeatedPeerAddresses, { - env: 'local', - } - ) + env: "local", + }, + ); // Expect 80 True and 5 False expect(canMessageRegisteredClients).toEqual( Array.from({ length: 80 }, () => true).concat( - Array.from({ length: 5 }, () => false) - ) - ) - }) -}) - -describe('listEnvelopes', () => { - it('has envelopes with senderHmac and shouldPush', async () => { - const alice = await newLocalHostClient() - const bob = await newLocalHostClient() - alice.registerCodec(new TestKeyCodec()) - const convo = await alice.conversations.newConversation(bob.address) - await convo.send('hi') - const key = PrivateKey.generate().publicKey + Array.from({ length: 5 }, () => false), + ), + ); + }); +}); + +describe("listEnvelopes", () => { + it("has envelopes with senderHmac and shouldPush", async () => { + const alice = await newLocalHostClient(); + const bob = await newLocalHostClient(); + alice.registerCodec(new TestKeyCodec()); + const convo = await alice.conversations.newConversation(bob.address); + await convo.send("hi"); + const key = PrivateKey.generate().publicKey; await convo.send(key, { contentType: ContentTypeTestKey, - }) + }); const envelopes = await alice.listEnvelopes( convo.topic, - (env: EnvelopeWithMessage) => Promise.resolve(env) - ) + (env: EnvelopeWithMessage) => Promise.resolve(env), + ); - const msg1 = message.Message.decode(envelopes[0].message) + const msg1 = message.Message.decode(envelopes[0].message); if (!msg1.v2) { - throw new Error('unknown message version') + throw new Error("unknown message version"); } - const header1 = message.MessageHeaderV2.decode(msg1.v2.headerBytes) - expect(header1.topic).toEqual(convo.topic) - expect(msg1.v2.senderHmac).toBeDefined() - expect(msg1.v2.shouldPush).toBe(true) + const header1 = message.MessageHeaderV2.decode(msg1.v2.headerBytes); + expect(header1.topic).toEqual(convo.topic); + expect(msg1.v2.senderHmac).toBeDefined(); + expect(msg1.v2.shouldPush).toBe(true); - const msg2 = message.Message.decode(envelopes[1].message) + const msg2 = message.Message.decode(envelopes[1].message); if (!msg2.v2) { - throw new Error('unknown message version') + throw new Error("unknown message version"); } - const header2 = message.MessageHeaderV2.decode(msg2.v2.headerBytes) - expect(header2.topic).toEqual(convo.topic) - expect(msg2.v2.senderHmac).toBeDefined() - expect(msg2.v2.shouldPush).toBe(false) - }) -}) - -describe('publishEnvelopes', () => { - it('can send a valid envelope', async () => { - const c = await newLocalHostClient() + const header2 = message.MessageHeaderV2.decode(msg2.v2.headerBytes); + expect(header2.topic).toEqual(convo.topic); + expect(msg2.v2.senderHmac).toBeDefined(); + expect(msg2.v2.shouldPush).toBe(false); + }); +}); + +describe("publishEnvelopes", () => { + it("can send a valid envelope", async () => { + const c = await newLocalHostClient(); const envelope = { - contentTopic: '/xmtp/0/foo/proto', - message: new TextEncoder().encode('hello world'), + contentTopic: "/xmtp/0/foo/proto", + message: new TextEncoder().encode("hello world"), timestamp: new Date(), - } - await c.publishEnvelopes([envelope]) - }) + }; + await c.publishEnvelopes([envelope]); + }); - it('rejects with invalid envelopes', async () => { - const c = await newLocalHostClient() + it("rejects with invalid envelopes", async () => { + const c = await newLocalHostClient(); // Set a bogus authenticator so we can have failing publishes c.apiClient.setAuthenticator({ // @ts-expect-error mock function createToken: async () => ({ - toBase64: () => 'derp!', + toBase64: () => "derp!", }), - }) + }); const envelope = { contentTopic: buildUserContactTopic(c.address), - message: new TextEncoder().encode('hello world'), + message: new TextEncoder().encode("hello world"), timestamp: new Date(), - } + }; - expect(c.publishEnvelopes([envelope])).rejects.toThrow() - }) -}) + expect(c.publishEnvelopes([envelope])).rejects.toThrow(); + }); +}); -describe('ClientOptions', () => { +describe("ClientOptions", () => { const tests = [ { - name: 'local docker node', + name: "local docker node", newClient: newLocalHostClient, }, - ] + ]; if (process.env.CI || process.env.TESTNET) { tests.push({ - name: 'dev', + name: "dev", newClient: newDevClient, - }) + }); } tests.forEach((testCase) => { - it('Default/empty options', async () => { - await testCase.newClient() - }) + it("Default/empty options", async () => { + await testCase.newClient(); + }); - it('Partial specification', async () => { + it("Partial specification", async () => { await testCase.newClient({ persistConversations: true, - }) - }) - }) - - describe('custom codecs', () => { - it('gives type errors when you use the wrong types', async () => { - const client = await Client.create(newWallet(), { env: 'local' }) - const other = await Client.create(newWallet(), { env: 'local' }) - const convo = await client.conversations.newConversation(other.address) - expect(convo).toBeTruthy() + }); + }); + }); + + describe("custom codecs", () => { + it("gives type errors when you use the wrong types", async () => { + const client = await Client.create(newWallet(), { env: "local" }); + const other = await Client.create(newWallet(), { env: "local" }); + const convo = await client.conversations.newConversation(other.address); + expect(convo).toBeTruthy(); try { // @ts-expect-error if we break the type casting someone will notice - await convo.send(123) - const messages = await convo.messages() + await convo.send(123); + const messages = await convo.messages(); for (const message of messages) { // @ts-expect-error Strings don't have this kind of method - message.toFixed() + message.toFixed(); } } catch (e) { - return + return; } - assert.fail() - }) + assert.fail(); + }); - it('allows you to use custom content types', async () => { + it("allows you to use custom content types", async () => { const ContentTypeCustom = new ContentTypeId({ - authorityId: 'xmtp.org', - typeId: 'text', + authorityId: "xmtp.org", + typeId: "text", versionMajor: 1, versionMinor: 0, - }) + }); class CustomCodec implements ContentCodec<{ custom: string }> { get contentType(): ContentTypeId { - return ContentTypeCustom + return ContentTypeCustom; } encode(content: { custom: string }): EncodedContent { @@ -440,128 +442,130 @@ describe('ClientOptions', () => { type: ContentTypeText, parameters: {}, content: new TextEncoder().encode(JSON.stringify(content)), - } + }; } decode(content: EncodedContent): { custom: string } { - const decodedContent = new TextDecoder().decode(content.content) - const parsedContent = JSON.parse(decodedContent) as { custom: string } + const decodedContent = new TextDecoder().decode(content.content); + const parsedContent = JSON.parse(decodedContent) as { + custom: string; + }; return { custom: parsedContent.custom, - } + }; } fallback() { - return undefined + return undefined; } shouldPush() { - return false + return false; } } const client = await Client.create(newWallet(), { codecs: [new CustomCodec()], - env: 'local', - }) - const other = await Client.create(newWallet(), { env: 'local' }) - const convo = await client.conversations.newConversation(other.address) - expect(convo).toBeTruthy() + env: "local", + }); + const other = await Client.create(newWallet(), { env: "local" }); + const convo = await client.conversations.newConversation(other.address); + expect(convo).toBeTruthy(); // This will have a type error if the codecs field isn't being respected - await convo.send({ custom: 'test' }) - }) - }) + await convo.send({ custom: "test" }); + }); + }); - describe('Pluggable API client', () => { - it('allows you to specify a custom API client factory', async () => { - const expectedError = new Error('CustomApiClient') + describe("Pluggable API client", () => { + it("allows you to specify a custom API client factory", async () => { + const expectedError = new Error("CustomApiClient"); class CustomApiClient extends HttpApiClient { publish(): Promise { - return Promise.reject(expectedError) + return Promise.reject(expectedError); } } const c = newLocalHostClient({ apiClientFactory: () => { - return new CustomApiClient(ApiUrls.local) + return new CustomApiClient(ApiUrls.local); }, - }) - await expect(c).rejects.toThrow(expectedError) - }) - }) + }); + await expect(c).rejects.toThrow(expectedError); + }); + }); - describe('pluggable persistence', () => { - it('allows for an override of the persistence engine', async () => { + describe("pluggable persistence", () => { + it("allows for an override of the persistence engine", async () => { class MyNewPersistence extends InMemoryPersistence { getItem(): Promise { - return Promise.reject(new Error('MyNewPersistence')) + return Promise.reject(new Error("MyNewPersistence")); } } const c = newLocalHostClient({ basePersistence: new MyNewPersistence(new LocalStoragePonyfill()), - }) - await expect(c).rejects.toThrow('MyNewPersistence') - }) - }) - - describe('canGetKeys', () => { - it('returns true if the useSnaps flag is false', async () => { - mockEthRequest.mockRejectedValue(new Error('foo')) - const isSnapsReady = await Client.isSnapsReady() - expect(isSnapsReady).toBe(false) - }) - - it('returns false if the user has a Snaps capable browser and snaps are enabled', async () => { - mockEthRequest.mockResolvedValue([]) - const isSnapsReady = await Client.isSnapsReady() - expect(isSnapsReady).toBe(true) - }) - }) - - describe('viem', () => { - it('allows you to use a viem WalletClient', async () => { - const privateKey = generatePrivateKey() - const account = privateKeyToAccount(privateKey) + }); + await expect(c).rejects.toThrow("MyNewPersistence"); + }); + }); + + describe("canGetKeys", () => { + it("returns true if the useSnaps flag is false", async () => { + mockEthRequest.mockRejectedValue(new Error("foo")); + const isSnapsReady = await Client.isSnapsReady(); + expect(isSnapsReady).toBe(false); + }); + + it("returns false if the user has a Snaps capable browser and snaps are enabled", async () => { + mockEthRequest.mockResolvedValue([]); + const isSnapsReady = await Client.isSnapsReady(); + expect(isSnapsReady).toBe(true); + }); + }); + + describe("viem", () => { + it("allows you to use a viem WalletClient", async () => { + const privateKey = generatePrivateKey(); + const account = privateKeyToAccount(privateKey); const walletClient = createWalletClient({ account, chain: mainnet, transport: http(), - }) + }); - const c = await Client.create(walletClient, { env: 'local' }) - expect(c).toBeDefined() - expect(c.address).toEqual(account.address) - }) + const c = await Client.create(walletClient, { env: "local" }); + expect(c).toBeDefined(); + expect(c.address).toEqual(account.address); + }); - it('creates an identical client between viem and ethers', async () => { - const randomWallet = Wallet.createRandom() - const privateKey = randomWallet.privateKey - const account = privateKeyToAccount(privateKey as `0x${string}`) + it("creates an identical client between viem and ethers", async () => { + const randomWallet = Wallet.createRandom(); + const privateKey = randomWallet.privateKey; + const account = privateKeyToAccount(privateKey as `0x${string}`); const walletClient = createWalletClient({ account, chain: mainnet, transport: http(), - }) + }); - const viemClient = await Client.create(walletClient, { env: 'local' }) - const ethersClient = await Client.create(randomWallet, { env: 'local' }) - expect(viemClient.address).toEqual(ethersClient.address) + const viemClient = await Client.create(walletClient, { env: "local" }); + const ethersClient = await Client.create(randomWallet, { env: "local" }); + expect(viemClient.address).toEqual(ethersClient.address); expect( - viemClient.publicKeyBundle.equals(ethersClient.publicKeyBundle) - ).toBe(true) - }) + viemClient.publicKeyBundle.equals(ethersClient.publicKeyBundle), + ).toBe(true); + }); - it('fails if you use a viem WalletClient without an account', async () => { + it("fails if you use a viem WalletClient without an account", async () => { const walletClient = createWalletClient({ chain: mainnet, transport: http(), - }) + }); await expect( - Client.create(walletClient, { env: 'local' }) - ).rejects.toThrow('WalletClient is not configured') - }) - }) -}) + Client.create(walletClient, { env: "local" }), + ).rejects.toThrow("WalletClient is not configured"); + }); + }); +}); diff --git a/packages/js-sdk/test/Compression.test.ts b/packages/js-sdk/test/Compression.test.ts index 00352be09..68c54ada7 100644 --- a/packages/js-sdk/test/Compression.test.ts +++ b/packages/js-sdk/test/Compression.test.ts @@ -1,43 +1,43 @@ -import { ContentTypeText } from '@xmtp/content-type-text' -import { content as proto } from '@xmtp/proto' +import { ContentTypeText } from "@xmtp/content-type-text"; +import { content as proto } from "@xmtp/proto"; import { compress, decompress, readStreamFromBytes, writeStreamToBytes, -} from '@/Compression' +} from "@/Compression"; -describe('Compression', function () { - it('can stream bytes from source to sink', async function () { - const from = new Uint8Array(111).fill(42) +describe("Compression", function () { + it("can stream bytes from source to sink", async function () { + const from = new Uint8Array(111).fill(42); // make sink smaller so that it has to grow a lot - const to = { bytes: new Uint8Array(3) } - await readStreamFromBytes(from, 23).pipeTo(writeStreamToBytes(to, 1000)) - expect(from).toEqual(to.bytes) - }) + const to = { bytes: new Uint8Array(3) }; + await readStreamFromBytes(from, 23).pipeTo(writeStreamToBytes(to, 1000)); + expect(from).toEqual(to.bytes); + }); - it('will not write beyond limit', () => { - const from = new Uint8Array(111).fill(42) - const to = { bytes: new Uint8Array(10) } + it("will not write beyond limit", () => { + const from = new Uint8Array(111).fill(42); + const to = { bytes: new Uint8Array(10) }; expect( - readStreamFromBytes(from, 23).pipeTo(writeStreamToBytes(to, 100)) - ).rejects.toThrow('maximum output size exceeded') - }) + readStreamFromBytes(from, 23).pipeTo(writeStreamToBytes(to, 100)), + ).rejects.toThrow("maximum output size exceeded"); + }); - it('compresses and decompresses', async function () { - const uncompressed = new Uint8Array(55).fill(42) + it("compresses and decompresses", async function () { + const uncompressed = new Uint8Array(55).fill(42); const compressed = new Uint8Array([ 120, 156, 211, 210, 34, 11, 0, 0, 252, 223, 9, 7, - ]) + ]); const content = { type: ContentTypeText, parameters: {}, content: uncompressed, compression: proto.Compression.COMPRESSION_DEFLATE, - } - await compress(content) - expect(content.content).toEqual(compressed) - await decompress(content, 1000) - expect(content.content).toEqual(uncompressed) - }) -}) + }; + await compress(content); + expect(content.content).toEqual(compressed); + await decompress(content, 1000); + expect(content.content).toEqual(uncompressed); + }); +}); diff --git a/packages/js-sdk/test/ContactBundle.test.ts b/packages/js-sdk/test/ContactBundle.test.ts index 8bc42d277..32b9ca156 100644 --- a/packages/js-sdk/test/ContactBundle.test.ts +++ b/packages/js-sdk/test/ContactBundle.test.ts @@ -1,30 +1,30 @@ -import { decodeContactBundle, encodeContactBundle } from '@/ContactBundle' +import { decodeContactBundle, encodeContactBundle } from "@/ContactBundle"; import { PrivateKeyBundleV1, PrivateKeyBundleV2, -} from '@/crypto/PrivateKeyBundle' +} from "@/crypto/PrivateKeyBundle"; import { PublicKeyBundle, SignedPublicKeyBundle, -} from '@/crypto/PublicKeyBundle' -import { newWallet } from './helpers' +} from "@/crypto/PublicKeyBundle"; +import { newWallet } from "./helpers"; -describe('ContactBundles', function () { - it('roundtrip', async function () { - const priv = await PrivateKeyBundleV1.generate() - const pub = priv.getPublicKeyBundle() - const bytes = encodeContactBundle(pub) - const cb = decodeContactBundle(bytes) - expect(cb).toBeInstanceOf(PublicKeyBundle) - expect(pub.equals(cb as PublicKeyBundle)).toBeTruthy() - }) - it('roundtrip v2', async function () { - const wallet = newWallet() - const priv = await PrivateKeyBundleV2.generate(wallet) - const pub = priv.getPublicKeyBundle() - const bytes = encodeContactBundle(pub) - const cb = decodeContactBundle(bytes) - expect(cb).toBeInstanceOf(SignedPublicKeyBundle) - expect(pub.equals(cb as SignedPublicKeyBundle)).toBeTruthy() - }) -}) +describe("ContactBundles", function () { + it("roundtrip", async function () { + const priv = await PrivateKeyBundleV1.generate(); + const pub = priv.getPublicKeyBundle(); + const bytes = encodeContactBundle(pub); + const cb = decodeContactBundle(bytes); + expect(cb).toBeInstanceOf(PublicKeyBundle); + expect(pub.equals(cb as PublicKeyBundle)).toBeTruthy(); + }); + it("roundtrip v2", async function () { + const wallet = newWallet(); + const priv = await PrivateKeyBundleV2.generate(wallet); + const pub = priv.getPublicKeyBundle(); + const bytes = encodeContactBundle(pub); + const cb = decodeContactBundle(bytes); + expect(cb).toBeInstanceOf(SignedPublicKeyBundle); + expect(pub.equals(cb as SignedPublicKeyBundle)).toBeTruthy(); + }); +}); diff --git a/packages/js-sdk/test/Contacts.test.ts b/packages/js-sdk/test/Contacts.test.ts index 0dda58231..0b10a64c6 100644 --- a/packages/js-sdk/test/Contacts.test.ts +++ b/packages/js-sdk/test/Contacts.test.ts @@ -1,304 +1,304 @@ -import { createConsentMessage } from '@xmtp/consent-proof-signature' -import { invitation } from '@xmtp/proto' -import Client from '@/Client' -import { Contacts } from '@/Contacts' -import { WalletSigner } from '@/crypto/Signature' -import { newLocalHostClient, newWallet } from './helpers' - -const alice = newWallet() -const bob = newWallet() -const carol = newWallet() - -let aliceClient: Client -let bobClient: Client -let carolClient: Client - -describe('Contacts', () => { +import { createConsentMessage } from "@xmtp/consent-proof-signature"; +import { invitation } from "@xmtp/proto"; +import Client from "@/Client"; +import { Contacts } from "@/Contacts"; +import { WalletSigner } from "@/crypto/Signature"; +import { newLocalHostClient, newWallet } from "./helpers"; + +const alice = newWallet(); +const bob = newWallet(); +const carol = newWallet(); + +let aliceClient: Client; +let bobClient: Client; +let carolClient: Client; + +describe("Contacts", () => { beforeEach(async () => { aliceClient = await Client.create(alice, { - env: 'local', - }) + env: "local", + }); bobClient = await Client.create(bob, { - env: 'local', - }) + env: "local", + }); carolClient = await Client.create(carol, { - env: 'local', - }) - }) - - it('should initialize with client', async () => { - expect(aliceClient.contacts).toBeInstanceOf(Contacts) - expect(aliceClient.contacts.addresses).toBeInstanceOf(Set) - expect(Array.from(aliceClient.contacts.addresses.keys()).length).toBe(0) - }) - - it('should allow and deny addresses', async () => { - await aliceClient.contacts.allow([bob.address]) - expect(aliceClient.contacts.consentState(bob.address)).toBe('allowed') - expect(aliceClient.contacts.isAllowed(bob.address)).toBe(true) - expect(aliceClient.contacts.isDenied(bob.address)).toBe(false) - - await aliceClient.contacts.deny([bob.address]) - expect(aliceClient.contacts.consentState(bob.address)).toBe('denied') - expect(aliceClient.contacts.isAllowed(bob.address)).toBe(false) - expect(aliceClient.contacts.isDenied(bob.address)).toBe(true) - }) - - it('should allow and deny groups', async () => { - await aliceClient.contacts.allowGroups(['foo']) - expect(aliceClient.contacts.groupConsentState('foo')).toBe('allowed') - expect(aliceClient.contacts.isGroupAllowed('foo')).toBe(true) - expect(aliceClient.contacts.isGroupDenied('foo')).toBe(false) - - await aliceClient.contacts.denyGroups(['foo']) - expect(aliceClient.contacts.groupConsentState('foo')).toBe('denied') - expect(aliceClient.contacts.isGroupAllowed('foo')).toBe(false) - expect(aliceClient.contacts.isGroupDenied('foo')).toBe(true) - }) - - it('should allow and deny inboxes', async () => { - await aliceClient.contacts.allowInboxes(['foo']) - expect(aliceClient.contacts.inboxConsentState('foo')).toBe('allowed') - expect(aliceClient.contacts.isInboxAllowed('foo')).toBe(true) - expect(aliceClient.contacts.isInboxDenied('foo')).toBe(false) - - await aliceClient.contacts.denyInboxes(['foo']) - expect(aliceClient.contacts.inboxConsentState('foo')).toBe('denied') - expect(aliceClient.contacts.isInboxAllowed('foo')).toBe(false) - expect(aliceClient.contacts.isInboxDenied('foo')).toBe(true) - }) - - it('should allow an address when a conversation is started', async () => { + env: "local", + }); + }); + + it("should initialize with client", async () => { + expect(aliceClient.contacts).toBeInstanceOf(Contacts); + expect(aliceClient.contacts.addresses).toBeInstanceOf(Set); + expect(Array.from(aliceClient.contacts.addresses.keys()).length).toBe(0); + }); + + it("should allow and deny addresses", async () => { + await aliceClient.contacts.allow([bob.address]); + expect(aliceClient.contacts.consentState(bob.address)).toBe("allowed"); + expect(aliceClient.contacts.isAllowed(bob.address)).toBe(true); + expect(aliceClient.contacts.isDenied(bob.address)).toBe(false); + + await aliceClient.contacts.deny([bob.address]); + expect(aliceClient.contacts.consentState(bob.address)).toBe("denied"); + expect(aliceClient.contacts.isAllowed(bob.address)).toBe(false); + expect(aliceClient.contacts.isDenied(bob.address)).toBe(true); + }); + + it("should allow and deny groups", async () => { + await aliceClient.contacts.allowGroups(["foo"]); + expect(aliceClient.contacts.groupConsentState("foo")).toBe("allowed"); + expect(aliceClient.contacts.isGroupAllowed("foo")).toBe(true); + expect(aliceClient.contacts.isGroupDenied("foo")).toBe(false); + + await aliceClient.contacts.denyGroups(["foo"]); + expect(aliceClient.contacts.groupConsentState("foo")).toBe("denied"); + expect(aliceClient.contacts.isGroupAllowed("foo")).toBe(false); + expect(aliceClient.contacts.isGroupDenied("foo")).toBe(true); + }); + + it("should allow and deny inboxes", async () => { + await aliceClient.contacts.allowInboxes(["foo"]); + expect(aliceClient.contacts.inboxConsentState("foo")).toBe("allowed"); + expect(aliceClient.contacts.isInboxAllowed("foo")).toBe(true); + expect(aliceClient.contacts.isInboxDenied("foo")).toBe(false); + + await aliceClient.contacts.denyInboxes(["foo"]); + expect(aliceClient.contacts.inboxConsentState("foo")).toBe("denied"); + expect(aliceClient.contacts.isInboxAllowed("foo")).toBe(false); + expect(aliceClient.contacts.isInboxDenied("foo")).toBe(true); + }); + + it("should allow an address when a conversation is started", async () => { const conversation = await aliceClient.conversations.newConversation( - carol.address - ) + carol.address, + ); - expect(aliceClient.contacts.consentState(carol.address)).toBe('allowed') - expect(aliceClient.contacts.isAllowed(carol.address)).toBe(true) - expect(aliceClient.contacts.isDenied(carol.address)).toBe(false) + expect(aliceClient.contacts.consentState(carol.address)).toBe("allowed"); + expect(aliceClient.contacts.isAllowed(carol.address)).toBe(true); + expect(aliceClient.contacts.isDenied(carol.address)).toBe(false); - expect(conversation.isAllowed).toBe(true) - expect(conversation.isDenied).toBe(false) - expect(conversation.consentState).toBe('allowed') - }) + expect(conversation.isAllowed).toBe(true); + expect(conversation.isDenied).toBe(false); + expect(conversation.consentState).toBe("allowed"); + }); - it('should allow an address when a conversation has an unknown consent state and a message is sent into it', async () => { - await aliceClient.conversations.newConversation(carol.address) + it("should allow an address when a conversation has an unknown consent state and a message is sent into it", async () => { + await aliceClient.conversations.newConversation(carol.address); - expect(carolClient.contacts.consentState(alice.address)).toBe('unknown') - expect(carolClient.contacts.isAllowed(carol.address)).toBe(false) - expect(carolClient.contacts.isDenied(carol.address)).toBe(false) + expect(carolClient.contacts.consentState(alice.address)).toBe("unknown"); + expect(carolClient.contacts.isAllowed(carol.address)).toBe(false); + expect(carolClient.contacts.isDenied(carol.address)).toBe(false); const carolConversation = await carolClient.conversations.newConversation( - alice.address - ) - expect(carolConversation.consentState).toBe('unknown') - expect(carolConversation.isAllowed).toBe(false) - expect(carolConversation.isDenied).toBe(false) + alice.address, + ); + expect(carolConversation.consentState).toBe("unknown"); + expect(carolConversation.isAllowed).toBe(false); + expect(carolConversation.isDenied).toBe(false); - await carolConversation.send('gm') + await carolConversation.send("gm"); - expect(carolConversation.consentState).toBe('allowed') - expect(carolConversation.isAllowed).toBe(true) - expect(carolConversation.isDenied).toBe(false) - }) + expect(carolConversation.consentState).toBe("allowed"); + expect(carolConversation.isAllowed).toBe(true); + expect(carolConversation.isDenied).toBe(false); + }); - it('should allow or deny an address from a conversation', async () => { + it("should allow or deny an address from a conversation", async () => { const conversation = await aliceClient.conversations.newConversation( - carol.address - ) + carol.address, + ); - await conversation.deny() + await conversation.deny(); - expect(aliceClient.contacts.consentState(carol.address)).toBe('denied') - expect(aliceClient.contacts.isAllowed(carol.address)).toBe(false) - expect(aliceClient.contacts.isDenied(carol.address)).toBe(true) + expect(aliceClient.contacts.consentState(carol.address)).toBe("denied"); + expect(aliceClient.contacts.isAllowed(carol.address)).toBe(false); + expect(aliceClient.contacts.isDenied(carol.address)).toBe(true); - expect(conversation.isAllowed).toBe(false) - expect(conversation.isDenied).toBe(true) - expect(conversation.consentState).toBe('denied') + expect(conversation.isAllowed).toBe(false); + expect(conversation.isDenied).toBe(true); + expect(conversation.consentState).toBe("denied"); - await conversation.allow() + await conversation.allow(); - expect(aliceClient.contacts.consentState(carol.address)).toBe('allowed') - expect(aliceClient.contacts.isAllowed(carol.address)).toBe(true) - expect(aliceClient.contacts.isDenied(carol.address)).toBe(false) + expect(aliceClient.contacts.consentState(carol.address)).toBe("allowed"); + expect(aliceClient.contacts.isAllowed(carol.address)).toBe(true); + expect(aliceClient.contacts.isDenied(carol.address)).toBe(false); - expect(conversation.isAllowed).toBe(true) - expect(conversation.isDenied).toBe(false) - expect(conversation.consentState).toBe('allowed') - }) + expect(conversation.isAllowed).toBe(true); + expect(conversation.isDenied).toBe(false); + expect(conversation.consentState).toBe("allowed"); + }); - it('should retrieve consent state', async () => { - const entries = await bobClient.contacts.refreshConsentList() + it("should retrieve consent state", async () => { + const entries = await bobClient.contacts.refreshConsentList(); - expect(entries.size).toBe(0) + expect(entries.size).toBe(0); - await bobClient.contacts.deny([alice.address]) - await bobClient.contacts.allow([carol.address]) - await bobClient.contacts.allow([alice.address]) - await bobClient.contacts.deny([carol.address]) - await bobClient.contacts.deny([alice.address]) - await bobClient.contacts.allow([carol.address]) - await bobClient.contacts.allowGroups(['foo', 'bar']) - await bobClient.contacts.denyGroups(['foo']) - await bobClient.contacts.allowGroups(['foo']) - await bobClient.contacts.denyGroups(['bar']) - await bobClient.contacts.allowInboxes(['baz', 'qux']) - await bobClient.contacts.denyInboxes(['baz']) - await bobClient.contacts.allowInboxes(['baz']) + await bobClient.contacts.deny([alice.address]); + await bobClient.contacts.allow([carol.address]); + await bobClient.contacts.allow([alice.address]); + await bobClient.contacts.deny([carol.address]); + await bobClient.contacts.deny([alice.address]); + await bobClient.contacts.allow([carol.address]); + await bobClient.contacts.allowGroups(["foo", "bar"]); + await bobClient.contacts.denyGroups(["foo"]); + await bobClient.contacts.allowGroups(["foo"]); + await bobClient.contacts.denyGroups(["bar"]); + await bobClient.contacts.allowInboxes(["baz", "qux"]); + await bobClient.contacts.denyInboxes(["baz"]); + await bobClient.contacts.allowInboxes(["baz"]); bobClient = await Client.create(bob, { - env: 'local', - }) + env: "local", + }); - expect(bobClient.contacts.consentState(alice.address)).toBe('unknown') - expect(bobClient.contacts.consentState(carol.address)).toBe('unknown') - expect(bobClient.contacts.groupConsentState('foo')).toBe('unknown') - expect(bobClient.contacts.groupConsentState('bar')).toBe('unknown') - expect(bobClient.contacts.inboxConsentState('baz')).toBe('unknown') - expect(bobClient.contacts.inboxConsentState('qux')).toBe('unknown') + expect(bobClient.contacts.consentState(alice.address)).toBe("unknown"); + expect(bobClient.contacts.consentState(carol.address)).toBe("unknown"); + expect(bobClient.contacts.groupConsentState("foo")).toBe("unknown"); + expect(bobClient.contacts.groupConsentState("bar")).toBe("unknown"); + expect(bobClient.contacts.inboxConsentState("baz")).toBe("unknown"); + expect(bobClient.contacts.inboxConsentState("qux")).toBe("unknown"); - const latestEntries = await bobClient.contacts.loadConsentList() + const latestEntries = await bobClient.contacts.loadConsentList(); - expect(latestEntries.size).toBe(6) + expect(latestEntries.size).toBe(6); expect(latestEntries).toEqual( new Map([ - [`address-${alice.address}`, 'denied'], - [`address-${carol.address}`, 'allowed'], - [`groupId-foo`, 'allowed'], - [`groupId-bar`, 'denied'], - [`inboxId-baz`, 'allowed'], - [`inboxId-qux`, 'allowed'], - ]) - ) - - expect(bobClient.contacts.consentState(alice.address)).toBe('denied') - expect(bobClient.contacts.consentState(carol.address)).toBe('allowed') - expect(bobClient.contacts.groupConsentState('foo')).toBe('allowed') - expect(bobClient.contacts.groupConsentState('bar')).toBe('denied') - expect(bobClient.contacts.inboxConsentState('baz')).toBe('allowed') - expect(bobClient.contacts.inboxConsentState('qux')).toBe('allowed') - }) - - it('should stream consent updates', async () => { - const aliceStream = await aliceClient.contacts.streamConsentList() - await aliceClient.conversations.newConversation(bob.address) - - let numActions = 0 + [`address-${alice.address}`, "denied"], + [`address-${carol.address}`, "allowed"], + [`groupId-foo`, "allowed"], + [`groupId-bar`, "denied"], + [`inboxId-baz`, "allowed"], + [`inboxId-qux`, "allowed"], + ]), + ); + + expect(bobClient.contacts.consentState(alice.address)).toBe("denied"); + expect(bobClient.contacts.consentState(carol.address)).toBe("allowed"); + expect(bobClient.contacts.groupConsentState("foo")).toBe("allowed"); + expect(bobClient.contacts.groupConsentState("bar")).toBe("denied"); + expect(bobClient.contacts.inboxConsentState("baz")).toBe("allowed"); + expect(bobClient.contacts.inboxConsentState("qux")).toBe("allowed"); + }); + + it("should stream consent updates", async () => { + const aliceStream = await aliceClient.contacts.streamConsentList(); + await aliceClient.conversations.newConversation(bob.address); + + let numActions = 0; // eslint-disable-next-line no-unreachable-loop for await (const action of aliceStream) { - numActions++ - expect(action.allowGroup).toBeUndefined() - expect(action.denyGroup).toBeUndefined() - expect(action.allowInboxId).toBeUndefined() - expect(action.denyInboxId).toBeUndefined() - expect(action.denyAddress).toBeUndefined() - expect(action.allowAddress?.walletAddresses).toEqual([bob.address]) - break + numActions++; + expect(action.allowGroup).toBeUndefined(); + expect(action.denyGroup).toBeUndefined(); + expect(action.allowInboxId).toBeUndefined(); + expect(action.denyInboxId).toBeUndefined(); + expect(action.denyAddress).toBeUndefined(); + expect(action.allowAddress?.walletAddresses).toEqual([bob.address]); + break; } - expect(numActions).toBe(1) - await aliceStream.return() - }) - - describe('consent proofs', () => { - it('handles consent proof on invitation', async () => { - const bo = await newLocalHostClient() - const wallet = newWallet() - const keySigner = new WalletSigner(wallet) - const alixAddress = await keySigner.wallet.getAddress() + expect(numActions).toBe(1); + await aliceStream.return(); + }); + + describe("consent proofs", () => { + it("handles consent proof on invitation", async () => { + const bo = await newLocalHostClient(); + const wallet = newWallet(); + const keySigner = new WalletSigner(wallet); + const alixAddress = await keySigner.wallet.getAddress(); const alix = await Client.create(wallet, { - env: 'local', - }) - const timestamp = Date.now() - const consentMessage = createConsentMessage(bo.address, timestamp) - const signedMessage = await keySigner.wallet.signMessage(consentMessage) + env: "local", + }); + const timestamp = Date.now(); + const consentMessage = createConsentMessage(bo.address, timestamp); + const signedMessage = await keySigner.wallet.signMessage(consentMessage); const consentProofPayload = invitation.ConsentProofPayload.fromPartial({ signature: signedMessage, timestamp, payloadVersion: invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1, - }) + }); const boConvo = await bo.conversations.newConversation( alixAddress, undefined, - consentProofPayload - ) - await alix.contacts.refreshConsentList() - const conversations = await alix.conversations.list() - const convo = conversations.find((c) => c.topic === boConvo.topic) - expect(convo).toBeTruthy() - const isApproved = await convo?.isAllowed - expect(isApproved).toBe(true) - }) - - it('consent proof yields to network consent', async () => { - const bo = await newLocalHostClient() - const wallet = newWallet() - const keySigner = new WalletSigner(wallet) - const alixAddress = await keySigner.wallet.getAddress() + consentProofPayload, + ); + await alix.contacts.refreshConsentList(); + const conversations = await alix.conversations.list(); + const convo = conversations.find((c) => c.topic === boConvo.topic); + expect(convo).toBeTruthy(); + const isApproved = await convo?.isAllowed; + expect(isApproved).toBe(true); + }); + + it("consent proof yields to network consent", async () => { + const bo = await newLocalHostClient(); + const wallet = newWallet(); + const keySigner = new WalletSigner(wallet); + const alixAddress = await keySigner.wallet.getAddress(); const alix1 = await Client.create(wallet, { - env: 'local', - }) - alix1.contacts.deny([bo.address]) + env: "local", + }); + alix1.contacts.deny([bo.address]); const alix2 = await Client.create(wallet, { - env: 'local', - }) - const timestamp = Date.now() - const consentMessage = createConsentMessage(bo.address, timestamp) - const signedMessage = await keySigner.wallet.signMessage(consentMessage) + env: "local", + }); + const timestamp = Date.now(); + const consentMessage = createConsentMessage(bo.address, timestamp); + const signedMessage = await keySigner.wallet.signMessage(consentMessage); const consentProofPayload = invitation.ConsentProofPayload.fromPartial({ signature: signedMessage, timestamp, payloadVersion: invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1, - }) + }); const boConvo = await bo.conversations.newConversation( alixAddress, undefined, - consentProofPayload - ) - const conversations = await alix2.conversations.list() - const convo = conversations.find((c) => c.topic === boConvo.topic) - expect(convo).toBeTruthy() - await alix2.contacts.refreshConsentList() - const isDenied = await alix2.contacts.isDenied(bo.address) - expect(isDenied).toBeTruthy() - }) - - it('consent proof should not approve for invalid signature', async () => { - const bo = await newLocalHostClient() - const wallet = newWallet() - const keySigner = new WalletSigner(wallet) - const alixAddress = await keySigner.wallet.getAddress() + consentProofPayload, + ); + const conversations = await alix2.conversations.list(); + const convo = conversations.find((c) => c.topic === boConvo.topic); + expect(convo).toBeTruthy(); + await alix2.contacts.refreshConsentList(); + const isDenied = await alix2.contacts.isDenied(bo.address); + expect(isDenied).toBeTruthy(); + }); + + it("consent proof should not approve for invalid signature", async () => { + const bo = await newLocalHostClient(); + const wallet = newWallet(); + const keySigner = new WalletSigner(wallet); + const alixAddress = await keySigner.wallet.getAddress(); const alix = await Client.create(wallet, { - env: 'local', - }) - const initialIsAllowed = await alix.contacts.isAllowed(bo.address) + env: "local", + }); + const initialIsAllowed = await alix.contacts.isAllowed(bo.address); expect( initialIsAllowed, - 'Should be not be allowed by default' - ).toBeFalsy() - const timestamp = Date.now() - const consentMessage = createConsentMessage(bo.address, timestamp) - const signedMessage = await keySigner.wallet.signMessage(consentMessage) + "Should be not be allowed by default", + ).toBeFalsy(); + const timestamp = Date.now(); + const consentMessage = createConsentMessage(bo.address, timestamp); + const signedMessage = await keySigner.wallet.signMessage(consentMessage); const consentProofPayload = invitation.ConsentProofPayload.fromPartial({ signature: signedMessage, timestamp: timestamp + 1000, payloadVersion: invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1, - }) + }); const boConvo = await bo.conversations.newConversation( alixAddress, undefined, - consentProofPayload - ) - const conversations = await alix.conversations.list() - const convo = conversations.find((c) => c.topic === boConvo.topic) - expect(convo).toBeTruthy() - await alix.contacts.refreshConsentList() - const isAllowed = await alix.contacts.isAllowed(bo.address) - expect(isAllowed).toBeFalsy() - }) - }) -}) + consentProofPayload, + ); + const conversations = await alix.conversations.list(); + const convo = conversations.find((c) => c.topic === boConvo.topic); + expect(convo).toBeTruthy(); + await alix.contacts.refreshConsentList(); + const isAllowed = await alix.contacts.isAllowed(bo.address); + expect(isAllowed).toBeFalsy(); + }); + }); +}); diff --git a/packages/js-sdk/test/ContentTypeTestKey.ts b/packages/js-sdk/test/ContentTypeTestKey.ts index e4d52dc7d..a805f4316 100644 --- a/packages/js-sdk/test/ContentTypeTestKey.ts +++ b/packages/js-sdk/test/ContentTypeTestKey.ts @@ -2,20 +2,20 @@ import { ContentTypeId, type ContentCodec, type EncodedContent, -} from '@xmtp/content-type-primitives' -import { publicKey } from '@xmtp/proto' -import { PublicKey } from '@/crypto/PublicKey' +} from "@xmtp/content-type-primitives"; +import { publicKey } from "@xmtp/proto"; +import { PublicKey } from "@/crypto/PublicKey"; export const ContentTypeTestKey = new ContentTypeId({ - authorityId: 'xmtp.test', - typeId: 'public-key', + authorityId: "xmtp.test", + typeId: "public-key", versionMajor: 1, versionMinor: 0, -}) +}); export class TestKeyCodec implements ContentCodec { get contentType(): ContentTypeId { - return ContentTypeTestKey + return ContentTypeTestKey; } encode(key: PublicKey): EncodedContent { @@ -23,19 +23,19 @@ export class TestKeyCodec implements ContentCodec { type: ContentTypeTestKey, parameters: {}, content: publicKey.PublicKey.encode(key).finish(), - } + }; } decode(content: EncodedContent): PublicKey { - return new PublicKey(publicKey.PublicKey.decode(content.content)) + return new PublicKey(publicKey.PublicKey.decode(content.content)); } // eslint-disable-next-line @typescript-eslint/no-unused-vars fallback(content: PublicKey): string | undefined { - return 'publickey bundle' + return "publickey bundle"; } shouldPush() { - return false + return false; } } diff --git a/packages/js-sdk/test/Invitation.test.ts b/packages/js-sdk/test/Invitation.test.ts index a46633ffa..03bfb905d 100644 --- a/packages/js-sdk/test/Invitation.test.ts +++ b/packages/js-sdk/test/Invitation.test.ts @@ -1,15 +1,15 @@ -import Long from 'long' -import Ciphertext from '@/crypto/Ciphertext' -import crypto from '@/crypto/crypto' -import { NoMatchingPreKeyError } from '@/crypto/errors' -import { PrivateKeyBundleV2 } from '@/crypto/PrivateKeyBundle' +import Long from "long"; +import Ciphertext from "@/crypto/Ciphertext"; +import crypto from "@/crypto/crypto"; +import { NoMatchingPreKeyError } from "@/crypto/errors"; +import { PrivateKeyBundleV2 } from "@/crypto/PrivateKeyBundle"; import { InvitationV1, SealedInvitation, SealedInvitationHeaderV1, SealedInvitationV1, -} from '@/Invitation' -import { newWallet } from './helpers' +} from "@/Invitation"; +import { newWallet } from "./helpers"; const createInvitation = (): InvitationV1 => { return new InvitationV1({ @@ -19,174 +19,174 @@ const createInvitation = (): InvitationV1 => { keyMaterial: crypto.getRandomValues(new Uint8Array(32)), }, consentProof: undefined, - }) -} + }); +}; -describe('Invitations', () => { - let alice: PrivateKeyBundleV2, bob: PrivateKeyBundleV2 +describe("Invitations", () => { + let alice: PrivateKeyBundleV2, bob: PrivateKeyBundleV2; beforeEach(async () => { - alice = await PrivateKeyBundleV2.generate(newWallet()) - bob = await PrivateKeyBundleV2.generate(newWallet()) - }) + alice = await PrivateKeyBundleV2.generate(newWallet()); + bob = await PrivateKeyBundleV2.generate(newWallet()); + }); - describe('SealedInvitation', () => { - it('can generate', async () => { - const invitation = createInvitation() + describe("SealedInvitation", () => { + it("can generate", async () => { + const invitation = createInvitation(); const newInvitation = await SealedInvitation.createV1({ sender: alice, recipient: bob.getPublicKeyBundle(), created: new Date(), invitation, - }) + }); // Ensure round trips correctly expect(newInvitation.toBytes()).toEqual( - SealedInvitation.fromBytes(newInvitation.toBytes()).toBytes() - ) + SealedInvitation.fromBytes(newInvitation.toBytes()).toBytes(), + ); // Ensure the headers haven't been mangled if (!newInvitation.v1) { - throw new Error('Unexpected null v1 invitation header') + throw new Error("Unexpected null v1 invitation header"); } - const v1 = newInvitation.v1 - const header = v1.header - expect(header.sender.equals(alice.getPublicKeyBundle())).toBeTruthy() - expect(header.recipient.equals(bob.getPublicKeyBundle())).toBeTruthy() + const v1 = newInvitation.v1; + const header = v1.header; + expect(header.sender.equals(alice.getPublicKeyBundle())).toBeTruthy(); + expect(header.recipient.equals(bob.getPublicKeyBundle())).toBeTruthy(); // Ensure alice can decrypt the invitation - const aliceInvite = await v1.getInvitation(alice) - expect(aliceInvite.topic).toEqual(invitation.topic) + const aliceInvite = await v1.getInvitation(alice); + expect(aliceInvite.topic).toEqual(invitation.topic); expect(aliceInvite.aes256GcmHkdfSha256.keyMaterial).toEqual( - invitation.aes256GcmHkdfSha256.keyMaterial - ) + invitation.aes256GcmHkdfSha256.keyMaterial, + ); // Ensure bob can decrypt the invitation - const bobInvite = await v1.getInvitation(bob) - expect(bobInvite.topic).toEqual(invitation.topic) + const bobInvite = await v1.getInvitation(bob); + expect(bobInvite.topic).toEqual(invitation.topic); expect(bobInvite.aes256GcmHkdfSha256.keyMaterial).toEqual( - invitation.aes256GcmHkdfSha256.keyMaterial - ) - }) + invitation.aes256GcmHkdfSha256.keyMaterial, + ); + }); - it('throws when bad data goes in', async () => { - const invitation = createInvitation() - const charlie = await PrivateKeyBundleV2.generate(newWallet()) + it("throws when bad data goes in", async () => { + const invitation = createInvitation(); + const charlie = await PrivateKeyBundleV2.generate(newWallet()); const sealedInvitationWithWrongSender = await SealedInvitation.createV1({ sender: charlie, recipient: bob.getPublicKeyBundle(), created: new Date(), invitation, - }) + }); expect( - sealedInvitationWithWrongSender.v1!.getInvitation(alice) - ).rejects.toThrow(NoMatchingPreKeyError) + sealedInvitationWithWrongSender.v1!.getInvitation(alice), + ).rejects.toThrow(NoMatchingPreKeyError); expect(() => { const _sealedInvite = new SealedInvitation({ v1: { headerBytes: Uint8Array.from([123]), ciphertext: undefined }, - }) - }).toThrow() - }) - }) + }); + }).toThrow(); + }); + }); - describe('SealedInvitationV1', () => { - it('can be created with valid inputs', () => { + describe("SealedInvitationV1", () => { + it("can be created with valid inputs", () => { const header = new SealedInvitationHeaderV1({ sender: alice.getPublicKeyBundle(), recipient: bob.getPublicKeyBundle(), createdNs: new Long(12), - }) + }); const ciphertext = new Ciphertext({ aes256GcmHkdfSha256: { hkdfSalt: crypto.getRandomValues(new Uint8Array(32)), gcmNonce: crypto.getRandomValues(new Uint8Array(12)), payload: crypto.getRandomValues(new Uint8Array(16)), }, - }) + }); const invite = new SealedInvitationV1({ headerBytes: header.toBytes(), ciphertext, - }) + }); expect( - invite.header.sender.equals(alice.getPublicKeyBundle()) - ).toBeTruthy() + invite.header.sender.equals(alice.getPublicKeyBundle()), + ).toBeTruthy(); expect( - invite.header.recipient.equals(bob.getPublicKeyBundle()) - ).toBeTruthy() - expect(invite.header.sender.equals(bob.getPublicKeyBundle())).toBeFalsy() + invite.header.recipient.equals(bob.getPublicKeyBundle()), + ).toBeTruthy(); + expect(invite.header.sender.equals(bob.getPublicKeyBundle())).toBeFalsy(); // Round trips expect(invite.toBytes()).toEqual( - SealedInvitationV1.fromBytes(invite.toBytes()).toBytes() - ) + SealedInvitationV1.fromBytes(invite.toBytes()).toBytes(), + ); expect( SealedInvitationV1.fromBytes(invite.toBytes()).header.sender.equals( - alice.getPublicKeyBundle() - ) - ).toBeTruthy() - }) + alice.getPublicKeyBundle(), + ), + ).toBeTruthy(); + }); - it('fails to create with invalid inputs', () => { + it("fails to create with invalid inputs", () => { expect( () => new SealedInvitationV1({ headerBytes: new Uint8Array(), ciphertext: undefined, - }) - ).toThrow('Missing header bytes') + }), + ).toThrow("Missing header bytes"); const header = new SealedInvitationHeaderV1({ sender: alice.getPublicKeyBundle(), recipient: bob.getPublicKeyBundle(), createdNs: new Long(12), - }) + }); expect( () => new SealedInvitationV1({ headerBytes: header.toBytes(), ciphertext: undefined, - }) - ).toThrow('Missing ciphertext') - }) - }) + }), + ).toThrow("Missing ciphertext"); + }); + }); - describe('SealedInvitationHeaderV1', () => { - it('can create with valid inputs', () => { + describe("SealedInvitationHeaderV1", () => { + it("can create with valid inputs", () => { const header = new SealedInvitationHeaderV1({ sender: alice.getPublicKeyBundle(), recipient: bob.getPublicKeyBundle(), createdNs: new Long(123), - }) + }); - expect(header.recipient.equals(bob.getPublicKeyBundle())).toBeTruthy() - expect(header.sender.equals(alice.getPublicKeyBundle())).toBeTruthy() - expect(header.createdNs.toString()).toEqual(new Long(123).toString()) - }) + expect(header.recipient.equals(bob.getPublicKeyBundle())).toBeTruthy(); + expect(header.sender.equals(alice.getPublicKeyBundle())).toBeTruthy(); + expect(header.createdNs.toString()).toEqual(new Long(123).toString()); + }); - it('fails to create with invalid inputs', () => { + it("fails to create with invalid inputs", () => { expect( () => new SealedInvitationHeaderV1({ sender: undefined, recipient: bob.getPublicKeyBundle(), createdNs: new Long(12), - }) - ).toThrow('Missing sender') + }), + ).toThrow("Missing sender"); expect( () => new SealedInvitationHeaderV1({ sender: alice.getPublicKeyBundle(), recipient: undefined, createdNs: new Long(12), - }) - ).toThrow('Missing recipient') - }) - }) - - describe('InvitationV1', () => { - it('can create with valid inputs', () => { - const keyMaterial = crypto.getRandomValues(new Uint8Array(32)) - const topic = 'foo' + }), + ).toThrow("Missing recipient"); + }); + }); + + describe("InvitationV1", () => { + it("can create with valid inputs", () => { + const keyMaterial = crypto.getRandomValues(new Uint8Array(32)); + const topic = "foo"; const invite = new InvitationV1({ topic, context: undefined, @@ -194,20 +194,20 @@ describe('Invitations', () => { keyMaterial, }, consentProof: undefined, - }) + }); - expect(invite.topic).toEqual(topic) - expect(invite.aes256GcmHkdfSha256.keyMaterial).toEqual(keyMaterial) + expect(invite.topic).toEqual(topic); + expect(invite.aes256GcmHkdfSha256.keyMaterial).toEqual(keyMaterial); // Round trips expect(invite.toBytes()).toEqual( - InvitationV1.fromBytes(invite.toBytes()).toBytes() - ) - }) + InvitationV1.fromBytes(invite.toBytes()).toBytes(), + ); + }); - it('fails to create with invalid inputs', () => { - const keyMaterial = crypto.getRandomValues(new Uint8Array(32)) - const topic = 'foo' + it("fails to create with invalid inputs", () => { + const keyMaterial = crypto.getRandomValues(new Uint8Array(32)); + const topic = "foo"; expect( () => @@ -216,18 +216,18 @@ describe('Invitations', () => { context: undefined, aes256GcmHkdfSha256: { keyMaterial: new Uint8Array() }, consentProof: undefined, - }) - ).toThrow('Missing key material') + }), + ).toThrow("Missing key material"); expect( () => new InvitationV1({ - topic: '', + topic: "", context: undefined, aes256GcmHkdfSha256: { keyMaterial }, consentProof: undefined, - }) - ).toThrow('Missing topic') - }) - }) -}) + }), + ).toThrow("Missing topic"); + }); + }); +}); diff --git a/packages/js-sdk/test/Keygen.test.ts b/packages/js-sdk/test/Keygen.test.ts index 81d897daf..3cbf7df97 100644 --- a/packages/js-sdk/test/Keygen.test.ts +++ b/packages/js-sdk/test/Keygen.test.ts @@ -1,51 +1,51 @@ -import ApiClient, { ApiUrls } from '@/ApiClient' -import Client, { defaultOptions } from '@/Client' -import { decodePrivateKeyBundle } from '@/crypto/PrivateKeyBundle' -import type { PublicKeyBundle } from '@/crypto/PublicKeyBundle' -import TopicPersistence from '@/keystore/persistence/TopicPersistence' -import NetworkKeyManager from '@/keystore/providers/NetworkKeyManager' -import type { Signer } from '@/types/Signer' -import { newWallet } from './helpers' +import ApiClient, { ApiUrls } from "@/ApiClient"; +import Client, { defaultOptions } from "@/Client"; +import { decodePrivateKeyBundle } from "@/crypto/PrivateKeyBundle"; +import type { PublicKeyBundle } from "@/crypto/PublicKeyBundle"; +import TopicPersistence from "@/keystore/persistence/TopicPersistence"; +import NetworkKeyManager from "@/keystore/providers/NetworkKeyManager"; +import type { Signer } from "@/types/Signer"; +import { newWallet } from "./helpers"; -describe('Key Generation', () => { - let wallet: Signer +describe("Key Generation", () => { + let wallet: Signer; beforeEach(async () => { - wallet = newWallet() - }) + wallet = newWallet(); + }); - test('Network store', async () => { + test("Network store", async () => { const opts = { - env: 'local' as keyof typeof ApiUrls, - } - const keys = await Client.getKeys(wallet, opts) + env: "local" as keyof typeof ApiUrls, + }; + const keys = await Client.getKeys(wallet, opts); const client = await Client.create(null, { ...opts, privateKeyOverride: keys, - }) + }); expect( ( decodePrivateKeyBundle(keys).getPublicKeyBundle() as PublicKeyBundle - ).equals(client.publicKeyBundle) - ).toBeTruthy() - }) + ).equals(client.publicKeyBundle), + ).toBeTruthy(); + }); // Make sure that the keys are being saved to the network upon generation - test('Ensure persistence', async () => { + test("Ensure persistence", async () => { const opts = defaultOptions({ - env: 'local' as keyof typeof ApiUrls, - }) - const keys = await Client.getKeys(wallet, opts) + env: "local" as keyof typeof ApiUrls, + }); + const keys = await Client.getKeys(wallet, opts); const manager = new NetworkKeyManager( wallet, - new TopicPersistence(new ApiClient(ApiUrls.local)) - ) + new TopicPersistence(new ApiClient(ApiUrls.local)), + ); expect( (await manager.loadPrivateKeyBundle()) ?.getPublicKeyBundle() .equals( - decodePrivateKeyBundle(keys).getPublicKeyBundle() as PublicKeyBundle - ) - ).toBeTruthy() - }) -}) + decodePrivateKeyBundle(keys).getPublicKeyBundle() as PublicKeyBundle, + ), + ).toBeTruthy(); + }); +}); diff --git a/packages/js-sdk/test/Message.test.ts b/packages/js-sdk/test/Message.test.ts index b242eb3ce..79f27fa2e 100644 --- a/packages/js-sdk/test/Message.test.ts +++ b/packages/js-sdk/test/Message.test.ts @@ -1,320 +1,320 @@ -import { ContentTypeText } from '@xmtp/content-type-text' -import type { Wallet } from 'ethers' -import { createWalletClient, http } from 'viem' -import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' -import { mainnet } from 'viem/chains' -import Client from '@/Client' -import { ConversationV1 } from '@/conversations/Conversation' -import { sha256 } from '@/crypto/encryption' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import { bytesToHex, equalBytes } from '@/crypto/utils' -import { KeystoreError } from '@/keystore/errors' -import InMemoryKeystore from '@/keystore/InMemoryKeystore' -import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' -import { DecodedMessage, MessageV1 } from '@/Message' -import { ContentTypeTestKey, TestKeyCodec } from './ContentTypeTestKey' -import { newWallet } from './helpers' - -describe('Message', function () { - let aliceWallet: Wallet - let bobWallet: Wallet - let alice: PrivateKeyBundleV1 - let bob: PrivateKeyBundleV1 +import { ContentTypeText } from "@xmtp/content-type-text"; +import type { Wallet } from "ethers"; +import { createWalletClient, http } from "viem"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { mainnet } from "viem/chains"; +import Client from "@/Client"; +import { ConversationV1 } from "@/conversations/Conversation"; +import { sha256 } from "@/crypto/encryption"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import { bytesToHex, equalBytes } from "@/crypto/utils"; +import { KeystoreError } from "@/keystore/errors"; +import InMemoryKeystore from "@/keystore/InMemoryKeystore"; +import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; +import { DecodedMessage, MessageV1 } from "@/Message"; +import { ContentTypeTestKey, TestKeyCodec } from "./ContentTypeTestKey"; +import { newWallet } from "./helpers"; + +describe("Message", function () { + let aliceWallet: Wallet; + let bobWallet: Wallet; + let alice: PrivateKeyBundleV1; + let bob: PrivateKeyBundleV1; beforeEach(async () => { - aliceWallet = newWallet() - bobWallet = newWallet() - alice = await PrivateKeyBundleV1.generate(aliceWallet) - bob = await PrivateKeyBundleV1.generate(bobWallet) - }) - it('fully encodes/decodes messages', async function () { + aliceWallet = newWallet(); + bobWallet = newWallet(); + alice = await PrivateKeyBundleV1.generate(aliceWallet); + bob = await PrivateKeyBundleV1.generate(bobWallet); + }); + it("fully encodes/decodes messages", async function () { // Alice's key bundle - const alicePub = alice.getPublicKeyBundle() - expect(alice.identityKey).toBeTruthy() - expect(alice.identityKey.publicKey).toEqual(alicePub.identityKey) + const alicePub = alice.getPublicKeyBundle(); + expect(alice.identityKey).toBeTruthy(); + expect(alice.identityKey.publicKey).toEqual(alicePub.identityKey); const bobWalletAddress = bob .getPublicKeyBundle() - .identityKey.walletSignatureAddress() + .identityKey.walletSignatureAddress(); const bobKeystore = await InMemoryKeystore.create( bob, - InMemoryPersistence.create() - ) + InMemoryPersistence.create(), + ); // Alice encodes message for Bob - const content = new TextEncoder().encode('Yo!') + const content = new TextEncoder().encode("Yo!"); const aliceKeystore = await InMemoryKeystore.create( alice, - InMemoryPersistence.create() - ) + InMemoryPersistence.create(), + ); const msg1 = await MessageV1.encode( aliceKeystore, content, alicePub, bob.getPublicKeyBundle(), - new Date() - ) + new Date(), + ); - expect(msg1.senderAddress).toEqual(aliceWallet.address) - expect(msg1.recipientAddress).toEqual(bobWalletAddress) + expect(msg1.senderAddress).toEqual(aliceWallet.address); + expect(msg1.recipientAddress).toEqual(bobWalletAddress); const decrypted = await msg1.decrypt( aliceKeystore, - alice.getPublicKeyBundle() - ) - expect(decrypted).toEqual(content) + alice.getPublicKeyBundle(), + ); + expect(decrypted).toEqual(content); // Bob decodes message from Alice - const msg2 = await MessageV1.fromBytes(msg1.toBytes()) + const msg2 = await MessageV1.fromBytes(msg1.toBytes()); const msg2Decrypted = await msg2.decrypt( bobKeystore, - bob.getPublicKeyBundle() - ) - expect(msg2Decrypted).toEqual(decrypted) - expect(msg2.senderAddress).toEqual(aliceWallet.address) - expect(msg2.recipientAddress).toEqual(bobWalletAddress) - }) - - it('undecodable returns with undefined decrypted value', async () => { - const eve = await PrivateKeyBundleV1.generate(newWallet()) + bob.getPublicKeyBundle(), + ); + expect(msg2Decrypted).toEqual(decrypted); + expect(msg2.senderAddress).toEqual(aliceWallet.address); + expect(msg2.recipientAddress).toEqual(bobWalletAddress); + }); + + it("undecodable returns with undefined decrypted value", async () => { + const eve = await PrivateKeyBundleV1.generate(newWallet()); const aliceKeystore = await InMemoryKeystore.create( alice, - InMemoryPersistence.create() - ) + InMemoryPersistence.create(), + ); const eveKeystore = await InMemoryKeystore.create( eve, - InMemoryPersistence.create() - ) + InMemoryPersistence.create(), + ); const msg = await MessageV1.encode( aliceKeystore, - new TextEncoder().encode('Hi'), + new TextEncoder().encode("Hi"), alice.getPublicKeyBundle(), bob.getPublicKeyBundle(), - new Date() - ) - expect(!msg.error).toBeTruthy() - const eveResult = msg.decrypt(eveKeystore, eve.getPublicKeyBundle()) - expect(eveResult).rejects.toThrow(KeystoreError) - }) - - it('Message create throws error for sender without wallet', async () => { - const amal = await PrivateKeyBundleV1.generate() + new Date(), + ); + expect(!msg.error).toBeTruthy(); + const eveResult = msg.decrypt(eveKeystore, eve.getPublicKeyBundle()); + expect(eveResult).rejects.toThrow(KeystoreError); + }); + + it("Message create throws error for sender without wallet", async () => { + const amal = await PrivateKeyBundleV1.generate(); const keystore = await InMemoryKeystore.create( bob, - InMemoryPersistence.create() - ) + InMemoryPersistence.create(), + ); expect( MessageV1.encode( keystore, - new TextEncoder().encode('hi'), + new TextEncoder().encode("hi"), amal.getPublicKeyBundle(), bob.getPublicKeyBundle(), - new Date() - ) - ).rejects.toThrow('key is not signed') - }) + new Date(), + ), + ).rejects.toThrow("key is not signed"); + }); - it('recipientAddress throws error without wallet', async () => { - const charlie = await PrivateKeyBundleV1.generate() + it("recipientAddress throws error without wallet", async () => { + const charlie = await PrivateKeyBundleV1.generate(); const keystore = await InMemoryKeystore.create( alice, - InMemoryPersistence.create() - ) + InMemoryPersistence.create(), + ); const msg = await MessageV1.encode( keystore, - new TextEncoder().encode('hi'), + new TextEncoder().encode("hi"), alice.getPublicKeyBundle(), charlie.getPublicKeyBundle(), - new Date() - ) + new Date(), + ); expect(() => { - const _ = msg.recipientAddress - }).toThrow('key is not signed') - }) + const _ = msg.recipientAddress; + }).toThrow("key is not signed"); + }); - it('id returns bytes as hex string of sha256 hash', async () => { + it("id returns bytes as hex string of sha256 hash", async () => { const keystore = await InMemoryKeystore.create( alice, - InMemoryPersistence.create() - ) + InMemoryPersistence.create(), + ); const msg = await MessageV1.encode( keystore, - new TextEncoder().encode('hi'), + new TextEncoder().encode("hi"), alice.getPublicKeyBundle(), alice.getPublicKeyBundle(), - new Date() - ) - expect(msg.id.length).toEqual(64) - expect(msg.id).toEqual(bytesToHex(await sha256(msg.toBytes()))) - }) - - describe('DecodedMessage', () => { - it('round trips V1 text messages', async () => { - const text = 'hi bob' + new Date(), + ); + expect(msg.id.length).toEqual(64); + expect(msg.id).toEqual(bytesToHex(await sha256(msg.toBytes()))); + }); + + describe("DecodedMessage", () => { + it("round trips V1 text messages", async () => { + const text = "hi bob"; const aliceClient = await Client.create(aliceWallet, { - env: 'local', + env: "local", privateKeyOverride: alice.encode(), - }) - const { payload } = await aliceClient.encodeContent(text) - const timestamp = new Date() - const sender = alice.getPublicKeyBundle() - const recipient = bob.getPublicKeyBundle() + }); + const { payload } = await aliceClient.encodeContent(text); + const timestamp = new Date(); + const sender = alice.getPublicKeyBundle(); + const recipient = bob.getPublicKeyBundle(); const message = await MessageV1.encode( aliceClient.keystore, payload, sender, recipient, - timestamp - ) + timestamp, + ); const decodedMessage = DecodedMessage.fromV1Message( message, text, ContentTypeText, payload, - 'foo', + "foo", new ConversationV1( aliceClient, bob.identityKey.publicKey.walletSignatureAddress(), - new Date() - ) - ) + new Date(), + ), + ); - const messageBytes = decodedMessage.toBytes() - expect(messageBytes).toBeDefined() + const messageBytes = decodedMessage.toBytes(); + expect(messageBytes).toBeDefined(); const restoredDecodedMessage = await DecodedMessage.fromBytes( messageBytes, - aliceClient - ) - expect(restoredDecodedMessage.toBytes()).toEqual(messageBytes) - expect(restoredDecodedMessage.content).toEqual(text) - expect(restoredDecodedMessage).toEqual(decodedMessage) - }) - - it('round trips V2 text messages', async () => { + aliceClient, + ); + expect(restoredDecodedMessage.toBytes()).toEqual(messageBytes); + expect(restoredDecodedMessage.content).toEqual(text); + expect(restoredDecodedMessage).toEqual(decodedMessage); + }); + + it("round trips V2 text messages", async () => { const aliceClient = await Client.create(aliceWallet, { - env: 'local', + env: "local", privateKeyOverride: alice.encode(), - }) + }); const bobClient = await Client.create(bobWallet, { - env: 'local', + env: "local", privateKeyOverride: bob.encode(), - }) + }); const convo = await aliceClient.conversations.newConversation( - bobClient.address - ) - const text = 'hi bob' - const sentMessage = await convo.send(text) + bobClient.address, + ); + const text = "hi bob"; + const sentMessage = await convo.send(text); - const sentMessageBytes = sentMessage.toBytes() - expect(sentMessageBytes).toBeDefined() + const sentMessageBytes = sentMessage.toBytes(); + expect(sentMessageBytes).toBeDefined(); const restoredDecodedMessage = await DecodedMessage.fromBytes( sentMessageBytes, - aliceClient - ) - expect(restoredDecodedMessage.toBytes()).toEqual(sentMessageBytes) - expect(restoredDecodedMessage.content).toEqual(text) - expect(restoredDecodedMessage).toEqual(sentMessage) - }) - - it('round trips V2 text messages with viem', async () => { + aliceClient, + ); + expect(restoredDecodedMessage.toBytes()).toEqual(sentMessageBytes); + expect(restoredDecodedMessage.content).toEqual(text); + expect(restoredDecodedMessage).toEqual(sentMessage); + }); + + it("round trips V2 text messages with viem", async () => { const aliceWalletClient = createWalletClient({ account: privateKeyToAccount(generatePrivateKey()), chain: mainnet, transport: http(), - }) + }); const aliceClient = await Client.create(aliceWalletClient, { - env: 'local', + env: "local", privateKeyOverride: alice.encode(), - }) + }); const bobWalletClient = createWalletClient({ account: privateKeyToAccount(generatePrivateKey()), chain: mainnet, transport: http(), - }) + }); const bobClient = await Client.create(bobWalletClient, { - env: 'local', + env: "local", privateKeyOverride: bob.encode(), - }) + }); const convo = await aliceClient.conversations.newConversation( - bobClient.address - ) - const text = 'hi bob' - const sentMessage = await convo.send(text) + bobClient.address, + ); + const text = "hi bob"; + const sentMessage = await convo.send(text); - const sentMessageBytes = sentMessage.toBytes() - expect(sentMessageBytes).toBeDefined() + const sentMessageBytes = sentMessage.toBytes(); + expect(sentMessageBytes).toBeDefined(); const restoredDecodedMessage = await DecodedMessage.fromBytes( sentMessageBytes, - aliceClient - ) - expect(restoredDecodedMessage.toBytes()).toEqual(sentMessageBytes) - expect(restoredDecodedMessage.content).toEqual(text) - expect(restoredDecodedMessage).toEqual(sentMessage) - }) - - it('round trips messages with custom content types', async () => { + aliceClient, + ); + expect(restoredDecodedMessage.toBytes()).toEqual(sentMessageBytes); + expect(restoredDecodedMessage.content).toEqual(text); + expect(restoredDecodedMessage).toEqual(sentMessage); + }); + + it("round trips messages with custom content types", async () => { // Alice has the custom codec and bob does not const aliceClient = await Client.create(aliceWallet, { codecs: [new TestKeyCodec()], - env: 'local', + env: "local", privateKeyOverride: alice.encode(), - }) + }); const bobClient = await Client.create(bobWallet, { - env: 'local', + env: "local", privateKeyOverride: bob.encode(), - }) + }); const convo = await aliceClient.conversations.newConversation( - bobClient.address - ) + bobClient.address, + ); - const msg = alice.identityKey.publicKey - const fallback = 'publickey bundle' + const msg = alice.identityKey.publicKey; + const fallback = "publickey bundle"; const sentMessage = await convo.send(msg, { contentType: ContentTypeTestKey, - }) - expect(sentMessage.contentType).toEqual(ContentTypeTestKey) + }); + expect(sentMessage.contentType).toEqual(ContentTypeTestKey); - const sentMessageBytes = sentMessage.toBytes() + const sentMessageBytes = sentMessage.toBytes(); const aliceRestoredMessage = await DecodedMessage.fromBytes( sentMessageBytes, - aliceClient - ) + aliceClient, + ); if ( - typeof aliceRestoredMessage.content === 'string' || + typeof aliceRestoredMessage.content === "string" || !aliceRestoredMessage.content ) { - throw new Error('Expected content to be a PublicKeyBundle') + throw new Error("Expected content to be a PublicKeyBundle"); } expect( equalBytes( aliceRestoredMessage.content?.secp256k1Uncompressed.bytes, - msg.secp256k1Uncompressed.bytes - ) - ).toBeTruthy() - expect(aliceRestoredMessage.contentType).toEqual(ContentTypeTestKey) + msg.secp256k1Uncompressed.bytes, + ), + ).toBeTruthy(); + expect(aliceRestoredMessage.contentType).toEqual(ContentTypeTestKey); const bobRestoredMessage = await DecodedMessage.fromBytes( sentMessageBytes, - bobClient - ) - expect(bobRestoredMessage.error).toBeTruthy() - expect(bobRestoredMessage.content).toBeUndefined() - expect(bobRestoredMessage.contentFallback).toEqual(fallback) - }) - }) -}) + bobClient, + ); + expect(bobRestoredMessage.error).toBeTruthy(); + expect(bobRestoredMessage.content).toBeUndefined(); + expect(bobRestoredMessage.contentFallback).toEqual(fallback); + }); + }); +}); diff --git a/packages/js-sdk/test/authn/Authn.test.ts b/packages/js-sdk/test/authn/Authn.test.ts index 58b520d95..13106343f 100644 --- a/packages/js-sdk/test/authn/Authn.test.ts +++ b/packages/js-sdk/test/authn/Authn.test.ts @@ -1,101 +1,101 @@ -import type { Wallet } from 'ethers' -import Long from 'long' -import { hexToBytes, keccak256 } from 'viem' -import AuthCache from '@/authn/AuthCache' -import Authenticator from '@/authn/LocalAuthenticator' -import Token from '@/authn/Token' -import { PrivateKey } from '@/crypto/PrivateKey' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import Signature from '@/crypto/Signature' -import { newWallet, sleep } from '@test/helpers' +import type { Wallet } from "ethers"; +import Long from "long"; +import { hexToBytes, keccak256 } from "viem"; +import AuthCache from "@/authn/AuthCache"; +import Authenticator from "@/authn/LocalAuthenticator"; +import Token from "@/authn/Token"; +import { PrivateKey } from "@/crypto/PrivateKey"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import Signature from "@/crypto/Signature"; +import { newWallet, sleep } from "@test/helpers"; -describe('authn', () => { - let authenticator: Authenticator - let privateKey: PrivateKey - let wallet: Wallet +describe("authn", () => { + let authenticator: Authenticator; + let privateKey: PrivateKey; + let wallet: Wallet; beforeEach(async () => { - wallet = newWallet() - const bundle = await PrivateKeyBundleV1.generate(wallet) - privateKey = bundle.identityKey - authenticator = new Authenticator(privateKey) - }) + wallet = newWallet(); + const bundle = await PrivateKeyBundleV1.generate(wallet); + privateKey = bundle.identityKey; + authenticator = new Authenticator(privateKey); + }); - it('can create a token', async () => { - const timestamp = new Date() - const token = await authenticator.createToken(timestamp) + it("can create a token", async () => { + const timestamp = new Date(); + const token = await authenticator.createToken(timestamp); expect(token.authData.walletAddr).toEqual( - privateKey.publicKey.walletSignatureAddress() - ) + privateKey.publicKey.walletSignatureAddress(), + ); expect(token.authData.createdNs).toEqual( - Long.fromNumber(timestamp.valueOf()).toUnsigned().multiply(1_000_000) - ) - expect(token.identityKey.timestamp).toEqual(privateKey.publicKey.timestamp) - expect(token.identityKey.signature).toEqual(privateKey.publicKey.signature) + Long.fromNumber(timestamp.valueOf()).toUnsigned().multiply(1_000_000), + ); + expect(token.identityKey.timestamp).toEqual(privateKey.publicKey.timestamp); + expect(token.identityKey.signature).toEqual(privateKey.publicKey.signature); expect(token.identityKey.secp256k1Uncompressed).toEqual( - privateKey.publicKey.secp256k1Uncompressed - ) - }) + privateKey.publicKey.secp256k1Uncompressed, + ); + }); - it('rejects unsigned identity keys', async () => { - const pk = PrivateKey.generate() - expect(pk.publicKey.signature).toBeUndefined() + it("rejects unsigned identity keys", async () => { + const pk = PrivateKey.generate(); + expect(pk.publicKey.signature).toBeUndefined(); expect(() => new Authenticator(pk)).toThrow( - 'Provided public key is not signed' - ) - }) + "Provided public key is not signed", + ); + }); - it('round trips safely', async () => { - const originalToken = await authenticator.createToken() - const bytes = originalToken.toBytes() - const newToken = Token.fromBytes(bytes) - expect(originalToken.authData).toEqual(newToken.authData) - expect(originalToken.toBytes()).toEqual(newToken.toBytes()) - }) + it("round trips safely", async () => { + const originalToken = await authenticator.createToken(); + const bytes = originalToken.toBytes(); + const newToken = Token.fromBytes(bytes); + expect(originalToken.authData).toEqual(newToken.authData); + expect(originalToken.toBytes()).toEqual(newToken.toBytes()); + }); - it('creates a signature that can be verified', async () => { - const token = await authenticator.createToken() - const digest = hexToBytes(keccak256(token.authDataBytes)) - const sig = new Signature(token.authDataSignature) + it("creates a signature that can be verified", async () => { + const token = await authenticator.createToken(); + const digest = hexToBytes(keccak256(token.authDataBytes)); + const sig = new Signature(token.authDataSignature); - expect(sig.getPublicKey(digest)?.equals(privateKey.publicKey)).toBeTruthy() - expect(privateKey.publicKey.verify(sig, digest)).toBeTruthy() - }) -}) + expect(sig.getPublicKey(digest)?.equals(privateKey.publicKey)).toBeTruthy(); + expect(privateKey.publicKey.verify(sig, digest)).toBeTruthy(); + }); +}); -describe('AuthCache', () => { - let authenticator: Authenticator - let privateKey: PrivateKey - let wallet: Wallet +describe("AuthCache", () => { + let authenticator: Authenticator; + let privateKey: PrivateKey; + let wallet: Wallet; beforeEach(async () => { - wallet = newWallet() - const bundle = await PrivateKeyBundleV1.generate(wallet) - privateKey = bundle.identityKey - authenticator = new Authenticator(privateKey) - }) + wallet = newWallet(); + const bundle = await PrivateKeyBundleV1.generate(wallet); + privateKey = bundle.identityKey; + authenticator = new Authenticator(privateKey); + }); - it('safely re-uses cached token', async () => { - const authCache = new AuthCache(authenticator) - const firstToken = await authCache.getToken() - const secondToken = await authCache.getToken() - expect(firstToken).toEqual(secondToken) - }) + it("safely re-uses cached token", async () => { + const authCache = new AuthCache(authenticator); + const firstToken = await authCache.getToken(); + const secondToken = await authCache.getToken(); + expect(firstToken).toEqual(secondToken); + }); - it('refreshes to new token', async () => { - const authCache = new AuthCache(authenticator) - const firstToken = await authCache.getToken() - await authCache.refresh() - const secondToken = await authCache.getToken() - expect(firstToken === secondToken).toBeFalsy() - }) + it("refreshes to new token", async () => { + const authCache = new AuthCache(authenticator); + const firstToken = await authCache.getToken(); + await authCache.refresh(); + const secondToken = await authCache.getToken(); + expect(firstToken === secondToken).toBeFalsy(); + }); - it('respects expiration', async () => { - const authCache = new AuthCache(authenticator, 0.01) - const firstToken = await authCache.getToken() - await sleep(50) - const secondToken = await authCache.getToken() - expect(firstToken === secondToken).toBeFalsy() - }) -}) + it("respects expiration", async () => { + const authCache = new AuthCache(authenticator, 0.01); + const firstToken = await authCache.getToken(); + await sleep(50); + const secondToken = await authCache.getToken(); + expect(firstToken === secondToken).toBeFalsy(); + }); +}); diff --git a/packages/js-sdk/test/conversations/Conversation.test.ts b/packages/js-sdk/test/conversations/Conversation.test.ts index db65a3747..7cb084845 100644 --- a/packages/js-sdk/test/conversations/Conversation.test.ts +++ b/packages/js-sdk/test/conversations/Conversation.test.ts @@ -1,564 +1,568 @@ -import { ContentTypeId } from '@xmtp/content-type-primitives' -import { ContentTypeText } from '@xmtp/content-type-text' -import { content as proto } from '@xmtp/proto' -import { assert, vi } from 'vitest' -import { SortDirection } from '@/ApiClient' -import type Client from '@/Client' -import { Compression } from '@/Client' -import { ConversationV2 } from '@/conversations/Conversation' -import { PrivateKey } from '@/crypto/PrivateKey' -import { SignedPublicKeyBundle } from '@/crypto/PublicKeyBundle' -import { DecodedMessage, MessageV1, type MessageV2 } from '@/Message' -import { sleep } from '@/utils/async' -import { buildDirectMessageTopic } from '@/utils/topic' -import { ContentTypeTestKey, TestKeyCodec } from '@test/ContentTypeTestKey' -import { newLocalHostClient, waitForUserContact } from '@test/helpers' - -describe('conversation', () => { - let alice: Client - let bob: Client - - describe('v1', () => { +import { ContentTypeId } from "@xmtp/content-type-primitives"; +import { ContentTypeText } from "@xmtp/content-type-text"; +import { content as proto } from "@xmtp/proto"; +import { assert, vi } from "vitest"; +import { SortDirection } from "@/ApiClient"; +import type Client from "@/Client"; +import { Compression } from "@/Client"; +import { ConversationV2 } from "@/conversations/Conversation"; +import { PrivateKey } from "@/crypto/PrivateKey"; +import { SignedPublicKeyBundle } from "@/crypto/PublicKeyBundle"; +import { DecodedMessage, MessageV1, type MessageV2 } from "@/Message"; +import { sleep } from "@/utils/async"; +import { buildDirectMessageTopic } from "@/utils/topic"; +import { ContentTypeTestKey, TestKeyCodec } from "@test/ContentTypeTestKey"; +import { newLocalHostClient, waitForUserContact } from "@test/helpers"; + +describe("conversation", () => { + let alice: Client; + let bob: Client; + + describe("v1", () => { beforeEach(async () => { - alice = await newLocalHostClient({ publishLegacyContact: true }) - bob = await newLocalHostClient({ publishLegacyContact: true }) - await waitForUserContact(alice, alice) - await waitForUserContact(bob, bob) - }) + alice = await newLocalHostClient({ publishLegacyContact: true }); + bob = await newLocalHostClient({ publishLegacyContact: true }); + await waitForUserContact(alice, alice); + await waitForUserContact(bob, bob); + }); - it('lists all messages', async () => { + it("lists all messages", async () => { const aliceConversation = await alice.conversations.newConversation( - bob.address - ) - expect(aliceConversation.conversationVersion).toBe('v1') + bob.address, + ); + expect(aliceConversation.conversationVersion).toBe("v1"); const bobConversation = await bob.conversations.newConversation( - alice.address - ) - expect(bobConversation.conversationVersion).toBe('v1') + alice.address, + ); + expect(bobConversation.conversationVersion).toBe("v1"); - const startingMessages = await aliceConversation.messages() - expect(startingMessages).toHaveLength(0) - await sleep(100) + const startingMessages = await aliceConversation.messages(); + expect(startingMessages).toHaveLength(0); + await sleep(100); - await bobConversation.send('Hi Alice') - await aliceConversation.send('Hi Bob') - await sleep(100) + await bobConversation.send("Hi Alice"); + await aliceConversation.send("Hi Bob"); + await sleep(100); const [aliceMessages, bobMessages] = await Promise.all([ aliceConversation.messages(), bobConversation.messages(), - ]) + ]); - expect(aliceMessages).toHaveLength(2) - expect(aliceMessages[0].messageVersion).toBe('v1') - expect(aliceMessages[0].error).toBeUndefined() - expect(aliceMessages[0].senderAddress).toBe(bob.address) - expect(aliceMessages[0].conversation.topic).toBe(aliceConversation.topic) + expect(aliceMessages).toHaveLength(2); + expect(aliceMessages[0].messageVersion).toBe("v1"); + expect(aliceMessages[0].error).toBeUndefined(); + expect(aliceMessages[0].senderAddress).toBe(bob.address); + expect(aliceMessages[0].conversation.topic).toBe(aliceConversation.topic); - expect(bobMessages).toHaveLength(2) - }) + expect(bobMessages).toHaveLength(2); + }); - it('lists paginated messages', async () => { + it("lists paginated messages", async () => { const aliceConversation = await alice.conversations.newConversation( - bob.address - ) + bob.address, + ); for (let i = 0; i < 10; i++) { - await aliceConversation.send('gm') + await aliceConversation.send("gm"); } - await sleep(100) + await sleep(100); - let numPages = 0 - const messageIds = new Set() + let numPages = 0; + const messageIds = new Set(); for await (const page of aliceConversation.messagesPaginated({ pageSize: 5, })) { - numPages++ - expect(page).toHaveLength(5) + numPages++; + expect(page).toHaveLength(5); for (const msg of page) { - expect(msg.content).toBe('gm') - messageIds.add(msg.id) + expect(msg.content).toBe("gm"); + messageIds.add(msg.id); } } - expect(numPages).toBe(2) - expect(messageIds.size).toBe(10) + expect(numPages).toBe(2); + expect(messageIds.size).toBe(10); // Test sorting - let lastMessage: DecodedMessage | undefined + let lastMessage: DecodedMessage | undefined; for await (const page of aliceConversation.messagesPaginated({ direction: SortDirection.SORT_DIRECTION_DESCENDING, })) { for (const msg of page) { if (lastMessage && lastMessage.sent) { expect(msg.sent?.valueOf()).toBeLessThanOrEqual( - lastMessage.sent?.valueOf() - ) + lastMessage.sent?.valueOf(), + ); } - expect(msg).toBeInstanceOf(DecodedMessage) - lastMessage = msg + expect(msg).toBeInstanceOf(DecodedMessage); + lastMessage = msg; } } - }) + }); - it('ignores failed decoding of messages', async () => { - const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + it("ignores failed decoding of messages", async () => { + const consoleWarn = vi + .spyOn(console, "warn") + .mockImplementation(() => {}); const aliceConversation = await alice.conversations.newConversation( - bob.address - ) + bob.address, + ); // This should be readable - await aliceConversation.send('gm') + await aliceConversation.send("gm"); // This should not be readable await alice.publishEnvelopes([ { message: Uint8Array.from([1, 2, 3]), contentTopic: buildDirectMessageTopic(alice.address, bob.address), }, - ]) - await sleep(100) + ]); + await sleep(100); - let numMessages = 0 + let numMessages = 0; for await (const page of aliceConversation.messagesPaginated()) { - numMessages += page.length + numMessages += page.length; } - expect(numMessages).toBe(1) - expect(consoleWarn).toBeCalledTimes(1) - consoleWarn.mockRestore() - }) + expect(numMessages).toBe(1); + expect(consoleWarn).toBeCalledTimes(1); + consoleWarn.mockRestore(); + }); - it('does not allow self messaging', async () => { + it("does not allow self messaging", async () => { expect( - alice.conversations.newConversation(alice.address) - ).rejects.toThrow('self messaging not supported') + alice.conversations.newConversation(alice.address), + ).rejects.toThrow("self messaging not supported"); expect( - alice.conversations.newConversation(alice.address.toLowerCase()) - ).rejects.toThrow('self messaging not supported') - }) + alice.conversations.newConversation(alice.address.toLowerCase()), + ).rejects.toThrow("self messaging not supported"); + }); - it('can send a prepared message v1', async () => { + it("can send a prepared message v1", async () => { const aliceConversation = await alice.conversations.newConversation( - bob.address - ) + bob.address, + ); - const preparedMessage = await aliceConversation.prepareMessage('1') - const messageID = await preparedMessage.messageID() + const preparedMessage = await aliceConversation.prepareMessage("1"); + const messageID = await preparedMessage.messageID(); - const sentMessage = await preparedMessage.send() + const sentMessage = await preparedMessage.send(); - const messages = await aliceConversation.messages() - const message = messages[0] - expect(message.id).toBe(messageID) - expect(sentMessage.id).toBe(messageID) - expect(sentMessage.messageVersion).toBe('v1') - }) + const messages = await aliceConversation.messages(); + const message = messages[0]; + expect(message.id).toBe(messageID); + expect(sentMessage.id).toBe(messageID); + expect(sentMessage.messageVersion).toBe("v1"); + }); - it('can send a prepared message v2', async () => { + it("can send a prepared message v2", async () => { const aliceConversation = await alice.conversations.newConversation( bob.address, { - conversationId: 'example.com', + conversationId: "example.com", metadata: {}, - } - ) + }, + ); - const preparedMessage = await aliceConversation.prepareMessage('sup') - const messageID = await preparedMessage.messageID() + const preparedMessage = await aliceConversation.prepareMessage("sup"); + const messageID = await preparedMessage.messageID(); - const sentMessage = await preparedMessage.send() + const sentMessage = await preparedMessage.send(); - const messages = await aliceConversation.messages() - const message = messages[0] - expect(message.id).toBe(messageID) - expect(message.content).toBe('sup') - expect(sentMessage.id).toBe(messageID) - expect(sentMessage.messageVersion).toBe('v2') - }) + const messages = await aliceConversation.messages(); + const message = messages[0]; + expect(message.id).toBe(messageID); + expect(message.content).toBe("sup"); + expect(sentMessage.id).toBe(messageID); + expect(sentMessage.messageVersion).toBe("v2"); + }); - it('can send and stream ephemeral topic v1', async () => { + it("can send and stream ephemeral topic v1", async () => { const aliceConversation = await alice.conversations.newConversation( - bob.address - ) + bob.address, + ); // Start the stream before sending the message to ensure delivery - const stream = await aliceConversation.streamEphemeral() + const stream = await aliceConversation.streamEphemeral(); if (!stream) { - assert.fail('no stream') + assert.fail("no stream"); } - await sleep(100) + await sleep(100); - await aliceConversation.send('hello', { ephemeral: true }) - await sleep(100) + await aliceConversation.send("hello", { ephemeral: true }); + await sleep(100); - const result = await stream.next() - const message = result.value + const result = await stream.next(); + const message = result.value; - expect(message.error).toBeUndefined() - expect(message.messageVersion).toBe('v1') - expect(message.content).toBe('hello') - expect(message.senderAddress).toBe(alice.address) + expect(message.error).toBeUndefined(); + expect(message.messageVersion).toBe("v1"); + expect(message.content).toBe("hello"); + expect(message.senderAddress).toBe(alice.address); - await sleep(100) + await sleep(100); // The message should not be persisted - expect(await aliceConversation.messages()).toHaveLength(0) - await stream.return() - }) + expect(await aliceConversation.messages()).toHaveLength(0); + await stream.return(); + }); - it('can send and stream ephemeral topic v2', async () => { + it("can send and stream ephemeral topic v2", async () => { const aliceConversation = await alice.conversations.newConversation( bob.address, { - conversationId: 'example.com', + conversationId: "example.com", metadata: {}, - } - ) + }, + ); // Start the stream before sending the message to ensure delivery - const stream = await aliceConversation.streamEphemeral() + const stream = await aliceConversation.streamEphemeral(); if (!stream) { - assert.fail('no stream') + assert.fail("no stream"); } - await sleep(100) + await sleep(100); - await aliceConversation.send('hello', { ephemeral: true }) - await sleep(100) + await aliceConversation.send("hello", { ephemeral: true }); + await sleep(100); - const result = await stream.next() - const message = result.value + const result = await stream.next(); + const message = result.value; - expect(message.error).toBeUndefined() - expect(message.messageVersion).toBe('v2') - expect(message.content).toBe('hello') - expect(message.senderAddress).toBe(alice.address) + expect(message.error).toBeUndefined(); + expect(message.messageVersion).toBe("v2"); + expect(message.content).toBe("hello"); + expect(message.senderAddress).toBe(alice.address); - await sleep(100) + await sleep(100); // The message should not be persisted - expect(await aliceConversation.messages()).toHaveLength(0) - await stream.return() - }) + expect(await aliceConversation.messages()).toHaveLength(0); + await stream.return(); + }); - it('allows for sorted listing', async () => { + it("allows for sorted listing", async () => { const aliceConversation = await alice.conversations.newConversation( - bob.address - ) - await aliceConversation.send('1') - await aliceConversation.send('2') - await sleep(100) + bob.address, + ); + await aliceConversation.send("1"); + await aliceConversation.send("2"); + await sleep(100); - const sortedAscending = await aliceConversation.messages() - expect(sortedAscending.length).toBe(2) - expect(sortedAscending[0].content).toBe('1') + const sortedAscending = await aliceConversation.messages(); + expect(sortedAscending.length).toBe(2); + expect(sortedAscending[0].content).toBe("1"); const sortedDescending = await aliceConversation.messages({ direction: SortDirection.SORT_DIRECTION_DESCENDING, - }) - expect(sortedDescending[0].content).toBe('2') - }) + }); + expect(sortedDescending[0].content).toBe("2"); + }); - it('streams messages', async () => { + it("streams messages", async () => { const aliceConversation = await alice.conversations.newConversation( - bob.address - ) + bob.address, + ); const bobConversation = await bob.conversations.newConversation( - alice.address - ) + alice.address, + ); // Start the stream before sending the message to ensure delivery - const stream = await aliceConversation.streamMessages() - await sleep(100) - await bobConversation.send('gm') + const stream = await aliceConversation.streamMessages(); + await sleep(100); + await bobConversation.send("gm"); - let numMessages = 0 + let numMessages = 0; for await (const message of stream) { - numMessages++ + numMessages++; expect(message.contentTopic).toBe( - buildDirectMessageTopic(alice.address, bob.address) - ) - expect(message.conversation.topic).toBe(aliceConversation.topic) - expect(message.error).toBeUndefined() - expect(message.messageVersion).toBe('v1') + buildDirectMessageTopic(alice.address, bob.address), + ); + expect(message.conversation.topic).toBe(aliceConversation.topic); + expect(message.error).toBeUndefined(); + expect(message.messageVersion).toBe("v1"); if (numMessages === 1) { - expect(message.content).toBe('gm') - expect(message.senderAddress).toBe(bob.address) - expect(message.recipientAddress).toBe(alice.address) + expect(message.content).toBe("gm"); + expect(message.senderAddress).toBe(bob.address); + expect(message.recipientAddress).toBe(alice.address); } else { - expect(message.content).toBe('gm to you too') - expect(message.senderAddress).toBe(alice.address) + expect(message.content).toBe("gm to you too"); + expect(message.senderAddress).toBe(alice.address); } if (numMessages === 5) { - break + break; } - await aliceConversation.send('gm to you too') + await aliceConversation.send("gm to you too"); } - const result = await stream.next() - expect(result.done).toBeTruthy() + const result = await stream.next(); + expect(result.done).toBeTruthy(); - await sleep(100) - expect(numMessages).toBe(5) - expect(await aliceConversation.messages()).toHaveLength(5) - await stream.return() - }) + await sleep(100); + expect(numMessages).toBe(5); + expect(await aliceConversation.messages()).toHaveLength(5); + await stream.return(); + }); - it('handles limiting page size', async () => { - const bobConvo = await alice.conversations.newConversation(bob.address) + it("handles limiting page size", async () => { + const bobConvo = await alice.conversations.newConversation(bob.address); for (let i = 0; i < 5; i++) { - await bobConvo.send('hi') + await bobConvo.send("hi"); } - const messages = await bobConvo.messages({ limit: 2 }) - expect(messages).toHaveLength(2) - }) + const messages = await bobConvo.messages({ limit: 2 }); + expect(messages).toHaveLength(2); + }); - it('queries with date filters', async () => { - const now = new Date().valueOf() + it("queries with date filters", async () => { + const now = new Date().valueOf(); const dates = [1, 2, 3, 4, 5].map( - (daysAgo) => new Date(now - daysAgo * 1000 * 60 * 60 * 24) - ) - const convo = await alice.conversations.newConversation(bob.address) + (daysAgo) => new Date(now - daysAgo * 1000 * 60 * 60 * 24), + ); + const convo = await alice.conversations.newConversation(bob.address); for (const date of dates) { - await convo.send('gm: ' + date.valueOf(), { timestamp: date }) + await convo.send("gm: " + date.valueOf(), { timestamp: date }); } - await sleep(100) + await sleep(100); - const fourDaysAgoOrMore = await convo.messages({ endTime: dates[3] }) - expect(fourDaysAgoOrMore).toHaveLength(2) + const fourDaysAgoOrMore = await convo.messages({ endTime: dates[3] }); + expect(fourDaysAgoOrMore).toHaveLength(2); - const twoDaysAgoOrLess = await convo.messages({ startTime: dates[1] }) - expect(twoDaysAgoOrLess).toHaveLength(2) + const twoDaysAgoOrLess = await convo.messages({ startTime: dates[1] }); + expect(twoDaysAgoOrLess).toHaveLength(2); const twoToFourDaysAgo = await convo.messages({ endTime: dates[1], startTime: dates[3], - }) - expect(twoToFourDaysAgo).toHaveLength(3) - }) + }); + expect(twoToFourDaysAgo).toHaveLength(3); + }); - it('can send compressed v1 messages', async () => { - const convo = await alice.conversations.newConversation(bob.address) - const content = 'A'.repeat(111) + it("can send compressed v1 messages", async () => { + const convo = await alice.conversations.newConversation(bob.address); + const content = "A".repeat(111); await convo.send(content, { contentType: ContentTypeText, compression: Compression.COMPRESSION_DEFLATE, - }) + }); - await sleep(100) + await sleep(100); // Verify that messages are actually compressed const envelopes = await alice.apiClient.query( { contentTopic: convo.topic, }, - { limit: 1 } - ) - const messageBytes = envelopes[0].message as Uint8Array - const decoded = await MessageV1.fromBytes(messageBytes) + { limit: 1 }, + ); + const messageBytes = envelopes[0].message as Uint8Array; + const decoded = await MessageV1.fromBytes(messageBytes); const decrypted = await decoded.decrypt( alice.keystore, - alice.publicKeyBundle - ) - const encodedContent = proto.EncodedContent.decode(decrypted) + alice.publicKeyBundle, + ); + const encodedContent = proto.EncodedContent.decode(decrypted); expect(encodedContent.content).not.toStrictEqual( - new Uint8Array(111).fill(65) - ) - expect(encodedContent.compression).toBe(Compression.COMPRESSION_DEFLATE) - - const results = await convo.messages() - expect(results).toHaveLength(1) - const msg = results[0] - expect(msg.content).toBe(content) - }) - - it('does not compress v1 messages less than 10 bytes', async () => { - const convo = await alice.conversations.newConversation(bob.address) - await convo.send('gm!', { + new Uint8Array(111).fill(65), + ); + expect(encodedContent.compression).toBe(Compression.COMPRESSION_DEFLATE); + + const results = await convo.messages(); + expect(results).toHaveLength(1); + const msg = results[0]; + expect(msg.content).toBe(content); + }); + + it("does not compress v1 messages less than 10 bytes", async () => { + const convo = await alice.conversations.newConversation(bob.address); + await convo.send("gm!", { contentType: ContentTypeText, compression: Compression.COMPRESSION_DEFLATE, - }) + }); const envelopes = await alice.apiClient.query( { contentTopic: convo.topic, }, - { limit: 1 } - ) - const messageBytes = envelopes[0].message as Uint8Array - const decoded = await MessageV1.fromBytes(messageBytes) + { limit: 1 }, + ); + const messageBytes = envelopes[0].message as Uint8Array; + const decoded = await MessageV1.fromBytes(messageBytes); const decrypted = await decoded.decrypt( alice.keystore, - alice.publicKeyBundle - ) - const encodedContent = proto.EncodedContent.decode(decrypted) - expect(encodedContent.compression).toBeUndefined() - }) - - it('throws when opening a conversation with an unknown address', () => { - expect(alice.conversations.newConversation('0xfoo')).rejects.toThrow() - const validButUnknown = '0x1111111111222222222233333333334444444444' + alice.publicKeyBundle, + ); + const encodedContent = proto.EncodedContent.decode(decrypted); + expect(encodedContent.compression).toBeUndefined(); + }); + + it("throws when opening a conversation with an unknown address", () => { + expect(alice.conversations.newConversation("0xfoo")).rejects.toThrow(); + const validButUnknown = "0x1111111111222222222233333333334444444444"; expect( - alice.conversations.newConversation(validButUnknown) + alice.conversations.newConversation(validButUnknown), ).rejects.toThrow( - `Recipient ${validButUnknown} is not on the XMTP network` - ) - }) + `Recipient ${validButUnknown} is not on the XMTP network`, + ); + }); - it('normalizes lowercase addresses', async () => { - const bobLower = bob.address.toLowerCase() + it("normalizes lowercase addresses", async () => { + const bobLower = bob.address.toLowerCase(); await expect( - alice.conversations.newConversation(bobLower) + alice.conversations.newConversation(bobLower), ).resolves.toMatchObject({ peerAddress: bob.address, - }) - }) - - it('filters out spoofed messages', async () => { - const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const aliceConvo = await alice.conversations.newConversation(bob.address) - const bobConvo = await bob.conversations.newConversation(alice.address) - const stream = await bobConvo.streamMessages() - await sleep(100) + }); + }); + + it("filters out spoofed messages", async () => { + const consoleWarn = vi + .spyOn(console, "warn") + .mockImplementation(() => {}); + const aliceConvo = await alice.conversations.newConversation(bob.address); + const bobConvo = await bob.conversations.newConversation(alice.address); + const stream = await bobConvo.streamMessages(); + await sleep(100); // mallory takes over alice's client - const mallory = await newLocalHostClient() - const aliceKeystore = alice.keystore - alice.keystore = mallory.keystore - await aliceConvo.send('Hello from Mallory') + const mallory = await newLocalHostClient(); + const aliceKeystore = alice.keystore; + alice.keystore = mallory.keystore; + await aliceConvo.send("Hello from Mallory"); // alice restores control - alice.keystore = aliceKeystore - await aliceConvo.send('Hello from Alice') - const result = await stream.next() - const msg = result.value - expect(msg.senderAddress).toBe(alice.address) - expect(msg.content).toBe('Hello from Alice') - await stream.return() - expect(consoleWarn).toBeCalledTimes(1) - consoleWarn.mockRestore() - }) - - it('can send custom content type', async () => { - const aliceConvo = await alice.conversations.newConversation(bob.address) - const bobConvo = await bob.conversations.newConversation(alice.address) - const aliceStream = await aliceConvo.streamMessages() - const bobStream = await bobConvo.streamMessages() - const key = PrivateKey.generate().publicKey + alice.keystore = aliceKeystore; + await aliceConvo.send("Hello from Alice"); + const result = await stream.next(); + const msg = result.value; + expect(msg.senderAddress).toBe(alice.address); + expect(msg.content).toBe("Hello from Alice"); + await stream.return(); + expect(consoleWarn).toBeCalledTimes(1); + consoleWarn.mockRestore(); + }); + + it("can send custom content type", async () => { + const aliceConvo = await alice.conversations.newConversation(bob.address); + const bobConvo = await bob.conversations.newConversation(alice.address); + const aliceStream = await aliceConvo.streamMessages(); + const bobStream = await bobConvo.streamMessages(); + const key = PrivateKey.generate().publicKey; // alice doesn't recognize the type await expect( // @ts-expect-error default client doesn't have the right type aliceConvo.send(key, { contentType: ContentTypeTestKey, - }) - ).rejects.toThrow('unknown content type xmtp.test/public-key:1.0') + }), + ).rejects.toThrow("unknown content type xmtp.test/public-key:1.0"); // bob doesn't recognize the type - alice.registerCodec(new TestKeyCodec()) + alice.registerCodec(new TestKeyCodec()); // @ts-expect-error default client doesn't have the right type await aliceConvo.send(key, { contentType: ContentTypeTestKey, - }) + }); - const aliceResult1 = await aliceStream.next() - const aliceMessage1 = aliceResult1.value - expect(aliceMessage1.content).toEqual(key) + const aliceResult1 = await aliceStream.next(); + const aliceMessage1 = aliceResult1.value; + expect(aliceMessage1.content).toEqual(key); - const bobResult1 = await bobStream.next() - const bobMessage1 = bobResult1.value - expect(bobMessage1).toBeTruthy() + const bobResult1 = await bobStream.next(); + const bobMessage1 = bobResult1.value; + expect(bobMessage1).toBeTruthy(); expect(bobMessage1.error?.message).toBe( - 'unknown content type xmtp.test/public-key:1.0' - ) - expect(bobMessage1.contentType).toBeTruthy() - expect(bobMessage1.contentType.sameAs(ContentTypeTestKey)) - expect(bobMessage1.content).toBeUndefined() - expect(bobMessage1.contentFallback).toBe('publickey bundle') + "unknown content type xmtp.test/public-key:1.0", + ); + expect(bobMessage1.contentType).toBeTruthy(); + expect(bobMessage1.contentType.sameAs(ContentTypeTestKey)); + expect(bobMessage1.content).toBeUndefined(); + expect(bobMessage1.contentFallback).toBe("publickey bundle"); // both recognize the type - bob.registerCodec(new TestKeyCodec()) + bob.registerCodec(new TestKeyCodec()); // @ts-expect-error default client doesn't have the right type await aliceConvo.send(key, { contentType: ContentTypeTestKey, - }) - const bobResult2 = await bobStream.next() - const bobMessage2 = bobResult2.value - expect(bobMessage2.contentType).toBeTruthy() - expect(bobMessage2.contentType.sameAs(ContentTypeTestKey)).toBeTruthy() - expect(key.equals(bobMessage2.content)).toBeTruthy() + }); + const bobResult2 = await bobStream.next(); + const bobMessage2 = bobResult2.value; + expect(bobMessage2.contentType).toBeTruthy(); + expect(bobMessage2.contentType.sameAs(ContentTypeTestKey)).toBeTruthy(); + expect(key.equals(bobMessage2.content)).toBeTruthy(); // alice tries to send version that is not supported const type2 = new ContentTypeId({ ...ContentTypeTestKey, versionMajor: 2, - }) + }); // @ts-expect-error default client doesn't have the right type expect(aliceConvo.send(key, { contentType: type2 })).rejects.toThrow( - 'unknown content type xmtp.test/public-key:2.0' - ) + "unknown content type xmtp.test/public-key:2.0", + ); - await bobStream.return() - await aliceStream.return() - }) - }) + await bobStream.return(); + await aliceStream.return(); + }); + }); - describe('v2', () => { + describe("v2", () => { beforeEach(async () => { - alice = await newLocalHostClient() - bob = await newLocalHostClient() - await waitForUserContact(alice, alice) - await waitForUserContact(bob, bob) - }) + alice = await newLocalHostClient(); + bob = await newLocalHostClient(); + await waitForUserContact(alice, alice); + await waitForUserContact(bob, bob); + }); - it('v2 conversation', async () => { + it("v2 conversation", async () => { expect(await bob.getUserContact(alice.address)).toBeInstanceOf( - SignedPublicKeyBundle - ) + SignedPublicKeyBundle, + ); expect(await alice.getUserContact(bob.address)).toBeInstanceOf( - SignedPublicKeyBundle - ) + SignedPublicKeyBundle, + ); - const ac = await alice.conversations.newConversation(bob.address) - expect(ac.conversationVersion).toBe('v2') + const ac = await alice.conversations.newConversation(bob.address); + expect(ac.conversationVersion).toBe("v2"); if (!(ac instanceof ConversationV2)) { - assert.fail() + assert.fail(); } - const as = await ac.streamMessages() - await sleep(100) + const as = await ac.streamMessages(); + await sleep(100); - const bcs = await bob.conversations.list() - expect(bcs).toHaveLength(1) - const bc = bcs[0] - expect(bc.conversationVersion).toBe('v2') + const bcs = await bob.conversations.list(); + expect(bcs).toHaveLength(1); + const bc = bcs[0]; + expect(bc.conversationVersion).toBe("v2"); if (!(bc instanceof ConversationV2)) { - assert.fail() + assert.fail(); } - expect(bc.topic).toBe(ac.topic) - const bs = await bc.streamMessages() - await sleep(100) + expect(bc.topic).toBe(ac.topic); + const bs = await bc.streamMessages(); + await sleep(100); - await ac.send('gm') - expect((await bs.next()).value.content).toBe('gm') - expect((await as.next()).value.content).toBe('gm') - await bc.send('gm to you too') - expect((await bs.next()).value.content).toBe('gm to you too') - expect((await as.next()).value.content).toBe('gm to you too') + await ac.send("gm"); + expect((await bs.next()).value.content).toBe("gm"); + expect((await as.next()).value.content).toBe("gm"); + await bc.send("gm to you too"); + expect((await bs.next()).value.content).toBe("gm to you too"); + expect((await as.next()).value.content).toBe("gm to you too"); - await bs.return() - await as.return() + await bs.return(); + await as.return(); const messages = await alice.listEnvelopes( ac.topic, - ac.processEnvelope.bind(ac) - ) + ac.processEnvelope.bind(ac), + ); - expect(messages).toHaveLength(2) - expect(messages[0].shouldPush).toBe(true) - expect(messages[0].senderHmac).toBeDefined() - expect(messages[1].shouldPush).toBe(true) - expect(messages[1].senderHmac).toBeDefined() - }) + expect(messages).toHaveLength(2); + expect(messages[0].shouldPush).toBe(true); + expect(messages[0].senderHmac).toBeDefined(); + expect(messages[1].shouldPush).toBe(true); + expect(messages[1].senderHmac).toBeDefined(); + }); // it('rejects spoofed contact bundles', async () => { // // Generated via exporting 1) conversationV2Export and 2) pre-crafted envelope with swapped contact bundles @@ -589,238 +593,238 @@ describe('conversation', () => { // ).rejects.toThrow('pre key not signed by identity key') // }) - it('does not compress v2 messages less than 10 bytes', async () => { + it("does not compress v2 messages less than 10 bytes", async () => { const convo = await alice.conversations.newConversation(bob.address, { - conversationId: 'example.com/nocompression', + conversationId: "example.com/nocompression", metadata: {}, - }) - await convo.send('gm!', { + }); + await convo.send("gm!", { contentType: ContentTypeText, compression: Compression.COMPRESSION_DEFLATE, - }) + }); const envelopes = await alice.apiClient.query( { contentTopic: convo.topic, }, - { limit: 1 } - ) - const msg = await convo.decodeMessage(envelopes[0]) - const decoded = proto.EncodedContent.decode(msg.contentBytes) - expect(decoded.compression).toBeUndefined() - }) - - it('can send compressed v2 messages of various lengths', async () => { + { limit: 1 }, + ); + const msg = await convo.decodeMessage(envelopes[0]); + const decoded = proto.EncodedContent.decode(msg.contentBytes); + expect(decoded.compression).toBeUndefined(); + }); + + it("can send compressed v2 messages of various lengths", async () => { const convo = await alice.conversations.newConversation(bob.address, { - conversationId: 'example.com/compressedv2', + conversationId: "example.com/compressedv2", metadata: {}, - }) - const content = 'A'.repeat(111) + }); + const content = "A".repeat(111); await convo.send(content, { contentType: ContentTypeText, compression: Compression.COMPRESSION_DEFLATE, - }) - await convo.send('gm!', { + }); + await convo.send("gm!", { contentType: ContentTypeText, compression: Compression.COMPRESSION_DEFLATE, - }) - const results = await convo.messages() - expect(results).toHaveLength(2) - expect(results[0].content).toBe(content) - expect(results[1].content).toBe('gm!') - }) - - it('can send compressed v2 prepared messages of various lengths', async () => { + }); + const results = await convo.messages(); + expect(results).toHaveLength(2); + expect(results[0].content).toBe(content); + expect(results[1].content).toBe("gm!"); + }); + + it("can send compressed v2 prepared messages of various lengths", async () => { const aliceConversation = await alice.conversations.newConversation( bob.address, { - conversationId: 'example.com', + conversationId: "example.com", metadata: {}, - } - ) + }, + ); - const preparedMessage = await aliceConversation.prepareMessage('gm!', { + const preparedMessage = await aliceConversation.prepareMessage("gm!", { compression: Compression.COMPRESSION_DEFLATE, - }) - const messageID = await preparedMessage.messageID() - const sentMessage = await preparedMessage.send() + }); + const messageID = await preparedMessage.messageID(); + const sentMessage = await preparedMessage.send(); const preparedMessage2 = await aliceConversation.prepareMessage( - 'A'.repeat(100), + "A".repeat(100), { compression: Compression.COMPRESSION_DEFLATE, - } - ) - const messageID2 = await preparedMessage2.messageID() - const sentMessage2 = await preparedMessage2.send() - - const messages = await aliceConversation.messages() - expect(messages[0].id).toBe(messageID) - expect(messages[0].content).toBe('gm!') - expect(sentMessage.id).toBe(messageID) - expect(sentMessage.messageVersion).toBe('v2') - expect(messages[1].id).toBe(messageID2) - expect(messages[1].content).toBe('A'.repeat(100)) - expect(sentMessage2.id).toBe(messageID2) - expect(sentMessage2.messageVersion).toBe('v2') - }) - - it('handles limiting page size', async () => { + }, + ); + const messageID2 = await preparedMessage2.messageID(); + const sentMessage2 = await preparedMessage2.send(); + + const messages = await aliceConversation.messages(); + expect(messages[0].id).toBe(messageID); + expect(messages[0].content).toBe("gm!"); + expect(sentMessage.id).toBe(messageID); + expect(sentMessage.messageVersion).toBe("v2"); + expect(messages[1].id).toBe(messageID2); + expect(messages[1].content).toBe("A".repeat(100)); + expect(sentMessage2.id).toBe(messageID2); + expect(sentMessage2.messageVersion).toBe("v2"); + }); + + it("handles limiting page size", async () => { const bobConvo = await alice.conversations.newConversation(bob.address, { - conversationId: 'xmtp.org/foo', + conversationId: "xmtp.org/foo", metadata: {}, - }) + }); for (let i = 0; i < 5; i++) { - await bobConvo.send('hi') + await bobConvo.send("hi"); } - await sleep(100) - const messages = await bobConvo.messages({ limit: 2 }) - expect(messages).toHaveLength(2) - }) - - it('conversation filtering', async () => { - const conversationId = 'xmtp.org/foo' - const title = 'foo' + await sleep(100); + const messages = await bobConvo.messages({ limit: 2 }); + expect(messages).toHaveLength(2); + }); + + it("conversation filtering", async () => { + const conversationId = "xmtp.org/foo"; + const title = "foo"; const convo = await alice.conversations.newConversation(bob.address, { conversationId, metadata: { title, }, - }) + }); - const stream = await convo.streamMessages() - await sleep(100) - const sentMessage = await convo.send('foo') + const stream = await convo.streamMessages(); + await sleep(100); + const sentMessage = await convo.send("foo"); if (!(sentMessage instanceof DecodedMessage)) { - throw new Error('Not a DecodedMessage') + throw new Error("Not a DecodedMessage"); } expect(sentMessage.conversation.context?.conversationId).toBe( - conversationId - ) - await sleep(100) + conversationId, + ); + await sleep(100); - const firstMessageFromStream = (await stream.next()).value - expect(firstMessageFromStream.messageVersion).toBe('v2') - expect(firstMessageFromStream.content).toBe('foo') + const firstMessageFromStream = (await stream.next()).value; + expect(firstMessageFromStream.messageVersion).toBe("v2"); + expect(firstMessageFromStream.content).toBe("foo"); expect(firstMessageFromStream.conversation.context?.conversationId).toBe( - conversationId - ) - - const messages = await convo.messages() - expect(messages).toHaveLength(1) - expect(messages[0].content).toBe('foo') - expect(messages[0].conversation).toBe(convo) - await stream.return() - }) - - it('queries with date filters', async () => { - const now = new Date().valueOf() + conversationId, + ); + + const messages = await convo.messages(); + expect(messages).toHaveLength(1); + expect(messages[0].content).toBe("foo"); + expect(messages[0].conversation).toBe(convo); + await stream.return(); + }); + + it("queries with date filters", async () => { + const now = new Date().valueOf(); const dates = [1, 2, 3, 4, 5].map( - (daysAgo) => new Date(now - daysAgo * 1000 * 60 * 60 * 24) - ) + (daysAgo) => new Date(now - daysAgo * 1000 * 60 * 60 * 24), + ); const convo = await alice.conversations.newConversation(bob.address, { - conversationId: 'xmtp.org/foo', + conversationId: "xmtp.org/foo", metadata: {}, - }) + }); for (const date of dates) { - await convo.send('gm: ' + date.valueOf(), { timestamp: date }) + await convo.send("gm: " + date.valueOf(), { timestamp: date }); } - await sleep(100) + await sleep(100); - const fourDaysAgoOrMore = await convo.messages({ endTime: dates[3] }) - expect(fourDaysAgoOrMore).toHaveLength(2) + const fourDaysAgoOrMore = await convo.messages({ endTime: dates[3] }); + expect(fourDaysAgoOrMore).toHaveLength(2); - const twoDaysAgoOrLess = await convo.messages({ startTime: dates[1] }) - expect(twoDaysAgoOrLess).toHaveLength(2) + const twoDaysAgoOrLess = await convo.messages({ startTime: dates[1] }); + expect(twoDaysAgoOrLess).toHaveLength(2); const twoToFourDaysAgo = await convo.messages({ endTime: dates[1], startTime: dates[3], - }) - expect(twoToFourDaysAgo).toHaveLength(3) - }) + }); + expect(twoToFourDaysAgo).toHaveLength(3); + }); - it('can send custom content type', async () => { + it("can send custom content type", async () => { const aliceConvo = await alice.conversations.newConversation( bob.address, { - conversationId: 'xmtp.org/key', + conversationId: "xmtp.org/key", metadata: {}, - } - ) + }, + ); if (!(aliceConvo instanceof ConversationV2)) { - assert.fail() + assert.fail(); } - await sleep(100) + await sleep(100); const bobConvo = await bob.conversations.newConversation(alice.address, { - conversationId: 'xmtp.org/key', + conversationId: "xmtp.org/key", metadata: {}, - }) - const aliceStream = await aliceConvo.streamMessages() - const bobStream = await bobConvo.streamMessages() - const key = PrivateKey.generate().publicKey + }); + const aliceStream = await aliceConvo.streamMessages(); + const bobStream = await bobConvo.streamMessages(); + const key = PrivateKey.generate().publicKey; // alice doesn't recognize the type expect( aliceConvo.send(key, { contentType: ContentTypeTestKey, - }) - ).rejects.toThrow('unknown content type xmtp.test/public-key:1.0') + }), + ).rejects.toThrow("unknown content type xmtp.test/public-key:1.0"); // bob doesn't recognize the type - alice.registerCodec(new TestKeyCodec()) + alice.registerCodec(new TestKeyCodec()); await aliceConvo.send(key, { contentType: ContentTypeTestKey, - }) + }); - const aliceResult1 = await aliceStream.next() - const aliceMessage1 = aliceResult1.value - expect(aliceMessage1.content).toEqual(key) + const aliceResult1 = await aliceStream.next(); + const aliceMessage1 = aliceResult1.value; + expect(aliceMessage1.content).toEqual(key); - const bobResult1 = await bobStream.next() - const bobMessage1 = bobResult1.value - expect(bobMessage1).toBeTruthy() + const bobResult1 = await bobStream.next(); + const bobMessage1 = bobResult1.value; + expect(bobMessage1).toBeTruthy(); expect(bobMessage1.error?.message).toBe( - 'unknown content type xmtp.test/public-key:1.0' - ) - expect(bobMessage1.contentType).toBeTruthy() - expect(bobMessage1.contentType.sameAs(ContentTypeTestKey)) - expect(bobMessage1.content).toBeUndefined() - expect(bobMessage1.contentFallback).toBe('publickey bundle') + "unknown content type xmtp.test/public-key:1.0", + ); + expect(bobMessage1.contentType).toBeTruthy(); + expect(bobMessage1.contentType.sameAs(ContentTypeTestKey)); + expect(bobMessage1.content).toBeUndefined(); + expect(bobMessage1.contentFallback).toBe("publickey bundle"); // both recognize the type - bob.registerCodec(new TestKeyCodec()) + bob.registerCodec(new TestKeyCodec()); await aliceConvo.send(key, { contentType: ContentTypeTestKey, - }) - const bobResult2 = await bobStream.next() - const bobMessage2 = bobResult2.value - expect(bobMessage2.contentType).toBeTruthy() - expect(bobMessage2.contentType.sameAs(ContentTypeTestKey)).toBeTruthy() - expect(key.equals(bobMessage2.content)).toBeTruthy() + }); + const bobResult2 = await bobStream.next(); + const bobMessage2 = bobResult2.value; + expect(bobMessage2.contentType).toBeTruthy(); + expect(bobMessage2.contentType.sameAs(ContentTypeTestKey)).toBeTruthy(); + expect(key.equals(bobMessage2.content)).toBeTruthy(); // alice tries to send version that is not supported const type2 = new ContentTypeId({ ...ContentTypeTestKey, versionMajor: 2, - }) + }); expect(aliceConvo.send(key, { contentType: type2 })).rejects.toThrow( - 'unknown content type xmtp.test/public-key:2.0' - ) + "unknown content type xmtp.test/public-key:2.0", + ); - await bobStream.return() - await aliceStream.return() + await bobStream.return(); + await aliceStream.return(); const messages = await alice.listEnvelopes( aliceConvo.topic, - aliceConvo.processEnvelope.bind(aliceConvo) - ) - - expect(messages).toHaveLength(2) - expect(messages[0].shouldPush).toBe(false) - expect(messages[0].senderHmac).toBeDefined() - expect(messages[1].shouldPush).toBe(false) - expect(messages[1].senderHmac).toBeDefined() - }) - }) -}) + aliceConvo.processEnvelope.bind(aliceConvo), + ); + + expect(messages).toHaveLength(2); + expect(messages[0].shouldPush).toBe(false); + expect(messages[0].senderHmac).toBeDefined(); + expect(messages[1].shouldPush).toBe(false); + expect(messages[1].senderHmac).toBeDefined(); + }); + }); +}); diff --git a/packages/js-sdk/test/conversations/Conversations.test.ts b/packages/js-sdk/test/conversations/Conversations.test.ts index 45a808835..bd5070697 100644 --- a/packages/js-sdk/test/conversations/Conversations.test.ts +++ b/packages/js-sdk/test/conversations/Conversations.test.ts @@ -1,429 +1,433 @@ -import type Client from '@/Client' -import { ConversationV1, ConversationV2 } from '@/conversations/Conversation' -import { sleep } from '@/utils/async' -import { buildDirectMessageTopic, buildUserIntroTopic } from '@/utils/topic' -import { newLocalHostClient } from '@test/helpers' +import type Client from "@/Client"; +import { ConversationV1, ConversationV2 } from "@/conversations/Conversation"; +import { sleep } from "@/utils/async"; +import { buildDirectMessageTopic, buildUserIntroTopic } from "@/utils/topic"; +import { newLocalHostClient } from "@test/helpers"; -describe('conversations', () => { - describe('listConversations', () => { - let alice: Client - let bob: Client +describe("conversations", () => { + describe("listConversations", () => { + let alice: Client; + let bob: Client; beforeEach(async () => { - alice = await newLocalHostClient({ publishLegacyContact: true }) - bob = await newLocalHostClient({ publishLegacyContact: true }) - }) + alice = await newLocalHostClient({ publishLegacyContact: true }); + bob = await newLocalHostClient({ publishLegacyContact: true }); + }); afterEach(async () => { - if (alice) await alice.close() - if (bob) await bob.close() - }) + if (alice) await alice.close(); + if (bob) await bob.close(); + }); - it('lists all conversations', async () => { - const aliceConversations = await alice.conversations.list() - expect(aliceConversations).toHaveLength(0) + it("lists all conversations", async () => { + const aliceConversations = await alice.conversations.list(); + expect(aliceConversations).toHaveLength(0); - const aliceToBob = await alice.conversations.newConversation(bob.address) - await aliceToBob.send('gm') + const aliceToBob = await alice.conversations.newConversation(bob.address); + await aliceToBob.send("gm"); - const aliceConversationsAfterMessage = await alice.conversations.list() - expect(aliceConversationsAfterMessage).toHaveLength(1) - expect(aliceConversationsAfterMessage[0].peerAddress).toBe(bob.address) + const aliceConversationsAfterMessage = await alice.conversations.list(); + expect(aliceConversationsAfterMessage).toHaveLength(1); + expect(aliceConversationsAfterMessage[0].peerAddress).toBe(bob.address); - const bobConversations = await bob.conversations.list() - expect(bobConversations).toHaveLength(1) - expect(bobConversations[0].peerAddress).toBe(alice.address) - }) + const bobConversations = await bob.conversations.list(); + expect(bobConversations).toHaveLength(1); + expect(bobConversations[0].peerAddress).toBe(alice.address); + }); - it('lists conversations from cache', async () => { - const aliceConversations = await alice.conversations.list() - expect(aliceConversations).toHaveLength(0) + it("lists conversations from cache", async () => { + const aliceConversations = await alice.conversations.list(); + expect(aliceConversations).toHaveLength(0); const aliceConversationsFromCache = - await alice.conversations.listFromCache() - expect(aliceConversationsFromCache).toHaveLength(0) + await alice.conversations.listFromCache(); + expect(aliceConversationsFromCache).toHaveLength(0); - const bobConversationsFromCache = await bob.conversations.listFromCache() - expect(bobConversationsFromCache).toHaveLength(0) + const bobConversationsFromCache = await bob.conversations.listFromCache(); + expect(bobConversationsFromCache).toHaveLength(0); - const aliceToBob = await alice.conversations.newConversation(bob.address) - await aliceToBob.send('gm') - await sleep(100) + const aliceToBob = await alice.conversations.newConversation(bob.address); + await aliceToBob.send("gm"); + await sleep(100); - expect(await alice.conversations.listFromCache()).toHaveLength(0) - expect(await bob.conversations.listFromCache()).toHaveLength(0) + expect(await alice.conversations.listFromCache()).toHaveLength(0); + expect(await bob.conversations.listFromCache()).toHaveLength(0); - const aliceConversationsAfterMessage = await alice.conversations.list() - expect(aliceConversationsAfterMessage).toHaveLength(1) - expect(aliceConversationsAfterMessage[0].peerAddress).toBe(bob.address) + const aliceConversationsAfterMessage = await alice.conversations.list(); + expect(aliceConversationsAfterMessage).toHaveLength(1); + expect(aliceConversationsAfterMessage[0].peerAddress).toBe(bob.address); const aliceConversationsFromCacheAfterMessage = - await alice.conversations.listFromCache() - expect(aliceConversationsFromCacheAfterMessage).toHaveLength(1) + await alice.conversations.listFromCache(); + expect(aliceConversationsFromCacheAfterMessage).toHaveLength(1); expect(aliceConversationsFromCacheAfterMessage[0].peerAddress).toBe( - bob.address - ) + bob.address, + ); - const bobConversations = await bob.conversations.list() - expect(bobConversations).toHaveLength(1) - expect(bobConversations[0].peerAddress).toBe(alice.address) + const bobConversations = await bob.conversations.list(); + expect(bobConversations).toHaveLength(1); + expect(bobConversations[0].peerAddress).toBe(alice.address); const bobConversationsFromCacheAfterMessage = - await bob.conversations.listFromCache() - expect(bobConversationsFromCacheAfterMessage).toHaveLength(1) + await bob.conversations.listFromCache(); + expect(bobConversationsFromCacheAfterMessage).toHaveLength(1); expect(bobConversationsFromCacheAfterMessage[0].peerAddress).toBe( - alice.address - ) - }) + alice.address, + ); + }); - it('resumes list with cache after new conversation is created', async () => { - const aliceConversations1 = await alice.conversations.list() - expect(aliceConversations1).toHaveLength(0) + it("resumes list with cache after new conversation is created", async () => { + const aliceConversations1 = await alice.conversations.list(); + expect(aliceConversations1).toHaveLength(0); await alice.conversations.newConversation(bob.address, { - conversationId: 'foo', + conversationId: "foo", metadata: {}, - }) - const aliceConversations2 = await alice.conversations.list() - expect(aliceConversations2).toHaveLength(1) + }); + const aliceConversations2 = await alice.conversations.list(); + expect(aliceConversations2).toHaveLength(1); await alice.conversations.newConversation(bob.address, { - conversationId: 'bar', + conversationId: "bar", metadata: {}, - }) + }); const fromKeystore = (await alice.keystore.getV2Conversations()) - .conversations - expect(fromKeystore[1].context?.conversationId).toBe('bar') + .conversations; + expect(fromKeystore[1].context?.conversationId).toBe("bar"); - const aliceConversations3 = await alice.conversations.list() - expect(aliceConversations3).toHaveLength(2) - }) - }) + const aliceConversations3 = await alice.conversations.list(); + expect(aliceConversations3).toHaveLength(2); + }); + }); - it('dedupes conversations when multiple messages are in the introduction topic', async () => { - const alice = await newLocalHostClient({ publishLegacyContact: true }) - const bob = await newLocalHostClient({ publishLegacyContact: true }) + it("dedupes conversations when multiple messages are in the introduction topic", async () => { + const alice = await newLocalHostClient({ publishLegacyContact: true }); + const bob = await newLocalHostClient({ publishLegacyContact: true }); const aliceConversation = await alice.conversations.newConversation( - bob.address - ) + bob.address, + ); const bobConversation = await bob.conversations.newConversation( - alice.address - ) + alice.address, + ); await Promise.all([ - aliceConversation.send('gm'), - bobConversation.send('gm'), - ]) + aliceConversation.send("gm"), + bobConversation.send("gm"), + ]); const [aliceConversationsList, bobConversationList] = await Promise.all([ alice.conversations.list(), bob.conversations.list(), - ]) - expect(aliceConversationsList).toHaveLength(1) - expect(bobConversationList).toHaveLength(1) - await alice.close() - await bob.close() - }) + ]); + expect(aliceConversationsList).toHaveLength(1); + expect(bobConversationList).toHaveLength(1); + await alice.close(); + await bob.close(); + }); - describe('newConversation', () => { - let alice: Client - let bob: Client + describe("newConversation", () => { + let alice: Client; + let bob: Client; beforeEach(async () => { - alice = await newLocalHostClient({ publishLegacyContact: true }) - bob = await newLocalHostClient({ publishLegacyContact: true }) - }) + alice = await newLocalHostClient({ publishLegacyContact: true }); + bob = await newLocalHostClient({ publishLegacyContact: true }); + }); afterEach(async () => { - if (alice) await alice.close() - if (bob) await bob.close() - }) - - it('uses an existing v1 conversation when one exists', async () => { - const aliceConvo = await alice.conversations.newConversation(bob.address) - expect(aliceConvo instanceof ConversationV1).toBeTruthy() - await aliceConvo.send('gm') - const bobConvo = await bob.conversations.newConversation(alice.address) - expect(bobConvo instanceof ConversationV1).toBeTruthy() - }) - - it('does not create a duplicate conversation with an address case mismatch', async () => { - const convo1 = await alice.conversations.newConversation(bob.address) - await convo1.send('gm') - const convos = await alice.conversations.list() - expect(convos).toHaveLength(1) + if (alice) await alice.close(); + if (bob) await bob.close(); + }); + + it("uses an existing v1 conversation when one exists", async () => { + const aliceConvo = await alice.conversations.newConversation(bob.address); + expect(aliceConvo instanceof ConversationV1).toBeTruthy(); + await aliceConvo.send("gm"); + const bobConvo = await bob.conversations.newConversation(alice.address); + expect(bobConvo instanceof ConversationV1).toBeTruthy(); + }); + + it("does not create a duplicate conversation with an address case mismatch", async () => { + const convo1 = await alice.conversations.newConversation(bob.address); + await convo1.send("gm"); + const convos = await alice.conversations.list(); + expect(convos).toHaveLength(1); const convo2 = await alice.conversations.newConversation( - bob.address.toLowerCase() - ) - await convo2.send('gm') - const convos2 = await alice.conversations.list() - expect(convos2).toHaveLength(1) - }) - - it('continues to use v1 conversation even after upgrading bundle', async () => { - const aliceConvo = await alice.conversations.newConversation(bob.address) - await aliceConvo.send('gm') - expect(aliceConvo instanceof ConversationV1).toBeTruthy() - await bob.publishUserContact(false) - alice.forgetContact(bob.address) - - const aliceConvo2 = await alice.conversations.newConversation(bob.address) - expect(aliceConvo2 instanceof ConversationV1).toBeTruthy() - await aliceConvo2.send('hi') - - const bobConvo = await bob.conversations.newConversation(alice.address) - expect(bobConvo instanceof ConversationV1).toBeTruthy() - const messages = await bobConvo.messages() - expect(messages.length).toBe(2) - expect(messages[0].content).toBe('gm') - expect(messages[1].content).toBe('hi') - }) - - it('creates a new V2 conversation when no existing convo and V2 bundle', async () => { - await bob.publishUserContact(false) - alice.forgetContact(bob.address) - - const aliceConvo = await alice.conversations.newConversation(bob.address) - expect(aliceConvo instanceof ConversationV2).toBeTruthy() - }) - - it('creates a v2 conversation when conversation ID is present', async () => { - const conversationId = 'xmtp.org/foo' + bob.address.toLowerCase(), + ); + await convo2.send("gm"); + const convos2 = await alice.conversations.list(); + expect(convos2).toHaveLength(1); + }); + + it("continues to use v1 conversation even after upgrading bundle", async () => { + const aliceConvo = await alice.conversations.newConversation(bob.address); + await aliceConvo.send("gm"); + expect(aliceConvo instanceof ConversationV1).toBeTruthy(); + await bob.publishUserContact(false); + alice.forgetContact(bob.address); + + const aliceConvo2 = await alice.conversations.newConversation( + bob.address, + ); + expect(aliceConvo2 instanceof ConversationV1).toBeTruthy(); + await aliceConvo2.send("hi"); + + const bobConvo = await bob.conversations.newConversation(alice.address); + expect(bobConvo instanceof ConversationV1).toBeTruthy(); + const messages = await bobConvo.messages(); + expect(messages.length).toBe(2); + expect(messages[0].content).toBe("gm"); + expect(messages[1].content).toBe("hi"); + }); + + it("creates a new V2 conversation when no existing convo and V2 bundle", async () => { + await bob.publishUserContact(false); + alice.forgetContact(bob.address); + + const aliceConvo = await alice.conversations.newConversation(bob.address); + expect(aliceConvo instanceof ConversationV2).toBeTruthy(); + }); + + it("creates a v2 conversation when conversation ID is present", async () => { + const conversationId = "xmtp.org/foo"; const aliceConvo = await alice.conversations.newConversation( bob.address, - { conversationId, metadata: { foo: 'bar' } } - ) - await sleep(100) + { conversationId, metadata: { foo: "bar" } }, + ); + await sleep(100); - expect(aliceConvo instanceof ConversationV2).toBeTruthy() - expect(aliceConvo.context?.conversationId).toBe(conversationId) - expect(aliceConvo.context?.metadata.foo).toBe('bar') + expect(aliceConvo instanceof ConversationV2).toBeTruthy(); + expect(aliceConvo.context?.conversationId).toBe(conversationId); + expect(aliceConvo.context?.metadata.foo).toBe("bar"); // Ensure alice received an invite - const aliceConvos = await alice.conversations.updateV2Conversations() - expect(aliceConvos).toHaveLength(1) - expect(aliceConvos[0].topic).toBe(aliceConvo.topic) + const aliceConvos = await alice.conversations.updateV2Conversations(); + expect(aliceConvos).toHaveLength(1); + expect(aliceConvos[0].topic).toBe(aliceConvo.topic); // Ensure bob received an invite - const bobConvos = await bob.conversations.updateV2Conversations() - expect(bobConvos).toHaveLength(1) - expect(bobConvos[0].topic).toBe(aliceConvo.topic) - }) + const bobConvos = await bob.conversations.updateV2Conversations(); + expect(bobConvos).toHaveLength(1); + expect(bobConvos[0].topic).toBe(aliceConvo.topic); + }); - it('re-uses same invite when multiple conversations started with the same ID', async () => { - const conversationId = 'xmtp.org/foo' + it("re-uses same invite when multiple conversations started with the same ID", async () => { + const conversationId = "xmtp.org/foo"; const aliceConvo1 = await alice.conversations.newConversation( bob.address, - { conversationId, metadata: {} } - ) - await sleep(100) + { conversationId, metadata: {} }, + ); + await sleep(100); const aliceConvo2 = await alice.conversations.newConversation( bob.address, - { conversationId, metadata: {} } - ) + { conversationId, metadata: {} }, + ); if ( aliceConvo1 instanceof ConversationV2 && aliceConvo2 instanceof ConversationV2 ) { - expect(aliceConvo2.topic).toBe(aliceConvo1.topic) + expect(aliceConvo2.topic).toBe(aliceConvo1.topic); } else { - throw new Error('Not a v2 conversation') + throw new Error("Not a v2 conversation"); } - const aliceConvos = await alice.conversations.updateV2Conversations() - expect(aliceConvos).toHaveLength(1) - expect(aliceConvos[0].topic).toBe(aliceConvo1.topic) - }) + const aliceConvos = await alice.conversations.updateV2Conversations(); + expect(aliceConvos).toHaveLength(1); + expect(aliceConvos[0].topic).toBe(aliceConvo1.topic); + }); - it('sends multiple invites when different IDs are used', async () => { - const conversationId1 = 'xmtp.org/foo' - const conversationId2 = 'xmtp.org/bar' + it("sends multiple invites when different IDs are used", async () => { + const conversationId1 = "xmtp.org/foo"; + const conversationId2 = "xmtp.org/bar"; const aliceConvo1 = await alice.conversations.newConversation( bob.address, - { conversationId: conversationId1, metadata: {} } - ) + { conversationId: conversationId1, metadata: {} }, + ); const aliceConvo2 = await alice.conversations.newConversation( bob.address, - { conversationId: conversationId2, metadata: {} } - ) + { conversationId: conversationId2, metadata: {} }, + ); if ( !(aliceConvo1 instanceof ConversationV2) || !(aliceConvo2 instanceof ConversationV2) ) { - throw new Error('Not a V2 conversation') + throw new Error("Not a V2 conversation"); } - expect(aliceConvo1.topic === aliceConvo2.topic).toBeFalsy() - const aliceInvites = await alice.listInvitations() - expect(aliceInvites).toHaveLength(2) + expect(aliceConvo1.topic === aliceConvo2.topic).toBeFalsy(); + const aliceInvites = await alice.listInvitations(); + expect(aliceInvites).toHaveLength(2); - const bobInvites = await bob.listInvitations() - expect(bobInvites).toHaveLength(2) - }) + const bobInvites = await bob.listInvitations(); + expect(bobInvites).toHaveLength(2); + }); - it('handles races', async () => { + it("handles races", async () => { const ctx = { - conversationId: 'xmtp.org/foo', + conversationId: "xmtp.org/foo", metadata: {}, - } + }; // Create three conversations in parallel await Promise.all([ alice.conversations.newConversation(bob.address, ctx), alice.conversations.newConversation(bob.address, ctx), alice.conversations.newConversation(bob.address, ctx), - ]) - await sleep(50) - - const invites = await alice.listInvitations() - expect(invites).toHaveLength(1) - }) - }) -}) - -describe.sequential('Conversation streams', () => { - it('streams conversations', async () => { - const alice = await newLocalHostClient({ publishLegacyContact: true }) - const bob = await newLocalHostClient({ publishLegacyContact: true }) - const stream = await alice.conversations.stream() - const conversation = await alice.conversations.newConversation(bob.address) - await conversation.send('hi bob') - - let numConversations = 0 + ]); + await sleep(50); + + const invites = await alice.listInvitations(); + expect(invites).toHaveLength(1); + }); + }); +}); + +describe.sequential("Conversation streams", () => { + it("streams conversations", async () => { + const alice = await newLocalHostClient({ publishLegacyContact: true }); + const bob = await newLocalHostClient({ publishLegacyContact: true }); + const stream = await alice.conversations.stream(); + const conversation = await alice.conversations.newConversation(bob.address); + await conversation.send("hi bob"); + + let numConversations = 0; // eslint-disable-next-line no-unreachable-loop for await (const conversation of stream) { - numConversations++ - expect(conversation.peerAddress).toBe(bob.address) - break + numConversations++; + expect(conversation.peerAddress).toBe(bob.address); + break; } - expect(numConversations).toBe(1) - await stream.return() - await alice.close() - await bob.close() - }) - - it('streams all conversation messages from empty state', async () => { - const alice = await newLocalHostClient({ publishLegacyContact: true }) - const bob = await newLocalHostClient({ publishLegacyContact: true }) - const charlie = await newLocalHostClient({ publishLegacyContact: true }) + expect(numConversations).toBe(1); + await stream.return(); + await alice.close(); + await bob.close(); + }); + + it("streams all conversation messages from empty state", async () => { + const alice = await newLocalHostClient({ publishLegacyContact: true }); + const bob = await newLocalHostClient({ publishLegacyContact: true }); + const charlie = await newLocalHostClient({ publishLegacyContact: true }); const aliceCharlie = await alice.conversations.newConversation( - charlie.address - ) - const bobAlice = await bob.conversations.newConversation(alice.address) + charlie.address, + ); + const bobAlice = await bob.conversations.newConversation(alice.address); - const stream = alice.conversations.streamAllMessages() - const messages = [] + const stream = alice.conversations.streamAllMessages(); + const messages = []; setTimeout(async () => { - await aliceCharlie.send('gm alice -charlie') - await bobAlice.send('gm alice -bob') - await aliceCharlie.send('gm charlie -alice') - }, 100) + await aliceCharlie.send("gm alice -charlie"); + await bobAlice.send("gm alice -bob"); + await aliceCharlie.send("gm charlie -alice"); + }, 100); - let numMessages = 0 + let numMessages = 0; for await (const message of await stream) { - numMessages++ - messages.push(message) + numMessages++; + messages.push(message); if (numMessages === 3) { - break + break; } } - expect(messages[0].contentTopic).toBe(buildUserIntroTopic(alice.address)) - expect(messages[0].content).toBe('gm alice -charlie') - expect(messages[1].contentTopic).toBe(buildUserIntroTopic(alice.address)) - expect(messages[1].content).toBe('gm alice -bob') + expect(messages[0].contentTopic).toBe(buildUserIntroTopic(alice.address)); + expect(messages[0].content).toBe("gm alice -charlie"); + expect(messages[1].contentTopic).toBe(buildUserIntroTopic(alice.address)); + expect(messages[1].content).toBe("gm alice -bob"); expect(messages[2].contentTopic).toBe( - buildDirectMessageTopic(alice.address, charlie.address) - ) - expect(messages[2].content).toBe('gm charlie -alice') - expect(numMessages).toBe(3) - await (await stream).return(undefined) - await alice.close() - await bob.close() - await charlie.close() - }) - - it('streams all conversation messages with a mix of v1 and v2 conversations', async () => { - const alice = await newLocalHostClient({ publishLegacyContact: true }) - const bob = await newLocalHostClient({ publishLegacyContact: true }) - const aliceBobV1 = await alice.conversations.newConversation(bob.address) + buildDirectMessageTopic(alice.address, charlie.address), + ); + expect(messages[2].content).toBe("gm charlie -alice"); + expect(numMessages).toBe(3); + await (await stream).return(undefined); + await alice.close(); + await bob.close(); + await charlie.close(); + }); + + it("streams all conversation messages with a mix of v1 and v2 conversations", async () => { + const alice = await newLocalHostClient({ publishLegacyContact: true }); + const bob = await newLocalHostClient({ publishLegacyContact: true }); + const aliceBobV1 = await alice.conversations.newConversation(bob.address); const aliceBobV2 = await alice.conversations.newConversation(bob.address, { - conversationId: 'xmtp.org/foo', + conversationId: "xmtp.org/foo", metadata: {}, - }) - - const stream = await alice.conversations.streamAllMessages() - await sleep(50) - - await aliceBobV1.send('V1') - const message1 = await stream.next() - expect(message1.value.content).toBe('V1') - expect(message1.value.contentTopic).toBe(buildUserIntroTopic(alice.address)) - - await aliceBobV2.send('V2') - const message2 = await stream.next() - expect(message2.value.content).toBe('V2') - expect(message2.value.contentTopic).toBe(aliceBobV2.topic) - - await aliceBobV1.send('Second message in V1 channel') - const message3 = await stream.next() - expect(message3.value.content).toBe('Second message in V1 channel') + }); + + const stream = await alice.conversations.streamAllMessages(); + await sleep(50); + + await aliceBobV1.send("V1"); + const message1 = await stream.next(); + expect(message1.value.content).toBe("V1"); + expect(message1.value.contentTopic).toBe( + buildUserIntroTopic(alice.address), + ); + + await aliceBobV2.send("V2"); + const message2 = await stream.next(); + expect(message2.value.content).toBe("V2"); + expect(message2.value.contentTopic).toBe(aliceBobV2.topic); + + await aliceBobV1.send("Second message in V1 channel"); + const message3 = await stream.next(); + expect(message3.value.content).toBe("Second message in V1 channel"); expect(message3.value.contentTopic).toBe( - buildDirectMessageTopic(alice.address, bob.address) - ) + buildDirectMessageTopic(alice.address, bob.address), + ); const aliceBobV2Bar = await alice.conversations.newConversation( bob.address, { - conversationId: 'xmtp.org/bar', + conversationId: "xmtp.org/bar", metadata: {}, - } - ) - await aliceBobV2Bar.send('bar') - const message4 = await stream.next() - expect(message4.value.content).toBe('bar') - await stream.return(undefined) - await alice.close() - await bob.close() - }) - - it('handles a mix of streaming and listing conversations', async () => { - const alice = await newLocalHostClient({ publishLegacyContact: true }) - const bob = await newLocalHostClient({ publishLegacyContact: true }) + }, + ); + await aliceBobV2Bar.send("bar"); + const message4 = await stream.next(); + expect(message4.value.content).toBe("bar"); + await stream.return(undefined); + await alice.close(); + await bob.close(); + }); + + it("handles a mix of streaming and listing conversations", async () => { + const alice = await newLocalHostClient({ publishLegacyContact: true }); + const bob = await newLocalHostClient({ publishLegacyContact: true }); await bob.conversations.newConversation(alice.address, { - conversationId: 'xmtp.org/1', + conversationId: "xmtp.org/1", metadata: {}, - }) - const aliceStream = await alice.conversations.stream() - await sleep(50) + }); + const aliceStream = await alice.conversations.stream(); + await sleep(50); await bob.conversations.newConversation(alice.address, { - conversationId: 'xmtp.org/2', + conversationId: "xmtp.org/2", metadata: {}, - }) + }); // Ensure the result has been received - await aliceStream.next() + await aliceStream.next(); // Expect that even though a new conversation was found while streaming the first conversation is still returned - expect(await alice.conversations.list()).toHaveLength(2) - await aliceStream.return() + expect(await alice.conversations.list()).toHaveLength(2); + await aliceStream.return(); // Do it again to make sure the cache is updated with an existing timestamp await bob.conversations.newConversation(alice.address, { - conversationId: 'xmtp.org/3', + conversationId: "xmtp.org/3", metadata: {}, - }) - const aliceStream2 = await alice.conversations.stream() - await sleep(50) + }); + const aliceStream2 = await alice.conversations.stream(); + await sleep(50); await bob.conversations.newConversation(alice.address, { - conversationId: 'xmtp.org/4', + conversationId: "xmtp.org/4", metadata: {}, - }) - await aliceStream2.next() - - expect(await alice.conversations.list()).toHaveLength(4) - await aliceStream2.return() - await alice.close() - await bob.close() - }) -}) + }); + await aliceStream2.next(); + + expect(await alice.conversations.list()).toHaveLength(4); + await aliceStream2.return(); + await alice.close(); + await bob.close(); + }); +}); diff --git a/packages/js-sdk/test/conversations/JobRunner.test.ts b/packages/js-sdk/test/conversations/JobRunner.test.ts index a004cdd35..cde7a5a06 100644 --- a/packages/js-sdk/test/conversations/JobRunner.test.ts +++ b/packages/js-sdk/test/conversations/JobRunner.test.ts @@ -1,103 +1,105 @@ -import { keystore as keystoreProto } from '@xmtp/proto' -import JobRunner from '@/conversations/JobRunner' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import InMemoryKeystore from '@/keystore/InMemoryKeystore' -import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' -import type { KeystoreInterface } from '@/keystore/rpcDefinitions' -import { nsToDate } from '@/utils/date' -import { newWallet, sleep } from '@test/helpers' - -describe('JobRunner', () => { - let keystore: KeystoreInterface +import { keystore as keystoreProto } from "@xmtp/proto"; +import JobRunner from "@/conversations/JobRunner"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import InMemoryKeystore from "@/keystore/InMemoryKeystore"; +import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; +import type { KeystoreInterface } from "@/keystore/rpcDefinitions"; +import { nsToDate } from "@/utils/date"; +import { newWallet, sleep } from "@test/helpers"; + +describe("JobRunner", () => { + let keystore: KeystoreInterface; beforeEach(async () => { - const bundle = await PrivateKeyBundleV1.generate(newWallet()) + const bundle = await PrivateKeyBundleV1.generate(newWallet()); keystore = await InMemoryKeystore.create( bundle, - InMemoryPersistence.create() - ) - }) + InMemoryPersistence.create(), + ); + }); - it('can set the job time correctly', async () => { - const v1Runner = new JobRunner('v1', keystore) + it("can set the job time correctly", async () => { + const v1Runner = new JobRunner("v1", keystore); await v1Runner.run(async (lastRun) => { - expect(lastRun).toBeUndefined() - }) + expect(lastRun).toBeUndefined(); + }); const { lastRunNs } = await keystore.getRefreshJob({ jobType: keystoreProto.JobType.JOB_TYPE_REFRESH_V1, - }) + }); // We don't know the exact timestamp that the runner will set from outside, so just assume it was within a second of now expect(new Date().getTime() - nsToDate(lastRunNs).getTime()).toBeLessThan( - 1000 - ) - }) + 1000, + ); + }); - it('sets different values for v1 and v2 runners', async () => { - const v1Runner = new JobRunner('v1', keystore) - const v2Runner = new JobRunner('v2', keystore) + it("sets different values for v1 and v2 runners", async () => { + const v1Runner = new JobRunner("v1", keystore); + const v2Runner = new JobRunner("v2", keystore); - await v1Runner.run(async () => {}) + await v1Runner.run(async () => {}); // Ensure they have different timestamps - await sleep(10) - await v2Runner.run(async () => {}) + await sleep(10); + await v2Runner.run(async () => {}); const { lastRunNs: v1LastRunNs } = await keystore.getRefreshJob({ jobType: keystoreProto.JobType.JOB_TYPE_REFRESH_V1, - }) + }); const { lastRunNs: v2LastRunNs } = await keystore.getRefreshJob({ jobType: keystoreProto.JobType.JOB_TYPE_REFRESH_V2, - }) + }); - expect(v1LastRunNs.gt(0)).toBeTruthy() - expect(v2LastRunNs.gt(0)).toBeTruthy() - expect(v1LastRunNs.eq(v2LastRunNs)).toBe(false) - }) + expect(v1LastRunNs.gt(0)).toBeTruthy(); + expect(v2LastRunNs.gt(0)).toBeTruthy(); + expect(v1LastRunNs.eq(v2LastRunNs)).toBe(false); + }); - it('fails with an invalid job type', async () => { + it("fails with an invalid job type", async () => { // @ts-expect-error test case - const v3Runner = new JobRunner('v3', keystore) - expect(v3Runner.run(async () => {})).rejects.toThrow('unknown job type: v3') - }) + const v3Runner = new JobRunner("v3", keystore); + expect(v3Runner.run(async () => {})).rejects.toThrow( + "unknown job type: v3", + ); + }); - it('returns the value from the callback', async () => { - const v1Runner = new JobRunner('v1', keystore) + it("returns the value from the callback", async () => { + const v1Runner = new JobRunner("v1", keystore); const result = await v1Runner.run(async () => { - return 'foo' - }) - expect(result).toBe('foo') - }) + return "foo"; + }); + expect(result).toBe("foo"); + }); - it('bubbles up errors from the callback', async () => { - const v1Runner = new JobRunner('v1', keystore) + it("bubbles up errors from the callback", async () => { + const v1Runner = new JobRunner("v1", keystore); await expect( v1Runner.run(async () => { - throw new Error('foo') - }) - ).rejects.toThrow('foo') - }) + throw new Error("foo"); + }), + ).rejects.toThrow("foo"); + }); - it('resets the last run time', async () => { - const userPreferencesRunner = new JobRunner('user-preferences', keystore) - await userPreferencesRunner.run(async () => {}) + it("resets the last run time", async () => { + const userPreferencesRunner = new JobRunner("user-preferences", keystore); + await userPreferencesRunner.run(async () => {}); const { lastRunNs: userPreferencesLastRunNs } = await keystore.getRefreshJob({ jobType: keystoreProto.JobType.JOB_TYPE_REFRESH_PPPP, - }) + }); - expect(userPreferencesLastRunNs.gt(0)).toBeTruthy() + expect(userPreferencesLastRunNs.gt(0)).toBeTruthy(); - await userPreferencesRunner.resetLastRunTime() + await userPreferencesRunner.resetLastRunTime(); const { lastRunNs: userPreferencesLastRunNs2 } = await keystore.getRefreshJob({ jobType: keystoreProto.JobType.JOB_TYPE_REFRESH_PPPP, - }) + }); - expect(userPreferencesLastRunNs2.eq(0)).toBeTruthy() - }) -}) + expect(userPreferencesLastRunNs2.eq(0)).toBeTruthy(); + }); +}); diff --git a/packages/js-sdk/test/crypto/PrivateKeyBundle.test.ts b/packages/js-sdk/test/crypto/PrivateKeyBundle.test.ts index 69398158b..d073a6bde 100644 --- a/packages/js-sdk/test/crypto/PrivateKeyBundle.test.ts +++ b/packages/js-sdk/test/crypto/PrivateKeyBundle.test.ts @@ -1,109 +1,109 @@ -import { hexToBytes } from 'viem' -import { PrivateKey } from '@/crypto/PrivateKey' +import { hexToBytes } from "viem"; +import { PrivateKey } from "@/crypto/PrivateKey"; import { decodePrivateKeyBundle, PrivateKeyBundleV1, PrivateKeyBundleV2, -} from '@/crypto/PrivateKeyBundle' -import { SignedPublicKeyBundle } from '@/crypto/PublicKeyBundle' -import { storageSigRequestText } from '@/keystore/providers/NetworkKeyManager' -import { newWallet } from '@test/helpers' +} from "@/crypto/PrivateKeyBundle"; +import { SignedPublicKeyBundle } from "@/crypto/PublicKeyBundle"; +import { storageSigRequestText } from "@/keystore/providers/NetworkKeyManager"; +import { newWallet } from "@test/helpers"; -describe('Crypto', function () { - describe('PrivateKeyBundle', function () { - it('v2 generate/encode/decode', async function () { - const wallet = newWallet() +describe("Crypto", function () { + describe("PrivateKeyBundle", function () { + it("v2 generate/encode/decode", async function () { + const wallet = newWallet(); // generate key bundle - const bundle = await PrivateKeyBundleV2.generate(wallet) - const bytes = bundle.encode() - const bundle2 = decodePrivateKeyBundle(bytes) - expect(bundle2).toBeInstanceOf(PrivateKeyBundleV2) - expect(bundle2.version).toBe(2) - expect(bundle.equals(bundle2 as PrivateKeyBundleV2)) + const bundle = await PrivateKeyBundleV2.generate(wallet); + const bytes = bundle.encode(); + const bundle2 = decodePrivateKeyBundle(bytes); + expect(bundle2).toBeInstanceOf(PrivateKeyBundleV2); + expect(bundle2.version).toBe(2); + expect(bundle.equals(bundle2 as PrivateKeyBundleV2)); expect( bundle .getPublicKeyBundle() - .equals((bundle2 as PrivateKeyBundleV2).getPublicKeyBundle()) - ) - }) + .equals((bundle2 as PrivateKeyBundleV2).getPublicKeyBundle()), + ); + }); - it('human-friendly storage signature request text', async function () { + it("human-friendly storage signature request text", async function () { const pri = PrivateKey.fromBytes( hexToBytes( - '0x08aaa9dad3ed2f12220a206fd789a6ee2376bb6595b4ebace57c7a79e6e4f1f12c8416d611399eda6c74cb1a4c08aaa9dad3ed2f1a430a4104e208133ea0973a9968fe5362e5ac0a8bbbe2aa16d796add31f3d027a1b894389873d7f282163bceb1fc3ca60d589d1e667956c40fed4cdaa7edc1392d2100b8a' - ) - ) - expect(pri.secp256k1).toBeTruthy() - const wallet = newWallet() - const _bundle = await PrivateKeyBundleV1.generate(wallet) + "0x08aaa9dad3ed2f12220a206fd789a6ee2376bb6595b4ebace57c7a79e6e4f1f12c8416d611399eda6c74cb1a4c08aaa9dad3ed2f1a430a4104e208133ea0973a9968fe5362e5ac0a8bbbe2aa16d796add31f3d027a1b894389873d7f282163bceb1fc3ca60d589d1e667956c40fed4cdaa7edc1392d2100b8a", + ), + ); + expect(pri.secp256k1).toBeTruthy(); + const wallet = newWallet(); + const _bundle = await PrivateKeyBundleV1.generate(wallet); const preKey = hexToBytes( - '0xf51bd1da9ec2239723ae2cf6a9f8d0ac37546b27e634002c653d23bacfcc67ad' - ) - const actual = storageSigRequestText(preKey) + "0xf51bd1da9ec2239723ae2cf6a9f8d0ac37546b27e634002c653d23bacfcc67ad", + ); + const actual = storageSigRequestText(preKey); const expected = - 'XMTP : Enable Identity\nf51bd1da9ec2239723ae2cf6a9f8d0ac37546b27e634002c653d23bacfcc67ad\n\nFor more info: https://xmtp.org/signatures/' - expect(actual).toEqual(expected) - }) + "XMTP : Enable Identity\nf51bd1da9ec2239723ae2cf6a9f8d0ac37546b27e634002c653d23bacfcc67ad\n\nFor more info: https://xmtp.org/signatures/"; + expect(actual).toEqual(expected); + }); - it('validates true for valid keys', async () => { - const wallet = newWallet() - const bundle = await PrivateKeyBundleV1.generate(wallet) - expect(bundle.validatePublicKeys()).toBe(true) - }) + it("validates true for valid keys", async () => { + const wallet = newWallet(); + const bundle = await PrivateKeyBundleV1.generate(wallet); + expect(bundle.validatePublicKeys()).toBe(true); + }); - it('fails validation when private key does not match public key', async () => { - const wallet = newWallet() - const bundle = await PrivateKeyBundleV1.generate(wallet) - const otherBundle = await PrivateKeyBundleV1.generate(newWallet()) - bundle.preKeys[0].publicKey = otherBundle.preKeys[0].publicKey - expect(bundle.validatePublicKeys()).toBe(false) - }) - }) + it("fails validation when private key does not match public key", async () => { + const wallet = newWallet(); + const bundle = await PrivateKeyBundleV1.generate(wallet); + const otherBundle = await PrivateKeyBundleV1.generate(newWallet()); + bundle.preKeys[0].publicKey = otherBundle.preKeys[0].publicKey; + expect(bundle.validatePublicKeys()).toBe(false); + }); + }); - describe('PrivateKey', () => { - it('validates true for valid keys', async () => { - const wallet = newWallet() - const bundle = await PrivateKeyBundleV1.generate(wallet) - expect(bundle.identityKey.validatePublicKey()).toBe(true) - }) + describe("PrivateKey", () => { + it("validates true for valid keys", async () => { + const wallet = newWallet(); + const bundle = await PrivateKeyBundleV1.generate(wallet); + expect(bundle.identityKey.validatePublicKey()).toBe(true); + }); - it('fails validation when private key does not match public key', async () => { - const wallet = newWallet() - const bundle = await PrivateKeyBundleV1.generate(wallet) - const otherBundle = await PrivateKeyBundleV1.generate(newWallet()) - bundle.identityKey.publicKey = otherBundle.identityKey.publicKey - expect(bundle.identityKey.validatePublicKey()).toBe(false) - }) - }) + it("fails validation when private key does not match public key", async () => { + const wallet = newWallet(); + const bundle = await PrivateKeyBundleV1.generate(wallet); + const otherBundle = await PrivateKeyBundleV1.generate(newWallet()); + bundle.identityKey.publicKey = otherBundle.identityKey.publicKey; + expect(bundle.identityKey.validatePublicKey()).toBe(false); + }); + }); - describe('SignedPrivateKey', () => { - it('validates true for valid keys', async () => { - const wallet = newWallet() - const bundle = await PrivateKeyBundleV2.generate(wallet) - expect(bundle.identityKey.validatePublicKey()).toBe(true) - }) + describe("SignedPrivateKey", () => { + it("validates true for valid keys", async () => { + const wallet = newWallet(); + const bundle = await PrivateKeyBundleV2.generate(wallet); + expect(bundle.identityKey.validatePublicKey()).toBe(true); + }); - it('fails validation when private key does not match public key', async () => { - const wallet = newWallet() - const bundle = await PrivateKeyBundleV2.generate(wallet) - const otherBundle = await PrivateKeyBundleV2.generate(newWallet()) - bundle.identityKey.publicKey = otherBundle.identityKey.publicKey - expect(bundle.identityKey.validatePublicKey()).toBe(false) - }) - }) + it("fails validation when private key does not match public key", async () => { + const wallet = newWallet(); + const bundle = await PrivateKeyBundleV2.generate(wallet); + const otherBundle = await PrivateKeyBundleV2.generate(newWallet()); + bundle.identityKey.publicKey = otherBundle.identityKey.publicKey; + expect(bundle.identityKey.validatePublicKey()).toBe(false); + }); + }); - describe('SignedPublicKeyBundle', () => { - it('legacy roundtrip', async function () { - const wallet = newWallet() - const pri = await PrivateKeyBundleV1.generate(wallet) + describe("SignedPublicKeyBundle", () => { + it("legacy roundtrip", async function () { + const wallet = newWallet(); + const pri = await PrivateKeyBundleV1.generate(wallet); const pub = SignedPublicKeyBundle.fromLegacyBundle( - pri.getPublicKeyBundle() - ) - expect(pub.isFromLegacyBundle()).toBeTruthy() - const leg = pub.toLegacyBundle() - const pub2 = SignedPublicKeyBundle.fromLegacyBundle(leg) - expect(pub.equals(pub2)).toBeTruthy() - expect(pub2.identityKey.verifyKey(pub2.preKey)).toBeTruthy() - }) - }) -}) + pri.getPublicKeyBundle(), + ); + expect(pub.isFromLegacyBundle()).toBeTruthy(); + const leg = pub.toLegacyBundle(); + const pub2 = SignedPublicKeyBundle.fromLegacyBundle(leg); + expect(pub.equals(pub2)).toBeTruthy(); + expect(pub2.identityKey.verifyKey(pub2.preKey)).toBeTruthy(); + }); + }); +}); diff --git a/packages/js-sdk/test/crypto/PublicKey.test.ts b/packages/js-sdk/test/crypto/PublicKey.test.ts index 13a599048..58ebab7a4 100644 --- a/packages/js-sdk/test/crypto/PublicKey.test.ts +++ b/packages/js-sdk/test/crypto/PublicKey.test.ts @@ -1,117 +1,117 @@ -import { Wallet } from 'ethers' -import Long from 'long' -import { hexToBytes } from 'viem' -import { PrivateKey, SignedPrivateKey } from '@/crypto/PrivateKey' -import { PublicKey, SignedPublicKey } from '@/crypto/PublicKey' -import Signature, { WalletSigner } from '@/crypto/Signature' -import { equalBytes } from '@/crypto/utils' -import { newWallet } from '@test/helpers' +import { Wallet } from "ethers"; +import Long from "long"; +import { hexToBytes } from "viem"; +import { PrivateKey, SignedPrivateKey } from "@/crypto/PrivateKey"; +import { PublicKey, SignedPublicKey } from "@/crypto/PublicKey"; +import Signature, { WalletSigner } from "@/crypto/Signature"; +import { equalBytes } from "@/crypto/utils"; +import { newWallet } from "@test/helpers"; -describe('Crypto', function () { - describe('Signed Keys', function () { - it('generate, verify, encode, decode', async function () { - const wallet = newWallet() - const keySigner = new WalletSigner(wallet) - const idPri = await SignedPrivateKey.generate(keySigner) - const idPub = idPri.publicKey - const prePri = await SignedPrivateKey.generate(idPri) - const prePub = prePri.publicKey - expect(idPub.verifyKey(prePub)).toBeTruthy() - let signer = await idPub.signerKey() - expect(signer).toBeTruthy() - expect(wallet.address).toEqual(signer!.getEthereumAddress()) - signer = await prePub.signerKey() - expect(signer).toBeTruthy() - expect(idPub.getEthereumAddress()).toEqual(signer!.getEthereumAddress()) - let bytes = idPub.toBytes() - const idPub2 = SignedPublicKey.fromBytes(bytes) - expect(idPub.equals(idPub2)).toBeTruthy() - bytes = idPri.toBytes() - const idPri2 = SignedPrivateKey.fromBytes(bytes) - expect(idPri.equals(idPri2)).toBeTruthy() - }) - it('legacy conversation fails for ns creation timestamps', async function () { - const wallet = newWallet() - const keySigner = new WalletSigner(wallet) - const idPri = await SignedPrivateKey.generate(keySigner) - expect(idPri.publicKey.isFromLegacyKey()).toBeFalsy() +describe("Crypto", function () { + describe("Signed Keys", function () { + it("generate, verify, encode, decode", async function () { + const wallet = newWallet(); + const keySigner = new WalletSigner(wallet); + const idPri = await SignedPrivateKey.generate(keySigner); + const idPub = idPri.publicKey; + const prePri = await SignedPrivateKey.generate(idPri); + const prePub = prePri.publicKey; + expect(idPub.verifyKey(prePub)).toBeTruthy(); + let signer = await idPub.signerKey(); + expect(signer).toBeTruthy(); + expect(wallet.address).toEqual(signer!.getEthereumAddress()); + signer = await prePub.signerKey(); + expect(signer).toBeTruthy(); + expect(idPub.getEthereumAddress()).toEqual(signer!.getEthereumAddress()); + let bytes = idPub.toBytes(); + const idPub2 = SignedPublicKey.fromBytes(bytes); + expect(idPub.equals(idPub2)).toBeTruthy(); + bytes = idPri.toBytes(); + const idPri2 = SignedPrivateKey.fromBytes(bytes); + expect(idPri.equals(idPri2)).toBeTruthy(); + }); + it("legacy conversation fails for ns creation timestamps", async function () { + const wallet = newWallet(); + const keySigner = new WalletSigner(wallet); + const idPri = await SignedPrivateKey.generate(keySigner); + expect(idPri.publicKey.isFromLegacyKey()).toBeFalsy(); expect(() => idPri.publicKey.toLegacyKey()).toThrow( - 'cannot be converted to legacy key' - ) - }) - it('public key legacy roundtrip', async function () { - const wallet = newWallet() - const idPri = PrivateKey.generate() - await idPri.publicKey.signWithWallet(wallet) - const idPub = SignedPublicKey.fromLegacyKey(idPri.publicKey, true) - expect(idPub.isFromLegacyKey()).toBeTruthy() - const idPubLeg = idPub.toLegacyKey() - const idPub2 = SignedPublicKey.fromLegacyKey(idPubLeg, true) - expect(idPub.equals(idPub2)).toBeTruthy() + "cannot be converted to legacy key", + ); + }); + it("public key legacy roundtrip", async function () { + const wallet = newWallet(); + const idPri = PrivateKey.generate(); + await idPri.publicKey.signWithWallet(wallet); + const idPub = SignedPublicKey.fromLegacyKey(idPri.publicKey, true); + expect(idPub.isFromLegacyKey()).toBeTruthy(); + const idPubLeg = idPub.toLegacyKey(); + const idPub2 = SignedPublicKey.fromLegacyKey(idPubLeg, true); + expect(idPub.equals(idPub2)).toBeTruthy(); - const prePri = PrivateKey.generate() - await idPri.signKey(prePri.publicKey) - const prePub = SignedPublicKey.fromLegacyKey(prePri.publicKey, false) - expect(prePub.isFromLegacyKey()).toBeTruthy() - const prePubLeg = prePub.toLegacyKey() - const prePub2 = SignedPublicKey.fromLegacyKey(prePubLeg, false) - expect(prePub.equals(prePub2)).toBeTruthy() - expect(idPub2.verifyKey(prePub2)).toBeTruthy() - }) - }) - describe('PublicKey', function () { - it('derives address from public key', function () { + const prePri = PrivateKey.generate(); + await idPri.signKey(prePri.publicKey); + const prePub = SignedPublicKey.fromLegacyKey(prePri.publicKey, false); + expect(prePub.isFromLegacyKey()).toBeTruthy(); + const prePubLeg = prePub.toLegacyKey(); + const prePub2 = SignedPublicKey.fromLegacyKey(prePubLeg, false); + expect(prePub.equals(prePub2)).toBeTruthy(); + expect(idPub2.verifyKey(prePub2)).toBeTruthy(); + }); + }); + describe("PublicKey", function () { + it("derives address from public key", function () { // using the sample from https://kobl.one/blog/create-full-ethereum-keypair-and-address/ const bytes = hexToBytes( - '0x04836b35a026743e823a90a0ee3b91bf615c6a757e2b60b9e1dc1826fd0dd16106f7bc1e8179f665015f43c6c81f39062fc2086ed849625c06e04697698b21855e' - ) + "0x04836b35a026743e823a90a0ee3b91bf615c6a757e2b60b9e1dc1826fd0dd16106f7bc1e8179f665015f43c6c81f39062fc2086ed849625c06e04697698b21855e", + ); const pub = new PublicKey({ secp256k1Uncompressed: { bytes }, timestamp: Long.fromNumber(new Date().getTime()), - }) - const address = pub.getEthereumAddress() - expect(address).toEqual('0x0BED7ABd61247635c1973eB38474A2516eD1D884') - }) + }); + const address = pub.getEthereumAddress(); + expect(address).toEqual("0x0BED7ABd61247635c1973eB38474A2516eD1D884"); + }); - it('human-friendly identity key signature request', async function () { + it("human-friendly identity key signature request", async function () { const alice = PrivateKey.fromBytes( hexToBytes( - '0x08aaa9dad3ed2f12220a206fd789a6ee2376bb6595b4ebace57c7a79e6e4f1f12c8416d611399eda6c74cb1a4c08aaa9dad3ed2f1a430a4104e208133ea0973a9968fe5362e5ac0a8bbbe2aa16d796add31f3d027a1b894389873d7f282163bceb1fc3ca60d589d1e667956c40fed4cdaa7edc1392d2100b8a' - ) - ) + "0x08aaa9dad3ed2f12220a206fd789a6ee2376bb6595b4ebace57c7a79e6e4f1f12c8416d611399eda6c74cb1a4c08aaa9dad3ed2f1a430a4104e208133ea0973a9968fe5362e5ac0a8bbbe2aa16d796add31f3d027a1b894389873d7f282163bceb1fc3ca60d589d1e667956c40fed4cdaa7edc1392d2100b8a", + ), + ); const actual = WalletSigner.identitySigRequestText( - alice.publicKey.bytesToSign() - ) + alice.publicKey.bytesToSign(), + ); const expected = - 'XMTP : Create Identity\n08aaa9dad3ed2f1a430a4104e208133ea0973a9968fe5362e5ac0a8bbbe2aa16d796add31f3d027a1b894389873d7f282163bceb1fc3ca60d589d1e667956c40fed4cdaa7edc1392d2100b8a\n\nFor more info: https://xmtp.org/signatures/' - expect(actual).toEqual(expected) - }) + "XMTP : Create Identity\n08aaa9dad3ed2f1a430a4104e208133ea0973a9968fe5362e5ac0a8bbbe2aa16d796add31f3d027a1b894389873d7f282163bceb1fc3ca60d589d1e667956c40fed4cdaa7edc1392d2100b8a\n\nFor more info: https://xmtp.org/signatures/"; + expect(actual).toEqual(expected); + }); - it('signs keys using a wallet', async function () { + it("signs keys using a wallet", async function () { // create a wallet using a generated key - const alice = PrivateKey.generate() - expect(alice.secp256k1).toBeTruthy() - const wallet = new Wallet(alice.secp256k1.bytes) + const alice = PrivateKey.generate(); + expect(alice.secp256k1).toBeTruthy(); + const wallet = new Wallet(alice.secp256k1.bytes); // sanity check that we agree with the wallet about the address - expect(wallet.address).toEqual(alice.publicKey.getEthereumAddress()) + expect(wallet.address).toEqual(alice.publicKey.getEthereumAddress()); // sign the public key using the wallet - await alice.publicKey.signWithWallet(wallet) - expect(alice.publicKey.signature).toBeTruthy() + await alice.publicKey.signWithWallet(wallet); + expect(alice.publicKey.signature).toBeTruthy(); // validate the key signature and return wallet address - const address = alice.publicKey.walletSignatureAddress() - expect(address).toEqual(wallet.address) - }) + const address = alice.publicKey.walletSignatureAddress(); + expect(address).toEqual(wallet.address); + }); - it('derives address from public key with malformed v1 signature', async function () { + it("derives address from public key with malformed v1 signature", async function () { // create a wallet using a generated key - const alice = PrivateKey.generate() - expect(alice.secp256k1).toBeTruthy() - const wallet = new Wallet(alice.secp256k1.bytes) + const alice = PrivateKey.generate(); + expect(alice.secp256k1).toBeTruthy(); + const wallet = new Wallet(alice.secp256k1.bytes); // sanity check that we agree with the wallet about the address - expect(wallet.address).toEqual(alice.publicKey.getEthereumAddress()) + expect(wallet.address).toEqual(alice.publicKey.getEthereumAddress()); // sign the public key using the wallet - await alice.publicKey.signWithWallet(wallet) - expect(alice.publicKey.signature?.ecdsaCompact).toBeTruthy() + await alice.publicKey.signWithWallet(wallet); + expect(alice.publicKey.signature?.ecdsaCompact).toBeTruthy(); // distort the v1 signature to only have a walletEcdsaCompact signature alice.publicKey.signature = new Signature({ @@ -119,48 +119,48 @@ describe('Crypto', function () { bytes: alice.publicKey.signature!.ecdsaCompact!.bytes, recovery: alice.publicKey.signature!.ecdsaCompact!.recovery, }, - }) + }); // create a new public key with the malformed signature - const publicKey = new PublicKey(alice.publicKey) + const publicKey = new PublicKey(alice.publicKey); // validate the key signature and return wallet address - expect(publicKey.signature?.ecdsaCompact).toBeTruthy() - const address = publicKey.walletSignatureAddress() - expect(address).toEqual(wallet.address) - }) + expect(publicKey.signature?.ecdsaCompact).toBeTruthy(); + const address = publicKey.walletSignatureAddress(); + expect(address).toEqual(wallet.address); + }); - it('converts legacy keys to new keys', async function () { + it("converts legacy keys to new keys", async function () { // Key signed by a wallet - const wallet = newWallet() - const identityKey = PrivateKey.generate() - await identityKey.publicKey.signWithWallet(wallet) - const iPub = identityKey.publicKey - expect(iPub.walletSignatureAddress(), wallet.address) - const iPub2 = SignedPublicKey.fromLegacyKey(iPub, true) + const wallet = newWallet(); + const identityKey = PrivateKey.generate(); + await identityKey.publicKey.signWithWallet(wallet); + const iPub = identityKey.publicKey; + expect(iPub.walletSignatureAddress(), wallet.address); + const iPub2 = SignedPublicKey.fromLegacyKey(iPub, true); expect( equalBytes( iPub2.secp256k1Uncompressed.bytes, - iPub.secp256k1Uncompressed.bytes - ) - ).toBe(true) - expect(iPub2.generated).toEqual(iPub.generated) - expect(equalBytes(iPub2.keyBytes, iPub.bytesToSign())).toBeTruthy() - const address = await iPub2.walletSignatureAddress() - expect(address).toEqual(wallet.address) + iPub.secp256k1Uncompressed.bytes, + ), + ).toBe(true); + expect(iPub2.generated).toEqual(iPub.generated); + expect(equalBytes(iPub2.keyBytes, iPub.bytesToSign())).toBeTruthy(); + const address = await iPub2.walletSignatureAddress(); + expect(address).toEqual(wallet.address); // Key signed by a key - const preKey = PrivateKey.generate() - await identityKey.signKey(preKey.publicKey) - const pPub = preKey.publicKey - const pPub2 = SignedPublicKey.fromLegacyKey(pPub) + const preKey = PrivateKey.generate(); + await identityKey.signKey(preKey.publicKey); + const pPub = preKey.publicKey; + const pPub2 = SignedPublicKey.fromLegacyKey(pPub); expect( equalBytes( pPub2.secp256k1Uncompressed.bytes, - pPub.secp256k1Uncompressed.bytes - ) - ).toBe(true) - expect(pPub2.generated).toEqual(pPub.generated) - expect(equalBytes(pPub2.keyBytes, pPub.bytesToSign())).toBeTruthy() - expect(iPub2.verifyKey(pPub2)).toBeTruthy() - }) - }) -}) + pPub.secp256k1Uncompressed.bytes, + ), + ).toBe(true); + expect(pPub2.generated).toEqual(pPub.generated); + expect(equalBytes(pPub2.keyBytes, pPub.bytesToSign())).toBeTruthy(); + expect(iPub2.verifyKey(pPub2)).toBeTruthy(); + }); + }); +}); diff --git a/packages/js-sdk/test/crypto/Signature.test.ts b/packages/js-sdk/test/crypto/Signature.test.ts index c8c0793fe..c5e43b175 100644 --- a/packages/js-sdk/test/crypto/Signature.test.ts +++ b/packages/js-sdk/test/crypto/Signature.test.ts @@ -1,41 +1,41 @@ -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import Signature from '@/crypto/Signature' -import { newWallet } from '@test/helpers' +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import Signature from "@/crypto/Signature"; +import { newWallet } from "@test/helpers"; -describe('Crypto', function () { - describe('Signature', function () { - it('transplanting a wallet signature changes the derived wallet address', async function () { - const alice = newWallet() - const alicePri = await PrivateKeyBundleV1.generate(alice) - const alicePub = alicePri.getPublicKeyBundle() +describe("Crypto", function () { + describe("Signature", function () { + it("transplanting a wallet signature changes the derived wallet address", async function () { + const alice = newWallet(); + const alicePri = await PrivateKeyBundleV1.generate(alice); + const alicePub = alicePri.getPublicKeyBundle(); expect(alicePub.identityKey.walletSignatureAddress()).toEqual( - alice.address - ) - const malory = newWallet() - expect(alice.address).not.toEqual(malory.address) - const maloryPri = await PrivateKeyBundleV1.generate(malory) - const maloryPub = maloryPri.getPublicKeyBundle() + alice.address, + ); + const malory = newWallet(); + expect(alice.address).not.toEqual(malory.address); + const maloryPri = await PrivateKeyBundleV1.generate(malory); + const maloryPub = maloryPri.getPublicKeyBundle(); expect(maloryPub.identityKey.walletSignatureAddress()).toEqual( - malory.address - ) + malory.address, + ); // malory transplants alice's wallet sig onto her own key bundle - maloryPub.identityKey.signature = alicePub.identityKey.signature + maloryPub.identityKey.signature = alicePub.identityKey.signature; expect(maloryPub.identityKey.walletSignatureAddress()).not.toEqual( - alice.address - ) + alice.address, + ); expect(maloryPub.identityKey.walletSignatureAddress()).not.toEqual( - malory.address - ) - }) + malory.address, + ); + }); - it('returns wallet address for either ecdsaCompact or walletEcdsaCompact signatures', async function () { - const alice = newWallet() - const alicePri = await PrivateKeyBundleV1.generate(alice) - const alicePub = alicePri.getPublicKeyBundle() - expect(alicePub.identityKey.signature?.ecdsaCompact).toBeTruthy() + it("returns wallet address for either ecdsaCompact or walletEcdsaCompact signatures", async function () { + const alice = newWallet(); + const alicePri = await PrivateKeyBundleV1.generate(alice); + const alicePub = alicePri.getPublicKeyBundle(); + expect(alicePub.identityKey.signature?.ecdsaCompact).toBeTruthy(); expect(alicePub.identityKey.walletSignatureAddress()).toEqual( - alice.address - ) + alice.address, + ); // create a malformed v1 signature alicePub.identityKey.signature = new Signature({ @@ -43,12 +43,12 @@ describe('Crypto', function () { bytes: alicePub.identityKey.signature!.ecdsaCompact!.bytes, recovery: alicePub.identityKey.signature!.ecdsaCompact!.recovery, }, - }) - expect(alicePub.identityKey.signature.walletEcdsaCompact).toBeTruthy() - expect(alicePub.identityKey.signature.ecdsaCompact).toEqual(undefined) + }); + expect(alicePub.identityKey.signature.walletEcdsaCompact).toBeTruthy(); + expect(alicePub.identityKey.signature.ecdsaCompact).toEqual(undefined); expect(alicePub.identityKey.walletSignatureAddress()).toEqual( - alice.address - ) - }) - }) -}) + alice.address, + ); + }); + }); +}); diff --git a/packages/js-sdk/test/crypto/SignedEciesCiphertext.test.ts b/packages/js-sdk/test/crypto/SignedEciesCiphertext.test.ts index bbc74417b..0ddd84be5 100644 --- a/packages/js-sdk/test/crypto/SignedEciesCiphertext.test.ts +++ b/packages/js-sdk/test/crypto/SignedEciesCiphertext.test.ts @@ -1,110 +1,110 @@ -import crypto from '@/crypto/crypto' -import { encrypt, getPublic } from '@/crypto/ecies' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import SignedEciesCiphertext from '@/crypto/SignedEciesCiphertext' -import { equalBytes } from '@/crypto/utils' -import { newWallet } from '@test/helpers' - -describe('SignedEciesCiphertext', () => { - let bundle: PrivateKeyBundleV1 +import crypto from "@/crypto/crypto"; +import { encrypt, getPublic } from "@/crypto/ecies"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import SignedEciesCiphertext from "@/crypto/SignedEciesCiphertext"; +import { equalBytes } from "@/crypto/utils"; +import { newWallet } from "@test/helpers"; + +describe("SignedEciesCiphertext", () => { + let bundle: PrivateKeyBundleV1; beforeEach(async () => { - bundle = await PrivateKeyBundleV1.generate(newWallet()) - }) + bundle = await PrivateKeyBundleV1.generate(newWallet()); + }); - it('round trips successfully', async () => { - const rawData = new TextEncoder().encode('hi') + it("round trips successfully", async () => { + const rawData = new TextEncoder().encode("hi"); const encrypted = await encrypt( getPublic(Buffer.from(bundle.identityKey.secp256k1.bytes)), - Buffer.from(rawData) - ) + Buffer.from(rawData), + ); const signedEcies = await SignedEciesCiphertext.create( encrypted, - bundle.identityKey - ) + bundle.identityKey, + ); - expect(signedEcies.signature).toBeDefined() - expect(signedEcies.ciphertext.mac).toHaveLength(32) - expect(signedEcies.ciphertext.iv).toHaveLength(16) - expect(signedEcies.ciphertext.ephemeralPublicKey).toHaveLength(65) + expect(signedEcies.signature).toBeDefined(); + expect(signedEcies.ciphertext.mac).toHaveLength(32); + expect(signedEcies.ciphertext.iv).toHaveLength(16); + expect(signedEcies.ciphertext.ephemeralPublicKey).toHaveLength(65); - const asBytes = signedEcies.toBytes() - expect(asBytes).toBeInstanceOf(Uint8Array) + const asBytes = signedEcies.toBytes(); + expect(asBytes).toBeInstanceOf(Uint8Array); - const fromBytes = await SignedEciesCiphertext.fromBytes(asBytes) + const fromBytes = await SignedEciesCiphertext.fromBytes(asBytes); expect(fromBytes.ciphertext.ciphertext).toEqual( - signedEcies.ciphertext.ciphertext - ) + signedEcies.ciphertext.ciphertext, + ); expect( equalBytes( fromBytes.signature.ecdsaCompact!.bytes, - signedEcies.signature.ecdsaCompact!.bytes - ) - ).toBeTruthy() + signedEcies.signature.ecdsaCompact!.bytes, + ), + ).toBeTruthy(); const verificationResult = await fromBytes.verify( - bundle.identityKey.publicKey - ) - expect(verificationResult).toBe(true) - }) + bundle.identityKey.publicKey, + ); + expect(verificationResult).toBe(true); + }); - it('rejects malformed inputs', async () => { - const rawData = new TextEncoder().encode('hello world') + it("rejects malformed inputs", async () => { + const rawData = new TextEncoder().encode("hello world"); const goodInput = await encrypt( getPublic(Buffer.from(bundle.identityKey.secp256k1.bytes)), - Buffer.from(rawData) - ) + Buffer.from(rawData), + ); - const badInput = crypto.getRandomValues(new Uint8Array(11)) + const badInput = crypto.getRandomValues(new Uint8Array(11)); expect( SignedEciesCiphertext.create( { ...goodInput, iv: badInput }, - bundle.identityKey - ) - ).rejects.toThrow('Invalid iv length') + bundle.identityKey, + ), + ).rejects.toThrow("Invalid iv length"); expect( SignedEciesCiphertext.create( { ...goodInput, ciphertext: badInput }, - bundle.identityKey - ) - ).rejects.toThrow('Invalid ciphertext length') + bundle.identityKey, + ), + ).rejects.toThrow("Invalid ciphertext length"); expect( SignedEciesCiphertext.create( { ...goodInput, mac: badInput }, - bundle.identityKey - ) - ).rejects.toThrow('Invalid mac length') + bundle.identityKey, + ), + ).rejects.toThrow("Invalid mac length"); expect( SignedEciesCiphertext.create( { ...goodInput, ephemeralPublicKey: badInput }, - bundle.identityKey - ) - ).rejects.toThrow('Invalid ephemPublicKey length') - }) - - it('rejects incorrect signatures', async () => { - const rawData = new TextEncoder().encode('hi') - const stranger = await PrivateKeyBundleV1.generate(newWallet()) + bundle.identityKey, + ), + ).rejects.toThrow("Invalid ephemPublicKey length"); + }); + + it("rejects incorrect signatures", async () => { + const rawData = new TextEncoder().encode("hi"); + const stranger = await PrivateKeyBundleV1.generate(newWallet()); const encrypted = await encrypt( getPublic(Buffer.from(bundle.identityKey.secp256k1.bytes)), - Buffer.from(rawData) - ) + Buffer.from(rawData), + ); const ciphertext = await SignedEciesCiphertext.create( encrypted, - bundle.identityKey - ) + bundle.identityKey, + ); const signedWithWrongKey = await SignedEciesCiphertext.create( encrypted, - stranger.identityKey - ) + stranger.identityKey, + ); - ciphertext.signature = signedWithWrongKey.signature + ciphertext.signature = signedWithWrongKey.signature; - expect(await ciphertext.verify(bundle.identityKey.publicKey)).toBe(false) - }) -}) + expect(await ciphertext.verify(bundle.identityKey.publicKey)).toBe(false); + }); +}); diff --git a/packages/js-sdk/test/crypto/encryption.test.ts b/packages/js-sdk/test/crypto/encryption.test.ts index 8b394f622..b282de007 100644 --- a/packages/js-sdk/test/crypto/encryption.test.ts +++ b/packages/js-sdk/test/crypto/encryption.test.ts @@ -1,72 +1,72 @@ -import crypto from '@/crypto/crypto' +import crypto from "@/crypto/crypto"; import { exportHmacKey, generateHmacSignature, hkdfHmacKey, importHmacKey, verifyHmacSignature, -} from '@/crypto/encryption' +} from "@/crypto/encryption"; -describe('HMAC encryption', () => { - it('generates and validates HMAC', async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)) - const info = crypto.getRandomValues(new Uint8Array(32)) - const message = crypto.getRandomValues(new Uint8Array(32)) - const hmac = await generateHmacSignature(secret, info, message) - const key = await hkdfHmacKey(secret, info) - const valid = await verifyHmacSignature(key, hmac, message) - expect(valid).toBe(true) - }) +describe("HMAC encryption", () => { + it("generates and validates HMAC", async () => { + const secret = crypto.getRandomValues(new Uint8Array(32)); + const info = crypto.getRandomValues(new Uint8Array(32)); + const message = crypto.getRandomValues(new Uint8Array(32)); + const hmac = await generateHmacSignature(secret, info, message); + const key = await hkdfHmacKey(secret, info); + const valid = await verifyHmacSignature(key, hmac, message); + expect(valid).toBe(true); + }); - it('generates and validates HMAC with imported key', async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)) - const info = crypto.getRandomValues(new Uint8Array(32)) - const message = crypto.getRandomValues(new Uint8Array(32)) - const hmac = await generateHmacSignature(secret, info, message) - const key = await hkdfHmacKey(secret, info) - const exportedKey = await exportHmacKey(key) - const importedKey = await importHmacKey(exportedKey) - const valid = await verifyHmacSignature(importedKey, hmac, message) - expect(valid).toBe(true) - }) + it("generates and validates HMAC with imported key", async () => { + const secret = crypto.getRandomValues(new Uint8Array(32)); + const info = crypto.getRandomValues(new Uint8Array(32)); + const message = crypto.getRandomValues(new Uint8Array(32)); + const hmac = await generateHmacSignature(secret, info, message); + const key = await hkdfHmacKey(secret, info); + const exportedKey = await exportHmacKey(key); + const importedKey = await importHmacKey(exportedKey); + const valid = await verifyHmacSignature(importedKey, hmac, message); + expect(valid).toBe(true); + }); - it('generates different HMAC keys with different infos', async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)) - const info1 = crypto.getRandomValues(new Uint8Array(32)) - const info2 = crypto.getRandomValues(new Uint8Array(32)) - const key1 = await hkdfHmacKey(secret, info1) - const key2 = await hkdfHmacKey(secret, info2) + it("generates different HMAC keys with different infos", async () => { + const secret = crypto.getRandomValues(new Uint8Array(32)); + const info1 = crypto.getRandomValues(new Uint8Array(32)); + const info2 = crypto.getRandomValues(new Uint8Array(32)); + const key1 = await hkdfHmacKey(secret, info1); + const key2 = await hkdfHmacKey(secret, info2); - expect(await exportHmacKey(key1)).not.toEqual(await exportHmacKey(key2)) - }) + expect(await exportHmacKey(key1)).not.toEqual(await exportHmacKey(key2)); + }); - it('fails to validate HMAC with wrong message', async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)) - const info = crypto.getRandomValues(new Uint8Array(32)) - const message = crypto.getRandomValues(new Uint8Array(32)) - const hmac = await generateHmacSignature(secret, info, message) - const key = await hkdfHmacKey(secret, info) + it("fails to validate HMAC with wrong message", async () => { + const secret = crypto.getRandomValues(new Uint8Array(32)); + const info = crypto.getRandomValues(new Uint8Array(32)); + const message = crypto.getRandomValues(new Uint8Array(32)); + const hmac = await generateHmacSignature(secret, info, message); + const key = await hkdfHmacKey(secret, info); const valid = await verifyHmacSignature( key, hmac, - crypto.getRandomValues(new Uint8Array(32)) - ) - expect(valid).toBe(false) - }) + crypto.getRandomValues(new Uint8Array(32)), + ); + expect(valid).toBe(false); + }); - it('fails to validate HMAC with wrong key', async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)) - const info = crypto.getRandomValues(new Uint8Array(32)) - const message = crypto.getRandomValues(new Uint8Array(32)) - const hmac = await generateHmacSignature(secret, info, message) + it("fails to validate HMAC with wrong key", async () => { + const secret = crypto.getRandomValues(new Uint8Array(32)); + const info = crypto.getRandomValues(new Uint8Array(32)); + const message = crypto.getRandomValues(new Uint8Array(32)); + const hmac = await generateHmacSignature(secret, info, message); const valid = await verifyHmacSignature( await hkdfHmacKey( crypto.getRandomValues(new Uint8Array(32)), - crypto.getRandomValues(new Uint8Array(32)) + crypto.getRandomValues(new Uint8Array(32)), ), hmac, - message - ) - expect(valid).toBe(false) - }) -}) + message, + ); + expect(valid).toBe(false); + }); +}); diff --git a/packages/js-sdk/test/crypto/index.test.ts b/packages/js-sdk/test/crypto/index.test.ts index 4a25e4352..abfdce06c 100644 --- a/packages/js-sdk/test/crypto/index.test.ts +++ b/packages/js-sdk/test/crypto/index.test.ts @@ -1,87 +1,89 @@ -import { assert } from 'vitest' -import crypto from '@/crypto/crypto' -import { decrypt, encrypt } from '@/crypto/encryption' -import { PrivateKey } from '@/crypto/PrivateKey' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import { PublicKeyBundle } from '@/crypto/PublicKeyBundle' +import { assert } from "vitest"; +import crypto from "@/crypto/crypto"; +import { decrypt, encrypt } from "@/crypto/encryption"; +import { PrivateKey } from "@/crypto/PrivateKey"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import { PublicKeyBundle } from "@/crypto/PublicKeyBundle"; -describe('Crypto', function () { - it('signs keys and verifies signatures', async function () { - const identityKey = PrivateKey.generate() - const preKey = PrivateKey.generate() - await identityKey.signKey(preKey.publicKey) - expect(await identityKey.publicKey.verifyKey(preKey.publicKey)).toBeTruthy() - }) +describe("Crypto", function () { + it("signs keys and verifies signatures", async function () { + const identityKey = PrivateKey.generate(); + const preKey = PrivateKey.generate(); + await identityKey.signKey(preKey.publicKey); + expect( + await identityKey.publicKey.verifyKey(preKey.publicKey), + ).toBeTruthy(); + }); - it('encrypts and decrypts payload', async function () { - const alice = PrivateKey.generate() - const bob = PrivateKey.generate() - const msg1 = 'Yo!' - const decrypted = new TextEncoder().encode(msg1) + it("encrypts and decrypts payload", async function () { + const alice = PrivateKey.generate(); + const bob = PrivateKey.generate(); + const msg1 = "Yo!"; + const decrypted = new TextEncoder().encode(msg1); // Alice encrypts msg for Bob. - const encrypted = await alice.encrypt(decrypted, bob.publicKey) + const encrypted = await alice.encrypt(decrypted, bob.publicKey); // Bob decrypts msg from Alice. - const decrypted2 = await bob.decrypt(encrypted, alice.publicKey) - const msg2 = new TextDecoder().decode(decrypted2) - expect(msg2).toEqual(msg1) - }) + const decrypted2 = await bob.decrypt(encrypted, alice.publicKey); + const msg2 = new TextDecoder().decode(decrypted2); + expect(msg2).toEqual(msg1); + }); - it('detects tampering with encrypted message', async function () { - const alice = PrivateKey.generate() - const bob = PrivateKey.generate() - const msg1 = 'Yo!' - const decrypted = new TextEncoder().encode(msg1) + it("detects tampering with encrypted message", async function () { + const alice = PrivateKey.generate(); + const bob = PrivateKey.generate(); + const msg1 = "Yo!"; + const decrypted = new TextEncoder().encode(msg1); // Alice encrypts msg for Bob. - const encrypted = await alice.encrypt(decrypted, bob.publicKey) + const encrypted = await alice.encrypt(decrypted, bob.publicKey); // Malory tampers with the message - expect(encrypted.aes256GcmHkdfSha256).toBeTruthy() - encrypted.aes256GcmHkdfSha256!.payload[2] ^= 4 // flip one bit + expect(encrypted.aes256GcmHkdfSha256).toBeTruthy(); + encrypted.aes256GcmHkdfSha256!.payload[2] ^= 4; // flip one bit // Bob attempts to decrypt msg from Alice. try { - await bob.decrypt(encrypted, alice.publicKey) - assert.fail('should have thrown') + await bob.decrypt(encrypted, alice.publicKey); + assert.fail("should have thrown"); } catch (e) { - expect(e).toBeTruthy() + expect(e).toBeTruthy(); } - }) + }); - it('derives public key from signature', async function () { - const pri = PrivateKey.generate() - const digest = crypto.getRandomValues(new Uint8Array(16)) - const sig = await pri.sign(digest) - const sigPub = sig.getPublicKey(digest) - expect(sigPub).toBeTruthy() - expect(sigPub!.secp256k1Uncompressed).toBeTruthy() - expect(pri.publicKey.secp256k1Uncompressed).toBeTruthy() + it("derives public key from signature", async function () { + const pri = PrivateKey.generate(); + const digest = crypto.getRandomValues(new Uint8Array(16)); + const sig = await pri.sign(digest); + const sigPub = sig.getPublicKey(digest); + expect(sigPub).toBeTruthy(); + expect(sigPub!.secp256k1Uncompressed).toBeTruthy(); + expect(pri.publicKey.secp256k1Uncompressed).toBeTruthy(); expect(sigPub!.secp256k1Uncompressed.bytes).toEqual( - pri.publicKey.secp256k1Uncompressed.bytes - ) - }) + pri.publicKey.secp256k1Uncompressed.bytes, + ); + }); - it('encrypts and decrypts payload with key bundles', async function () { - const alice = await PrivateKeyBundleV1.generate() - const bob = await PrivateKeyBundleV1.generate() - const msg1 = 'Yo!' - const decrypted = new TextEncoder().encode(msg1) + it("encrypts and decrypts payload with key bundles", async function () { + const alice = await PrivateKeyBundleV1.generate(); + const bob = await PrivateKeyBundleV1.generate(); + const msg1 = "Yo!"; + const decrypted = new TextEncoder().encode(msg1); // Alice encrypts msg for Bob. - const alicePublic = alice.getPublicKeyBundle() - const bobPublic = bob.getPublicKeyBundle() - let secret = await alice.sharedSecret(bobPublic, alicePublic.preKey, false) - const encrypted = await encrypt(decrypted, secret) + const alicePublic = alice.getPublicKeyBundle(); + const bobPublic = bob.getPublicKeyBundle(); + let secret = await alice.sharedSecret(bobPublic, alicePublic.preKey, false); + const encrypted = await encrypt(decrypted, secret); // Bob decrypts msg from Alice. - secret = await bob.sharedSecret(alicePublic, bobPublic.preKey, true) - const decrypted2 = await decrypt(encrypted, secret) - const msg2 = new TextDecoder().decode(decrypted2) - expect(msg2).toEqual(msg1) - }) + secret = await bob.sharedSecret(alicePublic, bobPublic.preKey, true); + const decrypted2 = await decrypt(encrypted, secret); + const msg2 = new TextDecoder().decode(decrypted2); + expect(msg2).toEqual(msg1); + }); - it('serializes and deserializes keys and signatures', async function () { - const alice = await PrivateKeyBundleV1.generate() - const bytes = alice.getPublicKeyBundle().toBytes() - expect(bytes.length >= 213).toBeTruthy() - const pub2 = PublicKeyBundle.fromBytes(bytes) - expect(pub2.identityKey).toBeTruthy() - expect(pub2.preKey).toBeTruthy() - expect(pub2.identityKey.verifyKey(pub2.preKey)).toBeTruthy() - }) -}) + it("serializes and deserializes keys and signatures", async function () { + const alice = await PrivateKeyBundleV1.generate(); + const bytes = alice.getPublicKeyBundle().toBytes(); + expect(bytes.length >= 213).toBeTruthy(); + const pub2 = PublicKeyBundle.fromBytes(bytes); + expect(pub2.identityKey).toBeTruthy(); + expect(pub2.preKey).toBeTruthy(); + expect(pub2.identityKey.verifyKey(pub2.preKey)).toBeTruthy(); + }); +}); diff --git a/packages/js-sdk/test/helpers.ts b/packages/js-sdk/test/helpers.ts index e8254b212..9b435e8c4 100644 --- a/packages/js-sdk/test/helpers.ts +++ b/packages/js-sdk/test/helpers.ts @@ -1,108 +1,111 @@ -import type { ContentCodec, ContentTypeId } from '@xmtp/content-type-primitives' -import { TextCodec } from '@xmtp/content-type-text' -import { fetcher, type messageApi } from '@xmtp/proto' -import { Wallet } from 'ethers' -import Client, { type ClientOptions } from '@/Client' -import { PrivateKey } from '@/crypto/PrivateKey' +import type { + ContentCodec, + ContentTypeId, +} from "@xmtp/content-type-primitives"; +import { TextCodec } from "@xmtp/content-type-text"; +import { fetcher, type messageApi } from "@xmtp/proto"; +import { Wallet } from "ethers"; +import Client, { type ClientOptions } from "@/Client"; +import { PrivateKey } from "@/crypto/PrivateKey"; import type { PublicKeyBundle, SignedPublicKeyBundle, -} from '@/crypto/PublicKeyBundle' -import type Stream from '@/Stream' -import type { Signer } from '@/types/Signer' -import { promiseWithTimeout } from '@/utils/async' -import { dateToNs, toNanoString } from '@/utils/date' +} from "@/crypto/PublicKeyBundle"; +import type Stream from "@/Stream"; +import type { Signer } from "@/types/Signer"; +import { promiseWithTimeout } from "@/utils/async"; +import { dateToNs, toNanoString } from "@/utils/date"; -const { b64Encode } = fetcher +const { b64Encode } = fetcher; export const sleep = (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)) + new Promise((resolve) => setTimeout(resolve, ms)); export async function pollFor( callback: () => Promise, timeoutMs: number, - delayMs: number + delayMs: number, ): Promise { - const started = Date.now() + const started = Date.now(); try { - return await callback() + return await callback(); } catch (err) { if (delayMs) { - await sleep(delayMs) + await sleep(delayMs); } - const elapsedMs = Date.now() - started - const remainingTimeoutMs = timeoutMs - elapsedMs + const elapsedMs = Date.now() - started; + const remainingTimeoutMs = timeoutMs - elapsedMs; if (remainingTimeoutMs <= 0) { - throw new Error('timeout exceeded') + throw new Error("timeout exceeded"); } - return await pollFor(callback, remainingTimeoutMs, delayMs) + return await pollFor(callback, remainingTimeoutMs, delayMs); } } export async function waitForUserContact( c1: Client, - c2: Client + c2: Client, ): Promise { return pollFor( async () => { - const contact = await c1.getUserContact(c2.address) - expect(contact).toBeTruthy() - return contact! + const contact = await c1.getUserContact(c2.address); + expect(contact).toBeTruthy(); + return contact!; }, 20000, - 200 - ) + 200, + ); } export async function dumpStream( stream: Stream, - timeoutMs = 1000 + timeoutMs = 1000, ): Promise { - const messages: T[] = [] + const messages: T[] = []; try { while (true) { const result = await promiseWithTimeout( timeoutMs, () => stream.next(), - 'timeout' - ) + "timeout", + ); if (result.done) { - break + break; } - messages.push(result.value) + messages.push(result.value); } } catch { } finally { - stream.return() + stream.return(); } - return messages + return messages; } export function newWallet(): Wallet { - const key = PrivateKey.generate() + const key = PrivateKey.generate(); if (!key.secp256k1) { - throw new Error('invalid key') + throw new Error("invalid key"); } - return new Wallet(key.secp256k1.bytes) + return new Wallet(key.secp256k1.bytes); } export function newCustomWallet(): Signer { - const ethersWallet = newWallet() + const ethersWallet = newWallet(); // Client apps don't have to use ethers, they can implement their // own Signer methods. Here we implement a custom Signer that is actually // backed by ethers. return { getAddress(): Promise { - return ethersWallet.getAddress() + return ethersWallet.getAddress(); }, signMessage(message: ArrayLike | string): Promise { return ethersWallet.signMessage(message).then((signature) => { return new Promise((resolve) => { - resolve(signature) - }) - }) + resolve(signature); + }); + }); }, - } + }; } export function wrapAsLedgerWallet(ethersWallet: Wallet): Signer { @@ -110,46 +113,46 @@ export function wrapAsLedgerWallet(ethersWallet: Wallet): Signer { // so 0x1b => 0x00 and 0x1c => 0x01 return { getAddress(): Promise { - return ethersWallet.getAddress() + return ethersWallet.getAddress(); }, signMessage(message: ArrayLike | string): Promise { return ethersWallet.signMessage(message).then((signature) => { - const bytes = Buffer.from(signature.slice(2), 'hex') - const lastByte = bytes[bytes.length - 1] + const bytes = Buffer.from(signature.slice(2), "hex"); + const lastByte = bytes[bytes.length - 1]; if (lastByte < 0x1b) { - return new Promise((resolve) => resolve(signature)) + return new Promise((resolve) => resolve(signature)); } - bytes[bytes.length - 1] = lastByte - 0x1b + bytes[bytes.length - 1] = lastByte - 0x1b; return new Promise((resolve) => { - resolve('0x' + bytes.toString('hex')) - }) - }) + resolve("0x" + bytes.toString("hex")); + }); + }); }, - } + }; } // A helper to replace a full Client in testing custom content types, // extracting just the codec registry aspect of the client. export class CodecRegistry { // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _codecs: Map> + private _codecs: Map>; constructor() { - this._codecs = new Map() - this.registerCodec(new TextCodec()) + this._codecs = new Map(); + this.registerCodec(new TextCodec()); } // eslint-disable-next-line @typescript-eslint/no-explicit-any registerCodec(codec: ContentCodec): void { - const id = codec.contentType - const key = `${id.authorityId}/${id.typeId}` - this._codecs.set(key, codec) + const id = codec.contentType; + const key = `${id.authorityId}/${id.typeId}`; + this._codecs.set(key, codec); } // eslint-disable-next-line @typescript-eslint/no-explicit-any codecFor(contentType: ContentTypeId): ContentCodec | undefined { - const key = `${contentType.authorityId}/${contentType.typeId}` - return this._codecs.get(key) + const key = `${contentType.authorityId}/${contentType.typeId}`; + return this._codecs.get(key); } } @@ -157,47 +160,47 @@ export class CodecRegistry { // see github.com/xmtp/xmtp-node-go/scripts/xmtp-js.sh export const newLocalHostClient = (opts?: Partial) => Client.create(newWallet(), { - env: 'local', + env: "local", ...opts, - }) + }); // client running against local node running on the host, // with a non-ethers wallet export const newLocalHostClientWithCustomWallet = ( - opts?: Partial + opts?: Partial, ) => Client.create(newCustomWallet(), { - env: 'local', + env: "local", ...opts, - }) + }); // client running against the dev cluster in AWS export const newDevClient = (opts?: Partial) => Client.create(newWallet(), { - env: 'dev', + env: "dev", ...opts, - }) + }); export const buildEnvelope = ( message: Uint8Array, contentTopic: string, - created: Date + created: Date, ): messageApi.Envelope => { return { contentTopic, timestampNs: toNanoString(created), message: b64Encode(message, 0, message.length) as unknown as Uint8Array, - } -} + }; +}; export const buildProtoEnvelope = ( payload: Uint8Array, contentTopic: string, - timestamp: Date + timestamp: Date, ) => { return { contentTopic, timestampNs: dateToNs(timestamp), payload, - } -} + }; +}; diff --git a/packages/js-sdk/test/keystore/InMemoryKeystore.test.ts b/packages/js-sdk/test/keystore/InMemoryKeystore.test.ts index 2a26bfe4e..e1be27ce2 100644 --- a/packages/js-sdk/test/keystore/InMemoryKeystore.test.ts +++ b/packages/js-sdk/test/keystore/InMemoryKeystore.test.ts @@ -1,138 +1,138 @@ -import { keystore, privateKey } from '@xmtp/proto' -import type { CreateInviteResponse } from '@xmtp/proto/ts/dist/types/keystore_api/v1/keystore.pb' -import Long from 'long' -import { toBytes } from 'viem' -import { assert } from 'vitest' -import Token from '@/authn/Token' +import { keystore, privateKey } from "@xmtp/proto"; +import type { CreateInviteResponse } from "@xmtp/proto/ts/dist/types/keystore_api/v1/keystore.pb"; +import Long from "long"; +import { toBytes } from "viem"; +import { assert } from "vitest"; +import Token from "@/authn/Token"; import { generateHmacSignature, hkdfHmacKey, importHmacKey, verifyHmacSignature, -} from '@/crypto/encryption' +} from "@/crypto/encryption"; import { PrivateKeyBundleV1, PrivateKeyBundleV2, -} from '@/crypto/PrivateKeyBundle' -import { SignedPublicKeyBundle } from '@/crypto/PublicKeyBundle' -import { equalBytes } from '@/crypto/utils' +} from "@/crypto/PrivateKeyBundle"; +import { SignedPublicKeyBundle } from "@/crypto/PublicKeyBundle"; +import { equalBytes } from "@/crypto/utils"; import { InvitationV1, SealedInvitation, type InvitationContext, -} from '@/Invitation' -import { decryptV1 } from '@/keystore/encryption' -import { KeystoreError } from '@/keystore/errors' -import InMemoryKeystore from '@/keystore/InMemoryKeystore' -import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' -import { getKeyMaterial } from '@/keystore/utils' -import { MessageV1 } from '@/Message' -import { dateToNs, nsToDate } from '@/utils/date' -import { buildProtoEnvelope, newWallet } from '@test/helpers' -import { randomBytes } from '@bench/helpers' - -describe('InMemoryKeystore', () => { - let aliceKeys: PrivateKeyBundleV1 - let aliceKeystore: InMemoryKeystore - let bobKeys: PrivateKeyBundleV1 - let bobKeystore: InMemoryKeystore +} from "@/Invitation"; +import { decryptV1 } from "@/keystore/encryption"; +import { KeystoreError } from "@/keystore/errors"; +import InMemoryKeystore from "@/keystore/InMemoryKeystore"; +import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; +import { getKeyMaterial } from "@/keystore/utils"; +import { MessageV1 } from "@/Message"; +import { dateToNs, nsToDate } from "@/utils/date"; +import { buildProtoEnvelope, newWallet } from "@test/helpers"; +import { randomBytes } from "@bench/helpers"; + +describe("InMemoryKeystore", () => { + let aliceKeys: PrivateKeyBundleV1; + let aliceKeystore: InMemoryKeystore; + let bobKeys: PrivateKeyBundleV1; + let bobKeystore: InMemoryKeystore; beforeEach(async () => { - aliceKeys = await PrivateKeyBundleV1.generate(newWallet()) + aliceKeys = await PrivateKeyBundleV1.generate(newWallet()); aliceKeystore = await InMemoryKeystore.create( aliceKeys, - InMemoryPersistence.create() - ) - bobKeys = await PrivateKeyBundleV1.generate(newWallet()) + InMemoryPersistence.create(), + ); + bobKeys = await PrivateKeyBundleV1.generate(newWallet()); bobKeystore = await InMemoryKeystore.create( bobKeys, - InMemoryPersistence.create() - ) - }) + InMemoryPersistence.create(), + ); + }); const buildInvite = async (context?: InvitationContext) => { - const invite = InvitationV1.createRandom(context) - const created = new Date() + const invite = InvitationV1.createRandom(context); + const created = new Date(); const sealed = await SealedInvitation.createV1({ sender: PrivateKeyBundleV2.fromLegacyBundle(aliceKeys), recipient: SignedPublicKeyBundle.fromLegacyBundle( - bobKeys.getPublicKeyBundle() + bobKeys.getPublicKeyBundle(), ), invitation: invite, created, - }) + }); - return { invite, created, sealed } - } + return { invite, created, sealed }; + }; - describe('encryptV1', () => { - it('can encrypt a batch of valid messages', async () => { + describe("encryptV1", () => { + it("can encrypt a batch of valid messages", async () => { const messages = Array.from({ length: 10 }, (v: unknown, i: number) => - new TextEncoder().encode(`message ${i}`) - ) + new TextEncoder().encode(`message ${i}`), + ); - const headerBytes = new Uint8Array(10) + const headerBytes = new Uint8Array(10); const req = messages.map((msg) => ({ recipient: bobKeys.getPublicKeyBundle(), payload: msg, headerBytes, - })) + })); - const res = await aliceKeystore.encryptV1({ requests: req }) - expect(res.responses).toHaveLength(req.length) + const res = await aliceKeystore.encryptV1({ requests: req }); + expect(res.responses).toHaveLength(req.length); for (const { error, result } of res.responses) { if (error || !result) { - throw error + throw error; } - const encrypted = result!.encrypted + const encrypted = result!.encrypted; if (!encrypted) { - throw new Error('No encrypted result') + throw new Error("No encrypted result"); } - expect(result.encrypted?.aes256GcmHkdfSha256?.gcmNonce).toBeTruthy() - expect(result.encrypted?.aes256GcmHkdfSha256?.hkdfSalt).toBeTruthy() - expect(result.encrypted?.aes256GcmHkdfSha256?.payload).toBeTruthy() + expect(result.encrypted?.aes256GcmHkdfSha256?.gcmNonce).toBeTruthy(); + expect(result.encrypted?.aes256GcmHkdfSha256?.hkdfSalt).toBeTruthy(); + expect(result.encrypted?.aes256GcmHkdfSha256?.payload).toBeTruthy(); // Ensure decryption doesn't throw await decryptV1( aliceKeys, bobKeys.getPublicKeyBundle(), encrypted, headerBytes, - true - ) + true, + ); } - }) + }); - it('fails to encrypt with invalid params', async () => { + it("fails to encrypt with invalid params", async () => { const requests = [ { recipient: {}, payload: new Uint8Array(10), headerBytes: new Uint8Array(10), }, - ] + ]; // @ts-expect-error test case - const res = await aliceKeystore.encryptV1({ requests }) - - expect(res.responses).toHaveLength(requests.length) - expect(res.responses[0]).toHaveProperty('error') - expect(res.responses[0].error).toHaveProperty('code') - }) - }) - - describe('decryptV1', () => { - it('can decrypt a valid message', async () => { - const msg = new TextEncoder().encode('Hello, world!') - const peerKeys = bobKeys.getPublicKeyBundle() + const res = await aliceKeystore.encryptV1({ requests }); + + expect(res.responses).toHaveLength(requests.length); + expect(res.responses[0]).toHaveProperty("error"); + expect(res.responses[0].error).toHaveProperty("code"); + }); + }); + + describe("decryptV1", () => { + it("can decrypt a valid message", async () => { + const msg = new TextEncoder().encode("Hello, world!"); + const peerKeys = bobKeys.getPublicKeyBundle(); const message = await MessageV1.encode( aliceKeystore, msg, aliceKeys.getPublicKeyBundle(), peerKeys, - new Date() - ) + new Date(), + ); const requests = [ { @@ -141,28 +141,28 @@ describe('InMemoryKeystore', () => { headerBytes: message.headerBytes, isSender: true, }, - ] + ]; - const { responses } = await aliceKeystore.decryptV1({ requests }) + const { responses } = await aliceKeystore.decryptV1({ requests }); - expect(responses).toHaveLength(requests.length) + expect(responses).toHaveLength(requests.length); if (responses[0].error) { - throw responses[0].error + throw responses[0].error; } - expect(equalBytes(responses[0]!.result!.decrypted, msg)).toBe(true) - }) + expect(equalBytes(responses[0]!.result!.decrypted, msg)).toBe(true); + }); - it('fails to decrypt an invalid message', async () => { - const msg = new TextEncoder().encode('Hello, world!') - const charlieKeys = await PrivateKeyBundleV1.generate(newWallet()) + it("fails to decrypt an invalid message", async () => { + const msg = new TextEncoder().encode("Hello, world!"); + const charlieKeys = await PrivateKeyBundleV1.generate(newWallet()); const message = await MessageV1.encode( bobKeystore, msg, bobKeys.getPublicKeyBundle(), charlieKeys.getPublicKeyBundle(), - new Date() - ) + new Date(), + ); const requests = [ { @@ -171,174 +171,174 @@ describe('InMemoryKeystore', () => { headerBytes: message.headerBytes, isSender: true, }, - ] + ]; - const { responses } = await aliceKeystore.decryptV1({ requests }) + const { responses } = await aliceKeystore.decryptV1({ requests }); - expect(responses).toHaveLength(requests.length) + expect(responses).toHaveLength(requests.length); if (!responses[0].error) { - throw new Error('should have errored') + throw new Error("should have errored"); } - }) - }) + }); + }); - describe('createInvite', () => { - it('creates a valid invite with no context', async () => { + describe("createInvite", () => { + it("creates a valid invite with no context", async () => { const recipient = SignedPublicKeyBundle.fromLegacyBundle( - bobKeys.getPublicKeyBundle() - ) - const createdNs = dateToNs(new Date()) + bobKeys.getPublicKeyBundle(), + ); + const createdNs = dateToNs(new Date()); const response = await aliceKeystore.createInvite({ recipient, createdNs, context: undefined, consentProof: undefined, - }) + }); - expect(response.conversation?.topic).toBeTruthy() - expect(response.conversation?.context).toBeUndefined() - expect(response.conversation?.createdNs.equals(createdNs)).toBeTruthy() - expect(response.payload).toBeInstanceOf(Uint8Array) - }) + expect(response.conversation?.topic).toBeTruthy(); + expect(response.conversation?.context).toBeUndefined(); + expect(response.conversation?.createdNs.equals(createdNs)).toBeTruthy(); + expect(response.payload).toBeInstanceOf(Uint8Array); + }); - it('creates a valid invite with context', async () => { + it("creates a valid invite with context", async () => { const recipient = SignedPublicKeyBundle.fromLegacyBundle( - bobKeys.getPublicKeyBundle() - ) - const createdNs = dateToNs(new Date()) - const context = { conversationId: 'xmtp.org/foo', metadata: {} } + bobKeys.getPublicKeyBundle(), + ); + const createdNs = dateToNs(new Date()); + const context = { conversationId: "xmtp.org/foo", metadata: {} }; const response = await aliceKeystore.createInvite({ recipient, createdNs, context, consentProof: undefined, - }) + }); - expect(response.conversation?.topic).toBeTruthy() - expect(response.conversation?.context).toEqual(context) - }) + expect(response.conversation?.topic).toBeTruthy(); + expect(response.conversation?.context).toEqual(context); + }); - it('throws if an invalid recipient is included', async () => { - const createdNs = dateToNs(new Date()) + it("throws if an invalid recipient is included", async () => { + const createdNs = dateToNs(new Date()); await expect(async () => { await aliceKeystore.createInvite({ recipient: {} as any, createdNs, context: undefined, consentProof: undefined, - }) - }).rejects.toThrow(KeystoreError) - }) - }) - - describe('saveInvites', () => { - it('can save a batch of valid envelopes', async () => { - const keystore = aliceKeystore - const { invite, created, sealed } = await buildInvite() - - const sealedBytes = sealed.toBytes() - const envelope = buildProtoEnvelope(sealedBytes, 'foo', created) + }); + }).rejects.toThrow(KeystoreError); + }); + }); + + describe("saveInvites", () => { + it("can save a batch of valid envelopes", async () => { + const keystore = aliceKeystore; + const { invite, created, sealed } = await buildInvite(); + + const sealedBytes = sealed.toBytes(); + const envelope = buildProtoEnvelope(sealedBytes, "foo", created); const { responses } = await keystore.saveInvites({ requests: [envelope], - }) + }); - expect(responses).toHaveLength(1) - const firstResult = responses[0] + expect(responses).toHaveLength(1); + const firstResult = responses[0]; if (firstResult.error) { - throw firstResult.error + throw firstResult.error; } expect( - nsToDate(firstResult.result!.conversation!.createdNs).getTime() - ).toEqual(created.getTime()) - expect(firstResult.result!.conversation!.topic).toEqual(invite.topic) - expect(firstResult.result!.conversation?.context).toBeUndefined() + nsToDate(firstResult.result!.conversation!.createdNs).getTime(), + ).toEqual(created.getTime()); + expect(firstResult.result!.conversation!.topic).toEqual(invite.topic); + expect(firstResult.result!.conversation?.context).toBeUndefined(); - const conversations = (await keystore.getV2Conversations()).conversations - expect(conversations).toHaveLength(1) - expect(conversations[0].topic).toBe(invite.topic) - }) + const conversations = (await keystore.getV2Conversations()).conversations; + expect(conversations).toHaveLength(1); + expect(conversations[0].topic).toBe(invite.topic); + }); - it('can save received invites', async () => { - const { created, sealed } = await buildInvite() + it("can save received invites", async () => { + const { created, sealed } = await buildInvite(); - const sealedBytes = sealed.toBytes() - const envelope = buildProtoEnvelope(sealedBytes, 'foo', created) + const sealedBytes = sealed.toBytes(); + const envelope = buildProtoEnvelope(sealedBytes, "foo", created); const { responses: [aliceResponse], } = await aliceKeystore.saveInvites({ requests: [envelope], - }) + }); if (aliceResponse.error) { - throw aliceResponse + throw aliceResponse; } const aliceConversations = (await aliceKeystore.getV2Conversations()) - .conversations - expect(aliceConversations).toHaveLength(1) + .conversations; + expect(aliceConversations).toHaveLength(1); const { responses: [bobResponse], - } = await bobKeystore.saveInvites({ requests: [envelope] }) + } = await bobKeystore.saveInvites({ requests: [envelope] }); if (bobResponse.error) { - throw bobResponse + throw bobResponse; } const bobConversations = (await bobKeystore.getV2Conversations()) - .conversations - expect(bobConversations).toHaveLength(1) - }) + .conversations; + expect(bobConversations).toHaveLength(1); + }); - it('ignores bad envelopes', async () => { - const conversationId = 'xmtp.org/foo' + it("ignores bad envelopes", async () => { + const conversationId = "xmtp.org/foo"; const { invite, created, sealed } = await buildInvite({ conversationId, metadata: {}, - }) + }); const envelopes = [ - buildProtoEnvelope(new Uint8Array(10), 'bar', new Date()), - buildProtoEnvelope(sealed.toBytes(), 'foo', created), - ] + buildProtoEnvelope(new Uint8Array(10), "bar", new Date()), + buildProtoEnvelope(sealed.toBytes(), "foo", created), + ]; - const response = await bobKeystore.saveInvites({ requests: envelopes }) - expect(response.responses).toHaveLength(2) + const response = await bobKeystore.saveInvites({ requests: envelopes }); + expect(response.responses).toHaveLength(2); const { responses: [firstResult, secondResult], - } = response + } = response; if (!firstResult.error) { - assert.fail('should have errored') + assert.fail("should have errored"); } - expect(firstResult.error.code).toBeTruthy() + expect(firstResult.error.code).toBeTruthy(); if (secondResult.error) { - assert.fail('should not have errored') + assert.fail("should not have errored"); } expect( - secondResult.result?.conversation?.createdNs.equals(dateToNs(created)) - ).toBeTruthy() - expect(secondResult.result?.conversation?.topic).toEqual(invite.topic) + secondResult.result?.conversation?.createdNs.equals(dateToNs(created)), + ).toBeTruthy(); + expect(secondResult.result?.conversation?.topic).toEqual(invite.topic); expect( - secondResult.result?.conversation?.context?.conversationId - ).toEqual(conversationId) - }) - }) + secondResult.result?.conversation?.context?.conversationId, + ).toEqual(conversationId); + }); + }); - describe('encryptV2/decryptV2', () => { - it('encrypts using a saved envelope', async () => { - const keystore = aliceKeystore - const { invite, created, sealed } = await buildInvite() + describe("encryptV2/decryptV2", () => { + it("encrypts using a saved envelope", async () => { + const keystore = aliceKeystore; + const { invite, created, sealed } = await buildInvite(); - const sealedBytes = sealed.toBytes() - const envelope = buildProtoEnvelope(sealedBytes, 'foo', created) - await keystore.saveInvites({ requests: [envelope] }) + const sealedBytes = sealed.toBytes(); + const envelope = buildProtoEnvelope(sealedBytes, "foo", created); + await keystore.saveInvites({ requests: [envelope] }); - const payload = new TextEncoder().encode('Hello, world!') - const headerBytes = new Uint8Array(10) + const payload = new TextEncoder().encode("Hello, world!"); + const headerBytes = new Uint8Array(10); const { responses: [encrypted], @@ -350,29 +350,29 @@ describe('InMemoryKeystore', () => { headerBytes, }, ], - }) + }); if (encrypted.error) { - throw encrypted + throw encrypted; } - expect(encrypted.result?.encrypted).toBeTruthy() - }) + expect(encrypted.result?.encrypted).toBeTruthy(); + }); - it('round trips using a created invite', async () => { + it("round trips using a created invite", async () => { const recipient = SignedPublicKeyBundle.fromLegacyBundle( - bobKeys.getPublicKeyBundle() - ) - const createdNs = dateToNs(new Date()) + bobKeys.getPublicKeyBundle(), + ); + const createdNs = dateToNs(new Date()); const response = await aliceKeystore.createInvite({ recipient, createdNs, context: undefined, consentProof: undefined, - }) + }); - const payload = new TextEncoder().encode('Hello, world!') - const headerBytes = new Uint8Array(10) + const payload = new TextEncoder().encode("Hello, world!"); + const headerBytes = new Uint8Array(10); const { responses: [encrypted], @@ -384,13 +384,13 @@ describe('InMemoryKeystore', () => { headerBytes, }, ], - }) + }); if (encrypted.error) { - throw encrypted.error + throw encrypted.error; } - expect(encrypted.result?.encrypted).toBeTruthy() + expect(encrypted.result?.encrypted).toBeTruthy(); const { responses: [decrypted], @@ -402,29 +402,29 @@ describe('InMemoryKeystore', () => { contentTopic: response.conversation!.topic, }, ], - }) + }); if (decrypted.error) { - throw decrypted.error + throw decrypted.error; } - expect(equalBytes(payload, decrypted.result!.decrypted)).toBeTruthy() - }) + expect(equalBytes(payload, decrypted.result!.decrypted)).toBeTruthy(); + }); - it('generates a valid sender HMAC', async () => { + it("generates a valid sender HMAC", async () => { const recipient = SignedPublicKeyBundle.fromLegacyBundle( - bobKeys.getPublicKeyBundle() - ) - const createdNs = dateToNs(new Date()) + bobKeys.getPublicKeyBundle(), + ); + const createdNs = dateToNs(new Date()); const response = await aliceKeystore.createInvite({ recipient, createdNs, context: undefined, consentProof: undefined, - }) + }); - const payload = new TextEncoder().encode('Hello, world!') - const headerBytes = new Uint8Array(10) + const payload = new TextEncoder().encode("Hello, world!"); + const headerBytes = new Uint8Array(10); const { responses: [encrypted], @@ -436,135 +436,139 @@ describe('InMemoryKeystore', () => { headerBytes, }, ], - }) + }); if (encrypted.error) { - throw encrypted.error + throw encrypted.error; } const thirtyDayPeriodsSinceEpoch = Math.floor( - Date.now() / 1000 / 60 / 60 / 24 / 30 - ) - const topicData = aliceKeystore.lookupTopic(response.conversation!.topic) - const keyMaterial = getKeyMaterial(topicData!.invitation) + Date.now() / 1000 / 60 / 60 / 24 / 30, + ); + const topicData = aliceKeystore.lookupTopic(response.conversation!.topic); + const keyMaterial = getKeyMaterial(topicData!.invitation); const hmacKey = await hkdfHmacKey( keyMaterial, new TextEncoder().encode( - `${thirtyDayPeriodsSinceEpoch}-${aliceKeystore.walletAddress}` - ) - ) + `${thirtyDayPeriodsSinceEpoch}-${aliceKeystore.walletAddress}`, + ), + ); - expect(encrypted.result?.senderHmac).toBeTruthy() + expect(encrypted.result?.senderHmac).toBeTruthy(); expect( await verifyHmacSignature( hmacKey, encrypted.result!.senderHmac, - headerBytes - ) - ).toBeTruthy() - }) - }) - - describe('SignDigest', () => { - it('signs a valid digest with the identity key', async () => { - const digest = randomBytes(32) + headerBytes, + ), + ).toBeTruthy(); + }); + }); + + describe("SignDigest", () => { + it("signs a valid digest with the identity key", async () => { + const digest = randomBytes(32); const signature = await aliceKeystore.signDigest({ digest, identityKey: true, prekeyIndex: undefined, - }) - expect(signature).toEqual(await aliceKeys.identityKey.sign(digest)) - }) + }); + expect(signature).toEqual(await aliceKeys.identityKey.sign(digest)); + }); - it('rejects an invalid digest', async () => { - const digest = new Uint8Array(0) + it("rejects an invalid digest", async () => { + const digest = new Uint8Array(0); await expect( aliceKeystore.signDigest({ digest, identityKey: true, prekeyIndex: undefined, - }) - ).rejects.toThrow() - }) + }), + ).rejects.toThrow(); + }); - it('signs a valid digest with a specified prekey', async () => { - const digest = randomBytes(32) + it("signs a valid digest with a specified prekey", async () => { + const digest = randomBytes(32); const signature = await aliceKeystore.signDigest({ digest, identityKey: false, prekeyIndex: 0, - }) - expect(signature).toEqual(await aliceKeys.preKeys[0].sign(digest)) - }) + }); + expect(signature).toEqual(await aliceKeys.preKeys[0].sign(digest)); + }); - it('rejects signing with an invalid prekey index', async () => { - const digest = randomBytes(32) + it("rejects signing with an invalid prekey index", async () => { + const digest = randomBytes(32); await expect( aliceKeystore.signDigest({ digest, identityKey: false, prekeyIndex: 100, - }) + }), ).rejects.toThrow( new KeystoreError( keystore.ErrorCode.ERROR_CODE_NO_MATCHING_PREKEY, - 'no prekey found' - ) - ) - }) - }) - - describe('getV2Conversations', () => { - it('correctly sorts conversations', async () => { - const baseTime = new Date() + "no prekey found", + ), + ); + }); + }); + + describe("getV2Conversations", () => { + it("correctly sorts conversations", async () => { + const baseTime = new Date(); const timestamps = Array.from( { length: 25 }, - (_, i) => new Date(baseTime.getTime() + i) - ) + (_, i) => new Date(baseTime.getTime() + i), + ); // Shuffle the order they go into the store - const shuffled = [...timestamps].sort(() => Math.random() - 0.5) + const shuffled = [...timestamps].sort(() => Math.random() - 0.5); await Promise.all( shuffled.map(async (createdAt) => { - const keys = await PrivateKeyBundleV1.generate(newWallet()) + const keys = await PrivateKeyBundleV1.generate(newWallet()); const recipient = SignedPublicKeyBundle.fromLegacyBundle( - keys.getPublicKeyBundle() - ) + keys.getPublicKeyBundle(), + ); return aliceKeystore.createInvite({ recipient, createdNs: dateToNs(createdAt), context: undefined, consentProof: undefined, - }) - }) - ) + }); + }), + ); - const convos = (await aliceKeystore.getV2Conversations()).conversations - let lastCreated = Long.fromNumber(0) + const convos = (await aliceKeystore.getV2Conversations()).conversations; + let lastCreated = Long.fromNumber(0); for (let i = 0; i < convos.length; i++) { - expect(convos[i].createdNs.equals(dateToNs(timestamps[i]))).toBeTruthy() - expect(convos[i].createdNs.greaterThanOrEqual(lastCreated)).toBeTruthy() - lastCreated = convos[i].createdNs + expect( + convos[i].createdNs.equals(dateToNs(timestamps[i])), + ).toBeTruthy(); + expect( + convos[i].createdNs.greaterThanOrEqual(lastCreated), + ).toBeTruthy(); + lastCreated = convos[i].createdNs; } - }) + }); - it('uses deterministic topic', async () => { + it("uses deterministic topic", async () => { const recipient = SignedPublicKeyBundle.fromLegacyBundle( - bobKeys.getPublicKeyBundle() - ) - const baseTime = new Date() + bobKeys.getPublicKeyBundle(), + ); + const baseTime = new Date(); const timestamps = Array.from( { length: 25 }, - (_, i) => new Date(baseTime.getTime() + i) - ) + (_, i) => new Date(baseTime.getTime() + i), + ); // Shuffle the order they go into the store - const shuffled = [...timestamps].sort(() => Math.random() - 0.5) + const shuffled = [...timestamps].sort(() => Math.random() - 0.5); - const responses: CreateInviteResponse[] = [] + const responses: CreateInviteResponse[] = []; await Promise.all( shuffled.map(async (createdAt) => { const response = await aliceKeystore.createInvite({ @@ -572,368 +576,370 @@ describe('InMemoryKeystore', () => { createdNs: dateToNs(createdAt), context: undefined, consentProof: undefined, - }) + }); - responses.push(response) + responses.push(response); - return response - }) - ) + return response; + }), + ); - const firstResponse: CreateInviteResponse = responses[0] - const topicName = firstResponse.conversation!.topic + const firstResponse: CreateInviteResponse = responses[0]; + const topicName = firstResponse.conversation!.topic; // eslint-disable-next-line no-control-regex - expect(topicName).toMatch(/^[\x00-\x7F]+$/) + expect(topicName).toMatch(/^[\x00-\x7F]+$/); expect( responses.filter((response) => { - return response.conversation!.topic === topicName - }) - ).toHaveLength(25) - }) + return response.conversation!.topic === topicName; + }), + ).toHaveLength(25); + }); - it('generates known deterministic topic', async () => { + it("generates known deterministic topic", async () => { aliceKeys = new PrivateKeyBundleV1( privateKey.PrivateKeyBundle.decode( toBytes( - '0x0a8a030ac20108c192a3f7923112220a2068d2eb2ef8c50c4916b42ce638c5610e44ff4eb3ecb098' + - 'c9dacf032625c72f101a940108c192a3f7923112460a440a40fc9822283078c323c9319c45e60ab4' + - '2c65f6e1744ed8c23c52728d456d33422824c98d307e8b1c86a26826578523ba15fe6f04a17fca17' + - '6664ee8017ec8ba59310011a430a410498dc2315dd45d99f5e900a071e7b56142de344540f07fbc7' + - '3a0f9a5d5df6b52eb85db06a3825988ab5e04746bc221fcdf5310a44d9523009546d4bfbfbb89cfb' + - '12c20108eb92a3f7923112220a20788be9da8e1a1a08b05f7cbf22d86980bc056b130c482fa5bd26' + - 'ccb8d29b30451a940108eb92a3f7923112460a440a40a7afa25cb6f3fbb98f9e5cd92a1df1898452' + - 'e0dfa1d7e5affe9eaf9b72dd14bc546d86c399768badf983f07fa7dd16eee8d793357ce6fccd6768' + - '07d87bcc595510011a430a410422931e6295c3c93a5f6f5e729dc02e1754e916cb9be16d36dc163a' + - '300931f42a0cd5fde957d75c2068e1980c5f86843daf16aba8ae57e8160b8b9f0191def09e' - ) - ).v1! - ) + "0x0a8a030ac20108c192a3f7923112220a2068d2eb2ef8c50c4916b42ce638c5610e44ff4eb3ecb098" + + "c9dacf032625c72f101a940108c192a3f7923112460a440a40fc9822283078c323c9319c45e60ab4" + + "2c65f6e1744ed8c23c52728d456d33422824c98d307e8b1c86a26826578523ba15fe6f04a17fca17" + + "6664ee8017ec8ba59310011a430a410498dc2315dd45d99f5e900a071e7b56142de344540f07fbc7" + + "3a0f9a5d5df6b52eb85db06a3825988ab5e04746bc221fcdf5310a44d9523009546d4bfbfbb89cfb" + + "12c20108eb92a3f7923112220a20788be9da8e1a1a08b05f7cbf22d86980bc056b130c482fa5bd26" + + "ccb8d29b30451a940108eb92a3f7923112460a440a40a7afa25cb6f3fbb98f9e5cd92a1df1898452" + + "e0dfa1d7e5affe9eaf9b72dd14bc546d86c399768badf983f07fa7dd16eee8d793357ce6fccd6768" + + "07d87bcc595510011a430a410422931e6295c3c93a5f6f5e729dc02e1754e916cb9be16d36dc163a" + + "300931f42a0cd5fde957d75c2068e1980c5f86843daf16aba8ae57e8160b8b9f0191def09e", + ), + ).v1!, + ); aliceKeystore = await InMemoryKeystore.create( aliceKeys, - InMemoryPersistence.create() - ) + InMemoryPersistence.create(), + ); bobKeys = new PrivateKeyBundleV1( privateKey.PrivateKeyBundle.decode( toBytes( - '0x0a88030ac001088cd68df7923112220a209057f8d813314a2aae74e6c4c30f909c1c496b6037ce32' + - 'a12c613558a8e961681a9201088cd68df7923112440a420a40501ae9b4f75d5bb5bae3ca4ecfda4e' + - 'de9edc5a9b7fc2d56dc7325b837957c23235cc3005b46bb9ef485f106404dcf71247097ed5096355' + - '90f4b7987b833d03661a430a4104e61a7ae511567f4a2b5551221024b6932d6cdb8ecf3876ec64cf' + - '29be4291dd5428fc0301963cdf6939978846e2c35fd38fcb70c64296a929f166ef6e4e91045712c2' + - '0108b8d68df7923112220a2027707399474d417bf6aae4baa3d73b285bf728353bc3e156b0e32461' + - 'ebb48f8c1a940108b8d68df7923112460a440a40fb96fa38c3f013830abb61cf6b39776e0475eb13' + - '79c66013569c3d2daecdd48c7fbee945dcdbdc5717d1f4ffd342c4d3f1b7215912829751a94e3ae1' + - '1007e0a110011a430a4104952b7158cfe819d92743a4132e2e3ae867d72f6a08292aebf471d0a7a2' + - '907f3e9947719033e20edc9ca9665874bd88c64c6b62c01928065f6069c5c80c699924' - ) - ).v1! - ) + "0x0a88030ac001088cd68df7923112220a209057f8d813314a2aae74e6c4c30f909c1c496b6037ce32" + + "a12c613558a8e961681a9201088cd68df7923112440a420a40501ae9b4f75d5bb5bae3ca4ecfda4e" + + "de9edc5a9b7fc2d56dc7325b837957c23235cc3005b46bb9ef485f106404dcf71247097ed5096355" + + "90f4b7987b833d03661a430a4104e61a7ae511567f4a2b5551221024b6932d6cdb8ecf3876ec64cf" + + "29be4291dd5428fc0301963cdf6939978846e2c35fd38fcb70c64296a929f166ef6e4e91045712c2" + + "0108b8d68df7923112220a2027707399474d417bf6aae4baa3d73b285bf728353bc3e156b0e32461" + + "ebb48f8c1a940108b8d68df7923112460a440a40fb96fa38c3f013830abb61cf6b39776e0475eb13" + + "79c66013569c3d2daecdd48c7fbee945dcdbdc5717d1f4ffd342c4d3f1b7215912829751a94e3ae1" + + "1007e0a110011a430a4104952b7158cfe819d92743a4132e2e3ae867d72f6a08292aebf471d0a7a2" + + "907f3e9947719033e20edc9ca9665874bd88c64c6b62c01928065f6069c5c80c699924", + ), + ).v1!, + ); bobKeystore = await InMemoryKeystore.create( bobKeys, - InMemoryPersistence.create() - ) + InMemoryPersistence.create(), + ); expect(await aliceKeystore.getAccountAddress()).toEqual( - '0xF56d1F3b1290204441Cb3843C2Cac1C2f5AEd690' - ) // alice + "0xF56d1F3b1290204441Cb3843C2Cac1C2f5AEd690", + ); // alice expect(bobKeys.getPublicKeyBundle().walletSignatureAddress()).toEqual( - '0x3De402A325323Bb97f00cE3ad5bFAc96A11F9A34' - ) // bob + "0x3De402A325323Bb97f00cE3ad5bFAc96A11F9A34", + ); // bob const aliceInvite = await aliceKeystore.createInvite({ recipient: SignedPublicKeyBundle.fromLegacyBundle( - bobKeys.getPublicKeyBundle() + bobKeys.getPublicKeyBundle(), ), createdNs: dateToNs(new Date()), context: { - conversationId: 'test', + conversationId: "test", metadata: {}, }, consentProof: undefined, - }) + }); expect(aliceInvite.conversation!.topic).toEqual( - '/xmtp/0/m-4b52be1e8567d72d0bc407debe2d3c7fca2ae93a47e58c3f9b5c5068aff80ec5/proto' - ) + "/xmtp/0/m-4b52be1e8567d72d0bc407debe2d3c7fca2ae93a47e58c3f9b5c5068aff80ec5/proto", + ); const bobInvite = await bobKeystore.createInvite({ recipient: SignedPublicKeyBundle.fromLegacyBundle( - aliceKeys.getPublicKeyBundle() + aliceKeys.getPublicKeyBundle(), ), createdNs: dateToNs(new Date()), context: { - conversationId: 'test', + conversationId: "test", metadata: {}, }, consentProof: undefined, - }) + }); expect(bobInvite.conversation!.topic).toEqual( - '/xmtp/0/m-4b52be1e8567d72d0bc407debe2d3c7fca2ae93a47e58c3f9b5c5068aff80ec5/proto' - ) - }) + "/xmtp/0/m-4b52be1e8567d72d0bc407debe2d3c7fca2ae93a47e58c3f9b5c5068aff80ec5/proto", + ); + }); - it('uses deterministic topic w/ conversation ID', async () => { + it("uses deterministic topic w/ conversation ID", async () => { const recipient = SignedPublicKeyBundle.fromLegacyBundle( - bobKeys.getPublicKeyBundle() - ) - const baseTime = new Date() + bobKeys.getPublicKeyBundle(), + ); + const baseTime = new Date(); const timestamps = Array.from( { length: 25 }, - (_, i) => new Date(baseTime.getTime() + i) - ) + (_, i) => new Date(baseTime.getTime() + i), + ); // Shuffle the order they go into the store - const shuffled = [...timestamps].sort(() => Math.random() - 0.5) + const shuffled = [...timestamps].sort(() => Math.random() - 0.5); - const responses: CreateInviteResponse[] = [] + const responses: CreateInviteResponse[] = []; await Promise.all( shuffled.map(async (createdAt) => { const response = await aliceKeystore.createInvite({ recipient, createdNs: dateToNs(createdAt), context: { - conversationId: 'test', + conversationId: "test", metadata: {}, }, consentProof: undefined, - }) + }); - responses.push(response) + responses.push(response); - return response - }) - ) + return response; + }), + ); - const firstResponse: CreateInviteResponse = responses[0] - const topicName = firstResponse.conversation!.topic + const firstResponse: CreateInviteResponse = responses[0]; + const topicName = firstResponse.conversation!.topic; expect( responses.filter((response) => { - return response.conversation!.topic === topicName - }) - ).toHaveLength(25) - }) + return response.conversation!.topic === topicName; + }), + ).toHaveLength(25); + }); - it('creates deterministic topics bidirectionally', async () => { + it("creates deterministic topics bidirectionally", async () => { const aliceInvite = await aliceKeystore.createInvite({ recipient: SignedPublicKeyBundle.fromLegacyBundle( - bobKeys.getPublicKeyBundle() + bobKeys.getPublicKeyBundle(), ), createdNs: dateToNs(new Date()), context: undefined, consentProof: undefined, - }) + }); const bobInvite = await bobKeystore.createInvite({ recipient: SignedPublicKeyBundle.fromLegacyBundle( - aliceKeys.getPublicKeyBundle() + aliceKeys.getPublicKeyBundle(), ), createdNs: dateToNs(new Date()), context: undefined, consentProof: undefined, - }) + }); expect( await aliceKeys.sharedSecret( bobKeys.getPublicKeyBundle(), aliceKeys.getCurrentPreKey().publicKey, - false - ) + false, + ), ).toEqual( await bobKeys.sharedSecret( aliceKeys.getPublicKeyBundle(), bobKeys.getCurrentPreKey().publicKey, - true - ) - ) + true, + ), + ); expect(aliceInvite.conversation!.topic).toEqual( - bobInvite.conversation!.topic - ) - }) - }) - - describe('createAuthToken', () => { - it('creates an auth token', async () => { - const authToken = new Token(await aliceKeystore.createAuthToken({})) - expect(authToken.authDataBytes).toBeDefined() - expect(Long.isLong(authToken.authData.createdNs)).toBe(true) - expect(authToken.authDataSignature).toBeDefined() - expect(authToken.identityKey?.secp256k1Uncompressed).toBeDefined() - expect(authToken.identityKey?.signature).toBeDefined() - }) - - it('creates an auth token with a defined time', async () => { - const definedTime = new Date(+new Date() - 5000) + bobInvite.conversation!.topic, + ); + }); + }); + + describe("createAuthToken", () => { + it("creates an auth token", async () => { + const authToken = new Token(await aliceKeystore.createAuthToken({})); + expect(authToken.authDataBytes).toBeDefined(); + expect(Long.isLong(authToken.authData.createdNs)).toBe(true); + expect(authToken.authDataSignature).toBeDefined(); + expect(authToken.identityKey?.secp256k1Uncompressed).toBeDefined(); + expect(authToken.identityKey?.signature).toBeDefined(); + }); + + it("creates an auth token with a defined time", async () => { + const definedTime = new Date(+new Date() - 5000); const token = new Token( await aliceKeystore.createAuthToken({ timestampNs: dateToNs(definedTime), - }) - ) - expect(token.ageMs).toBeGreaterThan(5000) - }) - }) - - describe('getPublicKeyBundle', () => { - it('can retrieve a valid bundle', async () => { - const bundle = await aliceKeystore.getPublicKeyBundle() - const wrappedBundle = SignedPublicKeyBundle.fromLegacyBundle(bundle) + }), + ); + expect(token.ageMs).toBeGreaterThan(5000); + }); + }); + + describe("getPublicKeyBundle", () => { + it("can retrieve a valid bundle", async () => { + const bundle = await aliceKeystore.getPublicKeyBundle(); + const wrappedBundle = SignedPublicKeyBundle.fromLegacyBundle(bundle); expect( wrappedBundle.equals( - SignedPublicKeyBundle.fromLegacyBundle(aliceKeys.getPublicKeyBundle()) - ) - ) - }) - }) - - describe('getAccountAddress', () => { - it('returns the wallet address', async () => { + SignedPublicKeyBundle.fromLegacyBundle( + aliceKeys.getPublicKeyBundle(), + ), + ), + ); + }); + }); + + describe("getAccountAddress", () => { + it("returns the wallet address", async () => { const aliceAddress = aliceKeys .getPublicKeyBundle() - .walletSignatureAddress() - const returnedAddress = await aliceKeystore.getAccountAddress() + .walletSignatureAddress(); + const returnedAddress = await aliceKeystore.getAccountAddress(); - expect(aliceAddress).toEqual(returnedAddress) - }) - }) + expect(aliceAddress).toEqual(returnedAddress); + }); + }); - describe('lookupTopic', () => { - it('looks up a topic that exists', async () => { - const { created, sealed, invite } = await buildInvite() + describe("lookupTopic", () => { + it("looks up a topic that exists", async () => { + const { created, sealed, invite } = await buildInvite(); - const sealedBytes = sealed.toBytes() - const envelope = buildProtoEnvelope(sealedBytes, 'foo', created) + const sealedBytes = sealed.toBytes(); + const envelope = buildProtoEnvelope(sealedBytes, "foo", created); const { responses: [aliceResponse], } = await aliceKeystore.saveInvites({ requests: [envelope], - }) + }); if (aliceResponse.error) { - throw aliceResponse + throw aliceResponse; } - const lookupResult = aliceKeystore.lookupTopic(invite.topic) + const lookupResult = aliceKeystore.lookupTopic(invite.topic); expect( - lookupResult?.invitation?.aes256GcmHkdfSha256?.keyMaterial - ).toEqual(invite.aes256GcmHkdfSha256.keyMaterial) - }) - - it('returns undefined for non-existent topic', async () => { - const lookupResult = aliceKeystore.lookupTopic('foo') - expect(lookupResult).toBeUndefined() - }) - }) - - describe('getRefreshJob/setRefreshJob', () => { - it('returns 0 value when empty', async () => { + lookupResult?.invitation?.aes256GcmHkdfSha256?.keyMaterial, + ).toEqual(invite.aes256GcmHkdfSha256.keyMaterial); + }); + + it("returns undefined for non-existent topic", async () => { + const lookupResult = aliceKeystore.lookupTopic("foo"); + expect(lookupResult).toBeUndefined(); + }); + }); + + describe("getRefreshJob/setRefreshJob", () => { + it("returns 0 value when empty", async () => { const job = await aliceKeystore.getRefreshJob( keystore.GetRefreshJobRequest.fromPartial({ jobType: keystore.JobType.JOB_TYPE_REFRESH_V1, - }) - ) - expect(job.lastRunNs.equals(Long.fromNumber(0))).toBeTruthy() - }) + }), + ); + expect(job.lastRunNs.equals(Long.fromNumber(0))).toBeTruthy(); + }); - it('returns a value when set', async () => { - const lastRunNs = dateToNs(new Date()) + it("returns a value when set", async () => { + const lastRunNs = dateToNs(new Date()); await aliceKeystore.setRefreshJob( keystore.SetRefeshJobRequest.fromPartial({ jobType: keystore.JobType.JOB_TYPE_REFRESH_V1, lastRunNs, - }) - ) + }), + ); const result = await aliceKeystore.getRefreshJob( keystore.GetRefreshJobRequest.fromPartial({ jobType: keystore.JobType.JOB_TYPE_REFRESH_V1, - }) - ) - expect(result.lastRunNs.equals(lastRunNs)).toBeTruthy() + }), + ); + expect(result.lastRunNs.equals(lastRunNs)).toBeTruthy(); const otherJob = await aliceKeystore.getRefreshJob( keystore.GetRefreshJobRequest.fromPartial({ jobType: keystore.JobType.JOB_TYPE_REFRESH_V2, - }) - ) - expect(otherJob.lastRunNs.equals(Long.fromNumber(0))).toBeTruthy() - }) + }), + ); + expect(otherJob.lastRunNs.equals(Long.fromNumber(0))).toBeTruthy(); + }); - it('overwrites a value when set', async () => { - const lastRunNs = dateToNs(new Date()) + it("overwrites a value when set", async () => { + const lastRunNs = dateToNs(new Date()); await aliceKeystore.setRefreshJob( keystore.SetRefeshJobRequest.fromPartial({ jobType: keystore.JobType.JOB_TYPE_REFRESH_V1, lastRunNs: Long.fromNumber(5), - }) - ) + }), + ); await aliceKeystore.setRefreshJob( keystore.SetRefeshJobRequest.fromPartial({ jobType: keystore.JobType.JOB_TYPE_REFRESH_V1, lastRunNs, - }) - ) + }), + ); expect( ( await aliceKeystore.getRefreshJob( keystore.GetRefreshJobRequest.fromPartial({ jobType: keystore.JobType.JOB_TYPE_REFRESH_V1, - }) + }), ) - ).lastRunNs.equals(lastRunNs) - ).toBeTruthy() - }) - }) - - describe('getV2ConversationHmacKeys', () => { - it('returns all conversation HMAC keys', async () => { - const baseTime = new Date() + ).lastRunNs.equals(lastRunNs), + ).toBeTruthy(); + }); + }); + + describe("getV2ConversationHmacKeys", () => { + it("returns all conversation HMAC keys", async () => { + const baseTime = new Date(); const timestamps = Array.from( { length: 5 }, - (_, i) => new Date(baseTime.getTime() + i) - ) + (_, i) => new Date(baseTime.getTime() + i), + ); const invites = await Promise.all( [...timestamps].map(async (createdAt) => { - const keys = await PrivateKeyBundleV1.generate(newWallet()) + const keys = await PrivateKeyBundleV1.generate(newWallet()); const recipient = SignedPublicKeyBundle.fromLegacyBundle( - keys.getPublicKeyBundle() - ) + keys.getPublicKeyBundle(), + ); return aliceKeystore.createInvite({ recipient, createdNs: dateToNs(createdAt), context: undefined, consentProof: undefined, - }) - }) - ) + }); + }), + ); const thirtyDayPeriodsSinceEpoch = Math.floor( - Date.now() / 1000 / 60 / 60 / 24 / 30 - ) + Date.now() / 1000 / 60 / 60 / 24 / 30, + ); const periods = [ thirtyDayPeriodsSinceEpoch - 1, thirtyDayPeriodsSinceEpoch, thirtyDayPeriodsSinceEpoch + 1, - ] + ]; - const { hmacKeys } = await aliceKeystore.getV2ConversationHmacKeys() + const { hmacKeys } = await aliceKeystore.getV2ConversationHmacKeys(); - const topics = Object.keys(hmacKeys) + const topics = Object.keys(hmacKeys); invites.forEach((invite) => { - expect(topics.includes(invite.conversation!.topic)).toBeTruthy() - }) + expect(topics.includes(invite.conversation!.topic)).toBeTruthy(); + }); const topicHmacs: { - [topic: string]: Uint8Array - } = {} - const headerBytes = new Uint8Array(10) + [topic: string]: Uint8Array; + } = {}; + const headerBytes = new Uint8Array(10); await Promise.all( invites.map(async (invite) => { - const topic = invite.conversation!.topic - const payload = new TextEncoder().encode('Hello, world!') + const topic = invite.conversation!.topic; + const payload = new TextEncoder().encode("Hello, world!"); const { responses: [encrypted], @@ -945,101 +951,101 @@ describe('InMemoryKeystore', () => { headerBytes, }, ], - }) + }); if (encrypted.error) { - throw encrypted.error + throw encrypted.error; } - const topicData = aliceKeystore.lookupTopic(topic) - const keyMaterial = getKeyMaterial(topicData!.invitation) - const info = `${thirtyDayPeriodsSinceEpoch}-${aliceKeystore.walletAddress}` + const topicData = aliceKeystore.lookupTopic(topic); + const keyMaterial = getKeyMaterial(topicData!.invitation); + const info = `${thirtyDayPeriodsSinceEpoch}-${aliceKeystore.walletAddress}`; const hmac = await generateHmacSignature( keyMaterial, new TextEncoder().encode(info), - headerBytes - ) + headerBytes, + ); - topicHmacs[topic] = hmac - }) - ) + topicHmacs[topic] = hmac; + }), + ); await Promise.all( Object.keys(hmacKeys).map(async (topic) => { - const hmacData = hmacKeys[topic] + const hmacData = hmacKeys[topic]; await Promise.all( hmacData.values.map( async ({ hmacKey, thirtyDayPeriodsSinceEpoch }, idx) => { - expect(thirtyDayPeriodsSinceEpoch).toBe(periods[idx]) + expect(thirtyDayPeriodsSinceEpoch).toBe(periods[idx]); const valid = await verifyHmacSignature( await importHmacKey(hmacKey), topicHmacs[topic], - headerBytes - ) - expect(valid).toBe(idx === 1) - } - ) - ) - }) - ) - }) - - it('returns specific conversation HMAC keys', async () => { - const baseTime = new Date() + headerBytes, + ); + expect(valid).toBe(idx === 1); + }, + ), + ); + }), + ); + }); + + it("returns specific conversation HMAC keys", async () => { + const baseTime = new Date(); const timestamps = Array.from( { length: 10 }, - (_, i) => new Date(baseTime.getTime() + i) - ) + (_, i) => new Date(baseTime.getTime() + i), + ); const invites = await Promise.all( [...timestamps].map(async (createdAt) => { - const keys = await PrivateKeyBundleV1.generate(newWallet()) + const keys = await PrivateKeyBundleV1.generate(newWallet()); const recipient = SignedPublicKeyBundle.fromLegacyBundle( - keys.getPublicKeyBundle() - ) + keys.getPublicKeyBundle(), + ); return aliceKeystore.createInvite({ recipient, createdNs: dateToNs(createdAt), context: undefined, consentProof: undefined, - }) - }) - ) + }); + }), + ); const thirtyDayPeriodsSinceEpoch = Math.floor( - Date.now() / 1000 / 60 / 60 / 24 / 30 - ) + Date.now() / 1000 / 60 / 60 / 24 / 30, + ); const periods = [ thirtyDayPeriodsSinceEpoch - 1, thirtyDayPeriodsSinceEpoch, thirtyDayPeriodsSinceEpoch + 1, - ] + ]; - const randomInvites = invites.slice(3, 8) + const randomInvites = invites.slice(3, 8); const { hmacKeys } = await aliceKeystore.getV2ConversationHmacKeys({ topics: randomInvites.map((invite) => invite.conversation!.topic), - }) + }); - const topics = Object.keys(hmacKeys) - expect(topics.length).toBe(randomInvites.length) + const topics = Object.keys(hmacKeys); + expect(topics.length).toBe(randomInvites.length); randomInvites.forEach((invite) => { - expect(topics.includes(invite.conversation!.topic)).toBeTruthy() - }) + expect(topics.includes(invite.conversation!.topic)).toBeTruthy(); + }); const topicHmacs: { - [topic: string]: Uint8Array - } = {} - const headerBytes = new Uint8Array(10) + [topic: string]: Uint8Array; + } = {}; + const headerBytes = new Uint8Array(10); await Promise.all( randomInvites.map(async (invite) => { - const topic = invite.conversation!.topic - const payload = new TextEncoder().encode('Hello, world!') + const topic = invite.conversation!.topic; + const payload = new TextEncoder().encode("Hello, world!"); const { responses: [encrypted], @@ -1051,44 +1057,44 @@ describe('InMemoryKeystore', () => { headerBytes, }, ], - }) + }); if (encrypted.error) { - throw encrypted.error + throw encrypted.error; } - const topicData = aliceKeystore.lookupTopic(topic) - const keyMaterial = getKeyMaterial(topicData!.invitation) - const info = `${thirtyDayPeriodsSinceEpoch}-${aliceKeystore.walletAddress}` + const topicData = aliceKeystore.lookupTopic(topic); + const keyMaterial = getKeyMaterial(topicData!.invitation); + const info = `${thirtyDayPeriodsSinceEpoch}-${aliceKeystore.walletAddress}`; const hmac = await generateHmacSignature( keyMaterial, new TextEncoder().encode(info), - headerBytes - ) + headerBytes, + ); - topicHmacs[topic] = hmac - }) - ) + topicHmacs[topic] = hmac; + }), + ); await Promise.all( Object.keys(hmacKeys).map(async (topic) => { - const hmacData = hmacKeys[topic] + const hmacData = hmacKeys[topic]; await Promise.all( hmacData.values.map( async ({ hmacKey, thirtyDayPeriodsSinceEpoch }, idx) => { - expect(thirtyDayPeriodsSinceEpoch).toBe(periods[idx]) + expect(thirtyDayPeriodsSinceEpoch).toBe(periods[idx]); const valid = await verifyHmacSignature( await importHmacKey(hmacKey), topicHmacs[topic], - headerBytes - ) - expect(valid).toBe(idx === 1) - } - ) - ) - }) - ) - }) - }) -}) + headerBytes, + ); + expect(valid).toBe(idx === 1); + }, + ), + ); + }), + ); + }); + }); +}); diff --git a/packages/js-sdk/test/keystore/conversationStores.test.ts b/packages/js-sdk/test/keystore/conversationStores.test.ts index 4ab3a2d1d..1283bf2f1 100644 --- a/packages/js-sdk/test/keystore/conversationStores.test.ts +++ b/packages/js-sdk/test/keystore/conversationStores.test.ts @@ -1,14 +1,14 @@ -import crypto from '@/crypto/crypto' +import crypto from "@/crypto/crypto"; import { V1Store, V2Store, type AddRequest, -} from '@/keystore/conversationStores' -import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' -import { dateToNs } from '@/utils/date' +} from "@/keystore/conversationStores"; +import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; +import { dateToNs } from "@/utils/date"; const buildAddRequest = (): AddRequest => { - const topic = crypto.getRandomValues(new Uint8Array(32)).toString() + const topic = crypto.getRandomValues(new Uint8Array(32)).toString(); return { topic, createdNs: dateToNs(new Date()).toUnsigned(), @@ -19,137 +19,137 @@ const buildAddRequest = (): AddRequest => { keyMaterial: crypto.getRandomValues(new Uint8Array(32)), }, context: { - conversationId: 'foo', + conversationId: "foo", metadata: {}, }, consentProof: undefined, }, - } -} - -describe('V2Store', () => { - it('can add and retrieve invites without persistence', async () => { - const store = await V2Store.create(InMemoryPersistence.create()) - const addRequest = buildAddRequest() - await store.add([addRequest]) - - const { topic, ...topicData } = addRequest - const result = store.lookup(topic) - expect(result).not.toBeNull() - expect(result).toEqual(topicData) - }) - - it('can add and retrieve invites with persistence', async () => { - const store = await V2Store.create(InMemoryPersistence.create()) - const topicData = buildAddRequest() - await store.add([topicData]) - - const result = store.lookup(topicData.topic) - expect(result?.invitation).toEqual(topicData.invitation) - expect(result?.peerAddress).toEqual(topicData.peerAddress) - expect(result?.createdNs.eq(topicData.createdNs)).toBeTruthy() - }) - - it('returns undefined when no match exists', async () => { - const store = await V2Store.create(InMemoryPersistence.create()) - const result = store.lookup('foo') - expect(result).toBeUndefined() - }) - - it('persists data between instances', async () => { - const persistence = InMemoryPersistence.create() - const store = await V2Store.create(persistence) - const topicData = buildAddRequest() - await store.add([topicData]) - - const result = store.lookup(topicData.topic) - expect(result?.invitation).toEqual(topicData.invitation) - expect(result?.createdNs.eq(topicData.createdNs)).toBeTruthy() - expect(result?.peerAddress).toEqual(topicData.peerAddress) - - const store2 = await V2Store.create(persistence) - const result2 = store2.lookup(topicData.topic) - expect(result2).toEqual(result) - }) - - it('handles concurrent access', async () => { - const persistence = InMemoryPersistence.create() - const store1 = await V2Store.create(persistence) - const store2 = await V2Store.create(persistence) + }; +}; + +describe("V2Store", () => { + it("can add and retrieve invites without persistence", async () => { + const store = await V2Store.create(InMemoryPersistence.create()); + const addRequest = buildAddRequest(); + await store.add([addRequest]); + + const { topic, ...topicData } = addRequest; + const result = store.lookup(topic); + expect(result).not.toBeNull(); + expect(result).toEqual(topicData); + }); + + it("can add and retrieve invites with persistence", async () => { + const store = await V2Store.create(InMemoryPersistence.create()); + const topicData = buildAddRequest(); + await store.add([topicData]); + + const result = store.lookup(topicData.topic); + expect(result?.invitation).toEqual(topicData.invitation); + expect(result?.peerAddress).toEqual(topicData.peerAddress); + expect(result?.createdNs.eq(topicData.createdNs)).toBeTruthy(); + }); + + it("returns undefined when no match exists", async () => { + const store = await V2Store.create(InMemoryPersistence.create()); + const result = store.lookup("foo"); + expect(result).toBeUndefined(); + }); + + it("persists data between instances", async () => { + const persistence = InMemoryPersistence.create(); + const store = await V2Store.create(persistence); + const topicData = buildAddRequest(); + await store.add([topicData]); + + const result = store.lookup(topicData.topic); + expect(result?.invitation).toEqual(topicData.invitation); + expect(result?.createdNs.eq(topicData.createdNs)).toBeTruthy(); + expect(result?.peerAddress).toEqual(topicData.peerAddress); + + const store2 = await V2Store.create(persistence); + const result2 = store2.lookup(topicData.topic); + expect(result2).toEqual(result); + }); + + it("handles concurrent access", async () => { + const persistence = InMemoryPersistence.create(); + const store1 = await V2Store.create(persistence); + const store2 = await V2Store.create(persistence); // Add an item to store 1 - await store1.add([buildAddRequest()]) - expect(store1.topics).toHaveLength(1) - expect(store2.topics).toHaveLength(0) - await store2.add([buildAddRequest()]) - expect(store2.topics).toHaveLength(2) - expect(await store2.getRevision()).toBe(2) - }) - - it('correctly handles revisions', async () => { - const persistence = InMemoryPersistence.create() - const store = await V2Store.create(persistence) + await store1.add([buildAddRequest()]); + expect(store1.topics).toHaveLength(1); + expect(store2.topics).toHaveLength(0); + await store2.add([buildAddRequest()]); + expect(store2.topics).toHaveLength(2); + expect(await store2.getRevision()).toBe(2); + }); + + it("correctly handles revisions", async () => { + const persistence = InMemoryPersistence.create(); + const store = await V2Store.create(persistence); for (let i = 0; i < 10; i++) { - await store.add([buildAddRequest()]) - expect(await store.getRevision()).toBe(i + 1) + await store.add([buildAddRequest()]); + expect(await store.getRevision()).toBe(i + 1); } - const newStore = await V2Store.create(persistence) - expect(await newStore.getRevision()).toBe(10) - }) - - it('omits bad data', async () => { - const store = await V2Store.create(InMemoryPersistence.create()) - const revision = await store.getRevision() - const topicData = { ...buildAddRequest(), invitation: undefined } - await store.add([topicData]) - expect(await store.getRevision()).toBe(revision) - expect(await store.topics).toHaveLength(0) - }) -}) - -describe('v1Store', () => { + const newStore = await V2Store.create(persistence); + expect(await newStore.getRevision()).toBe(10); + }); + + it("omits bad data", async () => { + const store = await V2Store.create(InMemoryPersistence.create()); + const revision = await store.getRevision(); + const topicData = { ...buildAddRequest(), invitation: undefined }; + await store.add([topicData]); + expect(await store.getRevision()).toBe(revision); + expect(await store.topics).toHaveLength(0); + }); +}); + +describe("v1Store", () => { const buildV1 = (): AddRequest => { - const peerAddress = crypto.getRandomValues(new Uint8Array(32)).toString() + const peerAddress = crypto.getRandomValues(new Uint8Array(32)).toString(); return { peerAddress, createdNs: dateToNs(new Date()).toUnsigned(), invitation: undefined, topic: `xmtp/${peerAddress}}`, - } - } - - it('can add and retrieve v1 convos', async () => { - const store = await V1Store.create(InMemoryPersistence.create()) - const addReq = buildV1() - await store.add([addReq]) - - const value = store.lookup(addReq.topic) - expect(value).toBeTruthy() - }) - - it('can round trip to persistence', async () => { - const persistence = InMemoryPersistence.create() - const store = await V1Store.create(persistence) - const requests = [buildV1(), buildV1()] - await store.add(requests) - const valuesFromFirstStore = store.topics - expect(valuesFromFirstStore).toHaveLength(2) - - const store2 = await V1Store.create(persistence) - const valuesFromSecondStore = store2.topics - expect(valuesFromFirstStore).toEqual(valuesFromSecondStore) - }) - - it('handles concurrent access', async () => { - const persistence = InMemoryPersistence.create() - const store1 = await V2Store.create(persistence) - const store2 = await V2Store.create(persistence) + }; + }; + + it("can add and retrieve v1 convos", async () => { + const store = await V1Store.create(InMemoryPersistence.create()); + const addReq = buildV1(); + await store.add([addReq]); + + const value = store.lookup(addReq.topic); + expect(value).toBeTruthy(); + }); + + it("can round trip to persistence", async () => { + const persistence = InMemoryPersistence.create(); + const store = await V1Store.create(persistence); + const requests = [buildV1(), buildV1()]; + await store.add(requests); + const valuesFromFirstStore = store.topics; + expect(valuesFromFirstStore).toHaveLength(2); + + const store2 = await V1Store.create(persistence); + const valuesFromSecondStore = store2.topics; + expect(valuesFromFirstStore).toEqual(valuesFromSecondStore); + }); + + it("handles concurrent access", async () => { + const persistence = InMemoryPersistence.create(); + const store1 = await V2Store.create(persistence); + const store2 = await V2Store.create(persistence); // Add an item to store 1 - await store1.add([buildAddRequest()]) - expect(store1.topics).toHaveLength(1) - expect(store2.topics).toHaveLength(0) - await store2.add([buildAddRequest()]) - expect(store2.topics).toHaveLength(2) - expect(await store1.getRevision()).toBe(2) - expect(await store2.getRevision()).toBe(2) - }) -}) + await store1.add([buildAddRequest()]); + expect(store1.topics).toHaveLength(1); + expect(store2.topics).toHaveLength(0); + await store2.add([buildAddRequest()]); + expect(store2.topics).toHaveLength(2); + expect(await store1.getRevision()).toBe(2); + expect(await store2.getRevision()).toBe(2); + }); +}); diff --git a/packages/js-sdk/test/keystore/encryption.test.ts b/packages/js-sdk/test/keystore/encryption.test.ts index 0cc91e079..a179b8da7 100644 --- a/packages/js-sdk/test/keystore/encryption.test.ts +++ b/packages/js-sdk/test/keystore/encryption.test.ts @@ -1,79 +1,79 @@ -import { Wallet } from 'ethers' -import Ciphertext from '@/crypto/Ciphertext' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import { equalBytes } from '@/crypto/utils' -import { decryptV1, encryptV1 } from '@/keystore/encryption' -import InMemoryKeystore from '@/keystore/InMemoryKeystore' -import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' -import type { KeystoreInterface } from '@/keystore/rpcDefinitions' -import { MessageV1 } from '@/Message' -import { newWallet } from '@test/helpers' +import { Wallet } from "ethers"; +import Ciphertext from "@/crypto/Ciphertext"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import { equalBytes } from "@/crypto/utils"; +import { decryptV1, encryptV1 } from "@/keystore/encryption"; +import InMemoryKeystore from "@/keystore/InMemoryKeystore"; +import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; +import type { KeystoreInterface } from "@/keystore/rpcDefinitions"; +import { MessageV1 } from "@/Message"; +import { newWallet } from "@test/helpers"; -describe('encryption primitives', () => { - let aliceKeys: PrivateKeyBundleV1 - let aliceWallet: Wallet - let aliceKeystore: KeystoreInterface - let bobKeys: PrivateKeyBundleV1 - let bobWallet: Wallet +describe("encryption primitives", () => { + let aliceKeys: PrivateKeyBundleV1; + let aliceWallet: Wallet; + let aliceKeystore: KeystoreInterface; + let bobKeys: PrivateKeyBundleV1; + let bobWallet: Wallet; beforeEach(async () => { - aliceWallet = newWallet() - aliceKeys = await PrivateKeyBundleV1.generate(aliceWallet) + aliceWallet = newWallet(); + aliceKeys = await PrivateKeyBundleV1.generate(aliceWallet); aliceKeystore = await InMemoryKeystore.create( aliceKeys, - InMemoryPersistence.create() - ) - bobWallet = newWallet() - bobKeys = await PrivateKeyBundleV1.generate(bobWallet) - }) + InMemoryPersistence.create(), + ); + bobWallet = newWallet(); + bobKeys = await PrivateKeyBundleV1.generate(bobWallet); + }); - describe('decryptV1', () => { - it('should decrypt a valid payload', async () => { - const messageText = 'Hello, world!' - const message = new TextEncoder().encode(messageText) + describe("decryptV1", () => { + it("should decrypt a valid payload", async () => { + const messageText = "Hello, world!"; + const message = new TextEncoder().encode(messageText); const payload = await MessageV1.encode( aliceKeystore, message, aliceKeys.getPublicKeyBundle(), bobKeys.getPublicKeyBundle(), - new Date() - ) + new Date(), + ); const aliceDecrypted = await decryptV1( aliceKeys, bobKeys.getPublicKeyBundle(), payload.ciphertext, payload.headerBytes, - true - ) - expect(new TextDecoder().decode(aliceDecrypted)).toEqual(messageText) + true, + ); + expect(new TextDecoder().decode(aliceDecrypted)).toEqual(messageText); const bobDecrypted = await decryptV1( bobKeys, aliceKeys.getPublicKeyBundle(), payload.ciphertext, payload.headerBytes, - false - ) - expect(new TextDecoder().decode(bobDecrypted)).toEqual(messageText) + false, + ); + expect(new TextDecoder().decode(bobDecrypted)).toEqual(messageText); - expect(equalBytes(aliceDecrypted, bobDecrypted)).toBeTruthy() - }) + expect(equalBytes(aliceDecrypted, bobDecrypted)).toBeTruthy(); + }); - it('fails to decrypt when wrong keys are used', async () => { - const message = new TextEncoder().encode('should fail') + it("fails to decrypt when wrong keys are used", async () => { + const message = new TextEncoder().encode("should fail"); const payload = await MessageV1.encode( aliceKeystore, message, aliceKeys.getPublicKeyBundle(), bobKeys.getPublicKeyBundle(), - new Date() - ) + new Date(), + ); const charlieKeys = await PrivateKeyBundleV1.generate( - Wallet.createRandom() - ) + Wallet.createRandom(), + ); expect(async () => { await decryptV1( @@ -81,34 +81,34 @@ describe('encryption primitives', () => { bobKeys.getPublicKeyBundle(), payload.ciphertext, payload.headerBytes, - true - ) - }).rejects.toThrow() - }) - }) + true, + ); + }).rejects.toThrow(); + }); + }); - describe('encryptV1', () => { - it('should round trip a valid payload', async () => { - const messageText = 'Hello, world!' - const message = new TextEncoder().encode(messageText) - const headerBytes = new Uint8Array(5) + describe("encryptV1", () => { + it("should round trip a valid payload", async () => { + const messageText = "Hello, world!"; + const message = new TextEncoder().encode(messageText); + const headerBytes = new Uint8Array(5); const ciphertext = await encryptV1( aliceKeys, bobKeys.getPublicKeyBundle(), message, - headerBytes - ) - expect(ciphertext).toBeInstanceOf(Ciphertext) + headerBytes, + ); + expect(ciphertext).toBeInstanceOf(Ciphertext); const decrypted = await decryptV1( aliceKeys, bobKeys.getPublicKeyBundle(), ciphertext, headerBytes, - true - ) - expect(equalBytes(message, decrypted)).toBeTruthy() - }) - }) -}) + true, + ); + expect(equalBytes(message, decrypted)).toBeTruthy(); + }); + }); +}); diff --git a/packages/js-sdk/test/keystore/persistence/EncryptedPersistence.test.ts b/packages/js-sdk/test/keystore/persistence/EncryptedPersistence.test.ts index 9a25d8b15..a98b2ec71 100644 --- a/packages/js-sdk/test/keystore/persistence/EncryptedPersistence.test.ts +++ b/packages/js-sdk/test/keystore/persistence/EncryptedPersistence.test.ts @@ -1,163 +1,163 @@ -import crypto from '@/crypto/crypto' -import type { PrivateKey } from '@/crypto/PrivateKey' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import SignedEciesCiphertext from '@/crypto/SignedEciesCiphertext' -import EncryptedPersistence from '@/keystore/persistence/EncryptedPersistence' -import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' +import crypto from "@/crypto/crypto"; +import type { PrivateKey } from "@/crypto/PrivateKey"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import SignedEciesCiphertext from "@/crypto/SignedEciesCiphertext"; +import EncryptedPersistence from "@/keystore/persistence/EncryptedPersistence"; +import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; -const TEST_KEY = 'test-key' -const TEST_KEY_2 = 'test-key-2' +const TEST_KEY = "test-key"; +const TEST_KEY_2 = "test-key-2"; -describe('EncryptedPersistence', () => { - let privateKey: PrivateKey +describe("EncryptedPersistence", () => { + let privateKey: PrivateKey; beforeEach(async () => { - const bundle = await PrivateKeyBundleV1.generate() - privateKey = bundle.identityKey - }) + const bundle = await PrivateKeyBundleV1.generate(); + privateKey = bundle.identityKey; + }); - it('can encrypt and decrypt a value', async () => { - const data = crypto.getRandomValues(new Uint8Array(128)) - const persistence = InMemoryPersistence.create() + it("can encrypt and decrypt a value", async () => { + const data = crypto.getRandomValues(new Uint8Array(128)); + const persistence = InMemoryPersistence.create(); const encryptedPersistence = new EncryptedPersistence( persistence, - privateKey - ) + privateKey, + ); - await encryptedPersistence.setItem(TEST_KEY, data) - const result = await encryptedPersistence.getItem(TEST_KEY) - expect(result).toEqual(data) + await encryptedPersistence.setItem(TEST_KEY, data); + const result = await encryptedPersistence.getItem(TEST_KEY); + expect(result).toEqual(data); - const rawResult = await persistence.getItem(TEST_KEY) - expect(rawResult).not.toEqual(data) - }) + const rawResult = await persistence.getItem(TEST_KEY); + expect(rawResult).not.toEqual(data); + }); - it('works with arbitrarily sized inputs', async () => { + it("works with arbitrarily sized inputs", async () => { const inputs = [ crypto.getRandomValues(new Uint8Array(32)), crypto.getRandomValues(new Uint8Array(128)), crypto.getRandomValues(new Uint8Array(1024)), - ] + ]; for (const input of inputs) { const encryptedPersistence = new EncryptedPersistence( InMemoryPersistence.create(), - privateKey - ) + privateKey, + ); - await encryptedPersistence.setItem(TEST_KEY, input) - const returnedResult = await encryptedPersistence.getItem(TEST_KEY) - expect(returnedResult).toEqual(input) + await encryptedPersistence.setItem(TEST_KEY, input); + const returnedResult = await encryptedPersistence.getItem(TEST_KEY); + expect(returnedResult).toEqual(input); } - }) + }); - it('uses random values to encrypt repeatedly', async () => { - const data = crypto.getRandomValues(new Uint8Array(128)) - const persistence = InMemoryPersistence.create() + it("uses random values to encrypt repeatedly", async () => { + const data = crypto.getRandomValues(new Uint8Array(128)); + const persistence = InMemoryPersistence.create(); const encryptedPersistence = new EncryptedPersistence( persistence, - privateKey - ) + privateKey, + ); - await encryptedPersistence.setItem(TEST_KEY, data) - await encryptedPersistence.setItem(TEST_KEY_2, data) + await encryptedPersistence.setItem(TEST_KEY, data); + await encryptedPersistence.setItem(TEST_KEY_2, data); const [rawResult1, rawResult2] = await Promise.all([ persistence.getItem(TEST_KEY), persistence.getItem(TEST_KEY_2), - ]) - expect(rawResult1).not.toEqual(rawResult2) - }) + ]); + expect(rawResult1).not.toEqual(rawResult2); + }); - it('catches garbage values', async () => { - const persistence = InMemoryPersistence.create() + it("catches garbage values", async () => { + const persistence = InMemoryPersistence.create(); const encryptedPersistence = new EncryptedPersistence( persistence, - privateKey - ) + privateKey, + ); // Set an unencrypted value of 'garbage' as bytes await persistence.setItem( TEST_KEY, - new Uint8Array([103, 97, 114, 98, 97, 103, 101]) - ) + new Uint8Array([103, 97, 114, 98, 97, 103, 101]), + ); // Expect an error if the ciphertext is tampered with - await expect(encryptedPersistence.getItem(TEST_KEY)).rejects.toThrow() - }) + await expect(encryptedPersistence.getItem(TEST_KEY)).rejects.toThrow(); + }); - it('detects bad mac', async () => { - const data = crypto.getRandomValues(new Uint8Array(128)) - const persistence = InMemoryPersistence.create() + it("detects bad mac", async () => { + const data = crypto.getRandomValues(new Uint8Array(128)); + const persistence = InMemoryPersistence.create(); const encryptedPersistence = new EncryptedPersistence( persistence, - privateKey - ) + privateKey, + ); // Write the value with encryption - await encryptedPersistence.setItem(TEST_KEY, data) + await encryptedPersistence.setItem(TEST_KEY, data); // Read the raw result, change one byte, write it back - const rawResult = await persistence.getItem(TEST_KEY)! - const parsedRawResult = SignedEciesCiphertext.fromBytes(rawResult!) + const rawResult = await persistence.getItem(TEST_KEY)!; + const parsedRawResult = SignedEciesCiphertext.fromBytes(rawResult!); const newCiphertext = { ...parsedRawResult.ciphertext, mac: crypto.getRandomValues(new Uint8Array(32)), - } + }; const newData = await SignedEciesCiphertext.create( newCiphertext, - privateKey - ) - await persistence.setItem(TEST_KEY, newData.toBytes()) + privateKey, + ); + await persistence.setItem(TEST_KEY, newData.toBytes()); // Expect an error if the ciphertext is tampered with await expect(encryptedPersistence.getItem(TEST_KEY)).rejects.toThrow( - 'Bad mac' - ) - }) + "Bad mac", + ); + }); - it('detects bad signature', async () => { - const persistence = InMemoryPersistence.create() + it("detects bad signature", async () => { + const persistence = InMemoryPersistence.create(); const encryptedPersistence = new EncryptedPersistence( persistence, - privateKey - ) - const data = crypto.getRandomValues(new Uint8Array(64)) - await encryptedPersistence.setItem(TEST_KEY, data) - const encryptedBytes = await persistence.getItem(TEST_KEY) - const goodData = SignedEciesCiphertext.fromBytes(encryptedBytes!) + privateKey, + ); + const data = crypto.getRandomValues(new Uint8Array(64)); + await encryptedPersistence.setItem(TEST_KEY, data); + const encryptedBytes = await persistence.getItem(TEST_KEY); + const goodData = SignedEciesCiphertext.fromBytes(encryptedBytes!); const signedBySomeoneElse = await SignedEciesCiphertext.create( goodData.ciphertext, - (await PrivateKeyBundleV1.generate()).identityKey - ) - await persistence.setItem(TEST_KEY, signedBySomeoneElse.toBytes()) + (await PrivateKeyBundleV1.generate()).identityKey, + ); + await persistence.setItem(TEST_KEY, signedBySomeoneElse.toBytes()); expect(encryptedPersistence.getItem(TEST_KEY)).rejects.toThrow( - 'signature validation failed' - ) - }) + "signature validation failed", + ); + }); - it('signed correctly and encrypted incorrectly', async () => { - const persistence = InMemoryPersistence.create() + it("signed correctly and encrypted incorrectly", async () => { + const persistence = InMemoryPersistence.create(); const encryptedPersistence = new EncryptedPersistence( persistence, - privateKey - ) - const data = crypto.getRandomValues(new Uint8Array(64)) - await encryptedPersistence.setItem(TEST_KEY, data) - const encryptedBytes = await persistence.getItem(TEST_KEY) - const goodData = SignedEciesCiphertext.fromBytes(encryptedBytes!) + privateKey, + ); + const data = crypto.getRandomValues(new Uint8Array(64)); + await encryptedPersistence.setItem(TEST_KEY, data); + const encryptedBytes = await persistence.getItem(TEST_KEY); + const goodData = SignedEciesCiphertext.fromBytes(encryptedBytes!); // Replace the ephemeralPublicKey with a valid length, but totally garbage, value const badEcies = { ...goodData.ciphertext, ephemeralPublicKey: crypto.getRandomValues(new Uint8Array(65)), - } + }; const signedBadEcies = await SignedEciesCiphertext.create( badEcies, - privateKey - ) - await persistence.setItem(TEST_KEY, signedBadEcies.toBytes()) + privateKey, + ); + await persistence.setItem(TEST_KEY, signedBadEcies.toBytes()); expect(encryptedPersistence.getItem(TEST_KEY)).rejects.toThrow( - 'Bad public key' - ) - }) -}) + "Bad public key", + ); + }); +}); diff --git a/packages/js-sdk/test/keystore/persistence/LocalStoragePersistence.test.ts b/packages/js-sdk/test/keystore/persistence/LocalStoragePersistence.test.ts index 2c2140f77..e17a0dd4b 100644 --- a/packages/js-sdk/test/keystore/persistence/LocalStoragePersistence.test.ts +++ b/packages/js-sdk/test/keystore/persistence/LocalStoragePersistence.test.ts @@ -1,35 +1,35 @@ import { decodePrivateKeyBundle, PrivateKeyBundleV1, -} from '@/crypto/PrivateKeyBundle' -import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' +} from "@/crypto/PrivateKeyBundle"; +import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; -describe('Persistence', () => { - describe('LocalStoragePersistence', () => { - let persistence: InMemoryPersistence - const key = 'test' +describe("Persistence", () => { + describe("LocalStoragePersistence", () => { + let persistence: InMemoryPersistence; + const key = "test"; beforeEach(async () => { - persistence = InMemoryPersistence.create() - }) + persistence = InMemoryPersistence.create(); + }); - it('can store and retrieve proto objects', async () => { - const pk = await PrivateKeyBundleV1.generate() - const encodedPk = Uint8Array.from(pk.encode()) - await persistence.setItem(key, encodedPk) - const retrieved = await persistence.getItem(key) - expect(retrieved).toBeTruthy() - const decoded = decodePrivateKeyBundle(retrieved as Uint8Array) + it("can store and retrieve proto objects", async () => { + const pk = await PrivateKeyBundleV1.generate(); + const encodedPk = Uint8Array.from(pk.encode()); + await persistence.setItem(key, encodedPk); + const retrieved = await persistence.getItem(key); + expect(retrieved).toBeTruthy(); + const decoded = decodePrivateKeyBundle(retrieved as Uint8Array); if (!(decoded instanceof PrivateKeyBundleV1)) { - throw new Error('Decoded key is not a PrivateKeyBundleV1') + throw new Error("Decoded key is not a PrivateKeyBundleV1"); } expect( - decoded.identityKey.publicKey.equals(pk.identityKey.publicKey) - ).toBeTruthy() - }) + decoded.identityKey.publicKey.equals(pk.identityKey.publicKey), + ).toBeTruthy(); + }); - it('returns null when no object found', async () => { - const result = await persistence.getItem('wrong key') - expect(result).toBeNull() - }) - }) -}) + it("returns null when no object found", async () => { + const result = await persistence.getItem("wrong key"); + expect(result).toBeNull(); + }); + }); +}); diff --git a/packages/js-sdk/test/keystore/persistence/PrefixedPersistence.test.ts b/packages/js-sdk/test/keystore/persistence/PrefixedPersistence.test.ts index a1a2964ad..cbf165dab 100644 --- a/packages/js-sdk/test/keystore/persistence/PrefixedPersistence.test.ts +++ b/packages/js-sdk/test/keystore/persistence/PrefixedPersistence.test.ts @@ -1,16 +1,16 @@ -import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' -import PrefixedPersistence from '@/keystore/persistence/PrefixedPersistence' +import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; +import PrefixedPersistence from "@/keystore/persistence/PrefixedPersistence"; -describe('PrefixedPersistence', () => { - it('correctly adds a prefix to keys', async () => { - const persistence = InMemoryPersistence.create() - const prefixedPersistence = new PrefixedPersistence('foo', persistence) - await prefixedPersistence.setItem('bar', new Uint8Array([1, 2, 3])) +describe("PrefixedPersistence", () => { + it("correctly adds a prefix to keys", async () => { + const persistence = InMemoryPersistence.create(); + const prefixedPersistence = new PrefixedPersistence("foo", persistence); + await prefixedPersistence.setItem("bar", new Uint8Array([1, 2, 3])); - const resultFromPrefixed = await prefixedPersistence.getItem('bar') - expect(resultFromPrefixed).toEqual(new Uint8Array([1, 2, 3])) + const resultFromPrefixed = await prefixedPersistence.getItem("bar"); + expect(resultFromPrefixed).toEqual(new Uint8Array([1, 2, 3])); - const resultFromRaw = await persistence.getItem('foobar') - expect(resultFromRaw).toEqual(new Uint8Array([1, 2, 3])) - }) -}) + const resultFromRaw = await persistence.getItem("foobar"); + expect(resultFromRaw).toEqual(new Uint8Array([1, 2, 3])); + }); +}); diff --git a/packages/js-sdk/test/keystore/persistence/TopicPersistence.test.ts b/packages/js-sdk/test/keystore/persistence/TopicPersistence.test.ts index ecceb7f77..31048e639 100644 --- a/packages/js-sdk/test/keystore/persistence/TopicPersistence.test.ts +++ b/packages/js-sdk/test/keystore/persistence/TopicPersistence.test.ts @@ -1,56 +1,56 @@ -import ApiClient, { ApiUrls } from '@/ApiClient' -import LocalAuthenticator from '@/authn/LocalAuthenticator' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import TopicPersistence from '@/keystore/persistence/TopicPersistence' -import { newWallet } from '@test/helpers' +import ApiClient, { ApiUrls } from "@/ApiClient"; +import LocalAuthenticator from "@/authn/LocalAuthenticator"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import TopicPersistence from "@/keystore/persistence/TopicPersistence"; +import { newWallet } from "@test/helpers"; // We restrict publishing to topics that do not match this pattern -const buildValidKey = (walletAddress: string) => `${walletAddress}/key_bundle` +const buildValidKey = (walletAddress: string) => `${walletAddress}/key_bundle`; -describe('TopicPersistence', () => { - let apiClient: ApiClient - let bundle: PrivateKeyBundleV1 +describe("TopicPersistence", () => { + let apiClient: ApiClient; + let bundle: PrivateKeyBundleV1; beforeEach(async () => { - apiClient = new ApiClient(ApiUrls.local) - bundle = await PrivateKeyBundleV1.generate(newWallet()) - }) - it('round trips items from the store', async () => { - const input = new TextEncoder().encode('hello') + apiClient = new ApiClient(ApiUrls.local); + bundle = await PrivateKeyBundleV1.generate(newWallet()); + }); + it("round trips items from the store", async () => { + const input = new TextEncoder().encode("hello"); const storageKey = buildValidKey( - bundle.identityKey.publicKey.walletSignatureAddress() - ) - apiClient.setAuthenticator(new LocalAuthenticator(bundle.identityKey)) - const store = new TopicPersistence(apiClient) + bundle.identityKey.publicKey.walletSignatureAddress(), + ); + apiClient.setAuthenticator(new LocalAuthenticator(bundle.identityKey)); + const store = new TopicPersistence(apiClient); try { - await store.setItem(storageKey, input) + await store.setItem(storageKey, input); } catch (e) { - console.log('Error setting item', e) + console.log("Error setting item", e); } - const output = await store.getItem(storageKey) - expect(output).toEqual(input) - }) + const output = await store.getItem(storageKey); + expect(output).toEqual(input); + }); - it('returns null for missing items', async () => { - const store = new TopicPersistence(apiClient) + it("returns null for missing items", async () => { + const store = new TopicPersistence(apiClient); const storageKey = buildValidKey( - bundle.identityKey.publicKey.walletSignatureAddress() - ) - expect(await store.getItem(storageKey)).toBeNull() - }) + bundle.identityKey.publicKey.walletSignatureAddress(), + ); + expect(await store.getItem(storageKey)).toBeNull(); + }); - it('allows overwriting of values', async () => { - const firstInput = new TextEncoder().encode('hello') + it("allows overwriting of values", async () => { + const firstInput = new TextEncoder().encode("hello"); const storageKey = buildValidKey( - bundle.identityKey.publicKey.walletSignatureAddress() - ) - const store = new TopicPersistence(apiClient) - store.setAuthenticator(new LocalAuthenticator(bundle.identityKey)) - await store.setItem(storageKey, firstInput) - expect(await store.getItem(storageKey)).toEqual(firstInput) + bundle.identityKey.publicKey.walletSignatureAddress(), + ); + const store = new TopicPersistence(apiClient); + store.setAuthenticator(new LocalAuthenticator(bundle.identityKey)); + await store.setItem(storageKey, firstInput); + expect(await store.getItem(storageKey)).toEqual(firstInput); - const secondInput = new TextEncoder().encode('goodbye') - await store.setItem(storageKey, secondInput) - expect(await store.getItem(storageKey)).toEqual(secondInput) - }) -}) + const secondInput = new TextEncoder().encode("goodbye"); + await store.setItem(storageKey, secondInput); + expect(await store.getItem(storageKey)).toEqual(secondInput); + }); +}); diff --git a/packages/js-sdk/test/keystore/privatePreferencesStore.test.ts b/packages/js-sdk/test/keystore/privatePreferencesStore.test.ts index f30101ace..b5c1400f6 100644 --- a/packages/js-sdk/test/keystore/privatePreferencesStore.test.ts +++ b/packages/js-sdk/test/keystore/privatePreferencesStore.test.ts @@ -1,10 +1,11 @@ -import type { PrivatePreferencesAction } from '@xmtp/proto/ts/dist/types/message_contents/private_preferences.pb' -import crypto from '@/crypto/crypto' -import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' -import { PrivatePreferencesStore } from '@/keystore/privatePreferencesStore' +import type { PrivatePreferencesAction } from "@xmtp/proto/ts/dist/types/message_contents/private_preferences.pb"; +import crypto from "@/crypto/crypto"; +import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; +import { PrivatePreferencesStore } from "@/keystore/privatePreferencesStore"; const generateActionsMap = (hashValue?: string) => { - const hash = hashValue ?? crypto.getRandomValues(new Uint8Array(8)).toString() + const hash = + hashValue ?? crypto.getRandomValues(new Uint8Array(8)).toString(); const action: PrivatePreferencesAction = { allowAddress: { walletAddresses: [crypto.getRandomValues(new Uint8Array(12)).toString()], @@ -24,83 +25,83 @@ const generateActionsMap = (hashValue?: string) => { denyInboxId: { inboxIds: [crypto.getRandomValues(new Uint8Array(12)).toString()], }, - } + }; return { hash, map: new Map([[hash, action]]), - } -} + }; +}; -describe('PrivatePreferencesStore', () => { - it('can add and retrieve actions', async () => { +describe("PrivatePreferencesStore", () => { + it("can add and retrieve actions", async () => { const store = await PrivatePreferencesStore.create( - InMemoryPersistence.create() - ) - const { hash, map } = generateActionsMap() - await store.add(map) + InMemoryPersistence.create(), + ); + const { hash, map } = generateActionsMap(); + await store.add(map); - const result = store.lookup(hash) - expect(result).toEqual(map.get(hash)) - }) + const result = store.lookup(hash); + expect(result).toEqual(map.get(hash)); + }); - it('returns undefined when no match exists', async () => { + it("returns undefined when no match exists", async () => { const store = await PrivatePreferencesStore.create( - InMemoryPersistence.create() - ) - const result = store.lookup('foo') - expect(result).toBeUndefined() - }) + InMemoryPersistence.create(), + ); + const result = store.lookup("foo"); + expect(result).toBeUndefined(); + }); - it('persists data between instances', async () => { - const persistence = InMemoryPersistence.create() - const store = await PrivatePreferencesStore.create(persistence) - const { hash, map } = generateActionsMap() - await store.add(map) + it("persists data between instances", async () => { + const persistence = InMemoryPersistence.create(); + const store = await PrivatePreferencesStore.create(persistence); + const { hash, map } = generateActionsMap(); + await store.add(map); - const result = store.lookup(hash) - expect(result).toEqual(map.get(hash)) + const result = store.lookup(hash); + expect(result).toEqual(map.get(hash)); - const store2 = await PrivatePreferencesStore.create(persistence) - const result2 = store2.lookup(hash) - expect(result2).toEqual(result) - }) + const store2 = await PrivatePreferencesStore.create(persistence); + const result2 = store2.lookup(hash); + expect(result2).toEqual(result); + }); - it('handles concurrent access', async () => { - const persistence = InMemoryPersistence.create() - const store1 = await PrivatePreferencesStore.create(persistence) - const store2 = await PrivatePreferencesStore.create(persistence) - const { map } = generateActionsMap() - await store1.add(map) - expect(store1.actions).toHaveLength(1) - expect(store2.actions).toHaveLength(0) - const { map: map2 } = generateActionsMap() - await store2.add(map2) - expect(store2.actions).toHaveLength(2) - expect(await store2.getRevision()).toBe(2) - }) + it("handles concurrent access", async () => { + const persistence = InMemoryPersistence.create(); + const store1 = await PrivatePreferencesStore.create(persistence); + const store2 = await PrivatePreferencesStore.create(persistence); + const { map } = generateActionsMap(); + await store1.add(map); + expect(store1.actions).toHaveLength(1); + expect(store2.actions).toHaveLength(0); + const { map: map2 } = generateActionsMap(); + await store2.add(map2); + expect(store2.actions).toHaveLength(2); + expect(await store2.getRevision()).toBe(2); + }); - it('correctly handles revisions', async () => { - const persistence = InMemoryPersistence.create() - const store = await PrivatePreferencesStore.create(persistence) + it("correctly handles revisions", async () => { + const persistence = InMemoryPersistence.create(); + const store = await PrivatePreferencesStore.create(persistence); for (let i = 0; i < 10; i++) { - const { map } = generateActionsMap() - await store.add(map) - expect(await store.getRevision()).toBe(i + 1) + const { map } = generateActionsMap(); + await store.add(map); + expect(await store.getRevision()).toBe(i + 1); } - const newStore = await PrivatePreferencesStore.create(persistence) - expect(await newStore.getRevision()).toBe(10) - }) + const newStore = await PrivatePreferencesStore.create(persistence); + expect(await newStore.getRevision()).toBe(10); + }); - it('ignores duplicate actions', async () => { + it("ignores duplicate actions", async () => { const store = await PrivatePreferencesStore.create( - InMemoryPersistence.create() - ) - const { hash, map } = generateActionsMap() - await store.add(map) - const revision = await store.getRevision() - const { map: map2 } = generateActionsMap(hash) - await store.add(map2) - expect(await store.getRevision()).toBe(revision) - expect(store.actions).toHaveLength(1) - }) -}) + InMemoryPersistence.create(), + ); + const { hash, map } = generateActionsMap(); + await store.add(map); + const revision = await store.getRevision(); + const { map: map2 } = generateActionsMap(hash); + await store.add(map2); + expect(await store.getRevision()).toBe(revision); + expect(store.actions).toHaveLength(1); + }); +}); diff --git a/packages/js-sdk/test/keystore/providers/KeyGeneratorKeystoreProvider.test.ts b/packages/js-sdk/test/keystore/providers/KeyGeneratorKeystoreProvider.test.ts index 6d11b6c2a..4410abc1e 100644 --- a/packages/js-sdk/test/keystore/providers/KeyGeneratorKeystoreProvider.test.ts +++ b/packages/js-sdk/test/keystore/providers/KeyGeneratorKeystoreProvider.test.ts @@ -1,60 +1,60 @@ -import { vi } from 'vitest' -import ApiClient, { ApiUrls } from '@/ApiClient' -import { KeystoreProviderUnavailableError } from '@/keystore/providers/errors' -import KeyGeneratorKeystoreProvider from '@/keystore/providers/KeyGeneratorKeystoreProvider' -import type { Signer } from '@/types/Signer' -import { newWallet } from '@test/helpers' -import { testProviderOptions } from './helpers' +import { vi } from "vitest"; +import ApiClient, { ApiUrls } from "@/ApiClient"; +import { KeystoreProviderUnavailableError } from "@/keystore/providers/errors"; +import KeyGeneratorKeystoreProvider from "@/keystore/providers/KeyGeneratorKeystoreProvider"; +import type { Signer } from "@/types/Signer"; +import { newWallet } from "@test/helpers"; +import { testProviderOptions } from "./helpers"; -describe('KeyGeneratorKeystoreProvider', () => { - let wallet: Signer - let apiClient: ApiClient +describe("KeyGeneratorKeystoreProvider", () => { + let wallet: Signer; + let apiClient: ApiClient; beforeEach(() => { - wallet = newWallet() - apiClient = new ApiClient(ApiUrls.local) - }) + wallet = newWallet(); + apiClient = new ApiClient(ApiUrls.local); + }); - it('creates a key when wallet supplied', async () => { - const provider = new KeyGeneratorKeystoreProvider() + it("creates a key when wallet supplied", async () => { + const provider = new KeyGeneratorKeystoreProvider(); const keystore = await provider.newKeystore( testProviderOptions({}), apiClient, - wallet - ) - expect(keystore).toBeDefined() - }) + wallet, + ); + expect(keystore).toBeDefined(); + }); - it('throws KeystoreProviderUnavailableError when no wallet supplied', async () => { - const provider = new KeyGeneratorKeystoreProvider() + it("throws KeystoreProviderUnavailableError when no wallet supplied", async () => { + const provider = new KeyGeneratorKeystoreProvider(); const prom = provider.newKeystore( testProviderOptions({}), apiClient, - undefined - ) - expect(prom).rejects.toThrow(KeystoreProviderUnavailableError) - }) + undefined, + ); + expect(prom).rejects.toThrow(KeystoreProviderUnavailableError); + }); - it('calls preCreateIdentityCallback when supplied', async () => { - const provider = new KeyGeneratorKeystoreProvider() - const preCreateIdentityCallback = vi.fn() + it("calls preCreateIdentityCallback when supplied", async () => { + const provider = new KeyGeneratorKeystoreProvider(); + const preCreateIdentityCallback = vi.fn(); const keystore = await provider.newKeystore( { ...testProviderOptions({}), preCreateIdentityCallback }, apiClient, - wallet - ) - expect(keystore).toBeDefined() - expect(preCreateIdentityCallback).toHaveBeenCalledTimes(1) - }) + wallet, + ); + expect(keystore).toBeDefined(); + expect(preCreateIdentityCallback).toHaveBeenCalledTimes(1); + }); - it('calls preEnableIdentityCallback when supplied', async () => { - const provider = new KeyGeneratorKeystoreProvider() - const preEnableIdentityCallback = vi.fn() + it("calls preEnableIdentityCallback when supplied", async () => { + const provider = new KeyGeneratorKeystoreProvider(); + const preEnableIdentityCallback = vi.fn(); const keystore = await provider.newKeystore( { ...testProviderOptions({}), preEnableIdentityCallback }, apiClient, - wallet - ) - expect(keystore).toBeDefined() - expect(preEnableIdentityCallback).toHaveBeenCalledTimes(1) - }) -}) + wallet, + ); + expect(keystore).toBeDefined(); + expect(preEnableIdentityCallback).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/js-sdk/test/keystore/providers/NetworkKeyManager.test.ts b/packages/js-sdk/test/keystore/providers/NetworkKeyManager.test.ts index c346266ca..8cef664b6 100644 --- a/packages/js-sdk/test/keystore/providers/NetworkKeyManager.test.ts +++ b/packages/js-sdk/test/keystore/providers/NetworkKeyManager.test.ts @@ -1,131 +1,131 @@ -import { vi } from 'vitest' -import ApiClient, { ApiUrls } from '@/ApiClient' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import BrowserStoragePersistence from '@/keystore/persistence/BrowserStoragePersistence' -import PrefixedPersistence from '@/keystore/persistence/PrefixedPersistence' -import TopicPersistence from '@/keystore/persistence/TopicPersistence' -import { buildPersistenceFromOptions } from '@/keystore/providers/helpers' -import NetworkKeyManager from '@/keystore/providers/NetworkKeyManager' -import type { Signer } from '@/types/Signer' -import { newWallet, pollFor, sleep, wrapAsLedgerWallet } from '@test/helpers' -import { testProviderOptions } from './helpers' +import { vi } from "vitest"; +import ApiClient, { ApiUrls } from "@/ApiClient"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import BrowserStoragePersistence from "@/keystore/persistence/BrowserStoragePersistence"; +import PrefixedPersistence from "@/keystore/persistence/PrefixedPersistence"; +import TopicPersistence from "@/keystore/persistence/TopicPersistence"; +import { buildPersistenceFromOptions } from "@/keystore/providers/helpers"; +import NetworkKeyManager from "@/keystore/providers/NetworkKeyManager"; +import type { Signer } from "@/types/Signer"; +import { newWallet, pollFor, sleep, wrapAsLedgerWallet } from "@test/helpers"; +import { testProviderOptions } from "./helpers"; -describe('NetworkKeyManager', () => { - let wallet: Signer - let persistence: TopicPersistence +describe("NetworkKeyManager", () => { + let wallet: Signer; + let persistence: TopicPersistence; beforeEach(async () => { - wallet = newWallet() - persistence = new TopicPersistence(new ApiClient(ApiUrls.local)) - }) + wallet = newWallet(); + persistence = new TopicPersistence(new ApiClient(ApiUrls.local)); + }); - it('round trips', async () => { - const manager = new NetworkKeyManager(wallet, persistence) - const bundle = await PrivateKeyBundleV1.generate(wallet) - await manager.storePrivateKeyBundle(bundle) + it("round trips", async () => { + const manager = new NetworkKeyManager(wallet, persistence); + const bundle = await PrivateKeyBundleV1.generate(wallet); + await manager.storePrivateKeyBundle(bundle); const returnedBundle = await pollFor( async () => { - const bundle = await manager.loadPrivateKeyBundle() + const bundle = await manager.loadPrivateKeyBundle(); if (!bundle) { - throw new Error('No bundle yet') + throw new Error("No bundle yet"); } - return bundle + return bundle; }, 15000, - 100 - ) + 100, + ); - expect(returnedBundle).toBeDefined() - expect(bundle.identityKey.toBytes()).toEqual(bundle.identityKey.toBytes()) + expect(returnedBundle).toBeDefined(); + expect(bundle.identityKey.toBytes()).toEqual(bundle.identityKey.toBytes()); expect(bundle.identityKey.publicKey.signature?.ecdsaCompact?.bytes).toEqual( - returnedBundle?.identityKey.publicKey.signature?.ecdsaCompact?.bytes - ) + returnedBundle?.identityKey.publicKey.signature?.ecdsaCompact?.bytes, + ); expect(bundle.identityKey.secp256k1).toEqual( - returnedBundle?.identityKey.secp256k1 - ) - expect(bundle.preKeys).toHaveLength(returnedBundle?.preKeys.length) + returnedBundle?.identityKey.secp256k1, + ); + expect(bundle.preKeys).toHaveLength(returnedBundle?.preKeys.length); expect(bundle.preKeys[0].toBytes()).toEqual( - returnedBundle?.preKeys[0].toBytes() - ) - }) + returnedBundle?.preKeys[0].toBytes(), + ); + }); - it('encrypts with Ledger and decrypts with Metamask', async () => { - const wallet = newWallet() - const ledgerLikeWallet = wrapAsLedgerWallet(wallet) + it("encrypts with Ledger and decrypts with Metamask", async () => { + const wallet = newWallet(); + const ledgerLikeWallet = wrapAsLedgerWallet(wallet); const secureLedgerStore = new NetworkKeyManager( ledgerLikeWallet, - persistence - ) - const secureNormalStore = new NetworkKeyManager(wallet, persistence) - const originalBundle = await PrivateKeyBundleV1.generate(ledgerLikeWallet) + persistence, + ); + const secureNormalStore = new NetworkKeyManager(wallet, persistence); + const originalBundle = await PrivateKeyBundleV1.generate(ledgerLikeWallet); - await secureLedgerStore.storePrivateKeyBundle(originalBundle) - await sleep(100) - const returnedBundle = await secureNormalStore.loadPrivateKeyBundle() + await secureLedgerStore.storePrivateKeyBundle(originalBundle); + await sleep(100); + const returnedBundle = await secureNormalStore.loadPrivateKeyBundle(); if (!returnedBundle) { - throw new Error('No bundle returned') + throw new Error("No bundle returned"); } - expect(returnedBundle).toBeDefined() + expect(returnedBundle).toBeDefined(); expect(originalBundle.identityKey.toBytes()).toEqual( - returnedBundle.identityKey.toBytes() - ) - expect(originalBundle.preKeys).toHaveLength(returnedBundle.preKeys.length) + returnedBundle.identityKey.toBytes(), + ); + expect(originalBundle.preKeys).toHaveLength(returnedBundle.preKeys.length); expect(originalBundle.preKeys[0].toBytes()).toEqual( - returnedBundle.preKeys[0].toBytes() - ) - }) + returnedBundle.preKeys[0].toBytes(), + ); + }); - it('encrypts with Metamask and decrypts with Ledger', async () => { - const wallet = newWallet() - const ledgerLikeWallet = wrapAsLedgerWallet(wallet) - const ledgerManager = new NetworkKeyManager(ledgerLikeWallet, persistence) - const normalManager = new NetworkKeyManager(wallet, persistence) - const originalBundle = await PrivateKeyBundleV1.generate(wallet) + it("encrypts with Metamask and decrypts with Ledger", async () => { + const wallet = newWallet(); + const ledgerLikeWallet = wrapAsLedgerWallet(wallet); + const ledgerManager = new NetworkKeyManager(ledgerLikeWallet, persistence); + const normalManager = new NetworkKeyManager(wallet, persistence); + const originalBundle = await PrivateKeyBundleV1.generate(wallet); - await normalManager.storePrivateKeyBundle(originalBundle) - await sleep(100) - const returnedBundle = await ledgerManager.loadPrivateKeyBundle() + await normalManager.storePrivateKeyBundle(originalBundle); + await sleep(100); + const returnedBundle = await ledgerManager.loadPrivateKeyBundle(); if (!returnedBundle) { - throw new Error('No bundle returned') + throw new Error("No bundle returned"); } - expect(returnedBundle).toBeDefined() + expect(returnedBundle).toBeDefined(); expect(originalBundle.identityKey.toBytes()).toEqual( - returnedBundle.identityKey.toBytes() - ) - expect(originalBundle.preKeys).toHaveLength(returnedBundle.preKeys.length) + returnedBundle.identityKey.toBytes(), + ); + expect(originalBundle.preKeys).toHaveLength(returnedBundle.preKeys.length); expect(originalBundle.preKeys[0].toBytes()).toEqual( - returnedBundle.preKeys[0].toBytes() - ) - }) + returnedBundle.preKeys[0].toBytes(), + ); + }); - it('respects the options provided', async () => { - const bundle = await PrivateKeyBundleV1.generate(wallet) + it("respects the options provided", async () => { + const bundle = await PrivateKeyBundleV1.generate(wallet); const shouldBePrefixed = await buildPersistenceFromOptions( testProviderOptions({ disablePersistenceEncryption: true, persistConversations: false, }), - bundle - ) - expect(shouldBePrefixed).toBeInstanceOf(BrowserStoragePersistence) + bundle, + ); + expect(shouldBePrefixed).toBeInstanceOf(BrowserStoragePersistence); const shouldBeEncrypted = await buildPersistenceFromOptions( testProviderOptions({ disablePersistenceEncryption: false, persistConversations: true, }), - bundle - ) - expect(shouldBeEncrypted).toBeInstanceOf(PrefixedPersistence) - }) + bundle, + ); + expect(shouldBeEncrypted).toBeInstanceOf(PrefixedPersistence); + }); - it('calls notifier on store', async () => { - const mockNotifier = vi.fn() - const manager = new NetworkKeyManager(wallet, persistence, mockNotifier) - const bundle = await PrivateKeyBundleV1.generate(wallet) - await manager.storePrivateKeyBundle(bundle) - expect(mockNotifier).toHaveBeenCalledTimes(1) - }) -}) + it("calls notifier on store", async () => { + const mockNotifier = vi.fn(); + const manager = new NetworkKeyManager(wallet, persistence, mockNotifier); + const bundle = await PrivateKeyBundleV1.generate(wallet); + await manager.storePrivateKeyBundle(bundle); + expect(mockNotifier).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/js-sdk/test/keystore/providers/NetworkKeystoreProvider.test.ts b/packages/js-sdk/test/keystore/providers/NetworkKeystoreProvider.test.ts index 5b874e8a1..0c1b94658 100644 --- a/packages/js-sdk/test/keystore/providers/NetworkKeystoreProvider.test.ts +++ b/packages/js-sdk/test/keystore/providers/NetworkKeystoreProvider.test.ts @@ -1,102 +1,102 @@ -import { privateKey } from '@xmtp/proto' -import { hexToBytes, type Hex } from 'viem' -import { vi } from 'vitest' -import ApiClient, { ApiUrls } from '@/ApiClient' -import LocalAuthenticator from '@/authn/LocalAuthenticator' -import crypto from '@/crypto/crypto' -import { encrypt } from '@/crypto/encryption' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import TopicPersistence from '@/keystore/persistence/TopicPersistence' -import { KeystoreProviderUnavailableError } from '@/keystore/providers/errors' +import { privateKey } from "@xmtp/proto"; +import { hexToBytes, type Hex } from "viem"; +import { vi } from "vitest"; +import ApiClient, { ApiUrls } from "@/ApiClient"; +import LocalAuthenticator from "@/authn/LocalAuthenticator"; +import crypto from "@/crypto/crypto"; +import { encrypt } from "@/crypto/encryption"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import TopicPersistence from "@/keystore/persistence/TopicPersistence"; +import { KeystoreProviderUnavailableError } from "@/keystore/providers/errors"; import NetworkKeyManager, { storageSigRequestText, -} from '@/keystore/providers/NetworkKeyManager' -import NetworkKeystoreProvider from '@/keystore/providers/NetworkKeystoreProvider' -import type { Signer } from '@/types/Signer' -import { newWallet } from '@test/helpers' -import { testProviderOptions } from './helpers' +} from "@/keystore/providers/NetworkKeyManager"; +import NetworkKeystoreProvider from "@/keystore/providers/NetworkKeystoreProvider"; +import type { Signer } from "@/types/Signer"; +import { newWallet } from "@test/helpers"; +import { testProviderOptions } from "./helpers"; -describe('NetworkKeystoreProvider', () => { - let apiClient: ApiClient - let bundle: PrivateKeyBundleV1 - let wallet: Signer +describe("NetworkKeystoreProvider", () => { + let apiClient: ApiClient; + let bundle: PrivateKeyBundleV1; + let wallet: Signer; beforeEach(async () => { - apiClient = new ApiClient(ApiUrls.local) - wallet = newWallet() - bundle = await PrivateKeyBundleV1.generate(wallet) - }) + apiClient = new ApiClient(ApiUrls.local); + wallet = newWallet(); + bundle = await PrivateKeyBundleV1.generate(wallet); + }); - it('fails gracefully when no keys are found', async () => { - const provider = new NetworkKeystoreProvider() + it("fails gracefully when no keys are found", async () => { + const provider = new NetworkKeystoreProvider(); expect( - provider.newKeystore(testProviderOptions({}), apiClient, wallet) - ).rejects.toThrow(KeystoreProviderUnavailableError) - }) + provider.newKeystore(testProviderOptions({}), apiClient, wallet), + ).rejects.toThrow(KeystoreProviderUnavailableError); + }); - it('loads keys when they are already set', async () => { + it("loads keys when they are already set", async () => { const manager = new NetworkKeyManager( wallet, - new TopicPersistence(apiClient) - ) - await manager.storePrivateKeyBundle(bundle) + new TopicPersistence(apiClient), + ); + await manager.storePrivateKeyBundle(bundle); - const provider = new NetworkKeystoreProvider() + const provider = new NetworkKeystoreProvider(); const keystore = await provider.newKeystore( testProviderOptions({}), apiClient, - wallet - ) + wallet, + ); expect(await keystore.getPublicKeyBundle()).toEqual( - bundle.getPublicKeyBundle() - ) - }) + bundle.getPublicKeyBundle(), + ); + }); - it('properly handles legacy keys', async () => { + it("properly handles legacy keys", async () => { // Create a legacy EncryptedPrivateKeyBundleV1 and store it on the node - const bytes = bundle.encode() - const wPreKey = crypto.getRandomValues(new Uint8Array(32)) - const input = storageSigRequestText(wPreKey) - const walletAddr = await wallet.getAddress() + const bytes = bundle.encode(); + const wPreKey = crypto.getRandomValues(new Uint8Array(32)); + const input = storageSigRequestText(wPreKey); + const walletAddr = await wallet.getAddress(); - const sig = await wallet.signMessage(input) - const secret = hexToBytes(sig as Hex) - const ciphertext = await encrypt(bytes, secret) + const sig = await wallet.signMessage(input); + const secret = hexToBytes(sig as Hex); + const ciphertext = await encrypt(bytes, secret); const bytesToStore = privateKey.EncryptedPrivateKeyBundleV1.encode({ ciphertext, walletPreKey: wPreKey, - }).finish() + }).finish(); // Store the legacy key on the node - apiClient.setAuthenticator(new LocalAuthenticator(bundle.identityKey)) - const persistence = new TopicPersistence(apiClient) - const key = `${walletAddr}/key_bundle` - await persistence.setItem(key, bytesToStore) + apiClient.setAuthenticator(new LocalAuthenticator(bundle.identityKey)); + const persistence = new TopicPersistence(apiClient); + const key = `${walletAddr}/key_bundle`; + await persistence.setItem(key, bytesToStore); // Now try and load it - const provider = new NetworkKeystoreProvider() + const provider = new NetworkKeystoreProvider(); const keystore = await provider.newKeystore( testProviderOptions({}), apiClient, - wallet - ) - expect(keystore).toBeDefined() - }) + wallet, + ); + expect(keystore).toBeDefined(); + }); - it('correctly calls notifier on load', async () => { + it("correctly calls notifier on load", async () => { const manager = new NetworkKeyManager( wallet, - new TopicPersistence(apiClient) - ) - await manager.storePrivateKeyBundle(bundle) + new TopicPersistence(apiClient), + ); + await manager.storePrivateKeyBundle(bundle); - const provider = new NetworkKeystoreProvider() - const mockNotifier = vi.fn() + const provider = new NetworkKeystoreProvider(); + const mockNotifier = vi.fn(); await provider.newKeystore( { ...testProviderOptions({}), preEnableIdentityCallback: mockNotifier }, apiClient, - wallet - ) - expect(mockNotifier).toHaveBeenCalledTimes(1) - }) -}) + wallet, + ); + expect(mockNotifier).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/js-sdk/test/keystore/providers/SnapProvider.test.ts b/packages/js-sdk/test/keystore/providers/SnapProvider.test.ts index bcddde258..f203994dc 100644 --- a/packages/js-sdk/test/keystore/providers/SnapProvider.test.ts +++ b/packages/js-sdk/test/keystore/providers/SnapProvider.test.ts @@ -1,99 +1,99 @@ -import { keystore as keystoreProto } from '@xmtp/proto' -import { vi, type Mock } from 'vitest' -import HttpApiClient, { ApiUrls, type ApiClient } from '@/ApiClient' -import { KeystoreProviderUnavailableError } from '@/keystore/providers/errors' -import type { KeystoreProviderOptions } from '@/keystore/providers/interfaces' -import SnapKeystoreProvider from '@/keystore/providers/SnapProvider' +import { keystore as keystoreProto } from "@xmtp/proto"; +import { vi, type Mock } from "vitest"; +import HttpApiClient, { ApiUrls, type ApiClient } from "@/ApiClient"; +import { KeystoreProviderUnavailableError } from "@/keystore/providers/errors"; +import type { KeystoreProviderOptions } from "@/keystore/providers/interfaces"; +import SnapKeystoreProvider from "@/keystore/providers/SnapProvider"; import { connectSnap, getSnap, getWalletStatus, hasMetamaskWithSnaps, initSnap, -} from '@/keystore/snapHelpers' -import type { Signer } from '@/types/Signer' -import { newWallet } from '@test/helpers' +} from "@/keystore/snapHelpers"; +import type { Signer } from "@/types/Signer"; +import { newWallet } from "@test/helpers"; -vi.mock('@/keystore/snapHelpers') +vi.mock("@/keystore/snapHelpers"); -describe('SnapProvider', () => { - const provider = new SnapKeystoreProvider() - const options = { env: 'local' } as KeystoreProviderOptions - let apiClient: ApiClient - let wallet: Signer +describe("SnapProvider", () => { + const provider = new SnapKeystoreProvider(); + const options = { env: "local" } as KeystoreProviderOptions; + let apiClient: ApiClient; + let wallet: Signer; beforeEach(async () => { - apiClient = new HttpApiClient(ApiUrls.local) - wallet = newWallet() - vi.resetAllMocks() - }) + apiClient = new HttpApiClient(ApiUrls.local); + wallet = newWallet(); + vi.resetAllMocks(); + }); - it('should throw an error if no wallet is provided', async () => { + it("should throw an error if no wallet is provided", async () => { await expect( - provider.newKeystore(options, apiClient, undefined) - ).rejects.toThrow('No wallet provided') - }) + provider.newKeystore(options, apiClient, undefined), + ).rejects.toThrow("No wallet provided"); + }); - it('should throw a KeystoreProviderUnavailableError if MetaMask with Snaps is not detected', async () => { - ;(hasMetamaskWithSnaps as Mock).mockReturnValue(Promise.resolve(false)) + it("should throw a KeystoreProviderUnavailableError if MetaMask with Snaps is not detected", async () => { + (hasMetamaskWithSnaps as Mock).mockReturnValue(Promise.resolve(false)); await expect( - provider.newKeystore(options, apiClient, wallet) + provider.newKeystore(options, apiClient, wallet), ).rejects.toThrow( - new KeystoreProviderUnavailableError('MetaMask with Snaps not detected') - ) - }) + new KeystoreProviderUnavailableError("MetaMask with Snaps not detected"), + ); + }); - it('should attempt to connect to the snap if it is not already connected', async () => { - ;(hasMetamaskWithSnaps as Mock).mockReturnValue(Promise.resolve(true)) - ;(getWalletStatus as Mock).mockReturnValue( + it("should attempt to connect to the snap if it is not already connected", async () => { + (hasMetamaskWithSnaps as Mock).mockReturnValue(Promise.resolve(true)); + (getWalletStatus as Mock).mockReturnValue( Promise.resolve( keystoreProto.GetKeystoreStatusResponse_KeystoreStatus - .KEYSTORE_STATUS_INITIALIZED - ) - ) - ;(getSnap as Mock).mockReturnValue(Promise.resolve(undefined)) + .KEYSTORE_STATUS_INITIALIZED, + ), + ); + (getSnap as Mock).mockReturnValue(Promise.resolve(undefined)); - const keystore = await provider.newKeystore(options, apiClient, wallet) + const keystore = await provider.newKeystore(options, apiClient, wallet); - expect(keystore).toBeDefined() - expect(getWalletStatus as Mock).toHaveBeenCalledTimes(1) - expect(getSnap as Mock).toHaveBeenCalledTimes(1) - expect(connectSnap as Mock).toHaveBeenCalledTimes(1) - expect(initSnap as Mock).not.toHaveBeenCalled() - }) + expect(keystore).toBeDefined(); + expect(getWalletStatus as Mock).toHaveBeenCalledTimes(1); + expect(getSnap as Mock).toHaveBeenCalledTimes(1); + expect(connectSnap as Mock).toHaveBeenCalledTimes(1); + expect(initSnap as Mock).not.toHaveBeenCalled(); + }); - it('does not attempt to connect to the snap if it is already connected', async () => { - ;(hasMetamaskWithSnaps as Mock).mockReturnValue(Promise.resolve(true)) - ;(getWalletStatus as Mock).mockReturnValue( + it("does not attempt to connect to the snap if it is already connected", async () => { + (hasMetamaskWithSnaps as Mock).mockReturnValue(Promise.resolve(true)); + (getWalletStatus as Mock).mockReturnValue( Promise.resolve( keystoreProto.GetKeystoreStatusResponse_KeystoreStatus - .KEYSTORE_STATUS_INITIALIZED - ) - ) - ;(getSnap as Mock).mockReturnValue(Promise.resolve({})) - ;(connectSnap as Mock).mockReturnValue(Promise.resolve()) + .KEYSTORE_STATUS_INITIALIZED, + ), + ); + (getSnap as Mock).mockReturnValue(Promise.resolve({})); + (connectSnap as Mock).mockReturnValue(Promise.resolve()); - await provider.newKeystore(options, apiClient, wallet) - expect(connectSnap as Mock).not.toHaveBeenCalled() - expect(initSnap as Mock).not.toHaveBeenCalled() - }) + await provider.newKeystore(options, apiClient, wallet); + expect(connectSnap as Mock).not.toHaveBeenCalled(); + expect(initSnap as Mock).not.toHaveBeenCalled(); + }); - it('initializes the snap if it is not already initialized', async () => { - ;(hasMetamaskWithSnaps as Mock).mockReturnValue(Promise.resolve(true)) - ;(getWalletStatus as Mock).mockReturnValue( + it("initializes the snap if it is not already initialized", async () => { + (hasMetamaskWithSnaps as Mock).mockReturnValue(Promise.resolve(true)); + (getWalletStatus as Mock).mockReturnValue( Promise.resolve( keystoreProto.GetKeystoreStatusResponse_KeystoreStatus - .KEYSTORE_STATUS_UNINITIALIZED - ) - ) - ;(getSnap as Mock).mockReturnValue(Promise.resolve({})) - ;(connectSnap as Mock).mockReturnValue(Promise.resolve()) - ;(initSnap as Mock).mockReturnValue(Promise.resolve()) + .KEYSTORE_STATUS_UNINITIALIZED, + ), + ); + (getSnap as Mock).mockReturnValue(Promise.resolve({})); + (connectSnap as Mock).mockReturnValue(Promise.resolve()); + (initSnap as Mock).mockReturnValue(Promise.resolve()); - const keystore = await provider.newKeystore(options, apiClient, wallet) + const keystore = await provider.newKeystore(options, apiClient, wallet); - expect(keystore).toBeDefined() - expect(initSnap as Mock).toHaveBeenCalledTimes(1) - }) -}) + expect(keystore).toBeDefined(); + expect(initSnap as Mock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/js-sdk/test/keystore/providers/StaticKeystoreProvider.test.ts b/packages/js-sdk/test/keystore/providers/StaticKeystoreProvider.test.ts index a14d1e9f6..513675f0b 100644 --- a/packages/js-sdk/test/keystore/providers/StaticKeystoreProvider.test.ts +++ b/packages/js-sdk/test/keystore/providers/StaticKeystoreProvider.test.ts @@ -1,51 +1,51 @@ -import { privateKey } from '@xmtp/proto' -import { PrivateKeyBundleV1 } from '@/crypto/PrivateKeyBundle' -import { KeystoreProviderUnavailableError } from '@/keystore/providers/errors' -import StaticKeystoreProvider from '@/keystore/providers/StaticKeystoreProvider' -import { newWallet } from '@test/helpers' -import { testProviderOptions } from './helpers' +import { privateKey } from "@xmtp/proto"; +import { PrivateKeyBundleV1 } from "@/crypto/PrivateKeyBundle"; +import { KeystoreProviderUnavailableError } from "@/keystore/providers/errors"; +import StaticKeystoreProvider from "@/keystore/providers/StaticKeystoreProvider"; +import { newWallet } from "@test/helpers"; +import { testProviderOptions } from "./helpers"; -const ENV = 'local' +const ENV = "local"; -describe('StaticKeystoreProvider', () => { - it('works with a valid key', async () => { - const key = await PrivateKeyBundleV1.generate(newWallet()) +describe("StaticKeystoreProvider", () => { + it("works with a valid key", async () => { + const key = await PrivateKeyBundleV1.generate(newWallet()); const keyBytes = privateKey.PrivateKeyBundle.encode({ v1: key, v2: undefined, - }).finish() - const provider = new StaticKeystoreProvider() + }).finish(); + const provider = new StaticKeystoreProvider(); const keystore = await provider.newKeystore( testProviderOptions({ privateKeyOverride: keyBytes, env: ENV, persistConversations: false, - }) - ) + }), + ); - expect(keystore).not.toBeNull() - }) + expect(keystore).not.toBeNull(); + }); - it('throws with an unset key', async () => { + it("throws with an unset key", async () => { expect( new StaticKeystoreProvider().newKeystore( testProviderOptions({ env: ENV, persistConversations: false, - }) - ) - ).rejects.toThrow(KeystoreProviderUnavailableError) - }) + }), + ), + ).rejects.toThrow(KeystoreProviderUnavailableError); + }); - it('fails with an invalid key', async () => { + it("fails with an invalid key", async () => { expect( new StaticKeystoreProvider().newKeystore( testProviderOptions({ privateKeyOverride: Uint8Array.from([1, 2, 3]), env: ENV, persistConversations: false, - }) - ) - ).rejects.toThrow() - }) -}) + }), + ), + ).rejects.toThrow(); + }); +}); diff --git a/packages/js-sdk/test/keystore/providers/helpers.ts b/packages/js-sdk/test/keystore/providers/helpers.ts index 1049da397..b02bc3e11 100644 --- a/packages/js-sdk/test/keystore/providers/helpers.ts +++ b/packages/js-sdk/test/keystore/providers/helpers.ts @@ -1,15 +1,15 @@ -import InMemoryPersistence from '@/keystore/persistence/InMemoryPersistence' -import type { KeystoreProviderOptions } from '@/keystore/providers/interfaces' +import InMemoryPersistence from "@/keystore/persistence/InMemoryPersistence"; +import type { KeystoreProviderOptions } from "@/keystore/providers/interfaces"; export const testProviderOptions = ({ privateKeyOverride = undefined, persistConversations = false, basePersistence = InMemoryPersistence.create(), - env = 'local' as const, + env = "local" as const, }: Partial) => ({ env, persistConversations, privateKeyOverride, basePersistence, disablePersistenceEncryption: false, -}) +}); diff --git a/packages/js-sdk/test/keystore/snapHelpers.test.ts b/packages/js-sdk/test/keystore/snapHelpers.test.ts index f6b72fbf4..f20d8a354 100644 --- a/packages/js-sdk/test/keystore/snapHelpers.test.ts +++ b/packages/js-sdk/test/keystore/snapHelpers.test.ts @@ -1,73 +1,73 @@ -import { keystore } from '@xmtp/proto' -import { vi } from 'vitest' -import { SNAP_LOCAL_ORIGIN } from '@/keystore/providers/SnapProvider' -import { getWalletStatus, hasMetamaskWithSnaps } from '@/keystore/snapHelpers' -import { b64Encode } from '@/utils/bytes' +import { keystore } from "@xmtp/proto"; +import { vi } from "vitest"; +import { SNAP_LOCAL_ORIGIN } from "@/keystore/providers/SnapProvider"; +import { getWalletStatus, hasMetamaskWithSnaps } from "@/keystore/snapHelpers"; +import { b64Encode } from "@/utils/bytes"; const { GetKeystoreStatusRequest, GetKeystoreStatusResponse, GetKeystoreStatusResponse_KeystoreStatus: KeystoreStatus, -} = keystore +} = keystore; // Setup the mocks for window.ethereum -const mockRequest = vi.hoisted(() => vi.fn()) -vi.mock('@/utils/ethereum', () => { +const mockRequest = vi.hoisted(() => vi.fn()); +vi.mock("@/utils/ethereum", () => { return { __esModule: true, getEthereum: vi.fn(() => { const ethereum: any = { request: mockRequest, - } - ethereum.providers = [ethereum] - ethereum.detected = [ethereum] - return ethereum + }; + ethereum.providers = [ethereum]; + ethereum.detected = [ethereum]; + return ethereum; }), - } -}) + }; +}); -describe('snapHelpers', () => { +describe("snapHelpers", () => { beforeEach(() => { - mockRequest.mockClear() - }) + mockRequest.mockClear(); + }); - it('can check if the user has Flask installed', async () => { - mockRequest.mockResolvedValue(['flask']) + it("can check if the user has Flask installed", async () => { + mockRequest.mockResolvedValue(["flask"]); - expect(await hasMetamaskWithSnaps()).toBe(true) - expect(mockRequest).toHaveBeenCalledTimes(1) - }) + expect(await hasMetamaskWithSnaps()).toBe(true); + expect(mockRequest).toHaveBeenCalledTimes(1); + }); - it('returns false when the user does not have flask installed', async () => { - mockRequest.mockRejectedValue(new Error('foo')) + it("returns false when the user does not have flask installed", async () => { + mockRequest.mockRejectedValue(new Error("foo")); - expect(await hasMetamaskWithSnaps()).toBe(false) - expect(mockRequest).toHaveBeenCalledTimes(2) - }) + expect(await hasMetamaskWithSnaps()).toBe(false); + expect(mockRequest).toHaveBeenCalledTimes(2); + }); - it('can check wallet status', async () => { - const method = 'getKeystoreStatus' - const walletAddress = '0xfoo' - const env = 'dev' + it("can check wallet status", async () => { + const method = "getKeystoreStatus"; + const walletAddress = "0xfoo"; + const env = "dev"; const resBytes = GetKeystoreStatusResponse.encode({ status: KeystoreStatus.KEYSTORE_STATUS_INITIALIZED, - }).finish() + }).finish(); mockRequest.mockResolvedValue({ res: b64Encode(resBytes, 0, resBytes.length), - }) + }); const status = await getWalletStatus( { walletAddress, env }, - SNAP_LOCAL_ORIGIN - ) - expect(status).toBe(KeystoreStatus.KEYSTORE_STATUS_INITIALIZED) + SNAP_LOCAL_ORIGIN, + ); + expect(status).toBe(KeystoreStatus.KEYSTORE_STATUS_INITIALIZED); const expectedRequest = GetKeystoreStatusRequest.encode({ walletAddress, - }).finish() + }).finish(); expect(mockRequest).toHaveBeenCalledWith({ - method: 'wallet_invokeSnap', + method: "wallet_invokeSnap", params: { snapId: SNAP_LOCAL_ORIGIN, request: { @@ -78,6 +78,6 @@ describe('snapHelpers', () => { }, }, }, - }) - }) -}) + }); + }); +}); diff --git a/packages/js-sdk/test/utils/semver.test.ts b/packages/js-sdk/test/utils/semver.test.ts index d77bee603..51b9787b0 100644 --- a/packages/js-sdk/test/utils/semver.test.ts +++ b/packages/js-sdk/test/utils/semver.test.ts @@ -1,39 +1,39 @@ -import { isSameMajorVersion, semverGreaterThan } from '@/utils/semver' +import { isSameMajorVersion, semverGreaterThan } from "@/utils/semver"; -describe('semver', () => { - describe('isSameMajorVersion', () => { - it('can parse major versions correctly', () => { - expect(isSameMajorVersion('1.0.0', '1.1.0')).toBe(true) - expect(isSameMajorVersion('1.0.0', '2.0.0')).toBe(false) - expect(isSameMajorVersion('1.1.0-beta.1', '1.1.0')).toBe(true) - expect(isSameMajorVersion('2.0.0', '1.5.0')).toBe(false) - }) +describe("semver", () => { + describe("isSameMajorVersion", () => { + it("can parse major versions correctly", () => { + expect(isSameMajorVersion("1.0.0", "1.1.0")).toBe(true); + expect(isSameMajorVersion("1.0.0", "2.0.0")).toBe(false); + expect(isSameMajorVersion("1.1.0-beta.1", "1.1.0")).toBe(true); + expect(isSameMajorVersion("2.0.0", "1.5.0")).toBe(false); + }); - it('handles undefined versions', () => { - expect(isSameMajorVersion(undefined, '1.0.0')).toBe(true) - expect(isSameMajorVersion('1.0.0', undefined)).toBe(true) - expect(isSameMajorVersion(undefined, undefined)).toBe(true) - }) - }) + it("handles undefined versions", () => { + expect(isSameMajorVersion(undefined, "1.0.0")).toBe(true); + expect(isSameMajorVersion("1.0.0", undefined)).toBe(true); + expect(isSameMajorVersion(undefined, undefined)).toBe(true); + }); + }); - describe('semverGreaterThan', () => { - it('can compare major and minor versions', () => { - expect(semverGreaterThan('1.0.0', '1.1.0')).toBe(false) - expect(semverGreaterThan('1.0.0', '2.0.0')).toBe(false) - expect(semverGreaterThan('1.10.0', '1.2.0')).toBe(true) - expect(semverGreaterThan('2.0.0', '1.0.0')).toBe(true) - expect(semverGreaterThan('10.0.0', '2.0.0')).toBe(true) - }) + describe("semverGreaterThan", () => { + it("can compare major and minor versions", () => { + expect(semverGreaterThan("1.0.0", "1.1.0")).toBe(false); + expect(semverGreaterThan("1.0.0", "2.0.0")).toBe(false); + expect(semverGreaterThan("1.10.0", "1.2.0")).toBe(true); + expect(semverGreaterThan("2.0.0", "1.0.0")).toBe(true); + expect(semverGreaterThan("10.0.0", "2.0.0")).toBe(true); + }); - it('can compare patch versions', () => { - expect(semverGreaterThan('1.0.0', '1.0.1')).toBe(false) - expect(semverGreaterThan('1.0.5', '1.0.1')).toBe(true) - expect(semverGreaterThan('1.0.0', '1.0.0-beta.1')).toBe(false) - expect(semverGreaterThan('1.0.0-beta.1', '1.0.0')).toBe(false) - expect(semverGreaterThan('1.1.1-beta.2', '1.1.1-beta.1')).toBe(true) - expect(semverGreaterThan('1.1.1-beta.1', '1.1.1-beta.1')).toBe(false) + it("can compare patch versions", () => { + expect(semverGreaterThan("1.0.0", "1.0.1")).toBe(false); + expect(semverGreaterThan("1.0.5", "1.0.1")).toBe(true); + expect(semverGreaterThan("1.0.0", "1.0.0-beta.1")).toBe(false); + expect(semverGreaterThan("1.0.0-beta.1", "1.0.0")).toBe(false); + expect(semverGreaterThan("1.1.1-beta.2", "1.1.1-beta.1")).toBe(true); + expect(semverGreaterThan("1.1.1-beta.1", "1.1.1-beta.1")).toBe(false); // Handles versions > 10 - expect(semverGreaterThan('1.1.1-beta.10', '1.1.1-beta.2')).toBe(true) - }) - }) -}) + expect(semverGreaterThan("1.1.1-beta.10", "1.1.1-beta.2")).toBe(true); + }); + }); +}); diff --git a/packages/js-sdk/test/utils/topic.test.ts b/packages/js-sdk/test/utils/topic.test.ts index d461a36a7..fcecdc7ee 100644 --- a/packages/js-sdk/test/utils/topic.test.ts +++ b/packages/js-sdk/test/utils/topic.test.ts @@ -1,48 +1,48 @@ -import crypto from '@/crypto/crypto' +import crypto from "@/crypto/crypto"; import { buildContentTopic, buildDirectMessageTopicV2, isValidTopic, -} from '@/utils/topic' +} from "@/utils/topic"; -describe('topic utils', () => { - describe('isValidTopic', () => { - it('validates topics correctly', () => { - expect(isValidTopic(buildContentTopic('foo'))).toBe(true) - expect(isValidTopic(buildContentTopic('123'))).toBe(true) - expect(isValidTopic(buildContentTopic('bar987'))).toBe(true) - expect(isValidTopic(buildContentTopic('*&+-)'))).toBe(true) - expect(isValidTopic(buildContentTopic('%#@='))).toBe(true) - expect(isValidTopic(buildContentTopic('<;.">'))).toBe(true) +describe("topic utils", () => { + describe("isValidTopic", () => { + it("validates topics correctly", () => { + expect(isValidTopic(buildContentTopic("foo"))).toBe(true); + expect(isValidTopic(buildContentTopic("123"))).toBe(true); + expect(isValidTopic(buildContentTopic("bar987"))).toBe(true); + expect(isValidTopic(buildContentTopic("*&+-)"))).toBe(true); + expect(isValidTopic(buildContentTopic("%#@="))).toBe(true); + expect(isValidTopic(buildContentTopic('<;.">'))).toBe(true); expect(isValidTopic(buildContentTopic(String.fromCharCode(33)))).toBe( - true - ) - expect(isValidTopic(buildContentTopic('∫ß'))).toBe(false) - expect(isValidTopic(buildContentTopic('\xA9'))).toBe(false) - expect(isValidTopic(buildContentTopic('\u2665'))).toBe(false) + true, + ); + expect(isValidTopic(buildContentTopic("∫ß"))).toBe(false); + expect(isValidTopic(buildContentTopic("\xA9"))).toBe(false); + expect(isValidTopic(buildContentTopic("\u2665"))).toBe(false); expect(isValidTopic(buildContentTopic(String.fromCharCode(1)))).toBe( - false - ) + false, + ); expect(isValidTopic(buildContentTopic(String.fromCharCode(23)))).toBe( - false - ) - }) + false, + ); + }); - it('validates random topics correctly', () => { + it("validates random topics correctly", () => { const topics = Array.from({ length: 100 }).map(() => buildDirectMessageTopicV2( Buffer.from(crypto.getRandomValues(new Uint8Array(32))) - .toString('base64') - .replace(/=*$/g, '') + .toString("base64") + .replace(/=*$/g, "") // Replace slashes with dashes so that the topic is still easily split by / // We do not treat this as needing to be valid Base64 anywhere - .replace(/\//g, '-') - ) - ) + .replace(/\//g, "-"), + ), + ); topics.forEach((topic) => { - expect(isValidTopic(topic)).toBe(true) - }) - }) - }) -}) + expect(isValidTopic(topic)).toBe(true); + }); + }); + }); +}); diff --git a/packages/js-sdk/vitest.config.ts b/packages/js-sdk/vitest.config.ts index b8b42adb3..0e8863206 100644 --- a/packages/js-sdk/vitest.config.ts +++ b/packages/js-sdk/vitest.config.ts @@ -1,12 +1,12 @@ /// -import { defineConfig, mergeConfig } from 'vite' -import tsconfigPaths from 'vite-tsconfig-paths' -import { defineConfig as defineVitestConfig } from 'vitest/config' +import { defineConfig, mergeConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig as defineVitestConfig } from "vitest/config"; // https://vitejs.dev/config/ const viteConfig = defineConfig({ plugins: [tsconfigPaths()], -}) +}); const vitestConfig = defineVitestConfig({ test: { @@ -14,6 +14,6 @@ const vitestConfig = defineVitestConfig({ testTimeout: 120000, hookTimeout: 60000, }, -}) +}); -export default mergeConfig(viteConfig, vitestConfig) +export default mergeConfig(viteConfig, vitestConfig); diff --git a/packages/mls-client/.eslintrc.cjs b/packages/mls-client/.eslintrc.cjs index e6468c571..da5b16990 100644 --- a/packages/mls-client/.eslintrc.cjs +++ b/packages/mls-client/.eslintrc.cjs @@ -1,44 +1,44 @@ module.exports = { - parser: '@typescript-eslint/parser', + parser: "@typescript-eslint/parser", extends: [ - 'eslint:recommended', - 'standard', - 'prettier', - 'plugin:@typescript-eslint/recommended', - 'eslint-config-prettier', + "eslint:recommended", + "standard", + "prettier", + "plugin:@typescript-eslint/recommended", + "eslint-config-prettier", ], parserOptions: { - sourceType: 'module', + sourceType: "module", warnOnUnsupportedTypeScriptVersion: false, - project: 'tsconfig.json', + project: "tsconfig.json", }, rules: { - '@typescript-eslint/consistent-type-exports': [ - 'error', + "@typescript-eslint/consistent-type-exports": [ + "error", { fixMixedExportsWithInlineTypeSpecifier: false, }, ], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/consistent-type-imports': 'error', - '@typescript-eslint/no-unused-vars': [ - 'error', + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/consistent-type-imports": "error", + "@typescript-eslint/no-unused-vars": [ + "error", { - argsIgnorePattern: '^_', - destructuredArrayIgnorePattern: '^_', + argsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", ignoreRestSiblings: true, - varsIgnorePattern: '^_', + varsIgnorePattern: "^_", }, ], - 'prettier/prettier': 'error', - 'no-restricted-syntax': [ - 'error', + "prettier/prettier": "error", + "no-restricted-syntax": [ + "error", { - selector: 'ImportDeclaration[source.value=/^\\.\\./]', + selector: "ImportDeclaration[source.value=/^\\.\\./]", message: - 'Relative parent imports are not allowed, use path aliases instead.', + "Relative parent imports are not allowed, use path aliases instead.", }, ], }, - plugins: ['@typescript-eslint', 'prettier'], -} + plugins: ["@typescript-eslint", "prettier"], +}; diff --git a/packages/mls-client/package.json b/packages/mls-client/package.json index 3374e56ff..b7fdfbc6c 100644 --- a/packages/mls-client/package.json +++ b/packages/mls-client/package.json @@ -38,14 +38,11 @@ ], "scripts": { "build": "yarn clean:dist && rollup -c", - "clean": "yarn clean:dbs && yarn clean:dist && yarn clean:deps && yarn clean:tests", - "clean:dbs": "rm -rf *.db3* ||:", - "clean:deps": "rm -rf node_modules", - "clean:dist": "rm -rf dist", - "clean:tests": "rm -rf test/*.db3* ||:", - "format": "yarn format:base -w .", - "format:base": "prettier --ignore-path ../../.gitignore", - "format:check": "yarn format:base -c .", + "clean": "rimraf .turbo && yarn clean:dbs && yarn clean:dist && yarn clean:deps && yarn clean:tests", + "clean:dbs": "rimraf *.db3* ||:", + "clean:deps": "rimraf node_modules", + "clean:dist": "rimraf dist", + "clean:tests": "rimraf test/*.db3* ||:", "lint": "eslint . --ignore-path ../../.gitignore", "test": "vitest run", "test:cov": "vitest run --coverage", @@ -58,7 +55,6 @@ "@xmtp/proto": "^3.62.1" }, "devDependencies": { - "@ianvs/prettier-plugin-sort-imports": "^4.3.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-typescript": "^12.1.0", "@types/node": "^20.14.10", @@ -75,13 +71,12 @@ "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-promise": "^6.4.0", "fast-glob": "^3.3.2", - "prettier": "^3.3.3", - "prettier-plugin-packagejson": "^2.5.2", + "rimraf": "^6.0.1", "rollup": "^4.24.0", "rollup-plugin-dts": "^6.1.1", "rollup-plugin-filesize": "^10.0.0", "rollup-plugin-tsconfig-paths": "^1.5.2", - "typescript": "^5.6.2", + "typescript": "^5.6.3", "viem": "^2.13.6", "vite": "5.4.8", "vite-tsconfig-paths": "^5.0.1", diff --git a/packages/mls-client/rollup.config.js b/packages/mls-client/rollup.config.js index f2451a4b5..0ab934c85 100644 --- a/packages/mls-client/rollup.config.js +++ b/packages/mls-client/rollup.config.js @@ -1,19 +1,19 @@ -import json from '@rollup/plugin-json' -import typescript from '@rollup/plugin-typescript' -import { defineConfig } from 'rollup' -import { dts } from 'rollup-plugin-dts' -import filesize from 'rollup-plugin-filesize' -import tsConfigPaths from 'rollup-plugin-tsconfig-paths' +import json from "@rollup/plugin-json"; +import typescript from "@rollup/plugin-typescript"; +import { defineConfig } from "rollup"; +import { dts } from "rollup-plugin-dts"; +import filesize from "rollup-plugin-filesize"; +import tsConfigPaths from "rollup-plugin-tsconfig-paths"; const external = [ - 'node:path', - 'node:process', - '@xmtp/content-type-text', - '@xmtp/content-type-primitives', - '@xmtp/mls-client-bindings-node', - '@xmtp/proto', - '@xmtp/xmtp-js', -] + "node:path", + "node:process", + "@xmtp/content-type-text", + "@xmtp/content-type-primitives", + "@xmtp/mls-client-bindings-node", + "@xmtp/proto", + "@xmtp/xmtp-js", +]; const plugins = [ tsConfigPaths(), @@ -27,35 +27,35 @@ const plugins = [ json({ preferConst: true, }), -] +]; export default defineConfig([ { - input: 'src/index.ts', + input: "src/index.ts", output: { - file: 'dist/index.js', - format: 'es', + file: "dist/index.js", + format: "es", sourcemap: true, }, plugins, external, }, { - input: 'src/index.ts', + input: "src/index.ts", output: { - file: 'dist/index.cjs', - format: 'cjs', + file: "dist/index.cjs", + format: "cjs", sourcemap: true, }, plugins, external, }, { - input: 'src/index.ts', + input: "src/index.ts", output: { - file: 'dist/index.d.ts', - format: 'es', + file: "dist/index.d.ts", + format: "es", }, plugins: [tsConfigPaths(), dts()], }, -]) +]); diff --git a/packages/mls-client/src/AsyncStream.ts b/packages/mls-client/src/AsyncStream.ts index 8f36428d1..1e8121d3c 100644 --- a/packages/mls-client/src/AsyncStream.ts +++ b/packages/mls-client/src/AsyncStream.ts @@ -1,69 +1,69 @@ type ResolveValue = { - value: T | undefined - done: boolean -} + value: T | undefined; + done: boolean; +}; -type ResolveNext = (resolveValue: ResolveValue) => void +type ResolveNext = (resolveValue: ResolveValue) => void; -export type StreamCallback = (err: Error | null, value: T) => void +export type StreamCallback = (err: Error | null, value: T) => void; export class AsyncStream { - #done = false - #resolveNext: ResolveNext | null - #queue: T[] + #done = false; + #resolveNext: ResolveNext | null; + #queue: T[]; - stopCallback: (() => void) | undefined = undefined + stopCallback: (() => void) | undefined = undefined; constructor() { - this.#queue = [] - this.#resolveNext = null - this.#done = false + this.#queue = []; + this.#resolveNext = null; + this.#done = false; } get isDone() { - return this.#done + return this.#done; } callback: StreamCallback = (err, value) => { if (err) { - console.error('stream error', err) - this.stop() - return + console.error("stream error", err); + this.stop(); + return; } if (this.#done) { - return + return; } if (this.#resolveNext) { - this.#resolveNext({ value, done: false }) - this.#resolveNext = null + this.#resolveNext({ value, done: false }); + this.#resolveNext = null; } else { - this.#queue.push(value) + this.#queue.push(value); } - } + }; stop = () => { - this.#done = true + this.#done = true; if (this.#resolveNext) { - this.#resolveNext({ value: undefined, done: true }) + this.#resolveNext({ value: undefined, done: true }); } - this.stopCallback?.() - } + this.stopCallback?.(); + }; next = (): Promise> => { if (this.#queue.length > 0) { - return Promise.resolve({ value: this.#queue.shift(), done: false }) + return Promise.resolve({ value: this.#queue.shift(), done: false }); } else if (this.#done) { - return Promise.resolve({ value: undefined, done: true }) + return Promise.resolve({ value: undefined, done: true }); } else { return new Promise((resolve) => { - this.#resolveNext = resolve - }) + this.#resolveNext = resolve; + }); } }; [Symbol.asyncIterator]() { - return this + return this; } } diff --git a/packages/mls-client/src/Client.ts b/packages/mls-client/src/Client.ts index cd852254f..c88582f16 100644 --- a/packages/mls-client/src/Client.ts +++ b/packages/mls-client/src/Client.ts @@ -1,11 +1,11 @@ -import { join } from 'node:path' -import process from 'node:process' +import { join } from "node:path"; +import process from "node:process"; import type { ContentCodec, ContentTypeId, EncodedContent, -} from '@xmtp/content-type-primitives' -import { TextCodec } from '@xmtp/content-type-text' +} from "@xmtp/content-type-primitives"; +import { TextCodec } from "@xmtp/content-type-text"; import { createClient, generateInboxId, @@ -14,20 +14,20 @@ import { NapiSignatureRequestType, type NapiClient, type NapiMessage, -} from '@xmtp/mls-client-bindings-node' +} from "@xmtp/mls-client-bindings-node"; import { ContentTypeGroupUpdated, GroupUpdatedCodec, -} from '@/codecs/GroupUpdatedCodec' -import { Conversations } from '@/Conversations' +} from "@/codecs/GroupUpdatedCodec"; +import { Conversations } from "@/Conversations"; export const ApiUrls = { - local: 'http://localhost:5556', - dev: 'https://grpc.dev.xmtp.network:443', - production: 'https://grpc.production.xmtp.network:443', -} as const + local: "http://localhost:5556", + dev: "https://grpc.dev.xmtp.network:443", + production: "https://grpc.production.xmtp.network:443", +} as const; -export type XmtpEnv = keyof typeof ApiUrls +export type XmtpEnv = keyof typeof ApiUrls; /** * Network options @@ -36,13 +36,13 @@ export type NetworkOptions = { /** * Specify which XMTP environment to connect to. (default: `dev`) */ - env?: XmtpEnv + env?: XmtpEnv; /** * apiUrl can be used to override the `env` flag and connect to a * specific endpoint */ - apiUrl?: string -} + apiUrl?: string; +}; /** * Encryption options @@ -51,8 +51,8 @@ export type EncryptionOptions = { /** * Encryption key to use for the local DB */ - encryptionKey?: Uint8Array | null -} + encryptionKey?: Uint8Array | null; +}; /** * Storage options @@ -61,43 +61,43 @@ export type StorageOptions = { /** * Path to the local DB */ - dbPath?: string -} + dbPath?: string; +}; export type ContentOptions = { /** * Allow configuring codecs for additional content types */ - codecs?: ContentCodec[] -} + codecs?: ContentCodec[]; +}; export type ClientOptions = NetworkOptions & EncryptionOptions & StorageOptions & - ContentOptions + ContentOptions; export class Client { - #innerClient: NapiClient - #conversations: Conversations - #codecs: Map> + #innerClient: NapiClient; + #conversations: Conversations; + #codecs: Map>; constructor(client: NapiClient, codecs: ContentCodec[]) { - this.#innerClient = client - this.#conversations = new Conversations(this, client.conversations()) + this.#innerClient = client; + this.#conversations = new Conversations(this, client.conversations()); this.#codecs = new Map( - codecs.map((codec) => [codec.contentType.toString(), codec]) - ) + codecs.map((codec) => [codec.contentType.toString(), codec]), + ); } static async create(accountAddress: string, options?: ClientOptions) { - const host = options?.apiUrl ?? ApiUrls[options?.env ?? 'dev'] - const isSecure = host.startsWith('https') + const host = options?.apiUrl ?? ApiUrls[options?.env ?? "dev"]; + const isSecure = host.startsWith("https"); const dbPath = - options?.dbPath ?? join(process.cwd(), `${accountAddress}.db3`) + options?.dbPath ?? join(process.cwd(), `${accountAddress}.db3`); const inboxId = (await getInboxIdForAddress(host, isSecure, accountAddress)) || - generateInboxId(accountAddress) + generateInboxId(accountAddress); return new Client( await createClient( @@ -106,77 +106,77 @@ export class Client { dbPath, inboxId, accountAddress, - options?.encryptionKey + options?.encryptionKey, ), - [new GroupUpdatedCodec(), new TextCodec(), ...(options?.codecs ?? [])] - ) + [new GroupUpdatedCodec(), new TextCodec(), ...(options?.codecs ?? [])], + ); } get accountAddress() { - return this.#innerClient.accountAddress + return this.#innerClient.accountAddress; } get inboxId() { - return this.#innerClient.inboxId() + return this.#innerClient.inboxId(); } get installationId() { - return this.#innerClient.installationId() + return this.#innerClient.installationId(); } get isRegistered() { - return this.#innerClient.isRegistered() + return this.#innerClient.isRegistered(); } async signatureText() { try { - const signatureText = await this.#innerClient.createInboxSignatureText() - return signatureText + const signatureText = await this.#innerClient.createInboxSignatureText(); + return signatureText; } catch (e) { - return null + return null; } } async canMessage(accountAddresses: string[]) { - return this.#innerClient.canMessage(accountAddresses) + return this.#innerClient.canMessage(accountAddresses); } addSignature(signatureBytes: Uint8Array) { this.#innerClient.addSignature( NapiSignatureRequestType.CreateInbox, - signatureBytes - ) + signatureBytes, + ); } async registerIdentity() { - return this.#innerClient.registerIdentity() + return this.#innerClient.registerIdentity(); } get conversations() { - return this.#conversations + return this.#conversations; } codecFor(contentType: ContentTypeId) { - return this.#codecs.get(contentType.toString()) + return this.#codecs.get(contentType.toString()); } encodeContent(content: any, contentType: ContentTypeId) { - const codec = this.codecFor(contentType) + const codec = this.codecFor(contentType); if (!codec) { - throw new Error(`no codec for ${contentType.toString()}`) + throw new Error(`no codec for ${contentType.toString()}`); } - const encoded = codec.encode(content, this) - const fallback = codec.fallback(content) + const encoded = codec.encode(content, this); + const fallback = codec.fallback(content); if (fallback) { - encoded.fallback = fallback + encoded.fallback = fallback; } - return encoded + return encoded; } decodeContent(message: NapiMessage, contentType: ContentTypeId) { - const codec = this.codecFor(contentType) + const codec = this.codecFor(contentType); if (!codec) { - throw new Error(`no codec for ${contentType.toString()}`) + throw new Error(`no codec for ${contentType.toString()}`); } // throw an error if there's an invalid group membership change message @@ -184,21 +184,21 @@ export class Client { contentType.sameAs(ContentTypeGroupUpdated) && message.kind !== NapiGroupMessageKind.MembershipChange ) { - throw new Error('Error decoding group membership change') + throw new Error("Error decoding group membership change"); } - return codec.decode(message.content as EncodedContent, this) + return codec.decode(message.content as EncodedContent, this); } async requestHistorySync() { - return this.#innerClient.requestHistorySync() + return this.#innerClient.requestHistorySync(); } async getInboxIdByAddress(accountAddress: string) { - return this.#innerClient.findInboxIdByAddress(accountAddress) + return this.#innerClient.findInboxIdByAddress(accountAddress); } async inboxState(refreshFromNetwork: boolean = false) { - return this.#innerClient.inboxState(refreshFromNetwork) + return this.#innerClient.inboxState(refreshFromNetwork); } } diff --git a/packages/mls-client/src/Conversation.ts b/packages/mls-client/src/Conversation.ts index 20edecc97..f778b2ae8 100644 --- a/packages/mls-client/src/Conversation.ts +++ b/packages/mls-client/src/Conversation.ts @@ -1,197 +1,197 @@ -import type { ContentTypeId } from '@xmtp/content-type-primitives' -import { ContentTypeText } from '@xmtp/content-type-text' +import type { ContentTypeId } from "@xmtp/content-type-primitives"; +import { ContentTypeText } from "@xmtp/content-type-text"; import type { NapiGroup, NapiListMessagesOptions, -} from '@xmtp/mls-client-bindings-node' -import { AsyncStream, type StreamCallback } from '@/AsyncStream' -import type { Client } from '@/Client' -import { DecodedMessage } from '@/DecodedMessage' -import { nsToDate } from '@/helpers/date' +} from "@xmtp/mls-client-bindings-node"; +import { AsyncStream, type StreamCallback } from "@/AsyncStream"; +import type { Client } from "@/Client"; +import { DecodedMessage } from "@/DecodedMessage"; +import { nsToDate } from "@/helpers/date"; export class Conversation { - #client: Client - #group: NapiGroup + #client: Client; + #group: NapiGroup; constructor(client: Client, group: NapiGroup) { - this.#client = client - this.#group = group + this.#client = client; + this.#group = group; } get id() { - return this.#group.id() + return this.#group.id(); } get name() { - return this.#group.groupName() + return this.#group.groupName(); } async updateName(name: string) { - return this.#group.updateGroupName(name) + return this.#group.updateGroupName(name); } get imageUrl() { - return this.#group.groupImageUrlSquare() + return this.#group.groupImageUrlSquare(); } async updateImageUrl(imageUrl: string) { - return this.#group.updateGroupImageUrlSquare(imageUrl) + return this.#group.updateGroupImageUrlSquare(imageUrl); } get description() { - return this.#group.groupDescription() + return this.#group.groupDescription(); } async updateDescription(description: string) { - return this.#group.updateGroupDescription(description) + return this.#group.updateGroupDescription(description); } get pinnedFrameUrl() { - return this.#group.groupPinnedFrameUrl() + return this.#group.groupPinnedFrameUrl(); } async updatePinnedFrameUrl(pinnedFrameUrl: string) { - return this.#group.updateGroupPinnedFrameUrl(pinnedFrameUrl) + return this.#group.updateGroupPinnedFrameUrl(pinnedFrameUrl); } get isActive() { - return this.#group.isActive() + return this.#group.isActive(); } get addedByInboxId() { - return this.#group.addedByInboxId() + return this.#group.addedByInboxId(); } get createdAtNs() { - return this.#group.createdAtNs() + return this.#group.createdAtNs(); } get createdAt() { - return nsToDate(this.createdAtNs) + return nsToDate(this.createdAtNs); } get metadata() { - const metadata = this.#group.groupMetadata() + const metadata = this.#group.groupMetadata(); return { creatorInboxId: metadata.creatorInboxId(), conversationType: metadata.conversationType(), - } + }; } async members() { - return this.#group.listMembers() + return this.#group.listMembers(); } get admins() { - return this.#group.adminList() + return this.#group.adminList(); } get superAdmins() { - return this.#group.superAdminList() + return this.#group.superAdminList(); } get permissions() { return { policyType: this.#group.groupPermissions().policyType(), policySet: this.#group.groupPermissions().policySet(), - } + }; } isAdmin(inboxId: string) { - return this.#group.isAdmin(inboxId) + return this.#group.isAdmin(inboxId); } isSuperAdmin(inboxId: string) { - return this.#group.isSuperAdmin(inboxId) + return this.#group.isSuperAdmin(inboxId); } async sync() { - return this.#group.sync() + return this.#group.sync(); } stream(callback?: StreamCallback) { - const asyncStream = new AsyncStream() + const asyncStream = new AsyncStream(); const stream = this.#group.stream((err, message) => { - const decodedMessage = new DecodedMessage(this.#client, message) - asyncStream.callback(err, decodedMessage) - callback?.(err, decodedMessage) - }) + const decodedMessage = new DecodedMessage(this.#client, message); + asyncStream.callback(err, decodedMessage); + callback?.(err, decodedMessage); + }); - asyncStream.stopCallback = stream.end.bind(stream) + asyncStream.stopCallback = stream.end.bind(stream); - return asyncStream + return asyncStream; } async addMembers(accountAddresses: string[]) { - return this.#group.addMembers(accountAddresses) + return this.#group.addMembers(accountAddresses); } async addMembersByInboxId(inboxIds: string[]) { - return this.#group.addMembersByInboxId(inboxIds) + return this.#group.addMembersByInboxId(inboxIds); } async removeMembers(accountAddresses: string[]) { - return this.#group.removeMembers(accountAddresses) + return this.#group.removeMembers(accountAddresses); } async removeMembersByInboxId(inboxIds: string[]) { - return this.#group.removeMembersByInboxId(inboxIds) + return this.#group.removeMembersByInboxId(inboxIds); } async addAdmin(inboxId: string) { - return this.#group.addAdmin(inboxId) + return this.#group.addAdmin(inboxId); } async removeAdmin(inboxId: string) { - return this.#group.removeAdmin(inboxId) + return this.#group.removeAdmin(inboxId); } async addSuperAdmin(inboxId: string) { - return this.#group.addSuperAdmin(inboxId) + return this.#group.addSuperAdmin(inboxId); } async removeSuperAdmin(inboxId: string) { - return this.#group.removeSuperAdmin(inboxId) + return this.#group.removeSuperAdmin(inboxId); } async publishMessages() { - return this.#group.publishMessages() + return this.#group.publishMessages(); } sendOptimistic(content: any, contentType?: ContentTypeId) { - if (typeof content !== 'string' && !contentType) { + if (typeof content !== "string" && !contentType) { throw new Error( - 'Content type is required when sending content other than text' - ) + "Content type is required when sending content other than text", + ); } const encodedContent = - typeof content === 'string' + typeof content === "string" ? this.#client.encodeContent(content, contentType ?? ContentTypeText) - : this.#client.encodeContent(content, contentType!) + : this.#client.encodeContent(content, contentType!); - return this.#group.sendOptimistic(encodedContent) + return this.#group.sendOptimistic(encodedContent); } async send(content: any, contentType?: ContentTypeId) { - if (typeof content !== 'string' && !contentType) { + if (typeof content !== "string" && !contentType) { throw new Error( - 'Content type is required when sending content other than text' - ) + "Content type is required when sending content other than text", + ); } const encodedContent = - typeof content === 'string' + typeof content === "string" ? this.#client.encodeContent(content, contentType ?? ContentTypeText) - : this.#client.encodeContent(content, contentType!) + : this.#client.encodeContent(content, contentType!); - return this.#group.send(encodedContent) + return this.#group.send(encodedContent); } messages(options?: NapiListMessagesOptions): DecodedMessage[] { return this.#group .findMessages(options) - .map((message) => new DecodedMessage(this.#client, message)) + .map((message) => new DecodedMessage(this.#client, message)); } } diff --git a/packages/mls-client/src/Conversations.ts b/packages/mls-client/src/Conversations.ts index 2aee1c94e..c1bdcb65b 100644 --- a/packages/mls-client/src/Conversations.ts +++ b/packages/mls-client/src/Conversations.ts @@ -2,93 +2,93 @@ import type { NapiConversations, NapiCreateGroupOptions, NapiListMessagesOptions, -} from '@xmtp/mls-client-bindings-node' -import { AsyncStream, type StreamCallback } from '@/AsyncStream' -import type { Client } from '@/Client' -import { Conversation } from '@/Conversation' -import { DecodedMessage } from '@/DecodedMessage' +} from "@xmtp/mls-client-bindings-node"; +import { AsyncStream, type StreamCallback } from "@/AsyncStream"; +import type { Client } from "@/Client"; +import { Conversation } from "@/Conversation"; +import { DecodedMessage } from "@/DecodedMessage"; export class Conversations { - #client: Client - #conversations: NapiConversations + #client: Client; + #conversations: NapiConversations; constructor(client: Client, conversations: NapiConversations) { - this.#client = client - this.#conversations = conversations + this.#client = client; + this.#conversations = conversations; } getConversationById(id: string) { try { // findGroupById will throw if group is not found - const group = this.#conversations.findGroupById(id) - return new Conversation(this.#client, group) + const group = this.#conversations.findGroupById(id); + return new Conversation(this.#client, group); } catch { - return null + return null; } } getMessageById(id: string) { try { // findMessageById will throw if message is not found - const message = this.#conversations.findMessageById(id) - return new DecodedMessage(this.#client, message) + const message = this.#conversations.findMessageById(id); + return new DecodedMessage(this.#client, message); } catch { - return null + return null; } } async newConversation( accountAddresses: string[], - options?: NapiCreateGroupOptions + options?: NapiCreateGroupOptions, ) { const group = await this.#conversations.createGroup( accountAddresses, - options - ) - const conversation = new Conversation(this.#client, group) - return conversation + options, + ); + const conversation = new Conversation(this.#client, group); + return conversation; } async list(options?: NapiListMessagesOptions) { - const groups = await this.#conversations.list(options) + const groups = await this.#conversations.list(options); return groups.map((group) => { - const conversation = new Conversation(this.#client, group) - return conversation - }) + const conversation = new Conversation(this.#client, group); + return conversation; + }); } async sync() { - return this.#conversations.sync() + return this.#conversations.sync(); } stream(callback?: StreamCallback) { - const asyncStream = new AsyncStream() + const asyncStream = new AsyncStream(); const stream = this.#conversations.stream((err, group) => { - const conversation = new Conversation(this.#client, group) - asyncStream.callback(err, conversation) - callback?.(err, conversation) - }) + const conversation = new Conversation(this.#client, group); + asyncStream.callback(err, conversation); + callback?.(err, conversation); + }); - asyncStream.stopCallback = stream.end.bind(stream) + asyncStream.stopCallback = stream.end.bind(stream); - return asyncStream + return asyncStream; } async streamAllMessages(callback?: StreamCallback) { // sync conversations first - await this.sync() + await this.sync(); - const asyncStream = new AsyncStream() + const asyncStream = new AsyncStream(); const stream = this.#conversations.streamAllMessages((err, message) => { - const decodedMessage = new DecodedMessage(this.#client, message) - asyncStream.callback(err, decodedMessage) - callback?.(err, decodedMessage) - }) + const decodedMessage = new DecodedMessage(this.#client, message); + asyncStream.callback(err, decodedMessage); + callback?.(err, decodedMessage); + }); - asyncStream.stopCallback = stream.end.bind(stream) + asyncStream.stopCallback = stream.end.bind(stream); - return asyncStream + return asyncStream; } } diff --git a/packages/mls-client/src/DecodedMessage.ts b/packages/mls-client/src/DecodedMessage.ts index aa707d2b0..e46f54e3b 100644 --- a/packages/mls-client/src/DecodedMessage.ts +++ b/packages/mls-client/src/DecodedMessage.ts @@ -1,65 +1,65 @@ -import { ContentTypeId } from '@xmtp/content-type-primitives' +import { ContentTypeId } from "@xmtp/content-type-primitives"; import { NapiDeliveryStatus, NapiGroupMessageKind, type NapiMessage, -} from '@xmtp/mls-client-bindings-node' -import type { Client } from '@/Client' -import { nsToDate } from '@/helpers/date' +} from "@xmtp/mls-client-bindings-node"; +import type { Client } from "@/Client"; +import { nsToDate } from "@/helpers/date"; -export type MessageKind = 'application' | 'membership_change' -export type MessageDeliveryStatus = 'unpublished' | 'published' | 'failed' +export type MessageKind = "application" | "membership_change"; +export type MessageDeliveryStatus = "unpublished" | "published" | "failed"; export class DecodedMessage { - #client: Client - content: any - contentType: ContentTypeId - conversationId: string - deliveryStatus: MessageDeliveryStatus - fallback?: string - compression?: number - id: string - kind: MessageKind - parameters: Record - senderInboxId: string - sentAt: Date - sentAtNs: number + #client: Client; + content: any; + contentType: ContentTypeId; + conversationId: string; + deliveryStatus: MessageDeliveryStatus; + fallback?: string; + compression?: number; + id: string; + kind: MessageKind; + parameters: Record; + senderInboxId: string; + sentAt: Date; + sentAtNs: number; constructor(client: Client, message: NapiMessage) { - this.#client = client - this.id = message.id - this.sentAtNs = message.sentAtNs - this.sentAt = nsToDate(message.sentAtNs) - this.conversationId = message.convoId - this.senderInboxId = message.senderInboxId + this.#client = client; + this.id = message.id; + this.sentAtNs = message.sentAtNs; + this.sentAt = nsToDate(message.sentAtNs); + this.conversationId = message.convoId; + this.senderInboxId = message.senderInboxId; switch (message.kind) { case NapiGroupMessageKind.Application: - this.kind = 'application' - break + this.kind = "application"; + break; case NapiGroupMessageKind.MembershipChange: - this.kind = 'membership_change' - break + this.kind = "membership_change"; + break; // no default } switch (message.deliveryStatus) { case NapiDeliveryStatus.Unpublished: - this.deliveryStatus = 'unpublished' - break + this.deliveryStatus = "unpublished"; + break; case NapiDeliveryStatus.Published: - this.deliveryStatus = 'published' - break + this.deliveryStatus = "published"; + break; case NapiDeliveryStatus.Failed: - this.deliveryStatus = 'failed' - break + this.deliveryStatus = "failed"; + break; // no default } - this.contentType = new ContentTypeId(message.content.type!) - this.parameters = message.content.parameters - this.fallback = message.content.fallback - this.compression = message.content.compression - this.content = this.#client.decodeContent(message, this.contentType) + this.contentType = new ContentTypeId(message.content.type!); + this.parameters = message.content.parameters; + this.fallback = message.content.fallback; + this.compression = message.content.compression; + this.content = this.#client.decodeContent(message, this.contentType); } } diff --git a/packages/mls-client/src/codecs/GroupUpdatedCodec.ts b/packages/mls-client/src/codecs/GroupUpdatedCodec.ts index 402318edc..73b3585d3 100644 --- a/packages/mls-client/src/codecs/GroupUpdatedCodec.ts +++ b/packages/mls-client/src/codecs/GroupUpdatedCodec.ts @@ -2,21 +2,21 @@ import { ContentTypeId, type ContentCodec, type EncodedContent, -} from '@xmtp/content-type-primitives' -import { mlsTranscriptMessages } from '@xmtp/proto' +} from "@xmtp/content-type-primitives"; +import { mlsTranscriptMessages } from "@xmtp/proto"; export const ContentTypeGroupUpdated = new ContentTypeId({ - authorityId: 'xmtp.org', - typeId: 'group_updated', + authorityId: "xmtp.org", + typeId: "group_updated", versionMajor: 1, versionMinor: 0, -}) +}); export class GroupUpdatedCodec implements ContentCodec { get contentType(): ContentTypeId { - return ContentTypeGroupUpdated + return ContentTypeGroupUpdated; } encode(content: mlsTranscriptMessages.GroupUpdated): EncodedContent { @@ -24,18 +24,18 @@ export class GroupUpdatedCodec type: this.contentType, parameters: {}, content: mlsTranscriptMessages.GroupUpdated.encode(content).finish(), - } + }; } decode(content: EncodedContent): mlsTranscriptMessages.GroupUpdated { - return mlsTranscriptMessages.GroupUpdated.decode(content.content) + return mlsTranscriptMessages.GroupUpdated.decode(content.content); } fallback(): undefined { - return undefined + return undefined; } shouldPush() { - return false + return false; } } diff --git a/packages/mls-client/src/helpers/date.ts b/packages/mls-client/src/helpers/date.ts index aa23e7c85..f1afefba1 100644 --- a/packages/mls-client/src/helpers/date.ts +++ b/packages/mls-client/src/helpers/date.ts @@ -1,3 +1,3 @@ export function nsToDate(ns: number): Date { - return new Date(ns / 1_000_000) + return new Date(ns / 1_000_000); } diff --git a/packages/mls-client/src/index.ts b/packages/mls-client/src/index.ts index 23b490609..6cd9e5c2b 100644 --- a/packages/mls-client/src/index.ts +++ b/packages/mls-client/src/index.ts @@ -4,14 +4,14 @@ export type { NetworkOptions, StorageOptions, XmtpEnv, -} from './Client' -export { Client, ApiUrls } from './Client' -export { Conversation } from './Conversation' -export { Conversations } from './Conversations' -export { DecodedMessage } from './DecodedMessage' +} from "./Client"; +export { Client, ApiUrls } from "./Client"; +export { Conversation } from "./Conversation"; +export { Conversations } from "./Conversations"; +export { DecodedMessage } from "./DecodedMessage"; export { ContentTypeGroupUpdated, GroupUpdatedCodec, -} from './codecs/GroupUpdatedCodec' -export type { StreamCallback } from './AsyncStream' -export type * from '@xmtp/mls-client-bindings-node' +} from "./codecs/GroupUpdatedCodec"; +export type { StreamCallback } from "./AsyncStream"; +export type * from "@xmtp/mls-client-bindings-node"; diff --git a/packages/mls-client/test/Client.test.ts b/packages/mls-client/test/Client.test.ts index 178f61cdc..8eb61b85e 100644 --- a/packages/mls-client/test/Client.test.ts +++ b/packages/mls-client/test/Client.test.ts @@ -1,46 +1,50 @@ -import { describe, expect, it } from 'vitest' -import { createClient, createRegisteredClient, createUser } from '@test/helpers' +import { describe, expect, it } from "vitest"; +import { + createClient, + createRegisteredClient, + createUser, +} from "@test/helpers"; -describe('Client', () => { - it('should create a client', async () => { - const user = createUser() - const client = await createClient(user) - expect(client.accountAddress).toBe(user.account.address) - expect(client.isRegistered).toBe(false) - expect(client.signatureText).not.toBe(null) - expect(client.inboxId).toBeDefined() - expect(client.installationId).toBeDefined() - }) +describe("Client", () => { + it("should create a client", async () => { + const user = createUser(); + const client = await createClient(user); + expect(client.accountAddress).toBe(user.account.address); + expect(client.isRegistered).toBe(false); + expect(client.signatureText).not.toBe(null); + expect(client.inboxId).toBeDefined(); + expect(client.installationId).toBeDefined(); + }); - it('should register an identity', async () => { - const user = createUser() - await createRegisteredClient(user) - const client2 = await createRegisteredClient(user) - expect(client2.isRegistered).toBe(true) - expect(await client2.signatureText()).toBe(null) + it("should register an identity", async () => { + const user = createUser(); + await createRegisteredClient(user); + const client2 = await createRegisteredClient(user); + expect(client2.isRegistered).toBe(true); + expect(await client2.signatureText()).toBe(null); expect(await client2.canMessage([user.account.address])).toEqual({ [user.account.address.toLowerCase()]: true, - }) - }) + }); + }); - it('should get an inbox ID from an address', async () => { - const user = createUser() - const client = await createRegisteredClient(user) - const inboxId = await client.getInboxIdByAddress(user.account.address) - expect(inboxId).toBe(client.inboxId) - }) + it("should get an inbox ID from an address", async () => { + const user = createUser(); + const client = await createRegisteredClient(user); + const inboxId = await client.getInboxIdByAddress(user.account.address); + expect(inboxId).toBe(client.inboxId); + }); - it('should return the correct inbox state', async () => { - const user = createUser() - const client = await createRegisteredClient(user) - const inboxState = await client.inboxState() - expect(inboxState.inboxId).toBe(client.inboxId) + it("should return the correct inbox state", async () => { + const user = createUser(); + const client = await createRegisteredClient(user); + const inboxState = await client.inboxState(); + expect(inboxState.inboxId).toBe(client.inboxId); expect(inboxState.installations.map((install) => install.id)).toEqual([ client.installationId, - ]) + ]); expect(inboxState.accountAddresses).toEqual([ user.account.address.toLowerCase(), - ]) - expect(inboxState.recoveryAddress).toBe(user.account.address.toLowerCase()) - }) -}) + ]); + expect(inboxState.recoveryAddress).toBe(user.account.address.toLowerCase()); + }); +}); diff --git a/packages/mls-client/test/Conversation.test.ts b/packages/mls-client/test/Conversation.test.ts index 541dae8db..82486e107 100644 --- a/packages/mls-client/test/Conversation.test.ts +++ b/packages/mls-client/test/Conversation.test.ts @@ -1,393 +1,393 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it } from "vitest"; import { ContentTypeTest, createRegisteredClient, createUser, TestCodec, -} from '@test/helpers' - -describe('Conversation', () => { - it('should update conversation name', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) +} from "@test/helpers"; + +describe("Conversation", () => { + it("should update conversation name", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) - const newName = 'foo' - await conversation.updateName(newName) - expect(conversation.name).toBe(newName) - const messages = conversation.messages() - expect(messages.length).toBe(2) - - await client2.conversations.sync() - const conversations = await client2.conversations.list() - expect(conversations.length).toBe(1) - - const conversation2 = conversations[0] - expect(conversation2).toBeDefined() - await conversation2.sync() - expect(conversation2.id).toBe(conversation.id) - expect(conversation2.name).toBe(newName) - expect(conversation2.messages().length).toBe(1) - }) - - it('should update conversation image URL', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) + ]); + const newName = "foo"; + await conversation.updateName(newName); + expect(conversation.name).toBe(newName); + const messages = conversation.messages(); + expect(messages.length).toBe(2); + + await client2.conversations.sync(); + const conversations = await client2.conversations.list(); + expect(conversations.length).toBe(1); + + const conversation2 = conversations[0]; + expect(conversation2).toBeDefined(); + await conversation2.sync(); + expect(conversation2.id).toBe(conversation.id); + expect(conversation2.name).toBe(newName); + expect(conversation2.messages().length).toBe(1); + }); + + it("should update conversation image URL", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) - const imageUrl = 'https://foo/bar.jpg' - await conversation.updateImageUrl(imageUrl) - expect(conversation.imageUrl).toBe(imageUrl) - const messages = conversation.messages() - expect(messages.length).toBe(2) - - await client2.conversations.sync() - const conversations = await client2.conversations.list() - expect(conversations.length).toBe(1) - - const conversation2 = conversations[0] - expect(conversation2).toBeDefined() - await conversation2.sync() - expect(conversation2.id).toBe(conversation.id) - expect(conversation2.imageUrl).toBe(imageUrl) - expect(conversation2.messages().length).toBe(1) - }) - - it('should update conversation description', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) + ]); + const imageUrl = "https://foo/bar.jpg"; + await conversation.updateImageUrl(imageUrl); + expect(conversation.imageUrl).toBe(imageUrl); + const messages = conversation.messages(); + expect(messages.length).toBe(2); + + await client2.conversations.sync(); + const conversations = await client2.conversations.list(); + expect(conversations.length).toBe(1); + + const conversation2 = conversations[0]; + expect(conversation2).toBeDefined(); + await conversation2.sync(); + expect(conversation2.id).toBe(conversation.id); + expect(conversation2.imageUrl).toBe(imageUrl); + expect(conversation2.messages().length).toBe(1); + }); + + it("should update conversation description", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) - const newDescription = 'foo' - await conversation.updateDescription(newDescription) - expect(conversation.description).toBe(newDescription) - const messages = conversation.messages() - expect(messages.length).toBe(2) - - await client2.conversations.sync() - const conversations = await client2.conversations.list() - expect(conversations.length).toBe(1) - - const conversation2 = conversations[0] - expect(conversation2).toBeDefined() - await conversation2.sync() - expect(conversation2.id).toBe(conversation.id) - expect(conversation2.description).toBe(newDescription) - expect(conversation2.messages().length).toBe(1) - }) - - it('should update conversation pinned frame URL', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) + ]); + const newDescription = "foo"; + await conversation.updateDescription(newDescription); + expect(conversation.description).toBe(newDescription); + const messages = conversation.messages(); + expect(messages.length).toBe(2); + + await client2.conversations.sync(); + const conversations = await client2.conversations.list(); + expect(conversations.length).toBe(1); + + const conversation2 = conversations[0]; + expect(conversation2).toBeDefined(); + await conversation2.sync(); + expect(conversation2.id).toBe(conversation.id); + expect(conversation2.description).toBe(newDescription); + expect(conversation2.messages().length).toBe(1); + }); + + it("should update conversation pinned frame URL", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) - const pinnedFrameUrl = 'https://foo/bar' - await conversation.updatePinnedFrameUrl(pinnedFrameUrl) - expect(conversation.pinnedFrameUrl).toBe(pinnedFrameUrl) - const messages = conversation.messages() - expect(messages.length).toBe(2) - - await client2.conversations.sync() - const conversations = await client2.conversations.list() - expect(conversations.length).toBe(1) - - const conversation2 = conversations[0] - expect(conversation2).toBeDefined() - await conversation2.sync() - expect(conversation2.id).toBe(conversation.id) - expect(conversation2.pinnedFrameUrl).toBe(pinnedFrameUrl) - expect(conversation2.messages().length).toBe(1) - }) - - it('should add and remove members', async () => { - const user1 = createUser() - const user2 = createUser() - const user3 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) - const client3 = await createRegisteredClient(user3) + ]); + const pinnedFrameUrl = "https://foo/bar"; + await conversation.updatePinnedFrameUrl(pinnedFrameUrl); + expect(conversation.pinnedFrameUrl).toBe(pinnedFrameUrl); + const messages = conversation.messages(); + expect(messages.length).toBe(2); + + await client2.conversations.sync(); + const conversations = await client2.conversations.list(); + expect(conversations.length).toBe(1); + + const conversation2 = conversations[0]; + expect(conversation2).toBeDefined(); + await conversation2.sync(); + expect(conversation2.id).toBe(conversation.id); + expect(conversation2.pinnedFrameUrl).toBe(pinnedFrameUrl); + expect(conversation2.messages().length).toBe(1); + }); + + it("should add and remove members", async () => { + const user1 = createUser(); + const user2 = createUser(); + const user3 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); + const client3 = await createRegisteredClient(user3); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) + ]); - const members = await conversation.members() + const members = await conversation.members(); - const memberInboxIds = members.map((member) => member.inboxId) - expect(memberInboxIds).toContain(client1.inboxId) - expect(memberInboxIds).toContain(client2.inboxId) - expect(memberInboxIds).not.toContain(client3.inboxId) + const memberInboxIds = members.map((member) => member.inboxId); + expect(memberInboxIds).toContain(client1.inboxId); + expect(memberInboxIds).toContain(client2.inboxId); + expect(memberInboxIds).not.toContain(client3.inboxId); - await conversation.addMembers([user3.account.address]) + await conversation.addMembers([user3.account.address]); - const members2 = await conversation.members() - expect(members2.length).toBe(3) + const members2 = await conversation.members(); + expect(members2.length).toBe(3); - const memberInboxIds2 = members2.map((member) => member.inboxId) - expect(memberInboxIds2).toContain(client1.inboxId) - expect(memberInboxIds2).toContain(client2.inboxId) - expect(memberInboxIds2).toContain(client3.inboxId) + const memberInboxIds2 = members2.map((member) => member.inboxId); + expect(memberInboxIds2).toContain(client1.inboxId); + expect(memberInboxIds2).toContain(client2.inboxId); + expect(memberInboxIds2).toContain(client3.inboxId); - await conversation.removeMembers([user2.account.address]) + await conversation.removeMembers([user2.account.address]); - const members3 = await conversation.members() - expect(members3.length).toBe(2) + const members3 = await conversation.members(); + expect(members3.length).toBe(2); - const memberInboxIds3 = members3.map((member) => member.inboxId) - expect(memberInboxIds3).toContain(client1.inboxId) - expect(memberInboxIds3).not.toContain(client2.inboxId) - expect(memberInboxIds3).toContain(client3.inboxId) - }) + const memberInboxIds3 = members3.map((member) => member.inboxId); + expect(memberInboxIds3).toContain(client1.inboxId); + expect(memberInboxIds3).not.toContain(client2.inboxId); + expect(memberInboxIds3).toContain(client3.inboxId); + }); - it('should add and remove members by inbox id', async () => { - const user1 = createUser() - const user2 = createUser() - const user3 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) - const client3 = await createRegisteredClient(user3) + it("should add and remove members by inbox id", async () => { + const user1 = createUser(); + const user2 = createUser(); + const user3 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); + const client3 = await createRegisteredClient(user3); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) + ]); - const members = await conversation.members() - const memberInboxIds = members.map((member) => member.inboxId) - expect(memberInboxIds).toContain(client1.inboxId) - expect(memberInboxIds).toContain(client2.inboxId) - expect(memberInboxIds).not.toContain(client3.inboxId) + const members = await conversation.members(); + const memberInboxIds = members.map((member) => member.inboxId); + expect(memberInboxIds).toContain(client1.inboxId); + expect(memberInboxIds).toContain(client2.inboxId); + expect(memberInboxIds).not.toContain(client3.inboxId); - await conversation.addMembersByInboxId([client3.inboxId]) + await conversation.addMembersByInboxId([client3.inboxId]); - const members2 = await conversation.members() - expect(members2.length).toBe(3) + const members2 = await conversation.members(); + expect(members2.length).toBe(3); - const memberInboxIds2 = members2.map((member) => member.inboxId) - expect(memberInboxIds2).toContain(client1.inboxId) - expect(memberInboxIds2).toContain(client2.inboxId) - expect(memberInboxIds2).toContain(client3.inboxId) + const memberInboxIds2 = members2.map((member) => member.inboxId); + expect(memberInboxIds2).toContain(client1.inboxId); + expect(memberInboxIds2).toContain(client2.inboxId); + expect(memberInboxIds2).toContain(client3.inboxId); - await conversation.removeMembersByInboxId([client2.inboxId]) + await conversation.removeMembersByInboxId([client2.inboxId]); - const members3 = await conversation.members() - expect(members3.length).toBe(2) + const members3 = await conversation.members(); + expect(members3.length).toBe(2); - const memberInboxIds3 = members3.map((member) => member.inboxId) - expect(memberInboxIds3).toContain(client1.inboxId) - expect(memberInboxIds3).not.toContain(client2.inboxId) - expect(memberInboxIds3).toContain(client3.inboxId) - }) + const memberInboxIds3 = members3.map((member) => member.inboxId); + expect(memberInboxIds3).toContain(client1.inboxId); + expect(memberInboxIds3).not.toContain(client2.inboxId); + expect(memberInboxIds3).toContain(client3.inboxId); + }); - it('should send and list messages', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) + it("should send and list messages", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) + ]); - const text = 'gm' - await conversation.send(text) + const text = "gm"; + await conversation.send(text); - const messages = conversation.messages() - expect(messages.length).toBe(2) - expect(messages[1].content).toBe(text) + const messages = conversation.messages(); + expect(messages.length).toBe(2); + expect(messages[1].content).toBe(text); - await client2.conversations.sync() - const conversations = await client2.conversations.list() - expect(conversations.length).toBe(1) + await client2.conversations.sync(); + const conversations = await client2.conversations.list(); + expect(conversations.length).toBe(1); - const conversation2 = conversations[0] - expect(conversation2).toBeDefined() - await conversation2.sync() - expect(conversation2.id).toBe(conversation.id) + const conversation2 = conversations[0]; + expect(conversation2).toBeDefined(); + await conversation2.sync(); + expect(conversation2.id).toBe(conversation.id); - const messages2 = conversation2.messages() - expect(messages2.length).toBe(1) - expect(messages2[0].content).toBe(text) - }) + const messages2 = conversation2.messages(); + expect(messages2.length).toBe(1); + expect(messages2[0].content).toBe(text); + }); - it('should require content type when sending non-string content', async () => { - const user1 = createUser() - const user2 = createUser() + it("should require content type when sending non-string content", async () => { + const user1 = createUser(); + const user2 = createUser(); const client1 = await createRegisteredClient(user1, { codecs: [new TestCodec()], - }) - await createRegisteredClient(user2) + }); + await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) + ]); - await expect(() => conversation.send(1)).rejects.toThrow() - await expect(() => conversation.send({ foo: 'bar' })).rejects.toThrow() + await expect(() => conversation.send(1)).rejects.toThrow(); + await expect(() => conversation.send({ foo: "bar" })).rejects.toThrow(); await expect( - conversation.send({ foo: 'bar' }, ContentTypeTest) - ).resolves.not.toThrow() - }) - - it('should optimistically send and list messages', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) + conversation.send({ foo: "bar" }, ContentTypeTest), + ).resolves.not.toThrow(); + }); + + it("should optimistically send and list messages", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) + ]); - const text = 'gm' - conversation.sendOptimistic(text) + const text = "gm"; + conversation.sendOptimistic(text); - const messages = conversation.messages() - expect(messages.length).toBe(2) - expect(messages[1].content).toBe(text) + const messages = conversation.messages(); + expect(messages.length).toBe(2); + expect(messages[1].content).toBe(text); - await client2.conversations.sync() - const conversations = await client2.conversations.list() - expect(conversations.length).toBe(1) + await client2.conversations.sync(); + const conversations = await client2.conversations.list(); + expect(conversations.length).toBe(1); - const conversation2 = conversations[0] - expect(conversation2).toBeDefined() + const conversation2 = conversations[0]; + expect(conversation2).toBeDefined(); - await conversation2.sync() - expect(conversation2.id).toBe(conversation.id) + await conversation2.sync(); + expect(conversation2.id).toBe(conversation.id); - const messages2 = conversation2.messages() - expect(messages2.length).toBe(0) + const messages2 = conversation2.messages(); + expect(messages2.length).toBe(0); - await conversation.publishMessages() - await conversation2.sync() + await conversation.publishMessages(); + await conversation2.sync(); - const messages4 = conversation2.messages() - expect(messages4.length).toBe(1) - expect(messages4[0].content).toBe(text) - }) + const messages4 = conversation2.messages(); + expect(messages4.length).toBe(1); + expect(messages4[0].content).toBe(text); + }); - it('should require content type when optimistically sending non-string content', async () => { - const user1 = createUser() - const user2 = createUser() + it("should require content type when optimistically sending non-string content", async () => { + const user1 = createUser(); + const user2 = createUser(); const client1 = await createRegisteredClient(user1, { codecs: [new TestCodec()], - }) - await createRegisteredClient(user2) + }); + await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) + ]); - expect(() => conversation.sendOptimistic(1)).toThrow() - expect(() => conversation.sendOptimistic({ foo: 'bar' })).toThrow() + expect(() => conversation.sendOptimistic(1)).toThrow(); + expect(() => conversation.sendOptimistic({ foo: "bar" })).toThrow(); expect(() => - conversation.sendOptimistic({ foo: 'bar' }, ContentTypeTest) - ).not.toThrow() - }) - - it('should throw when sending content without a codec', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - await createRegisteredClient(user2) + conversation.sendOptimistic({ foo: "bar" }, ContentTypeTest), + ).not.toThrow(); + }); + + it("should throw when sending content without a codec", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) + ]); await expect( - conversation.send({ foo: 'bar' }, ContentTypeTest) - ).rejects.toThrow() - }) - - it('should stream messages', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) + conversation.send({ foo: "bar" }, ContentTypeTest), + ).rejects.toThrow(); + }); + + it("should stream messages", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) + ]); - await client2.conversations.sync() - const conversation2 = await client2.conversations.list() - expect(conversation2.length).toBe(1) - expect(conversation2[0].id).toBe(conversation.id) + await client2.conversations.sync(); + const conversation2 = await client2.conversations.list(); + expect(conversation2.length).toBe(1); + expect(conversation2[0].id).toBe(conversation.id); - const stream = conversation2[0].stream() + const stream = conversation2[0].stream(); - await conversation.send('gm') - await conversation.send('gm2') + await conversation.send("gm"); + await conversation.send("gm2"); - let count = 0 + let count = 0; for await (const message of stream) { - count++ - expect(message).toBeDefined() + count++; + expect(message).toBeDefined(); if (count === 1) { - expect(message!.content).toBe('gm') + expect(message!.content).toBe("gm"); } if (count === 2) { - expect(message!.content).toBe('gm2') - break + expect(message!.content).toBe("gm2"); + break; } } - stream.stop() - }) - - it('should add and remove admins', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) + stream.stop(); + }); + + it("should add and remove admins", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) - - expect(conversation.isSuperAdmin(client1.inboxId)).toBe(true) - expect(conversation.superAdmins.length).toBe(1) - expect(conversation.superAdmins).toContain(client1.inboxId) - expect(conversation.isAdmin(client1.inboxId)).toBe(false) - expect(conversation.isAdmin(client2.inboxId)).toBe(false) - expect(conversation.admins.length).toBe(0) - - await conversation.addAdmin(client2.inboxId) - expect(conversation.isAdmin(client2.inboxId)).toBe(true) - expect(conversation.admins.length).toBe(1) - expect(conversation.admins).toContain(client2.inboxId) - - await conversation.removeAdmin(client2.inboxId) - expect(conversation.isAdmin(client2.inboxId)).toBe(false) - expect(conversation.admins.length).toBe(0) - }) - - it('should add and remove super admins', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) + ]); + + expect(conversation.isSuperAdmin(client1.inboxId)).toBe(true); + expect(conversation.superAdmins.length).toBe(1); + expect(conversation.superAdmins).toContain(client1.inboxId); + expect(conversation.isAdmin(client1.inboxId)).toBe(false); + expect(conversation.isAdmin(client2.inboxId)).toBe(false); + expect(conversation.admins.length).toBe(0); + + await conversation.addAdmin(client2.inboxId); + expect(conversation.isAdmin(client2.inboxId)).toBe(true); + expect(conversation.admins.length).toBe(1); + expect(conversation.admins).toContain(client2.inboxId); + + await conversation.removeAdmin(client2.inboxId); + expect(conversation.isAdmin(client2.inboxId)).toBe(false); + expect(conversation.admins.length).toBe(0); + }); + + it("should add and remove super admins", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) - - expect(conversation.isSuperAdmin(client1.inboxId)).toBe(true) - expect(conversation.isSuperAdmin(client2.inboxId)).toBe(false) - expect(conversation.superAdmins.length).toBe(1) - expect(conversation.superAdmins).toContain(client1.inboxId) - - await conversation.addSuperAdmin(client2.inboxId) - expect(conversation.isSuperAdmin(client2.inboxId)).toBe(true) - expect(conversation.superAdmins.length).toBe(2) - expect(conversation.superAdmins).toContain(client1.inboxId) - expect(conversation.superAdmins).toContain(client2.inboxId) - - await conversation.removeSuperAdmin(client2.inboxId) - expect(conversation.isSuperAdmin(client2.inboxId)).toBe(false) - expect(conversation.superAdmins.length).toBe(1) - expect(conversation.superAdmins).toContain(client1.inboxId) - }) -}) + ]); + + expect(conversation.isSuperAdmin(client1.inboxId)).toBe(true); + expect(conversation.isSuperAdmin(client2.inboxId)).toBe(false); + expect(conversation.superAdmins.length).toBe(1); + expect(conversation.superAdmins).toContain(client1.inboxId); + + await conversation.addSuperAdmin(client2.inboxId); + expect(conversation.isSuperAdmin(client2.inboxId)).toBe(true); + expect(conversation.superAdmins.length).toBe(2); + expect(conversation.superAdmins).toContain(client1.inboxId); + expect(conversation.superAdmins).toContain(client2.inboxId); + + await conversation.removeSuperAdmin(client2.inboxId); + expect(conversation.isSuperAdmin(client2.inboxId)).toBe(false); + expect(conversation.superAdmins.length).toBe(1); + expect(conversation.superAdmins).toContain(client1.inboxId); + }); +}); diff --git a/packages/mls-client/test/Conversations.test.ts b/packages/mls-client/test/Conversations.test.ts index 939dcd9f1..388a0d74d 100644 --- a/packages/mls-client/test/Conversations.test.ts +++ b/packages/mls-client/test/Conversations.test.ts @@ -1,35 +1,35 @@ -import { NapiGroupPermissionsOptions } from '@xmtp/mls-client-bindings-node' -import { describe, expect, it } from 'vitest' -import { createRegisteredClient, createUser } from '@test/helpers' +import { NapiGroupPermissionsOptions } from "@xmtp/mls-client-bindings-node"; +import { describe, expect, it } from "vitest"; +import { createRegisteredClient, createUser } from "@test/helpers"; -describe('Conversations', () => { - it('should not have initial conversations', async () => { - const user = createUser() - const client = await createRegisteredClient(user) - const conversations = client.conversations.list() - expect((await conversations).length).toBe(0) - }) +describe("Conversations", () => { + it("should not have initial conversations", async () => { + const user = createUser(); + const client = await createRegisteredClient(user); + const conversations = client.conversations.list(); + expect((await conversations).length).toBe(0); + }); - it('should create a new conversation', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) + it("should create a new conversation", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); const conversation = await client1.conversations.newConversation([ user2.account.address, - ]) - expect(conversation).toBeDefined() + ]); + expect(conversation).toBeDefined(); expect(client1.conversations.getConversationById(conversation.id)?.id).toBe( - conversation.id - ) - expect(conversation.id).toBeDefined() - expect(conversation.createdAt).toBeDefined() - expect(conversation.createdAtNs).toBeDefined() - expect(conversation.isActive).toBe(true) - expect(conversation.name).toBe('') + conversation.id, + ); + expect(conversation.id).toBeDefined(); + expect(conversation.createdAt).toBeDefined(); + expect(conversation.createdAtNs).toBeDefined(); + expect(conversation.isActive).toBe(true); + expect(conversation.name).toBe(""); expect(conversation.permissions.policyType).toBe( - NapiGroupPermissionsOptions.AllMembers - ) + NapiGroupPermissionsOptions.AllMembers, + ); expect(conversation.permissions.policySet).toEqual({ addMemberPolicy: 0, removeMemberPolicy: 2, @@ -39,116 +39,116 @@ describe('Conversations', () => { updateGroupDescriptionPolicy: 0, updateGroupImageUrlSquarePolicy: 0, updateGroupPinnedFrameUrlPolicy: 0, - }) - expect(conversation.addedByInboxId).toBe(client1.inboxId) - expect(conversation.messages().length).toBe(1) + }); + expect(conversation.addedByInboxId).toBe(client1.inboxId); + expect(conversation.messages().length).toBe(1); - const members = await conversation.members() - expect(members.length).toBe(2) - const memberInboxIds = members.map((member) => member.inboxId) - expect(memberInboxIds).toContain(client1.inboxId) - expect(memberInboxIds).toContain(client2.inboxId) + const members = await conversation.members(); + expect(members.length).toBe(2); + const memberInboxIds = members.map((member) => member.inboxId); + expect(memberInboxIds).toContain(client1.inboxId); + expect(memberInboxIds).toContain(client2.inboxId); expect(conversation.metadata).toEqual({ - conversationType: 'group', + conversationType: "group", creatorInboxId: client1.inboxId, - }) + }); - const conversations1 = await client1.conversations.list() - expect(conversations1.length).toBe(1) - expect(conversations1[0].id).toBe(conversation.id) + const conversations1 = await client1.conversations.list(); + expect(conversations1.length).toBe(1); + expect(conversations1[0].id).toBe(conversation.id); - expect((await client2.conversations.list()).length).toBe(0) + expect((await client2.conversations.list()).length).toBe(0); - await client2.conversations.sync() + await client2.conversations.sync(); - const conversations2 = await client2.conversations.list() - expect(conversations2.length).toBe(1) - expect(conversations2[0].id).toBe(conversation.id) - }) + const conversations2 = await client2.conversations.list(); + expect(conversations2.length).toBe(1); + expect(conversations2[0].id).toBe(conversation.id); + }); - it('should get a group by ID', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - await createRegisteredClient(user2) + it("should get a group by ID", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + await createRegisteredClient(user2); const group = await client1.conversations.newConversation([ user2.account.address, - ]) - expect(group).toBeDefined() - expect(group.id).toBeDefined() - const foundGroup = client1.conversations.getConversationById(group.id) - expect(foundGroup).toBeDefined() - expect(foundGroup!.id).toBe(group.id) - }) + ]); + expect(group).toBeDefined(); + expect(group.id).toBeDefined(); + const foundGroup = client1.conversations.getConversationById(group.id); + expect(foundGroup).toBeDefined(); + expect(foundGroup!.id).toBe(group.id); + }); - it('should get a message by ID', async () => { - const user1 = createUser() - const user2 = createUser() - const client1 = await createRegisteredClient(user1) - await createRegisteredClient(user2) + it("should get a message by ID", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + await createRegisteredClient(user2); const group = await client1.conversations.newConversation([ user2.account.address, - ]) - const messageId = await group.send('gm!') - expect(messageId).toBeDefined() + ]); + const messageId = await group.send("gm!"); + expect(messageId).toBeDefined(); - const message = client1.conversations.getMessageById(messageId) - expect(message).toBeDefined() - expect(message!.id).toBe(messageId) - }) + const message = client1.conversations.getMessageById(messageId); + expect(message).toBeDefined(); + expect(message!.id).toBe(messageId); + }); - it('should create a new conversation with options', async () => { - const user1 = createUser() - const user2 = createUser() - const user3 = createUser() - const user4 = createUser() - const user5 = createUser() - const client1 = await createRegisteredClient(user1) - await createRegisteredClient(user2) - await createRegisteredClient(user3) - await createRegisteredClient(user4) - await createRegisteredClient(user5) + it("should create a new conversation with options", async () => { + const user1 = createUser(); + const user2 = createUser(); + const user3 = createUser(); + const user4 = createUser(); + const user5 = createUser(); + const client1 = await createRegisteredClient(user1); + await createRegisteredClient(user2); + await createRegisteredClient(user3); + await createRegisteredClient(user4); + await createRegisteredClient(user5); const groupWithName = await client1.conversations.newConversation( [user2.account.address], { - groupName: 'foo', - } - ) - expect(groupWithName).toBeDefined() - expect(groupWithName.name).toBe('foo') - expect(groupWithName.imageUrl).toBe('') + groupName: "foo", + }, + ); + expect(groupWithName).toBeDefined(); + expect(groupWithName.name).toBe("foo"); + expect(groupWithName.imageUrl).toBe(""); const groupWithImageUrl = await client1.conversations.newConversation( [user3.account.address], { - groupImageUrlSquare: 'https://foo/bar.png', - } - ) - expect(groupWithImageUrl).toBeDefined() - expect(groupWithImageUrl.name).toBe('') - expect(groupWithImageUrl.imageUrl).toBe('https://foo/bar.png') + groupImageUrlSquare: "https://foo/bar.png", + }, + ); + expect(groupWithImageUrl).toBeDefined(); + expect(groupWithImageUrl.name).toBe(""); + expect(groupWithImageUrl.imageUrl).toBe("https://foo/bar.png"); const groupWithNameAndImageUrl = await client1.conversations.newConversation([user4.account.address], { - groupImageUrlSquare: 'https://foo/bar.png', - groupName: 'foo', - }) - expect(groupWithNameAndImageUrl).toBeDefined() - expect(groupWithNameAndImageUrl.name).toBe('foo') - expect(groupWithNameAndImageUrl.imageUrl).toBe('https://foo/bar.png') + groupImageUrlSquare: "https://foo/bar.png", + groupName: "foo", + }); + expect(groupWithNameAndImageUrl).toBeDefined(); + expect(groupWithNameAndImageUrl.name).toBe("foo"); + expect(groupWithNameAndImageUrl.imageUrl).toBe("https://foo/bar.png"); const groupWithPermissions = await client1.conversations.newConversation( [user4.account.address], { permissions: NapiGroupPermissionsOptions.AdminOnly, - } - ) - expect(groupWithPermissions).toBeDefined() - expect(groupWithPermissions.name).toBe('') - expect(groupWithPermissions.imageUrl).toBe('') + }, + ); + expect(groupWithPermissions).toBeDefined(); + expect(groupWithPermissions.name).toBe(""); + expect(groupWithPermissions.imageUrl).toBe(""); expect(groupWithPermissions.permissions.policyType).toBe( - NapiGroupPermissionsOptions.AdminOnly - ) + NapiGroupPermissionsOptions.AdminOnly, + ); expect(groupWithPermissions.permissions.policySet).toEqual({ addMemberPolicy: 2, @@ -159,101 +159,101 @@ describe('Conversations', () => { updateGroupDescriptionPolicy: 2, updateGroupImageUrlSquarePolicy: 2, updateGroupPinnedFrameUrlPolicy: 2, - }) + }); const groupWithDescription = await client1.conversations.newConversation( [user2.account.address], { - groupDescription: 'foo', - } - ) - expect(groupWithDescription).toBeDefined() - expect(groupWithDescription.name).toBe('') - expect(groupWithDescription.imageUrl).toBe('') - expect(groupWithDescription.description).toBe('foo') + groupDescription: "foo", + }, + ); + expect(groupWithDescription).toBeDefined(); + expect(groupWithDescription.name).toBe(""); + expect(groupWithDescription.imageUrl).toBe(""); + expect(groupWithDescription.description).toBe("foo"); const groupWithPinnedFrameUrl = await client1.conversations.newConversation( [user2.account.address], { - groupPinnedFrameUrl: 'https://foo/bar', - } - ) - expect(groupWithPinnedFrameUrl).toBeDefined() - expect(groupWithPinnedFrameUrl.name).toBe('') - expect(groupWithPinnedFrameUrl.imageUrl).toBe('') - expect(groupWithPinnedFrameUrl.description).toBe('') - expect(groupWithPinnedFrameUrl.pinnedFrameUrl).toBe('https://foo/bar') - }) + groupPinnedFrameUrl: "https://foo/bar", + }, + ); + expect(groupWithPinnedFrameUrl).toBeDefined(); + expect(groupWithPinnedFrameUrl.name).toBe(""); + expect(groupWithPinnedFrameUrl.imageUrl).toBe(""); + expect(groupWithPinnedFrameUrl.description).toBe(""); + expect(groupWithPinnedFrameUrl.pinnedFrameUrl).toBe("https://foo/bar"); + }); - it('should stream new conversations', async () => { - const user1 = createUser() - const user2 = createUser() - const user3 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) - const client3 = await createRegisteredClient(user3) - const stream = client3.conversations.stream() + it("should stream new conversations", async () => { + const user1 = createUser(); + const user2 = createUser(); + const user3 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); + const client3 = await createRegisteredClient(user3); + const stream = client3.conversations.stream(); const conversation1 = await client1.conversations.newConversation([ user3.account.address, - ]) + ]); const conversation2 = await client2.conversations.newConversation([ user3.account.address, - ]) - let count = 0 + ]); + let count = 0; for await (const convo of stream) { - count++ - expect(convo).toBeDefined() + count++; + expect(convo).toBeDefined(); if (count === 1) { - expect(convo!.id).toBe(conversation1.id) + expect(convo!.id).toBe(conversation1.id); } if (count === 2) { - expect(convo!.id).toBe(conversation2.id) - break + expect(convo!.id).toBe(conversation2.id); + break; } } - stream.stop() + stream.stop(); expect( - client3.conversations.getConversationById(conversation1.id)?.id - ).toBe(conversation1.id) + client3.conversations.getConversationById(conversation1.id)?.id, + ).toBe(conversation1.id); expect( - client3.conversations.getConversationById(conversation2.id)?.id - ).toBe(conversation2.id) - }) + client3.conversations.getConversationById(conversation2.id)?.id, + ).toBe(conversation2.id); + }); - it('should stream all messages', async () => { - const user1 = createUser() - const user2 = createUser() - const user3 = createUser() - const client1 = await createRegisteredClient(user1) - const client2 = await createRegisteredClient(user2) - const client3 = await createRegisteredClient(user3) - await client1.conversations.newConversation([user2.account.address]) - await client1.conversations.newConversation([user3.account.address]) + it("should stream all messages", async () => { + const user1 = createUser(); + const user2 = createUser(); + const user3 = createUser(); + const client1 = await createRegisteredClient(user1); + const client2 = await createRegisteredClient(user2); + const client3 = await createRegisteredClient(user3); + await client1.conversations.newConversation([user2.account.address]); + await client1.conversations.newConversation([user3.account.address]); - const stream = await client1.conversations.streamAllMessages() + const stream = await client1.conversations.streamAllMessages(); - await client2.conversations.sync() - const groups2 = await client2.conversations.list() + await client2.conversations.sync(); + const groups2 = await client2.conversations.list(); - await client3.conversations.sync() - const groups3 = await client3.conversations.list() + await client3.conversations.sync(); + const groups3 = await client3.conversations.list(); - await groups2[0].send('gm!') - await groups3[0].send('gm2!') + await groups2[0].send("gm!"); + await groups3[0].send("gm2!"); - let count = 0 + let count = 0; for await (const message of stream) { - count++ - expect(message).toBeDefined() + count++; + expect(message).toBeDefined(); if (count === 1) { - expect(message!.senderInboxId).toBe(client2.inboxId) + expect(message!.senderInboxId).toBe(client2.inboxId); } if (count === 2) { - expect(message!.senderInboxId).toBe(client3.inboxId) - break + expect(message!.senderInboxId).toBe(client3.inboxId); + break; } } - stream.stop() - }) -}) + stream.stop(); + }); +}); diff --git a/packages/mls-client/test/helpers.ts b/packages/mls-client/test/helpers.ts index 76bbb259d..57f087443 100644 --- a/packages/mls-client/test/helpers.ts +++ b/packages/mls-client/test/helpers.ts @@ -1,20 +1,20 @@ -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; import { ContentTypeId, type ContentCodec, type EncodedContent, -} from '@xmtp/content-type-primitives' -import { createWalletClient, http, toBytes } from 'viem' -import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' -import { sepolia } from 'viem/chains' -import { Client, type ClientOptions } from '@/Client' +} from "@xmtp/content-type-primitives"; +import { createWalletClient, http, toBytes } from "viem"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { sepolia } from "viem/chains"; +import { Client, type ClientOptions } from "@/Client"; -const __dirname = dirname(fileURLToPath(import.meta.url)) +const __dirname = dirname(fileURLToPath(import.meta.url)); export const createUser = () => { - const key = generatePrivateKey() - const account = privateKeyToAccount(key) + const key = generatePrivateKey(); + const account = privateKeyToAccount(key); return { key, account, @@ -23,58 +23,58 @@ export const createUser = () => { chain: sepolia, transport: http(), }), - } -} + }; +}; -export type User = ReturnType +export type User = ReturnType; export const getSignature = async (client: Client, user: User) => { - const signatureText = await client.signatureText() + const signatureText = await client.signatureText(); if (signatureText) { const signature = await user.wallet.signMessage({ message: signatureText, - }) - return toBytes(signature) + }); + return toBytes(signature); } - return null -} + return null; +}; export const createClient = async (user: User, options?: ClientOptions) => { const opts = { ...options, - env: options?.env ?? 'local', - } + env: options?.env ?? "local", + }; return Client.create(user.account.address, { ...opts, dbPath: join(__dirname, `./test-${user.account.address}.db3`), - }) -} + }); +}; export const createRegisteredClient = async ( user: User, - options?: ClientOptions + options?: ClientOptions, ) => { - const client = await createClient(user, options) + const client = await createClient(user, options); if (!client.isRegistered) { - const signature = await getSignature(client, user) + const signature = await getSignature(client, user); if (signature) { - client.addSignature(signature) + client.addSignature(signature); } - await client.registerIdentity() + await client.registerIdentity(); } - return client -} + return client; +}; export const ContentTypeTest = new ContentTypeId({ - authorityId: 'xmtp.org', - typeId: 'test', + authorityId: "xmtp.org", + typeId: "test", versionMajor: 1, versionMinor: 0, -}) +}); export class TestCodec implements ContentCodec> { get contentType(): ContentTypeId { - return ContentTypeTest + return ContentTypeTest; } encode(content: Record): EncodedContent { @@ -82,19 +82,19 @@ export class TestCodec implements ContentCodec> { type: this.contentType, parameters: {}, content: new TextEncoder().encode(JSON.stringify(content)), - } + }; } decode(content: EncodedContent) { - const decoded = new TextDecoder().decode(content.content) - return JSON.parse(decoded) + const decoded = new TextDecoder().decode(content.content); + return JSON.parse(decoded); } fallback() { - return undefined + return undefined; } shouldPush() { - return false + return false; } } diff --git a/packages/mls-client/vitest.config.ts b/packages/mls-client/vitest.config.ts index 2ce26afbe..d6b1e8b9b 100644 --- a/packages/mls-client/vitest.config.ts +++ b/packages/mls-client/vitest.config.ts @@ -1,20 +1,20 @@ /// -import { defineConfig, mergeConfig } from 'vite' -import tsconfigPaths from 'vite-tsconfig-paths' -import { defineConfig as defineVitestConfig } from 'vitest/config' +import { defineConfig, mergeConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig as defineVitestConfig } from "vitest/config"; // https://vitejs.dev/config/ const viteConfig = defineConfig({ plugins: [tsconfigPaths()], -}) +}); const vitestConfig = defineVitestConfig({ test: { globals: true, testTimeout: 120000, hookTimeout: 60000, - globalSetup: ['./vitest.setup.ts'], + globalSetup: ["./vitest.setup.ts"], }, -}) +}); -export default mergeConfig(viteConfig, vitestConfig) +export default mergeConfig(viteConfig, vitestConfig); diff --git a/packages/mls-client/vitest.setup.ts b/packages/mls-client/vitest.setup.ts index 76c91691d..95cad4163 100644 --- a/packages/mls-client/vitest.setup.ts +++ b/packages/mls-client/vitest.setup.ts @@ -1,12 +1,12 @@ -import { unlink } from 'node:fs/promises' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { glob } from 'fast-glob' +import { unlink } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { glob } from "fast-glob"; -const __dirname = dirname(fileURLToPath(import.meta.url)) -const testPath = join(__dirname, 'test') +const __dirname = dirname(fileURLToPath(import.meta.url)); +const testPath = join(__dirname, "test"); export const teardown = async () => { - const files = await glob('test-*.db3*', { cwd: testPath }) - await Promise.all(files.map((file) => unlink(join(testPath, file)))) -} + const files = await glob("test-*.db3*", { cwd: testPath }); + await Promise.all(files.map((file) => unlink(join(testPath, file)))); +}; diff --git a/shared/eslint-config-custom/index.js b/shared/eslint-config-custom/index.js new file mode 100644 index 000000000..3626b566d --- /dev/null +++ b/shared/eslint-config-custom/index.js @@ -0,0 +1,89 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: [ + "airbnb-base", + "airbnb-typescript/base", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "prettier", + ], + ignorePatterns: ["dist/**/*"], + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + }, + rules: { + "class-methods-use-this": "off", + "@typescript-eslint/naming-convention": [ + "error", + { + selector: ["variable"], + types: ["boolean"], + format: ["PascalCase"], + prefix: ["is", "should", "has", "can", "did", "will"], + }, + { + selector: "variable", + modifiers: ["destructured"], + format: null, + }, + ], + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/consistent-type-imports": "error", + "@typescript-eslint/unbound-method": [ + "error", + { + ignoreStatic: true, + }, + ], + "import/prefer-default-export": "off", + "no-console": ["error", { allow: ["error"] }], + "no-void": ["error", { allowAsStatement: true }], + "no-restricted-syntax": [ + "warn", + { + selector: "ForInStatement", + message: + "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.", + }, + { + selector: "ForOfStatement", + message: + "iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations.", + }, + { + selector: "WithStatement", + message: + "`with` is disallowed in strict mode because it makes code impossible to predict and optimize.", + }, + ], + }, + overrides: [ + // allow devDependencies in configuration files + { + files: ["*.ts", "*.js", "*.cjs"], + rules: { + "import/no-extraneous-dependencies": [ + "error", + { + devDependencies: true, + optionalDependencies: false, + peerDependencies: false, + }, + ], + "@typescript-eslint/no-var-requires": "off", + }, + }, + // allow require in .cjs files + { + files: ["*.cjs"], + rules: { + "global-require": "off", + }, + }, + ], +}; diff --git a/shared/eslint-config-custom/package.json b/shared/eslint-config-custom/package.json new file mode 100644 index 000000000..1e6715359 --- /dev/null +++ b/shared/eslint-config-custom/package.json @@ -0,0 +1,22 @@ +{ + "name": "eslint-config-custom", + "version": "0.0.0", + "private": true, + "license": "UNLICENSED", + "main": "index.js", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "eslint": "^8.57.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^18.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-config-standard-with-typescript": "^43.0.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-promise": "^6.1.1" + }, + "devDependencies": { + "typescript": "^5.6.3" + } +} diff --git a/shared/tsconfig/base.json b/shared/tsconfig/base.json new file mode 100644 index 000000000..3c9a87a7c --- /dev/null +++ b/shared/tsconfig/base.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Base", + "compilerOptions": { + "composite": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/shared/tsconfig/build.json b/shared/tsconfig/build.json new file mode 100644 index 000000000..4b65c0c33 --- /dev/null +++ b/shared/tsconfig/build.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Build", + "extends": "./base.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "sourceMap": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["vitest", "vitest/globals"] + }, + "exclude": ["node_modules"] +} diff --git a/shared/tsconfig/package.json b/shared/tsconfig/package.json new file mode 100644 index 000000000..a345f1e8a --- /dev/null +++ b/shared/tsconfig/package.json @@ -0,0 +1,6 @@ +{ + "name": "tsconfig", + "version": "0.0.0", + "private": true, + "license": "UNLICENSED" +} diff --git a/turbo.json b/turbo.json index ce93ec5de..7980eaa6b 100644 --- a/turbo.json +++ b/turbo.json @@ -16,12 +16,19 @@ "outputs": [] }, "lint": { + "dependsOn": ["^build"], "outputs": [] }, "test": { + "dependsOn": ["^build"], "outputs": [] }, "typecheck": { + "dependsOn": ["^build"], + "outputs": [] + }, + "@xmtp/content-type-reply#test": { + "dependsOn": ["@xmtp/content-type-remote-attachment#build"], "outputs": [] } } diff --git a/yarn.lock b/yarn.lock index 4dcd91a92..88b87f889 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,13 +5,6 @@ __metadata: version: 8 cacheKey: 10 -"@aashutoshrathi/word-wrap@npm:^1.2.3": - version: 1.2.6 - resolution: "@aashutoshrathi/word-wrap@npm:1.2.6" - checksum: 10/6eebd12a5cd03cee38fcb915ef9f4ea557df6a06f642dfc7fe8eb4839eb5c9ca55a382f3604d52c14200b0c214c12af5e1f23d2a6d8e23ef2d016b105a9d6c0a - languageName: node - linkType: hard - "@adraffy/ens-normalize@npm:1.10.0": version: 1.10.0 resolution: "@adraffy/ens-normalize@npm:1.10.0" @@ -19,6 +12,20 @@ __metadata: languageName: node linkType: hard +"@adraffy/ens-normalize@npm:1.10.1": + version: 1.10.1 + resolution: "@adraffy/ens-normalize@npm:1.10.1" + checksum: 10/4cb938c4abb88a346d50cb0ea44243ab3574330c81d4f5aaaf9dfee584b96189d0faa404de0fcbef5a1b73909ea4ebc3e63d84bd23f9949e5c8d4085207a5091 + languageName: node + linkType: hard + +"@adraffy/ens-normalize@npm:1.11.0": + version: 1.11.0 + resolution: "@adraffy/ens-normalize@npm:1.11.0" + checksum: 10/abef75f21470ea43dd6071168e092d2d13e38067e349e76186c78838ae174a46c3e18ca50921d05bea6ec3203074147c9e271f8cb6531d1c2c0e146f3199ddcb + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" @@ -73,295 +80,202 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.23.5": - version: 7.23.5 - resolution: "@babel/code-frame@npm:7.23.5" - dependencies: - "@babel/highlight": "npm:^7.23.4" - chalk: "npm:^2.4.2" - checksum: 10/44e58529c9d93083288dc9e649c553c5ba997475a7b0758cc3ddc4d77b8a7d985dbe78cc39c9bbc61f26d50af6da1ddf0a3427eae8cc222a9370619b671ed8f5 - languageName: node - linkType: hard - -"@babel/code-frame@npm:^7.24.2": - version: 7.24.7 - resolution: "@babel/code-frame@npm:7.24.7" +"@babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/code-frame@npm:7.25.7" dependencies: - "@babel/highlight": "npm:^7.24.7" + "@babel/highlight": "npm:^7.25.7" picocolors: "npm:^1.0.0" - checksum: 10/4812e94885ba7e3213d49583a155fdffb05292330f0a9b2c41b49288da70cf3c746a3fda0bf1074041a6d741c33f8d7be24be5e96f41ef77395eeddc5c9ff624 + checksum: 10/000fb8299fb35b6217d4f6c6580dcc1fa2f6c0f82d0a54b8a029966f633a8b19b490a7a906b56a94e9d8bee91c3bc44c74c44c33fb0abaa588202f6280186291 languageName: node linkType: hard -"@babel/compat-data@npm:^7.23.5": - version: 7.23.5 - resolution: "@babel/compat-data@npm:7.23.5" - checksum: 10/088f14f646ecbddd5ef89f120a60a1b3389a50a9705d44603dca77662707d0175a5e0e0da3943c3298f1907a4ab871468656fbbf74bb7842cd8b0686b2c19736 +"@babel/compat-data@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/compat-data@npm:7.25.7" + checksum: 10/8fdc451e0ed9e22d1324d504b84d4452ba6f4a806b0f5c364996ee4c2a77293f79ecf4da03033acb625c90bac115c61617eb6c894c2b88486724bcbe3af1a6eb languageName: node linkType: hard "@babel/core@npm:^7.24.0": - version: 7.24.0 - resolution: "@babel/core@npm:7.24.0" + version: 7.25.7 + resolution: "@babel/core@npm:7.25.7" dependencies: "@ampproject/remapping": "npm:^2.2.0" - "@babel/code-frame": "npm:^7.23.5" - "@babel/generator": "npm:^7.23.6" - "@babel/helper-compilation-targets": "npm:^7.23.6" - "@babel/helper-module-transforms": "npm:^7.23.3" - "@babel/helpers": "npm:^7.24.0" - "@babel/parser": "npm:^7.24.0" - "@babel/template": "npm:^7.24.0" - "@babel/traverse": "npm:^7.24.0" - "@babel/types": "npm:^7.24.0" + "@babel/code-frame": "npm:^7.25.7" + "@babel/generator": "npm:^7.25.7" + "@babel/helper-compilation-targets": "npm:^7.25.7" + "@babel/helper-module-transforms": "npm:^7.25.7" + "@babel/helpers": "npm:^7.25.7" + "@babel/parser": "npm:^7.25.7" + "@babel/template": "npm:^7.25.7" + "@babel/traverse": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10/1e22215cc89e061e0cbfed72f265ad24d363f3e9b24b51e9c4cf3ccb9222260a29a1c1e62edb439cb7e2229a3fce924edd43300500416613236c13fc8d62a947 + checksum: 10/f5fb7fb1e3ce357485cb33fe7984051a2d416472370b33144ae809df86a4663192b58cf0d828d40674d30f485790f3dd5aaf72eb659487673a4dc4be47cb3575 languageName: node linkType: hard -"@babel/generator@npm:^7.23.6": - version: 7.23.6 - resolution: "@babel/generator@npm:7.23.6" +"@babel/generator@npm:^7.23.6, @babel/generator@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/generator@npm:7.25.7" dependencies: - "@babel/types": "npm:^7.23.6" - "@jridgewell/gen-mapping": "npm:^0.3.2" - "@jridgewell/trace-mapping": "npm:^0.3.17" - jsesc: "npm:^2.5.1" - checksum: 10/864090d5122c0aa3074471fd7b79d8a880c1468480cbd28925020a3dcc7eb6e98bedcdb38983df299c12b44b166e30915b8085a7bc126e68fa7e2aadc7bd1ac5 + "@babel/types": "npm:^7.25.7" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^3.0.2" + checksum: 10/01542829621388077fa8a7464970c1db0f748f1482968dddf5332926afe4003f953cbe08e3bbbb0a335b11eba0126c9a81779bd1c5baed681a9ccec4ae63b217 languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.23.6": - version: 7.23.6 - resolution: "@babel/helper-compilation-targets@npm:7.23.6" +"@babel/helper-compilation-targets@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-compilation-targets@npm:7.25.7" dependencies: - "@babel/compat-data": "npm:^7.23.5" - "@babel/helper-validator-option": "npm:^7.23.5" - browserslist: "npm:^4.22.2" + "@babel/compat-data": "npm:^7.25.7" + "@babel/helper-validator-option": "npm:^7.25.7" + browserslist: "npm:^4.24.0" lru-cache: "npm:^5.1.1" semver: "npm:^6.3.1" - checksum: 10/05595cd73087ddcd81b82d2f3297aac0c0422858dfdded43d304786cf680ec33e846e2317e6992d2c964ee61d93945cbf1fa8ec80b55aee5bfb159227fb02cb9 - languageName: node - linkType: hard - -"@babel/helper-environment-visitor@npm:^7.22.20": - version: 7.22.20 - resolution: "@babel/helper-environment-visitor@npm:7.22.20" - checksum: 10/d80ee98ff66f41e233f36ca1921774c37e88a803b2f7dca3db7c057a5fea0473804db9fb6729e5dbfd07f4bed722d60f7852035c2c739382e84c335661590b69 - languageName: node - linkType: hard - -"@babel/helper-function-name@npm:^7.23.0": - version: 7.23.0 - resolution: "@babel/helper-function-name@npm:7.23.0" - dependencies: - "@babel/template": "npm:^7.22.15" - "@babel/types": "npm:^7.23.0" - checksum: 10/7b2ae024cd7a09f19817daf99e0153b3bf2bc4ab344e197e8d13623d5e36117ed0b110914bc248faa64e8ccd3e97971ec7b41cc6fd6163a2b980220c58dcdf6d - languageName: node - linkType: hard - -"@babel/helper-hoist-variables@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-hoist-variables@npm:7.22.5" - dependencies: - "@babel/types": "npm:^7.22.5" - checksum: 10/394ca191b4ac908a76e7c50ab52102669efe3a1c277033e49467913c7ed6f7c64d7eacbeabf3bed39ea1f41731e22993f763b1edce0f74ff8563fd1f380d92cc + checksum: 10/bbf9be8480da3f9a89e36e9ea2e1c76601014c1074ccada7c2edb1adeb3b62bc402cc4abaf8d16760734b25eceb187a9510ce44f6a7a6f696ccc74f69283625b languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.22.15": - version: 7.22.15 - resolution: "@babel/helper-module-imports@npm:7.22.15" +"@babel/helper-module-imports@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-module-imports@npm:7.25.7" dependencies: - "@babel/types": "npm:^7.22.15" - checksum: 10/5ecf9345a73b80c28677cfbe674b9f567bb0d079e37dcba9055e36cb337db24ae71992a58e1affa9d14a60d3c69907d30fe1f80aea105184501750a58d15c81c + "@babel/traverse": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" + checksum: 10/94556712c27058ea35a1a39e21a3a9f067cd699405b64333d7d92b2b3d2f24d6f0ffa51aedba0b908e320acb1854e70d296259622e636fb021eeae9a6d996f01 languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.23.3": - version: 7.23.3 - resolution: "@babel/helper-module-transforms@npm:7.23.3" +"@babel/helper-module-transforms@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-module-transforms@npm:7.25.7" dependencies: - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-module-imports": "npm:^7.22.15" - "@babel/helper-simple-access": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.22.6" - "@babel/helper-validator-identifier": "npm:^7.22.20" + "@babel/helper-module-imports": "npm:^7.25.7" + "@babel/helper-simple-access": "npm:^7.25.7" + "@babel/helper-validator-identifier": "npm:^7.25.7" + "@babel/traverse": "npm:^7.25.7" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/583fa580f8e50e6f45c4f46aa76a8e49c2528deb84e25f634d66461b9a0e2420e13979b0a607b67aef67eaf8db8668eb9edc038b4514b16e3879fe09e8fd294b + checksum: 10/480309b1272ceaa985de1393f0e4c41aede0d5921ca644cec5aeaf43c8e4192b6dd56a58ef6d7e9acd02a43184ab45d3b241fc8c3a0a00f9dbb30235fd8a1181 languageName: node linkType: hard -"@babel/helper-simple-access@npm:^7.22.5": - version: 7.22.5 - resolution: "@babel/helper-simple-access@npm:7.22.5" +"@babel/helper-simple-access@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-simple-access@npm:7.25.7" dependencies: - "@babel/types": "npm:^7.22.5" - checksum: 10/7d5430eecf880937c27d1aed14245003bd1c7383ae07d652b3932f450f60bfcf8f2c1270c593ab063add185108d26198c69d1aca0e6fb7c6fdada4bcf72ab5b7 + "@babel/traverse": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" + checksum: 10/42da1c358f2516337a4f2927c77ebb952907543b9f85d7cb1e2b5b5f6d808cdc081ee66a73e2ecdf48c315d9b0c2a81a857d5e1923ea210b8e81aba5e6cd2b53 languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.22.6": - version: 7.22.6 - resolution: "@babel/helper-split-export-declaration@npm:7.22.6" - dependencies: - "@babel/types": "npm:^7.22.5" - checksum: 10/e141cace583b19d9195f9c2b8e17a3ae913b7ee9b8120246d0f9ca349ca6f03cb2c001fd5ec57488c544347c0bb584afec66c936511e447fd20a360e591ac921 - languageName: node - linkType: hard - -"@babel/helper-string-parser@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/helper-string-parser@npm:7.23.4" - checksum: 10/c352082474a2ee1d2b812bd116a56b2e8b38065df9678a32a535f151ec6f58e54633cc778778374f10544b930703cca6ddf998803888a636afa27e2658068a9c - languageName: node - linkType: hard - -"@babel/helper-validator-identifier@npm:^7.22.20": - version: 7.22.20 - resolution: "@babel/helper-validator-identifier@npm:7.22.20" - checksum: 10/df882d2675101df2d507b95b195ca2f86a3ef28cb711c84f37e79ca23178e13b9f0d8b522774211f51e40168bf5142be4c1c9776a150cddb61a0d5bf3e95750b - languageName: node - linkType: hard - -"@babel/helper-validator-identifier@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-validator-identifier@npm:7.24.7" - checksum: 10/86875063f57361471b531dbc2ea10bbf5406e12b06d249b03827d361db4cad2388c6f00936bcd9dc86479f7e2c69ea21412c2228d4b3672588b754b70a449d4b +"@babel/helper-string-parser@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-string-parser@npm:7.25.7" + checksum: 10/2b8de9fa86c3f3090a349f1ce6e8ee2618a95355cbdafc6f228d82fa4808c84bf3d1d25290c6616d0a18b26b6cfeb6ec2aeebf01404bc8c60051d0094209f0e6 languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.23.5": - version: 7.23.5 - resolution: "@babel/helper-validator-option@npm:7.23.5" - checksum: 10/537cde2330a8aede223552510e8a13e9c1c8798afee3757995a7d4acae564124fe2bf7e7c3d90d62d3657434a74340a274b3b3b1c6f17e9a2be1f48af29cb09e +"@babel/helper-validator-identifier@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-validator-identifier@npm:7.25.7" + checksum: 10/ec6934cc47fc35baaeb968414a372b064f14f7b130cf6489a014c9486b0fd2549b3c6c682cc1fc35080075e8e38d96aeb40342d63d09fc1a62510c8ce25cde1e languageName: node linkType: hard -"@babel/helpers@npm:^7.24.0": - version: 7.24.0 - resolution: "@babel/helpers@npm:7.24.0" - dependencies: - "@babel/template": "npm:^7.24.0" - "@babel/traverse": "npm:^7.24.0" - "@babel/types": "npm:^7.24.0" - checksum: 10/cc82012161b30185c2698da359c7311cf019f0932f8fcb805e985fec9e0053c354f0534dc9961f3170eee579df6724eecd34b0f5ffaa155cdd456af59fbff86e +"@babel/helper-validator-option@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-validator-option@npm:7.25.7" + checksum: 10/3c46cbdd666d176f90a0b7e952a0c6e92184b66633336eca79aca243d1f86085ec339a6e45c3d44efa9e03f1829b470a350ddafa70926af6bbf1ac611284f8d3 languageName: node linkType: hard -"@babel/highlight@npm:^7.23.4": - version: 7.23.4 - resolution: "@babel/highlight@npm:7.23.4" +"@babel/helpers@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helpers@npm:7.25.7" dependencies: - "@babel/helper-validator-identifier": "npm:^7.22.20" - chalk: "npm:^2.4.2" - js-tokens: "npm:^4.0.0" - checksum: 10/62fef9b5bcea7131df4626d009029b1ae85332042f4648a4ce6e740c3fd23112603c740c45575caec62f260c96b11054d3be5987f4981a5479793579c3aac71f + "@babel/template": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" + checksum: 10/2632909f83aa99e8b0da4e10e5ab7fc4f0274e6497bb0f17071e004e037d25e4a595583620261dc21410a526fb32b4f7063c3e15e60ed7890a6f9b8ad52312c5 languageName: node linkType: hard -"@babel/highlight@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/highlight@npm:7.24.7" +"@babel/highlight@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/highlight@npm:7.25.7" dependencies: - "@babel/helper-validator-identifier": "npm:^7.24.7" + "@babel/helper-validator-identifier": "npm:^7.25.7" chalk: "npm:^2.4.2" js-tokens: "npm:^4.0.0" picocolors: "npm:^1.0.0" - checksum: 10/69b73f38cdd4f881b09b939a711e76646da34f4834f4ce141d7a49a6bb1926eab1c594148970a8aa9360398dff800f63aade4e81fafdd7c8d8a8489ea93bfec1 + checksum: 10/823be2523d246dbf80aab3cc81c2a36c6111b16ac2949ef06789da54387824c2bfaa88c6627cdeb4ba7151d047a5d6765e49ebd0b478aba09759250111e65e08 languageName: node linkType: hard -"@babel/parser@npm:^7.24.0": - version: 7.24.0 - resolution: "@babel/parser@npm:7.24.0" - bin: - parser: ./bin/babel-parser.js - checksum: 10/3e5ebb903a6f71629a9d0226743e37fe3d961e79911d2698b243637f66c4df7e3e0a42c07838bc0e7cc9fcd585d9be8f4134a145b9459ee4a459420fb0d1360b - languageName: node - linkType: hard - -"@babel/parser@npm:^7.24.4": - version: 7.24.8 - resolution: "@babel/parser@npm:7.24.8" +"@babel/parser@npm:^7.24.0, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/parser@npm:7.25.7" + dependencies: + "@babel/types": "npm:^7.25.7" bin: parser: ./bin/babel-parser.js - checksum: 10/e44b8327da46e8659bc9fb77f66e2dc4364dd66495fb17d046b96a77bf604f0446f1e9a89cf2f011d78fc3f5cdfbae2e9e0714708e1c985988335683b2e781ef + checksum: 10/98eaa81bd378734a5f2790f02c7c076ecaba0839217445b4b84f45a7b391d640c34034253231a5bb2b2daf8204796f03584c3f94c10d46b004369bbb426a418f languageName: node linkType: hard -"@babel/runtime@npm:^7.13.8": - version: 7.23.9 - resolution: "@babel/runtime@npm:7.23.9" +"@babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.5.5": + version: 7.25.7 + resolution: "@babel/runtime@npm:7.25.7" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: 10/9a520fe1bf72249f7dd60ff726434251858de15cccfca7aa831bd19d0d3fb17702e116ead82724659b8da3844977e5e13de2bae01eb8a798f2823a669f122be6 + checksum: 10/73411fe0f1bff3a962586cef05b30f49e554b6563767e6d84f7d79d605b2c20e7fc3df291a3aebef69043181a8f893afdab9e6672557a5c2d08b9377d6f678cd languageName: node linkType: hard -"@babel/runtime@npm:^7.5.5": - version: 7.24.5 - resolution: "@babel/runtime@npm:7.24.5" +"@babel/template@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/template@npm:7.25.7" dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: 10/e0f4f4d4503f7338749d1dd92361ad132d683bde64e6b61d6c855e100dcd01592295fcfdcc960c946b85ef7908dc2f501080da58447c05812cf3cd80c599bb62 + "@babel/code-frame": "npm:^7.25.7" + "@babel/parser": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" + checksum: 10/49e1e88d2eac17d31ae28d6cf13d6d29c1f49384c4f056a6751c065d6565c351e62c01ce6b11fef5edb5f3a77c87e114ea7326ca384fa618b4834e10cf9b20f3 languageName: node linkType: hard -"@babel/template@npm:^7.22.15, @babel/template@npm:^7.24.0": - version: 7.24.0 - resolution: "@babel/template@npm:7.24.0" +"@babel/traverse@npm:^7.24.0, @babel/traverse@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/traverse@npm:7.25.7" dependencies: - "@babel/code-frame": "npm:^7.23.5" - "@babel/parser": "npm:^7.24.0" - "@babel/types": "npm:^7.24.0" - checksum: 10/8c538338c7de8fac8ada691a5a812bdcbd60bd4a4eb5adae2cc9ee19773e8fb1a724312a00af9e1ce49056ffd3c3475e7287b5668cf6360bfb3f8ac827a06ffe - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.24.0": - version: 7.24.0 - resolution: "@babel/traverse@npm:7.24.0" - dependencies: - "@babel/code-frame": "npm:^7.23.5" - "@babel/generator": "npm:^7.23.6" - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-hoist-variables": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.22.6" - "@babel/parser": "npm:^7.24.0" - "@babel/types": "npm:^7.24.0" + "@babel/code-frame": "npm:^7.25.7" + "@babel/generator": "npm:^7.25.7" + "@babel/parser": "npm:^7.25.7" + "@babel/template": "npm:^7.25.7" + "@babel/types": "npm:^7.25.7" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: 10/5cc482248ebb79adcbcf021aab4e0e95bafe2a1736ee4b46abe6f88b59848ad73e15e219db8f06c9a33a14c64257e5b47e53876601e998a8c596accb1b7f4996 + checksum: 10/5b2d332fcd6bc78e6500c997e79f7e2a54dfb357e06f0908cb7f0cdd9bb54e7fd3c5673f45993849d433d01ea6076a6d04b825958f0cfa01288ad55ffa5c286f languageName: node linkType: hard -"@babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.0": - version: 7.24.0 - resolution: "@babel/types@npm:7.24.0" +"@babel/types@npm:^7.24.0, @babel/types@npm:^7.25.4, @babel/types@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/types@npm:7.25.7" dependencies: - "@babel/helper-string-parser": "npm:^7.23.4" - "@babel/helper-validator-identifier": "npm:^7.22.20" + "@babel/helper-string-parser": "npm:^7.25.7" + "@babel/helper-validator-identifier": "npm:^7.25.7" to-fast-properties: "npm:^2.0.0" - checksum: 10/a0b4875ce2e132f9daff0d5b27c7f4c4fcc97f2b084bdc5834e92c9d32592778489029e65d99d00c406da612d87b72d7a236c0afccaa1435c028d0c94c9b6da4 - languageName: node - linkType: hard - -"@babel/types@npm:^7.23.6, @babel/types@npm:^7.8.3": - version: 7.23.9 - resolution: "@babel/types@npm:7.23.9" - dependencies: - "@babel/helper-string-parser": "npm:^7.23.4" - "@babel/helper-validator-identifier": "npm:^7.22.20" - to-fast-properties: "npm:^2.0.0" - checksum: 10/bed9634e5fd0f9dc63c84cfa83316c4cb617192db9fedfea464fca743affe93736d7bf2ebf418ee8358751a9d388e303af87a0c050cb5d87d5870c1b0154f6cb + checksum: 10/4504e16a95b6a67d50cfaa389bcbc0621019084cff73784ad4797f82d1bb76c870cb0abb6d9881d5776eb06b4607419a2b1205a08c3e87b152d74bd0884b822a languageName: node linkType: hard @@ -606,68 +520,6 @@ __metadata: languageName: node linkType: hard -"@datadog/native-appsec@npm:7.0.0": - version: 7.0.0 - resolution: "@datadog/native-appsec@npm:7.0.0" - dependencies: - node-gyp: "npm:latest" - node-gyp-build: "npm:^3.9.0" - checksum: 10/ef72d89e9be686c4682e9e2b3cb89cf4ac1ce0130131b067a563a3687ae297740263b0640b3019b27fb6191c408e644f7e1376785b4e1bacf60e1897c433f63a - languageName: node - linkType: hard - -"@datadog/native-iast-rewriter@npm:2.2.3": - version: 2.2.3 - resolution: "@datadog/native-iast-rewriter@npm:2.2.3" - dependencies: - lru-cache: "npm:^7.14.0" - node-gyp-build: "npm:^4.5.0" - checksum: 10/9a4191c021ba4e2a4443c1358581adbc38db96f761467cadf7978893994af421dfdb27656f5268ab3ad0e280eaa2469cfe2e9ec05d1579cc71b908214dd20908 - languageName: node - linkType: hard - -"@datadog/native-iast-taint-tracking@npm:1.7.0": - version: 1.7.0 - resolution: "@datadog/native-iast-taint-tracking@npm:1.7.0" - dependencies: - node-gyp: "npm:latest" - node-gyp-build: "npm:^3.9.0" - checksum: 10/35d9187610a005622c82f2299cd4ca773a722b893ca7aa57e71de13362ee65e3e47e96b69bf36755cac9a60656f01f04914122cbdb99c64bef588f9dd703d758 - languageName: node - linkType: hard - -"@datadog/native-metrics@npm:^2.0.0": - version: 2.0.0 - resolution: "@datadog/native-metrics@npm:2.0.0" - dependencies: - node-addon-api: "npm:^6.1.0" - node-gyp: "npm:latest" - node-gyp-build: "npm:^3.9.0" - checksum: 10/a5536a4d9754a62a02218e9e3638820495fa3d5adaec724d32869ad6358df4871553cec7d54cedca91dbf9a3cd13a05cde38420292cdb49c8d7eec3ee668d150 - languageName: node - linkType: hard - -"@datadog/pprof@npm:5.0.0": - version: 5.0.0 - resolution: "@datadog/pprof@npm:5.0.0" - dependencies: - delay: "npm:^5.0.0" - node-gyp: "npm:latest" - node-gyp-build: "npm:<4.0" - p-limit: "npm:^3.1.0" - pprof-format: "npm:^2.0.7" - source-map: "npm:^0.7.4" - checksum: 10/525d6d46375372f5d9786731b0167bb323e9a00eb6eaeb743dfddfdfc1601b5e17033fb824d9c255fc2901325973d756e27979f783f14f6809232d0f95cdc14b - languageName: node - linkType: hard - -"@datadog/sketches-js@npm:^2.1.0": - version: 2.1.0 - resolution: "@datadog/sketches-js@npm:2.1.0" - checksum: 10/15dd9014a7bfb605631d38ee39b06155bd926687a1e7087ed776349f316abc4dca48d0781d0bca353816dd8ba6c6558bf6c106f1966dd876ab6e94b7a05c7556 - languageName: node - linkType: hard - "@es-joy/jsdoccomment@npm:~0.46.0": version: 0.46.0 resolution: "@es-joy/jsdoccomment@npm:0.46.0" @@ -679,13 +531,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/aix-ppc64@npm:0.19.12" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/aix-ppc64@npm:0.21.5" @@ -693,13 +538,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/android-arm64@npm:0.19.12" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-arm64@npm:0.21.5" @@ -707,13 +545,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/android-arm@npm:0.19.12" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-arm@npm:0.21.5" @@ -721,13 +552,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/android-x64@npm:0.19.12" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/android-x64@npm:0.21.5" @@ -735,13 +559,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/darwin-arm64@npm:0.19.12" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/darwin-arm64@npm:0.21.5" @@ -749,13 +566,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/darwin-x64@npm:0.19.12" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/darwin-x64@npm:0.21.5" @@ -763,13 +573,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/freebsd-arm64@npm:0.19.12" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/freebsd-arm64@npm:0.21.5" @@ -777,13 +580,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/freebsd-x64@npm:0.19.12" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/freebsd-x64@npm:0.21.5" @@ -791,13 +587,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-arm64@npm:0.19.12" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-arm64@npm:0.21.5" @@ -805,13 +594,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-arm@npm:0.19.12" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-arm@npm:0.21.5" @@ -819,13 +601,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-ia32@npm:0.19.12" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-ia32@npm:0.21.5" @@ -833,13 +608,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-loong64@npm:0.19.12" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-loong64@npm:0.21.5" @@ -847,13 +615,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-mips64el@npm:0.19.12" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-mips64el@npm:0.21.5" @@ -861,13 +622,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-ppc64@npm:0.19.12" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-ppc64@npm:0.21.5" @@ -875,13 +629,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-riscv64@npm:0.19.12" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-riscv64@npm:0.21.5" @@ -889,13 +636,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-s390x@npm:0.19.12" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-s390x@npm:0.21.5" @@ -903,13 +643,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/linux-x64@npm:0.19.12" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/linux-x64@npm:0.21.5" @@ -917,13 +650,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/netbsd-x64@npm:0.19.12" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/netbsd-x64@npm:0.21.5" @@ -931,13 +657,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/openbsd-x64@npm:0.19.12" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/openbsd-x64@npm:0.21.5" @@ -945,13 +664,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/sunos-x64@npm:0.19.12" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/sunos-x64@npm:0.21.5" @@ -959,13 +671,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/win32-arm64@npm:0.19.12" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-arm64@npm:0.21.5" @@ -973,13 +678,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/win32-ia32@npm:0.19.12" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-ia32@npm:0.21.5" @@ -987,13 +685,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.19.12": - version: 0.19.12 - resolution: "@esbuild/win32-x64@npm:0.19.12" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/win32-x64@npm:0.21.5" @@ -1012,10 +703,10 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.6.0, @eslint-community/regexpp@npm:^4.6.1": - version: 4.10.0 - resolution: "@eslint-community/regexpp@npm:4.10.0" - checksum: 10/8c36169c815fc5d726078e8c71a5b592957ee60d08c6470f9ce0187c8046af1a00afbda0a065cc40ff18d5d83f82aed9793c6818f7304a74a7488dc9f3ecbd42 +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.11.0, @eslint-community/regexpp@npm:^4.6.1": + version: 4.11.1 + resolution: "@eslint-community/regexpp@npm:4.11.1" + checksum: 10/934b6d3588c7f16b18d41efec4fdb89616c440b7e3256b8cb92cfd31ae12908600f2b986d6c1e61a84cbc10256b1dd3448cd1eec79904bd67ac365d0f1aba2e2 languageName: node linkType: hard @@ -1036,10 +727,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:8.57.0": - version: 8.57.0 - resolution: "@eslint/js@npm:8.57.0" - checksum: 10/3c501ce8a997cf6cbbaf4ed358af5492875e3550c19b9621413b82caa9ae5382c584b0efa79835639e6e0ddaa568caf3499318e5bdab68643ef4199dce5eb0a0 +"@eslint/js@npm:8.57.1": + version: 8.57.1 + resolution: "@eslint/js@npm:8.57.1" + checksum: 10/7562b21be10c2adbfa4aa5bb2eccec2cb9ac649a3569560742202c8d1cb6c931ce634937a2f0f551e078403a1c1285d6c2c0aa345dafc986149665cd69fe8b59 languageName: node linkType: hard @@ -1488,9 +1179,9 @@ __metadata: linkType: hard "@fastify/busboy@npm:^2.0.0": - version: 2.1.0 - resolution: "@fastify/busboy@npm:2.1.0" - checksum: 10/f22c1e5c52dc350ddf9ba8be9f87b48d3ea5af00a37fd0a0d1e3e4b37f94d96763e514c68a350c7f570260fdd2f08b55ee090cdd879f92a03249eb0e3fd19113 + version: 2.1.1 + resolution: "@fastify/busboy@npm:2.1.1" + checksum: 10/2bb8a7eca8289ed14c9eb15239bc1019797454624e769b39a0b90ed204d032403adc0f8ed0d2aef8a18c772205fa7808cf5a1b91f21c7bfc7b6032150b1062c5 languageName: node linkType: hard @@ -1501,14 +1192,14 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.14": - version: 0.11.14 - resolution: "@humanwhocodes/config-array@npm:0.11.14" +"@humanwhocodes/config-array@npm:^0.13.0": + version: 0.13.0 + resolution: "@humanwhocodes/config-array@npm:0.13.0" dependencies: - "@humanwhocodes/object-schema": "npm:^2.0.2" + "@humanwhocodes/object-schema": "npm:^2.0.3" debug: "npm:^4.3.1" minimatch: "npm:^3.0.5" - checksum: 10/3ffb24ecdfab64014a230e127118d50a1a04d11080cbb748bc21629393d100850496456bbcb4e8c438957fe0934430d731042f1264d6a167b62d32fc2863580a + checksum: 10/524df31e61a85392a2433bf5d03164e03da26c03d009f27852e7dcfdafbc4a23f17f021dacf88e0a7a9fe04ca032017945d19b57a16e2676d9114c22a53a9d11 languageName: node linkType: hard @@ -1519,10 +1210,10 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^2.0.2": - version: 2.0.2 - resolution: "@humanwhocodes/object-schema@npm:2.0.2" - checksum: 10/ef915e3e2f34652f3d383b28a9a99cfea476fa991482370889ab14aac8ecd2b38d47cc21932526c6d949da0daf4a4a6bf629d30f41b0caca25e146819cbfa70e +"@humanwhocodes/object-schema@npm:^2.0.3": + version: 2.0.3 + resolution: "@humanwhocodes/object-schema@npm:2.0.3" + checksum: 10/05bb99ed06c16408a45a833f03a732f59bf6184795d4efadd33238ff8699190a8c871ad1121241bb6501589a9598dc83bf25b99dcbcf41e155cdf36e35e937a3 languageName: node linkType: hard @@ -1567,18 +1258,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.0": - version: 0.3.3 - resolution: "@jridgewell/gen-mapping@npm:0.3.3" - dependencies: - "@jridgewell/set-array": "npm:^1.0.1" - "@jridgewell/sourcemap-codec": "npm:^1.4.10" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 10/072ace159c39ab85944bdabe017c3de15c5e046a4a4a772045b00ff05e2ebdcfa3840b88ae27e897d473eb4d4845b37be3c78e28910c779f5aeeeae2fb7f0cc2 - languageName: node - linkType: hard - -"@jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": +"@jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.5 resolution: "@jridgewell/gen-mapping@npm:0.3.5" dependencies: @@ -1596,13 +1276,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/set-array@npm:^1.0.1": - version: 1.1.2 - resolution: "@jridgewell/set-array@npm:1.1.2" - checksum: 10/69a84d5980385f396ff60a175f7177af0b8da4ddb81824cb7016a9ef914eee9806c72b6b65942003c63f7983d4f39a5c6c27185bbca88eb4690b62075602e28e - languageName: node - linkType: hard - "@jridgewell/set-array@npm:^1.2.1": version: 1.2.1 resolution: "@jridgewell/set-array@npm:1.2.1" @@ -1611,30 +1284,23 @@ __metadata: linkType: hard "@jridgewell/source-map@npm:^0.3.3": - version: 0.3.5 - resolution: "@jridgewell/source-map@npm:0.3.5" + version: 0.3.6 + resolution: "@jridgewell/source-map@npm:0.3.6" dependencies: - "@jridgewell/gen-mapping": "npm:^0.3.0" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 10/73838ac43235edecff5efc850c0d759704008937a56b1711b28c261e270fe4bf2dc06d0b08663aeb1ab304f81f6de4f5fb844344403cf53ba7096967a9953cae - languageName: node - linkType: hard - -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15": - version: 1.4.15 - resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" - checksum: 10/89960ac087781b961ad918978975bcdf2051cd1741880469783c42de64239703eab9db5230d776d8e6a09d73bb5e4cb964e07d93ee6e2e7aea5a7d726e865c09 + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + checksum: 10/0a9aca9320dc9044014ba0ef989b3a8411b0d778895553e3b7ca2ac0a75a20af4a5ad3f202acfb1879fa40466036a4417e1d5b38305baed8b9c1ebe6e4b3e7f5 languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.5.0": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": version: 1.5.0 resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" checksum: 10/4ed6123217569a1484419ac53f6ea0d9f3b57e5b57ab30d7c267bdb27792a27eb0e4b08e84a2680aa55cc2f2b411ffd6ec3db01c44fdc6dc43aca4b55f8374fd languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24": +"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" dependencies: @@ -1644,16 +1310,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.22 - resolution: "@jridgewell/trace-mapping@npm:0.3.22" - dependencies: - "@jridgewell/resolve-uri": "npm:^3.1.0" - "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10/48d3e3db00dbecb211613649a1849876ba5544a3f41cf5e6b99ea1130272d6cf18591b5b67389bce20f1c871b4ede5900c3b6446a7aab6d0a3b2fe806a834db7 - languageName: node - linkType: hard - "@manypkg/find-root@npm:^1.1.0": version: 1.1.0 resolution: "@manypkg/find-root@npm:1.1.0" @@ -1680,26 +1336,26 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-engine@npm:^9.0.1": - version: 9.0.1 - resolution: "@metamask/json-rpc-engine@npm:9.0.1" +"@metamask/json-rpc-engine@npm:^9.0.1, @metamask/json-rpc-engine@npm:^9.0.3": + version: 9.0.3 + resolution: "@metamask/json-rpc-engine@npm:9.0.3" dependencies: "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^9.0.0" - checksum: 10/edf8f7d22abba482f4a8e4774a72400b55be7ddb35270a0d33569827372b4ea0392fb0ef0584cbc0c9ca6b3c9964e9a18832b7975ed8d44fb2321636014f5352 + "@metamask/utils": "npm:^9.1.0" + checksum: 10/23a3cafb5869f6d5867105e3570ac4e214a72dda0b4b428cde6bae8856ec838c822b174f8cea054108122531d662cf93a65e92e1ee07da0485d5d0c0e5a1fca6 languageName: node linkType: hard "@metamask/json-rpc-middleware-stream@npm:^8.0.1": - version: 8.0.1 - resolution: "@metamask/json-rpc-middleware-stream@npm:8.0.1" + version: 8.0.3 + resolution: "@metamask/json-rpc-middleware-stream@npm:8.0.3" dependencies: - "@metamask/json-rpc-engine": "npm:^9.0.1" + "@metamask/json-rpc-engine": "npm:^9.0.3" "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^9.0.0" + "@metamask/utils": "npm:^9.1.0" readable-stream: "npm:^3.6.2" - checksum: 10/6313ae584efaa25503e281d6435e243c73243a3b040e3a867e9ca8ad5371d897fe10bcca180df7be77d0a9dba13cd1ee890fc09e2fd83004881537e850fd9ddd + checksum: 10/21299e30f735b56d50737739cdc711667e7862b27bf4ce6b73bfa5b9cd7763d5a3022caf1bb2786600169674ac87e7a141321752b92ddd46c12f41dd10b666ae languageName: node linkType: hard @@ -1714,8 +1370,8 @@ __metadata: linkType: hard "@metamask/providers@npm:^17.1.1": - version: 17.1.1 - resolution: "@metamask/providers@npm:17.1.1" + version: 17.2.1 + resolution: "@metamask/providers@npm:17.2.1" dependencies: "@metamask/json-rpc-engine": "npm:^9.0.1" "@metamask/json-rpc-middleware-stream": "npm:^8.0.1" @@ -1730,28 +1386,21 @@ __metadata: readable-stream: "npm:^3.6.2" peerDependencies: webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/9aed27c02ce97838705b3dacc4f2d8a821586e5aef2e98315f98e5be1a457ac5d48602fd7d5bdc4ac608e18cca13c723b685c3d03820b952f036647eac0121ce + checksum: 10/ff9cbcdd4cfa410c52ae0d9d39ad9285fb21f583bcb36a8a39d1862681fe17483008c15ab0ce87797ea94cad82a2f2e58b29b1db1f02df151f9cf3b05013e8a4 languageName: node linkType: hard "@metamask/rpc-errors@npm:^6.3.1": - version: 6.3.1 - resolution: "@metamask/rpc-errors@npm:6.3.1" + version: 6.4.0 + resolution: "@metamask/rpc-errors@npm:6.4.0" dependencies: "@metamask/utils": "npm:^9.0.0" fast-safe-stringify: "npm:^2.0.6" - checksum: 10/f968fb490b13b632c2ad4770a144d67cecdff8d539cb8b489c732b08dab7a62fae65d7a2908ce8c5b77260317aa618948a52463f093fa8d9f84aee1c5f6f5daf - languageName: node - linkType: hard - -"@metamask/safe-event-emitter@npm:^3.0.0": - version: 3.0.0 - resolution: "@metamask/safe-event-emitter@npm:3.0.0" - checksum: 10/8dc58a76f9f75bf2405931465fc311c68043d851e6b8ebe9f82ae339073a08a83430dba9338f8e3adc4bfc8067607125074bcafa32baee3a5157f42343dc89e5 + checksum: 10/9a17525aa8ce9ac142a94c04000dba7f0635e8e155c6c045f57eca36cc78c255318cca2fad4571719a427dfd2df64b70bc6442989523a8de555480668d666ad5 languageName: node linkType: hard -"@metamask/safe-event-emitter@npm:^3.1.1": +"@metamask/safe-event-emitter@npm:^3.0.0, @metamask/safe-event-emitter@npm:^3.1.1": version: 3.1.1 resolution: "@metamask/safe-event-emitter@npm:3.1.1" checksum: 10/e24db4d7c20764bfc5b025065f92518c805f0ffb1da4820078b8cff7dcae964c0f354cf053fcb7ac659de015d5ffdf21aae5e8d44e191ee8faa9066855f22653 @@ -1765,9 +1414,9 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^9.0.0": - version: 9.1.0 - resolution: "@metamask/utils@npm:9.1.0" +"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.1.0": + version: 9.3.0 + resolution: "@metamask/utils@npm:9.3.0" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/superstruct": "npm:^3.1.0" @@ -1778,7 +1427,7 @@ __metadata: pony-cause: "npm:^2.1.10" semver: "npm:^7.5.4" uuid: "npm:^9.0.1" - checksum: 10/7335e151a51be92e86868dc48b3ee78c376d4edd5d758d334176027247637ab22839d8f663bd02542c0a19b05ecec456bedab5f36436689cf3d953ca36d91781 + checksum: 10/ed6648cd973bbf3b4eb0e862903b795a99d27784c820e19f62f0bc0ddf353e98c2858d7e9aaebc0249a586391b344e35b9249d13c08e3ea0c74b23dc1c6b1558 languageName: node linkType: hard @@ -1791,12 +1440,21 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.3.0, @noble/curves@npm:~1.3.0": - version: 1.3.0 - resolution: "@noble/curves@npm:1.3.0" +"@noble/curves@npm:1.4.2, @noble/curves@npm:~1.4.0": + version: 1.4.2 + resolution: "@noble/curves@npm:1.4.2" dependencies: - "@noble/hashes": "npm:1.3.3" - checksum: 10/f3cbdd1af00179e30146eac5539e6df290228fb857a7a8ba36d1a772cbe59288a2ca83d06f175d3446ef00db3a80d7fd8b8347f7de9c2d4d5bf3865d8bb78252 + "@noble/hashes": "npm:1.4.0" + checksum: 10/f433a2e8811ae345109388eadfa18ef2b0004c1f79417553241db4f0ad0d59550be6298a4f43d989c627e9f7551ffae6e402a4edf0173981e6da95fc7cab5123 + languageName: node + linkType: hard + +"@noble/curves@npm:1.6.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:~1.6.0": + version: 1.6.0 + resolution: "@noble/curves@npm:1.6.0" + dependencies: + "@noble/hashes": "npm:1.5.0" + checksum: 10/9090b5a020b7e38c7b6d21506afaacd0c7557129d716a174334c1efc36385bf3ca6de16a543c216db58055e019c6a6c3bea8d9c0b79386e6bacff5c4c6b438a9 languageName: node linkType: hard @@ -1807,14 +1465,28 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.3.3, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.2": +"@noble/hashes@npm:1.4.0, @noble/hashes@npm:~1.4.0": + version: 1.4.0 + resolution: "@noble/hashes@npm:1.4.0" + checksum: 10/e156e65794c473794c52fa9d06baf1eb20903d0d96719530f523cc4450f6c721a957c544796e6efd0197b2296e7cd70efeb312f861465e17940a3e3c7e0febc6 + languageName: node + linkType: hard + +"@noble/hashes@npm:1.5.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:~1.5.0": + version: 1.5.0 + resolution: "@noble/hashes@npm:1.5.0" + checksum: 10/da7fc7af52af7afcf59810a7eea6155075464ff462ffda2572dc6d57d53e2669b1ea2ec774e814f6273f1697e567f28d36823776c9bf7068cba2a2855140f26e + languageName: node + linkType: hard + +"@noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.2": version: 1.3.3 resolution: "@noble/hashes@npm:1.3.3" checksum: 10/1025ddde4d24630e95c0818e63d2d54ee131b980fe113312d17ed7468bc18f54486ac86c907685759f8a7e13c2f9b9e83ec7b67d1cc20836f36b5e4a65bb102d languageName: node linkType: hard -"@noble/secp256k1@npm:1.7.1": +"@noble/secp256k1@npm:1.7.1, @noble/secp256k1@npm:^1.7.1": version: 1.7.1 resolution: "@noble/secp256k1@npm:1.7.1" checksum: 10/214d4756c20ed20809d948d0cc161e95664198cb127266faf747fd7deffe5444901f05fe9f833787738f2c6e60b09e544c2f737f42f73b3699e3999ba15b1b63 @@ -1849,15 +1521,15 @@ __metadata: linkType: hard "@npmcli/agent@npm:^2.0.0": - version: 2.2.1 - resolution: "@npmcli/agent@npm:2.2.1" + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" dependencies: agent-base: "npm:^7.1.0" http-proxy-agent: "npm:^7.0.0" https-proxy-agent: "npm:^7.0.1" lru-cache: "npm:^10.0.1" - socks-proxy-agent: "npm:^8.0.1" - checksum: 10/d4a48128f61e47f2f5c89315a5350e265dc619987e635bd62b52b29c7ed93536e724e721418c0ce352ceece86c13043c67aba1b70c3f5cc72fce6bb746706162 + socks-proxy-agent: "npm:^8.0.3" + checksum: 10/96fc0036b101bae5032dc2a4cd832efb815ce9b33f9ee2f29909ee49d96a0026b3565f73c507a69eb8603f5cb32e0ae45a70cab1e2655990a4e06ae99f7f572a languageName: node linkType: hard @@ -1872,11 +1544,11 @@ __metadata: linkType: hard "@npmcli/fs@npm:^3.1.0": - version: 3.1.0 - resolution: "@npmcli/fs@npm:3.1.0" + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" dependencies: semver: "npm:^7.3.5" - checksum: 10/f3a7ab3a31de65e42aeb6ed03ed035ef123d2de7af4deb9d4a003d27acc8618b57d9fb9d259fe6c28ca538032a028f37337264388ba27d26d37fff7dde22476e + checksum: 10/1e0e04087049b24b38bc0b30d87a9388ee3ca1d3fdfc347c2f77d84fcfe6a51f250bc57ba2c1f614d7e4285c6c62bf8c769bc19aa0949ea39e5b043ee023b0bd languageName: node linkType: hard @@ -1897,14 +1569,14 @@ __metadata: linkType: hard "@npmcli/installed-package-contents@npm:^2.0.1": - version: 2.0.2 - resolution: "@npmcli/installed-package-contents@npm:2.0.2" + version: 2.1.0 + resolution: "@npmcli/installed-package-contents@npm:2.1.0" dependencies: npm-bundled: "npm:^3.0.0" npm-normalize-package-bin: "npm:^3.0.0" bin: - installed-package-contents: lib/index.js - checksum: 10/4598a97e3d6e4c8602157d9ac47723071f09662852add0f275af62d1038d8e44d0c5ff9afa05358ba3ca7e100c860d679964be0a163add6ea028dc72d31f0af1 + installed-package-contents: bin/index.js + checksum: 10/68ab3ea2994f5ea21c61940de94ec4f2755fe569ef0b86e22db0695d651a3c88915c5eab61d634cfa203b9c801ee307c8aa134c2c4bd2e4fe1aa8d295ce8a163 languageName: node linkType: hard @@ -1947,31 +1619,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api@npm:^1.0.0": - version: 1.7.0 - resolution: "@opentelemetry/api@npm:1.7.0" - checksum: 10/bcf7afa7051dcd4583898a68f8a57fb4c85b5cedddf7b6eb3616595c0b3bcd7f5448143b8355b00935a755de004d6285489f8e132f34127efe7b1be404622a3e - languageName: node - linkType: hard - -"@opentelemetry/core@npm:^1.14.0": - version: 1.21.0 - resolution: "@opentelemetry/core@npm:1.21.0" - dependencies: - "@opentelemetry/semantic-conventions": "npm:1.21.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.8.0" - checksum: 10/7d34098c0cc83b3fde3fdd7bfb5ac652bfc793ce51f3af340ba2489e220097b90d9002b0f52da89cb2bda1dcf5fec17bc69109584a7e66118f677dc6d7ecae30 - languageName: node - linkType: hard - -"@opentelemetry/semantic-conventions@npm:1.21.0": - version: 1.21.0 - resolution: "@opentelemetry/semantic-conventions@npm:1.21.0" - checksum: 10/49503a01ea5bb0b067c08c33e5dc8f5ecc5ad269825f1b183a477ddaa496df05f47439ff381e9d5850257c2797afb47f7456fb605b07c4cbec517384c0b0d9b2 - languageName: node - linkType: hard - "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -2109,8 +1756,8 @@ __metadata: linkType: hard "@rollup/pluginutils@npm:^5.1.0": - version: 5.1.0 - resolution: "@rollup/pluginutils@npm:5.1.0" + version: 5.1.2 + resolution: "@rollup/pluginutils@npm:5.1.2" dependencies: "@types/estree": "npm:^1.0.0" estree-walker: "npm:^2.0.2" @@ -2120,14 +1767,7 @@ __metadata: peerDependenciesMeta: rollup: optional: true - checksum: 10/abb15eaec5b36f159ec351b48578401bedcefdfa371d24a914cfdbb1e27d0ebfbf895299ec18ccc343d247e71f2502cba21202bc1362d7ef27d5ded699e5c2b2 - languageName: node - linkType: hard - -"@rollup/rollup-android-arm-eabi@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.12.0" - conditions: os=android & cpu=arm + checksum: 10/cc1fe3285ab48915a6535ab2f0c90dc511bd3e63143f8e9994bb036c6c5071fd14d641cff6c89a7fde6a4faa85227d4e2cf46ee36b7d962099e0b9e4c9b8a4b0 languageName: node linkType: hard @@ -2138,13 +1778,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-android-arm64@npm:4.12.0" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@rollup/rollup-android-arm64@npm:4.24.0": version: 4.24.0 resolution: "@rollup/rollup-android-arm64@npm:4.24.0" @@ -2152,13 +1785,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-darwin-arm64@npm:4.12.0" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@rollup/rollup-darwin-arm64@npm:4.24.0": version: 4.24.0 resolution: "@rollup/rollup-darwin-arm64@npm:4.24.0" @@ -2166,13 +1792,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-darwin-x64@npm:4.12.0" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@rollup/rollup-darwin-x64@npm:4.24.0": version: 4.24.0 resolution: "@rollup/rollup-darwin-x64@npm:4.24.0" @@ -2180,13 +1799,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.12.0" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@rollup/rollup-linux-arm-gnueabihf@npm:4.24.0": version: 4.24.0 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.24.0" @@ -2201,13 +1813,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.12.0" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - "@rollup/rollup-linux-arm64-gnu@npm:4.24.0": version: 4.24.0 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.24.0" @@ -2215,13 +1820,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.12.0" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - "@rollup/rollup-linux-arm64-musl@npm:4.24.0": version: 4.24.0 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.24.0" @@ -2236,13 +1834,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.12.0" - conditions: os=linux & cpu=riscv64 & libc=glibc - languageName: node - linkType: hard - "@rollup/rollup-linux-riscv64-gnu@npm:4.24.0": version: 4.24.0 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.24.0" @@ -2257,13 +1848,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.12.0" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - "@rollup/rollup-linux-x64-gnu@npm:4.24.0": version: 4.24.0 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.24.0" @@ -2271,13 +1855,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.12.0" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - "@rollup/rollup-linux-x64-musl@npm:4.24.0": version: 4.24.0 resolution: "@rollup/rollup-linux-x64-musl@npm:4.24.0" @@ -2285,13 +1862,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.12.0" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@rollup/rollup-win32-arm64-msvc@npm:4.24.0": version: 4.24.0 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.24.0" @@ -2299,13 +1869,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.12.0" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@rollup/rollup-win32-ia32-msvc@npm:4.24.0": version: 4.24.0 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.24.0" @@ -2313,13 +1876,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.12.0": - version: 4.12.0 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.12.0" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@rollup/rollup-win32-x64-msvc@npm:4.24.0": version: 4.24.0 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.24.0" @@ -2327,10 +1883,17 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.0, @scure/base@npm:~1.1.2, @scure/base@npm:~1.1.4": - version: 1.1.5 - resolution: "@scure/base@npm:1.1.5" - checksum: 10/543fa9991c6378b6a0d5ab7f1e27b30bb9c1e860d3ac81119b4213cfdf0ad7b61be004e06506e89de7ce0cec9391c17f5c082bb34c3b617a2ee6a04129f52481 +"@rtsao/scc@npm:^1.1.0": + version: 1.1.0 + resolution: "@rtsao/scc@npm:1.1.0" + checksum: 10/17d04adf404e04c1e61391ed97bca5117d4c2767a76ae3e879390d6dec7b317fcae68afbf9e98badee075d0b64fa60f287729c4942021b4d19cd01db77385c01 + languageName: node + linkType: hard + +"@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.0, @scure/base@npm:~1.1.2, @scure/base@npm:~1.1.6, @scure/base@npm:~1.1.7, @scure/base@npm:~1.1.8": + version: 1.1.9 + resolution: "@scure/base@npm:1.1.9" + checksum: 10/f0ab7f687bbcdee2a01377fe3cd808bf63977999672751295b6a92625d5322f4754a96d40f6bd579bc367aad48ecf8a4e6d0390e70296e6ded1076f52adb16bb languageName: node linkType: hard @@ -2345,14 +1908,25 @@ __metadata: languageName: node linkType: hard -"@scure/bip32@npm:1.3.3": - version: 1.3.3 - resolution: "@scure/bip32@npm:1.3.3" +"@scure/bip32@npm:1.4.0": + version: 1.4.0 + resolution: "@scure/bip32@npm:1.4.0" dependencies: - "@noble/curves": "npm:~1.3.0" - "@noble/hashes": "npm:~1.3.2" - "@scure/base": "npm:~1.1.4" - checksum: 10/4b8b75567866ff7d6b3ba154538add02d2951e9433e8dd7f0014331ac500cda5a88fe3d39b408fcc36e86b633682013f172b967af022c2e4e4ab07336801d688 + "@noble/curves": "npm:~1.4.0" + "@noble/hashes": "npm:~1.4.0" + "@scure/base": "npm:~1.1.6" + checksum: 10/6cd5062d902564d9e970597ec8b1adacb415b2eadfbb95aee1a1a0480a52eb0de4d294d3753aa8b48548064c9795ed108d348a31a8ce3fc88785377bb12c63b9 + languageName: node + linkType: hard + +"@scure/bip32@npm:1.5.0": + version: 1.5.0 + resolution: "@scure/bip32@npm:1.5.0" + dependencies: + "@noble/curves": "npm:~1.6.0" + "@noble/hashes": "npm:~1.5.0" + "@scure/base": "npm:~1.1.7" + checksum: 10/17e296a782e09aec18ed27e2e8bb6a76072604c40997ec49a6840f223296421612dbe6b44275f04db9acd6da6cefb0322141110f5ac9dc686eb0c44d5bd868fa languageName: node linkType: hard @@ -2366,13 +1940,23 @@ __metadata: languageName: node linkType: hard -"@scure/bip39@npm:1.2.2": - version: 1.2.2 - resolution: "@scure/bip39@npm:1.2.2" +"@scure/bip39@npm:1.3.0": + version: 1.3.0 + resolution: "@scure/bip39@npm:1.3.0" dependencies: - "@noble/hashes": "npm:~1.3.2" - "@scure/base": "npm:~1.1.4" - checksum: 10/f71aceda10a7937bf3779fd2b4c4156c95ec9813269470ddca464cb8ab610d2451b173037f4b1e6dac45414e406e7adc7b5814c51279f4474d5d38140bbee542 + "@noble/hashes": "npm:~1.4.0" + "@scure/base": "npm:~1.1.6" + checksum: 10/7d71fd58153de22fe8cd65b525f6958a80487bc9d0fbc32c71c328aeafe41fa259f989d2f1e0fa4fdfeaf83b8fcf9310d52ed9862987e46c2f2bfb9dd8cf9fc1 + languageName: node + linkType: hard + +"@scure/bip39@npm:1.4.0": + version: 1.4.0 + resolution: "@scure/bip39@npm:1.4.0" + dependencies: + "@noble/hashes": "npm:~1.5.0" + "@scure/base": "npm:~1.1.8" + checksum: 10/f86e0e79768c95bc684ed6de92892b1a6f228db0f8fab836f091c0ec0f6d1e291b8c4391cfbeaa9ea83f41045613535b1940cd10e7d780a5b73db163b1e7f151 languageName: node linkType: hard @@ -2506,11 +2090,11 @@ __metadata: linkType: hard "@types/bn.js@npm:*": - version: 5.1.5 - resolution: "@types/bn.js@npm:5.1.5" + version: 5.1.6 + resolution: "@types/bn.js@npm:5.1.6" dependencies: "@types/node": "npm:*" - checksum: 10/9719330c86aeae0a6a447c974cf0f853ba3660ede20de61f435b03d699e30e6d8b35bf71a8dc9fdc8317784438e83177644ba068ed653d0ae0106e1ecbfe289e + checksum: 10/db565b5a2af59b09459d74441153bf23a0e80f1fb2d070330786054e7ce1a7285dc40afcd8f289426c61a83166bdd70814f70e2d439744686aac5d3ea75daf13 languageName: node linkType: hard @@ -2539,14 +2123,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:1.0.5, @types/estree@npm:^1.0.0": - version: 1.0.5 - resolution: "@types/estree@npm:1.0.5" - checksum: 10/7de6d928dd4010b0e20c6919e1a6c27b61f8d4567befa89252055fad503d587ecb9a1e3eab1b1901f923964d7019796db810b7fd6430acb26c32866d126fd408 - languageName: node - linkType: hard - -"@types/estree@npm:1.0.6": +"@types/estree@npm:1.0.6, @types/estree@npm:^1.0.0": version: 1.0.6 resolution: "@types/estree@npm:1.0.6" checksum: 10/9d35d475095199c23e05b431bcdd1f6fec7380612aed068b14b2a08aa70494de8a9026765a5a91b1073f636fb0368f6d8973f518a31391d519e20c59388ed88d @@ -2586,11 +2163,18 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>=13.7.0": - version: 20.11.19 - resolution: "@types/node@npm:20.11.19" + version: 22.7.5 + resolution: "@types/node@npm:22.7.5" dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/c7f4705d6c84aa21679ad180c33c13ca9567f650e66e14bcee77c7c43d14619c7cd3b4d7b2458947143030b7b1930180efa6d12d999b45366abff9fed7a17472 + undici-types: "npm:~6.19.2" + checksum: 10/e8ba102f8c1aa7623787d625389be68d64e54fcbb76d41f6c2c64e8cf4c9f4a2370e7ef5e5f1732f3c57529d3d26afdcb2edc0101c5e413a79081449825c57ac + languageName: node + linkType: hard + +"@types/node@npm:18.15.13": + version: 18.15.13 + resolution: "@types/node@npm:18.15.13" + checksum: 10/b9bbe923573797ef7c5fd2641a6793489e25d9369c32aeadcaa5c7c175c85b42eb12d6fe173f6781ab6f42eaa1ebd9576a419eeaa2a1ec810094adb8adaa9a54 languageName: node linkType: hard @@ -2601,22 +2185,31 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.14.10": - version: 20.14.10 - resolution: "@types/node@npm:20.14.10" +"@types/node@npm:^18.19.22": + version: 18.19.55 + resolution: "@types/node@npm:18.19.55" dependencies: undici-types: "npm:~5.26.4" - checksum: 10/672892cf94d0d95cf052f11271990686a0fd204cd1e5fe7a4ef240e5315e06711765dc47b9ec98627d3adac18b8c92bb7e2d8db21d18faa20bc3e3203a143e79 + checksum: 10/7ba2b7203338f855fd7e90d09de3196e0d534c361ee0fbd7e3f24d416b867a017530d5721c4186f2c5402184e824a1fe65d2b5f9291751b9d8a7d2f9824f9a73 + languageName: node + linkType: hard + +"@types/node@npm:^20.14.10": + version: 20.16.11 + resolution: "@types/node@npm:20.16.11" + dependencies: + undici-types: "npm:~6.19.2" + checksum: 10/6d2f92b7b320c32ba0c2bc54d21651bd21690998a2e27f00d15019d4db3e0ec30fce85332efed5e37d4cda078ff93ea86ee3e92b76b7a25a9b92a52a039b60b2 languageName: node linkType: hard "@types/readable-stream@npm:^4.0.0": - version: 4.0.10 - resolution: "@types/readable-stream@npm:4.0.10" + version: 4.0.15 + resolution: "@types/readable-stream@npm:4.0.15" dependencies: "@types/node": "npm:*" safe-buffer: "npm:~5.1.1" - checksum: 10/9154572484b28d74294862e6e30f347d36ad677e6a220379693dff08b0eb79581a9198978473335bce52ee4953b2016c5d04b944cdfcef05985ef9a8867f7f81 + checksum: 10/33a273dcd74bec84f0d7d507c0d719487f9d0b4f48cd9e3fd2b0c6e848f23ce0c6cac1250be03c94df29e78cfd29940aae80ed2a4407fe4188eb959d15b32646 languageName: node linkType: hard @@ -2650,6 +2243,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:^6.4.0": + version: 6.21.0 + resolution: "@typescript-eslint/parser@npm:6.21.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:6.21.0" + "@typescript-eslint/types": "npm:6.21.0" + "@typescript-eslint/typescript-estree": "npm:6.21.0" + "@typescript-eslint/visitor-keys": "npm:6.21.0" + debug: "npm:^4.3.4" + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/4d51cdbc170e72275efc5ef5fce48a81ec431e4edde8374f4d0213d8d370a06823e1a61ae31d502a5f1b0d1f48fc4d29a1b1b5c2dcf809d66d3872ccf6e46ac7 + languageName: node + linkType: hard + "@typescript-eslint/parser@npm:^7.18.0": version: 7.18.0 resolution: "@typescript-eslint/parser@npm:7.18.0" @@ -2668,6 +2279,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/scope-manager@npm:6.21.0" + dependencies: + "@typescript-eslint/types": "npm:6.21.0" + "@typescript-eslint/visitor-keys": "npm:6.21.0" + checksum: 10/fe91ac52ca8e09356a71dc1a2f2c326480f3cccfec6b2b6d9154c1a90651ab8ea270b07c67df5678956c3bbf0bbe7113ab68f68f21b20912ea528b1214197395 + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:7.18.0": version: 7.18.0 resolution: "@typescript-eslint/scope-manager@npm:7.18.0" @@ -2695,6 +2316,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/types@npm:6.21.0" + checksum: 10/e26da86d6f36ca5b6ef6322619f8ec55aabcd7d43c840c977ae13ae2c964c3091fc92eb33730d8be08927c9de38466c5323e78bfb270a9ff1d3611fe821046c5 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:7.18.0": version: 7.18.0 resolution: "@typescript-eslint/types@npm:7.18.0" @@ -2702,6 +2330,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/typescript-estree@npm:6.21.0" + dependencies: + "@typescript-eslint/types": "npm:6.21.0" + "@typescript-eslint/visitor-keys": "npm:6.21.0" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + minimatch: "npm:9.0.3" + semver: "npm:^7.5.4" + ts-api-utils: "npm:^1.0.1" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/b32fa35fca2a229e0f5f06793e5359ff9269f63e9705e858df95d55ca2cd7fdb5b3e75b284095a992c48c5fc46a1431a1a4b6747ede2dd08929dc1cbacc589b8 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:7.18.0": version: 7.18.0 resolution: "@typescript-eslint/typescript-estree@npm:7.18.0" @@ -2735,6 +2382,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/visitor-keys@npm:6.21.0" + dependencies: + "@typescript-eslint/types": "npm:6.21.0" + eslint-visitor-keys: "npm:^3.4.1" + checksum: 10/30422cdc1e2ffad203df40351a031254b272f9c6f2b7e02e9bfa39e3fc2c7b1c6130333b0057412968deda17a3a68a578a78929a8139c6acef44d9d841dc72e1 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:7.18.0": version: 7.18.0 resolution: "@typescript-eslint/visitor-keys@npm:7.18.0" @@ -2870,23 +2527,175 @@ __metadata: languageName: node linkType: hard -"@xmtp/content-type-primitives@npm:^1.0.1": - version: 1.0.1 - resolution: "@xmtp/content-type-primitives@npm:1.0.1" +"@xmtp/content-type-primitives@npm:^1.0.1, @xmtp/content-type-primitives@workspace:content-types/content-type-primitives": + version: 0.0.0-use.local + resolution: "@xmtp/content-type-primitives@workspace:content-types/content-type-primitives" dependencies: + "@rollup/plugin-terser": "npm:^0.4.4" + "@rollup/plugin-typescript": "npm:^12.1.0" + "@types/node": "npm:^18.19.22" "@xmtp/proto": "npm:^3.61.1" - checksum: 10/656826cda74328e3079c7f5937eeb694260bd68a66090303fdf6abf4c54c8bbf924064eb6895b9e66addee1269779dfe1c3f0e836fcd8857f784c5645c7b7bf5 - languageName: node - linkType: hard + eslint: "npm:^8.57.0" + eslint-config-custom: "workspace:*" + happy-dom: "npm:^13.7.3" + rimraf: "npm:^6.0.1" + rollup: "npm:^4.24.0" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-filesize: "npm:^10.0.0" + typescript: "npm:^5.6.3" + vite: "npm:^5.1.6" + vitest: "npm:^2.1.2" + languageName: unknown + linkType: soft -"@xmtp/content-type-text@npm:^1.0.0": - version: 1.0.0 - resolution: "@xmtp/content-type-text@npm:1.0.0" +"@xmtp/content-type-reaction@workspace:content-types/content-type-reaction": + version: 0.0.0-use.local + resolution: "@xmtp/content-type-reaction@workspace:content-types/content-type-reaction" dependencies: + "@rollup/plugin-terser": "npm:^0.4.4" + "@rollup/plugin-typescript": "npm:^12.1.0" + "@types/node": "npm:^18.19.22" "@xmtp/content-type-primitives": "npm:^1.0.1" - checksum: 10/b195060ad5686a6ace2772d5208d535d1f5062820629764aec52cedf3f2630885b5913aea6d2f0186a49139845c20d2ded783c6bf998884f09374c07b183141f - languageName: node - linkType: hard + "@xmtp/xmtp-js": "npm:^11.6.3" + buffer: "npm:^6.0.3" + eslint: "npm:^8.57.0" + eslint-config-custom: "workspace:*" + ethers: "npm:^6.11.1" + happy-dom: "npm:^13.7.3" + rimraf: "npm:^6.0.1" + rollup: "npm:^4.24.0" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-filesize: "npm:^10.0.0" + typescript: "npm:^5.6.3" + vite: "npm:^5.1.6" + vitest: "npm:^2.1.2" + languageName: unknown + linkType: soft + +"@xmtp/content-type-read-receipt@workspace:content-types/content-type-read-receipt": + version: 0.0.0-use.local + resolution: "@xmtp/content-type-read-receipt@workspace:content-types/content-type-read-receipt" + dependencies: + "@rollup/plugin-terser": "npm:^0.4.4" + "@rollup/plugin-typescript": "npm:^12.1.0" + "@types/node": "npm:^18.19.22" + "@xmtp/content-type-primitives": "npm:^1.0.1" + "@xmtp/xmtp-js": "npm:^11.6.3" + buffer: "npm:^6.0.3" + eslint: "npm:^8.57.0" + eslint-config-custom: "workspace:*" + ethers: "npm:^6.11.1" + happy-dom: "npm:^13.7.3" + rimraf: "npm:^6.0.1" + rollup: "npm:^4.24.0" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-filesize: "npm:^10.0.0" + typescript: "npm:^5.6.3" + vite: "npm:^5.1.6" + vitest: "npm:^2.1.2" + languageName: unknown + linkType: soft + +"@xmtp/content-type-remote-attachment@workspace:*, @xmtp/content-type-remote-attachment@workspace:content-types/content-type-remote-attachment": + version: 0.0.0-use.local + resolution: "@xmtp/content-type-remote-attachment@workspace:content-types/content-type-remote-attachment" + dependencies: + "@noble/secp256k1": "npm:^1.7.1" + "@rollup/plugin-terser": "npm:^0.4.4" + "@rollup/plugin-typescript": "npm:^12.1.0" + "@types/node": "npm:^18.19.22" + "@xmtp/content-type-primitives": "npm:^1.0.1" + "@xmtp/proto": "npm:^3.61.1" + "@xmtp/rollup-plugin-resolve-extensions": "npm:^1.0.1" + "@xmtp/xmtp-js": "npm:^11.6.3" + buffer: "npm:^6.0.3" + eslint: "npm:^8.57.0" + eslint-config-custom: "workspace:*" + ethers: "npm:^6.11.1" + happy-dom: "npm:^13.7.3" + rimraf: "npm:^6.0.1" + rollup: "npm:^4.24.0" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-filesize: "npm:^10.0.0" + typescript: "npm:^5.6.3" + vite: "npm:^5.1.6" + vitest: "npm:^2.1.2" + languageName: unknown + linkType: soft + +"@xmtp/content-type-reply@workspace:content-types/content-type-reply": + version: 0.0.0-use.local + resolution: "@xmtp/content-type-reply@workspace:content-types/content-type-reply" + dependencies: + "@rollup/plugin-terser": "npm:^0.4.4" + "@rollup/plugin-typescript": "npm:^12.1.0" + "@types/node": "npm:^18.19.22" + "@xmtp/content-type-primitives": "npm:^1.0.1" + "@xmtp/content-type-remote-attachment": "workspace:*" + "@xmtp/proto": "npm:^3.61.1" + "@xmtp/xmtp-js": "npm:^11.6.3" + buffer: "npm:^6.0.3" + eslint: "npm:^8.57.0" + eslint-config-custom: "workspace:*" + ethers: "npm:^6.11.1" + happy-dom: "npm:^13.7.3" + rimraf: "npm:^6.0.1" + rollup: "npm:^4.24.0" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-filesize: "npm:^10.0.0" + typescript: "npm:^5.6.3" + vite: "npm:^5.1.6" + vitest: "npm:^2.1.2" + languageName: unknown + linkType: soft + +"@xmtp/content-type-text@npm:^1.0.0, @xmtp/content-type-text@workspace:content-types/content-type-text": + version: 0.0.0-use.local + resolution: "@xmtp/content-type-text@workspace:content-types/content-type-text" + dependencies: + "@rollup/plugin-terser": "npm:^0.4.4" + "@rollup/plugin-typescript": "npm:^12.1.0" + "@types/node": "npm:^18.19.22" + "@xmtp/content-type-primitives": "npm:^1.0.1" + "@xmtp/xmtp-js": "npm:^11.6.3" + buffer: "npm:^6.0.3" + eslint: "npm:^8.57.0" + eslint-config-custom: "workspace:*" + ethers: "npm:^6.11.1" + happy-dom: "npm:^13.7.3" + rimraf: "npm:^6.0.1" + rollup: "npm:^4.24.0" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-filesize: "npm:^10.0.0" + typescript: "npm:^5.6.3" + vite: "npm:^5.1.6" + vitest: "npm:^2.1.2" + languageName: unknown + linkType: soft + +"@xmtp/content-type-transaction-reference@workspace:content-types/content-type-transaction-reference": + version: 0.0.0-use.local + resolution: "@xmtp/content-type-transaction-reference@workspace:content-types/content-type-transaction-reference" + dependencies: + "@rollup/plugin-terser": "npm:^0.4.4" + "@rollup/plugin-typescript": "npm:^12.1.0" + "@types/node": "npm:^18.19.22" + "@xmtp/content-type-primitives": "npm:^1.0.1" + "@xmtp/xmtp-js": "npm:^11.6.3" + buffer: "npm:^6.0.3" + eslint: "npm:^8.57.0" + eslint-config-custom: "workspace:*" + ethers: "npm:^6.11.1" + happy-dom: "npm:^13.7.3" + rimraf: "npm:^6.0.1" + rollup: "npm:^4.24.0" + rollup-plugin-dts: "npm:^6.1.1" + rollup-plugin-filesize: "npm:^10.0.0" + typescript: "npm:^5.6.3" + vite: "npm:^5.1.6" + vitest: "npm:^2.1.2" + languageName: unknown + linkType: soft "@xmtp/mls-client-bindings-node@npm:^0.0.12": version: 0.0.12 @@ -2899,7 +2708,6 @@ __metadata: version: 0.0.0-use.local resolution: "@xmtp/mls-client@workspace:packages/mls-client" dependencies: - "@ianvs/prettier-plugin-sort-imports": "npm:^4.3.1" "@rollup/plugin-json": "npm:^6.1.0" "@rollup/plugin-typescript": "npm:^12.1.0" "@types/node": "npm:^20.14.10" @@ -2920,13 +2728,12 @@ __metadata: eslint-plugin-prettier: "npm:^5.1.3" eslint-plugin-promise: "npm:^6.4.0" fast-glob: "npm:^3.3.2" - prettier: "npm:^3.3.3" - prettier-plugin-packagejson: "npm:^2.5.2" + rimraf: "npm:^6.0.1" rollup: "npm:^4.24.0" rollup-plugin-dts: "npm:^6.1.1" rollup-plugin-filesize: "npm:^10.0.0" rollup-plugin-tsconfig-paths: "npm:^1.5.2" - typescript: "npm:^5.6.2" + typescript: "npm:^5.6.3" viem: "npm:^2.13.6" vite: "npm:5.4.8" vite-tsconfig-paths: "npm:^5.0.1" @@ -2934,55 +2741,43 @@ __metadata: languageName: unknown linkType: soft -"@xmtp/proto@npm:3.56.0": - version: 3.56.0 - resolution: "@xmtp/proto@npm:3.56.0" - dependencies: - long: "npm:^5.2.0" - protobufjs: "npm:^7.0.0" - rxjs: "npm:^7.8.0" - undici: "npm:^5.8.1" - checksum: 10/f752e6858692464319d6f22861fe8f23c46d9bb0eb390fe2220e0b4932a4de84be2e9e1cbafc0200e1bfe2a0ed3a3fb6079941630e57fb80e6325bc2a52bf10d - languageName: node - linkType: hard - -"@xmtp/proto@npm:^3.61.1": - version: 3.61.1 - resolution: "@xmtp/proto@npm:3.61.1" +"@xmtp/proto@npm:3.54.0": + version: 3.54.0 + resolution: "@xmtp/proto@npm:3.54.0" dependencies: long: "npm:^5.2.0" protobufjs: "npm:^7.0.0" rxjs: "npm:^7.8.0" undici: "npm:^5.8.1" - checksum: 10/c5acae46ad301a50652f30384be55a3389b4c11994652fa5386052c7ff4111fcb15c0e9d267898d6173cbcb6559b24bd7bef7470f020388f8610fecd3b8deea9 + checksum: 10/536b846b234bdf49978716b232b6a25985a55bb0c10c825b090354cb43f0b071ee506e5a9765c69deb0e6a4b9aa4cd61abe2c9d2df563cfd6ec121197b591844 languageName: node linkType: hard -"@xmtp/proto@npm:^3.62.1": - version: 3.62.1 - resolution: "@xmtp/proto@npm:3.62.1" +"@xmtp/proto@npm:3.56.0": + version: 3.56.0 + resolution: "@xmtp/proto@npm:3.56.0" dependencies: long: "npm:^5.2.0" protobufjs: "npm:^7.0.0" rxjs: "npm:^7.8.0" undici: "npm:^5.8.1" - checksum: 10/7d6633f5ffb60725a6f3f128191944caf203fb3110a6076cdec31bc3cc6e5dca1b03170c6fa38b877125475ed3050031b93a47587c5e6bee6ebd91de53f8f0f2 + checksum: 10/f752e6858692464319d6f22861fe8f23c46d9bb0eb390fe2220e0b4932a4de84be2e9e1cbafc0200e1bfe2a0ed3a3fb6079941630e57fb80e6325bc2a52bf10d languageName: node linkType: hard -"@xmtp/proto@npm:^3.68.0": - version: 3.68.0 - resolution: "@xmtp/proto@npm:3.68.0" +"@xmtp/proto@npm:^3.61.1, @xmtp/proto@npm:^3.62.1, @xmtp/proto@npm:^3.68.0": + version: 3.71.0 + resolution: "@xmtp/proto@npm:3.71.0" dependencies: long: "npm:^5.2.0" protobufjs: "npm:^7.0.0" rxjs: "npm:^7.8.0" undici: "npm:^5.8.1" - checksum: 10/2cadf9d212ac01dc6a1d3a83e59ca559debff65ee83c7381e29e1a5e9dc2f0caa021a4c6df696957d77da8b5c1133fe02cbc8a4c3c7c4d2e45993366e309da63 + checksum: 10/db633ccbe1bc4015494daa4b788f51e672ab3afa5bb54bb82839fae5101a918c6bc6b0aaf14080c8f6d5873ec42fe8f67a47621bfc023b946432a2d9bf18f7b9 languageName: node linkType: hard -"@xmtp/rollup-plugin-resolve-extensions@npm:1.0.1": +"@xmtp/rollup-plugin-resolve-extensions@npm:1.0.1, @xmtp/rollup-plugin-resolve-extensions@npm:^1.0.1": version: 1.0.1 resolution: "@xmtp/rollup-plugin-resolve-extensions@npm:1.0.1" dependencies: @@ -3002,11 +2797,26 @@ __metadata: languageName: node linkType: hard +"@xmtp/xmtp-js@npm:^11.6.3": + version: 11.6.3 + resolution: "@xmtp/xmtp-js@npm:11.6.3" + dependencies: + "@noble/secp256k1": "npm:1.7.1" + "@xmtp/consent-proof-signature": "npm:^0.1.3" + "@xmtp/proto": "npm:3.54.0" + "@xmtp/user-preferences-bindings-wasm": "npm:^0.3.6" + async-mutex: "npm:^0.5.0" + elliptic: "npm:^6.5.4" + long: "npm:^5.2.3" + viem: "npm:2.7.15" + checksum: 10/0e211e532dca04a54f52b9b698e7903b65babc2625d5a6c6e1e83be065d148bb40cba27b9fbd7ad420fcca5266d45433fbfbc554ef79e9892a14327f9b4e9f37 + languageName: node + linkType: hard + "@xmtp/xmtp-js@workspace:^, @xmtp/xmtp-js@workspace:packages/js-sdk": version: 0.0.0-use.local resolution: "@xmtp/xmtp-js@workspace:packages/js-sdk" dependencies: - "@ianvs/prettier-plugin-sort-imports": "npm:^4.3.1" "@metamask/providers": "npm:^17.1.1" "@noble/secp256k1": "npm:1.7.1" "@rollup/plugin-json": "npm:^6.1.0" @@ -3028,7 +2838,6 @@ __metadata: "@xmtp/user-preferences-bindings-wasm": "npm:^0.3.6" async-mutex: "npm:^0.5.0" benny: "npm:^3.7.1" - dd-trace: "npm:5.5.0" elliptic: "npm:^6.5.7" eslint: "npm:^8.57.0" eslint-config-prettier: "npm:^9.1.0" @@ -3042,14 +2851,13 @@ __metadata: ethers: "npm:^5.7.2" happy-dom: "npm:^15.7.4" long: "npm:^5.2.3" - prettier: "npm:^3.3.3" - prettier-plugin-packagejson: "npm:^2.5.2" + rimraf: "npm:^6.0.1" rollup: "npm:^4.24.0" rollup-plugin-dts: "npm:^6.1.1" rollup-plugin-filesize: "npm:^10.0.0" rollup-plugin-tsconfig-paths: "npm:^1.5.2" typedoc: "npm:^0.26.8" - typescript: "npm:^5.6.2" + typescript: "npm:^5.6.3" viem: "npm:2.7.15" vite: "npm:5.4.8" vite-tsconfig-paths: "npm:^5.0.1" @@ -3086,6 +2894,21 @@ __metadata: languageName: node linkType: hard +"abitype@npm:1.0.6": + version: 1.0.6 + resolution: "abitype@npm:1.0.6" + peerDependencies: + typescript: ">=5.0.4" + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + checksum: 10/d04d58f90405c29a3c68353508502d7e870feb27418a6281ba9a13e6aaee42c26b2c5f08f648f058b8eaffac32927194b33f396d2451d18afeccfb654c7285c2 + languageName: node + linkType: hard + "abort-controller@npm:^3.0.0": version: 3.0.0 resolution: "abort-controller@npm:3.0.0" @@ -3095,15 +2918,6 @@ __metadata: languageName: node linkType: hard -"acorn-import-assertions@npm:^1.9.0": - version: 1.9.0 - resolution: "acorn-import-assertions@npm:1.9.0" - peerDependencies: - acorn: ^8 - checksum: 10/af8dd58f6b0c6a43e85849744534b99f2133835c6fcdabda9eea27d0a0da625a0d323c4793ba7cb25cf4507609d0f747c210ccc2fc9b5866de04b0e59c9c5617 - languageName: node - linkType: hard - "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -3113,12 +2927,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.8.2, acorn@npm:^8.9.0": - version: 8.11.3 - resolution: "acorn@npm:8.11.3" +"acorn@npm:^8.12.0, acorn@npm:^8.8.2, acorn@npm:^8.9.0": + version: 8.12.1 + resolution: "acorn@npm:8.12.1" bin: acorn: bin/acorn - checksum: 10/b688e7e3c64d9bfb17b596e1b35e4da9d50553713b3b3630cf5690f2b023a84eac90c56851e6912b483fe60e8b4ea28b254c07e92f17ef83d72d78745a8352dd + checksum: 10/d08c2d122bba32d0861e0aa840b2ee25946c286d5dc5990abca991baf8cdbfbe199b05aacb221b979411a2fea36f83e26b5ac4f6b4e0ce49038c62316c1848f0 languageName: node linkType: hard @@ -3129,6 +2943,13 @@ __metadata: languageName: node linkType: hard +"aes-js@npm:4.0.0-beta.5": + version: 4.0.0-beta.5 + resolution: "aes-js@npm:4.0.0-beta.5" + checksum: 10/8f745da2e8fb38e91297a8ec13c2febe3219f8383303cd4ed4660ca67190242ccfd5fdc2f0d1642fd1ea934818fb871cd4cc28d3f28e812e3dc6c3d0f1f97c24 + languageName: node + linkType: hard + "agent-base@npm:6, agent-base@npm:^6.0.2": version: 6.0.2 resolution: "agent-base@npm:6.0.2" @@ -3138,12 +2959,12 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": - version: 7.1.0 - resolution: "agent-base@npm:7.1.0" +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" dependencies: debug: "npm:^4.3.4" - checksum: 10/f7828f991470a0cc22cb579c86a18cbae83d8a3cbed39992ab34fc7217c4d126017f1c74d0ab66be87f71455318a8ea3e757d6a37881b8d0f2a2c6aa55e5418f + checksum: 10/c478fec8f79953f118704d007a38f2a185458853f5c45579b9669372bd0e12602e88dc2ad0233077831504f7cd6fcc8251c383375bba5eaaf563b102938bda26 languageName: node linkType: hard @@ -3211,9 +3032,9 @@ __metadata: linkType: hard "ansi-regex@npm:^6.0.1": - version: 6.0.1 - resolution: "ansi-regex@npm:6.0.1" - checksum: 10/1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 + version: 6.1.0 + resolution: "ansi-regex@npm:6.1.0" + checksum: 10/495834a53b0856c02acd40446f7130cb0f8284f4a39afdab20d5dc42b2e198b1196119fe887beed8f9055c4ff2055e3b2f6d4641d0be018cdfb64fedf6fc1aac languageName: node linkType: hard @@ -3292,16 +3113,17 @@ __metadata: languageName: node linkType: hard -"array-includes@npm:^3.1.7": - version: 3.1.7 - resolution: "array-includes@npm:3.1.7" +"array-includes@npm:^3.1.8": + version: 3.1.8 + resolution: "array-includes@npm:3.1.8" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - get-intrinsic: "npm:^1.2.1" + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.2" + es-object-atoms: "npm:^1.0.0" + get-intrinsic: "npm:^1.2.4" is-string: "npm:^1.0.7" - checksum: 10/856a8be5d118967665936ad33ff3b07adfc50b06753e596e91fb80c3da9b8c022e92e3cc6781156d6ad95db7109b9f603682c7df2d6a529ed01f7f6b39a4a360 + checksum: 10/290b206c9451f181fb2b1f79a3bf1c0b66bb259791290ffbada760c79b284eef6f5ae2aeb4bcff450ebc9690edd25732c4c73a3c2b340fcc0f4563aed83bf488 languageName: node linkType: hard @@ -3312,29 +3134,17 @@ __metadata: languageName: node linkType: hard -"array.prototype.filter@npm:^1.0.3": - version: 1.0.3 - resolution: "array.prototype.filter@npm:1.0.3" - dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - es-array-method-boxes-properly: "npm:^1.0.0" - is-string: "npm:^1.0.7" - checksum: 10/3da2189afb00f95559cc73fc3c50f17a071a65bb705c0b2f2e2a2b2142781215b622442368c8b4387389b6ab251adf09ad347f9a8a4cf29d24404cc5ea1e295c - languageName: node - linkType: hard - -"array.prototype.findlastindex@npm:^1.2.3": - version: 1.2.4 - resolution: "array.prototype.findlastindex@npm:1.2.4" +"array.prototype.findlastindex@npm:^1.2.5": + version: 1.2.5 + resolution: "array.prototype.findlastindex@npm:1.2.5" dependencies: - call-bind: "npm:^1.0.5" + call-bind: "npm:^1.0.7" define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.22.3" + es-abstract: "npm:^1.23.2" es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" es-shim-unscopables: "npm:^1.0.2" - checksum: 10/12d7de8da619065b9d4c40550d11c13f2fbbc863c4270ef01d022f49ef16fbe9022441ee9d60b1e952853c661dd4b3e05c21e4348d4631c6d93ddf802a252296 + checksum: 10/7c5c821f357cd53ab6cc305de8086430dd8d7a2485db87b13f843e868055e9582b1fd338f02338f67fc3a1603ceaf9610dd2a470b0b506f9d18934780f95b246 languageName: node linkType: hard @@ -3401,7 +3211,7 @@ __metadata: languageName: node linkType: hard -"available-typed-arrays@npm:^1.0.6, available-typed-arrays@npm:^1.0.7": +"available-typed-arrays@npm:^1.0.7": version: 1.0.7 resolution: "available-typed-arrays@npm:1.0.7" dependencies: @@ -3468,14 +3278,14 @@ __metadata: linkType: hard "bl@npm:*": - version: 6.0.11 - resolution: "bl@npm:6.0.11" + version: 6.0.16 + resolution: "bl@npm:6.0.16" dependencies: "@types/readable-stream": "npm:^4.0.0" buffer: "npm:^6.0.3" inherits: "npm:^2.0.4" readable-stream: "npm:^4.2.0" - checksum: 10/1d9dc6d5818c6cd454750744a9725210610fdc2c64ceeabac03523e554f093f395ff8554473d32ea7974d9649a947990f856eba1ac69a352803b54e7d409f63d + checksum: 10/6c4e5c01205b60263149ee37b896d078a9ae2302eabe52f0fdbc67f8ef6be46cae989e1711a53424a1db765bcc23a657e18909e5c63c8df20034e4da6601fe5d languageName: node linkType: hard @@ -3528,7 +3338,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.2": +"braces@npm:^3.0.3": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -3553,17 +3363,17 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.22.2": - version: 4.23.0 - resolution: "browserslist@npm:4.23.0" +"browserslist@npm:^4.24.0": + version: 4.24.0 + resolution: "browserslist@npm:4.24.0" dependencies: - caniuse-lite: "npm:^1.0.30001587" - electron-to-chromium: "npm:^1.4.668" - node-releases: "npm:^2.0.14" - update-browserslist-db: "npm:^1.0.13" + caniuse-lite: "npm:^1.0.30001663" + electron-to-chromium: "npm:^1.5.28" + node-releases: "npm:^2.0.18" + update-browserslist-db: "npm:^1.1.0" bin: browserslist: cli.js - checksum: 10/496c3862df74565dd942b4ae65f502c575cbeba1fa4a3894dad7aa3b16130dc3033bc502d8848147f7b625154a284708253d9598bcdbef5a1e34cf11dc7bad8e + checksum: 10/26c1b8ba257a0b51b102080ba9d42945af2abaa8c4cf6da21cd47b3f123fc1e81640203b293214356c2c17d9d265bb3a5ed428b6d302f383576dd6ce8fd5036c languageName: node linkType: hard @@ -3584,12 +3394,19 @@ __metadata: languageName: node linkType: hard -"builtins@npm:^5.0.0": - version: 5.0.1 - resolution: "builtins@npm:5.0.1" +"builtin-modules@npm:^3.3.0": + version: 3.3.0 + resolution: "builtin-modules@npm:3.3.0" + checksum: 10/62e063ab40c0c1efccbfa9ffa31873e4f9d57408cb396a2649981a0ecbce56aabc93c28feaccbc5658c95aab2703ad1d11980e62ec2e5e72637404e1eb60f39e + languageName: node + linkType: hard + +"builtins@npm:^5.0.1": + version: 5.1.0 + resolution: "builtins@npm:5.1.0" dependencies: semver: "npm:^7.0.0" - checksum: 10/90136fa0ba98b7a3aea33190b1262a5297164731efb6a323b0231acf60cc2ea0b2b1075dbf107038266b8b77d6045fa9631d1c3f90efc1c594ba61218fbfbb4c + checksum: 10/60aa9969f69656bf6eab82cd74b23ab805f112ae46a54b912bccc1533875760f2d2ce95e0a7d13144e35ada9f0386f17ed4961908bc9434b5a5e21375b1902b2 languageName: node linkType: hard @@ -3647,8 +3464,8 @@ __metadata: linkType: hard "cacache@npm:^18.0.0": - version: 18.0.2 - resolution: "cacache@npm:18.0.2" + version: 18.0.4 + resolution: "cacache@npm:18.0.4" dependencies: "@npmcli/fs": "npm:^3.1.0" fs-minipass: "npm:^3.0.0" @@ -3662,7 +3479,7 @@ __metadata: ssri: "npm:^10.0.0" tar: "npm:^6.1.11" unique-filename: "npm:^3.0.0" - checksum: 10/5ca58464f785d4d64ac2019fcad95451c8c89bea25949f63acd8987fcc3493eaef1beccc0fa39e673506d879d3fc1ab420760f8a14f8ddf46ea2d121805a5e96 + checksum: 10/ca2f7b2d3003f84d362da9580b5561058ccaecd46cba661cbcff0375c90734b610520d46b472a339fd032d91597ad6ed12dde8af81571197f3c9772b5d35b104 languageName: node linkType: hard @@ -3693,10 +3510,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001587": - version: 1.0.30001597 - resolution: "caniuse-lite@npm:1.0.30001597" - checksum: 10/44a268113faeee51e249cbcb3924dc3765f26cd527a134e3bb720ed20d50abd8b9291500a88beee061cc03ae9f15ddc9045d57e30d25a98efeaff4f7bb8965c1 +"caniuse-lite@npm:^1.0.30001663": + version: 1.0.30001667 + resolution: "caniuse-lite@npm:1.0.30001667" + checksum: 10/5f0c48abb806737c422f05d0d9dda72facc25ee8adbae2c2ea9c57b87d9c2fa9ad8c3f6d54f21aca4e31ee1742cb5dd1543bf6b9133e3f77f79a645876322414 languageName: node linkType: hard @@ -3783,13 +3600,6 @@ __metadata: languageName: node linkType: hard -"cjs-module-lexer@npm:^1.2.2": - version: 1.2.3 - resolution: "cjs-module-lexer@npm:1.2.3" - checksum: 10/f96a5118b0a012627a2b1c13bd2fcb92509778422aaa825c5da72300d6dcadfb47134dd2e9d97dfa31acd674891dd91642742772d19a09a8adc3e56bd2f5928c - languageName: node - linkType: hard - "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -3903,6 +3713,13 @@ __metadata: languageName: node linkType: hard +"confusing-browser-globals@npm:^1.0.10": + version: 1.0.11 + resolution: "confusing-browser-globals@npm:1.0.11" + checksum: 10/3afc635abd37e566477f610e7978b15753f0e84025c25d49236f1f14d480117185516bdd40d2a2167e6bed8048641a9854964b9c067e3dcdfa6b5d0ad3c3a5ef + languageName: node + linkType: hard + "console-control-strings@npm:^1.1.0": version: 1.1.0 resolution: "console-control-strings@npm:1.1.0" @@ -3948,69 +3765,48 @@ __metadata: languageName: node linkType: hard -"crypto-randomuuid@npm:^1.0.0": - version: 1.0.0 - resolution: "crypto-randomuuid@npm:1.0.0" - checksum: 10/b98b5723978da8561cf3fab5e8c31476682db80b4bd4592aa9ce0f83bbd85523e8659b49965d7d286de102fdcf327ee5b476695f1d574ba390dfd187c6a0b207 +"data-view-buffer@npm:^1.0.1": + version: 1.0.1 + resolution: "data-view-buffer@npm:1.0.1" + dependencies: + call-bind: "npm:^1.0.6" + es-errors: "npm:^1.3.0" + is-data-view: "npm:^1.0.1" + checksum: 10/5919a39a18ee919573336158fd162fdf8ada1bc23a139f28543fd45fac48e0ea4a3ad3bfde91de124d4106e65c4a7525f6a84c20ba0797ec890a77a96d13a82a languageName: node linkType: hard -"dc-polyfill@npm:^0.1.4": - version: 0.1.4 - resolution: "dc-polyfill@npm:0.1.4" - checksum: 10/9fbc90911dd3e1201062ce3087302298e03342e81552cee795d024a55805683554bbae29545b302be34bfdd70eb196ec81d18e5498629d0bfe41129d529272b7 +"data-view-byte-length@npm:^1.0.1": + version: 1.0.1 + resolution: "data-view-byte-length@npm:1.0.1" + dependencies: + call-bind: "npm:^1.0.7" + es-errors: "npm:^1.3.0" + is-data-view: "npm:^1.0.1" + checksum: 10/f33c65e58d8d0432ad79761f2e8a579818d724b5dc6dc4e700489b762d963ab30873c0f1c37d8f2ed12ef51c706d1195f64422856d25f067457aeec50cc40aac languageName: node linkType: hard -"dd-trace@npm:5.5.0": - version: 5.5.0 - resolution: "dd-trace@npm:5.5.0" - dependencies: - "@datadog/native-appsec": "npm:7.0.0" - "@datadog/native-iast-rewriter": "npm:2.2.3" - "@datadog/native-iast-taint-tracking": "npm:1.7.0" - "@datadog/native-metrics": "npm:^2.0.0" - "@datadog/pprof": "npm:5.0.0" - "@datadog/sketches-js": "npm:^2.1.0" - "@opentelemetry/api": "npm:^1.0.0" - "@opentelemetry/core": "npm:^1.14.0" - crypto-randomuuid: "npm:^1.0.0" - dc-polyfill: "npm:^0.1.4" - ignore: "npm:^5.2.4" - import-in-the-middle: "npm:^1.7.3" - int64-buffer: "npm:^0.1.9" - ipaddr.js: "npm:^2.1.0" - istanbul-lib-coverage: "npm:3.2.0" - jest-docblock: "npm:^29.7.0" - koalas: "npm:^1.0.2" - limiter: "npm:1.1.5" - lodash.sortby: "npm:^4.7.0" - lru-cache: "npm:^7.14.0" - methods: "npm:^1.1.2" - module-details-from-path: "npm:^1.0.3" - msgpack-lite: "npm:^0.1.26" - node-abort-controller: "npm:^3.1.1" - opentracing: "npm:>=0.12.1" - path-to-regexp: "npm:^0.1.2" - pprof-format: "npm:^2.0.7" - protobufjs: "npm:^7.2.5" - retry: "npm:^0.13.1" - semver: "npm:^7.5.4" - shell-quote: "npm:^1.8.1" - tlhunter-sorted-set: "npm:^0.1.0" - checksum: 10/7d7b8842a3976aba8f0beda7a052af877af0a8536c17748766c143ac6008342950878c0cd2cfafea3c12e17ce2e78d8fa24a8ae4ab4beee65b602586bb085a85 +"data-view-byte-offset@npm:^1.0.0": + version: 1.0.0 + resolution: "data-view-byte-offset@npm:1.0.0" + dependencies: + call-bind: "npm:^1.0.6" + es-errors: "npm:^1.3.0" + is-data-view: "npm:^1.0.1" + checksum: 10/96f34f151bf02affb7b9f98762fb7aca1dd5f4553cb57b80bce750ca609c15d33ca659568ef1d422f7e35680736cbccb893a3d4b012760c758c1446bbdc4c6db languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": - version: 4.3.4 - resolution: "debug@npm:4.3.4" +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6": + version: 4.3.7 + resolution: "debug@npm:4.3.7" dependencies: - ms: "npm:2.1.2" + ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/0073c3bcbd9cb7d71dd5f6b55be8701af42df3e56e911186dfa46fac3a5b9eb7ce7f377dd1d3be6db8977221f8eb333d945216f645cf56f6b688cd484837d255 + checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a languageName: node linkType: hard @@ -4023,30 +3819,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4.3.5": - version: 4.3.5 - resolution: "debug@npm:4.3.5" - dependencies: - ms: "npm:2.1.2" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10/cb6eab424c410e07813ca1392888589972ce9a32b8829c6508f5e1f25f3c3e70a76731610ae55b4bbe58d1a2fffa1424b30e97fa8d394e49cd2656a9643aedd2 - languageName: node - linkType: hard - -"debug@npm:^4.3.6": - version: 4.3.7 - resolution: "debug@npm:4.3.7" - dependencies: - ms: "npm:^2.1.3" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a - languageName: node - linkType: hard - "deep-eql@npm:^5.0.1": version: 5.0.2 resolution: "deep-eql@npm:5.0.2" @@ -4061,7 +3833,7 @@ __metadata: languageName: node linkType: hard -"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.2, define-data-property@npm:^1.1.4": +"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": version: 1.1.4 resolution: "define-data-property@npm:1.1.4" dependencies: @@ -4072,7 +3844,7 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": +"define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" dependencies: @@ -4083,13 +3855,6 @@ __metadata: languageName: node linkType: hard -"delay@npm:^5.0.0": - version: 5.0.0 - resolution: "delay@npm:5.0.0" - checksum: 10/62f151151ecfde0d9afbb8a6be37a6d103c4cb24f35a20ef3fe56f920b0d0d0bb02bc9c0a3084d0179ef669ca332b91155f2ee4d9854622cd2cdba5fc95285f9 - languageName: node - linkType: hard - "delegates@npm:^1.0.0": version: 1.0.0 resolution: "delegates@npm:1.0.0" @@ -4125,13 +3890,6 @@ __metadata: languageName: node linkType: hard -"detect-newline@npm:^3.0.0": - version: 3.1.0 - resolution: "detect-newline@npm:3.1.0" - checksum: 10/ae6cd429c41ad01b164c59ea36f264a2c479598e61cba7c99da24175a7ab80ddf066420f2bec9a1c57a6bead411b4655ff15ad7d281c000a89791f48cbe939e7 - languageName: node - linkType: hard - "detect-newline@npm:^4.0.0": version: 4.0.1 resolution: "detect-newline@npm:4.0.1" @@ -4196,10 +3954,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.668": - version: 1.4.703 - resolution: "electron-to-chromium@npm:1.4.703" - checksum: 10/e7927fbe75e56508dd0b4efeb0e69dfb8ee1e6e6aaf6f07c047b96ff530d8f49e1eaf51cae64c2d3c179e3932fb37661012ccaa4f36956dd96480219f3a23013 +"electron-to-chromium@npm:^1.5.28": + version: 1.5.35 + resolution: "electron-to-chromium@npm:1.5.35" + checksum: 10/fe8cf5f1ec8587bbd256ddede11272808dd25cdef9d06d546c66495364e03463e9a3b664bf11ec077c1dbc91114dbeb52802b01d944e57ce922112d00b722a74 languageName: node linkType: hard @@ -4218,7 +3976,7 @@ __metadata: languageName: node linkType: hard -"elliptic@npm:^6.5.7": +"elliptic@npm:^6.5.4, elliptic@npm:^6.5.7": version: 6.5.7 resolution: "elliptic@npm:6.5.7" dependencies: @@ -4257,12 +4015,12 @@ __metadata: linkType: hard "enhanced-resolve@npm:^5.17.0": - version: 5.17.0 - resolution: "enhanced-resolve@npm:5.17.0" + version: 5.17.1 + resolution: "enhanced-resolve@npm:5.17.1" dependencies: graceful-fs: "npm:^4.2.4" tapable: "npm:^2.2.0" - checksum: 10/8f7bf71537d78e7d20a27363793f2c9e13ec44800c7c7830364a448f80a44994aa19d64beecefa1ab49e4de6f7fbe18cc0931dc449c115f02918ff5fcbe7705f + checksum: 10/e8e03cb7a4bf3c0250a89afbd29e5ec20e90ba5fcd026066232a0754864d7d0a393fa6fc0e5379314a6529165a1834b36731147080714459d98924520410d8f5 languageName: node linkType: hard @@ -4297,17 +4055,21 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3": - version: 1.22.4 - resolution: "es-abstract@npm:1.22.4" +"es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3, es-abstract@npm:^1.23.0, es-abstract@npm:^1.23.2": + version: 1.23.3 + resolution: "es-abstract@npm:1.23.3" dependencies: array-buffer-byte-length: "npm:^1.0.1" arraybuffer.prototype.slice: "npm:^1.0.3" - available-typed-arrays: "npm:^1.0.6" + available-typed-arrays: "npm:^1.0.7" call-bind: "npm:^1.0.7" + data-view-buffer: "npm:^1.0.1" + data-view-byte-length: "npm:^1.0.1" + data-view-byte-offset: "npm:^1.0.0" es-define-property: "npm:^1.0.0" es-errors: "npm:^1.3.0" - es-set-tostringtag: "npm:^2.0.2" + es-object-atoms: "npm:^1.0.0" + es-set-tostringtag: "npm:^2.0.3" es-to-primitive: "npm:^1.2.1" function.prototype.name: "npm:^1.1.6" get-intrinsic: "npm:^1.2.4" @@ -4315,15 +4077,16 @@ __metadata: globalthis: "npm:^1.0.3" gopd: "npm:^1.0.1" has-property-descriptors: "npm:^1.0.2" - has-proto: "npm:^1.0.1" + has-proto: "npm:^1.0.3" has-symbols: "npm:^1.0.3" - hasown: "npm:^2.0.1" + hasown: "npm:^2.0.2" internal-slot: "npm:^1.0.7" is-array-buffer: "npm:^3.0.4" is-callable: "npm:^1.2.7" - is-negative-zero: "npm:^2.0.2" + is-data-view: "npm:^1.0.1" + is-negative-zero: "npm:^2.0.3" is-regex: "npm:^1.1.4" - is-shared-array-buffer: "npm:^1.0.2" + is-shared-array-buffer: "npm:^1.0.3" is-string: "npm:^1.0.7" is-typed-array: "npm:^1.1.13" is-weakref: "npm:^1.0.2" @@ -4331,25 +4094,18 @@ __metadata: object-keys: "npm:^1.1.1" object.assign: "npm:^4.1.5" regexp.prototype.flags: "npm:^1.5.2" - safe-array-concat: "npm:^1.1.0" + safe-array-concat: "npm:^1.1.2" safe-regex-test: "npm:^1.0.3" - string.prototype.trim: "npm:^1.2.8" - string.prototype.trimend: "npm:^1.0.7" - string.prototype.trimstart: "npm:^1.0.7" - typed-array-buffer: "npm:^1.0.1" - typed-array-byte-length: "npm:^1.0.0" - typed-array-byte-offset: "npm:^1.0.0" - typed-array-length: "npm:^1.0.4" + string.prototype.trim: "npm:^1.2.9" + string.prototype.trimend: "npm:^1.0.8" + string.prototype.trimstart: "npm:^1.0.8" + typed-array-buffer: "npm:^1.0.2" + typed-array-byte-length: "npm:^1.0.1" + typed-array-byte-offset: "npm:^1.0.2" + typed-array-length: "npm:^1.0.6" unbox-primitive: "npm:^1.0.2" - which-typed-array: "npm:^1.1.14" - checksum: 10/062e562a000e280c0c0683ad4a7b81732f97463bc769110c668a8edb739cd5df56975fa55965f5304a3256fd6eee03b9b66a47d863076f8976c2050731946b1f - languageName: node - linkType: hard - -"es-array-method-boxes-properly@npm:^1.0.0": - version: 1.0.0 - resolution: "es-array-method-boxes-properly@npm:1.0.0" - checksum: 10/27a8a21acf20f3f51f69dce8e643f151e380bffe569e95dc933b9ded9fcd89a765ee21b5229c93f9206c93f87395c6b75f80be8ac8c08a7ceb8771e1822ff1fb + which-typed-array: "npm:^1.1.15" + checksum: 10/2da795a6a1ac5fc2c452799a409acc2e3692e06dc6440440b076908617188899caa562154d77263e3053bcd9389a07baa978ab10ac3b46acc399bd0c77be04cb languageName: node linkType: hard @@ -4362,7 +4118,7 @@ __metadata: languageName: node linkType: hard -"es-errors@npm:^1.0.0, es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": +"es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": version: 1.3.0 resolution: "es-errors@npm:1.3.0" checksum: 10/96e65d640156f91b707517e8cdc454dd7d47c32833aa3e85d79f24f9eb7ea85f39b63e36216ef0114996581969b59fe609a94e30316b08f5f4df1d44134cf8d5 @@ -4376,7 +4132,16 @@ __metadata: languageName: node linkType: hard -"es-set-tostringtag@npm:^2.0.2": +"es-object-atoms@npm:^1.0.0": + version: 1.0.0 + resolution: "es-object-atoms@npm:1.0.0" + dependencies: + es-errors: "npm:^1.3.0" + checksum: 10/f8910cf477e53c0615f685c5c96210591841850871b81924fcf256bfbaa68c254457d994a4308c60d15b20805e7f61ce6abc669375e01a5349391a8c1767584f + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.0.3": version: 2.0.3 resolution: "es-set-tostringtag@npm:2.0.3" dependencies: @@ -4407,86 +4172,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.19.3": - version: 0.19.12 - resolution: "esbuild@npm:0.19.12" - dependencies: - "@esbuild/aix-ppc64": "npm:0.19.12" - "@esbuild/android-arm": "npm:0.19.12" - "@esbuild/android-arm64": "npm:0.19.12" - "@esbuild/android-x64": "npm:0.19.12" - "@esbuild/darwin-arm64": "npm:0.19.12" - "@esbuild/darwin-x64": "npm:0.19.12" - "@esbuild/freebsd-arm64": "npm:0.19.12" - "@esbuild/freebsd-x64": "npm:0.19.12" - "@esbuild/linux-arm": "npm:0.19.12" - "@esbuild/linux-arm64": "npm:0.19.12" - "@esbuild/linux-ia32": "npm:0.19.12" - "@esbuild/linux-loong64": "npm:0.19.12" - "@esbuild/linux-mips64el": "npm:0.19.12" - "@esbuild/linux-ppc64": "npm:0.19.12" - "@esbuild/linux-riscv64": "npm:0.19.12" - "@esbuild/linux-s390x": "npm:0.19.12" - "@esbuild/linux-x64": "npm:0.19.12" - "@esbuild/netbsd-x64": "npm:0.19.12" - "@esbuild/openbsd-x64": "npm:0.19.12" - "@esbuild/sunos-x64": "npm:0.19.12" - "@esbuild/win32-arm64": "npm:0.19.12" - "@esbuild/win32-ia32": "npm:0.19.12" - "@esbuild/win32-x64": "npm:0.19.12" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10/861fa8eb2428e8d6521a4b7c7930139e3f45e8d51a86985cc29408172a41f6b18df7b3401e7e5e2d528cdf83742da601ddfdc77043ddc4f1c715a8ddb2d8a255 - languageName: node - linkType: hard - "esbuild@npm:^0.21.3": version: 0.21.5 resolution: "esbuild@npm:0.21.5" @@ -4567,10 +4252,10 @@ __metadata: languageName: node linkType: hard -"escalade@npm:^3.1.1": - version: 3.1.2 - resolution: "escalade@npm:3.1.2" - checksum: 10/a1e07fea2f15663c30e40b9193d658397846ffe28ce0a3e4da0d8e485fedfeca228ab846aee101a05015829adf39f9934ff45b2a3fca47bed37a29646bd05cd3 +"escalade@npm:^3.2.0": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 10/9d7169e3965b2f9ae46971afa392f6e5a25545ea30f2e2dd99c9b0a95a3f52b5653681a84f5b2911a413ddad2d7a93d3514165072f349b5ffc59c75a899970d6 languageName: node linkType: hard @@ -4588,15 +4273,63 @@ __metadata: languageName: node linkType: hard -"eslint-compat-utils@npm:^0.1.2": - version: 0.1.2 - resolution: "eslint-compat-utils@npm:0.1.2" +"eslint-compat-utils@npm:^0.5.1": + version: 0.5.1 + resolution: "eslint-compat-utils@npm:0.5.1" + dependencies: + semver: "npm:^7.5.4" peerDependencies: eslint: ">=6.0.0" - checksum: 10/8c273889485ab863f2b6089c36f042dd2d9bf65d219fa256c1991f9466ea8261f3ab753a017a6d8e93bec84abd568fe3e10216f3fda5b41d05e3dce3b2a5e514 + checksum: 10/ac65ac1c6107cf19f63f5fc17cea361c9cb1336be7356f23dbb0fac10979974b4622e13e950be43cbf431801f2c07f7dab448573181ccf6edc0b86d5b5304511 languageName: node linkType: hard +"eslint-config-airbnb-base@npm:^15.0.0": + version: 15.0.0 + resolution: "eslint-config-airbnb-base@npm:15.0.0" + dependencies: + confusing-browser-globals: "npm:^1.0.10" + object.assign: "npm:^4.1.2" + object.entries: "npm:^1.1.5" + semver: "npm:^6.3.0" + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 + checksum: 10/daa68a1dcb7bff338747a952723b5fa9d159980ec3554c395a4b52a7f7d4f00a45e7b465420eb6d4d87a82cef6361e4cfd6dbb38c2f3f52f2140b6cf13654803 + languageName: node + linkType: hard + +"eslint-config-airbnb-typescript@npm:^18.0.0": + version: 18.0.0 + resolution: "eslint-config-airbnb-typescript@npm:18.0.0" + dependencies: + eslint-config-airbnb-base: "npm:^15.0.0" + peerDependencies: + "@typescript-eslint/eslint-plugin": ^7.0.0 + "@typescript-eslint/parser": ^7.0.0 + eslint: ^8.56.0 + checksum: 10/b913670baf3aa457aa1d514ea63813e76f2232a7efdb149ce96cecb10d836cadea6776a304529f1ae371d2e721479540461e89735bdde85a949e2bf62eb3187c + languageName: node + linkType: hard + +"eslint-config-custom@workspace:*, eslint-config-custom@workspace:shared/eslint-config-custom": + version: 0.0.0-use.local + resolution: "eslint-config-custom@workspace:shared/eslint-config-custom" + dependencies: + "@typescript-eslint/eslint-plugin": "npm:^7.18.0" + "@typescript-eslint/parser": "npm:^7.18.0" + eslint: "npm:^8.57.0" + eslint-config-airbnb-base: "npm:^15.0.0" + eslint-config-airbnb-typescript: "npm:^18.0.0" + eslint-config-prettier: "npm:^9.1.0" + eslint-config-standard-with-typescript: "npm:^43.0.1" + eslint-plugin-import: "npm:^2.29.1" + eslint-plugin-n: "npm:^16.6.2" + eslint-plugin-promise: "npm:^6.1.1" + typescript: "npm:^5.6.3" + languageName: unknown + linkType: soft + "eslint-config-prettier@npm:^9.1.0": version: 9.1.0 resolution: "eslint-config-prettier@npm:9.1.0" @@ -4608,7 +4341,24 @@ __metadata: languageName: node linkType: hard -"eslint-config-standard@npm:^17.1.0": +"eslint-config-standard-with-typescript@npm:^43.0.1": + version: 43.0.1 + resolution: "eslint-config-standard-with-typescript@npm:43.0.1" + dependencies: + "@typescript-eslint/parser": "npm:^6.4.0" + eslint-config-standard: "npm:17.1.0" + peerDependencies: + "@typescript-eslint/eslint-plugin": ^6.4.0 + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: "^15.0.0 || ^16.0.0 " + eslint-plugin-promise: ^6.0.0 + typescript: "*" + checksum: 10/26a460efd918262ef865abd1f1d7ad4a41c2f368b560ca0ccf16b674f2ca3a778ba5b2a43845421b48fa9ce118a68765222086c30c35e407f878fa1e9170576d + languageName: node + linkType: hard + +"eslint-config-standard@npm:17.1.0, eslint-config-standard@npm:^17.1.0": version: 17.1.0 resolution: "eslint-config-standard@npm:17.1.0" peerDependencies: @@ -4631,28 +4381,28 @@ __metadata: languageName: node linkType: hard -"eslint-module-utils@npm:^2.8.0": - version: 2.8.0 - resolution: "eslint-module-utils@npm:2.8.0" +"eslint-module-utils@npm:^2.12.0": + version: 2.12.0 + resolution: "eslint-module-utils@npm:2.12.0" dependencies: debug: "npm:^3.2.7" peerDependenciesMeta: eslint: optional: true - checksum: 10/a9a7ed93eb858092e3cdc797357d4ead2b3ea06959b0eada31ab13862d46a59eb064b9cb82302214232e547980ce33618c2992f6821138a4934e65710ed9cc29 + checksum: 10/dd27791147eca17366afcb83f47d6825b6ce164abb256681e5de4ec1d7e87d8605641eb869298a0dbc70665e2446dbcc2f40d3e1631a9475dd64dd23d4ca5dee languageName: node linkType: hard "eslint-plugin-es-x@npm:^7.5.0": - version: 7.5.0 - resolution: "eslint-plugin-es-x@npm:7.5.0" + version: 7.8.0 + resolution: "eslint-plugin-es-x@npm:7.8.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.1.2" - "@eslint-community/regexpp": "npm:^4.6.0" - eslint-compat-utils: "npm:^0.1.2" + "@eslint-community/regexpp": "npm:^4.11.0" + eslint-compat-utils: "npm:^0.5.1" peerDependencies: eslint: ">=8" - checksum: 10/b0aa59e5a9fe034d6d485969091abfcdc6893bc0b9b145864d29307b03465141cc073bed806d9cb1a343a561362f2d0e9b34526af8fe8b7ca3cd8aa144f3720a + checksum: 10/1df8d52c4fadc06854ce801af05b05f2642aa2deb918fb7d37738596eabd70b7f21a22b150b78ec9104bac6a1b6b4fb796adea2364ede91b01d20964849ce5f7 languageName: node linkType: hard @@ -4669,67 +4419,91 @@ __metadata: linkType: hard "eslint-plugin-import@npm:^2.29.1": - version: 2.29.1 - resolution: "eslint-plugin-import@npm:2.29.1" + version: 2.31.0 + resolution: "eslint-plugin-import@npm:2.31.0" dependencies: - array-includes: "npm:^3.1.7" - array.prototype.findlastindex: "npm:^1.2.3" + "@rtsao/scc": "npm:^1.1.0" + array-includes: "npm:^3.1.8" + array.prototype.findlastindex: "npm:^1.2.5" array.prototype.flat: "npm:^1.3.2" array.prototype.flatmap: "npm:^1.3.2" debug: "npm:^3.2.7" doctrine: "npm:^2.1.0" eslint-import-resolver-node: "npm:^0.3.9" - eslint-module-utils: "npm:^2.8.0" - hasown: "npm:^2.0.0" - is-core-module: "npm:^2.13.1" + eslint-module-utils: "npm:^2.12.0" + hasown: "npm:^2.0.2" + is-core-module: "npm:^2.15.1" is-glob: "npm:^4.0.3" minimatch: "npm:^3.1.2" - object.fromentries: "npm:^2.0.7" - object.groupby: "npm:^1.0.1" - object.values: "npm:^1.1.7" + object.fromentries: "npm:^2.0.8" + object.groupby: "npm:^1.0.3" + object.values: "npm:^1.2.0" semver: "npm:^6.3.1" + string.prototype.trimend: "npm:^1.0.8" tsconfig-paths: "npm:^3.15.0" peerDependencies: - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - checksum: 10/5865f05c38552145423c535326ec9a7113ab2305c7614c8b896ff905cfabc859c8805cac21e979c9f6f742afa333e6f62f812eabf891a7e8f5f0b853a32593c1 + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + checksum: 10/6b76bd009ac2db0615d9019699d18e2a51a86cb8c1d0855a35fb1b418be23b40239e6debdc6e8c92c59f1468ed0ea8d7b85c817117a113d5cc225be8a02ad31c languageName: node linkType: hard "eslint-plugin-jsdoc@npm:^48.7.0": - version: 48.7.0 - resolution: "eslint-plugin-jsdoc@npm:48.7.0" + version: 48.11.0 + resolution: "eslint-plugin-jsdoc@npm:48.11.0" dependencies: "@es-joy/jsdoccomment": "npm:~0.46.0" are-docs-informative: "npm:^0.0.2" comment-parser: "npm:1.4.1" debug: "npm:^4.3.5" escape-string-regexp: "npm:^4.0.0" + espree: "npm:^10.1.0" esquery: "npm:^1.6.0" parse-imports: "npm:^2.1.1" - semver: "npm:^7.6.2" + semver: "npm:^7.6.3" spdx-expression-parse: "npm:^4.0.0" - synckit: "npm:^0.9.0" + synckit: "npm:^0.9.1" peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - checksum: 10/7289c37e45429cf3661bd553bb6cdcb4a9877b8a1fc0e6f369fa049cf6690953333dcbc972f48f03991df53734afeb77c6025804392184df19e214fb4425c4dc + checksum: 10/3bc2533656e9ccfdadbcd71a6f7c1ec125b1965c6e399a43c40408b51b4f8c26e44031f077c947b15d68b9cd317e7e8be1e2b222a46fb3c24a25377a2643796b + languageName: node + linkType: hard + +"eslint-plugin-n@npm:^16.6.2": + version: 16.6.2 + resolution: "eslint-plugin-n@npm:16.6.2" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + builtins: "npm:^5.0.1" + eslint-plugin-es-x: "npm:^7.5.0" + get-tsconfig: "npm:^4.7.0" + globals: "npm:^13.24.0" + ignore: "npm:^5.2.4" + is-builtin-module: "npm:^3.2.1" + is-core-module: "npm:^2.12.1" + minimatch: "npm:^3.1.2" + resolve: "npm:^1.22.2" + semver: "npm:^7.5.3" + peerDependencies: + eslint: ">=7.0.0" + checksum: 10/e0f600d03d3a3df57e9a811648b1b534a6d67c90ea9406340ddf3763c2b87cf5ef910b390f787ca5cb27c8d8ff36aad42d70209b54e2a1cb4cc2507ca417229a languageName: node linkType: hard "eslint-plugin-n@npm:^17.9.0": - version: 17.9.0 - resolution: "eslint-plugin-n@npm:17.9.0" + version: 17.11.1 + resolution: "eslint-plugin-n@npm:17.11.1" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" enhanced-resolve: "npm:^5.17.0" eslint-plugin-es-x: "npm:^7.5.0" get-tsconfig: "npm:^4.7.0" - globals: "npm:^15.0.0" + globals: "npm:^15.8.0" ignore: "npm:^5.2.4" - minimatch: "npm:^9.0.0" + minimatch: "npm:^9.0.5" semver: "npm:^7.5.3" peerDependencies: eslint: ">=8.23.0" - checksum: 10/b1f4753d67e58b3a2dd5adb24f06d7f2ab731622f4b0f38ea980f236f1f5bd6edb97257c81ba545eec97c556213cc65d16e7118064eef302f7ab611fef6accd0 + checksum: 10/f9cd02f5ab6d45448cdc66e836978590d6ab0db3a4135d1463209b04a90b5b996b097687824a6951bae0b6a63d1da6a53f6559eb26efe1d0068dc5d03f7712ce languageName: node linkType: hard @@ -4750,11 +4524,11 @@ __metadata: linkType: hard "eslint-plugin-prettier@npm:^5.1.3": - version: 5.1.3 - resolution: "eslint-plugin-prettier@npm:5.1.3" + version: 5.2.1 + resolution: "eslint-plugin-prettier@npm:5.2.1" dependencies: prettier-linter-helpers: "npm:^1.0.0" - synckit: "npm:^0.8.6" + synckit: "npm:^0.9.1" peerDependencies: "@types/eslint": ">=8.0.0" eslint: ">=8.0.0" @@ -4765,16 +4539,16 @@ __metadata: optional: true eslint-config-prettier: optional: true - checksum: 10/4f26a30444adc61ed692cdb5a9f7e8d9f5794f0917151051e66755ce032a08c3cc72c8b5d56101412e90f6d77035bd8194ea8731e9c16aacdd5ae345a8dae188 + checksum: 10/10ddf68215237e327af09a47adab4c63f3885fda4fb28c4c42d1fc5f47d8a0cc45df6484799360ff1417a0aa3c77c3aaac49d7e9dfd145557b17e2d7ecc2a27c languageName: node linkType: hard -"eslint-plugin-promise@npm:^6.4.0": - version: 6.4.0 - resolution: "eslint-plugin-promise@npm:6.4.0" +"eslint-plugin-promise@npm:^6.1.1, eslint-plugin-promise@npm:^6.4.0": + version: 6.6.0 + resolution: "eslint-plugin-promise@npm:6.6.0" peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - checksum: 10/23da32294b8661e95d4243b7cc925aefe3522842d5f6e2a6f72d052f92cfd96536d592e5186be6eb471e477edc2fe20ca257e7e1b5a786a6e582be3d65fdc4f5 + checksum: 10/c2b5604efd7e1390c132fcbf06cb2f072c956ffa65c14a991cb74ba1e2327357797239cb5b9b292d5e4010301bb897bd85a6273d7873fb157edc46aa2d95cbd9 languageName: node linkType: hard @@ -4811,15 +4585,22 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^4.1.0": + version: 4.1.0 + resolution: "eslint-visitor-keys@npm:4.1.0" + checksum: 10/3fb5bd1b2f36db89d0ac57ddd66d36ccd3b1e3cddb2a55a0f9f6f1c85268cfcc1cc32e7eda4990e3423107a120dd254fb6cb52d6154cf81d344d8c3fa671f7c2 + languageName: node + linkType: hard + "eslint@npm:^8.57.0": - version: 8.57.0 - resolution: "eslint@npm:8.57.0" + version: 8.57.1 + resolution: "eslint@npm:8.57.1" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@eslint-community/regexpp": "npm:^4.6.1" "@eslint/eslintrc": "npm:^2.1.4" - "@eslint/js": "npm:8.57.0" - "@humanwhocodes/config-array": "npm:^0.11.14" + "@eslint/js": "npm:8.57.1" + "@humanwhocodes/config-array": "npm:^0.13.0" "@humanwhocodes/module-importer": "npm:^1.0.1" "@nodelib/fs.walk": "npm:^1.2.8" "@ungap/structured-clone": "npm:^1.2.0" @@ -4855,7 +4636,18 @@ __metadata: text-table: "npm:^0.2.0" bin: eslint: bin/eslint.js - checksum: 10/00496e218b23747a7a9817bf58b522276d0dc1f2e546dceb4eea49f9871574088f72f1f069a6b560ef537efa3a75261b8ef70e51ef19033da1cc4c86a755ef15 + checksum: 10/5504fa24879afdd9f9929b2fbfc2ee9b9441a3d464efd9790fbda5f05738858530182029f13323add68d19fec749d3ab4a70320ded091ca4432b1e9cc4ed104c + languageName: node + linkType: hard + +"espree@npm:^10.1.0": + version: 10.2.0 + resolution: "espree@npm:10.2.0" + dependencies: + acorn: "npm:^8.12.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^4.1.0" + checksum: 10/365076a963ca84244c1e2d36e4f812362d21cfa7e7df10d67f7b82b759467796df81184721d153c4e235d9ef5eb5b4d044167dd66be3be00f53a21a515b1bfb1 languageName: node linkType: hard @@ -4880,16 +4672,7 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.2": - version: 1.5.0 - resolution: "esquery@npm:1.5.0" - dependencies: - estraverse: "npm:^5.1.0" - checksum: 10/e65fcdfc1e0ff5effbf50fb4f31ea20143ae5df92bb2e4953653d8d40aa4bc148e0d06117a592ce4ea53eeab1dafdfded7ea7e22a5be87e82d73757329a1b01d - languageName: node - linkType: hard - -"esquery@npm:^1.6.0": +"esquery@npm:^1.4.2, esquery@npm:^1.6.0": version: 1.6.0 resolution: "esquery@npm:1.6.0" dependencies: @@ -4938,14 +4721,14 @@ __metadata: linkType: hard "ethereum-cryptography@npm:^2.0.0": - version: 2.1.3 - resolution: "ethereum-cryptography@npm:2.1.3" + version: 2.2.1 + resolution: "ethereum-cryptography@npm:2.2.1" dependencies: - "@noble/curves": "npm:1.3.0" - "@noble/hashes": "npm:1.3.3" - "@scure/bip32": "npm:1.3.3" - "@scure/bip39": "npm:1.2.2" - checksum: 10/cc5aa9a4368dc1dd7680ba921957c098ced7b3d7dbb1666334013ab2f8d4cd25a785ad84e66fd9f5c5a9b6de337930ea24ff8c722938f36a9c00cec597ca16b5 + "@noble/curves": "npm:1.4.2" + "@noble/hashes": "npm:1.4.0" + "@scure/bip32": "npm:1.4.0" + "@scure/bip39": "npm:1.3.0" + checksum: 10/ab123bbfe843500ac2d645ce9edc4bc814962ffb598db6bf8bf01fbecac656e6c81ff4cf2472f1734844bbcbad2bf658d8b699cb7248d768e0f06ae13ecf43b8 languageName: node linkType: hard @@ -4987,10 +4770,18 @@ __metadata: languageName: node linkType: hard -"event-lite@npm:^0.1.1": - version: 0.1.3 - resolution: "event-lite@npm:0.1.3" - checksum: 10/5185098b4f61285206cf145285079d7d99e7f54611d7ed15c3f867e0136e6b0d8c02d493775f8ac5844bb3eb7bf1ac6ce3a023ac421e0cc3d621399b3d814ea7 +"ethers@npm:^6.11.1": + version: 6.13.3 + resolution: "ethers@npm:6.13.3" + dependencies: + "@adraffy/ens-normalize": "npm:1.10.1" + "@noble/curves": "npm:1.2.0" + "@noble/hashes": "npm:1.3.2" + "@types/node": "npm:18.15.13" + aes-js: "npm:4.0.0-beta.5" + tslib: "npm:2.4.0" + ws: "npm:8.17.1" + checksum: 10/a3b11a5bd97269f2aa5e5cb844642a84fe139a188fd3c0d7d0c4c7b4958d56286e84c14cd41d1c53bd5dff8bf1060c73bd7a9bde8313f8a994d94881f4010037 languageName: node linkType: hard @@ -5185,12 +4976,12 @@ __metadata: linkType: hard "foreground-child@npm:^3.1.0": - version: 3.1.1 - resolution: "foreground-child@npm:3.1.1" + version: 3.3.0 + resolution: "foreground-child@npm:3.3.0" dependencies: cross-spawn: "npm:^7.0.0" signal-exit: "npm:^4.0.1" - checksum: 10/087edd44857d258c4f73ad84cb8df980826569656f2550c341b27adf5335354393eec24ea2fabd43a253233fb27cee177ebe46bd0b7ea129c77e87cb1e9936fb + checksum: 10/e3a60480f3a09b12273ce2c5fcb9514d98dd0e528f58656a1b04680225f918d60a2f81f6a368f2f3b937fcee9cfc0cbf16f1ad9a0bc6a3a6e103a84c9a90087e languageName: node linkType: hard @@ -5320,14 +5111,7 @@ __metadata: languageName: node linkType: hard -"get-func-name@npm:^2.0.1": - version: 2.0.2 - resolution: "get-func-name@npm:2.0.2" - checksum: 10/3f62f4c23647de9d46e6f76d2b3eafe58933a9b3830c60669e4180d6c601ce1b4aa310ba8366143f55e52b139f992087a9f0647274e8745621fa2af7e0acf13b - languageName: node - linkType: hard - -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2, get-intrinsic@npm:^1.2.3, get-intrinsic@npm:^1.2.4": +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.3, get-intrinsic@npm:^1.2.4": version: 1.2.4 resolution: "get-intrinsic@npm:1.2.4" dependencies: @@ -5359,11 +5143,11 @@ __metadata: linkType: hard "get-tsconfig@npm:^4.7.0": - version: 4.7.2 - resolution: "get-tsconfig@npm:4.7.2" + version: 4.8.1 + resolution: "get-tsconfig@npm:4.8.1" dependencies: resolve-pkg-maps: "npm:^1.0.0" - checksum: 10/f21135848fb5d16012269b7b34b186af7a41824830f8616aba17a15eb4d9e54fdc876833f1e21768395215a826c8145582f5acd594ae2b4de3284d10b38d20f8 + checksum: 10/3fb5a8ad57b9633eaea085d81661e9e5c9f78b35d8f8689eaf8b8b45a2a3ebf3b3422266d4d7df765e308cc1e6231648d114803ab3d018332e29916f2c1de036 languageName: node linkType: hard @@ -5392,34 +5176,35 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10": - version: 10.3.10 - resolution: "glob@npm:10.3.10" +"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.4.1": + version: 10.4.5 + resolution: "glob@npm:10.4.5" dependencies: foreground-child: "npm:^3.1.0" - jackspeak: "npm:^2.3.5" - minimatch: "npm:^9.0.1" - minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry: "npm:^1.10.1" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" bin: glob: dist/esm/bin.mjs - checksum: 10/38bdb2c9ce75eb5ed168f309d4ed05b0798f640b637034800a6bf306f39d35409bf278b0eaaffaec07591085d3acb7184a201eae791468f0f617771c2486a6a8 + checksum: 10/698dfe11828b7efd0514cd11e573eaed26b2dff611f0400907281ce3eab0c1e56143ef9b35adc7c77ecc71fba74717b510c7c223d34ca8a98ec81777b293d4ac languageName: node linkType: hard -"glob@npm:^10.4.1": - version: 10.4.5 - resolution: "glob@npm:10.4.5" +"glob@npm:^11.0.0": + version: 11.0.0 + resolution: "glob@npm:11.0.0" dependencies: foreground-child: "npm:^3.1.0" - jackspeak: "npm:^3.1.2" - minimatch: "npm:^9.0.4" + jackspeak: "npm:^4.0.1" + minimatch: "npm:^10.0.0" minipass: "npm:^7.1.2" package-json-from-dist: "npm:^1.0.0" - path-scurry: "npm:^1.11.1" + path-scurry: "npm:^2.0.0" bin: glob: dist/esm/bin.mjs - checksum: 10/698dfe11828b7efd0514cd11e573eaed26b2dff611f0400907281ce3eab0c1e56143ef9b35adc7c77ecc71fba74717b510c7c223d34ca8a98ec81777b293d4ac + checksum: 10/e66939201d11ae30fe97e3364ac2be5c59d6c9bfce18ac633edfad473eb6b46a7553f6f73658f67caaf6cccc1df1ae336298a45e9021fa5695fd78754cc1603e languageName: node linkType: hard @@ -5457,7 +5242,7 @@ __metadata: languageName: node linkType: hard -"globals@npm:^13.19.0": +"globals@npm:^13.19.0, globals@npm:^13.24.0": version: 13.24.0 resolution: "globals@npm:13.24.0" dependencies: @@ -5466,19 +5251,20 @@ __metadata: languageName: node linkType: hard -"globals@npm:^15.0.0": - version: 15.1.0 - resolution: "globals@npm:15.1.0" - checksum: 10/441931212b8b629940484aa6e505a4b7f590bed9f04f381d22e65d412bdb0c1e463aa6ca997ca0ce8a1a4966e934dc68c80ddbe74769adb33b6c7765ee6bef21 +"globals@npm:^15.8.0": + version: 15.11.0 + resolution: "globals@npm:15.11.0" + checksum: 10/14009ef1906ac929d930ed1c896a47159e7d11b4d201901ca5f3827766519191a3f5fb45124de43c4511fee04018704e7ed5a097fb37d23abf39523d1d41c85f languageName: node linkType: hard "globalthis@npm:^1.0.3": - version: 1.0.3 - resolution: "globalthis@npm:1.0.3" + version: 1.0.4 + resolution: "globalthis@npm:1.0.4" dependencies: - define-properties: "npm:^1.1.3" - checksum: 10/45ae2f3b40a186600d0368f2a880ae257e8278b4c7704f0417d6024105ad7f7a393661c5c2fa1334669cd485ea44bc883a08fdd4516df2428aec40c99f52aa89 + define-properties: "npm:^1.2.1" + gopd: "npm:^1.0.1" + checksum: 10/1f1fd078fb2f7296306ef9dd51019491044ccf17a59ed49d375b576ca108ff37e47f3d29aead7add40763574a992f16a5367dd1e2173b8634ef18556ab719ac4 languageName: node linkType: hard @@ -5548,6 +5334,17 @@ __metadata: languageName: node linkType: hard +"happy-dom@npm:^13.7.3": + version: 13.10.1 + resolution: "happy-dom@npm:13.10.1" + dependencies: + entities: "npm:^4.5.0" + webidl-conversions: "npm:^7.0.0" + whatwg-mimetype: "npm:^3.0.0" + checksum: 10/cb94fae0f93abbef85c98b883381ccde9a59fe158341fdea5b0fb6e04aee19026f9717cd019dab59197564292670de0f38be9678f9532559535ff71c13e2e03e + languageName: node + linkType: hard + "happy-dom@npm:^15.7.4": version: 15.7.4 resolution: "happy-dom@npm:15.7.4" @@ -5580,7 +5377,7 @@ __metadata: languageName: node linkType: hard -"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.1, has-property-descriptors@npm:^1.0.2": +"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": version: 1.0.2 resolution: "has-property-descriptors@npm:1.0.2" dependencies: @@ -5603,7 +5400,7 @@ __metadata: languageName: node linkType: hard -"has-tostringtag@npm:^1.0.0, has-tostringtag@npm:^1.0.1, has-tostringtag@npm:^1.0.2": +"has-tostringtag@npm:^1.0.0, has-tostringtag@npm:^1.0.2": version: 1.0.2 resolution: "has-tostringtag@npm:1.0.2" dependencies: @@ -5629,12 +5426,12 @@ __metadata: languageName: node linkType: hard -"hasown@npm:^2.0.0, hasown@npm:^2.0.1": - version: 2.0.1 - resolution: "hasown@npm:2.0.1" +"hasown@npm:^2.0.0, hasown@npm:^2.0.1, hasown@npm:^2.0.2": + version: 2.0.2 + resolution: "hasown@npm:2.0.2" dependencies: function-bind: "npm:^1.1.2" - checksum: 10/b7f9107387ee68abed88e965c2b99e868b5e0e9d289db1ddd080706ffafb69533b4f538b0e6362585bae8d6cbd080249f65e79702f74c225990f66d6106be3f6 + checksum: 10/7898a9c1788b2862cf0f9c345a6bec77ba4a0c0983c7f19d610c382343d4f98fa260686b225dfb1f88393a66679d2ec58ee310c1d6868c081eda7918f32cc70a languageName: node linkType: hard @@ -5739,12 +5536,12 @@ __metadata: linkType: hard "https-proxy-agent@npm:^7.0.1": - version: 7.0.4 - resolution: "https-proxy-agent@npm:7.0.4" + version: 7.0.5 + resolution: "https-proxy-agent@npm:7.0.5" dependencies: agent-base: "npm:^7.0.2" debug: "npm:4" - checksum: 10/405fe582bba461bfe5c7e2f8d752b384036854488b828ae6df6a587c654299cbb2c50df38c4b6ab303502c3c5e029a793fbaac965d1e86ee0be03faceb554d63 + checksum: 10/6679d46159ab3f9a5509ee80c3a3fc83fba3a920a5e18d32176c3327852c3c00ad640c0c4210a8fd70ea3c4a6d3a1b375bf01942516e7df80e2646bdc77658ab languageName: node linkType: hard @@ -5782,7 +5579,7 @@ __metadata: languageName: node linkType: hard -"ieee754@npm:^1.1.8, ieee754@npm:^1.2.1": +"ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" checksum: 10/d9f2557a59036f16c282aaeb107832dc957a93d73397d89bbad4eb1130560560eb695060145e8e6b3b498b15ab95510226649a0b8f52ae06583575419fe10fc4 @@ -5790,18 +5587,18 @@ __metadata: linkType: hard "ignore-walk@npm:^6.0.0": - version: 6.0.4 - resolution: "ignore-walk@npm:6.0.4" + version: 6.0.5 + resolution: "ignore-walk@npm:6.0.5" dependencies: minimatch: "npm:^9.0.0" - checksum: 10/a56c3f929bb0890ffb6e87dfaca7d5ce97f9e179fd68d49711edea55760aaee367cea3845d7620689b706249053c4b1805e21158f6751c7333f9b2ffb3668272 + checksum: 10/08757abff4dabca4f9f005f9a6cb6684e0c460a1e08c50319460ac13002de0ba8bbde6ad1f4477fefb264135d6253d1268339c18292f82485fcce576af0539d9 languageName: node linkType: hard "ignore@npm:^5.1.1, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1": - version: 5.3.1 - resolution: "ignore@npm:5.3.1" - checksum: 10/0a884c2fbc8c316f0b9f92beaf84464253b73230a4d4d286697be45fca081199191ca33e1c2e82d9e5f851f5e9a48a78e25a35c951e7eb41e59f150db3530065 + version: 5.3.2 + resolution: "ignore@npm:5.3.2" + checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 languageName: node linkType: hard @@ -5815,18 +5612,6 @@ __metadata: languageName: node linkType: hard -"import-in-the-middle@npm:^1.7.3": - version: 1.7.3 - resolution: "import-in-the-middle@npm:1.7.3" - dependencies: - acorn: "npm:^8.8.2" - acorn-import-assertions: "npm:^1.9.0" - cjs-module-lexer: "npm:^1.2.2" - module-details-from-path: "npm:^1.0.3" - checksum: 10/7176dbb5c8b27bd9684a5ef0bf93c2929f98ee301a58ca75dd443a88f0cfe26fa1cfe80ca7326029cbc74701b61a3733623e019faa14acab1c1c393a89ac057b - languageName: node - linkType: hard - "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -5865,13 +5650,6 @@ __metadata: languageName: node linkType: hard -"int64-buffer@npm:^0.1.9": - version: 0.1.10 - resolution: "int64-buffer@npm:0.1.10" - checksum: 10/63c5a8fadb02fb2f7ceee8c5c491b4be8a82d9c2dc89c79d5bf48a0aa4b9015a5077b084be8fe7078e3bb3a335783db72d0f51b9c82f1fe6a79b4d7d051a055d - languageName: node - linkType: hard - "internal-slot@npm:^1.0.7": version: 1.0.7 resolution: "internal-slot@npm:1.0.7" @@ -5893,13 +5671,6 @@ __metadata: languageName: node linkType: hard -"ipaddr.js@npm:^2.1.0": - version: 2.1.0 - resolution: "ipaddr.js@npm:2.1.0" - checksum: 10/42c16d95cf451399707c2c46e605b88db1ea2b1477b25774b5a7ee96852b0bb1efdc01adbff01fedbe702ff246e1aca5c5e915a6f5a1f1485233a5f7c2eb73c2 - languageName: node - linkType: hard - "is-array-buffer@npm:^3.0.4": version: 3.0.4 resolution: "is-array-buffer@npm:3.0.4" @@ -5929,6 +5700,15 @@ __metadata: languageName: node linkType: hard +"is-builtin-module@npm:^3.2.1": + version: 3.2.1 + resolution: "is-builtin-module@npm:3.2.1" + dependencies: + builtin-modules: "npm:^3.3.0" + checksum: 10/e8f0ffc19a98240bda9c7ada84d846486365af88d14616e737d280d378695c8c448a621dcafc8332dbf0fcd0a17b0763b845400709963fa9151ddffece90ae88 + languageName: node + linkType: hard + "is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": version: 1.2.7 resolution: "is-callable@npm:1.2.7" @@ -5936,12 +5716,21 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.13.1, is-core-module@npm:^2.8.1": - version: 2.13.1 - resolution: "is-core-module@npm:2.13.1" +"is-core-module@npm:^2.12.1, is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.8.1": + version: 2.15.1 + resolution: "is-core-module@npm:2.15.1" dependencies: - hasown: "npm:^2.0.0" - checksum: 10/d53bd0cc24b0a0351fb4b206ee3908f71b9bbf1c47e9c9e14e5f06d292af1663704d2abd7e67700d6487b2b7864e0d0f6f10a1edf1892864bdffcb197d1845a2 + hasown: "npm:^2.0.2" + checksum: 10/77316d5891d5743854bcef2cd2f24c5458fb69fbc9705c12ca17d54a2017a67d0693bbf1ba8c77af376c0eef6bf6d1b27a4ab08e4db4e69914c3789bdf2ceec5 + languageName: node + linkType: hard + +"is-data-view@npm:^1.0.1": + version: 1.0.1 + resolution: "is-data-view@npm:1.0.1" + dependencies: + is-typed-array: "npm:^1.1.13" + checksum: 10/4ba4562ac2b2ec005fefe48269d6bd0152785458cd253c746154ffb8a8ab506a29d0cfb3b74af87513843776a88e4981ae25c89457bf640a33748eab1a7216b5 languageName: node linkType: hard @@ -5984,7 +5773,7 @@ __metadata: languageName: node linkType: hard -"is-negative-zero@npm:^2.0.2": +"is-negative-zero@npm:^2.0.3": version: 2.0.3 resolution: "is-negative-zero@npm:2.0.3" checksum: 10/8fe5cffd8d4fb2ec7b49d657e1691889778d037494c6f40f4d1a524cadd658b4b53ad7b6b73a59bcb4b143ae9a3d15829af864b2c0f9d65ac1e678c4c80f17e5 @@ -6031,7 +5820,7 @@ __metadata: languageName: node linkType: hard -"is-shared-array-buffer@npm:^1.0.2": +"is-shared-array-buffer@npm:^1.0.2, is-shared-array-buffer@npm:^1.0.3": version: 1.0.3 resolution: "is-shared-array-buffer@npm:1.0.3" dependencies: @@ -6099,13 +5888,6 @@ __metadata: languageName: node linkType: hard -"isarray@npm:^1.0.0": - version: 1.0.0 - resolution: "isarray@npm:1.0.0" - checksum: 10/f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab - languageName: node - linkType: hard - "isarray@npm:^2.0.5": version: 2.0.5 resolution: "isarray@npm:2.0.5" @@ -6136,19 +5918,12 @@ __metadata: languageName: node linkType: hard -"isows@npm:1.0.4": - version: 1.0.4 - resolution: "isows@npm:1.0.4" +"isows@npm:1.0.6": + version: 1.0.6 + resolution: "isows@npm:1.0.6" peerDependencies: ws: "*" - checksum: 10/a3ee62e3d6216abb3adeeb2a551fe2e7835eac87b05a6ecc3e7739259bf5f8e83290501f49e26137390c8093f207fc3378d4a7653aab76ad7bbab4b2dba9c5b9 - languageName: node - linkType: hard - -"istanbul-lib-coverage@npm:3.2.0": - version: 3.2.0 - resolution: "istanbul-lib-coverage@npm:3.2.0" - checksum: 10/31621b84ad29339242b63d454243f558a7958ee0b5177749bacf1f74be7d95d3fd93853738ef7eebcddfaf3eab014716e51392a8dbd5aa1bdc1b15c2ebc53c24 + checksum: 10/ab9e85b50bcc3d70aa5ec875aa2746c5daf9321cb376ed4e5434d3c2643c5d62b1f466d93a05cd2ad0ead5297224922748c31707cb4fbd68f5d05d0479dce99c languageName: node linkType: hard @@ -6191,19 +5966,6 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^2.3.5": - version: 2.3.6 - resolution: "jackspeak@npm:2.3.6" - dependencies: - "@isaacs/cliui": "npm:^8.0.2" - "@pkgjs/parseargs": "npm:^0.11.0" - dependenciesMeta: - "@pkgjs/parseargs": - optional: true - checksum: 10/6e6490d676af8c94a7b5b29b8fd5629f21346911ebe2e32931c2a54210134408171c24cee1a109df2ec19894ad04a429402a8438cbf5cc2794585d35428ace76 - languageName: node - linkType: hard - "jackspeak@npm:^3.1.2": version: 3.4.3 resolution: "jackspeak@npm:3.4.3" @@ -6217,12 +5979,12 @@ __metadata: languageName: node linkType: hard -"jest-docblock@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-docblock@npm:29.7.0" +"jackspeak@npm:^4.0.1": + version: 4.0.2 + resolution: "jackspeak@npm:4.0.2" dependencies: - detect-newline: "npm:^3.0.0" - checksum: 10/8d48818055bc96c9e4ec2e217a5a375623c0d0bfae8d22c26e011074940c202aa2534a3362294c81d981046885c05d304376afba9f2874143025981148f3e96d + "@isaacs/cliui": "npm:^8.0.2" + checksum: 10/d9722f0e55f6c322c57aedf094c405f4201b834204629817187953988075521cfddb23df83e2a7b845723ca7eb0555068c5ce1556732e9c275d32a531881efa8 languageName: node linkType: hard @@ -6277,12 +6039,12 @@ __metadata: languageName: node linkType: hard -"jsesc@npm:^2.5.1": - version: 2.5.2 - resolution: "jsesc@npm:2.5.2" +"jsesc@npm:^3.0.2": + version: 3.0.2 + resolution: "jsesc@npm:3.0.2" bin: jsesc: bin/jsesc - checksum: 10/d2096abdcdec56969764b40ffc91d4a23408aa2f351b4d1c13f736f25476643238c43fdbaf38a191c26b1b78fd856d965f5d4d0dde7b89459cd94025190cdf13 + checksum: 10/8e5a7de6b70a8bd71f9cb0b5a7ade6a73ae6ab55e697c74cc997cede97417a3a65ed86c36f7dd6125fe49766e8386c845023d9e213916ca92c9dfdd56e2babf3 languageName: node linkType: hard @@ -6294,9 +6056,9 @@ __metadata: linkType: hard "json-parse-even-better-errors@npm:^3.0.0": - version: 3.0.1 - resolution: "json-parse-even-better-errors@npm:3.0.1" - checksum: 10/bf74fa3f715e56699ccd68b80a7d20908de432a3fae2d5aa2ed530a148e9d9ccdf8e6983b93d9966a553aa70dcf003ce3a7ffec2c0ce74d2a6173e3691a426f0 + version: 3.0.2 + resolution: "json-parse-even-better-errors@npm:3.0.2" + checksum: 10/6f04ea6c9ccb783630a59297959247e921cc90b917b8351197ca7fd058fccc7079268fd9362be21ba876fc26aa5039369dd0a2280aae49aae425784794a94927 languageName: node linkType: hard @@ -6395,13 +6157,6 @@ __metadata: languageName: node linkType: hard -"koalas@npm:^1.0.2": - version: 1.0.2 - resolution: "koalas@npm:1.0.2" - checksum: 10/a252c98af00376e11ca4387a9cc2ec9772d3f0c821216dff31893e9bc11dfbafeacc5627227e53abc3af7751c5503b2bd895c0a0bbf0255e7fc0975b97499a8e - languageName: node - linkType: hard - "levn@npm:^0.4.1": version: 0.4.1 resolution: "levn@npm:0.4.1" @@ -6412,13 +6167,6 @@ __metadata: languageName: node linkType: hard -"limiter@npm:1.1.5": - version: 1.1.5 - resolution: "limiter@npm:1.1.5" - checksum: 10/fa96e9912cf33ec36387e41a09694ccac7aaa8b86e1121333c30a3dfdf6265c849c980abd5f1689021bbab9aadca9d6df58d8db6ce5b999c26dd8cefe94168a9 - languageName: node - linkType: hard - "linkify-it@npm:^5.0.0": version: 5.0.0 resolution: "linkify-it@npm:5.0.0" @@ -6469,13 +6217,6 @@ __metadata: languageName: node linkType: hard -"lodash.sortby@npm:^4.7.0": - version: 4.7.0 - resolution: "lodash.sortby@npm:4.7.0" - checksum: 10/38df19ae28608af2c50ac342fc1f414508309d53e1d58ed9adfb2c3cd17c3af290058c0a0478028d932c5404df3d53349d19fa364ef6bed6145a6bc21320399e - languageName: node - linkType: hard - "lodash.startcase@npm:^4.4.0": version: 4.4.0 resolution: "lodash.startcase@npm:4.4.0" @@ -6510,28 +6251,26 @@ __metadata: linkType: hard "loupe@npm:^3.1.0, loupe@npm:^3.1.1": - version: 3.1.1 - resolution: "loupe@npm:3.1.1" - dependencies: - get-func-name: "npm:^2.0.1" - checksum: 10/56d71d64c5af109aaf2b5343668ea5952eed468ed2ff837373810e417bf8331f14491c6e4d38e08ff84a29cb18906e06e58ba660c53bd00f2989e1873fa2f54c - languageName: node - linkType: hard - -"lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.2.0 - resolution: "lru-cache@npm:10.2.0" - checksum: 10/502ec42c3309c0eae1ce41afca471f831c278566d45a5273a0c51102dee31e0e250a62fa9029c3370988df33a14188a38e682c16143b794de78668de3643e302 + version: 3.1.2 + resolution: "loupe@npm:3.1.2" + checksum: 10/8f5734e53fb64cd914aa7d986e01b6d4c2e3c6c56dcbd5428d71c2703f0ab46b5ab9f9eeaaf2b485e8a1c43f865bdd16ec08ae1a661c8f55acdbd9f4d59c607a languageName: node linkType: hard -"lru-cache@npm:^10.2.0": +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" checksum: 10/e6e90267360476720fa8e83cc168aa2bf0311f3f2eea20a6ba78b90a885ae72071d9db132f40fda4129c803e7dcec3a6b6a6fbb44ca90b081630b810b5d6a41a languageName: node linkType: hard +"lru-cache@npm:^11.0.0": + version: 11.0.1 + resolution: "lru-cache@npm:11.0.1" + checksum: 10/26688a1b2a4d7fb97e9ea1ffb15348f1ab21b7110496814f5ce9190d50258fbba8c1444ae7232876deae1fc54adb230aa63dd1efc5bd47f240620ba8bf218041 + languageName: node + linkType: hard + "lru-cache@npm:^4.0.1": version: 4.1.5 resolution: "lru-cache@npm:4.1.5" @@ -6551,16 +6290,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: "npm:^4.0.0" - checksum: 10/fc1fe2ee205f7c8855fa0f34c1ab0bcf14b6229e35579ec1fd1079f31d6fc8ef8eb6fd17f2f4d99788d7e339f50e047555551ebd5e434dda503696e7c6591825 - languageName: node - linkType: hard - -"lru-cache@npm:^7.14.0, lru-cache@npm:^7.4.4, lru-cache@npm:^7.5.1, lru-cache@npm:^7.7.1": +"lru-cache@npm:^7.4.4, lru-cache@npm:^7.5.1, lru-cache@npm:^7.7.1": version: 7.18.3 resolution: "lru-cache@npm:7.18.3" checksum: 10/6029ca5aba3aacb554e919d7ef804fffd4adfc4c83db00fac8248c7c78811fb6d4b6f70f7fd9d55032b3823446546a007edaa66ad1f2377ae833bd983fac5d98 @@ -6574,16 +6304,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.10": - version: 0.30.10 - resolution: "magic-string@npm:0.30.10" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.4.15" - checksum: 10/9f8bf6363a14c98a9d9f32ef833b194702a5c98fb931b05ac511b76f0b06fd30ed92beda6ca3261d2d52d21e39e891ef1136fbd032023f6cbb02d0b7d5767201 - languageName: node - linkType: hard - -"magic-string@npm:^0.30.11": +"magic-string@npm:^0.30.10, magic-string@npm:^0.30.11": version: 0.30.11 resolution: "magic-string@npm:0.30.11" dependencies: @@ -6593,13 +6314,13 @@ __metadata: linkType: hard "magicast@npm:^0.3.4": - version: 0.3.4 - resolution: "magicast@npm:0.3.4" + version: 0.3.5 + resolution: "magicast@npm:0.3.5" dependencies: - "@babel/parser": "npm:^7.24.4" - "@babel/types": "npm:^7.24.0" + "@babel/parser": "npm:^7.25.4" + "@babel/types": "npm:^7.25.4" source-map-js: "npm:^1.2.0" - checksum: 10/704f86639b01c8e063155408cb181d89d4444db3a4a473fb501107f30f19d9c39a159dd315ef9e54a22291c090170044efd9b49a9b3ab8d6deb948a9c99d90b3 + checksum: 10/3a2dba6b0bdde957797361d09c7931ebdc1b30231705360eeb40ed458d28e1c3112841c3ed4e1b87ceb28f741e333c7673cd961193aa9fdb4f4946b202e6205a languageName: node linkType: hard @@ -6660,8 +6381,8 @@ __metadata: linkType: hard "make-fetch-happen@npm:^13.0.0": - version: 13.0.0 - resolution: "make-fetch-happen@npm:13.0.0" + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" dependencies: "@npmcli/agent": "npm:^2.0.0" cacache: "npm:^18.0.0" @@ -6672,9 +6393,10 @@ __metadata: minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" negotiator: "npm:^0.6.3" + proc-log: "npm:^4.2.0" promise-retry: "npm:^2.0.1" ssri: "npm:^10.0.0" - checksum: 10/ded5a91a02b76381b06a4ec4d5c1d23ebbde15d402b3c3e4533b371dac7e2f7ca071ae71ae6dae72aa261182557b7b1b3fd3a705b39252dc17f74fa509d3e76f + checksum: 10/11bae5ad6ac59b654dbd854f30782f9de052186c429dfce308eda42374528185a100ee40ac9ffdc36a2b6c821ecaba43913e4730a12f06f15e895ea9cb23fa59 languageName: node linkType: hard @@ -6725,13 +6447,6 @@ __metadata: languageName: node linkType: hard -"methods@npm:^1.1.2": - version: 1.1.2 - resolution: "methods@npm:1.1.2" - checksum: 10/a385dd974faa34b5dd021b2bbf78c722881bf6f003bfe6d391d7da3ea1ed625d1ff10ddd13c57531f628b3e785be38d3eed10ad03cebd90b76932413df9a1820 - languageName: node - linkType: hard - "micro-ftch@npm:^0.3.1": version: 0.3.1 resolution: "micro-ftch@npm:0.3.1" @@ -6782,12 +6497,12 @@ __metadata: linkType: hard "micromatch@npm:^4.0.2, micromatch@npm:^4.0.4": - version: 4.0.5 - resolution: "micromatch@npm:4.0.5" + version: 4.0.8 + resolution: "micromatch@npm:4.0.8" dependencies: - braces: "npm:^3.0.2" + braces: "npm:^3.0.3" picomatch: "npm:^2.3.1" - checksum: 10/a749888789fc15cac0e03273844dbd749f9f8e8d64e70c564bcf06a033129554c789bb9e30d7566d7ff6596611a08e58ac12cf2a05f6e3c9c47c50c4c7e12fa2 + checksum: 10/6bf2a01672e7965eb9941d1f02044fad2bd12486b5553dc1116ff24c09a8723157601dc992e74c911d896175918448762df3b3fd0a6b61037dd1a9766ddfbf58 languageName: node linkType: hard @@ -6812,43 +6527,43 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" +"minimatch@npm:9.0.3": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10/e0b25b04cd4ec6732830344e5739b13f8690f8a012d73445a4a19fbc623f5dd481ef7a5827fde25954cd6026fede7574cc54dc4643c99d6c6b653d6203f94634 + brace-expansion: "npm:^2.0.1" + checksum: 10/c81b47d28153e77521877649f4bab48348d10938df9e8147a58111fe00ef89559a2938de9f6632910c4f7bf7bb5cd81191a546167e58d357f0cfb1e18cecc1c5 languageName: node linkType: hard -"minimatch@npm:^5.0.1": - version: 5.1.6 - resolution: "minimatch@npm:5.1.6" +"minimatch@npm:^10.0.0": + version: 10.0.1 + resolution: "minimatch@npm:10.0.1" dependencies: brace-expansion: "npm:^2.0.1" - checksum: 10/126b36485b821daf96d33b5c821dac600cc1ab36c87e7a532594f9b1652b1fa89a1eebcaad4dff17c764dce1a7ac1531327f190fed5f97d8f6e5f889c116c429 + checksum: 10/082e7ccbc090d5f8c4e4e029255d5a1d1e3af37bda837da2b8b0085b1503a1210c91ac90d9ebfe741d8a5f286ece820a1abb4f61dc1f82ce602a055d461d93f3 languageName: node linkType: hard -"minimatch@npm:^9.0.0, minimatch@npm:^9.0.1": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" +"minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/c81b47d28153e77521877649f4bab48348d10938df9e8147a58111fe00ef89559a2938de9f6632910c4f7bf7bb5cd81191a546167e58d357f0cfb1e18cecc1c5 + brace-expansion: "npm:^1.1.7" + checksum: 10/e0b25b04cd4ec6732830344e5739b13f8690f8a012d73445a4a19fbc623f5dd481ef7a5827fde25954cd6026fede7574cc54dc4643c99d6c6b653d6203f94634 languageName: node linkType: hard -"minimatch@npm:^9.0.4": - version: 9.0.4 - resolution: "minimatch@npm:9.0.4" +"minimatch@npm:^5.0.1": + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" dependencies: brace-expansion: "npm:^2.0.1" - checksum: 10/4cdc18d112b164084513e890d6323370db14c22249d536ad1854539577a895e690a27513dc346392f61a4a50afbbd8abc88f3f25558bfbbbb862cd56508b20f5 + checksum: 10/126b36485b821daf96d33b5c821dac600cc1ab36c87e7a532594f9b1652b1fa89a1eebcaad4dff17c764dce1a7ac1531327f190fed5f97d8f6e5f889c116c429 languageName: node linkType: hard -"minimatch@npm:^9.0.5": +"minimatch@npm:^9.0.0, minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": version: 9.0.5 resolution: "minimatch@npm:9.0.5" dependencies: @@ -6898,8 +6613,8 @@ __metadata: linkType: hard "minipass-fetch@npm:^3.0.0": - version: 3.0.4 - resolution: "minipass-fetch@npm:3.0.4" + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" dependencies: encoding: "npm:^0.1.13" minipass: "npm:^7.0.3" @@ -6908,7 +6623,7 @@ __metadata: dependenciesMeta: encoding: optional: true - checksum: 10/3edf72b900e30598567eafe96c30374432a8709e61bb06b87198fa3192d466777e2ec21c52985a0999044fa6567bd6f04651585983a1cbb27e2c1770a07ed2a2 + checksum: 10/c669948bec1373313aaa8f104b962a3ced9f45c49b26366a4b0ae27ccdfa9c5740d72c8a84d3f8623d7a61c5fc7afdfda44789008c078f61a62441142efc4a97 languageName: node linkType: hard @@ -6922,12 +6637,12 @@ __metadata: linkType: hard "minipass-json-stream@npm:^1.0.1": - version: 1.0.1 - resolution: "minipass-json-stream@npm:1.0.1" + version: 1.0.2 + resolution: "minipass-json-stream@npm:1.0.2" dependencies: jsonparse: "npm:^1.3.1" minipass: "npm:^3.0.0" - checksum: 10/3c65482c630b063c3fa86c853f324a50d9484f2eb6c3034f9c86c0b22f44181668848088f2c869cc764f8a9b8adc8f617f93762cd9d11521f563b8a71c5b815d + checksum: 10/e9df9d28bcbd87f8c134facd8c51a528ec4614a47d50a8f122ac6b666b45f4d35efa5109ccfc180c8911672bf1e146e6b20b4a459b0ea906a5ce887617b51942 languageName: node linkType: hard @@ -6965,14 +6680,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": - version: 7.0.4 - resolution: "minipass@npm:7.0.4" - checksum: 10/e864bd02ceb5e0707696d58f7ce3a0b89233f0d686ef0d447a66db705c0846a8dc6f34865cd85256c1472ff623665f616b90b8ff58058b2ad996c5de747d2d18 - languageName: node - linkType: hard - -"minipass@npm:^7.1.2": +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.1.2": version: 7.1.2 resolution: "minipass@npm:7.1.2" checksum: 10/c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950 @@ -6998,13 +6706,6 @@ __metadata: languageName: node linkType: hard -"module-details-from-path@npm:^1.0.3": - version: 1.0.3 - resolution: "module-details-from-path@npm:1.0.3" - checksum: 10/f93226e9154fc8cb91f4609b639167ec7ad9155b30be4924d9717656648a3ae5f181d4e2338434d4c5afc7b5f4c10dd3b64109e5b89a4be70b20a25ba3573d54 - languageName: node - linkType: hard - "mri@npm:^1.2.0": version: 1.2.0 resolution: "mri@npm:1.2.0" @@ -7012,13 +6713,6 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.2": - version: 2.1.2 - resolution: "ms@npm:2.1.2" - checksum: 10/673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f - languageName: node - linkType: hard - "ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -7026,20 +6720,6 @@ __metadata: languageName: node linkType: hard -"msgpack-lite@npm:^0.1.26": - version: 0.1.26 - resolution: "msgpack-lite@npm:0.1.26" - dependencies: - event-lite: "npm:^0.1.1" - ieee754: "npm:^1.1.8" - int64-buffer: "npm:^0.1.9" - isarray: "npm:^1.0.0" - bin: - msgpack: ./bin/msgpack - checksum: 10/3e283047204254a0adeb0497ee557a05cc7cfc346a60573a1c67d24425385b739b875231f6944d98bd0d6e72af854d40142957602abcc4ec38e91fc2367c8370 - languageName: node - linkType: hard - "nanoid@npm:^3.3.7": version: 3.3.7 resolution: "nanoid@npm:3.3.7" @@ -7063,44 +6743,6 @@ __metadata: languageName: node linkType: hard -"node-abort-controller@npm:^3.1.1": - version: 3.1.1 - resolution: "node-abort-controller@npm:3.1.1" - checksum: 10/0a2cdb7ec0aeaf3cb31e1ca0e192f5add48f1c5c9c9ed822129f9dddbd9432f69b7425982f94ce803c56a2104884530aa67cd57696e5774b2e5b8ec2f58de042 - languageName: node - linkType: hard - -"node-addon-api@npm:^6.1.0": - version: 6.1.0 - resolution: "node-addon-api@npm:6.1.0" - dependencies: - node-gyp: "npm:latest" - checksum: 10/8eea1d4d965930a177a0508695beb0d89b4c1d80bf330646a035357a1e8fc31e0d09686e2374996e96e757b947a7ece319f98ede3146683f162597c0bcb4df90 - languageName: node - linkType: hard - -"node-gyp-build@npm:<4.0, node-gyp-build@npm:^3.9.0": - version: 3.9.0 - resolution: "node-gyp-build@npm:3.9.0" - bin: - node-gyp-build: ./bin.js - node-gyp-build-optional: ./optional.js - node-gyp-build-test: ./build-test.js - checksum: 10/c94f1fc077852ff9b830f8f8f6bcb350441bf5ec5187cad46981eb5f4faad08a17c5fe9fa3450f346361d93c0984fde0e87c9615e52bc69ad4fb4ebf5e26259c - languageName: node - linkType: hard - -"node-gyp-build@npm:^4.5.0": - version: 4.8.0 - resolution: "node-gyp-build@npm:4.8.0" - bin: - node-gyp-build: bin.js - node-gyp-build-optional: optional.js - node-gyp-build-test: build-test.js - checksum: 10/80f410ab412df38e84171d3634a5716b6c6f14ecfa4eb971424d289381fb76f8bcbe1b666419ceb2c81060e558fd7c6d70cc0f60832bcca6a1559098925d9657 - languageName: node - linkType: hard - "node-gyp@npm:^9.0.0": version: 9.4.1 resolution: "node-gyp@npm:9.4.1" @@ -7123,8 +6765,8 @@ __metadata: linkType: hard "node-gyp@npm:latest": - version: 10.0.1 - resolution: "node-gyp@npm:10.0.1" + version: 10.2.0 + resolution: "node-gyp@npm:10.2.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" @@ -7132,20 +6774,20 @@ __metadata: graceful-fs: "npm:^4.2.6" make-fetch-happen: "npm:^13.0.0" nopt: "npm:^7.0.0" - proc-log: "npm:^3.0.0" + proc-log: "npm:^4.1.0" semver: "npm:^7.3.5" - tar: "npm:^6.1.2" + tar: "npm:^6.2.1" which: "npm:^4.0.0" bin: node-gyp: bin/node-gyp.js - checksum: 10/578cf0c821f258ce4b6ebce4461eca4c991a4df2dee163c0624f2fe09c7d6d37240be4942285a0048d307230248ee0b18382d6623b9a0136ce9533486deddfa8 + checksum: 10/41773093b1275751dec942b985982fd4e7a69b88cae719b868babcef3880ee6168aaec8dcaa8cd0b9fa7c84873e36cc549c6cac6a124ee65ba4ce1f1cc108cfe languageName: node linkType: hard -"node-releases@npm:^2.0.14": - version: 2.0.14 - resolution: "node-releases@npm:2.0.14" - checksum: 10/0f7607ec7db5ef1dc616899a5f24ae90c869b6a54c2d4f36ff6d84a282ab9343c7ff3ca3670fe4669171bb1e8a9b3e286e1ef1c131f09a83d70554f855d54f24 +"node-releases@npm:^2.0.18": + version: 2.0.18 + resolution: "node-releases@npm:2.0.18" + checksum: 10/241e5fa9556f1c12bafb83c6c3e94f8cf3d8f2f8f904906ecef6e10bcaa1d59aa61212d4651bec70052015fc54bd3fdcdbe7fc0f638a17e6685aa586c076ec4e languageName: node linkType: hard @@ -7161,13 +6803,13 @@ __metadata: linkType: hard "nopt@npm:^7.0.0": - version: 7.2.0 - resolution: "nopt@npm:7.2.0" + version: 7.2.1 + resolution: "nopt@npm:7.2.1" dependencies: abbrev: "npm:^2.0.0" bin: nopt: bin/nopt.js - checksum: 10/1e7489f17cbda452c8acaf596a8defb4ae477d2a9953b76eb96f4ec3f62c6b421cd5174eaa742f88279871fde9586d8a1d38fb3f53fa0c405585453be31dff4c + checksum: 10/95a1f6dec8a81cd18cdc2fed93e6f0b4e02cf6bdb4501c848752c6e34f9883d9942f036a5e3b21a699047d8a448562d891e67492df68ec9c373e6198133337ae languageName: node linkType: hard @@ -7184,11 +6826,11 @@ __metadata: linkType: hard "npm-bundled@npm:^3.0.0": - version: 3.0.0 - resolution: "npm-bundled@npm:3.0.0" + version: 3.0.1 + resolution: "npm-bundled@npm:3.0.1" dependencies: npm-normalize-package-bin: "npm:^3.0.0" - checksum: 10/704fce20114d36d665c20edc56d3f9f7778c52ca1cd48731ec31f65af9e65805f9308ca7ed9e5a6bd9fe22327a63aa5d83a8c5aaee0c715e5047de1fa659e8bf + checksum: 10/113c9a35526d9a563694e9bda401dbda592f664fa146d365028bef1e3bfdc2a7b60ac9315a727529ef7e8e8d80b8d9e217742ccc2808e0db99c2204a3e33a465 languageName: node linkType: hard @@ -7269,9 +6911,9 @@ __metadata: linkType: hard "object-inspect@npm:^1.13.1": - version: 1.13.1 - resolution: "object-inspect@npm:1.13.1" - checksum: 10/92f4989ed83422d56431bc39656d4c780348eb15d397ce352ade6b7fec08f973b53744bd41b94af021901e61acaf78fcc19e65bf464ecc0df958586a672700f0 + version: 1.13.2 + resolution: "object-inspect@npm:1.13.2" + checksum: 10/7ef65583b6397570a17c56f0c1841e0920e83900f2c94638927abb7b81ac08a19c7aae135bd9dcca96208cac0c7332b4650fb927f027b0cf92d71df2990d0561 languageName: node linkType: hard @@ -7282,7 +6924,7 @@ __metadata: languageName: node linkType: hard -"object.assign@npm:^4.1.5": +"object.assign@npm:^4.1.2, object.assign@npm:^4.1.5": version: 4.1.5 resolution: "object.assign@npm:4.1.5" dependencies: @@ -7294,38 +6936,48 @@ __metadata: languageName: node linkType: hard -"object.fromentries@npm:^2.0.7": - version: 2.0.7 - resolution: "object.fromentries@npm:2.0.7" +"object.entries@npm:^1.1.5": + version: 1.1.8 + resolution: "object.entries@npm:1.1.8" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - checksum: 10/1bfbe42a51f8d84e417d193fae78e4b8eebb134514cdd44406480f8e8a0e075071e0717635d8e3eccd50fec08c1d555fe505c38804cbac0808397187653edd59 + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10/2301918fbd1ee697cf6ff7cd94f060c738c0a7d92b22fd24c7c250e9b593642c9707ad2c44d339303c1439c5967d8964251cdfc855f7f6ec55db2dd79e8dc2a7 languageName: node linkType: hard -"object.groupby@npm:^1.0.1": - version: 1.0.2 - resolution: "object.groupby@npm:1.0.2" +"object.fromentries@npm:^2.0.8": + version: 2.0.8 + resolution: "object.fromentries@npm:2.0.8" dependencies: - array.prototype.filter: "npm:^1.0.3" - call-bind: "npm:^1.0.5" + call-bind: "npm:^1.0.7" define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.22.3" - es-errors: "npm:^1.0.0" - checksum: 10/07c1bea1772c45f7967a63358a683ef7b0bd99cabe0563e6fee3e8acc061cc5984d2f01a46472ebf10b2cb439298c46776b2134550dce457fd7240baaaa4f592 + es-abstract: "npm:^1.23.2" + es-object-atoms: "npm:^1.0.0" + checksum: 10/5b2e80f7af1778b885e3d06aeb335dcc86965e39464671adb7167ab06ac3b0f5dd2e637a90d8ebd7426d69c6f135a4753ba3dd7d0fe2a7030cf718dcb910fd92 languageName: node linkType: hard -"object.values@npm:^1.1.7": - version: 1.1.7 - resolution: "object.values@npm:1.1.7" +"object.groupby@npm:^1.0.3": + version: 1.0.3 + resolution: "object.groupby@npm:1.0.3" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - checksum: 10/20ab42c0bbf984405c80e060114b18cf5d629a40a132c7eac4fb79c5d06deb97496311c19297dcf9c61f45c2539cd4c7f7c5d6230e51db360ff297bbc9910162 + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.2" + checksum: 10/44cb86dd2c660434be65f7585c54b62f0425b0c96b5c948d2756be253ef06737da7e68d7106e35506ce4a44d16aa85a413d11c5034eb7ce5579ec28752eb42d0 + languageName: node + linkType: hard + +"object.values@npm:^1.2.0": + version: 1.2.0 + resolution: "object.values@npm:1.2.0" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10/db2e498019c354428c5dd30d02980d920ac365b155fce4dcf63eb9433f98ccf0f72624309e182ce7cc227c95e45d474e1d483418e60de2293dd23fa3ebe34903 languageName: node linkType: hard @@ -7356,24 +7008,17 @@ __metadata: languageName: node linkType: hard -"opentracing@npm:>=0.12.1": - version: 0.14.7 - resolution: "opentracing@npm:0.14.7" - checksum: 10/0159a5a2a40bef0722cd6e0607808355e0e22909fe54f3441fbce3c78183fed0a12f834ca43eff0c93abddb8b1ab89548162b05cd9b340678dfa3b5cb9eb04b8 - languageName: node - linkType: hard - "optionator@npm:^0.9.3": - version: 0.9.3 - resolution: "optionator@npm:0.9.3" + version: 0.9.4 + resolution: "optionator@npm:0.9.4" dependencies: - "@aashutoshrathi/word-wrap": "npm:^1.2.3" deep-is: "npm:^0.1.3" fast-levenshtein: "npm:^2.0.6" levn: "npm:^0.4.1" prelude-ls: "npm:^1.2.1" type-check: "npm:^0.4.0" - checksum: 10/fa28d3016395974f7fc087d6bbf0ac7f58ac3489f4f202a377e9c194969f329a7b88c75f8152b33fb08794a30dcd5c079db6bb465c28151357f113d80bbf67da + word-wrap: "npm:^1.2.5" + checksum: 10/a8398559c60aef88d7f353a4f98dcdff6090a4e70f874c827302bf1213d9106a1c4d5fcb68dacb1feb3c30a04c4102f41047aa55d4c576b863d6fc876e001af6 languageName: node linkType: hard @@ -7409,7 +7054,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": +"p-limit@npm:^3.0.2": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -7478,16 +7123,16 @@ __metadata: linkType: hard "package-json-from-dist@npm:^1.0.0": - version: 1.0.0 - resolution: "package-json-from-dist@npm:1.0.0" - checksum: 10/ac706ec856a5a03f5261e4e48fa974f24feb044d51f84f8332e2af0af04fbdbdd5bbbfb9cbbe354190409bc8307c83a9e38c6672c3c8855f709afb0006a009ea + version: 1.0.1 + resolution: "package-json-from-dist@npm:1.0.1" + checksum: 10/58ee9538f2f762988433da00e26acc788036914d57c71c246bf0be1b60cdbd77dd60b6a3e1a30465f0b248aeb80079e0b34cb6050b1dfa18c06953bb1cbc7602 languageName: node linkType: hard "package-manager-detector@npm:^0.2.0": - version: 0.2.1 - resolution: "package-manager-detector@npm:0.2.1" - checksum: 10/5aca296815768daa5cdef6b6545a5965576783cf1d66445cc3f0419eb4fbc3e2d1c49cd9dcfedd0adb7089878a223cef573f5dab4c9599820845c0feefdee248 + version: 0.2.2 + resolution: "package-manager-detector@npm:0.2.2" + checksum: 10/2dc2914aeff0729c37c1cf9762f65c0a6f09d6c64f666cc187e34de95bca54f16b4ca2e3c1e9ced87ea0637cfdb3c98261a838de04d9f1b1b26b6ae72bd55b80 languageName: node linkType: hard @@ -7529,12 +7174,12 @@ __metadata: linkType: hard "parse-imports@npm:^2.1.1": - version: 2.1.1 - resolution: "parse-imports@npm:2.1.1" + version: 2.2.1 + resolution: "parse-imports@npm:2.2.1" dependencies: es-module-lexer: "npm:^1.5.3" slashes: "npm:^3.0.12" - checksum: 10/466cba090fe8b77aa2edc2a7ebcde699a296f34db5384d89f2c78daa5e7a87979adbad8a478634a85f5546ec8b759b597cf1057d825b471db70ce5c1b0c8bbec + checksum: 10/db1d98077587d23bfa1f136abae158ea08e1e588d0260dfc0769092be86b842c798ae47466742b1d9bc106d3430cebbd9730fc34872a2c0e72b9ff720986e82e languageName: node linkType: hard @@ -7573,16 +7218,6 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.10.1": - version: 1.10.1 - resolution: "path-scurry@npm:1.10.1" - dependencies: - lru-cache: "npm:^9.1.1 || ^10.0.0" - minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - checksum: 10/eebfb8304fef1d4f7e1486df987e4fd77413de4fce16508dea69fcf8eb318c09a6b15a7a2f4c22877cec1cb7ecbd3071d18ca9de79eeece0df874a00f1f0bdc8 - languageName: node - linkType: hard - "path-scurry@npm:^1.11.1": version: 1.11.1 resolution: "path-scurry@npm:1.11.1" @@ -7593,10 +7228,13 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^0.1.2": - version: 0.1.7 - resolution: "path-to-regexp@npm:0.1.7" - checksum: 10/701c99e1f08e3400bea4d701cf6f03517474bb1b608da71c78b1eb261415b645c5670dfae49808c89e12cea2dccd113b069f040a80de012da0400191c6dbd1c8 +"path-scurry@npm:^2.0.0": + version: 2.0.0 + resolution: "path-scurry@npm:2.0.0" + dependencies: + lru-cache: "npm:^11.0.0" + minipass: "npm:^7.1.2" + checksum: 10/285ae0c2d6c34ae91dc1d5378ede21981c9a2f6de1ea9ca5a88b5a270ce9763b83dbadc7a324d512211d8d36b0c540427d3d0817030849d97a60fa840a2c59ec languageName: node linkType: hard @@ -7621,14 +7259,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0": - version: 1.0.0 - resolution: "picocolors@npm:1.0.0" - checksum: 10/a2e8092dd86c8396bdba9f2b5481032848525b3dc295ce9b57896f931e63fc16f79805144321f72976383fc249584672a75cc18d6777c6b757603f372f745981 - languageName: node - linkType: hard - -"picocolors@npm:^1.1.0": +"picocolors@npm:^1.0.0, picocolors@npm:^1.1.0": version: 1.1.0 resolution: "picocolors@npm:1.1.0" checksum: 10/a2ad60d94d185c30f2a140b19c512547713fb89b920d32cc6cf658fa786d63a37ba7b8451872c3d9fc34883971fb6e5878e07a20b60506e0bb2554dce9169ccb @@ -7657,9 +7288,9 @@ __metadata: linkType: hard "pony-cause@npm:^2.1.10": - version: 2.1.10 - resolution: "pony-cause@npm:2.1.10" - checksum: 10/906563565030996d0c40ba79a584e2f298391931acc59c98510f9fd583d72cd9e9c58b0fb5a25bbae19daf16840f94cb9c1ee72c7ed5ef249ecba147cee40495 + version: 2.1.11 + resolution: "pony-cause@npm:2.1.11" + checksum: 10/ed7d0bb6e3e69f753080bf736b71f40e6ae4c13ec0c8c473ff73345345c088819966fdd68a62ad7482d464bf41176cf9421f5f63715d1a4532005eedc099db55 languageName: node linkType: hard @@ -7670,17 +7301,6 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.35": - version: 8.4.35 - resolution: "postcss@npm:8.4.35" - dependencies: - nanoid: "npm:^3.3.7" - picocolors: "npm:^1.0.0" - source-map-js: "npm:^1.0.2" - checksum: 10/93a7ce50cd6188f5f486a9ca98950ad27c19dfed996c45c414fa242944497e4d084a8760d3537f078630226f2bd3c6ab84b813b488740f4432e7c7039cd73a20 - languageName: node - linkType: hard - "postcss@npm:^8.4.43": version: 8.4.47 resolution: "postcss@npm:8.4.47" @@ -7692,13 +7312,6 @@ __metadata: languageName: node linkType: hard -"pprof-format@npm:^2.0.7": - version: 2.0.7 - resolution: "pprof-format@npm:2.0.7" - checksum: 10/ea3ad85a9255c2d65e687159346788236b39d5727d4d80b85dc7dda545eecd1ca91a466e4aa240505781f14f468cb367fd268876b76fedc2bed2ac1d85b5a971 - languageName: node - linkType: hard - "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -7715,18 +7328,18 @@ __metadata: languageName: node linkType: hard -"prettier-plugin-packagejson@npm:^2.5.2": - version: 2.5.2 - resolution: "prettier-plugin-packagejson@npm:2.5.2" +"prettier-plugin-packagejson@npm:^2.5.3": + version: 2.5.3 + resolution: "prettier-plugin-packagejson@npm:2.5.3" dependencies: sort-package-json: "npm:2.10.1" - synckit: "npm:0.9.1" + synckit: "npm:0.9.2" peerDependencies: prettier: ">= 1.16.0" peerDependenciesMeta: prettier: optional: true - checksum: 10/f280d69327a468cd104c72a81134258d3573e56d697a88a5c4498c8d02cecda9a27d9eb3f1d29cc726491782eb3f279c9d41ecf8364a197e20b239c5ccfd0269 + checksum: 10/0b3063cb3d1be8e908fa2dcbee8601134f4aa847d41cdc7d3204b09a737b0c2b3606b2c531dfdbe4fc7ade9d21975528b9a56200f241c3450c461b9f046be3d7 languageName: node linkType: hard @@ -7755,6 +7368,13 @@ __metadata: languageName: node linkType: hard +"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 10/4e1394491b717f6c1ade15c570ecd4c2b681698474d3ae2d303c1e4b6ab9455bd5a81566211e82890d5a5ae9859718cc6954d5150bb18b09b72ecb297beae90a + languageName: node + linkType: hard + "process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" @@ -7786,9 +7406,9 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:^7.0.0, protobufjs@npm:^7.2.5": - version: 7.2.6 - resolution: "protobufjs@npm:7.2.6" +"protobufjs@npm:^7.0.0": + version: 7.4.0 + resolution: "protobufjs@npm:7.4.0" dependencies: "@protobufjs/aspromise": "npm:^1.1.2" "@protobufjs/base64": "npm:^1.1.2" @@ -7802,7 +7422,7 @@ __metadata: "@protobufjs/utf8": "npm:^1.1.0" "@types/node": "npm:>=13.7.0" long: "npm:^5.0.0" - checksum: 10/81ab853d28c71998d056d6b34f83c4bc5be40cb0b416585f99ed618aed833d64b2cf89359bad7474d345302f2b5e236c4519165f8483d7ece7fd5b0d9ac13f8b + checksum: 10/408423506610f70858d7593632f4a6aa4f05796c90fd632be9b9252457c795acc71aa6d3b54bb7f48a890141728fee4ca3906723ccea6c202ad71f21b3879b8b languageName: node linkType: hard @@ -7916,14 +7536,14 @@ __metadata: linkType: hard "regexp.prototype.flags@npm:^1.5.2": - version: 1.5.2 - resolution: "regexp.prototype.flags@npm:1.5.2" + version: 1.5.3 + resolution: "regexp.prototype.flags@npm:1.5.3" dependencies: - call-bind: "npm:^1.0.6" + call-bind: "npm:^1.0.7" define-properties: "npm:^1.2.1" es-errors: "npm:^1.3.0" - set-function-name: "npm:^2.0.1" - checksum: 10/9fffc01da9c4e12670ff95bc5204364615fcc12d86fc30642765af908675678ebb0780883c874b2dbd184505fb52fa603d80073ecf69f461ce7f56b15d10be9c + set-function-name: "npm:^2.0.2" + checksum: 10/fe17bc4eebbc72945aaf9dd059eb7784a5ca453a67cc4b5b3e399ab08452c9a05befd92063e2c52e7b24d9238c60031656af32dd57c555d1ba6330dbf8c23b43 languageName: node linkType: hard @@ -7955,7 +7575,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.10.1, resolve@npm:^1.22.1, resolve@npm:^1.22.4": +"resolve@npm:^1.10.1, resolve@npm:^1.22.1, resolve@npm:^1.22.2, resolve@npm:^1.22.4": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -7968,7 +7588,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.10.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": +"resolve@patch:resolve@npm%3A^1.10.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.2#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -7998,13 +7618,6 @@ __metadata: languageName: node linkType: hard -"retry@npm:^0.13.1": - version: 0.13.1 - resolution: "retry@npm:0.13.1" - checksum: 10/6125ec2e06d6e47e9201539c887defba4e47f63471db304c59e4b82fc63c8e89ca06a77e9d34939a9a42a76f00774b2f46c0d4a4cbb3e287268bd018ed69426d - languageName: node - linkType: hard - "reusify@npm:^1.0.4": version: 1.0.4 resolution: "reusify@npm:1.0.4" @@ -8023,6 +7636,18 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^6.0.1": + version: 6.0.1 + resolution: "rimraf@npm:6.0.1" + dependencies: + glob: "npm:^11.0.0" + package-json-from-dist: "npm:^1.0.0" + bin: + rimraf: dist/esm/bin.mjs + checksum: 10/0eb7edf08aa39017496c99ba675552dda11a20811ba78f8232da2ba945308c91e9cd673f95998b1a8202bc7436d33390831d23ea38ae52751038d56373ad99e2 + languageName: node + linkType: hard + "rollup-plugin-dts@npm:^6.1.1": version: 6.1.1 resolution: "rollup-plugin-dts@npm:6.1.1" @@ -8066,60 +7691,6 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.2.0": - version: 4.12.0 - resolution: "rollup@npm:4.12.0" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.12.0" - "@rollup/rollup-android-arm64": "npm:4.12.0" - "@rollup/rollup-darwin-arm64": "npm:4.12.0" - "@rollup/rollup-darwin-x64": "npm:4.12.0" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.12.0" - "@rollup/rollup-linux-arm64-gnu": "npm:4.12.0" - "@rollup/rollup-linux-arm64-musl": "npm:4.12.0" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.12.0" - "@rollup/rollup-linux-x64-gnu": "npm:4.12.0" - "@rollup/rollup-linux-x64-musl": "npm:4.12.0" - "@rollup/rollup-win32-arm64-msvc": "npm:4.12.0" - "@rollup/rollup-win32-ia32-msvc": "npm:4.12.0" - "@rollup/rollup-win32-x64-msvc": "npm:4.12.0" - "@types/estree": "npm:1.0.5" - fsevents: "npm:~2.3.2" - dependenciesMeta: - "@rollup/rollup-android-arm-eabi": - optional: true - "@rollup/rollup-android-arm64": - optional: true - "@rollup/rollup-darwin-arm64": - optional: true - "@rollup/rollup-darwin-x64": - optional: true - "@rollup/rollup-linux-arm-gnueabihf": - optional: true - "@rollup/rollup-linux-arm64-gnu": - optional: true - "@rollup/rollup-linux-arm64-musl": - optional: true - "@rollup/rollup-linux-riscv64-gnu": - optional: true - "@rollup/rollup-linux-x64-gnu": - optional: true - "@rollup/rollup-linux-x64-musl": - optional: true - "@rollup/rollup-win32-arm64-msvc": - optional: true - "@rollup/rollup-win32-ia32-msvc": - optional: true - "@rollup/rollup-win32-x64-msvc": - optional: true - fsevents: - optional: true - bin: - rollup: dist/bin/rollup - checksum: 10/098eac4dcaf051b71c4efb7e3df059f6563d030b4dfbd2622a4d70acf4d02c463885643c63a21dda45153470f9be5047acd11eab19d4b2ed1c06b8ff57997e8e - languageName: node - linkType: hard - "rollup@npm:^4.20.0, rollup@npm:^4.24.0": version: 4.24.0 resolution: "rollup@npm:4.24.0" @@ -8201,15 +7772,15 @@ __metadata: languageName: node linkType: hard -"safe-array-concat@npm:^1.1.0": - version: 1.1.0 - resolution: "safe-array-concat@npm:1.1.0" +"safe-array-concat@npm:^1.1.2": + version: 1.1.2 + resolution: "safe-array-concat@npm:1.1.2" dependencies: - call-bind: "npm:^1.0.5" - get-intrinsic: "npm:^1.2.2" + call-bind: "npm:^1.0.7" + get-intrinsic: "npm:^1.2.4" has-symbols: "npm:^1.0.3" isarray: "npm:^2.0.5" - checksum: 10/41ac35ce46c44e2e8637b1805b0697d5269507779e3082b7afb92c01605fd73ab813bbc799510c56e300cfc941b1447fd98a338205db52db7fd1322ab32d7c9f + checksum: 10/a54f8040d7cb696a1ee38d19cc71ab3cfb654b9b81bae00c6459618cfad8214ece7e6666592f9c925aafef43d0a20c5e6fbb3413a2b618e1ce9d516a2e6dcfc5 languageName: node linkType: hard @@ -8252,7 +7823,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.1.0, semver@npm:^6.3.1": +"semver@npm:^6.1.0, semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -8261,23 +7832,12 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": - version: 7.6.0 - resolution: "semver@npm:7.6.0" - dependencies: - lru-cache: "npm:^6.0.0" - bin: - semver: bin/semver.js - checksum: 10/1b41018df2d8aca5a1db4729985e8e20428c650daea60fcd16e926e9383217d00f574fab92d79612771884a98d2ee2a1973f49d630829a8d54d6570defe62535 - languageName: node - linkType: hard - -"semver@npm:^7.6.2": - version: 7.6.2 - resolution: "semver@npm:7.6.2" +"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3": + version: 7.6.3 + resolution: "semver@npm:7.6.3" bin: semver: bin/semver.js - checksum: 10/296b17d027f57a87ef645e9c725bff4865a38dfc9caf29b26aa084b85820972fbe7372caea1ba6857162fa990702c6d9c1d82297cecb72d56c78ab29070d2ca2 + checksum: 10/36b1fbe1a2b6f873559cd57b238f1094a053dbfd997ceeb8757d79d1d2089c56d1321b9f1069ce263dc64cfa922fa1d2ad566b39426fe1ac6c723c1487589e10 languageName: node linkType: hard @@ -8298,20 +7858,20 @@ __metadata: linkType: hard "set-function-length@npm:^1.2.1": - version: 1.2.1 - resolution: "set-function-length@npm:1.2.1" + version: 1.2.2 + resolution: "set-function-length@npm:1.2.2" dependencies: - define-data-property: "npm:^1.1.2" + define-data-property: "npm:^1.1.4" es-errors: "npm:^1.3.0" function-bind: "npm:^1.1.2" - get-intrinsic: "npm:^1.2.3" + get-intrinsic: "npm:^1.2.4" gopd: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.1" - checksum: 10/9ab1d200149574ab27c1a7acae56d6235e02568fc68655fe8afe63e4e02ccad3c27665f55c32408bd1ff40705939dbb7539abfb9c3a07fda27ecad1ab9e449f5 + has-property-descriptors: "npm:^1.0.2" + checksum: 10/505d62b8e088468917ca4e3f8f39d0e29f9a563b97dbebf92f4bd2c3172ccfb3c5b8e4566d5fcd00784a00433900e7cb8fbc404e2dbd8c3818ba05bb9d4a8a6d languageName: node linkType: hard -"set-function-name@npm:^2.0.1": +"set-function-name@npm:^2.0.2": version: 2.0.2 resolution: "set-function-name@npm:2.0.2" dependencies: @@ -8355,13 +7915,6 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:^1.8.1": - version: 1.8.1 - resolution: "shell-quote@npm:1.8.1" - checksum: 10/af19ab5a1ec30cb4b2f91fd6df49a7442d5c4825a2e269b3712eded10eedd7f9efeaab96d57829880733fc55bcdd8e9b1d8589b4befb06667c731d08145e274d - languageName: node - linkType: hard - "shiki@npm:^1.16.2": version: 1.22.0 resolution: "shiki@npm:1.22.0" @@ -8377,14 +7930,14 @@ __metadata: linkType: hard "side-channel@npm:^1.0.4": - version: 1.0.5 - resolution: "side-channel@npm:1.0.5" + version: 1.0.6 + resolution: "side-channel@npm:1.0.6" dependencies: - call-bind: "npm:^1.0.6" + call-bind: "npm:^1.0.7" es-errors: "npm:^1.3.0" get-intrinsic: "npm:^1.2.4" object-inspect: "npm:^1.13.1" - checksum: 10/27708b70b5d81bf18dc8cc23f38f1b6c9511691a64abc4aaf17956e67d132c855cf8b46f931e2fc5a6262b29371eb60da7755c1b9f4f862eccea8562b469f8f6 + checksum: 10/eb10944f38cebad8ad643dd02657592fa41273ce15b8bfa928d3291aff2d30c20ff777cfe908f76ccc4551ace2d1245822fdc576657cce40e9066c638ca8fa4d languageName: node linkType: hard @@ -8464,9 +8017,9 @@ __metadata: linkType: hard "smob@npm:^1.0.0": - version: 1.4.1 - resolution: "smob@npm:1.4.1" - checksum: 10/bc6ffcb9a1c3c875f9354cf814487d44cd925e2917683e2bf6f66a267eedf895f4989079541b73dc0ddc163cb0fa26078fa95067f1503707758437e9308afc2f + version: 1.5.0 + resolution: "smob@npm:1.5.0" + checksum: 10/a1ea453bcea89989062626ea30a1fcb42c62e96255619c8641ffa1d7ab42baf415975c67c718127036901b9e487d8bf4c46219e50cec54295412c1227700b8fe languageName: node linkType: hard @@ -8481,24 +8034,24 @@ __metadata: languageName: node linkType: hard -"socks-proxy-agent@npm:^8.0.1": - version: 8.0.2 - resolution: "socks-proxy-agent@npm:8.0.2" +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.4 + resolution: "socks-proxy-agent@npm:8.0.4" dependencies: - agent-base: "npm:^7.0.2" + agent-base: "npm:^7.1.1" debug: "npm:^4.3.4" - socks: "npm:^2.7.1" - checksum: 10/ea727734bd5b2567597aa0eda14149b3b9674bb44df5937bbb9815280c1586994de734d965e61f1dd45661183d7b41f115fb9e432d631287c9063864cfcc2ecc + socks: "npm:^2.8.3" + checksum: 10/c8e7c2b398338b49a0a0f4d2bae5c0602aeeca6b478b99415927b6c5db349ca258448f2c87c6958ebf83eea17d42cbc5d1af0bfecb276cac10b9658b0f07f7d7 languageName: node linkType: hard -"socks@npm:^2.6.2, socks@npm:^2.7.1": - version: 2.8.0 - resolution: "socks@npm:2.8.0" +"socks@npm:^2.6.2, socks@npm:^2.8.3": + version: 2.8.3 + resolution: "socks@npm:2.8.3" dependencies: ip-address: "npm:^9.0.5" smart-buffer: "npm:^4.2.0" - checksum: 10/ed0224ce2c7daaa7690cb87cf53d9703ffc4e983aca221f6f5b46767b232658df49494fd86acd0bf97ada6de05248ea8ea625c2343d48155d8463fc40d4a340f + checksum: 10/ffcb622c22481dfcd7589aae71fbfd71ca34334064d181df64bf8b7feaeee19706aba4cffd1de35cc7bbaeeaa0af96be2d7f40fcbc7bc0ab69533a7ae9ffc4fb languageName: node linkType: hard @@ -8527,21 +8080,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.0.2": - version: 1.0.2 - resolution: "source-map-js@npm:1.0.2" - checksum: 10/38e2d2dd18d2e331522001fc51b54127ef4a5d473f53b1349c5cca2123562400e0986648b52e9407e348eaaed53bce49248b6e2641e6d793ca57cb2c360d6d51 - languageName: node - linkType: hard - -"source-map-js@npm:^1.2.0": - version: 1.2.0 - resolution: "source-map-js@npm:1.2.0" - checksum: 10/74f331cfd2d121c50790c8dd6d3c9de6be21926de80583b23b37029b0f37aefc3e019fa91f9a10a5e120c08135297e1ecf312d561459c45908cb1e0e365f49e5 - languageName: node - linkType: hard - -"source-map-js@npm:^1.2.1": +"source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3 @@ -8565,13 +8104,6 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.7.4": - version: 0.7.4 - resolution: "source-map@npm:0.7.4" - checksum: 10/a0f7c9b797eda93139842fd28648e868a9a03ea0ad0d9fa6602a0c1f17b7fb6a7dcca00c144476cccaeaae5042e99a285723b1a201e844ad67221bf5d428f1dc - languageName: node - linkType: hard - "space-separated-tokens@npm:^2.0.0": version: 2.0.2 resolution: "space-separated-tokens@npm:2.0.2" @@ -8627,9 +8159,9 @@ __metadata: linkType: hard "spdx-license-ids@npm:^3.0.0": - version: 3.0.17 - resolution: "spdx-license-ids@npm:3.0.17" - checksum: 10/8f6c6ae02ebb25b4ca658b8990d9e8a8f8d8a95e1d8b9fd84d87eed80a7dc8f8073d6a8d50b8a0295c0e8399e1f8814f5c00e2985e6bf3731540a16f7241cbf1 + version: 3.0.20 + resolution: "spdx-license-ids@npm:3.0.20" + checksum: 10/30e566ea74b04232c64819d1f5313c00d92e9c73d054541650331fc794499b3bcc4991bcd90fa3c2fc4d040006f58f63104706255266e87a9d452e6574afc60c languageName: node linkType: hard @@ -8648,11 +8180,11 @@ __metadata: linkType: hard "ssri@npm:^10.0.0": - version: 10.0.5 - resolution: "ssri@npm:10.0.5" + version: 10.0.6 + resolution: "ssri@npm:10.0.6" dependencies: minipass: "npm:^7.0.3" - checksum: 10/453f9a1c241c13f5dfceca2ab7b4687bcff354c3ccbc932f35452687b9ef0ccf8983fd13b8a3baa5844c1a4882d6e3ddff48b0e7fd21d743809ef33b80616d79 + checksum: 10/f92c1b3cc9bfd0a925417412d07d999935917bc87049f43ebec41074661d64cf720315661844106a77da9f8204b6d55ae29f9514e673083cae39464343af2a8b languageName: node linkType: hard @@ -8701,36 +8233,37 @@ __metadata: languageName: node linkType: hard -"string.prototype.trim@npm:^1.2.8": - version: 1.2.8 - resolution: "string.prototype.trim@npm:1.2.8" +"string.prototype.trim@npm:^1.2.9": + version: 1.2.9 + resolution: "string.prototype.trim@npm:1.2.9" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - checksum: 10/9301f6cb2b6c44f069adde1b50f4048915985170a20a1d64cf7cb2dc53c5cd6b9525b92431f1257f894f94892d6c4ae19b5aa7f577c3589e7e51772dffc9d5a4 + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.0" + es-object-atoms: "npm:^1.0.0" + checksum: 10/b2170903de6a2fb5a49bb8850052144e04b67329d49f1343cdc6a87cb24fb4e4b8ad00d3e273a399b8a3d8c32c89775d93a8f43cb42fbff303f25382079fb58a languageName: node linkType: hard -"string.prototype.trimend@npm:^1.0.7": - version: 1.0.7 - resolution: "string.prototype.trimend@npm:1.0.7" +"string.prototype.trimend@npm:^1.0.8": + version: 1.0.8 + resolution: "string.prototype.trimend@npm:1.0.8" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - checksum: 10/3f0d3397ab9bd95cd98ae2fe0943bd3e7b63d333c2ab88f1875cf2e7c958c75dc3355f6fe19ee7c8fca28de6f39f2475e955e103821feb41299a2764a7463ffa + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10/c2e862ae724f95771da9ea17c27559d4eeced9208b9c20f69bbfcd1b9bc92375adf8af63a103194dba17c4cc4a5cb08842d929f415ff9d89c062d44689c8761b languageName: node linkType: hard -"string.prototype.trimstart@npm:^1.0.7": - version: 1.0.7 - resolution: "string.prototype.trimstart@npm:1.0.7" +"string.prototype.trimstart@npm:^1.0.8": + version: 1.0.8 + resolution: "string.prototype.trimstart@npm:1.0.8" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - checksum: 10/6e594d3a61b127d243b8be1312e9f78683abe452cfe0bcafa3e0dc62ad6f030ccfb64d87ed3086fb7cb540fda62442c164d237cc5cc4d53c6e3eb659c29a0aeb + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10/160167dfbd68e6f7cb9f51a16074eebfce1571656fc31d40c3738ca9e30e35496f2c046fe57b6ad49f65f238a152be8c86fd9a2dd58682b5eba39dad995b3674 languageName: node linkType: hard @@ -8810,33 +8343,13 @@ __metadata: languageName: node linkType: hard -"synckit@npm:0.9.1": - version: 0.9.1 - resolution: "synckit@npm:0.9.1" - dependencies: - "@pkgr/core": "npm:^0.1.0" - tslib: "npm:^2.6.2" - checksum: 10/bff3903976baf8b699b5483228116d70223781a93b17c70e685c277ee960cdfd1a09cb5a741e6a9ec35e2428f14f4664baec41ccc99a598f267608b2a54f529b - languageName: node - linkType: hard - -"synckit@npm:^0.8.6": - version: 0.8.8 - resolution: "synckit@npm:0.8.8" - dependencies: - "@pkgr/core": "npm:^0.1.0" - tslib: "npm:^2.6.2" - checksum: 10/2864a5c3e689ad5b991bebbd8a583c5682c4fa08a4f39986b510b6b5d160c08fc3672444069f8f96ed6a9d12772879c674c1f61e728573eadfa90af40a765b74 - languageName: node - linkType: hard - -"synckit@npm:^0.9.0": - version: 0.9.0 - resolution: "synckit@npm:0.9.0" +"synckit@npm:0.9.2, synckit@npm:^0.9.1": + version: 0.9.2 + resolution: "synckit@npm:0.9.2" dependencies: "@pkgr/core": "npm:^0.1.0" tslib: "npm:^2.6.2" - checksum: 10/e93f3f5ee43fa71d3bb2a345049642d9034f34fa9528706b5ef26e825335ca5446143c56c2b041810afe26aa6e343583ff08525f5530618a4707375270f87be1 + checksum: 10/d45c4288be9c0232343650643892a7edafb79152c0c08d7ae5d33ca2c296b67a0e15f8cb5c9153969612c4ea5cd5686297542384aab977db23cfa6653fe02027 languageName: node linkType: hard @@ -8847,7 +8360,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:^6.1.11, tar@npm:^6.1.2": +"tar@npm:^6.1.11, tar@npm:^6.1.2, tar@npm:^6.2.1": version: 6.2.1 resolution: "tar@npm:6.2.1" dependencies: @@ -8869,8 +8382,8 @@ __metadata: linkType: hard "terser@npm:^5.17.4, terser@npm:^5.6.0": - version: 5.27.2 - resolution: "terser@npm:5.27.2" + version: 5.34.1 + resolution: "terser@npm:5.34.1" dependencies: "@jridgewell/source-map": "npm:^0.3.3" acorn: "npm:^8.8.2" @@ -8878,7 +8391,7 @@ __metadata: source-map-support: "npm:~0.5.20" bin: terser: bin/terser - checksum: 10/589f1112d6cd7653f6e2d4a38970e97a160de01cddb214dc924aa330c22b8c3635067a47db1233e060e613e380b979ca336c3211b17507ea13b0adff10ecbd40 + checksum: 10/4389f39b5b841e2a7795ee733b54bf8fc44f8784a78c213dae32c7e6adc66c3bb258ebdcbacb8e7f1fa08fceb20bfc4ce4f7666d42bbfc29ab71126e89614c34 languageName: node linkType: hard @@ -8915,9 +8428,9 @@ __metadata: linkType: hard "tinypool@npm:^1.0.0": - version: 1.0.0 - resolution: "tinypool@npm:1.0.0" - checksum: 10/4041a9ae62200626dceedbf4e58589d067a203eadcb88588d5681369b9a3c68987de14ce220b32a7e4ebfabaaf51ab9fa69408a7758827b7873f8204cdc79aa1 + version: 1.0.1 + resolution: "tinypool@npm:1.0.1" + checksum: 10/eaceb93784b8e27e60c0e3e2c7d11c29e1e79b2a025b2c232215db73b90fe22bd4753ad53fc8e801c2b5a63b94a823af549555d8361272bc98271de7dd4a9925 languageName: node linkType: hard @@ -8929,16 +8442,9 @@ __metadata: linkType: hard "tinyspy@npm:^3.0.0": - version: 3.0.0 - resolution: "tinyspy@npm:3.0.0" - checksum: 10/b5b686acff2b88de60ff8ecf89a2042320406aaeee2fba1828a7ea8a925fad3ed9f5e4d7a068154a9134473c472aa03da8ca92ee994bc57a741c5ede5fa7de4d - languageName: node - linkType: hard - -"tlhunter-sorted-set@npm:^0.1.0": - version: 0.1.0 - resolution: "tlhunter-sorted-set@npm:0.1.0" - checksum: 10/908019aadd169263f63b63e6864a61fe93c08657ac5c8496bd72b291620c88b7a81e18e735cfbf044da2f9439c1f36ee3e740dd806781431214f3b01ed3df0a3 + version: 3.0.2 + resolution: "tinyspy@npm:3.0.2" + checksum: 10/5db671b2ff5cd309de650c8c4761ca945459d7204afb1776db9a04fb4efa28a75f08517a8620c01ee32a577748802231ad92f7d5b194dc003ee7f987a2a06337 languageName: node linkType: hard @@ -8974,7 +8480,7 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^1.3.0": +"ts-api-utils@npm:^1.0.1, ts-api-utils@npm:^1.3.0": version: 1.3.0 resolution: "ts-api-utils@npm:1.3.0" peerDependencies: @@ -8984,8 +8490,8 @@ __metadata: linkType: hard "tsconfck@npm:^3.0.3": - version: 3.0.3 - resolution: "tsconfck@npm:3.0.3" + version: 3.1.3 + resolution: "tsconfck@npm:3.1.3" peerDependencies: typescript: ^5.0.0 peerDependenciesMeta: @@ -8993,7 +8499,7 @@ __metadata: optional: true bin: tsconfck: bin/tsconfck.js - checksum: 10/1c17217dc3758e71bebdb223b7cd6e613f8f8c92a225cccc40d459554dfae50cbf9d339c6a4a5a8d04620fe1c21bb6d454b6e10421e3fcd808ea51d0b5039ffd + checksum: 10/bf9b9b72de5b83f833f5dea8b276e77bab08e85751589f36dd23854fa3d5f7955194086fb8424df388bf232f2fc9a067d7913bfa674cb1217be0bba648ec71f2 languageName: node linkType: hard @@ -9009,10 +8515,23 @@ __metadata: languageName: node linkType: hard +"tsconfig@workspace:shared/tsconfig": + version: 0.0.0-use.local + resolution: "tsconfig@workspace:shared/tsconfig" + languageName: unknown + linkType: soft + +"tslib@npm:2.4.0": + version: 2.4.0 + resolution: "tslib@npm:2.4.0" + checksum: 10/d8379e68b36caf082c1905ec25d17df8261e1d68ddc1abfd6c91158a064f6e4402039ae7c02cf4c81d12e3a2a2c7cd8ea2f57b233eb80136a2e3e7279daf2911 + languageName: node + linkType: hard + "tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 10/9a5b47ddac65874fa011c20ff76db69f97cf90c78cff5934799ab8894a5342db2d17b4e7613a087046bc1d133d21547ddff87ac558abeec31ffa929c88b7fce6 languageName: node linkType: hard @@ -9121,7 +8640,7 @@ __metadata: languageName: node linkType: hard -"typed-array-buffer@npm:^1.0.1": +"typed-array-buffer@npm:^1.0.2": version: 1.0.2 resolution: "typed-array-buffer@npm:1.0.2" dependencies: @@ -9132,7 +8651,7 @@ __metadata: languageName: node linkType: hard -"typed-array-byte-length@npm:^1.0.0": +"typed-array-byte-length@npm:^1.0.1": version: 1.0.1 resolution: "typed-array-byte-length@npm:1.0.1" dependencies: @@ -9145,7 +8664,7 @@ __metadata: languageName: node linkType: hard -"typed-array-byte-offset@npm:^1.0.0": +"typed-array-byte-offset@npm:^1.0.2": version: 1.0.2 resolution: "typed-array-byte-offset@npm:1.0.2" dependencies: @@ -9159,9 +8678,9 @@ __metadata: languageName: node linkType: hard -"typed-array-length@npm:^1.0.4": - version: 1.0.5 - resolution: "typed-array-length@npm:1.0.5" +"typed-array-length@npm:^1.0.6": + version: 1.0.6 + resolution: "typed-array-length@npm:1.0.6" dependencies: call-bind: "npm:^1.0.7" for-each: "npm:^0.3.3" @@ -9169,7 +8688,7 @@ __metadata: has-proto: "npm:^1.0.3" is-typed-array: "npm:^1.1.13" possible-typed-array-names: "npm:^1.0.0" - checksum: 10/f9a0da99c41880b44e2c5e5d0d01515c2a6e0f54b10c594151804f013272d837df3b67ea84d7304ecfbab2c10d99c3372168bf3a4bd295abf13ac5a72f93054a + checksum: 10/05e96cf4ff836743ebfc593d86133b8c30e83172cb5d16c56814d7bacfed57ce97e87ada9c4b2156d9aaa59f75cdef01c25bd9081c7826e0b869afbefc3e8c39 languageName: node linkType: hard @@ -9199,43 +8718,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.3.3": - version: 5.3.3 - resolution: "typescript@npm:5.3.3" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10/6e4e6a14a50c222b3d14d4ea2f729e79f972fa536ac1522b91202a9a65af3605c2928c4a790a4a50aa13694d461c479ba92cedaeb1e7b190aadaa4e4b96b8e18 - languageName: node - linkType: hard - -"typescript@npm:^5.6.2": - version: 5.6.2 - resolution: "typescript@npm:5.6.2" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10/f95365d4898f357823e93d334ecda9fcade54f009b397c7d05b7621cd9e865981033cf89ccde0f3e3a7b73b1fdbae18e92bc77db237b43e912f053fef0f9a53b - languageName: node - linkType: hard - -"typescript@patch:typescript@npm%3A^5.3.3#optional!builtin": - version: 5.3.3 - resolution: "typescript@patch:typescript@npm%3A5.3.3#optional!builtin::version=5.3.3&hash=e012d7" +"typescript@npm:^5.3.3, typescript@npm:^5.6.3": + version: 5.6.3 + resolution: "typescript@npm:5.6.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/c93786fcc9a70718ba1e3819bab56064ead5817004d1b8186f8ca66165f3a2d0100fee91fa64c840dcd45f994ca5d615d8e1f566d39a7470fc1e014dbb4cf15d + checksum: 10/c328e418e124b500908781d9f7b9b93cf08b66bf5936d94332b463822eea2f4e62973bfb3b8a745fdc038785cb66cf59d1092bac3ec2ac6a3e5854687f7833f1 languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.6.2#optional!builtin": - version: 5.6.2 - resolution: "typescript@patch:typescript@npm%3A5.6.2#optional!builtin::version=5.6.2&hash=8c6c40" +"typescript@patch:typescript@npm%3A^5.3.3#optional!builtin, typescript@patch:typescript@npm%3A^5.6.3#optional!builtin": + version: 5.6.3 + resolution: "typescript@patch:typescript@npm%3A5.6.3#optional!builtin::version=5.6.3&hash=8c6c40" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10/8bfc7ca0d9feca4c3fcbd6c70741abfcd714197d6448e68225ae71e462447d904d3bfba49759a8fbe4956d87f054e2d346833c8349c222daa594a2626d4e1be8 + checksum: 10/00504c01ee42d470c23495426af07512e25e6546bce7e24572e72a9ca2e6b2e9bea63de4286c3cfea644874da1467dcfca23f4f98f7caf20f8b03c0213bb6837 languageName: node linkType: hard @@ -9265,6 +8764,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 + languageName: node + linkType: hard + "undici@npm:^5.8.1": version: 5.28.4 resolution: "undici@npm:5.28.4" @@ -9379,17 +8885,17 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.0.13": - version: 1.0.13 - resolution: "update-browserslist-db@npm:1.0.13" +"update-browserslist-db@npm:^1.1.0": + version: 1.1.1 + resolution: "update-browserslist-db@npm:1.1.1" dependencies: - escalade: "npm:^3.1.1" - picocolors: "npm:^1.0.0" + escalade: "npm:^3.2.0" + picocolors: "npm:^1.1.0" peerDependencies: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: 10/9074b4ef34d2ed931f27d390aafdd391ee7c45ad83c508e8fed6aaae1eb68f81999a768ed8525c6f88d4001a4fbf1b8c0268f099d0e8e72088ec5945ac796acf + checksum: 10/7678dd8609750588d01aa7460e8eddf2ff9d16c2a52fb1811190e0d056390f1fdffd94db3cf8fb209cf634ab4fa9407886338711c71cc6ccade5eeb22b093734 languageName: node linkType: hard @@ -9429,11 +8935,9 @@ __metadata: linkType: hard "validate-npm-package-name@npm:^5.0.0": - version: 5.0.0 - resolution: "validate-npm-package-name@npm:5.0.0" - dependencies: - builtins: "npm:^5.0.0" - checksum: 10/5342a994986199b3c28e53a8452a14b2bb5085727691ea7aa0d284a6606b127c371e0925ae99b3f1ef7cc7d2c9de75f52eb61a3d1cc45e39bca1e3a9444cbb4e + version: 5.0.1 + resolution: "validate-npm-package-name@npm:5.0.1" + checksum: 10/0d583a1af23aeffea7748742cf22b6802458736fb8b60323ba5949763824d46f796474b0e1b9206beb716f9d75269e19dbd7795d6b038b29d561be95dd827381 languageName: node linkType: hard @@ -9479,23 +8983,24 @@ __metadata: linkType: hard "viem@npm:^2.13.6": - version: 2.13.6 - resolution: "viem@npm:2.13.6" - dependencies: - "@adraffy/ens-normalize": "npm:1.10.0" - "@noble/curves": "npm:1.2.0" - "@noble/hashes": "npm:1.3.2" - "@scure/bip32": "npm:1.3.2" - "@scure/bip39": "npm:1.2.1" - abitype: "npm:1.0.0" - isows: "npm:1.0.4" - ws: "npm:8.13.0" + version: 2.21.21 + resolution: "viem@npm:2.21.21" + dependencies: + "@adraffy/ens-normalize": "npm:1.11.0" + "@noble/curves": "npm:1.6.0" + "@noble/hashes": "npm:1.5.0" + "@scure/bip32": "npm:1.5.0" + "@scure/bip39": "npm:1.4.0" + abitype: "npm:1.0.6" + isows: "npm:1.0.6" + webauthn-p256: "npm:0.0.10" + ws: "npm:8.18.0" peerDependencies: typescript: ">=5.0.4" peerDependenciesMeta: typescript: optional: true - checksum: 10/07eab04b27b4bdb9913408fec1c7bb00dfaf5bb1e81359dbcc772fe2c22db06ee94d152011c159654eb962fca54feec33facb5970454b7786e166a0cdabd9c1b + checksum: 10/1fe36236c802a512a359c4710d98e627c04795cdf62bb0341c9ac6ab8afc5626431a1b471062ac29f5e2f23bca6ce933d552858f55ad1968514aaea1fb74324d languageName: node linkType: hard @@ -9529,7 +9034,7 @@ __metadata: languageName: node linkType: hard -"vite@npm:5.4.8": +"vite@npm:5.4.8, vite@npm:^5.0.0, vite@npm:^5.1.6": version: 5.4.8 resolution: "vite@npm:5.4.8" dependencies: @@ -9572,46 +9077,6 @@ __metadata: languageName: node linkType: hard -"vite@npm:^5.0.0": - version: 5.1.4 - resolution: "vite@npm:5.1.4" - dependencies: - esbuild: "npm:^0.19.3" - fsevents: "npm:~2.3.3" - postcss: "npm:^8.4.35" - rollup: "npm:^4.2.0" - peerDependencies: - "@types/node": ^18.0.0 || >=20.0.0 - less: "*" - lightningcss: ^1.21.0 - sass: "*" - stylus: "*" - sugarss: "*" - terser: ^5.4.0 - dependenciesMeta: - fsevents: - optional: true - peerDependenciesMeta: - "@types/node": - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - bin: - vite: bin/vite.js - checksum: 10/e9003b853f0784260f4fe7ce0190124b347fd8fd6bf889a07080facd0d9a9667eaff4022eddb1ba3f0283ef69d15d77f84bca832082e48874a7a62e7f6d66b08 - languageName: node - linkType: hard - "vitest@npm:^2.1.2": version: 2.1.2 resolution: "vitest@npm:2.1.2" @@ -9661,6 +9126,16 @@ __metadata: languageName: node linkType: hard +"webauthn-p256@npm:0.0.10": + version: 0.0.10 + resolution: "webauthn-p256@npm:0.0.10" + dependencies: + "@noble/curves": "npm:^1.4.0" + "@noble/hashes": "npm:^1.4.0" + checksum: 10/dde2b6313b6a0f20996f7ee90181258fc7685bfff401df7d904578da75b374f25d5b9c1189cd2fcec30625b1f276b393188d156d49783f0611623cd713bb5b09 + languageName: node + linkType: hard + "webidl-conversions@npm:^7.0.0": version: 7.0.0 resolution: "webidl-conversions@npm:7.0.0" @@ -9688,16 +9163,16 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.14": - version: 1.1.14 - resolution: "which-typed-array@npm:1.1.14" +"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.15": + version: 1.1.15 + resolution: "which-typed-array@npm:1.1.15" dependencies: - available-typed-arrays: "npm:^1.0.6" - call-bind: "npm:^1.0.5" + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.7" for-each: "npm:^0.3.3" gopd: "npm:^1.0.1" - has-tostringtag: "npm:^1.0.1" - checksum: 10/56253d2c9d6b41b8a4af96d8c2751bac5508906bd500cdcd0dc5301fb082de0391a4311ab21258bc8d2609ed593f422c1a66f0020fcb3a1e97f719bc928b9018 + has-tostringtag: "npm:^1.0.2" + checksum: 10/c3b6a99beadc971baa53c3ee5b749f2b9bdfa3b3b9a70650dd8511a48b61d877288b498d424712e9991d16019633086bd8b5923369460d93463c5825fa36c448 languageName: node linkType: hard @@ -9775,6 +9250,13 @@ __metadata: languageName: node linkType: hard +"word-wrap@npm:^1.2.5": + version: 1.2.5 + resolution: "word-wrap@npm:1.2.5" + checksum: 10/1ec6f6089f205f83037be10d0c4b34c9183b0b63fca0834a5b3cee55dd321429d73d40bb44c8fc8471b5203d6e8f8275717f49a8ff4b2b0ab41d7e1b563e0854 + languageName: node + linkType: hard + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -9845,14 +9327,46 @@ __metadata: languageName: node linkType: hard +"ws@npm:8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/4264ae92c0b3e59c7e309001e93079b26937aab181835fb7af79f906b22cd33b6196d96556dafb4e985742dd401e99139572242e9847661fdbc96556b9e6902d + languageName: node + linkType: hard + +"ws@npm:8.18.0": + version: 8.18.0 + resolution: "ws@npm:8.18.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/70dfe53f23ff4368d46e4c0b1d4ca734db2c4149c6f68bc62cb16fc21f753c47b35fcc6e582f3bdfba0eaeb1c488cddab3c2255755a5c3eecb251431e42b3ff6 + languageName: node + linkType: hard + "xmtp-js@workspace:.": version: 0.0.0-use.local resolution: "xmtp-js@workspace:." dependencies: "@changesets/changelog-git": "npm:^0.2.0" "@changesets/cli": "npm:^2.27.9" + "@ianvs/prettier-plugin-sort-imports": "npm:^4.3.1" prettier: "npm:^3.3.3" - prettier-plugin-packagejson: "npm:^2.5.2" + prettier-plugin-packagejson: "npm:^2.5.3" + rimraf: "npm:^6.0.1" turbo: "npm:^2.1.3" languageName: unknown linkType: soft @@ -9895,9 +9409,9 @@ __metadata: linkType: hard "yocto-queue@npm:^1.0.0": - version: 1.0.0 - resolution: "yocto-queue@npm:1.0.0" - checksum: 10/2cac84540f65c64ccc1683c267edce396b26b1e931aa429660aefac8fbe0188167b7aee815a3c22fa59a28a58d898d1a2b1825048f834d8d629f4c2a5d443801 + version: 1.1.1 + resolution: "yocto-queue@npm:1.1.1" + checksum: 10/f2e05b767ed3141e6372a80af9caa4715d60969227f38b1a4370d60bffe153c9c5b33a862905609afc9b375ec57cd40999810d20e5e10229a204e8bde7ef255c languageName: node linkType: hard