Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

main => beta #500

Merged
merged 22 commits into from
Dec 5, 2023
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
25 changes: 5 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

7 changes: 1 addition & 6 deletions src/ApiClient.ts
Original file line number Diff line number Diff line change
@@ -111,15 +111,13 @@ 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
}
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)
2 changes: 1 addition & 1 deletion src/Invitation.ts
Original file line number Diff line number Diff line change
@@ -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))

21 changes: 12 additions & 9 deletions src/conversations/Conversations.ts
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import {
buildUserIntroTopic,
buildUserInviteTopic,
dateToNs,
isValidTopic,
nsToDate,
} from '../utils'
import { PublicKeyBundle } from '../crypto'
@@ -77,14 +78,14 @@ export default class Conversations<ContentTypes = any> {
})

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<ContentTypes = any> {
shouldThrow = false
): Promise<ConversationV2<ContentTypes>[]> {
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<ContentTypes>[] = []
15 changes: 15 additions & 0 deletions src/utils/topic.ts
Original file line number Diff line number Diff line change
@@ -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
}
48 changes: 48 additions & 0 deletions test/utils/topic.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
})