Skip to content

chore(publish): per-package READMEs and MIT license field for all 9 packages#76

Merged
ssilvius merged 3 commits intomainfrom
chore/package-readmes
Apr 11, 2026
Merged

chore(publish): per-package READMEs and MIT license field for all 9 packages#76
ssilvius merged 3 commits intomainfrom
chore/package-readmes

Conversation

@ssilvius
Copy link
Copy Markdown
Contributor

Summary

  • Adds a README.md in every package so the npm package page renders properly instead of "no readme available"
  • Adds `"license": "MIT"` to every `package.json` so the npm license badge links via SPDX lookup. Actual LICENSE text lives in the org `.github` repo, not this repo.

Why

Pre-publish check surfaced that none of the 9 packages had a README or a license field. npm package pages without a README look like abandoned or broken packages; consumers deciding whether to install a 0.1.0 framework dep need to see a purpose statement within three seconds of landing.

The READMEs

Each one follows the same shape:

  • One-paragraph identity statement (what the package is for)
  • Install command
  • Minimal usage example with the main factory or export
  • Subpath export table for packages that have more than one entry point
  • Link back to the monorepo README for the wider architecture
  • License line

Per-package highlights:

  • `@rafters/mail` -- the adapter interface table that shows which vendor ships which adapter
  • `@rafters/mail-imap` -- the command surface grouped by category (auth, mailbox, message, session, extensions) plus runtime pairing (-cloudflare or -server)
  • `@rafters/mail-imap-cloudflare` -- the wrangler.jsonc snippet with DO migrations, D1 binding, R2 binding
  • `@rafters/mail-imap-server` -- both TLS modes (self-terminated and proxy) plus the config option table
  • `@rafters/mail-resend` -- transactional + mock + webhook handler across three subpaths
  • `@rafters/mail-cloudflare` -- R2 storage adapter + RFC 5322 parsing + inbound Email Routing handler example
  • `@rafters/mail-react-email` -- renderer + BaseEmail + OtpEmail usage
  • `@rafters/mail-workers-ai` -- classifier categories and Workers AI binding
  • `@rafters/better-auth-resend` -- the single `resendOTP` integration line and the 3 required config options

License field

`"license": "MIT"` added to every `package.json` after the `description` field. npm uses this for the license badge on the package page and for SPDX metadata. No LICENSE file in this repo -- the actual text is at the org level (`rafters-studio/.github`), which auto-links to all repos in the org.

Test plan

  • `pnpm publish --dry-run` on all 9 packages: README.md appears in tarball contents, file count incremented by 1 per package
  • `"license": "MIT"` present in all 9 package.json files
  • No LICENSE files added to this repo (respects org-level setup)
  • Format check clean on all READMEs
  • `legion-simplify` gate: clean

Stack

This PR chains off `chore/imap-build-pipeline` (#73) so all 9 packages have the tsup-updated `package.json` files as the base state. When #73 merges, this branch rebases cleanly onto main.

ssilvius added a commit that referenced this pull request Apr 11, 2026
The JSDoc above `AuthAdapter` said "App passwords are stored hashed
(argon2) in D1 and verified here." That's wrong on two axes:

1. The IMAP server does not own credential storage or hashing. That
   was the original design but it was rewritten: AuthAdapter is an
   interface contract only, the consumer brings their own auth system.
   Prescribing argon2 contradicts the rewritten contract.

2. Core docs must be generic: "blob storage" not "R2", "database" not
   "D1". Referencing D1 by name inside the core protocol package
   violates the zero-vendor-dependency principle.

Replaced with a proper JSDoc block that:
- States the interface-only posture explicitly
- Names the consumer's ownership (storage, hashing, generation,
  revocation)
- Lists the security guarantees the IMAP server DOES enforce
  regardless of adapter implementation (generic failure response,
  rate limit, disconnect after max attempts)
- Refers to the adapter lifecycle in neutral language

This matches the authoritative `authentication.md` doc shipped in PR
#76 and the interface-only posture in the `@rafters/mail-imap-server`
quickstart.md fixed in PR #78.

Verification:
- pnpm --filter @rafters/mail-imap test: 313 passing
- pnpm --filter @rafters/mail-imap typecheck: clean
- pnpm --filter @rafters/mail-imap build: ESM + dts clean
- pnpm lint: 0 warnings, 0 errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ssilvius added a commit that referenced this pull request Apr 11, 2026
After shipping the docs in PR #76 I spot-read them and found real
interface drift -- types, method signatures, and import paths that
did not match the current source. These would have broken every
consumer that copy-pasted the examples. The @vault-2026 technical
accuracy review covered the schema sections of core-reference.md
but did not cover the adapter package docs, which is where these
errors sit.

## packages/resend/docs/outbound.md

- EmailProvider.sendEmail: type names wrong. Doc said
  `sendEmail(params: SendEmailParams): Promise<SendEmailResult>`.
  Actual is `sendEmail(params: EmailParams): Promise<{ id: string }>`.
- EmailParams field table wrong: `from` is optional not required,
  `to` is a single string not array, `cc`/`bcc`/`headers` do not
  exist in EmailParams.
- Documented the full EmailProvider surface (mailing lists,
  subscribers, campaigns, audiences), which was omitted.
- Method calls wrong: doc called `mailService.compose(...)` and
  `mailService.replyToThread(...)`. Actual methods are
  `InboxEmailService.composeEmail` and `replyToThread`.
- Thread metadata update: doc said `messageCount`, schema has
  `unreadCount`.

## packages/cloudflare/docs/blob-storage.md

- BlobStorage interface signatures wrong: missing `options?` on
  `put` and `get`, `get` return type was `string | undefined`
  (actual is `BlobObject | null`), and `generateKey` was missing
  entirely.
- Added BlobObject, BlobPutOptions, and BlobGetOptions types
  alongside so consumers see the full shape.

## packages/cloudflare/docs/inbound.md

- InboundAdapter.handleIncoming return type wrong: said
  `Promise<void>`, actual is `Promise<{ messageId, threadId }>`.
- "Inbound email structure" section described a 14-field parsed
  form as if it were the input type. The actual InboundEmail is
  4 fields: `raw: ArrayBuffer`, `from`, `to`, `headers`. The
  parsed fields land on the message row AFTER the adapter does
  parsing work. Rewrote with a clear split between input shape
  and parsed-form schema fields.
- Classification section: `ClassificationAdapter` should be
  `EmailClassifier`, `summary` field does not exist on
  `EmailClassification` (actual fields are `tags`, `priority`).

## packages/workers-ai/docs/classification.md

- Interface name wrong: `ClassificationAdapter` should be
  `EmailClassifier`.
- Method signature wrong: `classify(message: ClassificationInput)`
  should be `classify(from: string, subject: string, body: string)`.
- Result type wrong: `ClassificationResult` with `summary` field
  should be `EmailClassification` with `tags` and `priority` fields.
- Example classifier implementation updated to the correct shape.

## packages/cloudflare/docs/quickstart.md

- Worker example imported `createR2BlobStorage` and
  `parseInboundEmail` from the package root. Actual exports are
  `createR2Storage` (from `/storage` subpath) and
  `parseEmailHeaders` / `hashContent` (from `/parsing` subpath).
  Also `createR2Storage` takes `{ bucket }` config, not a raw
  R2Bucket.
- `blobStorage.put(parsed.rawEmail)` was missing the required
  `key` parameter. Rewrote to use `storage.generateKey` and
  `storage.put(key, raw)`.
- `sendEmail` example showed `to: [array]`, actual is a single
  string.
- "Next steps" link list pointed at 5 docs at
  `./classification.md`, `./imap-quickstart.md`, etc. Only
  `./classification.md` does not exist in this package -- the
  rest live in other packages. Replaced with per-package npm
  links.

## Verification

- pnpm test: 609 passing across 37 files (no code change, same
  count as before)
- pnpm typecheck: clean
- pnpm lint: 0 warnings, 0 errors
- oxfmt normalized the 5 modified docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ssilvius and others added 3 commits April 11, 2026 12:00
…ackages

Adds a README.md in every package so the npm package page renders
properly instead of "no readme available". Adds "license": "MIT" to
every package.json so the license badge links via npm's SPDX lookup
to the MIT text (the actual LICENSE file lives in the org .github
repo, not this repo).

READMEs cover per-package scope, install command, minimal usage
examples, subpath export table where applicable, and a link back to
the monorepo README for the wider architecture. Each one starts with
a single-paragraph identity statement so the npm consumer sees what
the package is for within three seconds of landing.

Files added (9):
- packages/core/README.md
- packages/imap/README.md
- packages/imap-cloudflare/README.md
- packages/imap-server/README.md
- packages/resend/README.md
- packages/cloudflare/README.md
- packages/react-email/README.md
- packages/workers-ai/README.md
- packages/better-auth-resend/README.md

Files modified (9):
- all 9 package.json files -- add "license": "MIT" after description

Verification:
- pnpm publish --dry-run on all 9 packages shows README.md in the
  tarball contents and the correct file count bump (+1 per package).

CHANGELOGs are not hand-written -- they are auto-generated by the
pnpm changeset version step from the pending initial-release.md
changeset during the release flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Every package now has its own docs directory with topic-scoped
documentation, shipped in the npm tarball via files: ["dist", "docs"].
Consumers landing on an npm package page see real documentation, not
just a one-line README.

## Per-package docs

- **core** (5 docs):
  - reference.md (51.5 kB) -- complete schema and API reference for
    all 13 tables, service interfaces, Zod schemas, design decisions
  - threading.md -- RFC 5322 threading via In-Reply-To / References
  - migrations.md -- database migration workflow
  - newsletters.md -- mailing lists, subscribers, campaigns
  - adapters.md -- how adapters connect core to external services

- **imap** (2 docs):
  - commands.md -- full IMAP4rev1 command reference with RFC section
    citations
  - authentication.md -- AuthAdapter contract and consumer ownership

- **imap-server** (2 docs):
  - quickstart.md -- zero-to-running IMAP server
  - deployment.md -- Fly, Railway, Fargate, Docker, VPS per-platform
    guides with TLS handling

- **imap-cloudflare** (1 doc):
  - deployment.md -- Durable Object setup, bindings, cost model

- **resend** (1 doc):
  - outbound.md -- compose, send, webhook handling, audit

- **cloudflare** (3 docs):
  - quickstart.md -- Workers + D1 + R2 + Email Routing from zero
  - inbound.md -- parsing, dedupe, blob storage, thread matching
  - blob-storage.md -- R2 adapter, key schema, retrieval

- **react-email** (1 doc, newly written):
  - templates.md -- BaseEmail, OtpEmail, custom templates, renderer

- **workers-ai** (1 doc):
  - classification.md -- categories, priority derivation, tag patterns

- **better-auth-resend** (1 doc, newly written):
  - usage.md -- full better-auth integration, configuration, and how
    to replace the default template

Plus repo-level overview docs that do not ship with any package:
- docs/architecture.md -- 580 lines of internal design reference
- docs/getting-started.md -- 397 lines of cross-package quickstart

## Source

Most docs were copied from vault-2026/projects/rafters/mail-docs/
(the 16 docs written during the IMAP phase but never integrated into
the package tree). The two new ones (react-email/docs/templates.md,
better-auth-resend/docs/usage.md) were written fresh because vault
had nothing allocated to those packages.

## Shipping

Every package.json files field updated to ["dist", "docs"]. Every
README gained a Documentation section listing the local docs with
descriptions.

Verified via pnpm publish --dry-run on all 9 packages: each tarball
now includes the docs/ tree and the total file count reflects the
additions.

| Package               | Docs files | Tarball size | Total files |
|-----------------------|-----------:|-------------:|------------:|
| @rafters/mail         |          5 |     47.6 kB  |          34 |
| @rafters/mail-imap    |          2 |     24.9 kB  |          36 |
| @rafters/mail-imap-server  |     2 |      8.0 kB  |           6 |
| @rafters/mail-imap-cloudflare | 1 |      7.4 kB  |          11 |
| @rafters/mail-resend  |          1 |      8.5 kB  |          13 |
| @rafters/mail-cloudflare |       3 |      7.6 kB  |          13 |
| @rafters/mail-react-email |      1 |      5.0 kB  |          14 |
| @rafters/mail-workers-ai |       1 |      5.4 kB  |           8 |
| @rafters/better-auth-resend |    1 |      3.1 kB  |           5 |

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-CI format check caught 26 docs and READMEs that were not normalized
to oxfmt's canonical markdown form. CI runs pnpm format:check after
build / typecheck / lint, so unnormalized markdown fails the gate even
when the content is correct. Running pnpm format in place resolved
every file in one pass.

No content changes. Whitespace, table alignment, and code block
formatting only.

Affected files (26):
- docs/architecture.md, docs/getting-started.md (repo-level)
- packages/*/README.md (9 per-package READMEs)
- packages/*/docs/*.md (15 per-package docs)

Verification:
- pnpm format:check: clean
- pnpm test: 609 passing
- pnpm typecheck: clean
- pnpm lint: 0 warnings, 0 errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ssilvius ssilvius force-pushed the chore/package-readmes branch from d24e180 to ada95a9 Compare April 11, 2026 19:01
ssilvius added a commit that referenced this pull request Apr 11, 2026
After shipping the docs in PR #76 I spot-read them and found real
interface drift -- types, method signatures, and import paths that
did not match the current source. These would have broken every
consumer that copy-pasted the examples. The @vault-2026 technical
accuracy review covered the schema sections of core-reference.md
but did not cover the adapter package docs, which is where these
errors sit.

- EmailProvider.sendEmail: type names wrong. Doc said
  `sendEmail(params: SendEmailParams): Promise<SendEmailResult>`.
  Actual is `sendEmail(params: EmailParams): Promise<{ id: string }>`.
- EmailParams field table wrong: `from` is optional not required,
  `to` is a single string not array, `cc`/`bcc`/`headers` do not
  exist in EmailParams.
- Documented the full EmailProvider surface (mailing lists,
  subscribers, campaigns, audiences), which was omitted.
- Method calls wrong: doc called `mailService.compose(...)` and
  `mailService.replyToThread(...)`. Actual methods are
  `InboxEmailService.composeEmail` and `replyToThread`.
- Thread metadata update: doc said `messageCount`, schema has
  `unreadCount`.

- BlobStorage interface signatures wrong: missing `options?` on
  `put` and `get`, `get` return type was `string | undefined`
  (actual is `BlobObject | null`), and `generateKey` was missing
  entirely.
- Added BlobObject, BlobPutOptions, and BlobGetOptions types
  alongside so consumers see the full shape.

- InboundAdapter.handleIncoming return type wrong: said
  `Promise<void>`, actual is `Promise<{ messageId, threadId }>`.
- "Inbound email structure" section described a 14-field parsed
  form as if it were the input type. The actual InboundEmail is
  4 fields: `raw: ArrayBuffer`, `from`, `to`, `headers`. The
  parsed fields land on the message row AFTER the adapter does
  parsing work. Rewrote with a clear split between input shape
  and parsed-form schema fields.
- Classification section: `ClassificationAdapter` should be
  `EmailClassifier`, `summary` field does not exist on
  `EmailClassification` (actual fields are `tags`, `priority`).

- Interface name wrong: `ClassificationAdapter` should be
  `EmailClassifier`.
- Method signature wrong: `classify(message: ClassificationInput)`
  should be `classify(from: string, subject: string, body: string)`.
- Result type wrong: `ClassificationResult` with `summary` field
  should be `EmailClassification` with `tags` and `priority` fields.
- Example classifier implementation updated to the correct shape.

- Worker example imported `createR2BlobStorage` and
  `parseInboundEmail` from the package root. Actual exports are
  `createR2Storage` (from `/storage` subpath) and
  `parseEmailHeaders` / `hashContent` (from `/parsing` subpath).
  Also `createR2Storage` takes `{ bucket }` config, not a raw
  R2Bucket.
- `blobStorage.put(parsed.rawEmail)` was missing the required
  `key` parameter. Rewrote to use `storage.generateKey` and
  `storage.put(key, raw)`.
- `sendEmail` example showed `to: [array]`, actual is a single
  string.
- "Next steps" link list pointed at 5 docs at
  `./classification.md`, `./imap-quickstart.md`, etc. Only
  `./classification.md` does not exist in this package -- the
  rest live in other packages. Replaced with per-package npm
  links.

- pnpm test: 609 passing across 37 files (no code change, same
  count as before)
- pnpm typecheck: clean
- pnpm lint: 0 warnings, 0 errors
- oxfmt normalized the 5 modified docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ssilvius added a commit that referenced this pull request Apr 11, 2026
The JSDoc above `AuthAdapter` said "App passwords are stored hashed
(argon2) in D1 and verified here." That's wrong on two axes:

1. The IMAP server does not own credential storage or hashing. That
   was the original design but it was rewritten: AuthAdapter is an
   interface contract only, the consumer brings their own auth system.
   Prescribing argon2 contradicts the rewritten contract.

2. Core docs must be generic: "blob storage" not "R2", "database" not
   "D1". Referencing D1 by name inside the core protocol package
   violates the zero-vendor-dependency principle.

Replaced with a proper JSDoc block that:
- States the interface-only posture explicitly
- Names the consumer's ownership (storage, hashing, generation,
  revocation)
- Lists the security guarantees the IMAP server DOES enforce
  regardless of adapter implementation (generic failure response,
  rate limit, disconnect after max attempts)
- Refers to the adapter lifecycle in neutral language

This matches the authoritative `authentication.md` doc shipped in PR
#76 and the interface-only posture in the `@rafters/mail-imap-server`
quickstart.md fixed in PR #78.

Verification:
- pnpm --filter @rafters/mail-imap test: 313 passing
- pnpm --filter @rafters/mail-imap typecheck: clean
- pnpm --filter @rafters/mail-imap build: ESM + dts clean
- pnpm lint: 0 warnings, 0 errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ssilvius ssilvius merged commit a76526d into main Apr 11, 2026
1 check passed
ssilvius added a commit that referenced this pull request Apr 11, 2026
The JSDoc above `AuthAdapter` said "App passwords are stored hashed
(argon2) in D1 and verified here." That's wrong on two axes:

1. The IMAP server does not own credential storage or hashing. That
   was the original design but it was rewritten: AuthAdapter is an
   interface contract only, the consumer brings their own auth system.
   Prescribing argon2 contradicts the rewritten contract.

2. Core docs must be generic: "blob storage" not "R2", "database" not
   "D1". Referencing D1 by name inside the core protocol package
   violates the zero-vendor-dependency principle.

Replaced with a proper JSDoc block that:
- States the interface-only posture explicitly
- Names the consumer's ownership (storage, hashing, generation,
  revocation)
- Lists the security guarantees the IMAP server DOES enforce
  regardless of adapter implementation (generic failure response,
  rate limit, disconnect after max attempts)
- Refers to the adapter lifecycle in neutral language

This matches the authoritative `authentication.md` doc shipped in PR
#76 and the interface-only posture in the `@rafters/mail-imap-server`
quickstart.md fixed in PR #78.

Verification:
- pnpm --filter @rafters/mail-imap test: 313 passing
- pnpm --filter @rafters/mail-imap typecheck: clean
- pnpm --filter @rafters/mail-imap build: ESM + dts clean
- pnpm lint: 0 warnings, 0 errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ssilvius added a commit that referenced this pull request Apr 11, 2026
After shipping the docs in PR #76 I spot-read them and found real
interface drift -- types, method signatures, and import paths that
did not match the current source. These would have broken every
consumer that copy-pasted the examples. The @vault-2026 technical
accuracy review covered the schema sections of core-reference.md
but did not cover the adapter package docs, which is where these
errors sit.

- EmailProvider.sendEmail: type names wrong. Doc said
  `sendEmail(params: SendEmailParams): Promise<SendEmailResult>`.
  Actual is `sendEmail(params: EmailParams): Promise<{ id: string }>`.
- EmailParams field table wrong: `from` is optional not required,
  `to` is a single string not array, `cc`/`bcc`/`headers` do not
  exist in EmailParams.
- Documented the full EmailProvider surface (mailing lists,
  subscribers, campaigns, audiences), which was omitted.
- Method calls wrong: doc called `mailService.compose(...)` and
  `mailService.replyToThread(...)`. Actual methods are
  `InboxEmailService.composeEmail` and `replyToThread`.
- Thread metadata update: doc said `messageCount`, schema has
  `unreadCount`.

- BlobStorage interface signatures wrong: missing `options?` on
  `put` and `get`, `get` return type was `string | undefined`
  (actual is `BlobObject | null`), and `generateKey` was missing
  entirely.
- Added BlobObject, BlobPutOptions, and BlobGetOptions types
  alongside so consumers see the full shape.

- InboundAdapter.handleIncoming return type wrong: said
  `Promise<void>`, actual is `Promise<{ messageId, threadId }>`.
- "Inbound email structure" section described a 14-field parsed
  form as if it were the input type. The actual InboundEmail is
  4 fields: `raw: ArrayBuffer`, `from`, `to`, `headers`. The
  parsed fields land on the message row AFTER the adapter does
  parsing work. Rewrote with a clear split between input shape
  and parsed-form schema fields.
- Classification section: `ClassificationAdapter` should be
  `EmailClassifier`, `summary` field does not exist on
  `EmailClassification` (actual fields are `tags`, `priority`).

- Interface name wrong: `ClassificationAdapter` should be
  `EmailClassifier`.
- Method signature wrong: `classify(message: ClassificationInput)`
  should be `classify(from: string, subject: string, body: string)`.
- Result type wrong: `ClassificationResult` with `summary` field
  should be `EmailClassification` with `tags` and `priority` fields.
- Example classifier implementation updated to the correct shape.

- Worker example imported `createR2BlobStorage` and
  `parseInboundEmail` from the package root. Actual exports are
  `createR2Storage` (from `/storage` subpath) and
  `parseEmailHeaders` / `hashContent` (from `/parsing` subpath).
  Also `createR2Storage` takes `{ bucket }` config, not a raw
  R2Bucket.
- `blobStorage.put(parsed.rawEmail)` was missing the required
  `key` parameter. Rewrote to use `storage.generateKey` and
  `storage.put(key, raw)`.
- `sendEmail` example showed `to: [array]`, actual is a single
  string.
- "Next steps" link list pointed at 5 docs at
  `./classification.md`, `./imap-quickstart.md`, etc. Only
  `./classification.md` does not exist in this package -- the
  rest live in other packages. Replaced with per-package npm
  links.

- pnpm test: 609 passing across 37 files (no code change, same
  count as before)
- pnpm typecheck: clean
- pnpm lint: 0 warnings, 0 errors
- oxfmt normalized the 5 modified docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ssilvius added a commit that referenced this pull request Apr 11, 2026
… react-email

Second pass of accuracy fixes after finding more drift while reviewing
the 967-line adapters.md and cross-referencing against actual source.
Some of these errors were also present in files I introduced in PR #76
(now on main) -- fixing those here too.

## packages/core/docs/adapters.md

**Resend section:**
- Replaced `new ResendService(...) + new ResendProvider(resend)` with
  the actual factory: `createResendProvider({ apiKey, fromEmail })`.
  `ResendProvider` is not an exported class; `createResendProvider`
  is a factory function.
- `CampaignParams.audienceId` -> `listId`. The field name in the
  actual `campaignParamsSchema` is `listId`.
- Replaced `new MockEmailProvider()` with `createMockEmailProvider()`.
  Mock is also a factory, not a class.
- Documented the real mock surface: `sentEmails` as a top-level
  property, `getState()` for full internal state introspection,
  `clear()` for test resets.

**Cloudflare section:**
- Removed all references to `handleInboundEmail` (does not exist)
  and `R2BlobStorage` class (does not exist). Replaced with the
  actual building blocks: `createR2Storage`, `parseEmailHeaders`,
  `hashContent`.
- Fixed the `BlobStorage` interface to match the real shape:
  single `put(key, content, options?)`, `get(key, options?)` returns
  `BlobObject | null`, `delete(key)`, `generateKey(contentHash, extension)`.
  The doc had `putRaw`/`putText`/`putHtml` three-method split, which
  is not how the interface is shaped.
- Worker entry point example rewritten to use the actual exports
  and demonstrate the compose-your-own pattern (read bytes, parse
  headers, hash, store, insert row).
- Thread matching: clarified it is the consumer's responsibility,
  not a feature of the adapter. The adapter provides the parsing
  building blocks; thread matching queries are the consumer's code.
- `InMemoryBlobStorage` example rewritten as a factory function
  returning `BlobStorage` with the correct four methods.

**Workers AI section:**
- Removed `ClassifyEmailWorkflow` and `handleEmailClassifyQueue`
  references (do not exist). The package does not ship a workflow
  subpath or a queue subpath.
- Replaced `createEmailClassifier` with the real factory name
  `createWorkersAIClassifier(ai, config?)`.
- Queue consumer example rewritten to show consumer-implemented
  queue handling using the real classifier.
- Mock AI type is now `AiBinding` from the package, not `Ai`.

**React Email section:**
- Fixed import paths: `BaseEmail` lives at `/templates`, `OtpEmail`
  at `/otp`. Both were importing from the package root, which would
  fail at build time for consumers.
- Rendering section rewritten to show the registry pattern: the
  renderer is name-keyed, templates are registered at construction
  time (or via `.register()`), and `render(name, props)` looks up
  by name. Removed the incorrect pass-component-directly pattern.

**better-auth-resend section:**
- `resendOTP` config shape was entirely wrong. Doc claimed
  `resendOTP(env)` with `env = { RESEND_API_KEY, FROM_EMAIL }`,
  plus an optional second argument for branding. Actual is a single
  `ResendOTPConfig` object: `{ apiKey, fromEmail, brandName,
  logoUrl?, websiteUrl?, expiryMinutes?, baseUrl? }`. Rewrote
  integration example and documented the full config shape.

**Custom adapter example:**
- Replaced `class PostmarkProvider implements EmailProvider` with
  `createPostmarkProvider(config): EmailProvider` factory closure
  to match the monorepo factory-pattern convention in CLAUDE.md.

**Barrel exports section:**
- Replaced 4 wrong imports (`ResendProvider`, `R2BlobStorage`,
  `handleEmailClassifyQueue`, `ClassifyEmailWorkflow`) with the
  actual exports via their correct subpaths.

## packages/better-auth-resend/README.md (fixing #76 drift on main)

- `resendOTP` config: was `{ apiKey, from, appName }`, actual is
  `{ apiKey, fromEmail, brandName, ... }`. Fixed the example and
  the config table. Added the 4 optional fields (`logoUrl`,
  `websiteUrl`, `expiryMinutes`, `baseUrl`) the real schema exposes.

## packages/better-auth-resend/docs/usage.md (fixing #76 drift on main)

- Same `resendOTP` config fix as the README.
- "What the user sees" section updated: subject template is
  `"<brandName> verification code: <otp>"`, not `"Your <appName> code"`.
- "Replacing the template" example now uses the actual renderer
  registry pattern and `provider.sendEmail` (not `.send`).

## packages/react-email/README.md (fixing #76 drift on main)

- Renderer section now shows the registry pattern:
  `createReactEmailRenderer({ otp: OtpEmail })` then
  `renderer.render("otp", { code, brandName })`. The previous
  example called `render(OtpEmail({ otp, appName }))` which neither
  matches the interface nor uses the real OtpEmail prop names.
- Fixed OtpEmail prop names: `otp` -> `code`, `appName` -> `brandName`,
  `expiresInMinutes` -> `expiryMinutes`.

## packages/react-email/docs/templates.md (fixing #76 drift on main)

- Renderer section: same registry-pattern fix as the README, plus
  a "Why a registry instead of passing components" explanation
  (string-keyed templates let service code reference templates
  without importing React components).
- OtpEmail props: same `code`/`brandName`/`expiryMinutes` fix.
- "Writing your own template" example now registers the custom
  template with the renderer and calls `render(name, props)`
  instead of passing the component directly.

## Verification

- pnpm test: 609 passing
- pnpm typecheck: clean
- pnpm lint: 0 warnings, 0 errors
- pnpm format:check: clean on all modified files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ssilvius added a commit that referenced this pull request Apr 11, 2026
…on (#79)

The JSDoc above `AuthAdapter` said "App passwords are stored hashed
(argon2) in D1 and verified here." That's wrong on two axes:

1. The IMAP server does not own credential storage or hashing. That
   was the original design but it was rewritten: AuthAdapter is an
   interface contract only, the consumer brings their own auth system.
   Prescribing argon2 contradicts the rewritten contract.

2. Core docs must be generic: "blob storage" not "R2", "database" not
   "D1". Referencing D1 by name inside the core protocol package
   violates the zero-vendor-dependency principle.

Replaced with a proper JSDoc block that:
- States the interface-only posture explicitly
- Names the consumer's ownership (storage, hashing, generation,
  revocation)
- Lists the security guarantees the IMAP server DOES enforce
  regardless of adapter implementation (generic failure response,
  rate limit, disconnect after max attempts)
- Refers to the adapter lifecycle in neutral language

This matches the authoritative `authentication.md` doc shipped in PR
#76 and the interface-only posture in the `@rafters/mail-imap-server`
quickstart.md fixed in PR #78.

Verification:
- pnpm --filter @rafters/mail-imap test: 313 passing
- pnpm --filter @rafters/mail-imap typecheck: clean
- pnpm --filter @rafters/mail-imap build: ESM + dts clean
- pnpm lint: 0 warnings, 0 errors

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ssilvius added a commit that referenced this pull request Apr 11, 2026
* docs(packages): fix interface-shape drift in 5 adapter docs

After shipping the docs in PR #76 I spot-read them and found real
interface drift -- types, method signatures, and import paths that
did not match the current source. These would have broken every
consumer that copy-pasted the examples. The @vault-2026 technical
accuracy review covered the schema sections of core-reference.md
but did not cover the adapter package docs, which is where these
errors sit.

- EmailProvider.sendEmail: type names wrong. Doc said
  `sendEmail(params: SendEmailParams): Promise<SendEmailResult>`.
  Actual is `sendEmail(params: EmailParams): Promise<{ id: string }>`.
- EmailParams field table wrong: `from` is optional not required,
  `to` is a single string not array, `cc`/`bcc`/`headers` do not
  exist in EmailParams.
- Documented the full EmailProvider surface (mailing lists,
  subscribers, campaigns, audiences), which was omitted.
- Method calls wrong: doc called `mailService.compose(...)` and
  `mailService.replyToThread(...)`. Actual methods are
  `InboxEmailService.composeEmail` and `replyToThread`.
- Thread metadata update: doc said `messageCount`, schema has
  `unreadCount`.

- BlobStorage interface signatures wrong: missing `options?` on
  `put` and `get`, `get` return type was `string | undefined`
  (actual is `BlobObject | null`), and `generateKey` was missing
  entirely.
- Added BlobObject, BlobPutOptions, and BlobGetOptions types
  alongside so consumers see the full shape.

- InboundAdapter.handleIncoming return type wrong: said
  `Promise<void>`, actual is `Promise<{ messageId, threadId }>`.
- "Inbound email structure" section described a 14-field parsed
  form as if it were the input type. The actual InboundEmail is
  4 fields: `raw: ArrayBuffer`, `from`, `to`, `headers`. The
  parsed fields land on the message row AFTER the adapter does
  parsing work. Rewrote with a clear split between input shape
  and parsed-form schema fields.
- Classification section: `ClassificationAdapter` should be
  `EmailClassifier`, `summary` field does not exist on
  `EmailClassification` (actual fields are `tags`, `priority`).

- Interface name wrong: `ClassificationAdapter` should be
  `EmailClassifier`.
- Method signature wrong: `classify(message: ClassificationInput)`
  should be `classify(from: string, subject: string, body: string)`.
- Result type wrong: `ClassificationResult` with `summary` field
  should be `EmailClassification` with `tags` and `priority` fields.
- Example classifier implementation updated to the correct shape.

- Worker example imported `createR2BlobStorage` and
  `parseInboundEmail` from the package root. Actual exports are
  `createR2Storage` (from `/storage` subpath) and
  `parseEmailHeaders` / `hashContent` (from `/parsing` subpath).
  Also `createR2Storage` takes `{ bucket }` config, not a raw
  R2Bucket.
- `blobStorage.put(parsed.rawEmail)` was missing the required
  `key` parameter. Rewrote to use `storage.generateKey` and
  `storage.put(key, raw)`.
- `sendEmail` example showed `to: [array]`, actual is a single
  string.
- "Next steps" link list pointed at 5 docs at
  `./classification.md`, `./imap-quickstart.md`, etc. Only
  `./classification.md` does not exist in this package -- the
  rest live in other packages. Replaced with per-package npm
  links.

- pnpm test: 609 passing across 37 files (no code change, same
  count as before)
- pnpm typecheck: clean
- pnpm lint: 0 warnings, 0 errors
- oxfmt normalized the 5 modified docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(core): fix accuracy drift in threading, migrations, newsletters

Three more core docs had significant accuracy drift that would have
misled consumers reading them on npm. These were NOT covered by the
prior @vault-2026 schema review (which focused on core-reference.md).

## packages/core/docs/threading.md

- The doc described a threading engine that matches messages to
  threads using In-Reply-To / References / subject fallback. That
  engine does not exist in the core threading module -- threading.ts
  only exports generateMessageId, buildReferences, and generateSnippet
  as header-building blocks. Thread matching is the inbound adapter's
  responsibility and has a TODO in threading.ts pointing to #13.
- The doc claimed "subject matching within the same mailbox
  (configurable)". Grep for it -- no such feature exists. Subject
  matching is intentionally NOT part of the design because subject
  lines collide across unrelated conversations.
- The doc claimed "Deleting a thread soft-deletes all its messages".
  Checked the schema: inbox_message.threadId has ON DELETE CASCADE,
  which is a hard-delete cascade, not soft-delete. Soft-deleting a
  thread (setting deletedAt) does NOT cascade to messages. Clarified.
- The doc's thread model table was missing columns that exist in the
  schema: startedAt, updatedAt, archivedAt. Added.

## packages/core/docs/migrations.md

- The doc said "13 tables across two domains" and listed 3 newsletter
  tables as if they were part of migrationSQL. Actually migrationSQL
  only contains 10 inbox tables. The 3 newsletter tables
  (platform_audience, platform_subscriber, broadcast_audit) are
  Drizzle schema exports but are NOT in migrationSQL and are not
  written to by any shipped service.
- The newsletter table names were wrong: doc said mailing_list,
  subscriber, campaign. Actual names are platform_audience,
  platform_subscriber, broadcast_audit.
- The doc said system folders are "initialized automatically" when a
  mailbox is created. They are not. The consumer has to explicitly
  call FolderService.initSystemFolders(mailboxId).
- The "Conventions" section said "every table has deletedAt". Two
  label join tables (inbox_message_label, inbox_thread_label) do not
  have deletedAt because their delete semantic is direct row removal.

## packages/core/docs/newsletters.md

- The entire schema section described a completely different design
  than what exists in newsletter.ts. The doc had tables mailing_list,
  subscriber, campaign with fields like status, metadata, scheduledAt,
  htmlBody -- none of which exist. Actual tables are platform_audience
  (providerListId, name, description, slug), platform_subscriber
  (userId, audienceId, providerSubscriberId, subscribedAt), and
  broadcast_audit (providerCampaignId, subject, contentHash, sentBy,
  audienceName, recipientCount, sentAt).
- The doc described a "status lifecycle" (subscribed -> unsubscribed
  / bounced / complained) that does not exist because there is no
  status column on platform_subscriber.
- The doc described a campaign lifecycle (draft -> scheduled ->
  sending -> sent) that does not exist because there is no status
  column on broadcast_audit (which is an audit log, not a campaign
  record).
- Rewrote to accurately describe: (1) the EmailProvider surface as
  the authoritative API for mailing lists / subscribers / campaigns,
  (2) the 3 platform tables as OPTIONAL local mirror / audit tables
  the consumer may include in their own migrations, (3) that the
  provider (Resend) owns the authoritative data and the framework
  does not duplicate it locally by default.

## Verification

- pnpm test: 609 passing
- pnpm typecheck: clean
- pnpm lint: 0 warnings, 0 errors
- pnpm format:check: clean on modified files (.claude/settings.json
  is untracked session state, not part of this PR)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ssilvius added a commit that referenced this pull request Apr 11, 2026
…ct-email (#81)

* docs: fix interface drift across adapters.md, better-auth-resend, and react-email

Second pass of accuracy fixes after finding more drift while reviewing
the 967-line adapters.md and cross-referencing against actual source.
Some of these errors were also present in files I introduced in PR #76
(now on main) -- fixing those here too.

## packages/core/docs/adapters.md

**Resend section:**
- Replaced `new ResendService(...) + new ResendProvider(resend)` with
  the actual factory: `createResendProvider({ apiKey, fromEmail })`.
  `ResendProvider` is not an exported class; `createResendProvider`
  is a factory function.
- `CampaignParams.audienceId` -> `listId`. The field name in the
  actual `campaignParamsSchema` is `listId`.
- Replaced `new MockEmailProvider()` with `createMockEmailProvider()`.
  Mock is also a factory, not a class.
- Documented the real mock surface: `sentEmails` as a top-level
  property, `getState()` for full internal state introspection,
  `clear()` for test resets.

**Cloudflare section:**
- Removed all references to `handleInboundEmail` (does not exist)
  and `R2BlobStorage` class (does not exist). Replaced with the
  actual building blocks: `createR2Storage`, `parseEmailHeaders`,
  `hashContent`.
- Fixed the `BlobStorage` interface to match the real shape:
  single `put(key, content, options?)`, `get(key, options?)` returns
  `BlobObject | null`, `delete(key)`, `generateKey(contentHash, extension)`.
  The doc had `putRaw`/`putText`/`putHtml` three-method split, which
  is not how the interface is shaped.
- Worker entry point example rewritten to use the actual exports
  and demonstrate the compose-your-own pattern (read bytes, parse
  headers, hash, store, insert row).
- Thread matching: clarified it is the consumer's responsibility,
  not a feature of the adapter. The adapter provides the parsing
  building blocks; thread matching queries are the consumer's code.
- `InMemoryBlobStorage` example rewritten as a factory function
  returning `BlobStorage` with the correct four methods.

**Workers AI section:**
- Removed `ClassifyEmailWorkflow` and `handleEmailClassifyQueue`
  references (do not exist). The package does not ship a workflow
  subpath or a queue subpath.
- Replaced `createEmailClassifier` with the real factory name
  `createWorkersAIClassifier(ai, config?)`.
- Queue consumer example rewritten to show consumer-implemented
  queue handling using the real classifier.
- Mock AI type is now `AiBinding` from the package, not `Ai`.

**React Email section:**
- Fixed import paths: `BaseEmail` lives at `/templates`, `OtpEmail`
  at `/otp`. Both were importing from the package root, which would
  fail at build time for consumers.
- Rendering section rewritten to show the registry pattern: the
  renderer is name-keyed, templates are registered at construction
  time (or via `.register()`), and `render(name, props)` looks up
  by name. Removed the incorrect pass-component-directly pattern.

**better-auth-resend section:**
- `resendOTP` config shape was entirely wrong. Doc claimed
  `resendOTP(env)` with `env = { RESEND_API_KEY, FROM_EMAIL }`,
  plus an optional second argument for branding. Actual is a single
  `ResendOTPConfig` object: `{ apiKey, fromEmail, brandName,
  logoUrl?, websiteUrl?, expiryMinutes?, baseUrl? }`. Rewrote
  integration example and documented the full config shape.

**Custom adapter example:**
- Replaced `class PostmarkProvider implements EmailProvider` with
  `createPostmarkProvider(config): EmailProvider` factory closure
  to match the monorepo factory-pattern convention in CLAUDE.md.

**Barrel exports section:**
- Replaced 4 wrong imports (`ResendProvider`, `R2BlobStorage`,
  `handleEmailClassifyQueue`, `ClassifyEmailWorkflow`) with the
  actual exports via their correct subpaths.

## packages/better-auth-resend/README.md (fixing #76 drift on main)

- `resendOTP` config: was `{ apiKey, from, appName }`, actual is
  `{ apiKey, fromEmail, brandName, ... }`. Fixed the example and
  the config table. Added the 4 optional fields (`logoUrl`,
  `websiteUrl`, `expiryMinutes`, `baseUrl`) the real schema exposes.

## packages/better-auth-resend/docs/usage.md (fixing #76 drift on main)

- Same `resendOTP` config fix as the README.
- "What the user sees" section updated: subject template is
  `"<brandName> verification code: <otp>"`, not `"Your <appName> code"`.
- "Replacing the template" example now uses the actual renderer
  registry pattern and `provider.sendEmail` (not `.send`).

## packages/react-email/README.md (fixing #76 drift on main)

- Renderer section now shows the registry pattern:
  `createReactEmailRenderer({ otp: OtpEmail })` then
  `renderer.render("otp", { code, brandName })`. The previous
  example called `render(OtpEmail({ otp, appName }))` which neither
  matches the interface nor uses the real OtpEmail prop names.
- Fixed OtpEmail prop names: `otp` -> `code`, `appName` -> `brandName`,
  `expiresInMinutes` -> `expiryMinutes`.

## packages/react-email/docs/templates.md (fixing #76 drift on main)

- Renderer section: same registry-pattern fix as the README, plus
  a "Why a registry instead of passing components" explanation
  (string-keyed templates let service code reference templates
  without importing React components).
- OtpEmail props: same `code`/`brandName`/`expiryMinutes` fix.
- "Writing your own template" example now registers the custom
  template with the renderer and calls `render(name, props)`
  instead of passing the component directly.

## Verification

- pnpm test: 609 passing
- pnpm typecheck: clean
- pnpm lint: 0 warnings, 0 errors
- pnpm format:check: clean on all modified files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(architecture): fix package count, IMAP status, and consumer vs framework boundary

The repo-level docs/architecture.md had accumulated significant drift
from the current shipped state:

1. **Package count wrong.** Said "Six packages" -- it is nine now
   after the IMAP packages shipped this week (@rafters/mail-imap,
   @rafters/mail-imap-cloudflare, @rafters/mail-imap-server).
   Updated the count, the list, and the dependency graph.

2. **IMAP section said "designed, not built, planned for post-0.1.0."**
   All three IMAP packages ship in 0.1.0. Rewrote the section:
   - Status is "shipped in 0.1.0 across three packages"
   - Added the Node runtime alongside the DO runtime
   - Expanded the command set to match what is actually shipped:
     added UNSELECT (RFC 3691), COPY + MOVE (RFC 6851), APPEND
     (RFC 4315 APPENDUID). The previous list was the Phase 1 subset.
   - Added the advertised capability list from SERVER_CAPABILITIES
   - Added a transport comparison with the Spectrum future path
     (still blocked on Enterprise tier; issue #57)
   - Added the inbound signaling section covering both runtimes
     (DO: POST /notify, Node: server.notify() from PR #78)

3. **Invented exports in the "Extracted" list.** Replaced:
   - `ResendProvider` implementation -> `createResendProvider` factory
   - `MockEmailProvider` class -> `createMockEmailProvider` factory
   - `ClassifyEmailWorkflow` -> removed (does not exist)
   - Queue consumer `handleEmailClassifyQueue` -> removed (does not exist)
   - Added real exports: createImapDurableObject, createImapWorker,
     createImapServer, parseEmailHeaders, hashContent, classifier
     helper functions

4. **Wrong subpath import example.** `createR2BlobStorage` ->
   `createR2Storage`. Same bug as elsewhere.

5. **Inbound flow described as InboundAdapter doing the work.**
   The shipped adapters provide building blocks (parseEmailHeaders,
   hashContent, createR2Storage); the orchestration is consumer code.
   Rewrote the flow diagram with steps 5-8 clearly labeled as
   "Consumer-written" and added a callout explaining why there is
   no pre-baked handleInboundEmail (schema extensions + auth context
   + pipeline topology are all application decisions).

6. **Classification flow described as framework doing the work.**
   Steps 1-4 are the classifier (which ships in mail-workers-ai);
   steps 5-8 (DB update, spam folder move, label application) are
   consumer-written against their own schema and FolderService.
   Rewrote to make the boundary explicit.

7. **Threading logic described as shipped behavior.** Threading.ts
   exports generateMessageId, buildReferences, and generateSnippet
   as building blocks. Thread matching is NOT shipped -- it is the
   consumer's responsibility for inbound (no InboundAdapter
   implementation exists yet). Outbound threading in composeEmail/
   replyToThread IS shipped via InboxEmailService. Clarified the
   split.

No content additions that invent features. Every claim is verified
against actual source in packages/*.

## Verification

- pnpm test: 609 passing
- pnpm typecheck: clean
- pnpm lint: 0 warnings, 0 errors
- pnpm format:check: clean
- All referenced exports, method names, and command names verified
  against the actual TypeScript source

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(imap/commands): correct SEARCH criteria list to match parser

The SEARCH command section listed 6 criteria that the actual parser
does not implement: ANSWERED, DELETED, DRAFT, FLAGGED, SEEN, UNSEEN.
Any IMAP client sending one of these receives "BAD Unknown search
criterion: <name>" -- a hard failure, not a graceful fallback.

Verified against packages/imap/src/protocol/parser.ts: the switch
statement parseSearchCriteria has cases for ALL, NEW, FROM, TO, CC,
BCC, SUBJECT, BEFORE, ON, SINCE, LARGER, SMALLER, TEXT, BODY, UID,
NOT, OR plus bare sequence sets. Everything else throws via the
default branch.

Updated the supported list to reflect reality and added a "Not yet
supported" section that:

- Names the missing flag-based criteria explicitly
- Names other RFC 3501 criteria that are also missing (KEYWORD,
  UNKEYWORD, HEADER, SENTBEFORE / SENTON / SENTSINCE)
- Documents the practical workaround: SEARCH NEW matches
  RECENT + UNSEEN for the "new mail" UI case, and STATUS INBOX
  (UNSEEN) returns the unread count without needing the UNSEEN
  criterion.
- Notes that this is a real protocol gap being tracked in an issue

Updated the example to use FROM + SINCE (both supported) instead
of UNSEEN + SINCE (UNSEEN is not supported).

No implementation change in this PR. The parser gap is a separate
issue that needs a code change to add the flag-based criteria to
parseSearchCriteria and wire them to searchMessages in the message
adapter.

## Verification

- Verified every listed criterion against the case statements in
  parser.ts and the SearchCriterion type
- pnpm format:check: clean
- pnpm test: (unchanged)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant