chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:34:48 +08:00
commit 77bb5bf71f
3762 changed files with 353249 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
const _configKeys = ['API_URL', 'ENCRYPTION_KEY'] as const
const values: Partial<Record<ConfigKey, string>> = {}
export type ConfigKey = (typeof _configKeys)[number]
export const get = (key: ConfigKey): string => {
const cached = values[key]
if (cached) {
return cached
}
const value = process.env[key]
if (!value) {
throw new Error(`${key} not set`)
}
values[key] = value
console.info(`config: ${key}=${value}`)
return value
}
@@ -0,0 +1,81 @@
import { expect, test } from 'vitest'
import _ from 'lodash'
import * as utils from './utils'
import * as config from './config'
import * as chat from '../src'
const apiUrl = config.get('API_URL')
test('api prevents creating conversation with an invalid fid', async () => {
const client = await chat.Client.connect({ apiUrl })
const convFid = utils.getConversationFid()
const invalidUserIds = [
`invalid+${convFid}`, // invalid character
`invalid_${convFid}_${convFid}_${convFid}`, // too long
]
for (const id of invalidUserIds) {
const promise = client.createConversation({ id })
await expect(promise).rejects.toThrow(chat.InvalidPayloadError)
}
for (const id of invalidUserIds) {
const promise = client.getOrCreateConversation({ id })
await expect(promise).rejects.toThrow(chat.InvalidPayloadError)
}
})
test('api prevents creating multiple conversations with the same foreign id', async () => {
const client = await chat.Client.connect({ apiUrl })
const conversationFid = utils.getConversationFid()
const createConversation = () => client.createConversation({ id: conversationFid })
await createConversation()
await expect(createConversation).rejects.toThrow(chat.AlreadyExistsError)
})
test('get or create a conversation always returns the same conversation', async () => {
const client = await chat.Client.connect({ apiUrl })
const {
conversation: { id: conversationId },
} = await client.createConversation({})
const getOrCreate = () => client.getConversation({ id: conversationId }).then((r) => r.conversation.id)
expect(await getOrCreate()).toBe(conversationId)
expect(await getOrCreate()).toBe(conversationId) // operation is idempotent
})
test('get or create a conversation with a fid always returns the same conversation', async () => {
const client = await chat.Client.connect({ apiUrl })
const conversationFid = utils.getConversationFid()
await client.createConversation({ id: conversationFid })
const getOrCreate = () => client.getConversation({ id: conversationFid }).then((r) => r.conversation.id)
expect(await getOrCreate()).toBe(conversationFid)
expect(await getOrCreate()).toBe(conversationFid) // operation is idempotent
})
test('get conversation is only allowed for participants', async () => {
const client = new chat.Client({ apiUrl })
const user1 = await client.createUser({})
const user2 = await client.createUser({})
const user3 = await client.createUser({})
const { conversation } = await client.createConversation({ 'x-user-key': user1.key })
await client.addParticipant({ conversationId: conversation.id, 'x-user-key': user1.key, userId: user2.user.id })
const promise1 = client.getConversation({ id: conversation.id, 'x-user-key': user1.key })
await expect(promise1).resolves.toBeTruthy()
const promise2 = client.getConversation({ id: conversation.id, 'x-user-key': user2.key })
await expect(promise2).resolves.toBeTruthy()
const promise3 = client.getConversation({ id: conversation.id, 'x-user-key': user3.key })
await expect(promise3).rejects.toThrow(chat.ForbiddenError)
})
+46
View File
@@ -0,0 +1,46 @@
import { expect, test } from 'vitest'
import _ from 'lodash'
import * as config from './config'
import * as chat from '../src'
const apiUrl = config.get('API_URL')
test('api allows sending and receiving custom events', async () => {
const client = await chat.Client.connect({ apiUrl, debug: true })
const {
conversation: { id: conversationId },
} = await client.createConversation({})
const listener = await client.listenConversation({
id: conversationId,
})
const waitForEventPromise = new Promise<chat.Signals['event_created']>((resolve) => {
listener.onceOrMore('event_created', (ev) => {
if (ev.userId === client.user.id) {
return 'keep-listening'
}
resolve(ev)
return 'stop-listening'
})
})
const createEventRequest: chat.AuthenticatedClientRequests['createEvent'] = {
conversationId: conversationId,
payload: {
foo: 'bar',
},
}
const createEventPromise = client.createEvent(createEventRequest).then((res) => res.event)
const [eventReceived, eventSent] = await Promise.all([waitForEventPromise, createEventPromise])
const { event: eventFetched } = await client.getEvent({
id: eventSent.id,
})
expect(eventFetched).toEqual(eventSent)
expect(eventReceived.payload).toEqual(eventSent.payload) // hello bot just passes the payload through
})
+265
View File
@@ -0,0 +1,265 @@
import { expect, test } from 'vitest'
import _ from 'lodash'
import * as utils from './utils'
import * as config from './config'
import * as chat from '../src'
const apiUrl = config.get('API_URL')
const encryptionKey = config.get('ENCRYPTION_KEY')
const serverEventsProtocols = ['sse', 'websocket'] as const
type CheckApiCanSendAndReceiveMessagesProps = {
client: chat.AuthenticatedClient
conversationId: string
protocol: chat.ServerEventsProtocol
}
type MessagePayload = chat.AuthenticatedClientRequests['createMessage']['payload']
const checkApiCanSendAndReceiveMessages = async (
props: CheckApiCanSendAndReceiveMessagesProps,
payload: MessagePayload
): Promise<MessagePayload> => {
const { client, conversationId } = props
const listener = await client.listenConversation({
id: conversationId,
protocol: props.protocol,
})
const waitForResponsePromise = new Promise<chat.Signals['message_created']>((resolve) => {
listener.onceOrMore('message_created', (ev) => {
if (ev.userId === client.user.id) {
return 'keep-listening'
}
resolve(ev)
return 'stop-listening'
})
})
const createMessageRequest: chat.AuthenticatedClientRequests['createMessage'] = {
conversationId: conversationId,
payload,
}
const createMessagePromise = client.createMessage(createMessageRequest).then((res) => res.message)
const [{ isBot, ...messageReceived }, messageSent] = await Promise.all([waitForResponsePromise, createMessagePromise])
const { messages } = await client
.listMessages({
conversationId,
})
.then(({ messages }) => ({
messages: _.sortBy(messages, (m) => new Date(m.createdAt).getTime()),
}))
expect(messages.length).toBe(2)
expect(messages[0]).toEqual(messageSent)
expect(messages[1]).toEqual(messageReceived)
return messageReceived.payload
}
test.each(serverEventsProtocols)('api allows sending and receiving messages using botpress IDs', async (protocol) => {
const client = await chat.Client.connect({ apiUrl })
const {
conversation: { id: conversationId },
} = await client.createConversation({})
await checkApiCanSendAndReceiveMessages(
{
client,
conversationId,
protocol,
},
{
type: 'text',
text: 'hello world',
}
)
})
test.each(serverEventsProtocols)('api allows sending and receiving messages using foreign IDs', async (protocol) => {
const userId = utils.getUserFid()
const conversationId = utils.getConversationFid()
const client = await chat.Client.connect({ apiUrl, userId })
await client.createConversation({ id: conversationId })
await checkApiCanSendAndReceiveMessages(
{
client,
conversationId,
protocol,
},
{
type: 'text',
text: 'hello world',
}
)
})
test.each(serverEventsProtocols)(
'api allows sending and receiving messages using remotly generated JWTs',
async (protocol) => {
const userId = utils.getUserFid()
const conversationId = utils.getConversationFid()
const client = await chat.Client.connect({ apiUrl, userId, encryptionKey })
await client.getOrCreateConversation({ id: conversationId })
await checkApiCanSendAndReceiveMessages(
{
client,
conversationId,
protocol,
},
{
type: 'text',
text: 'hello world',
}
)
}
)
test.each(serverEventsProtocols)('api allows deleting a message', async (protocol) => {
const client = await chat.Client.connect({ apiUrl })
const { conversation } = await client.createConversation({})
const signalListener = await client.listenConversation({ id: conversation.id, protocol })
const [{ isBot, ...createdMessage }] = await Promise.all([
utils.waitFor(signalListener, 'message_created'),
client.createMessage({
conversationId: conversation.id,
payload: {
type: 'text',
text: 'hello world',
},
}),
])
const { message: fetchedMessage } = await client.getMessage({
id: createdMessage.id,
})
expect(fetchedMessage).toEqual(createdMessage)
const [deletedMessage] = await Promise.all([
utils.waitFor(signalListener, 'message_deleted'),
client.deleteMessage({
id: createdMessage.id,
}),
])
expect(deletedMessage.id).toEqual(createdMessage.id)
await expect(
client.getMessage({
id: createdMessage.id,
})
).rejects.toThrow(chat.ResourceNotFoundError)
})
test.each(serverEventsProtocols)('api allows sending and receiving messages with metadata', async (protocol) => {
type Message = Awaited<ReturnType<chat.Client['listMessages']>>['messages'][number]
const metadata = { foo: 'bar' }
const client = await chat.Client.connect({ apiUrl })
const { conversation } = await client.createConversation({})
const { id: conversationId } = conversation
const sendMessagePromise = client.createMessage({
conversationId,
payload: { type: 'text', text: 'metadata' }, // hello-bot forwards metadata when message is 'metadata'
metadata,
})
const listener = await client.listenConversation({ id: conversationId, protocol })
const receiveSelfMessagePromise = new Promise<Message>((resolve) =>
listener.on('message_created', (m) => {
if (m.userId === client.user.id) {
resolve(m)
}
})
)
const receiveBotMessagePromise = new Promise<Message>((resolve) =>
listener.on('message_created', (m) => {
if (m.userId !== client.user.id) {
resolve(m)
}
})
)
const [sentMessage, receivedSelfMessage, receivedBotMessage] = await Promise.all([
sendMessagePromise,
receiveSelfMessagePromise,
receiveBotMessagePromise,
])
expect(sentMessage.message.metadata).toEqual(metadata)
expect(receivedSelfMessage.metadata).toEqual(metadata)
expect(receivedBotMessage.metadata).toEqual(metadata)
expect(sentMessage.message.payload).not.toHaveProperty('metadata')
expect(receivedSelfMessage.payload).not.toHaveProperty('metadata')
expect(receivedBotMessage.payload).not.toHaveProperty('metadata')
const fetchedMessages = await client.listMessages({ conversationId }).then((r) => r.messages)
const fetchedSelfMessages = fetchedMessages.filter((m) => m.userId === client.user.id)
expect(fetchedSelfMessages.length).toBe(1)
const [fetchedSelfMessage] = fetchedSelfMessages
expect(fetchedSelfMessage!.metadata).toEqual(metadata)
})
test.each(serverEventsProtocols)('api allows sending bloc messages', async (protocol) => {
const client = await chat.Client.connect({ apiUrl })
const {
conversation: { id: conversationId },
} = await client.createConversation({})
await checkApiCanSendAndReceiveMessages(
{
client,
conversationId,
protocol,
},
{
type: 'bloc',
items: [
{
type: 'text',
text: 'hello world',
},
{
type: 'image',
imageUrl: 'https://fastly.picsum.photos/id/329/200/300.jpg?hmac=_yLyj0EqdpQ-cX84OlMxz3YzOjjd7liq6b25ldkVSpA',
},
],
}
)
})
test.each(serverEventsProtocols)('api allows receiving bloc messages from bot', async (protocol) => {
const client = await chat.Client.connect({ apiUrl })
const {
conversation: { id: conversationId },
} = await client.createConversation({})
const responsePayload: MessagePayload = await checkApiCanSendAndReceiveMessages(
{ client, conversationId, protocol },
{ type: 'text', text: 'bloc' }
)
expect(responsePayload.type).toEqual('bloc')
expect(responsePayload.items.length).toEqual(3)
})
@@ -0,0 +1,161 @@
import { expect, test } from 'vitest'
import _ from 'lodash'
import * as utils from './utils'
import * as config from './config'
import * as chat from '../src'
const apiUrl = config.get('API_URL')
const protocols = ['sse', 'websocket'] as const
test.each(protocols)('api allows adding and removing conversation participants', async (protocol) => {
const client = new chat.Client({ apiUrl })
const { key: userKey1 } = await client.createUser({})
const { user: user2, key: userKey2 } = await client.createUser({})
const { conversation } = await client.createConversation({ 'x-user-key': userKey1 })
const listener = await client.listenConversation({ id: conversation.id, 'x-user-key': userKey1, protocol })
await expect(
client.createMessage({
'x-user-key': userKey2,
conversationId: conversation.id,
payload: {
type: 'text',
text: 'hello world',
},
})
).rejects.toThrow(chat.ForbiddenError)
await Promise.all([
utils.waitFor(listener, 'participant_added'),
client.addParticipant({
'x-user-key': userKey1,
conversationId: conversation.id,
userId: user2.id,
}),
])
await expect(
client.createMessage({
'x-user-key': userKey2,
conversationId: conversation.id,
payload: {
type: 'text',
text: 'hello world',
},
})
).resolves.toBeTruthy()
await Promise.all([
utils.waitFor(listener, 'participant_removed'),
client.removeParticipant({
'x-user-key': userKey1,
conversationId: conversation.id,
userId: user2.id,
}),
])
await expect(
client.createMessage({
'x-user-key': userKey2,
conversationId: conversation.id,
payload: {
type: 'text',
text: 'hello world',
},
})
).rejects.toThrow(chat.ForbiddenError)
})
test('api forbids non-participants from listing participants', async () => {
const client = new chat.Client({ apiUrl })
const user1 = await client.createUser({})
const user2 = await client.createUser({})
const user3 = await client.createUser({})
const { conversation } = await client.createConversation({ 'x-user-key': user1.key })
await client.addParticipant({ 'x-user-key': user1.key, conversationId: conversation.id, userId: user2.user.id })
const promise1 = client.listParticipants({ 'x-user-key': user1.key, conversationId: conversation.id })
await expect(promise1).resolves.toBeTruthy()
const promise2 = client.listParticipants({ 'x-user-key': user2.key, conversationId: conversation.id })
await expect(promise2).resolves.toBeTruthy()
const promise3 = client.listParticipants({ 'x-user-key': user3.key, conversationId: conversation.id })
await expect(promise3).rejects.toThrow(chat.ForbiddenError)
})
test.each(protocols)('signal listener is disconnected when participant is removed', async (protocol) => {
const client = new chat.Client({ apiUrl })
const { user: user1, key: userKey1 } = await client.createUser({})
const { user: user2, key: userKey2 } = await client.createUser({})
const { conversation } = await client.createConversation({ 'x-user-key': userKey1 })
await client.addParticipant({ 'x-user-key': userKey1, conversationId: conversation.id, userId: user2.id })
const listener1 = await client.listenConversation({ id: conversation.id, 'x-user-key': userKey1, protocol })
const listener2 = await client.listenConversation({ id: conversation.id, 'x-user-key': userKey2, protocol })
const messages1: chat.Signals['message_created'][] = []
const messages2: chat.Signals['message_created'][] = []
listener1.on('message_created', (message) => messages1.push(message))
listener2.on('message_created', (message) => messages2.push(message))
await Promise.all([
utils.waitFor(listener1, 'message_created'),
client.createMessage({
'x-user-key': userKey1,
conversationId: conversation.id,
payload: {
type: 'text',
text: 'foo',
},
}),
])
await client.removeParticipant({ 'x-user-key': userKey1, conversationId: conversation.id, userId: user2.id })
await Promise.all([
utils.waitFor(listener1, 'message_created'),
client.createMessage({
'x-user-key': userKey1,
conversationId: conversation.id,
payload: {
type: 'text',
text: 'bar',
},
}),
])
const incomingMessages1 = messages1.filter((m) => !m.isBot)
const incomingMessages2 = messages2.filter((m) => !m.isBot)
expect(incomingMessages1.length).toBe(2)
expect(incomingMessages2.length).toBe(1)
await listener1.disconnect()
listener1.cleanup()
await listener2.disconnect()
listener2.cleanup()
})
test('api forbids removing owner from conversation participants', async () => {
const client = new chat.Client({ apiUrl })
const { user: user1, key: userKey1 } = await client.createUser({})
const { conversation } = await client.createConversation({ 'x-user-key': userKey1 })
await expect(
client.removeParticipant({
'x-user-key': userKey1,
conversationId: conversation.id,
userId: user1.id,
})
).rejects.toThrow(chat.InvalidPayloadError)
})
+100
View File
@@ -0,0 +1,100 @@
import { expect, test } from 'vitest'
import _ from 'lodash'
import * as utils from './utils'
import * as config from './config'
import * as chat from '../src'
const apiUrl = config.get('API_URL')
test('api prevents creating user with an invalid fid', async () => {
const client = new chat.Client({ apiUrl })
const userFid = utils.getUserFid()
const invalidUserIds = [
`invalid+${userFid}`, // invalid character
`invalid_${userFid}_${userFid}_${userFid}_${userFid}`, // too long
]
for (const id of invalidUserIds) {
await expect(client.createUser({ id })).rejects.toThrow(chat.InvalidPayloadError)
}
for (const id of invalidUserIds) {
const key = await utils.getUserKey(id)
await expect(client.getOrCreateUser({ 'x-user-key': key })).rejects.toThrow(chat.InvalidPayloadError)
}
})
test('api prevents creating multiple users with the same foreign id', async () => {
const client = new chat.Client({ apiUrl })
const userFid = utils.getUserFid()
const createUser = () =>
client.createUser({
id: userFid,
})
await createUser()
await expect(createUser).rejects.toThrow(chat.AlreadyExistsError)
})
test('get or create user should create a user', async () => {
const client = new chat.Client({ apiUrl })
const userFid = utils.getUserFid()
const userKey = await utils.getUserKey(userFid)
await client.getOrCreateUser({ 'x-user-key': userKey })
const {
user: { id: userId },
} = await client.getOrCreateUser({ 'x-user-key': userKey })
expect(userId).toBeDefined()
})
test('get or create user always returns the same user', async () => {
const client = new chat.Client({ apiUrl })
const { key: userKey } = await client.createUser({})
const {
user: { id: userId },
} = await client.getUser({
'x-user-key': userKey,
})
const getOrCreate = () =>
client
.getOrCreateUser({
'x-user-key': userKey,
})
.then((r) => r.user.id)
expect(await getOrCreate()).toBe(userId)
expect(await getOrCreate()).toBe(userId) // operation is idempotent
})
test('get or create user with a fid always returns the same user', async () => {
const client = new chat.Client({ apiUrl })
const userFid = utils.getUserFid()
const userKey = await utils.getUserKey(userFid)
await client.createUser({ id: userFid })
const {
user: { id: userId },
} = await client.getUser({
'x-user-key': userKey,
})
const getOrCreate = () =>
client
.getOrCreateUser({
'x-user-key': userKey,
})
.then((r) => r.user.id)
expect(await getOrCreate()).toBe(userId)
expect(await getOrCreate()).toBe(userId) // operation is idempotent
})
+23
View File
@@ -0,0 +1,23 @@
import * as uuid from 'uuid'
import * as chat from '../src'
import { signJwt } from '../src/jwt'
import * as config from './config'
const encryptionKey = config.get('ENCRYPTION_KEY')
export const halfId = () => {
const id = uuid.v4()
return id.slice(0, id.length / 2)
}
export const getUserFid = (): string => `test-user-${halfId()}`
export const getConversationFid = (): string => `test-conversation-${halfId()}`
export const getUserKey = (userId: string): Promise<string> => signJwt({ id: userId }, encryptionKey)
export const waitFor = <S extends keyof chat.Signals>(
listener: chat.SignalListener,
signal: S
): Promise<chat.Signals[S]> =>
new Promise((resolve, reject) => {
listener.once(signal, resolve)
listener.once('error', reject)
})
+12
View File
@@ -0,0 +1,12 @@
import { expect, test } from 'vitest'
import _ from 'lodash'
import * as config from './config'
import * as chat from '../src'
const invalidApiUrl = config.get('API_URL') + '1234'
test('client throws if webhook id is incorrect', async () => {
const client = new chat.Client({ apiUrl: invalidApiUrl })
const userPromise = client.createUser({ id: 'invalid' })
expect(userPromise).rejects.toThrow(chat.ChatClientError)
})