Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: ci

on:
pull_request:
push:
branches: [main]

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:16
env: { POSTGRES_PASSWORD: postgres, POSTGRES_DB: mailbox_core }
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
env:
MAILBOX_TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/mailbox_core
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: 24
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run check
- run: bun run test:e2e
- name: node consumer smoke
run: |
set -euo pipefail
TARBALL="$PWD/$(npm pack --silent)"
mkdir -p "$RUNNER_TEMP/c" && cd "$RUNNER_TEMP/c"
npm init -y >/dev/null && npm pkg set type=module >/dev/null
npm install "$TARBALL"
node -e 'import("@corbits/mailbox").then((m) => { for (const n of ["createMailboxRoutes", "runMailboxMigrations"]) if (typeof m[n] !== "function") throw new Error("missing export: " + n); })'
53 changes: 53 additions & 0 deletions .github/workflows/cla.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CLA Assistant

on:
issue_comment:
types: [created]
pull_request_target:
types: [opened, synchronize]

permissions:
actions: write
contents: write
pull-requests: write
statuses: write

concurrency:
group: cla-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
cancel-in-progress: true

jobs:
cla:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Maintainer fast path
if: >-
github.event_name == 'pull_request_target' &&
(contains(fromJSON('["TheGreatAxios","brianjfox"]'), github.event.pull_request.user.login) ||
endsWith(github.event.pull_request.user.login, '[bot]'))
run: echo "Maintainer or bot pull request; CLA not required."
- name: CLA Assistant
if: >-
((github.event.comment.body == 'recreate-signatures' ||
github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') ||
github.event_name == 'pull_request_target') &&
!(github.event_name == 'pull_request_target' &&
(contains(fromJSON('["TheGreatAxios","brianjfox"]'), github.event.pull_request.user.login) ||
endsWith(github.event.pull_request.user.login, '[bot]')))
# corbitsdev/cla-assistant-action v2.6.1-node24: upstream v2.6.1 on node24.
uses: corbitsdev/cla-assistant-action@ef6d3e51db8232fe93090810f13bde30497c1d74
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path-to-document: "https://github.com/${{ github.repository }}/blob/main/CLA.md"
path-to-signatures: "signatures/version1/cla.json"
branch: "cla-signatures"
allowlist: TheGreatAxios,brianjfox,*[bot]
custom-notsigned-prcomment: >-
Thank you for your contribution. Before it can be merged, please read our
[Contributor License Agreement](https://github.com/${{ github.repository }}/blob/main/CLA.md)
and sign it by posting a new comment on this pull request containing
exactly the line below (nothing else):
custom-pr-sign-comment: "I have read the CLA Document and I hereby sign the CLA"
custom-allsigned-prcomment: "All contributors have signed the CLA."
74 changes: 0 additions & 74 deletions .github/workflows/test.yml

This file was deleted.

6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
node_modules/
dist/
.env
.env.local
*.tsbuildinfo
*.tgz
coverage/
.env
.env.*
.DS_Store
*.tgz
45 changes: 45 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# AGENTS.md

## Purpose

`@corbits/mailbox` is a native `@intx/mailbox` `MailboxStore` over Postgres for
human principals, plus the routes a host's UI uses to list, read, file and
send. It owns the `mailbox` schema, its tables and migrations, the `/me/inbox*`
HTTP surface, MIME frame building and decoding, the durable write path and the
triage mechanism. The host supplies the Hono app, the database handle, the
caller's tenant and principal, the triage vocabulary, sender authorization,
display names, the event bus for multi-replica setups and the mail transport.
This package neither sends nor receives SMTP.

## Layout

- `src/mount.ts` — `createMailboxRoutes`, the `/me/inbox*` routes and SSE stream.
- `src/native-store.ts` — the native `MailboxStore` over Postgres (uid and modseq always set).
- `src/write.ts` — the write boundary every host-facing write path goes through.
- `src/persist.ts` — `createMailboxPersist`, the transport dual-write seam.
- `src/frame.ts` — RFC 5322 frame building and decoding.
- `src/recipients.ts` — address-list parsing and owned-mailbox resolution.
- `src/bus.ts` — the event bus contract and the in-memory default.
- `src/purge.ts` — explicit offboarding.
- `src/schema.ts`, `src/schema-check.ts` — drizzle tables and the live-schema check.
- `src/migrations.ts` — `runMailboxMigrations`, replays `migrations/*.sql`.
- `src/db.ts` — the `MailboxDb` handle type.
- `src/index.ts` — the only module consumers import from.
- `e2e/` — real-Postgres suites; shared harness in `e2e/helpers.ts`.

## Rules

- Everything from the host arrives through a declared seam, never an import. Wanting to import a host package means adding a port.
- `POST /me/inbox/send` builds the message, files a copy in `Sent`, then calls the host's `deliver` exactly once.
- `createMailboxPersist` calls the host's `upstream` unconditionally and layers the inbox write on top, so a transport failure never costs a recipient their copy, and a mailbox refusal never rejects upstream success.
- `schema.ts` and `migrations/*.sql` change together, in the same commit. Every migration is idempotent.
- Caps refuse rather than clamp: `?limit=` above 200, bulk actions above 50 ids, frames above `MAX_MAILBOX_FRAME_BYTES`, recipients above `MAX_MAILBOX_RECIPIENTS`.
- SSE events are best-effort nudges with a bounded queue (`MAX_PENDING_SSE_EVENTS`); clients refetch on reconnect. The in-memory bus is single-process.
- Nothing is mocked at the database boundary. Unit tests only for load-bearing logic (frame and recipient parsing, guards, migrations); everything else is e2e.
- Every `any` or cast carries a comment saying why the type system leaves no alternative.

## Local development

```sh
bun install && bun run check
```
Loading
Loading