From eafd2ea10e966b7b089563c4eafefa2d3eac5fcf Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 24 Sep 2026 23:15:26 -0700 Subject: [PATCH] feat(routes)!: createMailboxRoutes(deps) replaces mountMailbox The routes are now a Hono sub-app the host mounts with app.route, the shape Interchange's own route factories use. Every route is gated through deps.requireGrant on mailbox:* (read, create for send, manage for flag and move). The principal comes only from the tenant and principal the host's tenant middleware set, the same one requireGrant authorizes; every route returns 403 when the context carries none. Closes CL-9067, CL-9070. --- .github/workflows/test.yml | 2 +- ARCHITECTURE.md | 54 +++---- CHANGELOG.md | 8 + bun.lock | 320 +++++++++++++++++++++++++++++++++++++ package.json | 2 + src/index.ts | 4 +- src/mount-native.test.ts | 23 +-- src/mount-send.test.ts | 50 +++--- src/mount.test.ts | 139 +++++++++++++--- src/mount.ts | 144 +++++++++-------- src/sse-heartbeat.test.ts | 61 ++++--- src/sse-stream.test.ts | 86 ++++++---- src/test-helpers.ts | 45 ++++++ 13 files changed, 729 insertions(+), 209 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5226dfd..227ca81 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,7 +62,7 @@ jobs: npm install "$TARBALL" node -e ' import("@corbits/mailbox").then((m) => { - for (const name of ["mountMailbox", "runMailboxMigrations"]) { + for (const name of ["createMailboxRoutes", "runMailboxMigrations"]) { if (typeof m[name] !== "function") throw new Error(`missing export: ${name}`); } console.log("node consumer ok"); diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ac061f4..e9ef921 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -12,52 +12,48 @@ pool by default, owns no configuration, and starts no background work. A host calls two functions: - `runMailboxMigrations(db)` — once at boot, before serving. -- `mountMailbox(app, opts)` — registers routes under `/me/inbox*` on a Hono app - the host already built, and returns the same app. +- `createMailboxRoutes(deps)` — returns a `Hono` sub-app serving + `/me/inbox*`, which the host mounts with `app.route`. ## Where the routes are served -The core registers root-relative paths and takes no base path, so the *mount -point* is the host's decision. The convention every `@corbits/*-core` package -documents is **`/api`** — the prefix Interchange serves its own routes under. -No `/v1` segment, no vendor prefix. +The sub-app registers root-relative paths and takes no base path, so the +*mount point* is the host's decision, the same way Interchange's own +`createGrantRoutes` and friends are mounted: ```ts -const api = new Hono(); -mountMailbox(api, { db, bus, resolvePrincipal, vocabulary }); -app.route("/api", api); +app.route( + "/api/tenants/:tenantId/mailbox", + createMailboxRoutes({ db, bus, requireGrant, senderAddressFor, deliver }), +); ``` -which serves `/api/me/inbox`, `/api/me/inbox/unread-count`, -`/api/me/inbox/events`, `/api/me/inbox/:id`, and the `POST` mutations beneath -them. Nesting rather than teaching the core a base path keeps the frozen -`mountX(app, opts) => Hono` seam untouched, and lands the -mailbox behind whatever the host already declared for `/api/me/*` — on an -Interchange host, `requireAuth`. +The host's tenant middleware runs first and sets `tenant` and `principal` on +the context, exactly as it does for its own `TenantEnv` routes. -## The mount seam +## The routes seam -`mountMailbox(app: Hono, opts): Hono` is generic over the -host's Hono `Env` and returns the app unchanged in type. Everything the package -cannot know on its own arrives through `opts`; nothing is reached for. +`createMailboxRoutes(deps: CreateMailboxRoutesDeps): Hono`. +Everything the package cannot know on its own arrives through `deps`; nothing +is reached for. -| Option | Required | What it is | +| Dep | Required | What it is | | --- | --- | --- | | `db` | yes | A drizzle `postgres-js` handle. The schema generic is `any` on purpose, so the host passes the handle it already has instead of opening a second pool. | | `bus` | yes | `MailboxEventBus` — per-mailbox fan-out backing the SSE route, keyed by the `(tenantId, principalId)` pair (`MailboxEventScope`). `createInMemoryMailboxEventBus()` ships as the zero-config default. | -| `resolvePrincipal(ctx)` | yes | `{ tenantId, principalId } \| null`. `ctx` is typed `unknown`, so no Hono context typing leaks into the seam. | -| `vocabulary` | yes | `{ priorities, statuses }` — the host's triage taxonomy; there is no default. | -| `resolveSenderDisplays` | no | Batched `(tenantId, fromHeaders) => Map`. Omitted, messages carry only the raw `From:` header. | +| `requireGrant` | yes | `@intx/hub-api`'s `RequireGrant`. Reads are gated on `mailbox:*` `read`, send on `create`, flag and move verbs on `manage`. | +| `senderAddressFor` | yes | The caller's `From:` address, from the host's own directory. | +| `deliver` | yes | The host's transport, called once per send after the Sent copy is filed. | | `heartbeatIntervalMs` | no | SSE keep-alive period, default 25s (under the 30s idle timeout most proxies default to). Exists so a test can observe a heartbeat without waiting. | -What it does **not** require: no auth middleware, no session library, no logger +What it does **not** require: no session library, no logger configuration, no UI. What it *does* require of the database is an Interchange-shaped control plane: `public.tenant` and `public.principal` in the same database, in place before `runMailboxMigrations` runs, because the mailbox tables foreign-key to both. Nothing changed in Interchange to make that work — the coupling lives entirely on this side. -One further seam lives outside `mountMailbox`, on the write side: +One further seam lives outside `createMailboxRoutes`, on the write side: `createMailboxPersist(db, { upstream, authorizeSender, bus?, onRow?, resolveRefs? })` wraps a host's own mail-persist function so every addressed principal also gets a durable row. `authorizeSender(address) => { tenantId, domain } | null` @@ -101,9 +97,6 @@ rest of the wrapper — logged (naming `resolveRefs` as the failing stage), upstream unaffected (already ran, or still will, independently of this), and no mailbox row for that frame. -`resolvePrincipal`'s signature is identical across the Corbits cores, so a host -mounting more than one passes the same function to each. - ## Modules | File | Role | @@ -312,7 +305,7 @@ required `op` (`MailboxEventOp`: `create`, `mark_read`, `mark_unread`, `trash`, `archive`, `restore`, `enrich`, `assign`) and includes it on the published event. Every call site in this package passes one — the two delivery paths (`writeMailboxMessage`, `deliverInboxItems`) and the transport dual-write -(`createMailboxPersist`) publish `create`; `mountMailbox`'s route table passes +(`createMailboxPersist`) publish `create`; `createMailboxRoutes`'s route table passes the mutation's own identifier, reusing `MailboxBulkAction`'s vocabulary for the single-message verbs so "read one" and "read fifty" report the same op. `op` stays *optional on `MailboxEventSchema`* even though it is required to @@ -494,7 +487,8 @@ delegation ref. Supplied by the host: the Hono app and the database handle (pointed at the database where `tenant` and `principal` live); the triage **vocabulary**; who -the caller is (`resolvePrincipal`); whether a sender may deliver +the caller is (the `tenant` and `principal` its tenant middleware sets on the +context); whether a sender may deliver (`authorizeSender`) and to which tenant; display names (`resolveSenderDisplays`); an event bus, if one process is not enough; and the actual mail transport — this package neither sends nor receives SMTP. diff --git a/CHANGELOG.md b/CHANGELOG.md index d4fa6ad..44de4f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -214,6 +214,14 @@ always called out under their own heading. ### Breaking +- **`mountMailbox` is replaced by `createMailboxRoutes(deps): Hono`.** + The host mounts the returned sub-app with `app.route`. `deps.requireGrant` + (`@intx/hub-api`'s `RequireGrant`, now a peer) gates reads on `mailbox:*` + `read`, send on `create`, and the flag and move verbs on `manage`. + `resolvePrincipal` is removed: the routes read the `tenant` and `principal` + the host's tenant middleware sets on the context, the same principal + `requireGrant` authorizes. `MountMailboxOpts` is now + `CreateMailboxRoutesDeps`. - **The barrel no longer exports schema objects or internal constants.** `principalMail`, `mailboxPgSchema`, `expectedColumnTypes`, `assertExpectedColumnTypes`, `MESSAGE_ID_FALLBACK_DOMAIN`, and diff --git a/bun.lock b/bun.lock index ed675f7..1ddd13c 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "hono-openapi": "1.3.1", }, "devDependencies": { + "@intx/hub-api": "0.4.0", "@intx/log": "0.4.0", "@intx/mailbox": "0.4.0", "@intx/mime": "0.4.0", @@ -27,6 +28,7 @@ "typescript": "5.7.2", }, "peerDependencies": { + "@intx/hub-api": "^0.4.0", "@intx/log": "^0.4.0", "@intx/mailbox": "^0.4.0", "@intx/mime": "^0.4.0", @@ -45,22 +47,84 @@ "@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="], + "@better-auth/core": ["@better-auth/core@1.7.6", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.41.1", "@standard-schema/spec": "^1.1.0", "zod": "^4.5.4" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.2", "@opentelemetry/api": "^1.9.0", "better-call": "1.4.0", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-yBDDO0J4VCHT1qEZtzj6aMexX4etyLesitoxehcWHzPrzYqrFHwe/hgskye+imlGhQVOysuljMg8M8BA6GrtIg=="], + + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.7.6", "", { "peerDependencies": { "@better-auth/core": "^1.7.6", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-ENlhwC7kkTjquuiel3Efggik0hC4vtD7ppJ8FhsiHhRJA8K6RT4hbhzkfm6keNvjdnAeWwCejm7FmMO4GoSGpA=="], + + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.7.6", "", { "peerDependencies": { "@better-auth/core": "^1.7.6", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-BV+DX2/z/6ozNC9DYSge78wjLerq6Bxd/lSPjzzuq9eEutkQDAJTuiaETzgGYqNV5LlEWi8TZFAQTEq/wavOEQ=="], + + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.7.6", "", { "peerDependencies": { "@better-auth/core": "^1.7.6", "@better-auth/utils": "0.4.2" } }, "sha512-ng1xv8k2JsEIjnqzJzdoVOQrnoCLRuk4EiQL9yYAThyg6zYzV00LynlhH+O9OMUZ+yGVpGqGZmr5xcTiUSV2uA=="], + + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.7.6", "", { "peerDependencies": { "@better-auth/core": "^1.7.6", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-GvBwZli8i4XINMAj9tlW7eV9E8Dr+JWqavXjUs2o81GZt/Vh8wiX+31KK1jQ4iOuejfjpLoXcoFtlIPgf/pVog=="], + + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.7.6", "", { "peerDependencies": { "@better-auth/core": "^1.7.6", "@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-31rz2OXIx2Q1L4Jhd5F71dafIWqder/Bo6vJ+eCgO5iZ0gvnv7fUmM95DvbVUoPC69XYpcIM/6PJmQ3M6RydWg=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.7.6", "", { "peerDependencies": { "@better-auth/core": "^1.7.6", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.2" } }, "sha512-xgRwPja2wTJtcu+OQZR2IiNpAmviddnW3n5Do5umXRugJFW3v9KlSmYjYm580mVsPnU4upvZkvSRl9v8DgSDtA=="], + + "@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.2", "", {}, "sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow=="], + + "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], + "@hono/standard-validator": ["@hono/standard-validator@0.2.3", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "hono": ">=3.9.0" } }, "sha512-bp9vHu6Va6SfMHC3D4ZLBbT/woi+AZ9CRdTXQu3kLJuLh2W/Gb9UO4hijS+BQAGFXi4EGpXdetxpzwTAawSVeg=="], + "@intx/agent": ["@intx/agent@0.4.0", "", { "dependencies": { "@intx/inference": "0.4.0", "@intx/log": "0.4.0", "@intx/mime": "0.4.0", "@intx/types": "0.4.0", "arktype": "^2.1.29" } }, "sha512-qBlqDYnsaRMsug/lEKwUvPgQfjiwppTpfw5JaUh6pwKi0dGvckv3ugjJvLdHM052nTFMgLgOwo1LvbRjPZ5Uwg=="], + + "@intx/authz": ["@intx/authz@0.4.0", "", { "dependencies": { "@intx/types": "0.4.0" } }, "sha512-A9x4EzcECnFjhDoqj0XjaOb1GaxRyJucXOuIbY+fcPluUuMCGFvExD6DJZwjJmIYdeCvL8i96gPkncu/fAR9qA=="], + "@intx/crypto": ["@intx/crypto@0.4.0", "", { "dependencies": { "@intx/types": "0.4.0" } }, "sha512-4/qWypC90GevbV9u1i/93tn46jsXhj8wE1Pn+awg8/ECpwhHnhPLT5Uy423Arr9eGmRmF6HiMyXcEgH1Z9/Rkw=="], + "@intx/db": ["@intx/db@0.4.0", "", { "dependencies": { "@intx/authz": "0.4.0", "@intx/crypto": "0.4.0", "@intx/hub-common": "0.4.0", "@intx/log": "0.4.0", "@intx/types": "0.4.0", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "postgres": "^3.4.8" } }, "sha512-B26emrRrpIAlRygTWs6akVA6V/rN07v4YRxj3iTxzAOJVayIHSmPuPmKxqKN/AsBgLq6H1q94RP9sq05yb4twg=="], + + "@intx/hub-api": ["@intx/hub-api@0.4.0", "", { "dependencies": { "@hono/standard-validator": "^0.2.2", "@intx/agent": "0.4.0", "@intx/authz": "0.4.0", "@intx/crypto": "0.4.0", "@intx/db": "0.4.0", "@intx/hub-common": "0.4.0", "@intx/hub-sessions": "0.4.0", "@intx/log": "0.4.0", "@intx/mime": "0.4.0", "@intx/storage-isogit": "0.4.0", "@intx/types": "0.4.0", "@intx/workflow-deploy": "0.4.0", "arktype": "^2.1.29", "better-auth": "^1.4.18", "drizzle-orm": "^0.45.1", "hono": "^4.11.9", "hono-openapi": "^1.2.0", "isomorphic-git": "^1.27.2", "ssri": "^12.0.0" } }, "sha512-3Txw7Im38BdRujYeRlxgrzCdvOG1v92556AjNa/6WXSwgXt9Mn2fB4RoQrA6oUHcm10Y94WZh3IjaQsXFl7Qvg=="], + + "@intx/hub-common": ["@intx/hub-common@0.4.0", "", { "dependencies": { "@intx/types": "0.4.0" } }, "sha512-4dcRX3kamqduf1Yil85GVsjXKnAnu6uqSYrAsl4vGpuV6R2DCy7z/PHd+DtIqk6TJrRdW7DtfkNXwSEc3dn2VQ=="], + + "@intx/hub-sessions": ["@intx/hub-sessions@0.4.0", "", { "dependencies": { "@intx/agent": "0.4.0", "@intx/crypto": "0.4.0", "@intx/db": "0.4.0", "@intx/hub-common": "0.4.0", "@intx/log": "0.4.0", "@intx/mime": "0.4.0", "@intx/pack-transport": "0.4.0", "@intx/storage-isogit": "0.4.0", "@intx/tool-packaging": "0.4.0", "@intx/types": "0.4.0", "@intx/workflow": "0.4.0", "@intx/workflow-deploy": "0.4.0", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "isomorphic-git": "^1.27.2", "tar": "^7.5.1" } }, "sha512-/3nd/B/gENy3lq+MA2AR95tBu3Rkkh176XM+A8/mNMGnolPBGw+1JBUGGJ8gvdvAfo64dwRcEs3BI/VD7zFlPQ=="], + + "@intx/inference": ["@intx/inference@0.4.0", "", { "dependencies": { "@intx/log": "0.4.0", "@intx/types": "0.4.0", "arktype": "^2.1.29" } }, "sha512-cuaqCqotmjg0OzGUm8QmFJr1xX5wPVS0LeuhrxHsj0X3ImoCzVB4e/ttTc+DaP3K1tTMQ/8Q8HHCAPX36mOecQ=="], + "@intx/log": ["@intx/log@0.4.0", "", { "dependencies": { "@logtape/hono": "^2.0.2", "@logtape/logtape": "^2.0.2" }, "peerDependencies": { "hono": "^4.0.0" }, "optionalPeers": ["hono"] }, "sha512-f1IPZ/9YpEqlA0I2UZPOlRJOTDoOFEZRwZoUVuw8DpgR1WatOMm4hA0E8o6Mtz+pA0ux7gVdls4y/JZcgypsfQ=="], "@intx/mailbox": ["@intx/mailbox@0.4.0", "", { "dependencies": { "@intx/crypto": "0.4.0", "@intx/mime": "0.4.0", "@intx/types": "0.4.0", "arktype": "^2.1.29" } }, "sha512-u+It1h0NJqodX3wj52H5Ldsn7upkfo3mHWW9fyOzTBaga6vo5ozWVm0zS3b68eb11RaeaA7WlBefxK4aA5eyEg=="], "@intx/mime": ["@intx/mime@0.4.0", "", { "dependencies": { "@intx/crypto": "0.4.0", "@intx/types": "0.4.0", "arktype": "^2.1.29" } }, "sha512-sWrohF2uwVL5AZHbxIEUNiVUBhULff4XB7g6keCmyQWr296GKSvG/HmbihSXxiIvmmDzh9cl+6ZAJz/taxFNog=="], + "@intx/pack-transport": ["@intx/pack-transport@0.4.0", "", { "dependencies": { "@intx/types": "0.4.0" } }, "sha512-I+axd067ZFXaTRtr2zmBXl6/yU253wLc/YWEdXiPiqz06mIspR2BFJMbSXPzV4pM26n+1tv/9E5VNBQtdiYmSA=="], + + "@intx/storage-isogit": ["@intx/storage-isogit@0.4.0", "", { "dependencies": { "@intx/log": "0.4.0", "@intx/mime": "0.4.0", "@intx/types": "0.4.0", "@isomorphic-git/lightning-fs": "4.7.0", "arktype": "^2.1.29", "buffer": "^6.0.3", "isomorphic-git": "^1.27.2" } }, "sha512-3H4dhUsWmAixLEBp79vq9NVUf6Z4++pKzaODWFEgNfhLX2WeV5J/t6soUamL6T/8eok/SDgQ/sZiZMiJ5kDKOw=="], + + "@intx/tool-packaging": ["@intx/tool-packaging@0.4.0", "", { "dependencies": { "@intx/agent": "0.4.0", "@intx/log": "0.4.0", "@intx/storage-isogit": "0.4.0", "@intx/types": "0.4.0", "arktype": "^2.1.29", "isomorphic-git": "^1.27.2", "npm-package-arg": "^12.0.2", "npm-pick-manifest": "^10.0.0", "npm-registry-fetch": "^19.0.0", "semver": "^7.7.2", "ssri": "^12.0.0", "tar": "^7.5.1" } }, "sha512-eBZIbGUcEBp0ppMCceA1PPd75LvXpUyWitqTIu7AsLxRNlU9gbE1hymWPXnhUKnt3LgSlHUi959Ehd6jG6mR7g=="], + "@intx/types": ["@intx/types@0.4.0", "", { "dependencies": { "arktype": "^2.1.29", "semver": "^7.7.2" } }, "sha512-nJ2fcmskMOZNMRfbHs5ueKpCANHNttoL+sZgIoFaVUF/QLpFWfVp0YV72CQAaa9bVv9j5kRSwtSOCCGSlwQ6Dw=="], + "@intx/workflow": ["@intx/workflow@0.4.0", "", { "dependencies": { "@intx/agent": "0.4.0", "@intx/inference": "0.4.0", "@intx/types": "0.4.0", "arktype": "^2.1.29" } }, "sha512-F5Is/lyKb4/WguAVYMDXwtI4ddUvqXbg34rcM6hPUOqupJHpbbD9+ZK/6TDfvbCiEgginbTVwMmNj6Fyg8vU+Q=="], + + "@intx/workflow-deploy": ["@intx/workflow-deploy@0.4.0", "", { "dependencies": { "@intx/agent": "0.4.0", "@intx/tool-packaging": "0.4.0", "@intx/types": "0.4.0", "@intx/workflow": "0.4.0" } }, "sha512-TSLg9lPZvJ+leg2n0I/SWxZjcNQtzsH2oKLD04ptEsBZcbIuMINsQeZBmYV5Brdovy0J/SrWZyrOJ8lc+k/Fdw=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@isomorphic-git/idb-keyval": ["@isomorphic-git/idb-keyval@3.3.2", "", {}, "sha512-r8/AdpiS0/WJCNR/t/gsgL+M8NMVj/ek7s60uz3LmpCaTF2mEVlZJlB01ZzalgYzRLXwSPC92o+pdzjM7PN/pA=="], + + "@isomorphic-git/lightning-fs": ["@isomorphic-git/lightning-fs@4.7.0", "", { "dependencies": { "@isomorphic-git/idb-keyval": "3.3.2", "isomorphic-textencoder": "1.0.1", "just-debounce-it": "1.1.0", "just-once": "1.1.0" }, "bin": { "superblocktxt": "src/superblocktxt.js" } }, "sha512-eJg541itXKCOyj3DBAnd0KNY1mNgi29GCpy7FgKWsgatu/ulEKv+dMxxjhUNL68Jh3uPTaGNfeAjdJiHUYEGnw=="], + "@logtape/hono": ["@logtape/hono@2.3.6", "", { "peerDependencies": { "@logtape/logtape": "^2.3.6", "hono": "^4.0.0" } }, "sha512-AHVyHBP7i/qTZMZLr2sFi91wz+Dpg6wIZuoFbU7IjkeYF/myOtOLF3GrrbBUd82inmtjSNJEttegwAiUmX2+QQ=="], "@logtape/logtape": ["@logtape/logtape@2.3.6", "", {}, "sha512-1ajI6YRkq+gIpgYVhOcJ4Mn5QUarSvWnnuj8SNMjwrjIcOy1ANEIcZyaLN+vPtL2MCa845D8ghXR8xDA+c3Khg=="], + "@noble/ciphers": ["@noble/ciphers@2.4.0", "", {}, "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw=="], + + "@noble/hashes": ["@noble/hashes@2.4.0", "", {}, "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="], + + "@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=="], + + "@npmcli/fs": ["@npmcli/fs@5.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og=="], + + "@npmcli/redact": ["@npmcli/redact@4.0.0", "", {}, "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], + "@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="], "@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="], @@ -75,32 +139,288 @@ "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], "arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="], + "async-lock": ["async-lock@1.4.1", "", {}, "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "better-auth": ["better-auth@1.7.6", "", { "dependencies": { "@better-auth/core": "1.7.6", "@better-auth/drizzle-adapter": "1.7.6", "@better-auth/kysely-adapter": "1.7.6", "@better-auth/memory-adapter": "1.7.6", "@better-auth/mongo-adapter": "1.7.6", "@better-auth/prisma-adapter": "1.7.6", "@better-auth/telemetry": "1.7.6", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.2", "@noble/ciphers": "^2.2.0", "@noble/hashes": "^2.2.0", "better-call": "1.4.0", "defu": "^6.1.4", "jose": "^6.2.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.3.0", "zod": "^4.5.4" }, "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", "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0", "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 || ^5.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-WTMqOpmTTj+oBoX7zzPOzL85342Upyf+/v0IkH1kvGRA85lV4JGRFIYMIRKqvjZ6ShsuL5VX/c9C13jtoZXcKA=="], + + "better-call": ["better-call@1.4.0", "", { "dependencies": { "@better-auth/utils": "^0.5.0", "@better-fetch/fetch": "^1.3.1", "rou3": "^0.9.1", "set-cookie-parser": "^3.1.2" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA=="], + + "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "bun-types": ["bun-types@1.1.37", "", { "dependencies": { "@types/node": "~20.12.8", "@types/ws": "~8.5.10" } }, "sha512-C65lv6eBr3LPJWFZ2gswyrGZ82ljnH8flVE03xeXxKhi2ZGtFiO4isRKTKnitbSqtRAcaqYSR6djt1whI66AbA=="], + "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="], + + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "clean-git-ref": ["clean-git-ref@2.0.1", "", {}, "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw=="], + + "content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + + "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=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + + "diff3": ["diff3@0.0.3", "", {}, "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="], + "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "fast-text-encoding": ["fast-text-encoding@1.0.6", "", {}, "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + "hono": ["hono@4.12.32", "", {}, "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg=="], "hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], + "hosted-git-info": ["hosted-git-info@8.1.0", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw=="], + + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ip-address": ["ip-address@10.7.2", "", {}, "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isomorphic-git": ["isomorphic-git@1.42.2", "", { "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-Rb7czWhLO4JYqmg4an01nNomuAgFbWWK2CVfkHGBOvK5t7hrGTKOXPx/csscJ1j9o3uXFzNcC6njgvCafSmIPw=="], + + "isomorphic-textencoder": ["isomorphic-textencoder@1.0.1", "", { "dependencies": { "fast-text-encoding": "^1.0.0" } }, "sha512-676hESgHullDdHDsj469hr+7t3i/neBKU9J7q1T4RHaWwLAsaQnywC0D1dIUId0YZ+JtVrShzuBk1soo0+GVcQ=="], + + "jose": ["jose@6.2.12", "", {}, "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw=="], + + "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], + + "just-debounce-it": ["just-debounce-it@1.1.0", "", {}, "sha512-87Nnc0qZKgBZuhFZjYVjSraic0x7zwjhaTMrCKlj0QYKH6lh0KbFzVnfu6LHan03NO7J8ygjeBeD0epejn5Zcg=="], + + "just-once": ["just-once@1.1.0", "", {}, "sha512-+rZVpl+6VyTilK7vB/svlMPil4pxqIJZkbnN7DKZTOzyXfun6ZiFeq2Pk4EtCEHZ0VU4EkdFzG8ZK5F3PErcDw=="], + + "kysely": ["kysely@0.29.6", "", {}, "sha512-hHaB8C/rfzDDtr/t8YZwxAuPJTT0zHyaPoVzcXwDYhYNAgH/4sIfVhi/XLLIY+bL/FqaIJnjATDbi8ObSELmxg=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minimisted": ["minimisted@2.0.1", "", { "dependencies": { "minimist": "^1.2.5" } }, "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], + + "minipass-fetch": ["minipass-fetch@5.0.2", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^2.0.0", "minizlib": "^3.0.1" }, "optionalDependencies": { "iconv-lite": "^0.7.2" } }, "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ=="], + + "minipass-flush": ["minipass-flush@1.0.7", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA=="], + + "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], + + "minipass-sized": ["minipass-sized@2.0.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanostores": ["nanostores@1.5.3", "", {}, "sha512-rQLB6eV4f2AW/n3L0JmwCROpaisYy9EDEADvEFSd1C/qG8hB6O5TPlh9A791JRbJr4CnMQBzptDcvD9OR1+6WA=="], + + "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + + "npm-install-checks": ["npm-install-checks@7.1.2", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ=="], + + "npm-normalize-package-bin": ["npm-normalize-package-bin@4.0.0", "", {}, "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w=="], + + "npm-package-arg": ["npm-package-arg@12.0.2", "", { "dependencies": { "hosted-git-info": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^6.0.0" } }, "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA=="], + + "npm-pick-manifest": ["npm-pick-manifest@10.0.0", "", { "dependencies": { "npm-install-checks": "^7.1.0", "npm-normalize-package-bin": "^4.0.0", "npm-package-arg": "^12.0.0", "semver": "^7.3.5" } }, "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ=="], + + "npm-registry-fetch": ["npm-registry-fetch@19.1.1", "", { "dependencies": { "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", "npm-package-arg": "^13.0.0", "proc-log": "^6.0.0" } }, "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "p-map": ["p-map@7.0.8", "", {}, "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw=="], + + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + "postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="], + "proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], + + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "rou3": ["rou3@0.9.2", "", {}, "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "sha.js": ["sha.js@2.4.12", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" } }, "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w=="], + + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "socks": ["socks@2.8.10", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ=="], + + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + + "ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], + + "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + "typescript": ["typescript@5.7.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "validate-npm-package-name": ["validate-npm-package-name@6.0.2", "", {}, "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ=="], + + "which-typed-array": ["which-typed-array@1.1.24", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="], + + "@npmcli/agent/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], + + "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], + "bun-types/@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="], + "cacache/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], + + "cacache/ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], + + "make-fetch-happen/proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], + + "make-fetch-happen/ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="], + + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "npm-registry-fetch/npm-package-arg": ["npm-package-arg@13.0.2", "", { "dependencies": { "hosted-git-info": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^7.0.0" } }, "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA=="], + + "npm-registry-fetch/proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], + + "path-scurry/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], + "bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "npm-registry-fetch/npm-package-arg/hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], + + "npm-registry-fetch/npm-package-arg/validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], + + "npm-registry-fetch/npm-package-arg/hosted-git-info/lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], } } diff --git a/package.json b/package.json index be898d4..0d09aff 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "hono-openapi": "1.3.1" }, "peerDependencies": { + "@intx/hub-api": "^0.4.0", "@intx/log": "^0.4.0", "@intx/mailbox": "^0.4.0", "@intx/mime": "^0.4.0", @@ -70,6 +71,7 @@ "postgres": "^3.4.0" }, "devDependencies": { + "@intx/hub-api": "0.4.0", "@intx/log": "0.4.0", "@intx/mailbox": "0.4.0", "@intx/mime": "0.4.0", diff --git a/src/index.ts b/src/index.ts index 8c7fc67..836ff49 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,9 @@ // human principals. This library exists ONLY to give a human principal a // native `@intx/mailbox` `MailboxStore` over Postgres, and the routes that // let a host's UI list, read, and file it — nothing else. -export { mountMailbox, MAX_MAILBOX_PAGE_LIMIT } from "./mount.js"; +export { createMailboxRoutes, MAX_MAILBOX_PAGE_LIMIT } from "./mount.js"; export type { - MountMailboxOpts, + CreateMailboxRoutesDeps, ResolvedPrincipal, OutgoingMailboxMessage, } from "./mount.js"; diff --git a/src/mount-native.test.ts b/src/mount-native.test.ts index f6d058a..c2b4fac 100644 --- a/src/mount-native.test.ts +++ b/src/mount-native.test.ts @@ -1,11 +1,10 @@ // The thin route layer over the native store: list (search + keyset), // read/unread flags, and archive/trash/restore moves. import { beforeEach, describe, expect, test } from "bun:test"; -import { Hono } from "hono"; -import { mountMailbox } from "./mount.js"; +import { createMailboxRoutes } from "./mount.js"; import { createInMemoryMailboxEventBus } from "./bus.js"; import { writeMailboxMessage } from "./write.js"; -import { withTestDb, seedScope } from "./test-helpers.js"; +import { allowAllGrants, mountAs, withTestDb, seedScope } from "./test-helpers.js"; import type { MailboxDb } from "./db.js"; let db: MailboxDb; @@ -17,14 +16,16 @@ beforeEach(async () => { }); function buildApp() { - const app = new Hono(); - mountMailbox(app, { - db, - bus: createInMemoryMailboxEventBus(), - resolvePrincipal: () => SCOPE, - senderAddressFor: () => "p1@t1.example", - deliver: () => {}, - }); + const app = mountAs( + SCOPE, + createMailboxRoutes({ + db, + requireGrant: allowAllGrants, + bus: createInMemoryMailboxEventBus(), + senderAddressFor: () => "p1@t1.example", + deliver: () => {}, + }), + ); return app; } diff --git a/src/mount-send.test.ts b/src/mount-send.test.ts index 07b2a2c..aac1dd6 100644 --- a/src/mount-send.test.ts +++ b/src/mount-send.test.ts @@ -2,11 +2,15 @@ // caller's Sent folder, and hands it to the host's `deliver` — this package // owns no transport of its own. import { beforeEach, describe, expect, test } from "bun:test"; -import { Hono } from "hono"; -import { mountMailbox, type OutgoingMailboxMessage } from "./mount.js"; +import { createMailboxRoutes, type OutgoingMailboxMessage } from "./mount.js"; import { createInMemoryMailboxEventBus } from "./bus.js"; import { openNativeMailboxStore } from "./native-store.js"; -import { withTestDb, seedScope } from "./test-helpers.js"; +import { + allowAllGrants, + mountAs, + withTestDb, + seedScope, +} from "./test-helpers.js"; import type { MailboxDb } from "./db.js"; let db: MailboxDb; @@ -19,16 +23,18 @@ beforeEach(async () => { }); function buildApp(deliveries: OutgoingMailboxMessage[]) { - const app = new Hono(); - mountMailbox(app, { - db, - bus: createInMemoryMailboxEventBus(), - resolvePrincipal: () => SCOPE, - senderAddressFor: () => FROM, - deliver: (message) => { - deliveries.push(message); - }, - }); + const app = mountAs( + SCOPE, + createMailboxRoutes({ + db, + requireGrant: allowAllGrants, + bus: createInMemoryMailboxEventBus(), + senderAddressFor: () => FROM, + deliver: (message) => { + deliveries.push(message); + }, + }), + ); return app; } @@ -121,14 +127,16 @@ describe("POST /me/inbox/send", () => { }); test("403s with no resolvable principal", async () => { - const app = new Hono(); - mountMailbox(app, { - db, - bus: createInMemoryMailboxEventBus(), - resolvePrincipal: () => null, - senderAddressFor: () => FROM, - deliver: () => {}, - }); + const app = mountAs( + null, + createMailboxRoutes({ + db, + requireGrant: allowAllGrants, + bus: createInMemoryMailboxEventBus(), + senderAddressFor: () => FROM, + deliver: () => {}, + }), + ); const res = await app.request("/me/inbox/send", { method: "POST", diff --git a/src/mount.test.ts b/src/mount.test.ts index bf82c86..8d8220f 100644 --- a/src/mount.test.ts +++ b/src/mount.test.ts @@ -1,8 +1,9 @@ import { beforeEach, describe, expect, test } from "bun:test"; import { Hono } from "hono"; -import { mountMailbox } from "./mount.js"; +import type { RequireGrant, TenantEnv } from "@intx/hub-api"; +import { createMailboxRoutes, type ResolvedPrincipal } from "./mount.js"; import { createInMemoryMailboxEventBus } from "./bus.js"; -import { withTestDb, seedScope } from "./test-helpers.js"; +import { allowAllGrants, mountAs, withTestDb, seedScope } from "./test-helpers.js"; import type { MailboxDb } from "./db.js"; let db: MailboxDb; @@ -13,29 +14,30 @@ beforeEach(async () => { }); function buildApp( - resolvePrincipal: () => { tenantId: string; principalId: string } | null, + scope: ResolvedPrincipal | null, ) { - const app = new Hono(); - mountMailbox(app, { - db, - bus: createInMemoryMailboxEventBus(), - resolvePrincipal, - senderAddressFor: () => "sender@t1.example", - deliver: () => {}, - }); + const app = mountAs( + scope, + createMailboxRoutes({ + db, + requireGrant: allowAllGrants, + bus: createInMemoryMailboxEventBus(), + senderAddressFor: () => "sender@t1.example", + deliver: () => {}, + }), + ); return app; } -describe("no-member asymmetry", () => { - test("list returns empty 200 when resolvePrincipal yields null", async () => { - const app = buildApp(() => null); +describe("no principal", () => { + test("list returns 403 when the context carries no principal", async () => { + const app = buildApp(null); const res = await app.request("/me/inbox"); - expect(res.status).toBe(200); - expect(await res.json()).toEqual({ messages: [] }); + expect(res.status).toBe(403); }); - test("events returns 403 when resolvePrincipal yields null", async () => { - const app = buildApp(() => null); + test("events returns 403 when the context carries no principal", async () => { + const app = buildApp(null); const res = await app.request("/me/inbox/events"); expect(res.status).toBe(403); }); @@ -44,10 +46,107 @@ describe("no-member asymmetry", () => { // table below is what proves each registered route actually reaches it — a // verb wired straight to its handler would slip past a one-verb test. for (const verb of ["read", "unread", "trash", "archive", "restore"]) { - test(`${verb} mutation returns 403 when resolvePrincipal yields null`, async () => { - const app = buildApp(() => null); + test(`${verb} mutation returns 403 when the context carries no principal`, async () => { + const app = buildApp(null); const res = await app.request(`/me/inbox/1/${verb}`, { method: "POST" }); expect(res.status).toBe(403); }); } }); + +describe("grant gating", () => { + function buildGatedApp(checked: string[]) { + const requireGrant: RequireGrant = (resource, action) => async (c) => { + checked.push(`${String(resource)} ${action}`); + return c.json({ error: "forbidden" }, 403); + }; + return mountAs( + { tenantId: "t1", principalId: "p1" }, + createMailboxRoutes({ + db, + requireGrant, + bus: createInMemoryMailboxEventBus(), + senderAddressFor: () => "sender@t1.example", + deliver: () => {}, + }), + ); + } + + test("send requires mailbox:* create", async () => { + const checked: string[] = []; + const res = await buildGatedApp(checked).request("/me/inbox/send", { + method: "POST", + }); + expect(res.status).toBe(403); + expect(checked).toEqual(["mailbox:* create"]); + }); + + for (const verb of ["read", "unread", "trash", "archive", "restore"]) { + test(`${verb} requires mailbox:* manage`, async () => { + const checked: string[] = []; + const res = await buildGatedApp(checked).request(`/me/inbox/1/${verb}`, { + method: "POST", + }); + expect(res.status).toBe(403); + expect(checked).toEqual(["mailbox:* manage"]); + }); + } + + for (const path of ["/me/inbox", "/me/inbox/threads", "/me/inbox/threads/1", "/me/inbox/events"]) { + test(`${path} requires mailbox:* read`, async () => { + const checked: string[] = []; + const res = await buildGatedApp(checked).request(path); + expect(res.status).toBe(403); + expect(checked).toEqual(["mailbox:* read"]); + }); + } +}); + +describe("principal from the tenant middleware", () => { + test("reads the tenant and principal the host middleware set", async () => { + const now = new Date(); + const host = new Hono(); + host.use(async (c, next) => { + c.set("tenant", { + id: "t1", + name: "t1", + slug: "t1", + domain: "t1.example", + parentId: null, + config: null, + createdAt: now, + updatedAt: now, + }); + c.set("principal", { + id: "p1", + tenantId: "t1", + kind: "user", + refId: "p1", + status: "active", + createdAt: now, + updatedAt: now, + }); + await next(); + }); + host.route( + "/mailbox", + createMailboxRoutes({ + db, + requireGrant: allowAllGrants, + bus: createInMemoryMailboxEventBus(), + senderAddressFor: () => "p1@t1.example", + deliver: () => {}, + }), + ); + + const send = await host.request("/mailbox/me/inbox/send", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ to: ["x@t1.example"], body: "hi" }), + }); + expect(send.status).toBe(200); + const list = await host.request("/mailbox/me/inbox?folder=Sent"); + const { messages } = (await list.json()) as { messages: unknown[] }; + expect(messages).toHaveLength(1); + }); +}); diff --git a/src/mount.ts b/src/mount.ts index dba5078..011b5d8 100644 --- a/src/mount.ts +++ b/src/mount.ts @@ -1,7 +1,8 @@ -import type { Context, Env, Hono } from "hono"; +import { Hono, type Context } from "hono"; import { streamSSE } from "hono/streaming"; import { describeRoute } from "hono-openapi"; import { type } from "arktype"; +import type { RequireGrant, TenantEnv } from "@intx/hub-api"; import { getLogger } from "@intx/log"; import { executeSearch, executeThread } from "@intx/mailbox"; import type { Thread } from "@intx/types/runtime"; @@ -28,12 +29,11 @@ export type OutgoingMailboxMessage = { messageId: string; }; -export type MountMailboxOpts = { +export type CreateMailboxRoutesDeps = { db: MailboxDb; bus: MailboxEventBus; - resolvePrincipal: ( - ctx: unknown, - ) => Promise | ResolvedPrincipal | null; + /** `mailbox:*` `read` for reads, `create` for send, `manage` for flag/move. */ + requireGrant: RequireGrant; /** * SSE keep-alive period. Defaults to 25s — under the 30s idle timeout most * proxies default to. @@ -114,12 +114,16 @@ function parseUid(raw: string): number | null { } const TAGS = ["mailbox"]; -const ID_PARAM = { - name: "uid", - in: "path" as const, - required: true, - schema: { type: "integer" as const }, -}; +function uidParam(name: string) { + return { + name, + in: "path" as const, + required: true, + schema: { type: "integer" as const }, + }; +} + +const ID_PARAM = uidParam("uid"); /** * One item of `GET /me/inbox`: `@intx/mailbox`'s `executeSearch`'s ref, plus the @@ -157,17 +161,20 @@ type MailboxThreadNode = { function enrichThread(store: NativeMailboxStore, node: Thread): MailboxThreadNode { const message = store.find(node.ref.uid); + if (!message) { + throw new Error(`thread node uid ${node.ref.uid} is not in the store`); + } return { uid: node.ref.uid, - flags: message ? [...message.flags] : [], + flags: [...message.flags], envelope: { - messageId: message?.envelope.messageId ?? "", - from: message?.envelope.from ?? "", - to: message?.envelope.to ?? [], - subject: message?.envelope.subject ?? "", - date: new Date(message?.envelope.date ?? 0).toISOString(), - inReplyTo: message?.envelope.inReplyTo, - references: message?.envelope.references ?? [], + messageId: message.envelope.messageId, + from: message.envelope.from, + to: message.envelope.to, + subject: message.envelope.subject, + date: new Date(message.envelope.date).toISOString(), + inReplyTo: message.envelope.inReplyTo, + references: message.envelope.references, }, children: node.children.map((child) => enrichThread(store, child)), }; @@ -185,32 +192,47 @@ const MOVE_VERBS = [ ] as const; /** - * Mount the mailbox routes onto a host Hono app under `/me/inbox*`. + * Whose mailbox this request reads: the `tenant` and `principal` the host's + * tenant middleware set, the same principal `requireGrant` authorizes. + */ +function resolvePrincipal(c: Context): ResolvedPrincipal | null { + const tenantId = c.get("tenant")?.id; + const principalId = c.get("principal")?.id; + return tenantId && principalId ? { tenantId, principalId } : null; +} + +function inFolder( + scope: ResolvedPrincipal, + folder: string, +): ResolvedPrincipal & { folder: string } { + return { tenantId: scope.tenantId, principalId: scope.principalId, folder }; +} + +/** + * The mailbox routes under `/me/inbox*`, as a sub-app the host mounts with + * `app.route`. * * This library exists ONLY to give human principals a native Interchange * mailbox — list, read/unread, archive/trash/restore, and a live SSE stream. * Every route is a thin wrapper over `NativeMailboxStore` and * `@intx/mailbox`'s `executeSearch`. * - * "No-member asymmetry" is intentional, spec'd behavior: when - * `resolvePrincipal` yields no principal, list returns an EMPTY result (200) - * — a caller with no mailbox identity simply sees an empty inbox — while - * events and mutations return 403, since those operate on (or stream) a - * specific identity that does not exist. + * Every route returns 403 when the context carries no principal. */ -export function mountMailbox( - app: Hono, - opts: MountMailboxOpts, -): Hono { - const { db, bus, resolvePrincipal, senderAddressFor, deliver } = opts; +export function createMailboxRoutes( + deps: CreateMailboxRoutesDeps, +): Hono { + const { db, bus, requireGrant, senderAddressFor, deliver } = deps; const heartbeatIntervalMs = - opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS; + deps.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS; if (!Number.isFinite(heartbeatIntervalMs) || heartbeatIntervalMs <= 0) { throw new RangeError( "mailbox heartbeatIntervalMs must be a finite positive number", ); } + const app = new Hono(); + function publish( scope: ResolvedPrincipal, id: string, @@ -221,13 +243,13 @@ export function mountMailbox( app.get( "/me/inbox", + requireGrant("mailbox:*", "read"), describeRoute({ tags: TAGS, summary: "List the caller's inbox", description: "Newest first, keyset-paginated over the native mailbox store's uid " + - "counter. With no resolvable principalId this returns an empty list, " + - "not a 403.", + "counter.", parameters: [ { name: "folder", @@ -263,10 +285,10 @@ export function mountMailbox( const parsedCursor = parseCursor(c.req.query("cursor")); if ("error" in parsedCursor) return c.json({ error: parsedCursor.error }, 400); - const resolved = await resolvePrincipal(c); - if (!resolved) return c.json({ messages: [] }); + const resolved = resolvePrincipal(c); + if (!resolved) return c.json({ error: "No resolvable principalId" }, 403); - const store = await openNativeMailboxStore(db, { ...resolved, folder }); + const store = await openNativeMailboxStore(db, inFolder(resolved, folder)); // No query predicate: `executeSearch` returns every ref, in store order // (uid ascending, since `append` only ever grows uid). Reversed for // newest-first, then paged with a plain uid keyset. @@ -307,14 +329,14 @@ export function mountMailbox( app.get( "/me/inbox/threads", + requireGrant("mailbox:*", "read"), describeRoute({ tags: TAGS, summary: "The caller's inbox as threads", description: "`@intx/mailbox`'s `executeThread` (REFERENCES algorithm) run over the " + "folder's native store — roots plus children, each ref carrying the " + - "same envelope fields `GET /me/inbox` returns. With no resolvable " + - "principalId this returns an empty list, not a 403.", + "same envelope fields `GET /me/inbox` returns.", parameters: [ { name: "folder", @@ -334,10 +356,10 @@ export function mountMailbox( if (!isListFolder(folder)) { return c.json({ error: "invalid folder" }, 400); } - const resolved = await resolvePrincipal(c); - if (!resolved) return c.json({ threads: [] }); + const resolved = resolvePrincipal(c); + if (!resolved) return c.json({ error: "No resolvable principalId" }, 403); - const store = await openNativeMailboxStore(db, { ...resolved, folder }); + const store = await openNativeMailboxStore(db, inFolder(resolved, folder)); const threads = await executeThread(folder, store, "references"); return c.json({ threads: threads.map((thread) => enrichThread(store, thread)), @@ -347,11 +369,12 @@ export function mountMailbox( app.get( "/me/inbox/threads/:rootUid", + requireGrant("mailbox:*", "read"), describeRoute({ tags: TAGS, summary: "One thread, rooted at the given uid", parameters: [ - { ...ID_PARAM, name: "rootUid" }, + uidParam("rootUid"), { name: "folder", in: "query", @@ -376,10 +399,10 @@ export function mountMailbox( if (!isListFolder(folder)) { return c.json({ error: "invalid folder" }, 400); } - const resolved = await resolvePrincipal(c); + const resolved = resolvePrincipal(c); if (!resolved) return c.json({ error: "No resolvable principalId" }, 403); - const store = await openNativeMailboxStore(db, { ...resolved, folder }); + const store = await openNativeMailboxStore(db, inFolder(resolved, folder)); const threads = await executeThread(folder, store, "references"); const root = threads.find((thread) => thread.ref.uid === rootUid); if (!root) return c.json({ error: "Thread not found" }, 404); @@ -389,6 +412,7 @@ export function mountMailbox( app.post( "/me/inbox/send", + requireGrant("mailbox:*", "create"), describeRoute({ tags: TAGS, summary: "Send a message from the caller's mailbox", @@ -402,9 +426,8 @@ export function mountMailbox( 403: { description: "No resolvable principalId" }, }, }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async (c: Context) => { - const resolved = await resolvePrincipal(c); + async (c) => { + const resolved = resolvePrincipal(c); if (!resolved) return c.json({ error: "No resolvable principalId" }, 403); let json: unknown; @@ -438,10 +461,7 @@ export function mountMailbox( // back. let parentReferences: string[] = []; for (const folder of LIST_FOLDERS) { - const folderStore = await openNativeMailboxStore(db, { - ...resolved, - folder, - }); + const folderStore = await openNativeMailboxStore(db, inFolder(resolved, folder)); const parent = folderStore.messages.find( (m) => m.envelope.messageId === inReplyTo, ); @@ -464,10 +484,7 @@ export function mountMailbox( if (references !== undefined) frameArgs.references = references; const raw = buildMailFrame(frameArgs); - const sentStore = await openNativeMailboxStore(db, { - ...resolved, - folder: "Sent", - }); + const sentStore = await openNativeMailboxStore(db, inFolder(resolved, "Sent")); const uid = sentStore.append( raw, { @@ -494,6 +511,7 @@ export function mountMailbox( app.get( "/me/inbox/events", + requireGrant("mailbox:*", "read"), describeRoute({ tags: TAGS, summary: "Server-sent stream of mailbox events for the caller", @@ -508,7 +526,7 @@ export function mountMailbox( }, }), async (c) => { - const resolved = await resolvePrincipal(c); + const resolved = resolvePrincipal(c); if (!resolved) { return c.json({ error: "No resolvable principalId" }, 403); } @@ -570,6 +588,7 @@ export function mountMailbox( for (const { verb, op, flags, add } of READ_VERBS) { app.post( `/me/inbox/:uid/${verb}`, + requireGrant("mailbox:*", "manage"), describeRoute({ tags: TAGS, summary: verb === "read" ? "Mark a message read" : "Mark a message unread", @@ -581,14 +600,13 @@ export function mountMailbox( 404: { description: "No message with that uid in this mailbox" }, }, }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async (c: Context) => { + async (c) => { const uid = parseUid(c.req.param("uid") ?? ""); if (uid === null) return c.json({ error: "uid must be a positive integer" }, 400); - const resolved = await resolvePrincipal(c); + const resolved = resolvePrincipal(c); if (!resolved) return c.json({ error: "No resolvable principalId" }, 403); const folder = c.req.query("folder") ?? DEFAULT_FOLDER; - const store = await openNativeMailboxStore(db, { ...resolved, folder }); + const store = await openNativeMailboxStore(db, inFolder(resolved, folder)); if (!store.find(uid)) return c.json({ error: "Message not found" }, 404); if (add) store.addFlags(uid, [...flags]); else store.removeFlags(uid, [...flags]); @@ -602,6 +620,7 @@ export function mountMailbox( for (const { verb, op, from, to } of MOVE_VERBS) { app.post( `/me/inbox/:uid/${verb}`, + requireGrant("mailbox:*", "manage"), describeRoute({ tags: TAGS, summary: `Move a message to ${to}`, @@ -613,11 +632,10 @@ export function mountMailbox( 404: { description: "No message with that uid in the source folder" }, }, }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async (c: Context) => { + async (c) => { const uid = parseUid(c.req.param("uid") ?? ""); if (uid === null) return c.json({ error: "uid must be a positive integer" }, 400); - const resolved = await resolvePrincipal(c); + const resolved = resolvePrincipal(c); if (!resolved) return c.json({ error: "No resolvable principalId" }, 403); // `restore` has no fixed source: a message can be restored out of // either Archive or Trash, named by `?folder=`. diff --git a/src/sse-heartbeat.test.ts b/src/sse-heartbeat.test.ts index 4f1f114..ec4d9db 100644 --- a/src/sse-heartbeat.test.ts +++ b/src/sse-heartbeat.test.ts @@ -4,25 +4,27 @@ // because an idle stream and a broken keep-alive look identical until a proxy // drops the connection in production. import { describe, expect, test } from "bun:test"; -import { Hono } from "hono"; -import { mountMailbox } from "./mount.js"; +import { createMailboxRoutes } from "./mount.js"; import { createInMemoryMailboxEventBus } from "./bus.js"; import { writeMailboxMessage } from "./write.js"; -import { withTestDb, seedScope } from "./test-helpers.js"; +import { allowAllGrants, mountAs, withTestDb, seedScope } from "./test-helpers.js"; import type { MailboxDb } from "./db.js"; const SCOPE = { tenantId: "t1", principalId: "p1" }; function stream(db: MailboxDb, heartbeatIntervalMs: number) { const bus = createInMemoryMailboxEventBus(); - const app = mountMailbox(new Hono(), { - db, - bus, - resolvePrincipal: () => SCOPE, - senderAddressFor: () => "sender@t1.example", - deliver: () => {}, - heartbeatIntervalMs, - }); + const app = mountAs( + SCOPE, + createMailboxRoutes({ + db, + requireGrant: allowAllGrants, + bus, + senderAddressFor: () => "sender@t1.example", + deliver: () => {}, + heartbeatIntervalMs, + }), + ); return { app, bus }; } @@ -120,13 +122,16 @@ describe("SSE heartbeat", () => { test("the default interval is the documented 25s, not the test override", async () => { const db = await withTestDb(); const bus = createInMemoryMailboxEventBus(); - const app = mountMailbox(new Hono(), { - db, - bus, - resolvePrincipal: () => SCOPE, - senderAddressFor: () => "sender@t1.example", - deliver: () => {}, - }); + const app = mountAs( + SCOPE, + createMailboxRoutes({ + db, + requireGrant: allowAllGrants, + bus, + senderAddressFor: () => "sender@t1.example", + deliver: () => {}, + }), + ); const res = await app.request("/me/inbox/events"); // With no override, nothing may arrive within a second — otherwise the @@ -144,16 +149,20 @@ describe("SSE heartbeat", () => { // NaN/Infinity are the same class of host misconfiguration. Fail at mount, // not on the first request, same as a bad vocabulary. const db = await withTestDb(); - const base = { - db, - bus: createInMemoryMailboxEventBus(), - resolvePrincipal: () => SCOPE, - senderAddressFor: () => "sender@t1.example", - deliver: () => {}, - }; + const bus = createInMemoryMailboxEventBus(); for (const heartbeatIntervalMs of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) { expect(() => - mountMailbox(new Hono(), { ...base, heartbeatIntervalMs }), + mountAs( + SCOPE, + createMailboxRoutes({ + db, + requireGrant: allowAllGrants, + bus, + senderAddressFor: () => "sender@t1.example", + deliver: () => {}, + heartbeatIntervalMs, + }), + ), ).toThrow(RangeError); } }); diff --git a/src/sse-stream.test.ts b/src/sse-stream.test.ts index f56fc0e..92c23e4 100644 --- a/src/sse-stream.test.ts +++ b/src/sse-stream.test.ts @@ -1,13 +1,17 @@ import { describe, test, expect, spyOn } from "bun:test"; -import { Hono } from "hono"; import { SSEStreamingApi } from "hono/streaming"; -import { mountMailbox, MAX_PENDING_SSE_EVENTS } from "./mount.js"; +import { createMailboxRoutes, MAX_PENDING_SSE_EVENTS } from "./mount.js"; import { createInMemoryMailboxEventBus, type MailboxEventBus, type MailboxEventScope, } from "./bus.js"; -import { withTestDb, seedScope } from "./test-helpers.js"; +import { + allowAllGrants, + mountAs, + withTestDb, + seedScope, +} from "./test-helpers.js"; import { writeMailboxMessage } from "./write.js"; describe("SSE stream", () => { @@ -15,13 +19,16 @@ describe("SSE stream", () => { const db = await withTestDb(); await seedScope(db, "t1", "p1"); const bus = createInMemoryMailboxEventBus(); - const app = mountMailbox(new Hono(), { - db, - bus, - resolvePrincipal: () => ({ tenantId: "t1", principalId: "p1" }), - senderAddressFor: () => "sender@t1.example", - deliver: () => {}, - }); + const app = mountAs( + { tenantId: "t1", principalId: "p1" }, + createMailboxRoutes({ + db, + requireGrant: allowAllGrants, + bus, + senderAddressFor: () => "sender@t1.example", + deliver: () => {}, + }), + ); const res = await app.request("/me/inbox/events"); expect(res.status).toBe(200); expect(res.headers.get("content-type")).toContain("text/event-stream"); @@ -68,13 +75,16 @@ describe("SSE stream", () => { await seedScope(db, "tenantA", "alice"); await seedScope(db, "tenantB", "alice"); const bus = createInMemoryMailboxEventBus(); - const app = mountMailbox(new Hono(), { - db, - bus, - resolvePrincipal: () => ({ tenantId: "tenantA", principalId: "alice" }), - senderAddressFor: () => "sender@tenantA.example", - deliver: () => {}, - }); + const app = mountAs( + { tenantId: "tenantA", principalId: "alice" }, + createMailboxRoutes({ + db, + requireGrant: allowAllGrants, + bus, + senderAddressFor: () => "sender@tenantA.example", + deliver: () => {}, + }), + ); const res = await app.request("/me/inbox/events"); expect(res.status).toBe(200); const reader = res.body!.getReader(); @@ -131,15 +141,18 @@ describe("SSE stream", () => { await seedScope(db, "t1", "p1"); const bus = createInMemoryMailboxEventBus(); const scope = { tenantId: "t1", principalId: "p1" }; - const app = mountMailbox(new Hono(), { - db, - bus, - resolvePrincipal: () => scope, - senderAddressFor: () => "sender@t1.example", - deliver: () => {}, - // Short heartbeat so the handler notices the overflow-close promptly. - heartbeatIntervalMs: 50, - }); + const app = mountAs( + scope, + createMailboxRoutes({ + db, + requireGrant: allowAllGrants, + bus, + senderAddressFor: () => "sender@t1.example", + deliver: () => {}, + // Short heartbeat so the handler notices the overflow-close promptly. + heartbeatIntervalMs: 50, + }), + ); const res = await app.request("/me/inbox/events"); expect(res.status).toBe(200); const reader = res.body!.getReader(); @@ -198,15 +211,18 @@ describe("SSE stream", () => { }; }, }; - const app = mountMailbox(new Hono(), { - db, - bus, - resolvePrincipal: () => scope, - senderAddressFor: () => "sender@t1.example", - deliver: () => {}, - // Short heartbeat so the loop notices `closed` and runs finally promptly. - heartbeatIntervalMs: 50, - }); + const app = mountAs( + scope, + createMailboxRoutes({ + db, + requireGrant: allowAllGrants, + bus, + senderAddressFor: () => "sender@t1.example", + deliver: () => {}, + // Short heartbeat so the loop notices `closed` and runs finally promptly. + heartbeatIntervalMs: 50, + }), + ); const writeSSE = spyOn( SSEStreamingApi.prototype, "writeSSE", diff --git a/src/test-helpers.ts b/src/test-helpers.ts index 754e747..8ebafe8 100644 --- a/src/test-helpers.ts +++ b/src/test-helpers.ts @@ -1,6 +1,9 @@ import { createMailboxDb, type MailboxDb } from "./db.js"; import { runMailboxMigrations } from "./migrations.js"; import { sql } from "drizzle-orm"; +import { Hono } from "hono"; +import type { RequireGrant, TenantEnv } from "@intx/hub-api"; +import type { ResolvedPrincipal } from "./mount.js"; export const TEST_DATABASE_URL = process.env.MAILBOX_TEST_DATABASE_URL ?? @@ -75,3 +78,45 @@ export async function withTestDb(): Promise { await db.execute(sql`TRUNCATE TABLE "tenant", "principal" CASCADE`); return db; } + +/** A `requireGrant` that lets every request through, for route tests. */ +export const allowAllGrants: RequireGrant = () => async (_c, next) => { + await next(); +}; + +/** + * `routes` behind a stand-in for the host's tenant middleware that puts + * `scope` on the context, or nothing when `scope` is null. + */ +export function mountAs( + scope: ResolvedPrincipal | null, + routes: Hono, +): Hono { + const host = new Hono(); + if (scope) { + const now = new Date(); + host.use(async (c, next) => { + c.set("tenant", { + id: scope.tenantId, + name: scope.tenantId, + slug: scope.tenantId, + domain: `${scope.tenantId}.example`, + parentId: null, + config: null, + createdAt: now, + updatedAt: now, + }); + c.set("principal", { + id: scope.principalId, + tenantId: scope.tenantId, + kind: "user", + refId: scope.principalId, + status: "active", + createdAt: now, + updatedAt: now, + }); + await next(); + }); + } + return host.route("/", routes); +}