├── .nvmrc ├── .husky └── pre-commit ├── .npmrc ├── packages ├── utils │ ├── ms.ts │ ├── tsconfig.json │ ├── sanitizers.ts │ ├── uiColors.ts │ ├── spaces.ts │ ├── package.json │ ├── password.ts │ ├── zodSchemas.ts │ └── dns │ │ ├── txtParsers.ts │ │ └── index.ts ├── hono │ ├── tsconfig.json │ ├── src │ │ └── helpers.ts │ └── package.json ├── otel │ ├── tsconfig.json │ ├── exports.ts │ ├── index.ts │ ├── logger.ts │ ├── helpers.ts │ ├── env.ts │ ├── package.json │ └── hono.ts ├── database │ ├── tsconfig.json │ ├── dbSeed.ts │ ├── drizzle.config.ts │ ├── migrations │ │ ├── 0001_flimsy_mercury.sql │ │ └── meta │ │ │ └── _journal.json │ ├── env.ts │ ├── orm.ts │ ├── migrate.ts │ ├── package.json │ ├── dbClean.ts │ └── index.ts ├── tiptap │ ├── utils │ │ ├── store.ts │ │ └── atoms.ts │ ├── tsconfig.json │ ├── react.ts │ ├── index.ts │ ├── components │ │ ├── index.ts │ │ ├── editor-bubble-item.tsx │ │ └── editor-command-item.tsx │ └── package.json ├── eslint-plugin │ ├── index.js │ ├── package.json │ └── rules │ │ └── table-needs-org-id.js └── realtime │ ├── tsconfig.json │ ├── package.json │ └── events.ts ├── tsconfig.json ├── apps ├── storage │ ├── tsconfig.json │ ├── ctx.ts │ ├── tracing.ts │ ├── s3.ts │ ├── tsup.config.ts │ ├── package.json │ ├── storage.ts │ ├── env.ts │ ├── proxy │ │ └── avatars.ts │ ├── api │ │ ├── mailfetch.ts │ │ ├── deleteAttachments.ts │ │ └── internalPresign.ts │ └── app.ts ├── worker │ ├── tsconfig.json │ ├── ctx.ts │ ├── trpc │ │ ├── index.ts │ │ ├── trpc.ts │ │ └── routers │ │ │ └── jobs-router.ts │ ├── tracing.ts │ ├── env.ts │ ├── functions │ │ └── cleanup-expired-sessions.ts │ ├── services │ │ ├── expired-session-cleanup.ts │ │ └── dns-check-queue.ts │ ├── tsup.config.ts │ ├── package.json │ ├── middlewares.ts │ ├── utils │ │ └── queue-helpers.ts │ └── app.ts ├── mail-bridge │ ├── tsconfig.json │ ├── postal-routes │ │ ├── events.ts │ │ ├── inbound.ts │ │ └── signature-middleware.ts │ ├── tracing.ts │ ├── utils │ │ ├── purify.ts │ │ ├── queue-helpers.ts │ │ └── tiptap-utils.ts │ ├── ctx.ts │ ├── tsup.config.ts │ ├── postal-db │ │ └── index.ts │ ├── trpc │ │ ├── routers │ │ │ └── smtpRouter.ts │ │ ├── index.ts │ │ └── trpc.ts │ ├── smtp │ │ ├── sendEmail.ts │ │ └── auth.ts │ ├── package.json │ └── types.ts ├── web │ ├── public │ │ └── logo.png │ ├── src │ │ ├── fonts │ │ │ └── CalSans-SemiBold.woff2 │ │ ├── app │ │ │ ├── [orgShortcode] │ │ │ │ ├── [spaceShortcode] │ │ │ │ │ ├── convo │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ │ ├── new │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── [convoId] │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ └── welcome │ │ │ │ │ │ │ ├── _components │ │ │ │ │ │ │ ├── welcome-message.tsx │ │ │ │ │ │ │ └── welcome-messages.tsx │ │ │ │ │ │ │ └── _data │ │ │ │ │ │ │ └── welcomeMessages.tsx │ │ │ │ │ ├── settings │ │ │ │ │ │ └── _components │ │ │ │ │ │ │ └── settingsTitle.tsx │ │ │ │ │ └── route.ts │ │ │ │ ├── _components │ │ │ │ │ ├── atoms.ts │ │ │ │ │ └── claim-email-identity.tsx │ │ │ │ ├── convo │ │ │ │ │ ├── [convoId] │ │ │ │ │ │ ├── atoms.ts │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── atoms.ts │ │ │ │ │ ├── new │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── _components │ │ │ │ │ │ └── convo-list.tsx │ │ │ │ ├── route.ts │ │ │ │ └── settings │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── org │ │ │ │ │ ├── users │ │ │ │ │ │ ├── teams │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── members │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ └── invites │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── mail │ │ │ │ │ │ ├── domains │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ └── addresses │ │ │ │ │ │ ├── _components │ │ │ │ │ │ └── columns.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── _components │ │ │ │ │ └── page-title.tsx │ │ │ └── join │ │ │ │ ├── invite │ │ │ │ └── [code] │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ └── _components │ │ │ │ └── stepper.tsx │ │ ├── components │ │ │ ├── shadcn-ui │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── progress.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── slider.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── hover-card.tsx │ │ │ │ ├── tooltip.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── scroll-area.tsx │ │ │ │ └── toggle-group.tsx │ │ │ ├── smart-date-time.tsx │ │ │ ├── posthog-page-view.tsx │ │ │ ├── shared │ │ │ │ ├── strength-meter.tsx │ │ │ │ └── editable-text.tsx │ │ │ ├── turnstile.tsx │ │ │ ├── copy-button.tsx │ │ │ └── password-input.tsx │ │ ├── hooks │ │ │ ├── use-page-title.ts │ │ │ ├── use-is-mobile.ts │ │ │ └── use-time-ago.ts │ │ ├── instrumentation.ts │ │ ├── providers │ │ │ ├── posthog-provider.tsx │ │ │ └── realtime-provider.tsx │ │ ├── lib │ │ │ ├── upload.ts │ │ │ └── middleware-utils.ts │ │ └── middleware.ts │ ├── postcss.config.js │ ├── components.json │ ├── .gitignore │ ├── tsconfig.json │ └── next.config.js └── platform │ ├── trpc │ └── routers │ │ ├── spaceRouter │ │ └── membersRouter.ts │ │ ├── contactRouter │ │ └── contactRouter.ts │ │ └── orgRouter │ │ ├── iCanHaz │ │ └── iCanHazRouter.ts │ │ └── setup │ │ └── profileRouter.ts │ ├── tsconfig.json │ ├── tracing.ts │ ├── utils │ ├── cookieNames.ts │ ├── account.ts │ ├── tiptap-utils.ts │ ├── session.ts │ └── tRPCServerClients.ts │ ├── tsup.config.ts │ ├── routes │ ├── services.ts │ └── auth.ts │ ├── ctx.ts │ ├── package.json │ ├── app.ts │ └── middlewares.ts ├── ee ├── apps │ ├── billing │ │ ├── tsconfig.json │ │ ├── ctx.ts │ │ ├── trpc │ │ │ ├── index.ts │ │ │ └── trpc.ts │ │ ├── stripe.ts │ │ ├── tsup.config.ts │ │ ├── env.ts │ │ ├── package.json │ │ ├── middlewares.ts │ │ ├── app.ts │ │ └── validateLicenseKey.ts │ └── command │ │ ├── public │ │ └── favicon.ico │ │ ├── postcss.config.js │ │ ├── src │ │ ├── lib │ │ │ ├── utils.ts │ │ │ └── get-account.ts │ │ ├── middleware.ts │ │ ├── server │ │ │ └── trpc │ │ │ │ ├── index.ts │ │ │ │ ├── routers │ │ │ │ └── internalRouter.ts │ │ │ │ └── trpc.ts │ │ ├── app │ │ │ ├── api │ │ │ │ └── trpc │ │ │ │ │ └── [trpc] │ │ │ │ │ └── route.ts │ │ │ ├── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── remove-expired-sessions │ │ │ │ └── page.tsx │ │ │ └── unin │ │ │ │ └── page.tsx │ │ ├── components │ │ │ └── ui │ │ │ │ ├── label.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── input.tsx │ │ │ │ └── button.tsx │ │ └── styles │ │ │ └── globals.css │ │ ├── next.config.js │ │ ├── components.json │ │ ├── tsconfig.json │ │ └── package.json └── README.md ├── .eslintignore ├── .prettierignore ├── .infisical.json ├── pnpm-workspace.yaml ├── .editorconfig ├── .vscode ├── uninbox-code-snippets.code-snippets ├── extensions.json ├── cspell.json └── settings.json ├── .github ├── CODEOWNERS ├── workflows │ ├── autofix.yml │ └── check_and_build_pull_requests.yaml └── pull_request_template.md ├── prettier.config.cjs ├── .gitignore ├── predev.ts ├── turbo.json └── .eslintrc.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.15 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true -------------------------------------------------------------------------------- /packages/utils/ms.ts: -------------------------------------------------------------------------------- 1 | export * from 'itty-time'; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { "extends": "@u22n/tsconfig" } 2 | -------------------------------------------------------------------------------- /apps/storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "extends": "@u22n/tsconfig" } 2 | -------------------------------------------------------------------------------- /apps/worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "extends": "@u22n/tsconfig" } 2 | -------------------------------------------------------------------------------- /packages/hono/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "extends": "@u22n/tsconfig" } 2 | -------------------------------------------------------------------------------- /packages/otel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "extends": "@u22n/tsconfig" } 2 | -------------------------------------------------------------------------------- /apps/mail-bridge/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "extends": "@u22n/tsconfig" } 2 | -------------------------------------------------------------------------------- /ee/apps/billing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "extends": ["@u22n/tsconfig"] } 2 | -------------------------------------------------------------------------------- /packages/database/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "extends": "@u22n/tsconfig" } 2 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "extends": "@u22n/tsconfig" } 2 | -------------------------------------------------------------------------------- /packages/otel/exports.ts: -------------------------------------------------------------------------------- 1 | export { flatten, unflatten } from 'flat'; 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # other artefacts 2 | .output 3 | node_modules 4 | dist 5 | public 6 | -------------------------------------------------------------------------------- /apps/web/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/inbox/HEAD/apps/web/public/logo.png -------------------------------------------------------------------------------- /apps/worker/ctx.ts: -------------------------------------------------------------------------------- 1 | import type { HonoContext } from '@u22n/hono'; 2 | 3 | export type Ctx = HonoContext; 4 | -------------------------------------------------------------------------------- /ee/apps/command/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/inbox/HEAD/ee/apps/command/public/favicon.ico -------------------------------------------------------------------------------- /packages/otel/index.ts: -------------------------------------------------------------------------------- 1 | import { env } from './env'; 2 | export const opentelemetryEnabled = env.OTEL_ENABLED; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .turbo 3 | .pnpm-store 4 | pnpm-lock.yaml 5 | .next 6 | packages/database/migrations -------------------------------------------------------------------------------- /packages/tiptap/utils/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'jotai'; 2 | 3 | export const editorStore = createStore(); 4 | -------------------------------------------------------------------------------- /apps/web/src/fonts/CalSans-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/un/inbox/HEAD/apps/web/src/fonts/CalSans-SemiBold.woff2 -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {} 4 | } 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /ee/apps/command/postcss.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {} 4 | } 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/[spaceShortcode]/convo/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Page from '../../convo/page'; 3 | export default Page; 4 | -------------------------------------------------------------------------------- /packages/utils/sanitizers.ts: -------------------------------------------------------------------------------- 1 | export function sanitizeFilename(filename: string) { 2 | return filename.replace(/[^a-zA-Z0-9-_.]/g, '_'); 3 | } 4 | -------------------------------------------------------------------------------- /packages/eslint-plugin/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'table-needs-org-id': require('./rules/table-needs-org-id') 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /.infisical.json: -------------------------------------------------------------------------------- 1 | { 2 | "workspaceId": "6488789573de3b388fec7d44", 3 | "defaultEnvironment": "local", 4 | "gitBranchToEnvironmentMapping": null 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/[spaceShortcode]/convo/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Layout from '../../convo/layout'; 3 | export default Layout; 4 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/[spaceShortcode]/convo/new/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Page from '../../../convo/new/page'; 3 | export default Page; 4 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/_components/atoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | 3 | export const sidebarSubmenuOpenAtom = atom(false); 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'apps/*' 4 | - 'layers/*' 5 | - 'ee/packages/*' 6 | - 'ee/apps/*' 7 | - 'ee/layers/*' 8 | -------------------------------------------------------------------------------- /apps/platform/trpc/routers/spaceRouter/membersRouter.ts: -------------------------------------------------------------------------------- 1 | import { router } from '~platform/trpc/trpc'; 2 | 3 | export const spaceMembersRouter = router({}); 4 | -------------------------------------------------------------------------------- /apps/platform/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@u22n/tsconfig", 3 | "compilerOptions": { 4 | "paths": { 5 | "~platform/*": ["./*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/[spaceShortcode]/convo/[convoId]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Page from '../../../convo/[convoId]/page'; 3 | export default Page; 4 | -------------------------------------------------------------------------------- /packages/realtime/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@u22n/tsconfig", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "jsx": "preserve" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/tiptap/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@u22n/tsconfig", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ee/apps/billing/ctx.ts: -------------------------------------------------------------------------------- 1 | import type { HonoContext } from '@u22n/hono'; 2 | import type Stripe from 'stripe'; 3 | 4 | export type Ctx = HonoContext<{ 5 | stripeEvent: Stripe.Event; 6 | }>; 7 | -------------------------------------------------------------------------------- /packages/eslint-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@u22n/eslint-plugin-custom", 3 | "version": "1.0.0", 4 | "description": "ESLint plugin for the Inbox monorepo", 5 | "main": "index.js" 6 | } 7 | -------------------------------------------------------------------------------- /packages/tiptap/utils/atoms.ts: -------------------------------------------------------------------------------- 1 | import type { Range } from '@tiptap/core'; 2 | import { atom } from 'jotai'; 3 | 4 | export const queryAtom = atom(''); 5 | export const rangeAtom = atom(null); 6 | -------------------------------------------------------------------------------- /packages/tiptap/react.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This entire package is a modified version of steven-tey/novel modified to our needs 3 | * https://github.com/steven-tey/novel 4 | * 5 | */ 6 | 7 | export * from '@tiptap/react'; 8 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/[spaceShortcode]/settings/_components/settingsTitle.tsx: -------------------------------------------------------------------------------- 1 | export function SettingsTitle({ title }: { title: string }) { 2 | return {title}; 3 | } 4 | -------------------------------------------------------------------------------- /ee/apps/command/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/src/app/join/invite/[code]/page.tsx: -------------------------------------------------------------------------------- 1 | import InviteCard from './_components/invite-card'; 2 | 3 | export default function Page({ params }: { params: { code: string } }) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /packages/hono/src/helpers.ts: -------------------------------------------------------------------------------- 1 | export { setCookie, getCookie, deleteCookie } from 'hono/cookie'; 2 | export { createMiddleware } from 'hono/factory'; 3 | export { zValidator } from '@hono/zod-validator'; 4 | export type { Context } from 'hono'; 5 | -------------------------------------------------------------------------------- /apps/storage/ctx.ts: -------------------------------------------------------------------------------- 1 | import type { HonoContext } from '@u22n/hono'; 2 | import type { Session } from './storage'; 3 | 4 | export type Ctx = HonoContext<{ 5 | account: { 6 | id: number; 7 | session: Session; 8 | } | null; 9 | }>; 10 | -------------------------------------------------------------------------------- /apps/worker/trpc/index.ts: -------------------------------------------------------------------------------- 1 | import { jobsRouter } from './routers/jobs-router'; 2 | import { router } from './trpc'; 3 | 4 | export const workerRouter = router({ 5 | jobs: jobsRouter 6 | }); 7 | 8 | export type TrpcWorkerRouter = typeof workerRouter; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /apps/mail-bridge/postal-routes/events.ts: -------------------------------------------------------------------------------- 1 | import { createHonoApp } from '@u22n/hono'; 2 | 3 | export const eventApi = createHonoApp().post( 4 | '/events/:params{.+}', 5 | async (c) => { 6 | return c.json({ error: 'Not implemented' }, 400); 7 | } 8 | ); 9 | -------------------------------------------------------------------------------- /packages/database/dbSeed.ts: -------------------------------------------------------------------------------- 1 | // //! IMPORTANT: This file is outdated and needs to be updated to the new schema 2 | // //! IMPORTANT: This file is only for development purposes, it should not be used in production 3 | // //! CHECK THE COMMENTS FOR REQUIRED FIELDS 4 | -------------------------------------------------------------------------------- /apps/worker/tracing.ts: -------------------------------------------------------------------------------- 1 | import { opentelemetryEnabled } from '@u22n/otel'; 2 | import { name, version } from './package.json'; 3 | 4 | if (opentelemetryEnabled) { 5 | const { setupOpentelemetry } = await import('@u22n/otel/setup'); 6 | setupOpentelemetry({ name, version }); 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/uninbox-code-snippets.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "DrizzleORM Ops": { 3 | "prefix": "orm", 4 | "description": "Import the conditional operators from Drizzle ORM via @u22n/database", 5 | "scope": "", 6 | "body": ["import { $1 } from '@u22n/database/orm'"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/mail-bridge/tracing.ts: -------------------------------------------------------------------------------- 1 | import { opentelemetryEnabled } from '@u22n/otel'; 2 | import { name, version } from './package.json'; 3 | 4 | if (opentelemetryEnabled) { 5 | const { setupOpentelemetry } = await import('@u22n/otel/setup'); 6 | setupOpentelemetry({ name, version }); 7 | } 8 | -------------------------------------------------------------------------------- /apps/platform/tracing.ts: -------------------------------------------------------------------------------- 1 | import { opentelemetryEnabled } from '@u22n/otel'; 2 | import { name, version } from './package.json'; 3 | 4 | if (opentelemetryEnabled) { 5 | const { setupOpentelemetry } = await import('@u22n/otel/setup'); 6 | setupOpentelemetry({ name, version }); 7 | } 8 | -------------------------------------------------------------------------------- /apps/storage/tracing.ts: -------------------------------------------------------------------------------- 1 | import { opentelemetryEnabled } from '@u22n/otel'; 2 | import { name, version } from './package.json'; 3 | 4 | if (opentelemetryEnabled) { 5 | const { setupOpentelemetry } = await import('@u22n/otel/setup'); 6 | setupOpentelemetry({ name, version }); 7 | } 8 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 2 | 3 | # These owners will be the default owners for everything in 4 | # the repo. Unless a later match takes precedence, 5 | * @mcpizza0 6 | -------------------------------------------------------------------------------- /packages/tiptap/index.ts: -------------------------------------------------------------------------------- 1 | export * as tiptapCore from '@tiptap/core'; 2 | export * as tiptapHtml from '@tiptap/html'; 3 | export * as tiptapStarterKit from '@tiptap/starter-kit'; 4 | 5 | export const emptyTiptapEditorContent = { 6 | type: 'doc', 7 | content: [{ type: 'paragraph' }] 8 | }; 9 | -------------------------------------------------------------------------------- /apps/web/src/app/join/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export default function Page({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
6 | {children} 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/mail-bridge/utils/purify.ts: -------------------------------------------------------------------------------- 1 | import DOMPurify from 'dompurify'; 2 | import { JSDOM } from 'jsdom'; 3 | 4 | const window = new JSDOM('').window; 5 | const purify = DOMPurify(window); 6 | 7 | export const sanitize = (html: string) => 8 | purify.sanitize(html, { USE_PROFILES: { html: true } }); 9 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/convo/[convoId]/atoms.ts: -------------------------------------------------------------------------------- 1 | import { type TypeId } from '@u22n/utils/typeid'; 2 | import { atom } from 'jotai'; 3 | 4 | export const replyToMessageAtom = atom | null>(null); 5 | export const emailIdentityAtom = atom | null>(null); 6 | -------------------------------------------------------------------------------- /packages/database/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'drizzle-kit'; 2 | import { env } from './env'; 3 | 4 | export default defineConfig({ 5 | schema: './schema.ts', 6 | out: './migrations', 7 | dialect: 'mysql', 8 | dbCredentials: { 9 | url: env.DB_MYSQL_MIGRATION_URL 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/route.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from 'next/server'; 2 | import { redirect } from 'next/navigation'; 3 | 4 | export function GET( 5 | _: NextRequest, 6 | { params }: { params: { orgShortcode: string } } 7 | ) { 8 | redirect(`/${params.orgShortcode}/personal/convo`); 9 | } 10 | -------------------------------------------------------------------------------- /packages/database/migrations/0001_flimsy_mercury.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `account_credentials`;--> statement-breakpoint 2 | DROP INDEX `provider_account_id_idx` ON `authenticators`;--> statement-breakpoint 3 | ALTER TABLE `accounts` DROP COLUMN `pre_account`;--> statement-breakpoint 4 | ALTER TABLE `authenticators` DROP COLUMN `account_credential_id`; -------------------------------------------------------------------------------- /apps/worker/trpc/trpc.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from '@trpc/server'; 2 | import SuperJSON from 'superjson'; 3 | 4 | const trpcContext = initTRPC.context().create({ transformer: SuperJSON }); 5 | 6 | // We don't need auth as Hono middleware handles it 7 | export const procedure = trpcContext.procedure; 8 | 9 | export const router = trpcContext.router; 10 | -------------------------------------------------------------------------------- /apps/platform/utils/cookieNames.ts: -------------------------------------------------------------------------------- 1 | export const COOKIE_SESSION = 'un-session'; 2 | export const COOKIE_ELEVATED_TOKEN = 'un-elevated-token'; 3 | export const COOKIE_PASSKEY_CHALLENGE = 'un-passkey-challenge'; 4 | export const COOKIE_TWO_FACTOR_RESET_CHALLENGE = 'un-2fa-reset-challenge'; 5 | export const COOKIE_TWO_FACTOR_LOGIN_CHALLENGE = 'un-2fa-login-challenge'; 6 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/[spaceShortcode]/route.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from 'next/server'; 2 | import { redirect } from 'next/navigation'; 3 | 4 | export function GET( 5 | _: NextRequest, 6 | { params }: { params: { orgShortcode: string; spaceShortcode: string } } 7 | ) { 8 | redirect(`/${params.orgShortcode}/${params.spaceShortcode}/convo`); 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/src/lib/utils'; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-page-title.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export function usePageTitle(title?: string) { 4 | useEffect(() => { 5 | if (typeof window === 'undefined' || !title) return; 6 | const prevTitle = document.title; 7 | document.title = title; 8 | return () => { 9 | document.title = prevTitle; 10 | }; 11 | }, [title]); 12 | } 13 | -------------------------------------------------------------------------------- /apps/mail-bridge/ctx.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from '@u22n/hono/helpers'; 2 | import type { HonoContext } from '@u22n/hono'; 3 | import type { db } from '@u22n/database'; 4 | import type { env } from './env'; 5 | 6 | export type Ctx = HonoContext; 7 | 8 | export type TRPCContext = { 9 | auth: boolean; 10 | db: typeof db; 11 | config: typeof env; 12 | context: Context; 13 | }; 14 | -------------------------------------------------------------------------------- /ee/apps/command/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | webpack: (config) => { 4 | config.externals.push('@node-rs/argon2', '@node-rs/bcrypt'); 5 | return config; 6 | }, 7 | typescript: { 8 | ignoreBuildErrors: true 9 | }, 10 | eslint: { 11 | ignoreDuringBuilds: true 12 | } 13 | }; 14 | 15 | export default nextConfig; 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "amirha.better-comments-2", // Comment Highlighting for important/explainer comments 4 | "yzhang.markdown-all-in-one", // Markdown support 5 | "dbaeumer.vscode-eslint", // eslint plugin 6 | "streetsidesoftware.code-spell-checker", // Spell checker with custom dict 7 | "esbenp.prettier-vscode" // Prettier plugin 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /apps/storage/s3.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from '@aws-sdk/client-s3'; 2 | import { env } from './env'; 3 | 4 | export const s3Client = new S3Client({ 5 | region: env.STORAGE_S3_REGION, 6 | endpoint: env.STORAGE_S3_ENDPOINT, 7 | forcePathStyle: true, 8 | credentials: { 9 | accessKeyId: env.STORAGE_S3_ACCESS_KEY_ID, 10 | secretAccessKey: env.STORAGE_S3_SECRET_ACCESS_KEY 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/convo/[convoId]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ConvoView, ConvoNotFound } from './_components/convo-views'; 4 | import { useCurrentConvoId } from '@/src/hooks/use-params'; 5 | 6 | export default function ConvoPage() { 7 | const convoId = useCurrentConvoId(); 8 | if (!convoId) return ; 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /ee/apps/command/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest, NextResponse } from 'next/server'; 2 | import { getAccount } from './lib/get-account'; 3 | 4 | export default async function middleware(req: NextRequest) { 5 | const account = await getAccount(req); 6 | if (!account) { 7 | return NextResponse.redirect(`${process.env.WEBAPP_URL}`); 8 | } 9 | return NextResponse.next(); 10 | } 11 | -------------------------------------------------------------------------------- /packages/database/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from '@t3-oss/env-core'; 2 | import { z } from 'zod'; 3 | 4 | export const env = createEnv({ 5 | server: { 6 | DB_PLANETSCALE_HOST: z.string().min(1), 7 | DB_PLANETSCALE_USERNAME: z.string().min(1), 8 | DB_PLANETSCALE_PASSWORD: z.string().min(1), 9 | DB_MYSQL_MIGRATION_URL: z.string().min(1) 10 | }, 11 | runtimeEnv: process.env 12 | }); 13 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | bracketSameLine: true, 4 | singleQuote: true, 5 | jsxSingleQuote: false, 6 | trailingComma: 'none', 7 | endOfLine: 'lf', 8 | semi: true, 9 | printWidth: 80, 10 | tabWidth: 2, 11 | arrowParens: 'always', 12 | plugins: ['prettier-plugin-sort-imports', 'prettier-plugin-tailwindcss'], 13 | singleAttributePerLine: true 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/convo/atoms.ts: -------------------------------------------------------------------------------- 1 | import { type TypeId } from '@u22n/utils/typeid'; 2 | import { atom } from 'jotai'; 3 | 4 | export const showNewConvoPanel = atom(false); 5 | export const convoListSelection = atom[]>([]); 6 | export const convoListSelecting = atom( 7 | (get) => get(convoListSelection).length > 0 8 | ); 9 | export const lastSelectedConvo = atom | null>(null); 10 | -------------------------------------------------------------------------------- /packages/utils/uiColors.ts: -------------------------------------------------------------------------------- 1 | export const uiColors = [ 2 | 'bronze', 3 | 'gold', 4 | 'brown', 5 | 'orange', 6 | 'tomato', 7 | 'red', 8 | 'ruby', 9 | 'crimson', 10 | 'pink', 11 | 'plum', 12 | 'purple', 13 | 'violet', 14 | 'iris', 15 | 'indigo', 16 | 'blue', 17 | 'cyan', 18 | 'teal', 19 | 'jade', 20 | 'green', 21 | 'grass' 22 | ] as const; 23 | 24 | export type UiColor = (typeof uiColors)[number]; 25 | -------------------------------------------------------------------------------- /apps/web/src/instrumentation.ts: -------------------------------------------------------------------------------- 1 | export async function register() { 2 | if (process.env.NEXT_RUNTIME === 'nodejs') { 3 | const { opentelemetryEnabled } = await import('@u22n/otel'); 4 | 5 | if (opentelemetryEnabled) { 6 | const { name, version } = await import('../package.json'); 7 | const { setupOpentelemetry } = await import('@u22n/otel/setup'); 8 | setupOpentelemetry({ name, version }); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ee/apps/command/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/database/orm.ts: -------------------------------------------------------------------------------- 1 | export { 2 | eq, 3 | ne, 4 | gt, 5 | gte, 6 | lt, 7 | lte, 8 | isNull, 9 | isNotNull, 10 | inArray, 11 | notInArray, 12 | exists, 13 | notExists, 14 | between, 15 | notBetween, 16 | like, 17 | ilike, 18 | notIlike, 19 | not, 20 | and, 21 | or, 22 | sql, 23 | asc, 24 | desc 25 | } from 'drizzle-orm'; 26 | 27 | export type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; 28 | -------------------------------------------------------------------------------- /packages/hono/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@u22n/hono", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "exports": { 6 | ".": "./src/index.ts", 7 | "./helpers": "./src/helpers.ts" 8 | }, 9 | "scripts": { 10 | "check": "tsc --noEmit" 11 | }, 12 | "dependencies": { 13 | "@hono/node-server": "^1.12.1", 14 | "@hono/trpc-server": "^0.3.2", 15 | "@hono/zod-validator": "^0.2.2", 16 | "hono": "^4.5.8" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ee/apps/command/src/server/trpc/index.ts: -------------------------------------------------------------------------------- 1 | import { internalRouter } from './routers/internalRouter'; 2 | import { accountRouter } from './routers/accountRouter'; 3 | import { orgRouter } from './routers/orgRoutes'; 4 | import { router } from './trpc'; 5 | 6 | export const trpcCommandRouter = router({ 7 | orgs: orgRouter, 8 | accounts: accountRouter, 9 | internal: internalRouter 10 | }); 11 | 12 | export type TrpcCommandRouter = typeof trpcCommandRouter; 13 | -------------------------------------------------------------------------------- /packages/database/migrate.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from 'drizzle-orm/mysql2/migrator'; 2 | import { createConnection } from 'mysql2/promise'; 3 | import { drizzle } from 'drizzle-orm/mysql2'; 4 | import { env } from './env'; 5 | 6 | const connection = await createConnection({ uri: env.DB_MYSQL_MIGRATION_URL }); 7 | const db = drizzle(connection, { mode: 'planetscale', logger: true }); 8 | await migrate(db, { migrationsFolder: 'migrations' }); 9 | await connection.end(); 10 | -------------------------------------------------------------------------------- /packages/database/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "mysql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1724678051930, 9 | "tag": "0000_cultured_cable", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "5", 15 | "when": 1725123674507, 16 | "tag": "0001_flimsy_mercury", 17 | "breakpoints": true 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/src/components", 15 | "utils": "@/src/lib/utils", 16 | "ui": "@/src/components/shadcn-ui" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/worker/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from '@t3-oss/env-core'; 2 | import { z } from 'zod'; 3 | 4 | export const env = createEnv({ 5 | server: { 6 | PRIMARY_DOMAIN: z.string(), 7 | PLATFORM_URL: z.string().url(), 8 | DB_REDIS_CONNECTION_STRING: z.string().min(1), 9 | WORKER_ACCESS_KEY: z.string().min(32), 10 | PORT: z.coerce.number().int().min(1).max(65535).default(3400), 11 | NODE_ENV: z.enum(['development', 'production']).default('development') 12 | }, 13 | runtimeEnv: process.env 14 | }); 15 | -------------------------------------------------------------------------------- /apps/worker/functions/cleanup-expired-sessions.ts: -------------------------------------------------------------------------------- 1 | import { sessions } from '@u22n/database/schema'; 2 | import { lte } from '@u22n/database/orm'; 3 | import { db } from '@u22n/database'; 4 | 5 | export async function cleanupExpiredSessions() { 6 | const now = performance.now(); 7 | const { rowsAffected } = await db 8 | .delete(sessions) 9 | .where(lte(sessions.expiresAt, new Date())); 10 | const elapsed = performance.now() - now; 11 | return { removedSessions: rowsAffected, timeElapsed: elapsed }; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/settings/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import SettingsSidebarContent from './_components/settings-sidebar'; 4 | import { useIsMobile } from '@/src/hooks/use-is-mobile'; 5 | import UserProfilePage from './user/profile/page'; 6 | 7 | export default function Page() { 8 | const isMobile = useIsMobile(); 9 | return ( 10 |
11 | {isMobile ? : } 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /ee/apps/command/src/server/trpc/routers/internalRouter.ts: -------------------------------------------------------------------------------- 1 | import { router, accountProcedure } from '../trpc'; 2 | import { sessions } from '@u22n/database/schema'; 3 | import { lte } from '@u22n/database/orm'; 4 | import { db } from '@u22n/database'; 5 | 6 | export const internalRouter = router({ 7 | removeExpiredSessions: accountProcedure.mutation(async () => { 8 | const { rowsAffected } = await db 9 | .delete(sessions) 10 | .where(lte(sessions.expiresAt, new Date())); 11 | return { count: rowsAffected }; 12 | }) 13 | }); 14 | -------------------------------------------------------------------------------- /packages/otel/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from 'winston'; 2 | import { env } from './env'; 3 | 4 | export const logger = createLogger({ 5 | level: 'info', 6 | format: format.json(), 7 | transports: [ 8 | env.NODE_ENV === 'production' 9 | ? new transports.Console() 10 | : new transports.Console({ 11 | format: format.combine( 12 | format.timestamp(), 13 | format.colorize(), 14 | format.simple() 15 | ) 16 | }) 17 | ], 18 | exitOnError: false 19 | }); 20 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import SettingsSidebarContent from './_components/settings-sidebar'; 3 | import { useIsMobile } from '@/src/hooks/use-is-mobile'; 4 | 5 | export default function Layout({ 6 | children 7 | }: Readonly<{ children: React.ReactNode }>) { 8 | const isMobile = useIsMobile(); 9 | return ( 10 |
11 | {!isMobile && } 12 |
{children}
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/platform/utils/account.ts: -------------------------------------------------------------------------------- 1 | import type { OrgContext } from '~platform/ctx'; 2 | 3 | export async function isAccountAdminOfOrg(orgContext: OrgContext) { 4 | if (!orgContext?.memberId) return false; 5 | const accountOrgMembership = orgContext?.members.find((member) => { 6 | return member.id === orgContext?.memberId; 7 | }); 8 | if (!accountOrgMembership) { 9 | return false; 10 | } 11 | if ( 12 | accountOrgMembership.role !== 'admin' || 13 | accountOrgMembership.status !== 'active' 14 | ) { 15 | return false; 16 | } 17 | return true; 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/convo/new/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import CreateConvoForm from '../_components/create-convo-form'; 3 | import { useQueryState } from 'nuqs'; 4 | 5 | export default function Page() { 6 | const [emails] = useQueryState('emails', { parse: (v) => v?.split(',') }); 7 | const [subject] = useQueryState('subject'); 8 | 9 | return ( 10 |
11 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/worker/services/expired-session-cleanup.ts: -------------------------------------------------------------------------------- 1 | import { cleanupExpiredSessions } from '../functions/cleanup-expired-sessions'; 2 | import { discord } from '@u22n/utils/discord'; 3 | import { CronJob } from 'cron'; 4 | 5 | // This CronJob will cleanup expired sessions every day at 3 UTC 6 | export const sessionCleanupCronJob = new CronJob('0 3 * * *', async () => { 7 | const { removedSessions, timeElapsed } = await cleanupExpiredSessions(); 8 | await discord.info( 9 | `Expired Session Cleanup. Removed ${removedSessions} sessions, Took ${timeElapsed}ms to complete.` 10 | ); 11 | }); 12 | -------------------------------------------------------------------------------- /ee/apps/command/src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; 2 | import { trpcCommandRouter } from '@/server/trpc'; 3 | import type { NextRequest } from 'next/server'; 4 | import { db } from '@u22n/database'; 5 | 6 | function handler(req: NextRequest) { 7 | return fetchRequestHandler({ 8 | endpoint: '/api/trpc', 9 | req, 10 | router: trpcCommandRouter, 11 | createContext: () => ({ 12 | db, 13 | account: null, 14 | event: req 15 | }) 16 | }); 17 | } 18 | 19 | export { handler as GET, handler as POST }; 20 | -------------------------------------------------------------------------------- /ee/apps/billing/trpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@trpc/server'; 2 | import { subscriptionsRouter } from './routers/subscriptionsRouter'; 3 | import { stripeLinksRouter } from './routers/stripeLinksRouter'; 4 | import { iCanHazRouter } from './routers/iCanHazRouter'; 5 | import { router } from './trpc'; 6 | 7 | const stripeRouter = router({ 8 | links: stripeLinksRouter, 9 | subscriptions: subscriptionsRouter 10 | }); 11 | 12 | export const trpcBillingRouter = router({ 13 | stripe: stripeRouter, 14 | iCanHaz: iCanHazRouter 15 | }); 16 | 17 | export type TrpcBillingRouter = typeof trpcBillingRouter; 18 | -------------------------------------------------------------------------------- /packages/tiptap/components/index.ts: -------------------------------------------------------------------------------- 1 | export { useCurrentEditor as useEditor } from '@tiptap/react'; 2 | export { type Editor as EditorInstance } from '@tiptap/core'; 3 | export type { JSONContent } from '@tiptap/react'; 4 | export { 5 | EditorRoot, 6 | EditorContent, 7 | type EditorContentProps, 8 | type EditorFunctions 9 | } from './editor'; 10 | export { EditorBubble } from './editor-bubble'; 11 | export { EditorBubbleItem } from './editor-bubble-item'; 12 | export { EditorCommand, EditorCommandList } from './editor-command'; 13 | export { EditorCommandItem, EditorCommandEmpty } from './editor-command-item'; 14 | -------------------------------------------------------------------------------- /ee/apps/billing/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe'; 2 | import { env } from './env'; 3 | 4 | export const stripeSdk = new Stripe(env.BILLING_STRIPE_KEY, { 5 | apiVersion: '2024-06-20' 6 | }); 7 | 8 | export const stripeData = { 9 | plans: { 10 | pro: { 11 | monthly: env.BILLING_STRIPE_PLAN_PRO_MONTHLY_ID, 12 | yearly: env.BILLING_STRIPE_PLAN_PRO_YEARLY_ID 13 | } 14 | }, 15 | key: env.BILLING_STRIPE_KEY, 16 | webhookKey: env.BILLING_STRIPE_WEBHOOK_KEY 17 | }; 18 | 19 | export const stripePlans = ['pro'] as const; 20 | export const stripeBillingPeriods = ['monthly', 'yearly'] as const; 21 | -------------------------------------------------------------------------------- /packages/realtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@u22n/realtime", 3 | "version": "1.0.0", 4 | "private": true, 5 | "types": "./index.ts", 6 | "main": "index.js", 7 | "type": "module", 8 | "exports": { 9 | "./client": "./client.ts", 10 | "./server": "./server.ts", 11 | "./events": "./events.ts" 12 | }, 13 | "scripts": { 14 | "check": "tsc --noEmit" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@u22n/utils": "workspace:^", 21 | "pusher": "^5.2.0", 22 | "pusher-js": "8.4.0-rc2", 23 | "zod": "^3.23.8" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/platform/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['app.ts', 'tracing.ts'], 5 | outDir: '.output', 6 | format: 'esm', 7 | target: 'esnext', 8 | clean: true, 9 | bundle: true, 10 | treeshake: true, 11 | noExternal: [/^@u22n\/.*/], 12 | minify: false, 13 | keepNames: true, 14 | banner: { 15 | js: [ 16 | `import { createRequire } from 'module';`, 17 | `const require = createRequire(import.meta.url);` 18 | ].join('\n') 19 | }, 20 | esbuildOptions: (options) => { 21 | options.legalComments = 'none'; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /apps/storage/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['app.ts', 'tracing.ts'], 5 | outDir: '.output', 6 | format: 'esm', 7 | target: 'esnext', 8 | clean: true, 9 | bundle: true, 10 | treeshake: true, 11 | noExternal: [/^@u22n\/.*/], 12 | minify: false, 13 | keepNames: true, 14 | banner: { 15 | js: [ 16 | `import { createRequire } from 'module';`, 17 | `const require = createRequire(import.meta.url);` 18 | ].join('\n') 19 | }, 20 | esbuildOptions: (options) => { 21 | options.legalComments = 'none'; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /apps/worker/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['app.ts', 'tracing.ts'], 5 | outDir: '.output', 6 | format: 'esm', 7 | target: 'esnext', 8 | clean: true, 9 | bundle: true, 10 | treeshake: true, 11 | noExternal: [/^@u22n\/.*/], 12 | minify: false, 13 | keepNames: true, 14 | banner: { 15 | js: [ 16 | `import { createRequire } from 'module';`, 17 | `const require = createRequire(import.meta.url);` 18 | ].join('\n') 19 | }, 20 | esbuildOptions: (options) => { 21 | options.legalComments = 'none'; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /apps/mail-bridge/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['app.ts', 'tracing.ts'], 5 | outDir: '.output', 6 | format: 'esm', 7 | target: 'esnext', 8 | clean: true, 9 | bundle: true, 10 | treeshake: true, 11 | noExternal: [/^@u22n\/.*/], 12 | minify: false, 13 | keepNames: true, 14 | banner: { 15 | js: [ 16 | `import { createRequire } from 'module';`, 17 | `const require = createRequire(import.meta.url);` 18 | ].join('\n') 19 | }, 20 | esbuildOptions: (options) => { 21 | options.legalComments = 'none'; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/[spaceShortcode]/convo/welcome/_components/welcome-message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface WelcomeMessageProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | export function WelcomeMessage({ children }: WelcomeMessageProps) { 8 | return ( 9 |
10 | UnInbox Team 15 |
16 |

UnInbox Team

17 | {children} 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /ee/apps/billing/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['app.ts'], 5 | outDir: '.output', 6 | format: 'esm', 7 | target: 'esnext', 8 | clean: true, 9 | bundle: true, 10 | treeshake: true, 11 | noExternal: [/^@u22n\/.*/], 12 | cjsInterop: true, 13 | shims: true, 14 | minify: false, 15 | banner: { 16 | js: [ 17 | `import { createRequire } from 'module';`, 18 | `const require = createRequire(import.meta.url);` 19 | ].join('\n') 20 | }, 21 | esbuildOptions: (options) => { 22 | options.legalComments = 'none'; 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /packages/utils/spaces.ts: -------------------------------------------------------------------------------- 1 | export const spaceTypeArray = ['open', 'private'] as const; 2 | export type SpaceType = (typeof spaceTypeArray)[number]; 3 | 4 | export const spaceMemberRoleArray = ['member', 'admin'] as const; 5 | export type SpaceMemberRole = (typeof spaceMemberRoleArray)[number]; 6 | 7 | export const spaceMemberNotificationArray = ['active', 'muted', 'off'] as const; 8 | export type SpaceMemberNotification = 9 | (typeof spaceMemberNotificationArray)[number]; 10 | 11 | export const spaceWorkflowTypeArray = ['open', 'active', 'closed'] as const; 12 | export type SpaceWorkflowType = (typeof spaceWorkflowTypeArray)[number]; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | package-lock.json 8 | 9 | # common build outputs 10 | .cache 11 | .output 12 | dist 13 | 14 | # misc 15 | .DS_Store 16 | *.pem 17 | .idea 18 | 19 | # debug 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | .pnpm-debug.log* 24 | *.log* 25 | 26 | # local env files 27 | .env 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # turbo 34 | .turbo 35 | 36 | # next.js 37 | .next 38 | out 39 | next-env.d.ts 40 | 41 | # typescript 42 | *.tsbuildinfo -------------------------------------------------------------------------------- /apps/mail-bridge/postal-db/index.ts: -------------------------------------------------------------------------------- 1 | import { activePostalServer, env } from '../env'; 2 | import { drizzle } from 'drizzle-orm/mysql2'; 3 | import mysql from 'mysql2/promise'; 4 | import * as schema from './schema'; 5 | 6 | const isLocal = env.MAILBRIDGE_LOCAL_MODE; 7 | export const connection = mysql.createPool({ 8 | uri: isLocal 9 | ? // we actually don't use the db in local mode, it is set to the local docker db to avoid throwing connection errors 10 | env.DB_MYSQL_MIGRATION_URL 11 | : `${activePostalServer.dbConnectionString}/postal`, 12 | multipleStatements: true 13 | }); 14 | 15 | export const postalDB = drizzle(connection, { schema, mode: 'default' }); 16 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/convo/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useOrgScopedRouter } from '@/src/hooks/use-params'; 3 | import { Button } from '@/src/components/shadcn-ui/button'; 4 | import { useIsMobile } from '@/src/hooks/use-is-mobile'; 5 | import Link from 'next/link'; 6 | 7 | export default function Page() { 8 | const { scopedUrl } = useOrgScopedRouter(); 9 | const isMobile = useIsMobile(); 10 | 11 | return isMobile ? null : ( 12 |
13 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /ee/apps/command/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@u22n/tsconfig", 3 | "compilerOptions": { 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [{ "name": "next" }], 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /apps/worker/trpc/routers/jobs-router.ts: -------------------------------------------------------------------------------- 1 | import { cleanupExpiredSessions } from '../../functions/cleanup-expired-sessions'; 2 | import { addImmediateDnsCheckJob } from '../../services/dns-check-queue'; 3 | import { typeIdValidator } from '@u22n/utils/typeid'; 4 | import { procedure, router } from '../trpc'; 5 | import { z } from 'zod'; 6 | 7 | export const jobsRouter = router({ 8 | cleanUpExpiredSessions: procedure.mutation(async () => { 9 | await cleanupExpiredSessions(); 10 | }), 11 | addImmediateDnsCheck: procedure 12 | .input(z.object({ domainPublicId: typeIdValidator('domains') })) 13 | .mutation(async ({ input }) => { 14 | await addImmediateDnsCheckJob(input.domainPublicId); 15 | }) 16 | }); 17 | -------------------------------------------------------------------------------- /apps/mail-bridge/trpc/routers/smtpRouter.ts: -------------------------------------------------------------------------------- 1 | import { validateSmtpCredentials } from '../../smtp/auth'; 2 | import { protectedProcedure, router } from '../trpc'; 3 | import { z } from 'zod'; 4 | 5 | export const smtpRouter = router({ 6 | validateSmtpCredentials: protectedProcedure 7 | .input( 8 | z.object({ 9 | host: z.string(), 10 | port: z.number(), 11 | username: z.string(), 12 | password: z.string(), 13 | encryption: z.enum(['none', 'ssl', 'tls', 'starttls']), 14 | authMethod: z.enum(['plain', 'login']) 15 | }) 16 | ) 17 | .query(async ({ input }) => { 18 | const result = await validateSmtpCredentials(input); 19 | 20 | return { result }; 21 | }) 22 | }); 23 | -------------------------------------------------------------------------------- /apps/web/src/components/smart-date-time.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, TooltipContent, TooltipTrigger } from './shadcn-ui/tooltip'; 2 | import { useTimeAgo } from '../hooks/use-time-ago'; 3 | import { ms } from '@u22n/utils/ms'; 4 | 5 | export function SmartDateTime({ 6 | date, 7 | relativeUntil = ms('1 day') 8 | }: { 9 | date: Date; 10 | relativeUntil?: number; 11 | }) { 12 | const timeAgo = useTimeAgo(date); 13 | const showRealDate = date.getTime() - Date.now() > relativeUntil; 14 | 15 | return ( 16 | 17 | 18 | {showRealDate ? date.toLocaleDateString() : timeAgo} 19 | 20 | {date.toLocaleString()} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /ee/apps/billing/trpc/trpc.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError, initTRPC } from '@trpc/server'; 2 | import type { stripeData } from '../stripe'; 3 | import type { db } from '@u22n/database'; 4 | import superjson from 'superjson'; 5 | 6 | export const trpcContext = initTRPC 7 | .context<{ 8 | auth: boolean; 9 | stripe: typeof stripeData; 10 | db: typeof db; 11 | }>() 12 | .create({ transformer: superjson }); 13 | 14 | export const publicProcedure = trpcContext.procedure; 15 | export const protectedProcedure = trpcContext.procedure.use(({ next, ctx }) => { 16 | if (!ctx.auth) throw new TRPCError({ code: 'UNAUTHORIZED' }); 17 | return next(); 18 | }); 19 | export const router = trpcContext.router; 20 | export const middleware = trpcContext.middleware; 21 | -------------------------------------------------------------------------------- /predev.ts: -------------------------------------------------------------------------------- 1 | import { lstatSync, readFileSync } from 'fs'; 2 | 3 | // Check Node version 4 | const nodeVersion = process.version; 5 | const requiredVersion = readFileSync('.nvmrc', 'utf-8').trim(); 6 | 7 | if (!nodeVersion.startsWith(requiredVersion)) { 8 | console.error( 9 | `You are using Node ${nodeVersion}, but this project requires Node ${requiredVersion}.\nUse the correct node version to run this project` 10 | ); 11 | process.exit(1); 12 | } 13 | 14 | // Check for env file 15 | const envFile = lstatSync('.env.local', { throwIfNoEntry: false }); 16 | if (!envFile?.isFile()) { 17 | console.error( 18 | 'You are missing a .env.local file. Please refer to the README for instructions on how to create one.' 19 | ); 20 | process.exit(1); 21 | } 22 | -------------------------------------------------------------------------------- /apps/mail-bridge/postal-routes/inbound.ts: -------------------------------------------------------------------------------- 1 | import { postalMessageSchema, mailParamsSchema } from '../queue/mail-processor'; 2 | import { mailProcessorQueue } from '../queue/mail-processor'; 3 | import { zValidator } from '@u22n/hono/helpers'; 4 | import { createHonoApp } from '@u22n/hono'; 5 | import type { Ctx } from '../ctx'; 6 | 7 | export const inboundApi = createHonoApp(); 8 | 9 | inboundApi.post( 10 | '/mail/inbound/:orgId/:mailserverId', 11 | zValidator('json', postalMessageSchema), 12 | zValidator('param', mailParamsSchema), 13 | async (c) => { 14 | await mailProcessorQueue.add(`mail-processor`, { 15 | rawMessage: c.req.valid('json'), 16 | params: c.req.valid('param') 17 | }); 18 | return c.text('OK', 200); 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /apps/mail-bridge/trpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@trpc/server'; 2 | import { sendMailRouter } from './routers/sendMailRouter'; 3 | import { domainRouter } from './routers/domainRouter'; 4 | import { smtpRouter } from './routers/smtpRouter'; 5 | import { orgRouter } from './routers/orgRouter'; 6 | import { router } from './trpc'; 7 | 8 | export const trpcMailBridgePostalRouter = router({ 9 | org: orgRouter, 10 | domains: domainRouter 11 | }); 12 | export const trpcMailBridgeMailRouter = router({ 13 | send: sendMailRouter 14 | }); 15 | export const trpcMailBridgeRouter = router({ 16 | mail: trpcMailBridgeMailRouter, 17 | postal: trpcMailBridgePostalRouter, 18 | smtp: smtpRouter 19 | }); 20 | 21 | export type TrpcMailBridgeRouter = typeof trpcMailBridgeRouter; 22 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # local env files 34 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | -------------------------------------------------------------------------------- /ee/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # UnInbox Enterprise Edition 4 | 5 | Welcome to the Enterprise Edition ("/ee") of UnInbox.com. 6 | 7 | The [/ee](https://github.com/un/inbox/tree/main/ee) subfolder is the place for all the **Enterprise Edition** features from our [hosted](https://uninbox.com/pricing) plan and enterprise-grade features for for self hosted installs. 8 | 9 | > _❗ WARNING: This repository is copyrighted (unlike our [main repo](https://github.com/un/inbox)). You are not allowed to use this code to host your own version of app.uninbox.com without obtaining a proper license first❗ Contact support for more information_ 10 | 11 | ## Enabling features 12 | 13 | Check the readme in each app/package for instructions on how to enable the various **Enterprise Edition** features 14 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci 2 | on: 3 | pull_request: 4 | types: [review_requested, ready_for_review] 5 | push: 6 | branches: ['main'] 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | autofix: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v3 18 | 19 | - name: Install Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | cache: 'pnpm' 23 | node-version: '20' 24 | 25 | - run: pnpm install 26 | 27 | - name: Run ESLint 28 | run: pnpm fix 29 | 30 | - name: Run Prettier 31 | run: pnpm format 32 | 33 | - uses: autofix-ci/action@dd55f44df8f7cdb7a6bf74c78677eb8acd40cd0a 34 | -------------------------------------------------------------------------------- /ee/apps/command/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import Link from 'next/link'; 3 | 4 | export default function Page() { 5 | return ( 6 |
7 |

Command Panel

8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/src/components/posthog-page-view.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { usePathname, useSearchParams } from 'next/navigation'; 4 | import { usePostHog } from 'posthog-js/react'; 5 | import { useEffect } from 'react'; 6 | 7 | export default function PostHogPageView(): null { 8 | const pathname = usePathname(); 9 | const searchParams = useSearchParams(); 10 | const posthog = usePostHog(); 11 | useEffect(() => { 12 | // Track pageviews 13 | if (pathname && posthog) { 14 | let url = window.origin + pathname; 15 | if (searchParams.toString()) { 16 | url = url + `?${searchParams.toString()}`; 17 | } 18 | posthog.capture('$pageview', { 19 | $current_url: url 20 | }); 21 | } 22 | }, [pathname, searchParams, posthog]); 23 | 24 | return null; 25 | } 26 | -------------------------------------------------------------------------------- /apps/platform/trpc/routers/contactRouter/contactRouter.ts: -------------------------------------------------------------------------------- 1 | import { router, orgProcedure } from '~platform/trpc/trpc'; 2 | import { contacts } from '@u22n/database/schema'; 3 | import { eq } from '@u22n/database/orm'; 4 | 5 | export const contactsRouter = router({ 6 | getOrgContacts: orgProcedure.query(async ({ ctx }) => { 7 | const { db, org } = ctx; 8 | const orgId = org.id; 9 | 10 | const orgContactsResponse = await db.query.contacts.findMany({ 11 | where: eq(contacts.orgId, orgId), 12 | columns: { 13 | publicId: true, 14 | avatarTimestamp: true, 15 | emailUsername: true, 16 | emailDomain: true, 17 | name: true, 18 | setName: true, 19 | screenerStatus: true 20 | } 21 | }); 22 | 23 | return { contacts: orgContactsResponse }; 24 | }) 25 | }); 26 | -------------------------------------------------------------------------------- /apps/storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@u22n/storage", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "scripts": { 7 | "dev": "tsx watch --clear-screen=false --import ./tracing.ts app.ts", 8 | "start": "node --import ./.output/tracing.js .output/app.js", 9 | "build": "tsup", 10 | "check": "tsc --noEmit" 11 | }, 12 | "dependencies": { 13 | "@aws-sdk/client-s3": "^3.637.0", 14 | "@aws-sdk/s3-request-presigner": "^3.637.0", 15 | "@t3-oss/env-core": "^0.11.0", 16 | "@u22n/database": "workspace:*", 17 | "@u22n/hono": "workspace:^", 18 | "@u22n/otel": "workspace:^", 19 | "@u22n/utils": "workspace:*", 20 | "sharp": "^0.33.5", 21 | "unstorage": "^1.10.2", 22 | "zod": "^3.23.8" 23 | }, 24 | "devDependencies": { 25 | "tsup": "^8.2.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ee/apps/billing/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from '@t3-oss/env-core'; 2 | import { z } from 'zod'; 3 | 4 | export const env = createEnv({ 5 | server: { 6 | WEBAPP_URL: z.string().url(), 7 | EE_LICENSE_KEY: z.string().nullable().default(null), 8 | BILLING_KEY: z.string().min(1), 9 | BILLING_URL: z.string().url().min(1), 10 | BILLING_STRIPE_PLAN_PRO_MONTHLY_ID: z.string().min(1), 11 | BILLING_STRIPE_PLAN_PRO_YEARLY_ID: z.string().min(1), 12 | BILLING_STRIPE_KEY: z.string().min(1), 13 | BILLING_STRIPE_WEBHOOK_KEY: z.string().min(1), 14 | PORT: z.coerce.number().int().min(1).max(65535).default(3800), 15 | NODE_ENV: z.enum(['development', 'production']).default('development') 16 | }, 17 | client: {}, 18 | clientPrefix: '_', // Not used, just for making TS happy 19 | runtimeEnv: process.env 20 | }); 21 | -------------------------------------------------------------------------------- /apps/storage/storage.ts: -------------------------------------------------------------------------------- 1 | import { createStorage, type Driver, type StorageValue } from 'unstorage'; 2 | import redisDriver from 'unstorage/drivers/redis'; 3 | import { ms } from '@u22n/utils/ms'; 4 | import { env } from './env'; 5 | 6 | export type Session = { 7 | attributes: { 8 | account: { id: number; publicId: string; username: string }; 9 | }; 10 | }; 11 | 12 | const createCachedStorage = ( 13 | base: string, 14 | ttl: number 15 | ) => 16 | createStorage({ 17 | driver: redisDriver({ 18 | url: env.DB_REDIS_CONNECTION_STRING, 19 | ttl, 20 | base 21 | }) as Driver 22 | }); 23 | 24 | export const storage = { 25 | session: createCachedStorage( 26 | 'sessions', 27 | env.NODE_ENV === 'development' ? ms('12 hours') : ms('30 days') 28 | ) 29 | }; 30 | -------------------------------------------------------------------------------- /apps/mail-bridge/smtp/sendEmail.ts: -------------------------------------------------------------------------------- 1 | import { createTransport } from 'nodemailer'; 2 | import type { AuthOptions } from './auth'; 3 | 4 | export type SendEmailOptions = { 5 | to: string[]; 6 | from: string; 7 | raw: string; 8 | }; 9 | 10 | export async function sendEmail({ 11 | auth: { host, port, username, password, encryption, authMethod }, 12 | email: { to, from, raw } 13 | }: { 14 | auth: AuthOptions; 15 | email: SendEmailOptions; 16 | }) { 17 | const transport = createTransport({ 18 | host, 19 | port, 20 | secure: encryption === 'ssl' || encryption === 'tls', 21 | auth: { 22 | user: username, 23 | pass: password, 24 | method: authMethod.toUpperCase() 25 | } 26 | }); 27 | const res = await transport.sendMail({ 28 | envelope: { to, from }, 29 | raw 30 | }); 31 | return res; 32 | } 33 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/label.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | import * as LabelPrimitive from '@radix-ui/react-label'; 3 | import * as React from 'react'; 4 | 5 | import { cn } from '@/src/lib/utils'; 6 | 7 | const labelVariants = cva( 8 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | Label.displayName = LabelPrimitive.Root.displayName; 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /ee/apps/billing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uninbox-ee/billing", 3 | "license": "COMMERCIAL", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | "./trpc": { 8 | "types": "./trpc/index.ts" 9 | } 10 | }, 11 | "scripts": { 12 | "ee:dev": "tsx watch --clear-screen=false app.ts", 13 | "ee:start": "node .output/app.js", 14 | "ee:build": "tsup", 15 | "check": "tsc --noEmit" 16 | }, 17 | "dependencies": { 18 | "@t3-oss/env-core": "^0.11.0", 19 | "@trpc/client": "11.0.0-rc.485", 20 | "@trpc/server": "11.0.0-rc.485", 21 | "@u22n/database": "workspace:*", 22 | "@u22n/hono": "workspace:^", 23 | "@u22n/utils": "workspace:*", 24 | "stripe": "^16.8.0", 25 | "superjson": "^2.2.1", 26 | "zod": "^3.23.8" 27 | }, 28 | "devDependencies": { 29 | "tsup": "^8.2.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/storage/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from '@t3-oss/env-core'; 2 | import { z } from 'zod'; 3 | 4 | export const env = createEnv({ 5 | server: { 6 | WEBAPP_URL: z.string().url(), 7 | STORAGE_KEY: z.string().min(1), 8 | STORAGE_S3_ENDPOINT: z.string().min(1), 9 | STORAGE_S3_REGION: z.string().min(1), 10 | STORAGE_S3_ACCESS_KEY_ID: z.string().min(1), 11 | STORAGE_S3_SECRET_ACCESS_KEY: z.string().min(1), 12 | STORAGE_S3_BUCKET_ATTACHMENTS: z.string().min(1), 13 | STORAGE_S3_BUCKET_AVATARS: z.string().min(1), 14 | DB_REDIS_CONNECTION_STRING: z.string().min(1), 15 | PORT: z.coerce.number().int().min(1).max(65535).default(3200), 16 | NODE_ENV: z.enum(['development', 'production']).default('development') 17 | }, 18 | client: {}, 19 | clientPrefix: '_', // Not used, just for making TS happy 20 | runtimeEnv: process.env 21 | }); 22 | -------------------------------------------------------------------------------- /apps/storage/proxy/avatars.ts: -------------------------------------------------------------------------------- 1 | import { createHonoApp } from '@u22n/hono'; 2 | import type { Ctx } from '../ctx'; 3 | import { env } from '../env'; 4 | 5 | // Proxy to `${process.env.STORAGE_S3_ENDPOINT}/${process.env.STORAGE_S3_BUCKET_AVATARS}/${proxy}` 6 | export const avatarProxy = createHonoApp().get( 7 | '/:proxy{.+}', 8 | async (c) => { 9 | const proxy = c.req.param('proxy'); 10 | const res = await fetch( 11 | `${env.STORAGE_S3_ENDPOINT}/${env.STORAGE_S3_BUCKET_AVATARS}/${proxy}` 12 | ); 13 | if (res.status === 404) { 14 | return c.json({ error: 'Not Found' }, 404); 15 | } 16 | // Avatars are immutable so we can cache them for a long time 17 | c.header( 18 | 'Cache-Control', 19 | 'public, immutable, max-age=86400, stale-while-revalidate=604800' 20 | ); 21 | return c.body(res.body, res); 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /ee/apps/billing/middlewares.ts: -------------------------------------------------------------------------------- 1 | import { createMiddleware } from '@u22n/hono/helpers'; 2 | import { stripeSdk } from './stripe'; 3 | import type { Ctx } from './ctx'; 4 | import { env } from './env'; 5 | 6 | export const stripeWebhookMiddleware = createMiddleware( 7 | async (ctx, next) => { 8 | const stripeSignature = ctx.req.header('stripe-signature'); 9 | const body = await ctx.req.raw.text(); 10 | if (!stripeSignature || !body) { 11 | return ctx.json(null, 401); 12 | } 13 | try { 14 | ctx.set( 15 | 'stripeEvent', 16 | stripeSdk.webhooks.constructEvent( 17 | body, 18 | stripeSignature, 19 | env.BILLING_STRIPE_WEBHOOK_KEY 20 | ) 21 | ); 22 | await next(); 23 | } catch (e) { 24 | console.error(e); 25 | return ctx.json(null, 401); 26 | } 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /apps/web/src/providers/posthog-provider.tsx: -------------------------------------------------------------------------------- 1 | // app/providers.tsx 2 | 'use client'; 3 | import { PostHogProvider } from 'posthog-js/react'; 4 | import posthog from 'posthog-js'; 5 | import { env } from '../env'; 6 | 7 | const initPostHog = () => { 8 | posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY!, { 9 | api_host: '/ingest', 10 | person_profiles: 'identified_only', 11 | capture_pageview: false // Disable automatic pageview capture, as we capture manually 12 | }); 13 | }; 14 | 15 | if ( 16 | typeof window !== 'undefined' && 17 | env.NEXT_PUBLIC_POSTHOG_ENABLED === 'true' 18 | ) { 19 | initPostHog(); 20 | } 21 | 22 | export function PHProvider({ children }: { children: React.ReactNode }) { 23 | if (env.NEXT_PUBLIC_POSTHOG_ENABLED === 'true') { 24 | return {children}; 25 | } 26 | return children; 27 | } 28 | -------------------------------------------------------------------------------- /ee/apps/command/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | import { cn } from '../../lib/utils'; 6 | import * as React from 'react'; 7 | 8 | const labelVariants = cva( 9 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' 10 | ); 11 | 12 | const Label = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef & 15 | VariantProps 16 | >(({ className, ...props }, ref) => ( 17 | 22 | )); 23 | Label.displayName = LabelPrimitive.Root.displayName; 24 | 25 | export { Label }; 26 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-is-mobile.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useLayoutEffect, useState } from 'react'; 4 | 5 | function useMaxWidth(maxWidth: number) { 6 | const [isHittingMaxWidth, setIsHittingMaxWidth] = useState(false); 7 | 8 | useLayoutEffect(() => { 9 | const handleSize = () => { 10 | const isCurrentlyHitting = window.innerWidth <= maxWidth; 11 | if (isCurrentlyHitting !== isHittingMaxWidth) { 12 | setIsHittingMaxWidth(isCurrentlyHitting); 13 | } 14 | }; 15 | handleSize(); 16 | window.addEventListener('resize', handleSize); 17 | return () => window.removeEventListener('resize', handleSize); 18 | }); 19 | 20 | if (typeof window === 'undefined') return false; 21 | return isHittingMaxWidth; 22 | } 23 | 24 | export const useIsMobile = () => useMaxWidth(768); 25 | export const useIsSidebarAutoCollapsed = () => useMaxWidth(1024); 26 | -------------------------------------------------------------------------------- /ee/apps/command/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Toaster as Sonner } from 'sonner'; 4 | 5 | type ToasterProps = React.ComponentProps; 6 | 7 | const Toaster = ({ ...props }: ToasterProps) => { 8 | return ( 9 | 24 | ); 25 | }; 26 | 27 | export { Toaster }; 28 | -------------------------------------------------------------------------------- /apps/platform/routes/services.ts: -------------------------------------------------------------------------------- 1 | import { updateDnsRecords } from '~platform/utils/updateDnsRecords'; 2 | import { typeIdValidator } from '@u22n/utils/typeid'; 3 | import { zValidator } from '@u22n/hono/helpers'; 4 | import { createHonoApp } from '@u22n/hono'; 5 | import type { Ctx } from '~platform/ctx'; 6 | import { db } from '@u22n/database'; 7 | import { z } from 'zod'; 8 | 9 | export const servicesApi = createHonoApp(); 10 | 11 | servicesApi.post( 12 | '/dns-check', 13 | zValidator( 14 | 'json', 15 | z.object({ 16 | orgId: z.number(), 17 | domainPublicId: typeIdValidator('domains') 18 | }) 19 | ), 20 | async (c) => { 21 | const { orgId, domainPublicId } = c.req.valid('json'); 22 | const results = await updateDnsRecords({ domainPublicId, orgId }, db).catch( 23 | (e: Error) => ({ error: e.message }) 24 | ); 25 | return c.json({ results }); 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /ee/apps/command/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '../../lib/utils'; 2 | import * as React from 'react'; 3 | 4 | export type InputProps = React.InputHTMLAttributes; 5 | 6 | const Input = React.forwardRef( 7 | ({ className, type, ...props }, ref) => { 8 | return ( 9 | 18 | ); 19 | } 20 | ); 21 | Input.displayName = 'Input'; 22 | 23 | export { Input }; 24 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as ProgressPrimitive from '@radix-ui/react-progress'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/src/lib/utils'; 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 17 | 21 | 22 | )); 23 | Progress.displayName = ProgressPrimitive.Root.displayName; 24 | 25 | export { Progress }; 26 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as SeparatorPrimitive from '@radix-ui/react-separator'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/src/lib/utils'; 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = 'horizontal', decorative = true, ...props }, 12 | ref 13 | ) => ( 14 | 25 | ) 26 | ); 27 | Separator.displayName = SeparatorPrimitive.Root.displayName; 28 | 29 | export { Separator }; 30 | -------------------------------------------------------------------------------- /ee/apps/command/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from '@/components/ui/sonner'; 2 | import { TRPCReactProvider } from '@/lib/trpc'; 3 | import { Inter } from 'next/font/google'; 4 | import type { Metadata } from 'next'; 5 | import { cn } from '@/lib/utils'; 6 | import '../styles/globals.css'; 7 | 8 | const inter = Inter({ subsets: ['latin'] }); 9 | 10 | export const metadata: Metadata = { 11 | title: 'UnInbox Command', 12 | description: 'UnInbox Command Panel' 13 | }; 14 | 15 | export default function RootLayout({ 16 | children 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | return ( 21 | 24 | 25 | 26 | {children} 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/otel/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { Span } from '@opentelemetry/api'; 2 | import { opentelemetryEnabled } from '.'; 3 | 4 | // Import OpenTelemetry API only if it's enabled 5 | const { trace } = opentelemetryEnabled 6 | ? await import('@opentelemetry/api') 7 | : { trace: undefined }; 8 | 9 | const { wrapTracer } = opentelemetryEnabled 10 | ? await import('@opentelemetry/api/experimental') 11 | : { wrapTracer: undefined }; 12 | 13 | export function getTracer(name: string) { 14 | if (!trace || !wrapTracer) 15 | return { 16 | startActiveSpan: (name: string, fn: (span?: Span) => Fn) => fn() 17 | }; 18 | const tracer = wrapTracer(trace.getTracer(name)); 19 | return { 20 | startActiveSpan: tracer.withActiveSpan.bind(tracer) 21 | }; 22 | } 23 | 24 | export function inActiveSpan(fn: (span?: Span) => Fn) { 25 | if (!trace) return fn(); 26 | const span = trace.getActiveSpan(); 27 | return fn(span); 28 | } 29 | -------------------------------------------------------------------------------- /apps/worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@u22n/worker", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "scripts": { 7 | "dev": "tsx watch --clear-screen=false --import ./tracing.ts app.ts", 8 | "start": "node --import ./.output/tracing.js .output/app.js", 9 | "build": "tsup", 10 | "check": "tsc --noEmit" 11 | }, 12 | "exports": { 13 | "./trpc": { 14 | "types": "./trpc/index.ts" 15 | } 16 | }, 17 | "dependencies": { 18 | "@t3-oss/env-core": "^0.11.0", 19 | "@trpc/client": "11.0.0-rc.485", 20 | "@trpc/server": "11.0.0-rc.485", 21 | "@u22n/database": "workspace:^", 22 | "@u22n/hono": "workspace:^", 23 | "@u22n/otel": "workspace:^", 24 | "@u22n/utils": "workspace:^", 25 | "bullmq": "^5.12.10", 26 | "cron": "^3.1.7", 27 | "superjson": "^2.2.1", 28 | "zod": "^3.23.8" 29 | }, 30 | "devDependencies": { 31 | "tsup": "^8.2.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/realtime/events.ts: -------------------------------------------------------------------------------- 1 | import { typeIdValidator } from '@u22n/utils/typeid'; 2 | import { z } from 'zod'; 3 | 4 | export const eventDataMaps = { 5 | 'platform:update': z.object({ version: z.string() }), 6 | 'convo:new': z.object({ publicId: typeIdValidator('convos') }), 7 | 'convo:hidden': z.object({ 8 | publicId: z.array(typeIdValidator('convos')).or(typeIdValidator('convos')), 9 | hidden: z.boolean() 10 | }), 11 | 'convo:deleted': z.object({ 12 | publicId: z.array(typeIdValidator('convos')).or(typeIdValidator('convos')) 13 | }), 14 | 'convo:entry:new': z.object({ 15 | convoPublicId: typeIdValidator('convos'), 16 | convoEntryPublicId: typeIdValidator('convoEntries') 17 | }), 18 | 'convo:workflow:update': z.object({ 19 | convoPublicId: typeIdValidator('convos'), 20 | orgShortcode: z.string() 21 | }), 22 | 'admin:issue:refresh': z.object({}) 23 | } as const; 24 | 25 | export type EventDataMap = typeof eventDataMaps; 26 | -------------------------------------------------------------------------------- /packages/database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@u22n/database", 3 | "private": true, 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.ts", 7 | "./orm": "./orm.ts", 8 | "./schema": "./schema.ts" 9 | }, 10 | "scripts": { 11 | "check": "tsc --noEmit", 12 | "db:push": "drizzle-kit push", 13 | "db:migrate": "tsx migrate.ts", 14 | "db:studio": "drizzle-kit studio --port 3333", 15 | "db:check": "drizzle-kit check", 16 | "db:generate": "drizzle-kit generate", 17 | "db:clean": "tsx dbClean.ts", 18 | "db:up": "drizzle-kit up", 19 | "db:drop": "drizzle-kit drop" 20 | }, 21 | "dependencies": { 22 | "@planetscale/database": "^1.19.0", 23 | "@t3-oss/env-core": "^0.11.0", 24 | "@u22n/otel": "workspace:^", 25 | "@u22n/utils": "workspace:*", 26 | "drizzle-orm": "^0.33.0", 27 | "mysql2": "^3.11.0", 28 | "zod": "^3.23.8" 29 | }, 30 | "devDependencies": { 31 | "drizzle-kit": "0.24.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/src/components/shared/strength-meter.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode } from 'react'; 2 | import { cn } from '@/src/lib/utils'; 3 | 4 | export function StrengthMeter({ 5 | message = null, 6 | strength = 0, 7 | error = false 8 | }: { 9 | strength?: number; 10 | message?: ReactNode; 11 | error?: boolean; 12 | }) { 13 | return ( 14 |
15 |
16 | Strength: 17 | {message} 18 |
19 |
20 | {Array.from({ length: 5 }).map((_, i) => ( 21 |
28 | ))} 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /apps/worker/middlewares.ts: -------------------------------------------------------------------------------- 1 | import { createMiddleware } from '@u22n/hono/helpers'; 2 | import { getTracer } from '@u22n/otel/helpers'; 3 | import { logger } from '@u22n/otel/logger'; 4 | import type { Ctx } from './ctx'; 5 | import { env } from './env'; 6 | 7 | const middlewareTracer = getTracer('worker/hono/middleware'); 8 | 9 | export const commandControlAuthMiddleware = createMiddleware( 10 | async (c, next) => 11 | middlewareTracer.startActiveSpan('Service Middleware', async (span) => { 12 | const authToken = c.req.header('Authorization'); 13 | span?.setAttribute('req.service.meta.has_header', !!authToken); 14 | if (authToken !== env.WORKER_ACCESS_KEY) { 15 | const ip = c.env.incoming.socket.remoteAddress; 16 | logger.info( 17 | `Unauthorized Request from ${ip}\n Path: ${c.req.path}\nHeaders: ${JSON.stringify(c.req.header())}` 18 | ); 19 | return c.json({ error: 'Unauthorized' }, 400); 20 | } 21 | await next(); 22 | }) 23 | ); 24 | -------------------------------------------------------------------------------- /apps/platform/ctx.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from '@u22n/hono/helpers'; 2 | import type { TypeId } from '@u22n/utils/typeid'; 3 | import type { HonoContext } from '@u22n/hono'; 4 | import type { DBType } from '@u22n/database'; 5 | import type { DatabaseSession } from 'lucia'; 6 | 7 | export type Ctx = HonoContext<{ 8 | account: AccountContext; 9 | }>; 10 | 11 | export type OrgContext = { 12 | id: number; 13 | publicId: TypeId<'org'>; 14 | name: string; 15 | memberId?: number; 16 | members: { 17 | id: number; 18 | accountId: number | null; 19 | // Refer to DB schema orgMembers.role and orgMembers.status 20 | status: 'invited' | 'active' | 'removed'; 21 | role: 'admin' | 'member'; 22 | }[]; 23 | } | null; 24 | 25 | export type AccountContext = { 26 | id: number; 27 | session: DatabaseSession; 28 | } | null; 29 | 30 | export type TrpcContext = { 31 | db: DBType; 32 | account: AccountContext; 33 | org: OrgContext; 34 | event: Context; 35 | selfHosted: boolean; 36 | }; 37 | -------------------------------------------------------------------------------- /apps/mail-bridge/smtp/auth.ts: -------------------------------------------------------------------------------- 1 | import { createTransport } from 'nodemailer'; 2 | 3 | export type AuthOptions = { 4 | host: string; 5 | port?: number; 6 | username: string; 7 | password: string; 8 | encryption: 'ssl' | 'tls' | 'starttls' | 'none'; 9 | authMethod: 'plain' | 'login'; 10 | }; 11 | 12 | export async function validateSmtpCredentials({ 13 | host, 14 | port, 15 | username, 16 | password, 17 | encryption, 18 | authMethod 19 | }: AuthOptions) { 20 | const transport = createTransport({ 21 | host, 22 | port, 23 | secure: encryption === 'ssl' || encryption === 'tls', 24 | auth: { 25 | user: username, 26 | pass: password, 27 | method: authMethod.toUpperCase() 28 | } 29 | }); 30 | const status = await transport 31 | .verify() 32 | .then(() => ({ valid: true, error: null }) as const) 33 | .catch( 34 | (e: Error) => 35 | ({ 36 | valid: false, 37 | error: e.message 38 | }) as const 39 | ); 40 | transport.close(); 41 | return status; 42 | } 43 | -------------------------------------------------------------------------------- /ee/apps/command/src/app/remove-expired-sessions/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | import { api } from '@/lib/trpc'; 5 | import { toast } from 'sonner'; 6 | 7 | export default function Page() { 8 | const { isPending, error, mutateAsync } = 9 | api.internal.removeExpiredSessions.useMutation({ 10 | onSuccess: (data) => { 11 | toast.success(`Removed ${data.count} expired sessions`); 12 | }, 13 | onError: (error) => { 14 | toast.error(error.message); 15 | } 16 | }); 17 | 18 | return ( 19 |
20 |

Remove Expired Sessions

21 |
22 | 27 |
28 | {error &&
{error.message}
} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@u22n/utils", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "scripts": { 7 | "check": "tsc --noEmit" 8 | }, 9 | "exports": { 10 | "./password": "./password.ts", 11 | "./ms": "./ms.ts", 12 | "./dns": "./dns/index.ts", 13 | "./typeid": "./typeid.ts", 14 | "./colors": "./uiColors.ts", 15 | "./zodSchemas": "./zodSchemas.ts", 16 | "./discord": "./discord.ts", 17 | "./spaces": "./spaces.ts", 18 | "./sanitizers": "./sanitizers.ts" 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC", 23 | "engines": { 24 | "node": ">=20", 25 | "pnpm": ">=8" 26 | }, 27 | "dependencies": { 28 | "@discordjs/builders": "^1.8.2", 29 | "@zxcvbn-ts/core": "^3.0.4", 30 | "@zxcvbn-ts/language-common": "^3.0.4", 31 | "@zxcvbn-ts/matcher-pwned": "^3.0.4", 32 | "drizzle-orm": "^0.33.0", 33 | "human-format": "^1.2.0", 34 | "itty-time": "^1.0.6", 35 | "nanoid": "^5.0.7", 36 | "typeid-js": "^1.0.0", 37 | "zod": "^3.23.8" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/otel/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from '@t3-oss/env-core'; 2 | import { z } from 'zod'; 3 | 4 | const stringToJSON = z.string().transform((str, ctx) => { 5 | try { 6 | return JSON.parse(str) as unknown; 7 | } catch (e) { 8 | ctx.addIssue({ code: 'custom', message: 'Invalid JSON' }); 9 | return z.NEVER; 10 | } 11 | }); 12 | 13 | export const env = createEnv({ 14 | server: { 15 | OTEL_ENABLED: z 16 | .string() 17 | .optional() 18 | .transform((v) => v === 'true'), 19 | OTEL_EXPORTER_TRACES_ENDPOINT: z.string().url().optional(), 20 | OTEL_EXPORTER_TRACES_HEADERS: stringToJSON 21 | .pipe(z.record(z.string())) 22 | .optional(), 23 | OTEL_EXPORTER_LOGS_ENDPOINT: z.string().url().optional(), 24 | OTEL_EXPORTER_LOGS_HEADERS: stringToJSON 25 | .pipe(z.record(z.string())) 26 | .optional(), 27 | OTEL_EXPORTER_METRICS_ENDPOINT: z.string().url().optional(), 28 | NODE_ENV: z.enum(['development', 'production']).default('development') 29 | }, 30 | runtimeEnv: process.env, 31 | emptyStringAsUndefined: true 32 | }); 33 | -------------------------------------------------------------------------------- /apps/web/src/app/join/_components/stepper.tsx: -------------------------------------------------------------------------------- 1 | import { Check } from '@phosphor-icons/react'; 2 | import { cn } from '@/src/lib/utils'; 3 | 4 | export default function Stepper({ 5 | step, 6 | total 7 | }: { 8 | step: number; 9 | total: number; 10 | }) { 11 | return ( 12 |
13 | {`This is step ${step} of ${total}`} 14 | {Array.from({ length: total }).map((_, i) => ( 15 |
i + 1 && 'bg-green-9 text-base-1 border-none' 21 | )}> 22 | {step > i + 1 ? ( 23 | 27 | ) : ( 28 | i + 1 29 | )} 30 |
31 | ))} 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Toaster as _Sonner } from 'sonner'; 4 | import { useTheme } from 'next-themes'; 5 | import { memo } from 'react'; 6 | 7 | type ToasterProps = React.ComponentProps; 8 | 9 | const Sonner = memo(_Sonner); 10 | 11 | const Toaster = ({ ...props }: ToasterProps) => { 12 | const { theme = 'system' } = useTheme(); 13 | 14 | return ( 15 | 31 | ); 32 | }; 33 | 34 | export { Toaster }; 35 | -------------------------------------------------------------------------------- /ee/apps/command/src/server/trpc/trpc.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError, initTRPC } from '@trpc/server'; 2 | import { getAccount } from '@/lib/get-account'; 3 | import type { NextRequest } from 'next/server'; 4 | import type { db } from '@u22n/database'; 5 | import superjson from 'superjson'; 6 | 7 | export const trpcContext = initTRPC 8 | .context<{ 9 | db: typeof db; 10 | account: { id: number; username: string } | null; 11 | event: NextRequest; 12 | }>() 13 | .create({ transformer: superjson }); 14 | 15 | const isAccountAuthenticated = trpcContext.middleware(async ({ next, ctx }) => { 16 | const account = await getAccount(ctx.event); 17 | 18 | if (!account?.id || !account.username) { 19 | throw new TRPCError({ 20 | code: 'UNAUTHORIZED', 21 | message: 'You are not logged in, redirecting...' 22 | }); 23 | } 24 | 25 | return next({ ctx: { ...ctx, account } }); 26 | }); 27 | 28 | export const accountProcedure = trpcContext.procedure.use( 29 | isAccountAuthenticated 30 | ); 31 | 32 | export const router = trpcContext.router; 33 | export const middleware = trpcContext.middleware; 34 | -------------------------------------------------------------------------------- /apps/worker/utils/queue-helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Queue, 3 | Worker, 4 | type Job, 5 | type QueueOptions, 6 | type WorkerOptions 7 | } from 'bullmq'; 8 | import { env } from '../env'; 9 | 10 | const { host, username, password, port } = new URL( 11 | env.DB_REDIS_CONNECTION_STRING 12 | ); 13 | 14 | const connection = { 15 | host: host.split(':')[0], 16 | port: Number(port), 17 | username, 18 | password 19 | }; 20 | 21 | export function createQueue( 22 | name: string, 23 | options: Omit = {} 24 | ) { 25 | const queue = new Queue(name, { 26 | connection, 27 | ...options 28 | }); 29 | return queue; 30 | } 31 | 32 | export function createWorker( 33 | name: string, 34 | jobHandler: (job: Job) => Promise, 35 | options: Omit = {} 36 | ) { 37 | const worker = new Worker(name, jobHandler, { 38 | connection, 39 | ...options 40 | }); 41 | worker.on('error', (error) => { 42 | console.error(`Worker for queue ${name} encountered an error:`, error); 43 | }); 44 | return worker; 45 | } 46 | -------------------------------------------------------------------------------- /apps/mail-bridge/utils/queue-helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Queue, 3 | Worker, 4 | type Job, 5 | type QueueOptions, 6 | type WorkerOptions 7 | } from 'bullmq'; 8 | import { env } from '../env'; 9 | 10 | const { host, username, password, port } = new URL( 11 | env.DB_REDIS_CONNECTION_STRING 12 | ); 13 | 14 | const connection = { 15 | host: host.split(':')[0], 16 | port: Number(port), 17 | username, 18 | password 19 | }; 20 | 21 | export function createQueue( 22 | name: string, 23 | options: Omit = {} 24 | ) { 25 | const queue = new Queue(name, { 26 | connection, 27 | ...options 28 | }); 29 | return queue; 30 | } 31 | 32 | export function createWorker( 33 | name: string, 34 | jobHandler: (job: Job) => Promise, 35 | options: Omit = {} 36 | ) { 37 | const worker = new Worker(name, jobHandler, { 38 | connection, 39 | ...options 40 | }); 41 | worker.on('error', (error) => { 42 | console.error(`Worker for queue ${name} encountered an error:`, error); 43 | }); 44 | return worker; 45 | } 46 | -------------------------------------------------------------------------------- /apps/storage/api/mailfetch.ts: -------------------------------------------------------------------------------- 1 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 2 | import { checkAuthorizedService } from '../middlewares'; 3 | import { GetObjectCommand } from '@aws-sdk/client-s3'; 4 | import { zValidator } from '@u22n/hono/helpers'; 5 | import { createHonoApp } from '@u22n/hono'; 6 | import type { Ctx } from '../ctx'; 7 | import { s3Client } from '../s3'; 8 | import { env } from '../env'; 9 | import { z } from 'zod'; 10 | 11 | export const mailfetchApi = createHonoApp().post( 12 | '/attachments/mailfetch', 13 | checkAuthorizedService, 14 | zValidator( 15 | 'json', 16 | z.object({ 17 | orgPublicId: z.string(), 18 | attachmentPublicId: z.string(), 19 | filename: z.string() 20 | }) 21 | ), 22 | async (c) => { 23 | const { orgPublicId, attachmentPublicId, filename } = c.req.valid('json'); 24 | const command = new GetObjectCommand({ 25 | Bucket: env.STORAGE_S3_BUCKET_ATTACHMENTS, 26 | Key: `${orgPublicId}/${attachmentPublicId}/${filename}` 27 | }); 28 | const url = await getSignedUrl(s3Client, command, { expiresIn: 300 }); 29 | return c.json({ url }); 30 | } 31 | ); 32 | -------------------------------------------------------------------------------- /packages/tiptap/components/editor-bubble-item.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef, ReactNode } from 'react'; 2 | import { useCurrentEditor } from '@tiptap/react'; 3 | import { Slot } from '@radix-ui/react-slot'; 4 | import type { Editor } from '@tiptap/react'; 5 | import { forwardRef } from 'react'; 6 | 7 | interface EditorBubbleItemProps { 8 | readonly children: ReactNode; 9 | readonly asChild?: boolean; 10 | readonly onSelect?: (editor: Editor) => void; 11 | } 12 | 13 | export const EditorBubbleItem = forwardRef< 14 | HTMLDivElement, 15 | EditorBubbleItemProps & Omit, 'onSelect'> 16 | >(({ children, asChild, onSelect, ...rest }, ref) => { 17 | const { editor } = useCurrentEditor(); 18 | const Comp = asChild ? Slot : 'div'; 19 | 20 | if (!editor) return null; 21 | 22 | return ( 23 | { 27 | e.preventDefault(); 28 | onSelect?.(editor); 29 | }}> 30 | {children} 31 | 32 | ); 33 | }); 34 | 35 | EditorBubbleItem.displayName = 'EditorBubbleItem'; 36 | 37 | export default EditorBubbleItem; 38 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/slider.tsx: -------------------------------------------------------------------------------- 1 | import * as SliderPrimitive from '@radix-ui/react-slider'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/src/lib/utils'; 5 | 6 | const Slider = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 17 | 18 | 19 | 20 | 21 | 22 | )); 23 | Slider.displayName = SliderPrimitive.Root.displayName; 24 | 25 | export { Slider }; 26 | -------------------------------------------------------------------------------- /.vscode/cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "ignorePaths": ["package.json"], 4 | "dictionaryDefinitions": [], 5 | "dictionaries": [], 6 | "words": [ 7 | "Authed", 8 | "bullmq", 9 | "Commenters", 10 | "composables", 11 | "convo", 12 | "convos", 13 | "dmarc", 14 | "domainkey", 15 | "hono", 16 | "hookform", 17 | "MAILBRIDGE", 18 | "mailfetch", 19 | "mailserver", 20 | "mediumint", 21 | "opentelemetry", 22 | "otel", 23 | "partialize", 24 | "planetscale", 25 | "posthog", 26 | "presign", 27 | "presigner", 28 | "ratelimit", 29 | "Ratelimiter", 30 | "RPID", 31 | "shadcn", 32 | "Shortcode", 33 | "Shortcodes", 34 | "simplewebauthn", 35 | "sonner", 36 | "starttls", 37 | "superjson", 38 | "tanstack", 39 | "tinyint", 40 | "tiptap", 41 | "totp", 42 | "trpc", 43 | "Typesafe", 44 | "unboarding", 45 | "Unin", 46 | "uninbox", 47 | "unkey", 48 | "unstorage", 49 | "unvalidated", 50 | "waitlist", 51 | "zustand", 52 | "zxcvbn" 53 | ], 54 | "ignoreWords": ["ABCDEFGHJKMNPQRSTVWXYZ"], 55 | "import": [] 56 | } 57 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/settings/org/users/teams/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PageTitle } from '../../../_components/page-title'; 4 | import { NewTeamModal } from './_components/new-team-modal'; 5 | import { DataTable } from '@/src/components/shared/table'; 6 | import { useOrgShortcode } from '@/src/hooks/use-params'; 7 | import { columns } from './_components/columns'; 8 | import { platform } from '@/src/lib/trpc'; 9 | 10 | export default function Page() { 11 | const orgShortcode = useOrgShortcode(); 12 | const { data: teamList, isLoading } = 13 | platform.org.users.teams.getOrgTeams.useQuery({ 14 | orgShortcode 15 | }); 16 | 17 | return ( 18 |
19 | 20 | 21 | 22 | {isLoading &&
Loading...
} 23 | {teamList && ( 24 | 28 | `/${orgShortcode}/settings/org/users/teams/${row.publicId}` 29 | } 30 | /> 31 | )} 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-time-ago.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | import { intlFormatDistance } from 'date-fns'; 3 | import { ms } from '@u22n/utils/ms'; 4 | 5 | function format(time: Date) { 6 | return intlFormatDistance(time, new Date(), { locale: 'en' }); 7 | } 8 | 9 | function calculateOptimalInterval(time: Date) { 10 | const diff = new Date().getTime() - time.getTime(); 11 | if (diff < ms('1 minute')) return ms('1 second'); 12 | if (diff < ms('1 hour')) return ms('1 minutes'); 13 | return ms('1 hour'); 14 | } 15 | 16 | export function useTimeAgo(time: Date) { 17 | const [timeAgo, setTimeAgo] = useState(() => format(time)); 18 | const [updateInterval, setUpdateInterval] = useState(() => 19 | calculateOptimalInterval(time) 20 | ); 21 | const updateInfo = useCallback(() => { 22 | setTimeAgo(format(time)); 23 | setUpdateInterval(calculateOptimalInterval(time)); 24 | }, [time]); 25 | 26 | useEffect(() => { 27 | updateInfo(); 28 | const interval = setInterval(() => updateInfo(), updateInterval); 29 | return () => clearInterval(interval); 30 | }, [time, updateInfo, updateInterval]); 31 | 32 | return timeAgo; 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/[spaceShortcode]/convo/welcome/_components/welcome-messages.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { welcomeMessages } from '../_data/welcomeMessages'; 4 | import { useOrgShortcode } from '@/src/hooks/use-params'; 5 | import { WelcomeMessage } from './welcome-message'; 6 | import React, { useState, useEffect } from 'react'; 7 | 8 | export function WelcomeMessages() { 9 | const orgShortcode = useOrgShortcode(); 10 | const [messageIndex, setMessageIndex] = useState(0); 11 | 12 | useEffect(() => { 13 | const timer = setInterval(() => { 14 | setMessageIndex((prev) => { 15 | if (prev < welcomeMessages.length - 1) { 16 | return prev + 1; 17 | } 18 | clearInterval(timer); 19 | return prev; 20 | }); 21 | }, 1000); 22 | 23 | return () => clearInterval(timer); 24 | }, []); 25 | 26 | return ( 27 |
28 | {welcomeMessages.slice(0, messageIndex + 1).map((Message, index) => ( 29 | 30 | 31 | 32 | ))} 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/settings/org/mail/domains/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AddDomainModal } from './_components/add-domain-modal'; 4 | import { PageTitle } from '../../../_components/page-title'; 5 | import { DataTable } from '@/src/components/shared/table'; 6 | import { useOrgShortcode } from '@/src/hooks/use-params'; 7 | import { columns } from './_components/columns'; 8 | import { platform } from '@/src/lib/trpc'; 9 | 10 | export default function Page() { 11 | const orgShortcode = useOrgShortcode(); 12 | const { data: domainList, isLoading } = 13 | platform.org.mail.domains.getOrgDomains.useQuery({ 14 | orgShortcode 15 | }); 16 | 17 | return ( 18 |
19 | 20 | 21 | 22 | 23 | {isLoading &&
Loading...
} 24 | {domainList && ( 25 | 29 | `/${orgShortcode}/settings/org/mail/domains/${row.publicId}` 30 | } 31 | /> 32 | )} 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /apps/web/src/components/turnstile.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | Turnstile, 5 | type TurnstileProps, 6 | type TurnstileInstance 7 | } from '@marsidev/react-turnstile'; 8 | import { useTheme } from 'next-themes'; 9 | import { forwardRef } from 'react'; 10 | import { env } from '../env'; 11 | import React from 'react'; 12 | 13 | export const turnstileEnabled = Boolean(env.NEXT_PUBLIC_TURNSTILE_SITE_KEY); 14 | 15 | export const TurnstileComponent = forwardRef< 16 | TurnstileInstance, 17 | Omit 18 | >(({ options, ...props }, ref) => { 19 | const { resolvedTheme } = useTheme(); 20 | return turnstileEnabled ? ( 21 |
22 |
23 | 32 |
33 |
34 | ) : null; 35 | }); 36 | 37 | TurnstileComponent.displayName = 'TurnstileComponent'; 38 | -------------------------------------------------------------------------------- /packages/otel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@u22n/otel", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.ts", 7 | "./setup": "./setup.ts", 8 | "./hono": "./hono.ts", 9 | "./exports": "./exports.ts", 10 | "./logger": "./logger.ts", 11 | "./helpers": "./helpers.ts" 12 | }, 13 | "dependencies": { 14 | "@opentelemetry/api": "^1.9.0", 15 | "@opentelemetry/api-logs": "^0.52.1", 16 | "@opentelemetry/exporter-logs-otlp-http": "^0.52.1", 17 | "@opentelemetry/exporter-metrics-otlp-http": "^0.52.1", 18 | "@opentelemetry/exporter-trace-otlp-http": "^0.52.1", 19 | "@opentelemetry/instrumentation": "^0.52.1", 20 | "@opentelemetry/instrumentation-http": "^0.52.1", 21 | "@opentelemetry/instrumentation-undici": "^0.4.0", 22 | "@opentelemetry/instrumentation-winston": "^0.39.0", 23 | "@opentelemetry/resources": "^1.25.1", 24 | "@opentelemetry/sdk-logs": "^0.52.1", 25 | "@opentelemetry/sdk-trace-node": "^1.25.1", 26 | "@opentelemetry/winston-transport": "^0.5.0", 27 | "@t3-oss/env-core": "^0.11.0", 28 | "@u22n/hono": "workspace:^", 29 | "flat": "^6.0.1", 30 | "winston": "^3.13.1", 31 | "zod": "^3.23.8" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; 2 | import { Check } from '@phosphor-icons/react'; 3 | import * as React from 'react'; 4 | 5 | import { cn } from '@/src/lib/utils'; 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 18 | 19 | 24 | 25 | 26 | )); 27 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 28 | 29 | export { Checkbox }; 30 | -------------------------------------------------------------------------------- /ee/apps/command/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uninbox-ee/command", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "ee:dev": "PORT=3820 next dev", 8 | "ee:build": "next build", 9 | "ee:start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-label": "^2.1.0", 14 | "@radix-ui/react-slot": "^1.1.0", 15 | "@tanstack/react-query": "^5.52.1", 16 | "@trpc/client": "11.0.0-rc.485", 17 | "@trpc/react-query": "11.0.0-rc.485", 18 | "@trpc/server": "11.0.0-rc.485", 19 | "@u22n/database": "workspace:^", 20 | "@u22n/utils": "workspace:^", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.1.1", 23 | "next": "^14.2.6", 24 | "oslo": "^1.2.0", 25 | "qrcode.react": "^3.1.0", 26 | "react": "^18.3.1", 27 | "react-dom": "^18.3.1", 28 | "sonner": "^1.5.0", 29 | "superjson": "^2.2.1", 30 | "tailwind-merge": "^2.3.0", 31 | "tailwindcss-animate": "^1.0.7", 32 | "zod": "^3.23.8" 33 | }, 34 | "devDependencies": { 35 | "@types/react": "^18.3.4", 36 | "@types/react-dom": "^18.3.0", 37 | "postcss": "^8.4.41", 38 | "tailwindcss": "^3.4.10" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Base Options: */ 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "skipDefaultLibCheck": true, 7 | "target": "es2022", 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "moduleDetection": "force", 11 | "isolatedModules": true, 12 | 13 | /* Strictness */ 14 | "strict": true, 15 | "noUncheckedIndexedAccess": true, 16 | "checkJs": true, 17 | 18 | /* Bundled projects */ 19 | "lib": ["dom", "dom.iterable", "ES2022"], 20 | "noEmit": true, 21 | "module": "ESNext", 22 | "moduleResolution": "Bundler", 23 | "jsx": "preserve", 24 | "plugins": [{ "name": "next" }], 25 | "incremental": true, 26 | 27 | /* Path Aliases */ 28 | 29 | "paths": { 30 | "@/*": ["./*"], 31 | // Make typescript know about the platform folder with its alias 32 | "~platform/*": ["../platform/*"] 33 | } 34 | }, 35 | "include": [ 36 | ".eslintrc.cjs", 37 | "next-env.d.ts", 38 | "**/*.ts", 39 | "**/*.tsx", 40 | "**/*.cjs", 41 | "**/*.js", 42 | ".next/types/**/*.ts", 43 | "postcss.config.js", 44 | "next.config.js" 45 | ], 46 | "exclude": ["node_modules"] 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | /** @type {import("next").NextConfig} */ 4 | const config = { 5 | // Checked in CI anyways 6 | typescript: { 7 | ignoreBuildErrors: true 8 | }, 9 | eslint: { 10 | ignoreDuringBuilds: true 11 | }, 12 | output: 'standalone', 13 | experimental: { 14 | outputFileTracingRoot: join( 15 | new URL('.', import.meta.url).pathname, 16 | '../../' 17 | ), 18 | outputFileTracingIncludes: { 19 | '/': ['./public/*'] 20 | }, 21 | instrumentationHook: true 22 | }, 23 | productionBrowserSourceMaps: true, 24 | // https://posthog.com/docs/advanced/proxy/nextjs 25 | async rewrites() { 26 | return [ 27 | { 28 | source: '/ingest/static/:path*', 29 | destination: 'https://us-assets.i.posthog.com/static/:path*' 30 | }, 31 | { 32 | source: '/ingest/:path*', 33 | destination: 'https://us.i.posthog.com/:path*' 34 | }, 35 | { 36 | source: '/ingest/decide', 37 | destination: 'https://us.i.posthog.com/decide' 38 | } 39 | ]; 40 | }, 41 | // This is required to support PostHog trailing slash API requests 42 | skipTrailingSlashRedirect: true 43 | }; 44 | 45 | export default config; 46 | -------------------------------------------------------------------------------- /apps/mail-bridge/utils/tiptap-utils.ts: -------------------------------------------------------------------------------- 1 | import type { JSONContent } from '@u22n/tiptap/react'; 2 | import { validateTypeId } from '@u22n/utils/typeid'; 3 | 4 | export function walkAndReplaceImages( 5 | jsonContent: JSONContent, 6 | callback: (url: string) => string 7 | ) { 8 | for (const element of jsonContent.content ?? []) { 9 | if (element.type === 'image' && typeof element.attrs?.src === 'string') { 10 | const newUrl = callback(element.attrs.src); 11 | if (element.attrs) { 12 | element.attrs.src = newUrl; 13 | } 14 | } 15 | if (element.content) { 16 | walkAndReplaceImages(element, callback); 17 | } 18 | } 19 | } 20 | 21 | export function tryParseInlineAttachmentUrl(url: string) { 22 | try { 23 | const urlObject = new URL(url); 24 | const [base, orgShortcode, attachmentPublicId, fileName] = 25 | urlObject.pathname.split('/').splice(1); 26 | 27 | if ( 28 | base !== 'attachment' || 29 | !orgShortcode || 30 | !validateTypeId('convoAttachments', attachmentPublicId) || 31 | !fileName 32 | ) 33 | return null; 34 | 35 | return { 36 | orgShortcode, 37 | attachmentPublicId, 38 | fileName 39 | }; 40 | } catch (e) { 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/src/lib/utils'; 5 | 6 | const badgeVariants = cva( 7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | 'border-transparent bg-accent-9 text-accent-1 hover:bg-accent-8', 13 | secondary: 14 | 'border-transparent bg-base-9 text-secondary-foreground hover:bg-base-8', 15 | destructive: 'border-transparent bg-red-9 text-red-1 hover:bg-red-8', 16 | outline: 'text-base-12' 17 | } 18 | }, 19 | defaultVariants: { 20 | variant: 'default' 21 | } 22 | } 23 | ); 24 | 25 | export interface BadgeProps 26 | extends React.HTMLAttributes, 27 | VariantProps {} 28 | 29 | function Badge({ className, variant, ...props }: BadgeProps) { 30 | return ( 31 |
35 | ); 36 | } 37 | 38 | export { Badge, badgeVariants }; 39 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as SwitchPrimitives from '@radix-ui/react-switch'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/src/lib/utils'; 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 17 | 22 | 23 | )); 24 | Switch.displayName = SwitchPrimitives.Root.displayName; 25 | 26 | export { Switch }; 27 | -------------------------------------------------------------------------------- /apps/storage/api/deleteAttachments.ts: -------------------------------------------------------------------------------- 1 | import { DeleteObjectsCommand } from '@aws-sdk/client-s3'; 2 | import { checkAuthorizedService } from '../middlewares'; 3 | import { zValidator } from '@u22n/hono/helpers'; 4 | import { createHonoApp } from '@u22n/hono'; 5 | import type { Ctx } from '../ctx'; 6 | import { s3Client } from '../s3'; 7 | import { env } from '../env'; 8 | import { z } from 'zod'; 9 | 10 | export const deleteAttachmentsApi = createHonoApp().post( 11 | '/attachments/deleteAttachments', 12 | checkAuthorizedService, 13 | zValidator( 14 | 'json', 15 | z.object({ 16 | attachments: z.string().array() 17 | }) 18 | ), 19 | async (c) => { 20 | const { attachments } = c.req.valid('json'); 21 | const attachmentKeys = attachments.map((attachment) => ({ 22 | Key: attachment 23 | })); 24 | const command = new DeleteObjectsCommand({ 25 | Bucket: env.STORAGE_S3_BUCKET_ATTACHMENTS, 26 | Delete: { 27 | Objects: attachmentKeys 28 | } 29 | }); 30 | 31 | await s3Client.send(command).catch((err: Error) => { 32 | console.error('Error while deleting some attachments', { 33 | attachments, 34 | err 35 | }); 36 | }); 37 | 38 | return c.json({ message: 'ok' }); 39 | } 40 | ); 41 | -------------------------------------------------------------------------------- /apps/web/src/components/shared/editable-text.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { cn } from '@/src/lib/utils'; 3 | 4 | type EditableTextProps = { 5 | value: string; 6 | setValue: (value: string) => void; 7 | }; 8 | 9 | export function EditableText({ value, setValue }: EditableTextProps) { 10 | const [editingState, setEditingState] = useState(value); 11 | const [isEditing, setIsEditing] = useState(false); 12 | const inputRef = useRef(null); 13 | 14 | return isEditing ? ( 15 | setEditingState(e.target.value)} 19 | onBlur={() => { 20 | if (editingState.trim() === '' || editingState === value) { 21 | setEditingState(value); 22 | } else { 23 | setValue(editingState); 24 | } 25 | setIsEditing(false); 26 | }} 27 | className="w-fit border-none outline-none" 28 | /> 29 | ) : ( 30 | { 32 | setEditingState(value); 33 | setIsEditing(true); 34 | setTimeout(() => inputRef.current?.focus(), 10); 35 | }} 36 | className={cn(value && 'decoration-blue-5 underline')}> 37 | {value || '...'} 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /apps/platform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@u22n/platform", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "scripts": { 7 | "dev": "tsx watch --clear-screen=false --import ./tracing.ts app.ts", 8 | "start": "node --import ./.output/tracing.js .output/app.js", 9 | "build": "tsup", 10 | "check": "tsc --noEmit" 11 | }, 12 | "exports": { 13 | "./trpc": { 14 | "types": "./trpc/index.ts" 15 | } 16 | }, 17 | "dependencies": { 18 | "@simplewebauthn/server": "^9.0.3", 19 | "@t3-oss/env-core": "^0.11.0", 20 | "@trpc/client": "11.0.0-rc.485", 21 | "@trpc/server": "11.0.0-rc.485", 22 | "@u22n/database": "workspace:^", 23 | "@u22n/hono": "workspace:^", 24 | "@u22n/otel": "workspace:^", 25 | "@u22n/realtime": "workspace:^", 26 | "@u22n/tiptap": "workspace:^", 27 | "@u22n/utils": "workspace:^", 28 | "@unkey/ratelimit": "^0.4.3", 29 | "lucia": "^3.2.0", 30 | "oslo": "^1.2.1", 31 | "superjson": "^2.2.1", 32 | "ua-parser-js": "2.0.0-beta.3", 33 | "unstorage": "^1.10.2", 34 | "zod": "^3.23.8" 35 | }, 36 | "devDependencies": { 37 | "@simplewebauthn/types": "^9.0.1", 38 | "@u22n/mail-bridge": "workspace:^", 39 | "@uninbox-ee/billing": "workspace:^", 40 | "tsup": "^8.2.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/storage/api/internalPresign.ts: -------------------------------------------------------------------------------- 1 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 2 | import { checkAuthorizedService } from '../middlewares'; 3 | import { PutObjectCommand } from '@aws-sdk/client-s3'; 4 | import { typeIdGenerator } from '@u22n/utils/typeid'; 5 | import { zValidator } from '@u22n/hono/helpers'; 6 | import { createHonoApp } from '@u22n/hono'; 7 | import type { Ctx } from '../ctx'; 8 | import { s3Client } from '../s3'; 9 | import { env } from '../env'; 10 | import { z } from 'zod'; 11 | 12 | export const internalPresignApi = createHonoApp().post( 13 | '/attachments/internalPresign', 14 | checkAuthorizedService, 15 | zValidator( 16 | 'json', 17 | z.object({ 18 | orgPublicId: z.string(), 19 | filename: z.string() 20 | }) 21 | ), 22 | async (c) => { 23 | const { filename, orgPublicId } = c.req.valid('json'); 24 | const attachmentPublicId = typeIdGenerator('convoAttachments'); 25 | 26 | const command = new PutObjectCommand({ 27 | Bucket: env.STORAGE_S3_BUCKET_ATTACHMENTS, 28 | Key: `${orgPublicId}/${attachmentPublicId}/${filename}` 29 | }); 30 | const signedUrl = await getSignedUrl(s3Client, command, { 31 | expiresIn: 3600 32 | }); 33 | return c.json({ publicId: attachmentPublicId, signedUrl }); 34 | } 35 | ); 36 | -------------------------------------------------------------------------------- /packages/tiptap/components/editor-command-item.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentPropsWithoutRef } from 'react'; 2 | import type { Editor, Range } from '@tiptap/core'; 3 | import { useCurrentEditor } from '@tiptap/react'; 4 | import { CommandEmpty, CommandItem } from 'cmdk'; 5 | import { rangeAtom } from '../utils/atoms'; 6 | import { useAtomValue } from 'jotai'; 7 | import { forwardRef } from 'react'; 8 | 9 | interface EditorCommandItemProps { 10 | readonly onCommand: ({ 11 | editor, 12 | range 13 | }: { 14 | editor: Editor; 15 | range: Range; 16 | }) => void; 17 | } 18 | 19 | export const EditorCommandItem = forwardRef< 20 | HTMLDivElement, 21 | EditorCommandItemProps & ComponentPropsWithoutRef 22 | >(({ children, onCommand, ...rest }, ref) => { 23 | const { editor } = useCurrentEditor(); 24 | const range = useAtomValue(rangeAtom); 25 | 26 | if (!editor || !range) return null; 27 | 28 | return ( 29 | { 32 | onCommand({ editor, range }); 33 | }} 34 | {...rest}> 35 | {children} 36 | 37 | ); 38 | }); 39 | 40 | EditorCommandItem.displayName = 'EditorCommandItem'; 41 | 42 | export const EditorCommandEmpty = CommandEmpty; 43 | 44 | export default EditorCommandItem; 45 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/settings/org/users/members/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PageTitle } from '../../../_components/page-title'; 4 | import { Button } from '@/src/components/shadcn-ui/button'; 5 | import { DataTable } from '@/src/components/shared/table'; 6 | import { useOrgShortcode } from '@/src/hooks/use-params'; 7 | import { columns } from './_components/columns'; 8 | import { platform } from '@/src/lib/trpc'; 9 | import Link from 'next/link'; 10 | 11 | export default function Page() { 12 | const orgShortcode = useOrgShortcode(); 13 | const { data: memberList, isLoading } = 14 | platform.org.users.members.getOrgMembers.useQuery({ 15 | orgShortcode 16 | }); 17 | 18 | return ( 19 |
20 | 23 | 28 | 29 | 30 | {isLoading &&
Loading...
} 31 | {memberList && ( 32 | 36 | )} 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /packages/database/dbClean.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 4 | /* eslint-disable @typescript-eslint/no-floating-promises */ 5 | /* eslint-disable no-console */ 6 | 7 | import * as schema from './schema'; 8 | import { sql } from './orm'; 9 | import { db } from '.'; 10 | 11 | (async () => { 12 | console.log('🔥 Cleaning the database of all entries'); 13 | console.time('🧼 All clean'); 14 | 15 | const tableNames: string[] = []; 16 | 17 | for (const key in schema) { 18 | if (!key.includes('Relations')) { 19 | const tableName = 20 | // @ts-expect-error, don't care about types here 21 | schema[key][Symbol.for('drizzle:Name')]; 22 | tableNames.push(tableName); 23 | } 24 | } 25 | 26 | // Create an array of Promises for executing the truncate statements 27 | const truncatePromises = tableNames.map(async (tableName) => { 28 | try { 29 | await db.execute(sql.raw(`drop table ${tableName}`)); 30 | } catch (e) { 31 | console.error(e); 32 | } 33 | }); 34 | 35 | // Execute all the truncate statements concurrently 36 | await Promise.all(truncatePromises); 37 | 38 | console.timeEnd('🧼 All clean'); 39 | })(); 40 | -------------------------------------------------------------------------------- /packages/otel/hono.ts: -------------------------------------------------------------------------------- 1 | import { createMiddleware } from '@u22n/hono/helpers'; 2 | import { getTracer, inActiveSpan } from './helpers'; 3 | 4 | function formatHeaders(headers: Record | Headers) { 5 | return Object.entries(headers).map(([key, value]) => `${key}: ${value}`); 6 | } 7 | 8 | export const opentelemetry = (name?: string) => { 9 | const tracer = getTracer(name ?? 'hono'); 10 | return createMiddleware<{ Variables: { requestId: string } }>((c, next) => 11 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 12 | inActiveSpan(async (parent) => { 13 | parent?.updateName(`HTTP ${c.req.method} ${c.req.path}`); 14 | if (c.req.method === 'OPTIONS') return next(); 15 | return tracer.startActiveSpan(`Hono Handler`, async (span) => { 16 | span?.addEvent('hono.start'); 17 | span?.setAttributes({ 18 | 'hono.req.headers': formatHeaders(c.req.header()) 19 | }); 20 | await next().catch((e) => { 21 | if (e instanceof Error) span?.recordException(e); 22 | throw e; 23 | }); 24 | span?.setAttributes({ 25 | 'hono.res.status': c.res.status, 26 | 'hono.res.headers': formatHeaders(c.res.headers) 27 | }); 28 | span?.addEvent('hono.end'); 29 | }); 30 | }) 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /ee/apps/billing/app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createHonoApp, 3 | setupCors, 4 | setupErrorHandlers, 5 | setupHealthReporting, 6 | setupHonoListener, 7 | setupRuntime, 8 | setupTrpcHandler, 9 | setupRouteLogger 10 | } from '@u22n/hono'; 11 | import { stripeWebhookMiddleware } from './middlewares'; 12 | import { validateLicense } from './validateLicenseKey'; 13 | import { stripeApi } from './routes/stripe'; 14 | import { trpcBillingRouter } from './trpc'; 15 | import { stripeData } from './stripe'; 16 | import { db } from '@u22n/database'; 17 | import { type Ctx } from './ctx'; 18 | import { env } from './env'; 19 | 20 | await validateLicense(); 21 | 22 | const app = createHonoApp(); 23 | setupRouteLogger(app, env.NODE_ENV === 'development'); 24 | setupCors(app, { origin: env.WEBAPP_URL }); 25 | setupHealthReporting(app, { service: 'Billing' }); 26 | setupErrorHandlers(app); 27 | 28 | setupTrpcHandler(app, trpcBillingRouter, (_, c) => { 29 | const authToken = c.req.header('Authorization'); 30 | return { 31 | auth: authToken === env.BILLING_KEY, 32 | stripe: stripeData, 33 | db 34 | }; 35 | }); 36 | 37 | // Stripe webhook middleware & API 38 | app.use('/stripe/*', stripeWebhookMiddleware); 39 | app.route('/stripe', stripeApi); 40 | 41 | const cleanup = setupHonoListener(app, { port: env.PORT }); 42 | setupRuntime([cleanup]); 43 | -------------------------------------------------------------------------------- /apps/mail-bridge/trpc/trpc.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError, initTRPC } from '@trpc/server'; 2 | import { getTracer } from '@u22n/otel/helpers'; 3 | import { flatten } from '@u22n/otel/exports'; 4 | import type { TRPCContext } from '../ctx'; 5 | import superjson from 'superjson'; 6 | 7 | export const trpcContext = initTRPC 8 | .context() 9 | .create({ transformer: superjson }); 10 | 11 | const isServiceAuthenticated = trpcContext.middleware(({ next, ctx }) => { 12 | if (!ctx.auth) { 13 | throw new TRPCError({ code: 'UNAUTHORIZED' }); 14 | } 15 | return next({ ctx }); 16 | }); 17 | 18 | const trpcTracer = getTracer('mail-bridge/trpc'); 19 | export const publicProcedure = trpcContext.procedure.use( 20 | async ({ type, path, next }) => 21 | trpcTracer.startActiveSpan(`TRPC ${type} ${path}`, async (span) => { 22 | const result = await next(); 23 | if (span) { 24 | span.setAttributes( 25 | flatten({ 26 | trpc: { 27 | type: type, 28 | path: path, 29 | ok: result.ok 30 | } 31 | }) 32 | ); 33 | } 34 | return result; 35 | }) 36 | ); 37 | export const protectedProcedure = publicProcedure.use(isServiceAuthenticated); 38 | export const router = trpcContext.router; 39 | export const middleware = trpcContext.middleware; 40 | -------------------------------------------------------------------------------- /apps/worker/app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createHonoApp, 3 | setupErrorHandlers, 4 | setupHealthReporting, 5 | setupHonoListener, 6 | setupRouteLogger, 7 | setupRuntime, 8 | setupTrpcHandler 9 | } from '@u22n/hono'; 10 | import { sessionCleanupCronJob } from './services/expired-session-cleanup'; 11 | import { dnsCheckWorker, masterCronJob } from './services/dns-check-queue'; 12 | import { commandControlAuthMiddleware } from './middlewares'; 13 | import { jobsRouter } from './trpc/routers/jobs-router'; 14 | import { opentelemetry } from '@u22n/otel/hono'; 15 | import type { Ctx } from './ctx'; 16 | import { env } from './env'; 17 | 18 | const app = createHonoApp(); 19 | app.use(opentelemetry('worker/hono')); 20 | 21 | setupRouteLogger(app, env.NODE_ENV === 'development'); 22 | setupHealthReporting(app, { service: 'Worker' }); 23 | setupErrorHandlers(app); 24 | 25 | // Auth middleware 26 | app.use('*', commandControlAuthMiddleware); 27 | 28 | setupTrpcHandler(app, jobsRouter); 29 | const cleanup = setupHonoListener(app, { port: env.PORT }); 30 | 31 | void dnsCheckWorker.run(); 32 | masterCronJob.start(); 33 | sessionCleanupCronJob.start(); 34 | 35 | console.info('Worker Jobs are running'); 36 | 37 | setupRuntime([ 38 | cleanup, 39 | () => dnsCheckWorker.close(), 40 | () => sessionCleanupCronJob.stop(), 41 | () => masterCronJob.stop() 42 | ]); 43 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDotEnv": [".env.local"], 4 | "globalDependencies": ["**/.env.*local"], 5 | "pipeline": { 6 | "check": { 7 | "dependsOn": ["^check"] 8 | }, 9 | "build": { 10 | "dependsOn": ["^build"], 11 | "outputs": [".output/**", ".next/**"] 12 | }, 13 | "start": { 14 | "cache": false 15 | }, 16 | "db:migrate": { 17 | "cache": false 18 | }, 19 | "db:generate": { 20 | "cache": false 21 | }, 22 | "db:push": { 23 | "cache": false 24 | }, 25 | "db:drop": { 26 | "cache": false 27 | }, 28 | "db:studio": { 29 | "cache": false 30 | }, 31 | "lint": { 32 | "outputs": [] 33 | }, 34 | "docker:up": { 35 | "cache": false 36 | }, 37 | "dev": { 38 | "dependsOn": ["^docker:up", "db:studio"], 39 | "cache": false 40 | }, 41 | "dev:r": { 42 | "dependsOn": ["db:studio:r"], 43 | "cache": false 44 | }, 45 | "db:studio:r": { 46 | "cache": false 47 | }, 48 | "ee:dev": { 49 | "dependsOn": [], 50 | "cache": false 51 | }, 52 | "ee:build": { 53 | "dependsOn": ["^build"], 54 | "outputs": [".output/**", ".next/**"] 55 | }, 56 | "cloud": { 57 | "cache": false 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/check_and_build_pull_requests.yaml: -------------------------------------------------------------------------------- 1 | name: check-and-build-pull-requests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | 12 | concurrency: 13 | cancel-in-progress: true 14 | group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha }} 15 | 16 | jobs: 17 | check: 18 | name: Check and Build 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 15 21 | steps: 22 | - name: Checkout Code 🛎 23 | uses: actions/checkout@v4 24 | 25 | - name: Cache turbo build setup 🚀 26 | uses: actions/cache@v4 27 | with: 28 | path: .turbo 29 | key: ${{ runner.os }}-turbo-${{ github.sha }} 30 | restore-keys: | 31 | ${{ runner.os }}-turbo- 32 | 33 | - name: Setup pnpm 📦 34 | uses: pnpm/action-setup@v3 35 | 36 | - name: Setup Node.js 🟩 37 | uses: actions/setup-node@v4 38 | with: 39 | cache: 'pnpm' 40 | node-version: '20' 41 | 42 | - name: Install Dependencies 📦 43 | run: pnpm install 44 | 45 | - name: Copy Example Env 📝 46 | run: cp .env.local.example .env.local 47 | 48 | - name: Check 🚨 49 | run: pnpm check 50 | 51 | - name: Build 🏗 52 | run: pnpm build:all 53 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as PopoverPrimitive from '@radix-ui/react-popover'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/src/lib/utils'; 5 | 6 | const Popover = PopoverPrimitive.Root; 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger; 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 14 | 15 | 25 | 26 | )); 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 28 | 29 | export { Popover, PopoverTrigger, PopoverContent }; 30 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/settings/org/mail/addresses/_components/columns.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createColumnHelper, type ColumnDef } from '@tanstack/react-table'; 4 | import type { RouterOutputs } from '@/src/lib/trpc'; 5 | 6 | type EmailIdentity = 7 | RouterOutputs['org']['mail']['emailIdentities']['getOrgEmailIdentities']['emailIdentityData'][number]; 8 | 9 | const columnHelper = createColumnHelper(); 10 | 11 | export const columns: ColumnDef[] = [ 12 | columnHelper.display({ 13 | id: 'username', 14 | header: 'Username', 15 | cell: ({ row }) => ( 16 |
{row.original.username}
17 | ) 18 | }), 19 | columnHelper.display({ 20 | id: 'domain', 21 | header: 'Domain', 22 | cell: ({ row }) => ( 23 |
{row.original.domainName}
24 | ) 25 | }), 26 | columnHelper.display({ 27 | id: 'send-name', 28 | header: 'Send Name', 29 | cell: ({ row }) => ( 30 |
{row.original.sendName}
31 | ) 32 | }), 33 | columnHelper.display({ 34 | id: 'destination', 35 | header: 'Destination', 36 | cell: ({ row }) => ( 37 |
38 | {row.original.routingRules.description} 39 |
40 | ) 41 | }) 42 | ]; 43 | -------------------------------------------------------------------------------- /apps/web/src/components/copy-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button, type ButtonProps } from '@/src/components/shadcn-ui/button'; 4 | import { type ElementRef, forwardRef, useState } from 'react'; 5 | import { Check, Copy } from '@phosphor-icons/react'; 6 | import { cn, copyToClipboard } from '../lib/utils'; 7 | import { toast } from 'sonner'; 8 | 9 | export const CopyButton = forwardRef< 10 | ElementRef<'button'>, 11 | Omit & { 12 | text: string; 13 | onCopy?: (data: string) => void; 14 | iconSize?: number; 15 | } 16 | >(({ text, onCopy, iconSize = 15, ...props }, ref) => { 17 | const [hasCopied, setHasCopied] = useState(false); 18 | 19 | return ( 20 | 39 | ); 40 | }); 41 | 42 | CopyButton.displayName = 'CopyButton'; 43 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | import * as HoverCardPrimitive from '@radix-ui/react-hover-card'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/src/lib/utils'; 5 | 6 | const HoverCard = HoverCardPrimitive.Root; 7 | 8 | const HoverCardTrigger = HoverCardPrimitive.Trigger; 9 | 10 | const HoverCardContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 14 | 24 | )); 25 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; 26 | 27 | const HoverCardPortal = HoverCardPrimitive.Portal; 28 | 29 | export { HoverCard, HoverCardTrigger, HoverCardContent, HoverCardPortal }; 30 | -------------------------------------------------------------------------------- /apps/web/src/lib/upload.ts: -------------------------------------------------------------------------------- 1 | type UploadTrackerOptions = { 2 | formData: FormData | File; 3 | method: 'POST' | 'PUT'; 4 | url: string; 5 | headers?: Record; 6 | includeCredentials?: boolean; 7 | onProgress?: (progress: number) => void; 8 | }; 9 | 10 | export default function uploadTracker({ 11 | formData, 12 | method, 13 | url, 14 | headers, 15 | includeCredentials = true, 16 | onProgress 17 | }: UploadTrackerOptions) { 18 | return new Promise((resolve, reject) => { 19 | const xhr = new XMLHttpRequest(); 20 | xhr.open(method, url, true); 21 | 22 | if (headers) { 23 | Object.entries(headers).forEach(([key, value]) => { 24 | xhr.setRequestHeader(key, value); 25 | }); 26 | } 27 | 28 | xhr.upload.onprogress = (event) => { 29 | if (event.lengthComputable) { 30 | const progress = (event.loaded / event.total) * 100; 31 | onProgress?.(progress); 32 | } 33 | }; 34 | 35 | xhr.onload = () => { 36 | if (xhr.status >= 200 && xhr.status < 300) { 37 | resolve(xhr.response); 38 | } else { 39 | reject(new Error(`Failed to upload file: ${xhr.statusText}`)); 40 | } 41 | }; 42 | 43 | xhr.onerror = () => { 44 | reject(new Error('Failed to upload file')); 45 | }; 46 | 47 | xhr.withCredentials = includeCredentials; 48 | 49 | xhr.send(formData); 50 | }); 51 | } 52 | 53 | export { uploadTracker }; 54 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/src/lib/utils'; 5 | 6 | const TooltipProvider = React.memo(TooltipPrimitive.Provider); 7 | 8 | const Tooltip = React.memo(TooltipPrimitive.Root); 9 | 10 | const TooltipTrigger = React.memo(TooltipPrimitive.Trigger); 11 | 12 | const TooltipContent = React.memo( 13 | React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, sideOffset = 4, ...props }, ref) => ( 17 | 18 | 27 | 28 | )) 29 | ); 30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 31 | 32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 33 | -------------------------------------------------------------------------------- /apps/platform/utils/tiptap-utils.ts: -------------------------------------------------------------------------------- 1 | import type { JSONContent } from '@u22n/tiptap/react'; 2 | import { validateTypeId } from '@u22n/utils/typeid'; 3 | 4 | export function walkAndReplaceImages( 5 | jsonContent: JSONContent, 6 | callback: (url: string) => string 7 | ) { 8 | for (const element of jsonContent.content ?? []) { 9 | if (element.type === 'image' && typeof element.attrs?.src === 'string') { 10 | const newUrl = callback(element.attrs.src); 11 | if (element.attrs) { 12 | element.attrs.src = newUrl; 13 | } 14 | } 15 | if (element.content) { 16 | walkAndReplaceImages(element, callback); 17 | } 18 | } 19 | } 20 | 21 | export function tryParseInlineProxyUrl(url: string) { 22 | try { 23 | const urlObject = new URL(url); 24 | const [base, orgShortcode, attachmentPublicId, fileName] = 25 | urlObject.pathname.split('/').splice(1); 26 | if ( 27 | base !== 'inline-proxy' || 28 | !orgShortcode || 29 | !validateTypeId('convoAttachments', attachmentPublicId) || 30 | !fileName 31 | ) 32 | return null; 33 | const fileType = decodeURIComponent( 34 | urlObject.searchParams.get('type') ?? 'image/png' 35 | ); 36 | const size = Number(urlObject.searchParams.get('size') ?? 0) || 0; 37 | 38 | return { 39 | orgShortcode, 40 | attachmentPublicId, 41 | fileName, 42 | fileType, 43 | size, 44 | inline: true 45 | }; 46 | } catch (e) { 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/settings/org/users/invites/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PageTitle } from '../../../_components/page-title'; 4 | import { DataTable } from '@/src/components/shared/table'; 5 | import { InviteModal } from './_components/invite-modal'; 6 | import { useOrgShortcode } from '@/src/hooks/use-params'; 7 | import { SpinnerGap } from '@phosphor-icons/react'; 8 | import { columns } from './_components/columns'; 9 | import { platform } from '@/src/lib/trpc'; 10 | 11 | export default function Page() { 12 | const orgShortcode = useOrgShortcode(); 13 | const { data: inviteList, isLoading } = 14 | platform.org.users.invites.viewInvites.useQuery({ 15 | orgShortcode 16 | }); 17 | 18 | return ( 19 |
20 | 23 | 24 | 25 | 26 | {isLoading && ( 27 |
28 | 32 | Loading... 33 |
34 | )} 35 | {inviteList && ( 36 |
37 | 41 |
42 | )} 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('eslint').Linter.Config} 3 | */ 4 | module.exports = { 5 | root: true, 6 | parser: '@typescript-eslint/parser', 7 | plugins: ['@typescript-eslint', 'drizzle'], 8 | parserOptions: { 9 | project: true 10 | }, 11 | extends: [ 12 | 'plugin:drizzle/all', 13 | 'plugin:@typescript-eslint/recommended-type-checked', 14 | 'plugin:@typescript-eslint/stylistic-type-checked' 15 | ], 16 | rules: { 17 | '@typescript-eslint/array-type': 'off', 18 | '@typescript-eslint/consistent-type-definitions': 'off', 19 | '@typescript-eslint/consistent-type-imports': [ 20 | 'warn', 21 | { prefer: 'type-imports', fixStyle: 'inline-type-imports' } 22 | ], 23 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 24 | '@typescript-eslint/require-await': 'off', 25 | '@typescript-eslint/no-misused-promises': [ 26 | 'error', 27 | { checksVoidReturn: { attributes: false } } 28 | ], 29 | 'no-console': ['error', { allow: ['info', 'warn', 'trace', 'error'] }] 30 | }, 31 | overrides: [ 32 | { 33 | files: ['./packages/database/**/*'], 34 | plugins: ['@u22n/custom'], 35 | rules: { '@u22n/custom/table-needs-org-id': 'error' } 36 | }, 37 | { 38 | files: ['./apps/web/**/*'], 39 | extends: ['next/core-web-vitals'], 40 | rules: { 41 | 'react/no-children-prop': ['warn', { allowFunctions: true }], 42 | '@next/next/no-img-element': 'off' 43 | } 44 | } 45 | ] 46 | }; 47 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/settings/_components/page-title.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/src/components/shadcn-ui/button'; 4 | import { useOrgShortcode } from '@/src/hooks/use-params'; 5 | import { useIsMobile } from '@/src/hooks/use-is-mobile'; 6 | import { ArrowLeft } from '@phosphor-icons/react'; 7 | import Link from 'next/link'; 8 | 9 | type PageTitleProps = { 10 | title: string; 11 | description?: string; 12 | children?: React.ReactNode; 13 | backButtonLink?: string; 14 | }; 15 | export function PageTitle({ 16 | title, 17 | description, 18 | children, 19 | backButtonLink 20 | }: PageTitleProps) { 21 | const orgShortcode = useOrgShortcode(); 22 | const isMobile = useIsMobile(); 23 | 24 | return ( 25 |
26 |
27 | {isMobile && ( 28 | 36 | )} 37 |
38 | {title} 39 | {description && ( 40 | {description} 41 | )} 42 |
43 |
44 | 45 | {children} 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /apps/platform/utils/session.ts: -------------------------------------------------------------------------------- 1 | import { setCookie, type Context } from '@u22n/hono/helpers'; 2 | import { accounts } from '@u22n/database/schema'; 3 | import type { TypeId } from '@u22n/utils/typeid'; 4 | import { eq } from '@u22n/database/orm'; 5 | import { UAParser } from 'ua-parser-js'; 6 | import { db } from '@u22n/database'; 7 | import { lucia } from './auth'; 8 | 9 | type SessionInfo = { 10 | accountId: number; 11 | username: string; 12 | publicId: TypeId<'account'>; 13 | }; 14 | 15 | /** 16 | * Create a Lucia session cookie for given session info, set the cookie in for the event, update last login and return the cookie. 17 | */ 18 | export async function createLuciaSessionCookie( 19 | event: Context, 20 | info: SessionInfo 21 | ) { 22 | const { device, os, browser } = UAParser(event.req.header('User-Agent')); 23 | const userDevice = 24 | device.type === 'mobile' 25 | ? device.toString() 26 | : (device.vendor ?? device.model ?? device.type ?? 'Unknown'); 27 | const { accountId, username, publicId } = info; 28 | const accountSession = await lucia.createSession(accountId, { 29 | account: { 30 | id: accountId, 31 | username, 32 | publicId 33 | }, 34 | device: userDevice, 35 | os: `${browser.toString()} ${os.name ?? 'Unknown'}` 36 | }); 37 | const cookie = lucia.createSessionCookie(accountSession.id); 38 | setCookie(event, cookie.name, cookie.value, cookie.attributes); 39 | await db 40 | .update(accounts) 41 | .set({ lastLoginAt: new Date() }) 42 | .where(eq(accounts.id, accountId)); 43 | return cookie; 44 | } 45 | -------------------------------------------------------------------------------- /apps/web/src/components/password-input.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Input, type InputProps } from '@/src/components/shadcn-ui/input'; 4 | import { type ElementRef, forwardRef, useState } from 'react'; 5 | import { Button } from '@/src/components/shadcn-ui/button'; 6 | import { Eye, EyeSlash } from '@phosphor-icons/react'; 7 | import { cn } from '../lib/utils'; 8 | 9 | export const PasswordInput = forwardRef< 10 | ElementRef<'input'>, 11 | Omit 12 | >(({ className, ...props }, ref) => { 13 | const [showPassword, setShowPassword] = useState(false); 14 | 15 | return ( 16 | ( 21 | 43 | )} 44 | {...props} 45 | /> 46 | ); 47 | }); 48 | 49 | PasswordInput.displayName = 'PasswordInput'; 50 | -------------------------------------------------------------------------------- /ee/apps/billing/validateLicenseKey.ts: -------------------------------------------------------------------------------- 1 | import { env } from './env'; 2 | 3 | async function validateLicenseKey(key: string, appUrl: string) { 4 | const response = (await fetch('https://ee.u22n.com/api/license/', { 5 | method: 'POST', 6 | headers: { 'Content-Type': 'application/json' }, 7 | body: JSON.stringify({ key, appUrl }) 8 | }).then((response) => { 9 | if (!response.ok) { 10 | throw new Error('Network response was not ok'); 11 | } 12 | return response.json(); 13 | })) as { valid: boolean }; 14 | 15 | if (!response.valid) { 16 | throw new Error( 17 | '🚨 You are attempting to run software that requires a paid license but have not provided a valid license key. Please check https://github.com/un/inbox/tree/main/ee for more information about the license' 18 | ); 19 | } 20 | return true; 21 | } 22 | 23 | export const validateLicense = async () => { 24 | if (env.EE_LICENSE_KEY && env.WEBAPP_URL) { 25 | await validateLicenseKey(env.EE_LICENSE_KEY, env.WEBAPP_URL).catch( 26 | (error) => { 27 | console.error(error); 28 | throw new Error( 29 | '🚨 Something went wrong when trying to validate the license key. Please check https://github.com/un/inbox/tree/main/ee for more information about the license' 30 | ); 31 | } 32 | ); 33 | } else { 34 | throw new Error( 35 | '🚨 You are attempting to run software that requires a paid license but have not provided a valid license key or app url. Please check https://github.com/un/inbox/tree/main/ee for more information about the license' 36 | ); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /packages/tiptap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@u22n/tiptap", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.ts", 7 | "./react": "./react.ts", 8 | "./extensions": "./extensions/index.ts", 9 | "./extensions/slash-command": "./extensions/slash-command.ts", 10 | "./extensions/image-uploader": "./extensions/image-uploader.ts", 11 | "./components": "./components/index.ts" 12 | }, 13 | "scripts": { 14 | "check": "tsc --noEmit" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@radix-ui/react-slot": "^1.1.0", 21 | "@tiptap/core": "^2.6.6", 22 | "@tiptap/extension-color": "^2.6.6", 23 | "@tiptap/extension-link": "^2.6.6", 24 | "@tiptap/extension-placeholder": "^2.6.6", 25 | "@tiptap/extension-subscript": "^2.6.6", 26 | "@tiptap/extension-superscript": "^2.6.6", 27 | "@tiptap/extension-text-style": "^2.6.6", 28 | "@tiptap/extension-underline": "^2.6.6", 29 | "@tiptap/html": "^2.6.6", 30 | "@tiptap/pm": "^2.6.6", 31 | "@tiptap/react": "^2.6.6", 32 | "@tiptap/starter-kit": "^2.6.6", 33 | "@tiptap/suggestion": "^2.6.6", 34 | "cmdk": "^1.0.0", 35 | "jotai": "^2.9.3", 36 | "tippy.js": "^6.3.7", 37 | "tiptap-extension-resize-image": "^1.1.8", 38 | "tiptap-markdown": "^0.8.10", 39 | "tunnel-rat": "^0.1.2" 40 | }, 41 | "peerDependencies": { 42 | "react": "^18.3.1", 43 | "react-dom": "^18.3.1" 44 | }, 45 | "devDependencies": { 46 | "@types/react": "^18.3.4", 47 | "@types/react-dom": "^18.3.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/mail-bridge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@u22n/mail-bridge", 3 | "private": true, 4 | "type": "module", 5 | "version": "1.0.0", 6 | "scripts": { 7 | "dev": "tsx watch --clear-screen=false --import ./tracing.ts app.ts", 8 | "start": "node --import ./.output/tracing.js .output/app.js", 9 | "build": "tsup", 10 | "check": "tsc --noEmit", 11 | "mock:incoming-mail": "tsx ./scripts/mock-incoming.ts", 12 | "stress:email": "tsx ./scripts/email-stress-test.ts" 13 | }, 14 | "exports": { 15 | "./trpc": { 16 | "types": "./trpc/index.ts" 17 | } 18 | }, 19 | "dependencies": { 20 | "@t3-oss/env-core": "^0.11.0", 21 | "@trpc/client": "11.0.0-rc.485", 22 | "@trpc/server": "11.0.0-rc.485", 23 | "@u22n/database": "workspace:*", 24 | "@u22n/hono": "workspace:^", 25 | "@u22n/mailtools": "^0.1.2", 26 | "@u22n/otel": "workspace:^", 27 | "@u22n/realtime": "workspace:^", 28 | "@u22n/tiptap": "workspace:^", 29 | "@u22n/utils": "workspace:*", 30 | "bullmq": "^5.12.10", 31 | "dompurify": "^3.1.6", 32 | "drizzle-orm": "^0.33.0", 33 | "jsdom": "^24.1.1", 34 | "mailauth": "^4.6.9", 35 | "mailparser": "^3.7.1", 36 | "mime": "^4.0.4", 37 | "mysql2": "^3.11.0", 38 | "nanoid": "^5.0.7", 39 | "nodemailer": "^6.9.14", 40 | "superjson": "^2.2.1", 41 | "zod": "^3.23.8" 42 | }, 43 | "devDependencies": { 44 | "@clack/prompts": "^0.7.0", 45 | "@types/dompurify": "^3.0.5", 46 | "@types/jsdom": "^21.1.7", 47 | "@types/mailparser": "^3.4.4", 48 | "@types/nodemailer": "^6.4.15", 49 | "tsup": "^8.2.4" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /apps/platform/app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createHonoApp, 3 | setupCors, 4 | setupErrorHandlers, 5 | setupHealthReporting, 6 | setupHonoListener, 7 | setupRouteLogger, 8 | setupRuntime, 9 | setupTrpcHandler 10 | } from '@u22n/hono'; 11 | import { authMiddleware, serviceMiddleware } from './middlewares'; 12 | import { realtimeApi } from './routes/realtime'; 13 | import { servicesApi } from './routes/services'; 14 | import { opentelemetry } from '@u22n/otel/hono'; 15 | import type { Ctx, TrpcContext } from './ctx'; 16 | import { trpcPlatformRouter } from './trpc'; 17 | import { authApi } from './routes/auth'; 18 | import { db } from '@u22n/database'; 19 | import { env } from './env'; 20 | 21 | const app = createHonoApp(); 22 | 23 | app.use(opentelemetry('platform/hono')); 24 | 25 | setupRouteLogger(app, env.NODE_ENV === 'development'); 26 | setupCors(app, { origin: [env.WEBAPP_URL], exposeHeaders: ['Location'] }); 27 | setupHealthReporting(app, { service: 'Platform' }); 28 | setupErrorHandlers(app); 29 | 30 | // Auth middleware 31 | app.use('*', authMiddleware); 32 | 33 | setupTrpcHandler( 34 | app, 35 | trpcPlatformRouter, 36 | (_, c) => 37 | ({ 38 | db, 39 | account: c.get('account'), 40 | org: null, 41 | event: c, 42 | selfHosted: !env.EE_LICENSE_KEY 43 | }) satisfies TrpcContext 44 | ); 45 | 46 | // Routes 47 | app.route('/auth', authApi); 48 | app.route('/realtime', realtimeApi); 49 | // Service Endpoints 50 | app.use('/services/*', serviceMiddleware); 51 | app.route('/services', servicesApi); 52 | 53 | const cleanup = setupHonoListener(app, { port: env.PORT }); 54 | setupRuntime([cleanup]); 55 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/toggle.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | import * as TogglePrimitive from '@radix-ui/react-toggle'; 3 | import * as React from 'react'; 4 | 5 | import { cn } from '@/src/lib/utils'; 6 | 7 | const toggleVariants = cva( 8 | 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-base-11 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-9', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-transparent', 13 | outline: 14 | 'border border-base-6 bg-transparent hover:bg-accent hover:text-accent-9' 15 | }, 16 | size: { 17 | default: 'h-10 px-3', 18 | xs: 'h-6 px-1.5', 19 | sm: 'h-9 px-2.5', 20 | lg: 'h-11 px-5' 21 | } 22 | }, 23 | defaultVariants: { 24 | variant: 'default', 25 | size: 'default' 26 | } 27 | } 28 | ); 29 | 30 | const Toggle = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef & 33 | VariantProps 34 | >(({ className, variant, size, ...props }, ref) => ( 35 | 40 | )); 41 | 42 | Toggle.displayName = TogglePrimitive.Root.displayName; 43 | 44 | export { Toggle, toggleVariants }; 45 | -------------------------------------------------------------------------------- /apps/web/src/components/shadcn-ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as AvatarPrimitive from '@radix-ui/react-avatar'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/src/lib/utils'; 5 | 6 | //! This component has been modified 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | Avatar.displayName = AvatarPrimitive.Root.displayName; 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )); 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )); 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 49 | 50 | export { Avatar, AvatarImage, AvatarFallback }; 51 | -------------------------------------------------------------------------------- /ee/apps/command/src/lib/get-account.ts: -------------------------------------------------------------------------------- 1 | import { sessions } from '@u22n/database/schema'; 2 | import type { NextRequest } from 'next/server'; 3 | import { eq } from '@u22n/database/orm'; 4 | import { db } from '@u22n/database'; 5 | 6 | export async function getAccount(req: NextRequest) { 7 | const sessionCookie = req.cookies.get('un-session'); 8 | 9 | if (!sessionCookie) { 10 | // Redirect to the webapp if the user is not logged in 11 | return null; 12 | } 13 | 14 | const sessionData = await db.query.sessions.findFirst({ 15 | where: eq(sessions.sessionToken, sessionCookie.value), 16 | columns: { 17 | id: true 18 | }, 19 | with: { 20 | account: { 21 | columns: { 22 | username: true, 23 | id: true, 24 | metadata: true 25 | }, 26 | with: { 27 | orgMemberships: { 28 | columns: { 29 | role: true 30 | }, 31 | with: { 32 | org: { 33 | columns: { 34 | id: true, 35 | name: true 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | }); 44 | 45 | if (!sessionData) { 46 | // Redirect to the webapp if the user is not logged in 47 | return null; 48 | } 49 | 50 | // is user in org 1? 51 | // if not, throw error 52 | const inOrg = sessionData.account.orgMemberships.some( 53 | (membership) => membership.org.id === 1 54 | ); 55 | 56 | if (!inOrg) { 57 | // Redirect to the webapp if the user is not in the command organization 58 | return null; 59 | } 60 | 61 | return sessionData.account; 62 | } 63 | -------------------------------------------------------------------------------- /packages/utils/password.ts: -------------------------------------------------------------------------------- 1 | import { dictionary, adjacencyGraphs } from '@zxcvbn-ts/language-common'; 2 | import { haveIBeenPwned } from '@zxcvbn-ts/matcher-pwned'; 3 | import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core'; 4 | import humanFormat from 'human-format'; 5 | import { z } from 'zod'; 6 | 7 | zxcvbnOptions.setOptions({ 8 | dictionary, 9 | graphs: adjacencyGraphs 10 | }); 11 | 12 | export async function calculatePasswordStrength(password: string) { 13 | const pawned = await haveIBeenPwned(password, { 14 | universalFetch: fetch 15 | }); 16 | 17 | if (pawned) { 18 | return { 19 | score: 0, 20 | crackTime: 'a few moments', 21 | allowed: false 22 | }; 23 | } 24 | 25 | const { score, crackTimesSeconds } = zxcvbn(password); 26 | return { 27 | score, 28 | crackTime: humanFormat(crackTimesSeconds.offlineSlowHashing1e4PerSecond, { 29 | separator: ' ', 30 | scale: new humanFormat.Scale({ 31 | milliseconds: 1 / 1000, 32 | seconds: 1, 33 | minutes: 60, 34 | hours: 60 * 60, 35 | days: 60 * 60 * 24, 36 | weeks: 60 * 60 * 24 * 7, 37 | months: 60 * 60 * 24 * 30, 38 | years: 60 * 60 * 24 * 365, 39 | decades: 60 * 60 * 24 * 365 * 10, 40 | centuries: 60 * 60 * 24 * 365 * 100 41 | }) 42 | }), 43 | allowed: score >= 3 44 | }; 45 | } 46 | 47 | export const strongPasswordSchema = z 48 | .string() 49 | .min(8, { message: 'Minimum 8 characters required' }) 50 | .refine( 51 | async (password) => (await calculatePasswordStrength(password)).allowed, 52 | { 53 | message: 'Password is too weak', 54 | path: ['password'] 55 | } 56 | ); 57 | -------------------------------------------------------------------------------- /apps/platform/utils/tRPCServerClients.ts: -------------------------------------------------------------------------------- 1 | import type { TrpcMailBridgeRouter } from '@u22n/mail-bridge/trpc'; 2 | import type { TrpcBillingRouter } from '@uninbox-ee/billing/trpc'; 3 | import { createTRPCClient, httpBatchLink } from '@trpc/client'; 4 | import { loggerLink } from '@trpc/client'; 5 | import { env } from '~platform/env'; 6 | import SuperJSON from 'superjson'; 7 | 8 | export const mailBridgeTrpcClient = createTRPCClient({ 9 | links: [ 10 | loggerLink({ 11 | enabled: (opts) => 12 | env.NODE_ENV === 'development' && 13 | opts.direction === 'down' && 14 | opts.result instanceof Error 15 | }), 16 | httpBatchLink({ 17 | url: `${env.MAILBRIDGE_URL}/trpc`, 18 | transformer: SuperJSON, 19 | maxURLLength: 2083, 20 | headers() { 21 | return { 22 | Authorization: env.MAILBRIDGE_KEY 23 | }; 24 | } 25 | }) 26 | ] 27 | }); 28 | 29 | // TODO: Make this conditional on EE license. If no EE then it should not be available. 30 | export const billingTrpcClient = createTRPCClient({ 31 | links: [ 32 | loggerLink({ 33 | enabled: (opts) => 34 | env.NODE_ENV === 'development' || 35 | (opts.direction === 'down' && opts.result instanceof Error) 36 | }), 37 | httpBatchLink({ 38 | url: `${env.BILLING_URL}/trpc`, 39 | transformer: SuperJSON, 40 | maxURLLength: 2083, 41 | headers() { 42 | if (!env.BILLING_KEY) { 43 | throw new Error('Tried to use billing client without key'); 44 | } 45 | return { 46 | Authorization: env.BILLING_KEY 47 | }; 48 | } 49 | }) 50 | ] 51 | }); 52 | -------------------------------------------------------------------------------- /apps/web/src/providers/realtime-provider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | useContext, 4 | useEffect, 5 | useState, 6 | type PropsWithChildren 7 | } from 'react'; 8 | import { useOrgShortcode } from '../hooks/use-params'; 9 | import RealtimeClient from '@u22n/realtime/client'; 10 | import { toast } from 'sonner'; 11 | import { env } from '../env'; 12 | 13 | const realtimeContext = createContext(null); 14 | 15 | export function RealtimeProvider({ children }: PropsWithChildren) { 16 | const orgShortcode = useOrgShortcode(); 17 | 18 | const [client] = useState( 19 | () => 20 | new RealtimeClient({ 21 | appKey: env.NEXT_PUBLIC_REALTIME_APP_KEY, 22 | host: env.NEXT_PUBLIC_REALTIME_HOST, 23 | port: env.NEXT_PUBLIC_REALTIME_PORT, 24 | authEndpoint: `${env.NEXT_PUBLIC_PLATFORM_URL}/realtime/auth`, 25 | channelAuthorizationEndpoint: `${env.NEXT_PUBLIC_PLATFORM_URL}/realtime/authorize` 26 | }) 27 | ); 28 | 29 | useEffect(() => { 30 | void client.connect({ orgShortcode }).catch(() => { 31 | toast.error( 32 | 'UnInbox encountered an error while trying to connect to the realtime server' 33 | ); 34 | }); 35 | return () => { 36 | if (client.isConnected) client.disconnect(); 37 | }; 38 | }, [client, orgShortcode]); 39 | 40 | return ( 41 | 42 | {children} 43 | 44 | ); 45 | } 46 | 47 | export function useRealtime() { 48 | const client = useContext(realtimeContext); 49 | if (!client) { 50 | throw new Error('useRealtime must be used within RealtimeProvider'); 51 | } 52 | return client; 53 | } 54 | -------------------------------------------------------------------------------- /apps/storage/app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createHonoApp, 3 | setupCors, 4 | setupErrorHandlers, 5 | setupHealthReporting, 6 | setupHonoListener, 7 | setupRouteLogger, 8 | setupRuntime 9 | } from '@u22n/hono'; 10 | import { deleteAttachmentsApi } from './api/deleteAttachments'; 11 | import { internalPresignApi } from './api/internalPresign'; 12 | import { attachmentProxy } from './proxy/attachment'; 13 | import { inlineProxy } from './proxy/inline-proxy'; 14 | import { deleteOrgsApi } from './api/deleteOrg'; 15 | import { opentelemetry } from '@u22n/otel/hono'; 16 | import { mailfetchApi } from './api/mailfetch'; 17 | import { authMiddleware } from './middlewares'; 18 | import { avatarProxy } from './proxy/avatars'; 19 | import { presignApi } from './api/presign'; 20 | import { avatarApi } from './api/avatar'; 21 | import type { Ctx } from './ctx'; 22 | import { env } from './env'; 23 | 24 | const app = createHonoApp(); 25 | 26 | app.use(opentelemetry('storage/hono')); 27 | 28 | setupRouteLogger(app, env.NODE_ENV === 'development'); 29 | setupCors(app, { origin: [env.WEBAPP_URL] }); 30 | setupHealthReporting(app, { service: 'Storage' }); 31 | setupErrorHandlers(app); 32 | 33 | // Auth middleware 34 | app.use('*', authMiddleware); 35 | // Proxies 36 | app.route('/avatar', avatarProxy); 37 | app.route('/attachment', attachmentProxy); 38 | app.route('/inline-proxy', inlineProxy); 39 | 40 | // APIs 41 | app.route('/api', avatarApi); 42 | app.route('/api', presignApi); 43 | app.route('/api', internalPresignApi); 44 | app.route('/api', mailfetchApi); 45 | app.route('/api', deleteAttachmentsApi); 46 | app.route('/api', deleteOrgsApi); 47 | 48 | const cleanup = setupHonoListener(app, { port: env.PORT }); 49 | setupRuntime([cleanup]); 50 | -------------------------------------------------------------------------------- /apps/web/src/lib/middleware-utils.ts: -------------------------------------------------------------------------------- 1 | import { cookies, headers as nextHeaders } from 'next/headers'; 2 | import 'server-only'; 3 | 4 | function getPlatformUrl() { 5 | if (process.env.PLATFORM_URL) return process.env.PLATFORM_URL; 6 | throw new Error('PLATFORM_URL is not set'); 7 | } 8 | 9 | function clientHeaders() { 10 | const allHeaders = nextHeaders(); 11 | const clientHeaders = new Headers(); 12 | const notAllowedHeaders = ['host', 'origin', 'referer']; 13 | allHeaders.forEach((value, key) => { 14 | if (notAllowedHeaders.includes(key)) return; 15 | clientHeaders.append(key, value); 16 | }); 17 | return clientHeaders; 18 | } 19 | 20 | export async function getAuthRedirection() { 21 | if (!cookies().has('un-session')) return { defaultOrgShortcode: null }; 22 | return fetch(`${getPlatformUrl()}/auth/redirection`, { 23 | headers: clientHeaders() 24 | }).then((r) => (r.ok ? r.json() : { defaultOrgShortcode: null })) as Promise<{ 25 | defaultOrgShortcode: string | null; 26 | }>; 27 | } 28 | 29 | // Skip the cookie validation on client with shallow=true 30 | // it is checked on server anyways and may cause performance issues if checked on client on every request 31 | export async function isAuthenticated(shallow = false) { 32 | if (!cookies().has('un-session')) return false; 33 | if (shallow) return true; 34 | try { 35 | const data = (await fetch(`${getPlatformUrl()}/auth/status`, { 36 | headers: clientHeaders() 37 | }).then((r) => r.json())) as { 38 | authStatus: 'authenticated' | 'unauthenticated'; 39 | }; 40 | const authenticated = data.authStatus === 'authenticated'; 41 | return authenticated; 42 | } catch (e) { 43 | console.error(e); 44 | return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/settings/org/mail/addresses/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PageTitle } from '../../../_components/page-title'; 4 | import { Button } from '@/src/components/shadcn-ui/button'; 5 | import { DataTable } from '@/src/components/shared/table'; 6 | import { useOrgShortcode } from '@/src/hooks/use-params'; 7 | import { SpinnerGap } from '@phosphor-icons/react'; 8 | import { columns } from './_components/columns'; 9 | import { platform } from '@/src/lib/trpc'; 10 | import Link from 'next/link'; 11 | 12 | export default function Page() { 13 | const orgShortcode = useOrgShortcode(); 14 | const { data: emailsList, isLoading } = 15 | platform.org.mail.emailIdentities.getOrgEmailIdentities.useQuery({ 16 | orgShortcode 17 | }); 18 | 19 | return ( 20 |
21 | 24 | 29 | 30 | {isLoading && ( 31 |
32 | 36 | Loading... 37 |
38 | )} 39 | {emailsList && ( 40 | 44 | `/${orgShortcode}/settings/org/mail/addresses/${row.publicId}` 45 | } 46 | /> 47 | )} 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /apps/web/src/app/[orgShortcode]/convo/_components/convo-list.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | useOrgScopedRouter, 5 | useOrgShortcode, 6 | useSpaceShortcode 7 | } from '@/src/hooks/use-params'; 8 | import { ConvoListBase } from './convo-list-base'; 9 | import { useEffect, useMemo } from 'react'; 10 | import { platform } from '@/src/lib/trpc'; 11 | import { ms } from '@u22n/utils/ms'; 12 | 13 | // type Props = { 14 | // hidden: boolean; 15 | // }; 16 | 17 | export function ConvoList(/*{hidden} : Props*/) { 18 | const orgShortcode = useOrgShortcode(); 19 | const spaceShortcode = useSpaceShortcode(); 20 | const { scopedRedirect, scopedUrl } = useOrgScopedRouter(); 21 | 22 | const { 23 | data: convos, 24 | fetchNextPage, 25 | isLoading, 26 | hasNextPage, 27 | isFetchingNextPage, 28 | error 29 | } = platform.spaces.getSpaceConvos.useInfiniteQuery( 30 | { 31 | orgShortcode, 32 | spaceShortcode: spaceShortcode ?? 'all' 33 | // includeHidden: hidden 34 | }, 35 | { 36 | getNextPageParam: (lastPage) => lastPage.cursor ?? undefined, 37 | staleTime: ms('1 hour') 38 | } 39 | ); 40 | 41 | const allConvos = useMemo( 42 | () => (convos ? convos?.pages.flatMap((page) => page.data) : []), 43 | [convos] 44 | ); 45 | 46 | useEffect(() => { 47 | if (error?.data?.code === 'FORBIDDEN') { 48 | scopedRedirect('/personal/convo'); 49 | } 50 | }, [error, scopedRedirect]); 51 | 52 | return ( 53 |