From d8f9d3fd1db75131236b945c2c5ee0e51230ffe5 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 8 Aug 2026 17:33:30 -0700 Subject: [PATCH 1/2] Promote mailbox to top-level package for git installability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The package lived at packages/mailbox/ in a private-root monorepo. A git install (bun add github:...) gets the repository root, so the published package.json must be at the root for exports, files, and the prepare hook to resolve under a consumer's node_modules. - Move src/ to repo root; delete packages/ shell - Promote packages/mailbox/package.json to root (name, exports, files, deps, peerDependencies with @intx/* as peers — never vendored) - Add scripts/prepare.mjs: mirrors @corbits/react-ui — builds dist/ in a scratch dir when installed as a dependency (bun omits devDeps for git deps, and tsc cannot emit .d.ts inside a consumer node_modules tree) - Add root tsconfig.build.json (extends ./tsconfig.json, declaration emit) - Update README for the flat layout - examples/reference-host: file:../.. dep (bun workspaces cannot self-reference the root package by name) --- README.md | 31 +- bun.lock | 81 ++- packages/mailbox/bunfig.toml => bunfig.toml | 0 examples/reference-host/package.json | 2 +- package.json | 83 ++- packages/mailbox/LICENSE | 501 ------------------ packages/mailbox/README.md | 340 ------------ packages/mailbox/package.json | 84 --- packages/mailbox/tsconfig.json | 6 - scripts/prepare.mjs | 93 ++++ .../src => src}/bus-unsubscribe.test.ts | 0 {packages/mailbox/src => src}/bus.test.ts | 0 {packages/mailbox/src => src}/bus.ts | 0 .../mailbox/src => src}/concurrency.test.ts | 0 {packages/mailbox/src => src}/db.ts | 0 .../mailbox/src => src}/enrichment.test.ts | 0 .../src => src}/filter-sort-assign.test.ts | 0 .../mailbox/src => src}/fk-cascade.test.ts | 0 .../src => src}/frame-multipart.test.ts | 0 {packages/mailbox/src => src}/frame.test.ts | 0 {packages/mailbox/src => src}/frame.ts | 0 {packages/mailbox/src => src}/index.ts | 0 .../mailbox/src => src}/log-warnings.test.ts | 0 .../src => src}/message-schemas.test.ts | 0 .../mailbox/src => src}/migrations.test.ts | 0 {packages/mailbox/src => src}/migrations.ts | 0 .../src => src}/mount-bus-failure.test.ts | 0 .../src => src}/mount-event-op.test.ts | 0 {packages/mailbox/src => src}/mount.test.ts | 0 {packages/mailbox/src => src}/mount.ts | 0 .../mailbox/src => src}/mutations.test.ts | 0 {packages/mailbox/src => src}/mutations.ts | 0 {packages/mailbox/src => src}/openapi.test.ts | 0 {packages/mailbox/src => src}/persist.test.ts | 0 {packages/mailbox/src => src}/persist.ts | 0 {packages/mailbox/src => src}/purge.test.ts | 0 {packages/mailbox/src => src}/purge.ts | 0 .../read-direction-and-date.test.ts | 0 .../src => src}/read-fallbacks.test.ts | 0 .../read-microsecond-cursor.test.ts | 0 .../src => src}/read-multipart.test.ts | 0 .../src => src}/read-non-utc-session.test.ts | 0 .../src => src}/read-page-boundary.test.ts | 0 {packages/mailbox/src => src}/read.test.ts | 0 {packages/mailbox/src => src}/read.ts | 0 .../mailbox/src => src}/recipients.test.ts | 0 {packages/mailbox/src => src}/recipients.ts | 0 .../mailbox/src => src}/schema-check.test.ts | 0 {packages/mailbox/src => src}/schema-check.ts | 0 .../src => src}/schema-ddl-parity.test.ts | 0 {packages/mailbox/src => src}/schema.ts | 0 .../src => src}/scope-validation.test.ts | 0 .../src => src}/sender-display.test.ts | 0 .../mailbox/src => src}/sse-heartbeat.test.ts | 0 .../mailbox/src => src}/sse-stream.test.ts | 0 {packages/mailbox/src => src}/test-helpers.ts | 0 .../mailbox/src => src}/vocabulary.test.ts | 0 {packages/mailbox/src => src}/vocabulary.ts | 0 {packages/mailbox/src => src}/write.test.ts | 0 {packages/mailbox/src => src}/write.ts | 0 ...tsconfig.build.json => tsconfig.build.json | 2 +- 61 files changed, 229 insertions(+), 994 deletions(-) rename packages/mailbox/bunfig.toml => bunfig.toml (100%) delete mode 100644 packages/mailbox/LICENSE delete mode 100644 packages/mailbox/README.md delete mode 100644 packages/mailbox/package.json delete mode 100644 packages/mailbox/tsconfig.json create mode 100644 scripts/prepare.mjs rename {packages/mailbox/src => src}/bus-unsubscribe.test.ts (100%) rename {packages/mailbox/src => src}/bus.test.ts (100%) rename {packages/mailbox/src => src}/bus.ts (100%) rename {packages/mailbox/src => src}/concurrency.test.ts (100%) rename {packages/mailbox/src => src}/db.ts (100%) rename {packages/mailbox/src => src}/enrichment.test.ts (100%) rename {packages/mailbox/src => src}/filter-sort-assign.test.ts (100%) rename {packages/mailbox/src => src}/fk-cascade.test.ts (100%) rename {packages/mailbox/src => src}/frame-multipart.test.ts (100%) rename {packages/mailbox/src => src}/frame.test.ts (100%) rename {packages/mailbox/src => src}/frame.ts (100%) rename {packages/mailbox/src => src}/index.ts (100%) rename {packages/mailbox/src => src}/log-warnings.test.ts (100%) rename {packages/mailbox/src => src}/message-schemas.test.ts (100%) rename {packages/mailbox/src => src}/migrations.test.ts (100%) rename {packages/mailbox/src => src}/migrations.ts (100%) rename {packages/mailbox/src => src}/mount-bus-failure.test.ts (100%) rename {packages/mailbox/src => src}/mount-event-op.test.ts (100%) rename {packages/mailbox/src => src}/mount.test.ts (100%) rename {packages/mailbox/src => src}/mount.ts (100%) rename {packages/mailbox/src => src}/mutations.test.ts (100%) rename {packages/mailbox/src => src}/mutations.ts (100%) rename {packages/mailbox/src => src}/openapi.test.ts (100%) rename {packages/mailbox/src => src}/persist.test.ts (100%) rename {packages/mailbox/src => src}/persist.ts (100%) rename {packages/mailbox/src => src}/purge.test.ts (100%) rename {packages/mailbox/src => src}/purge.ts (100%) rename {packages/mailbox/src => src}/read-direction-and-date.test.ts (100%) rename {packages/mailbox/src => src}/read-fallbacks.test.ts (100%) rename {packages/mailbox/src => src}/read-microsecond-cursor.test.ts (100%) rename {packages/mailbox/src => src}/read-multipart.test.ts (100%) rename {packages/mailbox/src => src}/read-non-utc-session.test.ts (100%) rename {packages/mailbox/src => src}/read-page-boundary.test.ts (100%) rename {packages/mailbox/src => src}/read.test.ts (100%) rename {packages/mailbox/src => src}/read.ts (100%) rename {packages/mailbox/src => src}/recipients.test.ts (100%) rename {packages/mailbox/src => src}/recipients.ts (100%) rename {packages/mailbox/src => src}/schema-check.test.ts (100%) rename {packages/mailbox/src => src}/schema-check.ts (100%) rename {packages/mailbox/src => src}/schema-ddl-parity.test.ts (100%) rename {packages/mailbox/src => src}/schema.ts (100%) rename {packages/mailbox/src => src}/scope-validation.test.ts (100%) rename {packages/mailbox/src => src}/sender-display.test.ts (100%) rename {packages/mailbox/src => src}/sse-heartbeat.test.ts (100%) rename {packages/mailbox/src => src}/sse-stream.test.ts (100%) rename {packages/mailbox/src => src}/test-helpers.ts (100%) rename {packages/mailbox/src => src}/vocabulary.test.ts (100%) rename {packages/mailbox/src => src}/vocabulary.ts (100%) rename {packages/mailbox/src => src}/write.test.ts (100%) rename {packages/mailbox/src => src}/write.ts (100%) rename packages/mailbox/tsconfig.build.json => tsconfig.build.json (89%) diff --git a/README.md b/README.md index 17d204a..357ddc4 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,30 @@ # corbits-mailbox -Home of **[`@corbits/mailbox`](./packages/mailbox)** — a universal, -principal-keyed inbox, mountable onto a Hono host backed by an Interchange-shaped -Postgres. Its tables live in a dedicated `mailbox` schema in the host's database, -foreign-keyed to the host's `tenant` and `principal` tables. Backend only; this -package ships no UI. +**[`@corbits/mailbox`](./package.json)** — a universal, principal-keyed inbox, +mountable onto a Hono host backed by an Interchange-shaped Postgres. Its tables +live in a dedicated `mailbox` schema in the host's database, foreign-keyed to +the host's `tenant` and `principal` tables. Backend only; this package ships no +UI. -See the [package README](./packages/mailbox/README.md) for install, the mount -snippet and the seams, and [ARCHITECTURE.md](./ARCHITECTURE.md) for the data model. +Requires `@intx` 0.2.2 or newer. + +See [ARCHITECTURE.md](./ARCHITECTURE.md) for the data model. + +## Install + +```sh +# from npm (ships prebuilt dist/) +bun add @corbits/mailbox + +# from git (prepare hook builds dist/ on the way in) +bun add github:corbitsdev/corbits-mailbox +``` ## Layout | | | | --- | --- | -| `packages/mailbox` | The published package. Owns `principal_mail` (the message, immutable) and `mailbox` (the management layer, created eagerly with each message). | +| `src/` | The published package. Owns `principal_mail` (the message, immutable) and `mailbox` (the management layer, created eagerly with each message). | | `examples/reference-host` | Mounts it on a real `@intx/hub-api` app against a live Postgres and asserts the acceptance scenarios end to end. | ## Working on it @@ -23,8 +34,8 @@ bun install docker run -d --name mailbox-pg -p 5433:5432 \ -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=mailbox_core postgres:16 -bun run --cwd packages/mailbox test # unit + integration -bun run --cwd packages/mailbox build # dist/ (JS + .d.ts) +bun run test # unit + integration +bun run build # dist/ (JS + .d.ts) bun test --cwd examples/reference-host # acceptance scenarios ``` diff --git a/bun.lock b/bun.lock index fcd488b..785ac9b 100644 --- a/bun.lock +++ b/bun.lock @@ -3,32 +3,7 @@ "configVersion": 1, "workspaces": { "": { - "name": "corbits-mailbox", - "devDependencies": { - "@types/bun": "1.1.14", - "@types/node": "22.10.5", - "typescript": "5.7.2", - }, - }, - "examples/reference-host": { - "name": "reference-host", - "dependencies": { - "@corbits/mailbox": "workspace:*", - "@intx/db": "0.2.2", - "@intx/hub-api": "0.2.2", - "@intx/hub-sessions": "0.2.2", - "drizzle-orm": "0.45.2", - "hono": "4.12.32", - "postgres": "3.4.9", - }, - "devDependencies": { - "@types/bun": "1.1.14", - "typescript": "5.7.2", - }, - }, - "packages/mailbox": { "name": "@corbits/mailbox", - "version": "0.1.0", "dependencies": { "@hono/standard-validator": "0.2.3", "@standard-community/standard-json": "0.3.5", @@ -58,6 +33,22 @@ "postgres": "^3.4.0", }, }, + "examples/reference-host": { + "name": "reference-host", + "dependencies": { + "@corbits/mailbox": "file:../..", + "@intx/db": "0.2.2", + "@intx/hub-api": "0.2.2", + "@intx/hub-sessions": "0.2.2", + "drizzle-orm": "0.45.2", + "hono": "4.12.32", + "postgres": "3.4.9", + }, + "devDependencies": { + "@types/bun": "1.1.14", + "typescript": "5.7.2", + }, + }, }, "overrides": { "drizzle-orm": "0.45.2", @@ -67,25 +58,25 @@ "@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="], - "@better-auth/core": ["@better-auth/core@1.6.25", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.7", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-lMTlhtwyK4NpY9kPF+2rQCRKYpg136d3gM2xl8esxT1PjJx5Nh5YwZvxcYCIjDuO759sx6TCloJTuwcZGG6ZBw=="], + "@better-auth/core": ["@better-auth/core@1.6.26", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.7", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-Ud4FqnjIJDvmeo+3bN+OsuVVAiuvFjNERbW7G8/65ictSxCox62gNgTOsJ4YXmbwXzBzyyekStzrupg2qNGEkA=="], - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.25", "", { "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-ru/DeKjFPQUVeKkxF/ScazmPqIY7lwfkAV5Yt4j24wmn1Y8vFwoiPRnHgXUeZqBs10+nubaRwEqLF39CP6EhRw=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.26", "", { "peerDependencies": { "@better-auth/core": "^1.6.26", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-SMvAeeUqEsz0BLtWVVp+HdStAVOwcxs4vPkL/AVmFJ0e9dDKItXl881xWeB/Ze4NgBW6NYmUId9qI4LrdQu3hA=="], - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.25", "", { "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-zxiePhtN1YClS1irKYPVwWfN6kYp+QoYlz1hdQUOj8hXyo2aE/ny4RNAb6v332b0+U6Vu88EhYITRPdmvCo6uA=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.26", "", { "peerDependencies": { "@better-auth/core": "^1.6.26", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-Y0Kdqn8JQMR8dHMtUnbBNqq7Mk8mDf18DwTves2uhjtRfcOWByVTGxsFMDVUbedMuoM+Ab+v04bwCk94sAo8NA=="], - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.25", "", { "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2" } }, "sha512-GhEzTumc8yfTz+OZ6pMg06BA49xob49x1bX+1mEl/FStDJoSF+6mTfI5M2ytFxaiN89336/aUjkW8u+qRyLexw=="], + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.26", "", { "peerDependencies": { "@better-auth/core": "^1.6.26", "@better-auth/utils": "0.4.2" } }, "sha512-kb5ahphEp9jyMleXU4T8I/xgWPa8gjOLFKKsnaVqR/BU+h7xqTOsjJwTY3evY+PFYHvjNFUlrjZ1Km8A/w1p/w=="], - "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.25", "", { "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-ZtMmjcOdXR2Ziqx5y8ptTOaNpe0snNfALbBUPXJsgeyeRkDJDYzyLZ8MpuvNBTNllNeIFDbiXWAK5k+pEBZrUQ=="], + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.26", "", { "peerDependencies": { "@better-auth/core": "^1.6.26", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-jYGhuIQqj48h69ROLPYdc8jgqDaMmRRLLFiRViZFYarNyBuYUV07DywJXvRuw8l9ADx+M1TB+BJZGnLE2nnmwQ=="], - "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.25", "", { "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-ym7B6Iqcry+/4aQnYpFwqP/GBIiXvjrm/5B6+0qmx8mkTY/apHFTpHuGzUYYNf4vPTtzF3eYY2+s2GOsomKaRg=="], + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.26", "", { "peerDependencies": { "@better-auth/core": "^1.6.26", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-ILQoYmnoYyghDP4iN1h/Gkd9Jt7Xs/vxoNAt4AuuJJbqs73u27LlQM4P1YiP7nJ+bM1iVTBI1dDF73myIAtjzQ=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.6.25", "", { "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1" } }, "sha512-2ZfC9lp7tU6Jw/q2Lz/bKfQqGMdMwc/IQDTYdBhvtGi24qInYVnhp2ZCW57hHM9j+fq1ULOtxgg6M3T1LEaihw=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.26", "", { "peerDependencies": { "@better-auth/core": "^1.6.26", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1" } }, "sha512-pBs69RORUSJHRF9r5PdeY7pzLRga09YSWew8igFYrBvT2tLt7DktrbD5WjryLZpAZOuKAMZ+Xo0DlJbnvLWGpQ=="], "@better-auth/utils": ["@better-auth/utils@0.4.2", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], - "@corbits/mailbox": ["@corbits/mailbox@workspace:packages/mailbox"], + "@corbits/mailbox": ["@corbits/mailbox@root:", {}], "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], @@ -125,13 +116,13 @@ "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], - "@logtape/hono": ["@logtape/hono@2.2.4", "", { "peerDependencies": { "@logtape/logtape": "^2.2.4", "hono": "^4.0.0" } }, "sha512-y0iKPQ4vOS+s3C2sj40juY7DtyYGnylvdWi1s6J7+5hE3RyW5jBNDax+XiXPEJnVBMg5zU5MHR7dbqGvmPu1hg=="], + "@logtape/hono": ["@logtape/hono@2.3.0", "", { "peerDependencies": { "@logtape/logtape": "^2.3.0", "hono": "^4.0.0" } }, "sha512-+aYoPfhEeOVW1hiQwx7Tv2fbNSbOPQhx5EBZ/8JXAXt9j1w0h63X0M/MjyhjBh6Mp22gnOdNLTF6l5Tg2RIcJw=="], - "@logtape/logtape": ["@logtape/logtape@2.2.4", "", {}, "sha512-2rALzv9m4ibE5FyB8/FMm5MPMMlK7ujgy3ufricVKIxj6e7SZbw4w6J16/fRAsXgdDlOXPUA0aa6XoEQvdlKxw=="], + "@logtape/logtape": ["@logtape/logtape@2.3.0", "", {}, "sha512-s/pxCgf9Gg75ypTV/bRUq355Dy/JP/zUJWfgJQPO5pTkXC23X2AXBcmBBgFiWH+bSi4yiX/G06qGV8CdAWD3SA=="], - "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], + "@noble/ciphers": ["@noble/ciphers@2.3.0", "", {}, "sha512-Clu/xdfgVTf9o7ngLOURaxePwR0j8sjclKEtVij10/jGulwFsPWCvvRgG/XjUVf8Nei+jLG6uwyXzUTGY1DQrw=="], - "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], + "@noble/hashes": ["@noble/hashes@2.3.0", "", {}, "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ=="], "@npmcli/agent": ["@npmcli/agent@4.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg=="], @@ -171,11 +162,11 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "better-auth": ["better-auth@1.6.25", "", { "dependencies": { "@better-auth/core": "1.6.25", "@better-auth/drizzle-adapter": "1.6.25", "@better-auth/kysely-adapter": "1.6.25", "@better-auth/memory-adapter": "1.6.25", "@better-auth/mongo-adapter": "1.6.25", "@better-auth/prisma-adapter": "1.6.25", "@better-auth/telemetry": "1.6.25", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.7", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-fvoq+oCO+FF5fpP3XfU7znRyGFpHB77UG2EyxsKNy+Cak7Q5pELu+auvvDveQbWQxcoKugZ7jYQQPFQLpUTGOw=="], + "better-auth": ["better-auth@1.6.26", "", { "dependencies": { "@better-auth/core": "1.6.26", "@better-auth/drizzle-adapter": "1.6.26", "@better-auth/kysely-adapter": "1.6.26", "@better-auth/memory-adapter": "1.6.26", "@better-auth/mongo-adapter": "1.6.26", "@better-auth/prisma-adapter": "1.6.26", "@better-auth/telemetry": "1.6.26", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.7", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-nhXWrDDj+EnZsHq1j0z1c6DowOMFZWZHe6LCaXbBfLIgHHZm6dyazBQcbRspM4spUIcUt250Mc1BFOSuP7eniQ=="], "better-call": ["better-call@1.3.7", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w=="], - "brace-expansion": ["brace-expansion@5.0.8", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg=="], + "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], @@ -195,7 +186,7 @@ "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], @@ -261,7 +252,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ip-address": ["ip-address@10.3.1", "", {}, "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g=="], + "ip-address": ["ip-address@10.4.0", "", {}, "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ=="], "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], @@ -269,9 +260,9 @@ "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - "isomorphic-git": ["isomorphic-git@1.40.0", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "pify": "^4.0.1", "readable-stream": "^4.0.0", "sha.js": "^2.4.12", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-/CbnxwZqIm17y3c/z0INbkgEKSvFerXtO/NGgaRxZ8nvL3eoMtbjuAS7f4Pj7lZzj8HaultvDD1ClJTBVDl89g=="], + "isomorphic-git": ["isomorphic-git@1.41.0", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "pify": "^4.0.1", "readable-stream": "^4.0.0", "sha.js": "^2.4.12", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-YADpOKD/pLemtcyZ9jssNXnPVhfDObGl/BAKMtvmU17svgNzOKTT6AHX68DzFHpie5hAZHRtutC0Cka3lYdmBA=="], - "jose": ["jose@6.2.4", "", {}, "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA=="], + "jose": ["jose@6.2.8", "", {}, "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ=="], "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], @@ -285,7 +276,7 @@ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -307,7 +298,7 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanostores": ["nanostores@1.4.1", "", {}, "sha512-PGd3uPojJB9Z07d5NX3Db/SOSBbyy3wLMUGq0GpnEEJfVzY9mq7daPMAZ3jObV5D3Jn+YKND636eI5ULg7F80Q=="], + "nanostores": ["nanostores@1.4.2", "", {}, "sha512-Wxv8Roefr2nqtiRG0bnaFlpYqpIVtOEeJZHaH+4nGgOK1/7n6OHOuHCb/bhqrNQgZM8fyd0s1PqhdrJc9Ib44g=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], diff --git a/packages/mailbox/bunfig.toml b/bunfig.toml similarity index 100% rename from packages/mailbox/bunfig.toml rename to bunfig.toml diff --git a/examples/reference-host/package.json b/examples/reference-host/package.json index e803213..2d2f166 100644 --- a/examples/reference-host/package.json +++ b/examples/reference-host/package.json @@ -6,7 +6,7 @@ "test": "bun test" }, "dependencies": { - "@corbits/mailbox": "workspace:*", + "@corbits/mailbox": "file:../..", "@intx/db": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/hub-sessions": "0.2.2", diff --git a/package.json b/package.json index 67919f5..2016ebf 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,89 @@ { - "name": "corbits-mailbox", - "private": true, - "license": "LGPL-2.1-only", + "name": "@corbits/mailbox", + "version": "0.1.0", "type": "module", - "workspaces": ["packages/*", "examples/*"], + "license": "LGPL-2.1-only", + "description": "Universal principal-keyed inbox, mountable onto any Interchange host. Requires @intx 0.2.2 or newer.", + "keywords": [ + "corbits", + "interchange", + "intx", + "mailbox", + "inbox", + "hono", + "drizzle", + "postgres" + ], + "author": "Corbits", + "homepage": "https://github.com/corbitsdev/corbits-mailbox#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/corbitsdev/corbits-mailbox.git" + }, + "bugs": { + "url": "https://github.com/corbitsdev/corbits-mailbox/issues" + }, + "engines": { + "node": ">=22" + }, + "corbits": { + "minimumIntxVersion": "0.2.2" + }, + "workspaces": [ + "examples/*" + ], + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "src", + "!src/**/*.test.ts", + "!src/test-helpers.ts", + "LICENSE", + "README.md" + ], + "sideEffects": false, "scripts": { + "build": "rm -rf dist && tsc -p tsconfig.build.json", + "prepack": "bun run build", + "prepare": "node scripts/prepare.mjs", "typecheck": "tsc --noEmit", - "build": "bun run --cwd packages/mailbox build", - "test": "bun run --cwd packages/mailbox test", + "test": "bun test src", + "test:coverage": "bun test src --coverage", "test:acceptance": "bun run build && bun test --cwd examples/reference-host" }, + "dependencies": { + "@hono/standard-validator": "0.2.3", + "@standard-community/standard-json": "0.3.5", + "@standard-community/standard-openapi": "0.2.9", + "arktype": "2.1.29", + "hono-openapi": "1.3.1" + }, + "peerDependencies": { + "@intx/log": "^0.2.2", + "@intx/mime": "^0.2.2", + "@intx/types": "^0.2.2", + "drizzle-orm": "^0.45.2", + "hono": "^4.12.0", + "postgres": "^3.4.0" + }, "devDependencies": { + "@intx/log": "0.2.2", + "@intx/mime": "0.2.2", + "@intx/types": "0.2.2", "@types/bun": "1.1.14", + "@types/json-schema": "7.0.15", "@types/node": "22.10.5", + "drizzle-orm": "0.45.2", + "hono": "4.12.32", + "openapi-types": "12.1.3", + "postgres": "3.4.9", "typescript": "5.7.2" }, "overrides": { diff --git a/packages/mailbox/LICENSE b/packages/mailbox/LICENSE deleted file mode 100644 index f6683e7..0000000 --- a/packages/mailbox/LICENSE +++ /dev/null @@ -1,501 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -not price. Our General Public Licenses are designed to make sure that -you have the freedom to distribute copies of free software (and charge -for this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser General Public License from time to time. -Such new versions will be similar in spirit to the present version, -but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY -KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU -FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR -CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE -LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING -RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, see . - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random Hacker. - - , 1 April 1990 - Moe Ghoul, President of Vice - -That's all there is to it! diff --git a/packages/mailbox/README.md b/packages/mailbox/README.md deleted file mode 100644 index 7543abf..0000000 --- a/packages/mailbox/README.md +++ /dev/null @@ -1,340 +0,0 @@ -# @corbits/mailbox - -A universal, principal-keyed inbox you mount onto an existing [Hono](https://hono.dev) -app. Backend only — this package ships no UI. - -It owns two tables and its own migration ledger — all in a dedicated `mailbox` -Postgres schema in the host's database — and the `/api/me/inbox*` routes: - -- **`principal_mail`** — the message as delivered, immutable. Reads 1-1 with - Interchange's `session_mail`. -- **`mailbox`** — the mutable management layer keyed by mail id (read/archive/trash - plus triage columns), created **eagerly with its message in one transaction**. An - all-NULL row means delivered-and-untouched. - -**Detail is frame-authoritative; list is not.** `GET …/inbox/:id` re-derives subject, -sender, recipients, date, Message-ID and snippet from the stored RFC 2822 frame -(cached columns as fallback), so a malformed frame degrades to a partial message rather -than a failed request. **List** never selects `raw` and never decodes: it projects -`subject`/`from` from the write-time cached columns, omits `snippet`, sets -`date` from `created_at`, `messageId` to the row id, and `to` to `[address]`. - -See [ARCHITECTURE.md](../../ARCHITECTURE.md) for the data model and the design -rationale behind it. - -## Requirements - -| | | -| --- | --- | -| Runtime | Node 22+ or Bun 1.1+ | -| Postgres | 13+ | -| Control plane | Interchange-shaped `public.tenant` and `public.principal` tables in the **same database**, created before `runMailboxMigrations` — the mailbox tables carry hard foreign keys to both | -| Minimum `@intx/*` | **0.2.2** — 0.1.x does not compile (no `SidecarAuthenticator`, four-verb `SessionService`) | - -`hono`, `drizzle-orm`, `postgres`, `@intx/log`, `@intx/mime` and `@intx/types` are -**peer dependencies**. Each breaks quietly with a second copy in the tree — -`@intx/log` configures LogTape's module-global registry, so two copies means your -sinks silently miss this package's logs. Install one copy of each at the top level. - -## Install - -```sh -npm install @corbits/mailbox -``` - -> **Not on npm yet.** Until the first release, consume it from a git checkout or an -> `npm pack` tarball. The `@intx/*` packages *are* published, at `0.2.2`. - -## Mount - -```ts -import { Hono } from "hono"; -import { - mountMailbox, - runMailboxMigrations, - createInMemoryMailboxEventBus, -} from "@corbits/mailbox"; - -// Your own drizzle handle. The mailbox never opens a second pool. -await runMailboxMigrations(db); - -const api = new Hono(); -mountMailbox(api, { - db, - bus: createInMemoryMailboxEventBus(), - resolvePrincipal: (ctx) => { - const user = (ctx as Context).get("user"); - return user ? { tenantId: user.tenantId, principalId: user.id } : null; - }, - // Your taxonomy, not ours. `priorities` is ORDERED, most urgent first. - vocabulary: { - priorities: ["urgent", "high", "normal", "low"], - statuses: ["needs-action", "done"], - }, -}); - -const app = new Hono(); -app.route("/api", api); -``` - -Routes are registered root-relative and served under `/api` — the prefix Interchange -serves its own routes under. No `/v1`, no vendor prefix. - -`examples/reference-host` in this repository is a complete `@intx/hub-api` host with -this package mounted and the acceptance suite pointed at it. Start there if you need -the whole `createApp` wiring. - -No handle of your own? `createMailboxDb(url)` opens one and returns `{ db, close }`. - -## The seams - -| Seam | Contract | -| --- | --- | -| `db` | Your drizzle handle, pointed at the HOST's database — the one your control plane lives in. `MailboxDb` is schema-agnostic in the drizzle sense (every query names its table explicitly, nothing uses `db.query.*`), so a handle bound to your schema is assignable. There is no separate-database mode: the control-plane foreign keys can only hold when both planes share one database. | -| `resolvePrincipal(ctx)` | Takes the host context as `unknown`, returns `{ tenantId, principalId } \| null`. The only authentication seam — this package has no opinion about sessions, cookies or tokens. | -| `bus` | SSE fan-out, keyed by the `(tenantId, principalId)` pair (`MailboxEventScope`) — a principal id is only unique within its tenant, so a principal-only key would leak one tenant's events to another's same-named principal. The in-memory default suits a single process; supply a broker-backed `MailboxEventBus` to fan out across replicas. | -| `vocabulary` | **Required, no default.** Priority ranking, query validation and OpenAPI enums are all generated from your lists. | -| `resolveSenderDisplays` | Optional. Batched per read; turns sender addresses into human labels. Best-effort — a resolver that throws costs the page its labels, never the page. | -| `runMailboxMigrations` | Idempotent, advisory-locked, own ledger (`"mailbox"."corbits_mailbox_migrations"`). Call on every boot from every replica — after the host's control plane exists, since the DDL's foreign keys reference `public.tenant` and `public.principal`. Migrations are checksummed — editing one that already shipped fails the next boot loudly. | - -## Routes - -| Method | Path | | -| --- | --- | --- | -| `GET` | `/api/me/inbox` | List. `view=all\|unread\|archived\|trash`, `limit`, `cursor`, `sort=date\|priority`, plus `priority`/`classification`/`status`/`assignee` filters. | -| `GET` | `/api/me/inbox/unread-count` | Unread, non-archived, non-trashed count. | -| `GET` | `/api/me/inbox/events` | SSE stream, `: heartbeat` every 25s. See the client contract below. | -| `GET` | `/api/me/inbox/:id` | One message with its full body. | -| `POST` | `/api/me/inbox/:id/{read,unread,trash,archive,restore}` | Single-message mutations. | -| `POST` | `/api/me/inbox/:id/enrich` | Stamp triage `priority`/`classification`/`status`. | -| `POST` | `/api/me/inbox/:id/assign` | Delegate to another principal (`null` un-assigns). | -| `POST` | `/api/me/inbox/bulk` | One action across up to 50 ids, partial success. | - -All routes carry `describeRoute`, so they appear in the host's OpenAPI document. - -### The SSE client contract - -**Events are non-durable nudges, not data.** Each event carries an id to -refetch and, when the publisher knows it, `op` — which operation fired -(`create`, `mark_read`, `mark_unread`, `trash`, `archive`, `restore`, -`enrich`, `assign`). `op` is **additive**: it is optional on the wire, so a -client that only reads `id` (the original shape) keeps working unchanged, -and a client that wants to react to a specific kind of change (e.g. badge a -new arrival differently from a read receipt) can switch on it instead of -re-fetching and diffing every message. Postgres remains the source of truth -either way — `op` narrows what changed, it does not replace a refetch when -you need the new state. The server queues at most `MAX_PENDING_SSE_EVENTS` -(100) events per connection — a consumer that stops reading is -**disconnected**, not buffered for. So the contract for any client: - -- On **any** disconnect — network drop, server restart, or an overflow close — - reconnect and **refetch from the API**: the list and the unread count. Never - assume the stream told you everything that happened while you were away. -- Do not treat the stream as a change log. It may drop events (publish is - best-effort after commit) and the server may close a stream whose consumer - stops draining it. -- **Events can be missed, duplicated, or arrive out of order** — this is a - best-effort nudge channel, not a durable log: - - *Missed*: publish failures are logged and swallowed, never retried; an - overflowing connection is disconnected, not buffered for. - - *Duplicated*: redelivery is deduped by default — an inbox item with a - stable external identifier inserts once, on conflict-do-nothing. Only a - redelivered item with no such identifier inserts a second row and - publishes a second `create`; a broker-backed bus a host supplies for - multi-replica fan-out may also redeliver on its own. Treat a repeat of - an already-applied `op` for the same `id` as a no-op. - - *Out of order*: the default in-memory bus preserves publish order within - one process for one mailbox, but a broker-backed bus, or multiple - replicas publishing concurrently, gives no such guarantee. Never infer - "later event = later state" from arrival order. - -Four more behaviors to know before wiring a UI: - -- **No-identity asymmetry** (the cross-core rule, not a mailbox quirk). With no - resolvable principal, collection reads answer `200` with an empty result — "show me - my inbox" has a truthful answer for someone with no inbox. Everything else names or - streams a *specific* identity and answers `403`. -- **Cursors are bound to the result set that minted them.** Replaying one under a - different view, sort or filter set is a `400`; paging a `priority=high` cursor into - an unfiltered list would silently skip messages. Malformed cursors are also always - a `400`, never a `500`: the timestamp must be exactly the microsecond shape this - package mints and the priority rank a safe integer, so a crafted cursor never - reaches Postgres. -- **`bulk` with `ids: []` is a `200`** (`{updated: 0, results: []}`), not a `400` — the - partial-success contract applied to zero ids. -- **`limit` is rejected, never clamped.** Default 50, ceiling `MAX_MAILBOX_PAGE_LIMIT` - (200). A caller handed 200 after asking for 500 advances its paging by 500 and skips - the difference. -- **Frame size and transport recipient count are hard caps.** Direct write - (`writeMailboxMessage` / `deliverInboxItems`) throws `RangeError` when a built - frame would exceed `MAX_MAILBOX_FRAME_BYTES` (1 MiB). The transport dual-write - path applies the same frame-byte cap and a hard ceiling of - `MAX_MAILBOX_RECIPIENTS` (50) on the **raw** recipient address list (before - domain filter / principal resolve — never clamps to the first 50). Over-cap - refusal there is logged and swallowed (dual-write independence) so - `createMailboxPersist` still returns upstream success; hosts that need larger - fan-out must chunk across calls. - -## Writing into a mailbox - -```ts -const delivered = await deliverInboxItems(db, items, { bus }); -// each result is { messageKey, id }; `id === null` means already delivered -``` - -`deliverInboxItems` is the ingress-adapter seam, deduping on -`mailboxKey.inbox(source, externalId)` — a versioned length-prefixed encoding -(`inbox2:::`) so pairs that contain `:` cannot -collide (NUL-join is injective too, but Postgres text rejects U+0000). The -`inbox2:` prefix keeps the space disjoint from pre-upgrade -`inbox::` keys: length-prefix under `inbox:` alone would -false-collide when a historical source was pure decimal. Pre-upgrade rows will -not dedupe against the new encoding and cannot false-collide with it; no -migration is performed. - -After blank-scope prevalidation, **the entire batch commits in one transaction** -(or none): a mid-batch FK / driver failure rolls back every new row from that -call. Deduped keys (`id: null`) are no-ops inside the transaction. Bus publish -and the optional host `enqueue` hook run only after commit, and only for newly -inserted ids. `enqueue` is best-effort — a throw is logged with the message id -and never rejects the delivery (same posture as bus publish). A host whose hook -permanently fails on the first try must triage independently; retries of the -same items will dedupe and skip enqueue. - -`writeMailboxMessage` inserts a single row. Idempotency keys are namespaced -(`mailboxKey.inbox`/`.gate`/`.run`). - -Every delivery writes the mail row **and its management row in one transaction** — -the management row is eager, so every mutation and the unread count are plain -operations on `mailbox`. `InboxItem` carries optional triage fields, so an adapter -that already knows an item's verdict stamps them at delivery instead of writing then -updating; a crash between two separate writes could otherwise strand a delivered -message behind the dedupe with no management row. - -Only inbound rows are written, by design — the read path filters `direction = 'inbound'`, -and the sender's outbound record belongs to whatever transport actually sent the frame. - -## Triage enriches the mail row - -Mail is the single work surface: triage does not spawn a task object. - -```ts -await enrichMailboxMessage(db, { tenantId, principalId, id }, { - priority: "urgent", // from YOUR vocabulary - classification: "deal-risk", - status: "needs-action", -}); -``` - -Each field applies independently — an omitted key leaves the stored value alone, an -explicit `null` clears it, and an enrichment that sets nothing is refused. Read it back -with `listUserMailbox`'s `filter` (ANDed) and `sort=priority`, where untriaged mail -ranks last. `filter.priority`/`status` are checked against your vocabulary (unknown -value → `400`); `classification`/`assignee` are open strings. - -Delegation is an `assignee` ref on the `mailbox` row, not a forwarded copy: the item -stays in the delegator's mailbox, so `?assignee=user-2` answers "what have I handed to -user-2". It does not put the item into user-2's inbox — write a second row for that. - -Validate untrusted bodies with `MailboxEnrichmentSchema` / `MailboxAssignmentSchema`. - -## Hard foreign keys — and what they buy you - -Both tables reference the host's control plane: `tenant_id → public.tenant(id)` and -`principal_id → public.principal(id)`, both `ON DELETE CASCADE`. That is the same -posture as Interchange's own `session_mail.tenant_id REFERENCES tenant ON DELETE -CASCADE`, extended to the principal, and it does two jobs the database is better at -than application code: - -**Writes to a scope the control plane does not know are refused by the database.** A -tenant or principal id that matches no control-plane row is a foreign-key violation, -not a stored orphan. Blank or whitespace-only ids are still refused earlier, with a -`RangeError` at the write boundary — the FK would catch them too, but only as a driver -error deep in the insert, long after the caller who typed `""` lost its stack. -Identifiers are never trimmed for you. `assertMailboxScope` and `MailboxScopeIdsSchema` -are exported for your own boundary. - -**Offboarding is a cascade.** Deleting a tenant or principal row in your control plane -carries every one of its mailbox rows out with it — no window in which orphaned mail -exists. The explicit purges remain for hosts that soft-delete or archive control-plane -rows instead, where no cascade ever fires but the mail data must still go: - -```ts -await purgeTenantMailbox(db, tenantId); // the whole tenant -await purgePrincipalMailbox(db, { tenantId, principalId }); // one person -``` - -Each purge is a **single `DELETE` on `principal_mail`** — the management rows follow -through the `mailbox.id → principal_mail.id` cascade, so it is atomic by construction. -Both return the message count and take the handle you pass, so a transaction runs the -purge inside your own offboarding unit of work. Both are irreversible; `raw` is the -only copy of the frame held here. - -## Message schemas - -`MailboxMessageSchema`, `MailboxMessageDetailSchema`, `MailboxListResponseSchema` and -`MailboxRefSchema` are real arktype schemas, so a consumer decoding this package's JSON -validates rather than casts. - -- **`from` is always present** — on detail the header → cached column → default chain - ends in `""`; on list only the cached column → default chain applies. Either way a - client never branches on its absence. `subject` has no such default: an empty subject - is distinct from no subject. -- **Body and snippet come from the frame on detail only, multipart included.** Detail - walks MIME path `1` (or `1.1` when part 1 is itself multipart) via `@intx/mime`, - rather than returning the MIME envelope verbatim. An unwalkable frame yields an empty - body, never a 500. List omits `snippet` entirely and never loads `raw`. -- **Message-ID fallback is `hub.invalid`** (`MESSAGE_ID_FALLBACK_DOMAIN`) — RFC 2606 - reserved, guaranteed never to resolve, so a malformed sender cannot mint a routable- - looking id. `generateMailboxMessageId` is exported. - -## The transport dual-write seam - -If your host already has a mail transport that persists frames, `createMailboxPersist` -wraps it so every addressed principal also gets a durable inbound row: - -```ts -const persistMail = createMailboxPersist(db, { - upstream: transport.persistMail, - authorizeSender: async (senderAddress) => { - const instance = await findActiveInstance(senderAddress); - return instance ? { tenantId: instance.tenantId, domain: instance.domain } : null; - }, - bus, -}); -``` - -Sender authorization is the host's — whether an address belongs to a live agent -instance is the host's call, not a schema fact. The enforcement is ours: an -unauthorized sender gets no mailbox row while the frame is still delegated upstream, -and recipients outside the authorized `domain` are skipped, so cross-tenant delivery is -impossible by construction. Recipient local parts are sender-controlled, so addresses -that resolve to no known principal in the tenant are skipped with a warning rather than -minting a phantom mailbox row — and one typo'd address never costs the real recipients -on the same frame their durable copy. - -Dual-write independence holds both ways: `upstream` throwing still attempts the mailbox -write then re-throws unchanged, and a mailbox-write failure is logged loudly but never -rejects a persist that already succeeded upstream. - -Mailbox inserts are idempotent under transport retry: each row is stamped with a -package-owned `messageKey` (`transport:mid::` when the -frame carries a Message-ID, otherwise `transport:raw::`) and -inserted with `onConflictDoNothing` on the existing partial unique index. Management -rows and bus announcements only follow rows actually returned by `RETURNING`, so a -retried frame does not fail on unique-violation and does not re-announce duplicates. - -## Development - -```sh -bun run test # unit + integration (needs Postgres) -bun run test:coverage -bun run build # dist/ — JS + .d.ts, consumable from Node -``` - -The tarball ships `src/` alongside `dist/`, so the emitted `.js.map` and `.d.ts.map` -resolve: go-to-definition and debugger steps land on real TypeScript. - -## License - -LGPL-2.1-only. See [LICENSE](./LICENSE). diff --git a/packages/mailbox/package.json b/packages/mailbox/package.json deleted file mode 100644 index e0216a4..0000000 --- a/packages/mailbox/package.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "name": "@corbits/mailbox", - "version": "0.1.0", - "type": "module", - "license": "LGPL-2.1-only", - "description": "Universal principal-keyed inbox, mountable onto any Interchange host. Requires @intx 0.2.2 or newer.", - "keywords": [ - "corbits", - "interchange", - "intx", - "mailbox", - "inbox", - "hono", - "drizzle", - "postgres" - ], - "author": "Corbits", - "homepage": "https://github.com/corbitsdev/corbits-mailbox#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/corbitsdev/corbits-mailbox.git", - "directory": "packages/mailbox" - }, - "bugs": { - "url": "https://github.com/corbitsdev/corbits-mailbox/issues" - }, - "engines": { - "node": ">=22" - }, - "corbits": { - "minimumIntxVersion": "0.2.2" - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - }, - "files": [ - "dist", - "src", - "!src/**/*.test.ts", - "!src/test-helpers.ts", - "LICENSE", - "README.md" - ], - "sideEffects": false, - "scripts": { - "build": "rm -rf dist && tsc -p tsconfig.build.json", - "prepack": "bun run build", - "test": "bun test", - "test:coverage": "bun test --coverage" - }, - "dependencies": { - "@hono/standard-validator": "0.2.3", - "@standard-community/standard-json": "0.3.5", - "@standard-community/standard-openapi": "0.2.9", - "arktype": "2.1.29", - "hono-openapi": "1.3.1" - }, - "peerDependencies": { - "@intx/log": "^0.2.2", - "@intx/mime": "^0.2.2", - "@intx/types": "^0.2.2", - "drizzle-orm": "^0.45.2", - "hono": "^4.12.0", - "postgres": "^3.4.0" - }, - "devDependencies": { - "@intx/log": "0.2.2", - "@intx/mime": "0.2.2", - "@intx/types": "0.2.2", - "@types/bun": "1.1.14", - "@types/json-schema": "7.0.15", - "@types/node": "22.10.5", - "drizzle-orm": "0.45.2", - "hono": "4.12.32", - "openapi-types": "12.1.3", - "postgres": "3.4.9", - "typescript": "5.7.2" - } -} diff --git a/packages/mailbox/tsconfig.json b/packages/mailbox/tsconfig.json deleted file mode 100644 index 2e69040..0000000 --- a/packages/mailbox/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "include": [ - "src" - ] -} diff --git a/scripts/prepare.mjs b/scripts/prepare.mjs new file mode 100644 index 0000000..bbd9af4 --- /dev/null +++ b/scripts/prepare.mjs @@ -0,0 +1,93 @@ +/** + * `prepare` hook — builds `dist/` when the package is installed from git. + * + * The package publishes `dist/` (and `src`) via `files`, and the `exports` map + * points at `dist/`. A registry tarball arrives already built, via `prepack`. + * A git install does not: it gets the repository, so something has to build on + * the way in, and `prepare` is the one lifecycle hook package managers run at + * that moment. Without it every `@corbits/mailbox` import in the consumer + * resolves to a file that was never generated, and the failure lands in the + * consumer's tree, where they cannot fix it. + * + * Three things make this more than a one-line script: + * + * 1. `prepare` also runs for the *root* project on a plain `bun install` here + * in the repository. Building then would be wasted work at best, and + * recursive at worst (see 2). Being installed as a dependency is what + * distinguishes the two cases, and the honest signal for it is that the + * package directory sits inside a `node_modules` tree. + * 2. bun does not install a git dependency's devDependencies, and the whole + * toolchain (tsc) is a devDependency — as it should be, a consumer must not + * inherit it as a runtime dep. So the git-install path installs them itself, + * immediately before the build that needs them. + * 3. That build cannot run inside the consumer's `node_modules`. Declaration + * emit has to name the host's types, and installing the toolchain in place + * puts them under paths TypeScript refuses to write into a `.d.ts` + * (TS2742, "cannot be named without a reference to ..."). Building in a + * scratch directory outside any `node_modules` tree keeps those paths + * nameable; it is the same layout the build already runs in during + * development and on CI. + * + * Deliberately a no-op when `dist/` is already present: repeat installs of the + * same commit should not pay for a rebuild. + */ +import { execFileSync } from "node:child_process"; +import { cpSync, existsSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, sep } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = dirname(dirname(fileURLToPath(import.meta.url))); + +if (existsSync(join(packageDir, "dist", "index.js"))) { + process.exit(0); +} + +const installedAsDependency = packageDir.split(sep).includes("node_modules"); +if (!installedAsDependency) { + // Developing the repository itself. `bun run build` is a documented step of + // getting set up (README.md) and stays an explicit one — a `prepare` that + // silently ran the whole gate on every `bun install` would be a surprise, + // and the recursive `bun install` below would be a worse one. + process.exit(0); +} + +// The toolchain install below is itself a `bun install`. It no longer runs in +// this directory, but the scratch copy carries this same script, and bun runs +// `prepare` for the root project too — so without a marker the copy would +// start the whole dance again. This breaks that cycle. +const RECURSION_GUARD = "CORBITS_MAILBOX_PREPARE"; +if (process.env[RECURSION_GUARD] === "1") { + process.exit(0); +} + +function run(command, args, cwd) { + execFileSync(command, args, { + cwd, + stdio: "inherit", + env: { ...process.env, [RECURSION_GUARD]: "1" }, + }); +} + +console.log("@corbits/mailbox: git install — building dist/"); + +const buildDir = mkdtempSync(join(tmpdir(), "corbits-mailbox-")); +try { + cpSync(packageDir, buildDir, { + recursive: true, + // `node_modules` is what carries the unnameable layout, so copying it + // would defeat the point; `dist` is the thing being built. + filter: (source) => { + if (source === packageDir) return true; + const [top] = source.slice(packageDir.length + 1).split(sep); + return top !== "node_modules" && top !== "dist"; + }, + }); + + run("bun", ["install"], buildDir); + run("bun", ["run", "build"], buildDir); + + cpSync(join(buildDir, "dist"), join(packageDir, "dist"), { recursive: true }); +} finally { + rmSync(buildDir, { recursive: true, force: true }); +} diff --git a/packages/mailbox/src/bus-unsubscribe.test.ts b/src/bus-unsubscribe.test.ts similarity index 100% rename from packages/mailbox/src/bus-unsubscribe.test.ts rename to src/bus-unsubscribe.test.ts diff --git a/packages/mailbox/src/bus.test.ts b/src/bus.test.ts similarity index 100% rename from packages/mailbox/src/bus.test.ts rename to src/bus.test.ts diff --git a/packages/mailbox/src/bus.ts b/src/bus.ts similarity index 100% rename from packages/mailbox/src/bus.ts rename to src/bus.ts diff --git a/packages/mailbox/src/concurrency.test.ts b/src/concurrency.test.ts similarity index 100% rename from packages/mailbox/src/concurrency.test.ts rename to src/concurrency.test.ts diff --git a/packages/mailbox/src/db.ts b/src/db.ts similarity index 100% rename from packages/mailbox/src/db.ts rename to src/db.ts diff --git a/packages/mailbox/src/enrichment.test.ts b/src/enrichment.test.ts similarity index 100% rename from packages/mailbox/src/enrichment.test.ts rename to src/enrichment.test.ts diff --git a/packages/mailbox/src/filter-sort-assign.test.ts b/src/filter-sort-assign.test.ts similarity index 100% rename from packages/mailbox/src/filter-sort-assign.test.ts rename to src/filter-sort-assign.test.ts diff --git a/packages/mailbox/src/fk-cascade.test.ts b/src/fk-cascade.test.ts similarity index 100% rename from packages/mailbox/src/fk-cascade.test.ts rename to src/fk-cascade.test.ts diff --git a/packages/mailbox/src/frame-multipart.test.ts b/src/frame-multipart.test.ts similarity index 100% rename from packages/mailbox/src/frame-multipart.test.ts rename to src/frame-multipart.test.ts diff --git a/packages/mailbox/src/frame.test.ts b/src/frame.test.ts similarity index 100% rename from packages/mailbox/src/frame.test.ts rename to src/frame.test.ts diff --git a/packages/mailbox/src/frame.ts b/src/frame.ts similarity index 100% rename from packages/mailbox/src/frame.ts rename to src/frame.ts diff --git a/packages/mailbox/src/index.ts b/src/index.ts similarity index 100% rename from packages/mailbox/src/index.ts rename to src/index.ts diff --git a/packages/mailbox/src/log-warnings.test.ts b/src/log-warnings.test.ts similarity index 100% rename from packages/mailbox/src/log-warnings.test.ts rename to src/log-warnings.test.ts diff --git a/packages/mailbox/src/message-schemas.test.ts b/src/message-schemas.test.ts similarity index 100% rename from packages/mailbox/src/message-schemas.test.ts rename to src/message-schemas.test.ts diff --git a/packages/mailbox/src/migrations.test.ts b/src/migrations.test.ts similarity index 100% rename from packages/mailbox/src/migrations.test.ts rename to src/migrations.test.ts diff --git a/packages/mailbox/src/migrations.ts b/src/migrations.ts similarity index 100% rename from packages/mailbox/src/migrations.ts rename to src/migrations.ts diff --git a/packages/mailbox/src/mount-bus-failure.test.ts b/src/mount-bus-failure.test.ts similarity index 100% rename from packages/mailbox/src/mount-bus-failure.test.ts rename to src/mount-bus-failure.test.ts diff --git a/packages/mailbox/src/mount-event-op.test.ts b/src/mount-event-op.test.ts similarity index 100% rename from packages/mailbox/src/mount-event-op.test.ts rename to src/mount-event-op.test.ts diff --git a/packages/mailbox/src/mount.test.ts b/src/mount.test.ts similarity index 100% rename from packages/mailbox/src/mount.test.ts rename to src/mount.test.ts diff --git a/packages/mailbox/src/mount.ts b/src/mount.ts similarity index 100% rename from packages/mailbox/src/mount.ts rename to src/mount.ts diff --git a/packages/mailbox/src/mutations.test.ts b/src/mutations.test.ts similarity index 100% rename from packages/mailbox/src/mutations.test.ts rename to src/mutations.test.ts diff --git a/packages/mailbox/src/mutations.ts b/src/mutations.ts similarity index 100% rename from packages/mailbox/src/mutations.ts rename to src/mutations.ts diff --git a/packages/mailbox/src/openapi.test.ts b/src/openapi.test.ts similarity index 100% rename from packages/mailbox/src/openapi.test.ts rename to src/openapi.test.ts diff --git a/packages/mailbox/src/persist.test.ts b/src/persist.test.ts similarity index 100% rename from packages/mailbox/src/persist.test.ts rename to src/persist.test.ts diff --git a/packages/mailbox/src/persist.ts b/src/persist.ts similarity index 100% rename from packages/mailbox/src/persist.ts rename to src/persist.ts diff --git a/packages/mailbox/src/purge.test.ts b/src/purge.test.ts similarity index 100% rename from packages/mailbox/src/purge.test.ts rename to src/purge.test.ts diff --git a/packages/mailbox/src/purge.ts b/src/purge.ts similarity index 100% rename from packages/mailbox/src/purge.ts rename to src/purge.ts diff --git a/packages/mailbox/src/read-direction-and-date.test.ts b/src/read-direction-and-date.test.ts similarity index 100% rename from packages/mailbox/src/read-direction-and-date.test.ts rename to src/read-direction-and-date.test.ts diff --git a/packages/mailbox/src/read-fallbacks.test.ts b/src/read-fallbacks.test.ts similarity index 100% rename from packages/mailbox/src/read-fallbacks.test.ts rename to src/read-fallbacks.test.ts diff --git a/packages/mailbox/src/read-microsecond-cursor.test.ts b/src/read-microsecond-cursor.test.ts similarity index 100% rename from packages/mailbox/src/read-microsecond-cursor.test.ts rename to src/read-microsecond-cursor.test.ts diff --git a/packages/mailbox/src/read-multipart.test.ts b/src/read-multipart.test.ts similarity index 100% rename from packages/mailbox/src/read-multipart.test.ts rename to src/read-multipart.test.ts diff --git a/packages/mailbox/src/read-non-utc-session.test.ts b/src/read-non-utc-session.test.ts similarity index 100% rename from packages/mailbox/src/read-non-utc-session.test.ts rename to src/read-non-utc-session.test.ts diff --git a/packages/mailbox/src/read-page-boundary.test.ts b/src/read-page-boundary.test.ts similarity index 100% rename from packages/mailbox/src/read-page-boundary.test.ts rename to src/read-page-boundary.test.ts diff --git a/packages/mailbox/src/read.test.ts b/src/read.test.ts similarity index 100% rename from packages/mailbox/src/read.test.ts rename to src/read.test.ts diff --git a/packages/mailbox/src/read.ts b/src/read.ts similarity index 100% rename from packages/mailbox/src/read.ts rename to src/read.ts diff --git a/packages/mailbox/src/recipients.test.ts b/src/recipients.test.ts similarity index 100% rename from packages/mailbox/src/recipients.test.ts rename to src/recipients.test.ts diff --git a/packages/mailbox/src/recipients.ts b/src/recipients.ts similarity index 100% rename from packages/mailbox/src/recipients.ts rename to src/recipients.ts diff --git a/packages/mailbox/src/schema-check.test.ts b/src/schema-check.test.ts similarity index 100% rename from packages/mailbox/src/schema-check.test.ts rename to src/schema-check.test.ts diff --git a/packages/mailbox/src/schema-check.ts b/src/schema-check.ts similarity index 100% rename from packages/mailbox/src/schema-check.ts rename to src/schema-check.ts diff --git a/packages/mailbox/src/schema-ddl-parity.test.ts b/src/schema-ddl-parity.test.ts similarity index 100% rename from packages/mailbox/src/schema-ddl-parity.test.ts rename to src/schema-ddl-parity.test.ts diff --git a/packages/mailbox/src/schema.ts b/src/schema.ts similarity index 100% rename from packages/mailbox/src/schema.ts rename to src/schema.ts diff --git a/packages/mailbox/src/scope-validation.test.ts b/src/scope-validation.test.ts similarity index 100% rename from packages/mailbox/src/scope-validation.test.ts rename to src/scope-validation.test.ts diff --git a/packages/mailbox/src/sender-display.test.ts b/src/sender-display.test.ts similarity index 100% rename from packages/mailbox/src/sender-display.test.ts rename to src/sender-display.test.ts diff --git a/packages/mailbox/src/sse-heartbeat.test.ts b/src/sse-heartbeat.test.ts similarity index 100% rename from packages/mailbox/src/sse-heartbeat.test.ts rename to src/sse-heartbeat.test.ts diff --git a/packages/mailbox/src/sse-stream.test.ts b/src/sse-stream.test.ts similarity index 100% rename from packages/mailbox/src/sse-stream.test.ts rename to src/sse-stream.test.ts diff --git a/packages/mailbox/src/test-helpers.ts b/src/test-helpers.ts similarity index 100% rename from packages/mailbox/src/test-helpers.ts rename to src/test-helpers.ts diff --git a/packages/mailbox/src/vocabulary.test.ts b/src/vocabulary.test.ts similarity index 100% rename from packages/mailbox/src/vocabulary.test.ts rename to src/vocabulary.test.ts diff --git a/packages/mailbox/src/vocabulary.ts b/src/vocabulary.ts similarity index 100% rename from packages/mailbox/src/vocabulary.ts rename to src/vocabulary.ts diff --git a/packages/mailbox/src/write.test.ts b/src/write.test.ts similarity index 100% rename from packages/mailbox/src/write.test.ts rename to src/write.test.ts diff --git a/packages/mailbox/src/write.ts b/src/write.ts similarity index 100% rename from packages/mailbox/src/write.ts rename to src/write.ts diff --git a/packages/mailbox/tsconfig.build.json b/tsconfig.build.json similarity index 89% rename from packages/mailbox/tsconfig.build.json rename to tsconfig.build.json index 962e292..c94dafa 100644 --- a/packages/mailbox/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "./tsconfig.json", "compilerOptions": { "types": [ "node" From df649aa9755da6cc2dabc58bdb5cc1b9d843f5d8 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 10 Aug 2026 08:10:22 -0700 Subject: [PATCH 2/2] Fix CI for the top-level layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build before typecheck so @corbits/mailbox resolves through dist/, then force-reinstall to refresh the reference host's copied file: dependency — bun copies file: deps at install time, before dist/ exists. Point the remaining workflow and doc references at the repo root instead of the removed packages/mailbox directory. --- .github/workflows/test.yml | 21 +++++++++++++-------- ARCHITECTURE.md | 2 +- package.json | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4bcc31b..7dec469 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,16 +34,22 @@ jobs: - run: bun install --frozen-lockfile + # Emits dist/, which is what the reference host and the typecheck + # resolve @corbits/mailbox through — so everything downstream proves + # the published artifact, not just the sources. + - name: build + run: bun run build + + # bun copies `file:` dependencies at install time, before dist/ exists; + # a forced reinstall refreshes the copy with the built output. + - name: refresh reference-host dependency copy + run: bun install --force + - name: typecheck run: bun run typecheck - name: unit + integration tests - run: bun run --cwd packages/mailbox test:coverage - - # Emits dist/, which is also what the reference host resolves through — - # so this proves the published artifact, not just the sources. - - name: build - run: bun run build + run: bun run test:coverage - name: reference-host acceptance run: bun test --cwd examples/reference-host @@ -54,8 +60,7 @@ jobs: - name: node consumer smoke test run: | set -euo pipefail - TARBALL="$(cd packages/mailbox && npm pack --silent)" - TARBALL="$PWD/packages/mailbox/$TARBALL" + TARBALL="$PWD/$(npm pack --silent)" mkdir -p "$RUNNER_TEMP/consumer" && cd "$RUNNER_TEMP/consumer" npm init -y >/dev/null && npm pkg set type=module >/dev/null npm install "$TARBALL" diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 073efac..7219477 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,7 +2,7 @@ How `@corbits/mailbox` is put together, and what it does and does not ask of the host that mounts it. For install, the mount snippet, the route table and the -response contracts, see the [package README](./packages/mailbox/README.md) — +response contracts, see the [README](./README.md) — this document is about structure and reasoning, and does not repeat them. ## The shape of the thing diff --git a/package.json b/package.json index 2016ebf..63d79c2 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "typecheck": "tsc --noEmit", "test": "bun test src", "test:coverage": "bun test src --coverage", - "test:acceptance": "bun run build && bun test --cwd examples/reference-host" + "test:acceptance": "bun run build && bun install --force && bun test --cwd examples/reference-host" }, "dependencies": { "@hono/standard-validator": "0.2.3",