Sendi is a Webex to Twilio SMS gateway written in TypeScript that runs directly on Node 22.18+ via native type-stripping (no bundler/transpiler). Each Webex space represents one external SMS contact; inbound MMS is proxied both ways, with delivery-status cards for outbound sends. Highlights: - Express 5 HTTP surface for Twilio /sms and /callback (signature-validated) - webex-node-bot-framework in websocket transport mode for bot control plane - Prisma 6 + SQLite via better-sqlite3 driver adapter - Zod-validated env with fail-fast startup and pino secret redaction - Pino logging: pretty in dev, structured JSON in prod - 13 vitest unit tests (phone normalization, Twilio signature validation, card action dispatch) - Native --require preload patches Node 22's read-only globalThis.navigator so @webex/internal-media-core (transitive) can load Co-authored-by: Cursor <cursoragent@cursor.com>
108 lines
2.7 KiB
Text
108 lines
2.7 KiB
Text
generator client {
|
|
provider = "prisma-client-js"
|
|
previewFeatures = ["driverAdapters"]
|
|
}
|
|
|
|
datasource db {
|
|
provider = "sqlite"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
model Space {
|
|
id String @id @default(uuid())
|
|
roomId String @unique
|
|
smsPhone String
|
|
wbxTmPhone String
|
|
smsName String
|
|
title String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
messages Message[]
|
|
bulkJobs BulkJob[]
|
|
surveys Survey[]
|
|
pendingMessages PendingMessage[]
|
|
|
|
@@index([smsPhone, wbxTmPhone])
|
|
}
|
|
|
|
model Contact {
|
|
id String @id @default(uuid())
|
|
name String
|
|
phone String @unique
|
|
blocked Boolean @default(false)
|
|
blockReason String?
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model Message {
|
|
id String @id @default(uuid())
|
|
sid String? @unique
|
|
roomId String
|
|
from String // person email or phone
|
|
body String
|
|
mediaUrls String // JSON stringified array e.g. '["url1", "url2"]'
|
|
status String // sent, delivered, failed, undelivered, etc.
|
|
createdAt DateTime @default(now())
|
|
|
|
space Space? @relation(fields: [roomId], references: [roomId], onDelete: Cascade)
|
|
|
|
@@index([roomId])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
model BulkJob {
|
|
id String @id @default(uuid())
|
|
parentId String
|
|
senderEmail String
|
|
text String
|
|
files String // JSON stringified array of file URLs/paths
|
|
status String @default("pending")
|
|
analytics Json?
|
|
sentAt DateTime?
|
|
completedAt DateTime?
|
|
spaceId String?
|
|
space Space? @relation(fields: [spaceId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model Survey {
|
|
id String @id @default(uuid())
|
|
roomId String
|
|
question String
|
|
endDate DateTime
|
|
responses String // JSON stringified array of {phone, response}
|
|
completed Boolean @default(false)
|
|
|
|
space Space? @relation(fields: [roomId], references: [roomId], onDelete: Cascade)
|
|
}
|
|
|
|
model OutboundRouting {
|
|
email String @id
|
|
phoneNumber String
|
|
groups String // JSON stringified array e.g. '["ottawa", "hazleton"]'
|
|
}
|
|
|
|
model InboundRouting {
|
|
phoneNumber String @id
|
|
site String
|
|
people String // JSON stringified array
|
|
keywords Json?
|
|
outOfOffice Json?
|
|
}
|
|
|
|
model BlockNumber {
|
|
phoneNumber String @id
|
|
name String
|
|
reason String
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
// Tracks SMS sends that are waiting for the user to upload an attachment in Webex.
|
|
// Used by the PendingMessageStore service. One row per room (overwrites previous).
|
|
model PendingMessage {
|
|
roomId String @id
|
|
text String
|
|
createdAt DateTime @default(now())
|
|
|
|
space Space? @relation(fields: [roomId], references: [roomId], onDelete: Cascade)
|
|
}
|