diff --git a/LICENSE b/LICENSE index ff0f50665..11eedfd19 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 XMTP Labs (xmtp.com) +Copyright (c) 2023 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 diff --git a/README.md b/README.md index 4bda8614d..46675d946 100644 --- a/README.md +++ b/README.md @@ -37,30 +37,15 @@ Additional configuration is required in React environments due to the removal of ## Troubleshoot -If you get into issues with Buffer and polyfills check out our [fix below](https://xmtp.org/docs/developer-quickstart#troubleshooting). +### Buffer polyfill -### Create React App +If you run into issues with Buffer and polyfills, see this [solution](https://xmtp.org/docs/faq#why-my-app-is-failing-saying-buffer-is-not-found). -Use `react-scripts` prior to version `5.0.0`. For example: +### BigInt polyfill -```bash -npx create-react-app my-app --scripts-version 4.0.2 -``` - -Or downgrade after creating your app. - -### Next.js - -In `next.config.js`: +This SDK uses `BigInt` in a way that's incompatible with polyfills. To ensure that a polyfill isn't added to your application bundle, update your [browserslist](https://github.com/browserslist/browserslist) configuration to exclude browsers that don't support `BigInt`. -```js -webpack: (config, { isServer }) => { - if (!isServer) { - config.resolve.fallback.fs = false - } - return config -} -``` +For the list of browsers that don't support `BigInt`, see this [compatibility list](https://caniuse.com/bigint). ## Usage diff --git a/src/ApiClient.ts b/src/ApiClient.ts index 1859f6124..90ff0ba5b 100644 --- a/src/ApiClient.ts +++ b/src/ApiClient.ts @@ -111,7 +111,6 @@ const isAbortError = (err?: Error): boolean => { return false } -// eslint-disable-next-line @typescript-eslint/no-explicit-any const isAuthError = (err?: GrpcError | Error): boolean => { if (err && 'code' in err && err.code === ERR_CODE_UNAUTHENTICATED) { return true @@ -119,7 +118,6 @@ const isAuthError = (err?: GrpcError | Error): boolean => { return false } -// eslint-disable-next-line @typescript-eslint/no-explicit-any const isNotAuthError = (err?: Error): boolean => !isAuthError(err) export interface ApiClient { @@ -290,10 +288,7 @@ export default class HttpApiClient implements ApiClient { if (isAbortError(err) || abortController.signal.aborted) { return } - console.info( - 'Stream connection closed. Resubscribing', - err.toString() - ) + console.info('Stream connection closed. Resubscribing') if (new Date().getTime() - startTime < 1000) { await sleep(1000) diff --git a/src/Invitation.ts b/src/Invitation.ts index b9286d2b7..07cecd8f5 100644 --- a/src/Invitation.ts +++ b/src/Invitation.ts @@ -47,7 +47,7 @@ export class InvitationV1 implements invitation.InvitationV1 { .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('/', '-') + .replace(/\//g, '-') ) const keyMaterial = crypto.getRandomValues(new Uint8Array(32)) diff --git a/src/conversations/Conversations.ts b/src/conversations/Conversations.ts index 3f474c1e6..a4706587f 100644 --- a/src/conversations/Conversations.ts +++ b/src/conversations/Conversations.ts @@ -12,6 +12,7 @@ import { buildUserIntroTopic, buildUserInviteTopic, dateToNs, + isValidTopic, nsToDate, } from '../utils' import { PublicKeyBundle } from '../crypto' @@ -77,14 +78,14 @@ export default class Conversations { }) await this.client.keystore.saveV1Conversations({ - conversations: Array.from(seenPeers).map( - ([peerAddress, createdAt]) => ({ + conversations: Array.from(seenPeers) + .map(([peerAddress, createdAt]) => ({ peerAddress, createdNs: dateToNs(createdAt), topic: buildDirectMessageTopic(peerAddress, this.client.address), context: undefined, - }) - ), + })) + .filter((c) => isValidTopic(c.topic)), }) return ( @@ -152,11 +153,13 @@ export default class Conversations { shouldThrow = false ): Promise[]> { const { responses } = await this.client.keystore.saveInvites({ - requests: envelopes.map((env) => ({ - payload: env.message as Uint8Array, - timestampNs: Long.fromString(env.timestampNs as string), - contentTopic: env.contentTopic as string, - })), + requests: envelopes + .map((env) => ({ + payload: env.message as Uint8Array, + timestampNs: Long.fromString(env.timestampNs as string), + contentTopic: env.contentTopic as string, + })) + .filter((req) => isValidTopic(req.contentTopic)), }) const out: ConversationV2[] = [] diff --git a/src/utils/topic.ts b/src/utils/topic.ts index 2acc22818..c2ec64758 100644 --- a/src/utils/topic.ts +++ b/src/utils/topic.ts @@ -39,3 +39,18 @@ export const buildUserPrivateStoreTopic = (addrPrefixedKey: string): string => { export const buildUserPrivatePreferencesTopic = (identifier: string) => buildContentTopic(`user-preferences-${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/') + if (index !== -1) { + const unwrappedTopic = topic.substring( + index + 2, + topic.lastIndexOf('/proto') + ) + return regex.test(unwrappedTopic) + } + return false +} diff --git a/test/utils/topic.test.ts b/test/utils/topic.test.ts new file mode 100644 index 000000000..2baf3d6e0 --- /dev/null +++ b/test/utils/topic.test.ts @@ -0,0 +1,48 @@ +import { + buildContentTopic, + buildDirectMessageTopicV2, + isValidTopic, +} from '../../src/utils/topic' +import crypto from '../../src/crypto/crypto' + +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) + expect(isValidTopic(buildContentTopic(String.fromCharCode(1)))).toBe( + false + ) + expect(isValidTopic(buildContentTopic(String.fromCharCode(23)))).toBe( + false + ) + }) + + it('validates random topics correctly', () => { + const topics = Array.from({ length: 100 }).map(() => + buildDirectMessageTopicV2( + Buffer.from(crypto.getRandomValues(new Uint8Array(32))) + .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, '-') + ) + ) + + topics.forEach((topic) => { + expect(isValidTopic(topic)).toBe(true) + }) + }) + }) +})