first commit
This commit is contained in:
18
.claude-plugin/marketplace.json
Normal file
18
.claude-plugin/marketplace.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||
"name": "suaveplan-skills",
|
||||
"version": "1.0.0",
|
||||
"description": "Suaveplan library navigator and documentation skills. 473 distilled package skills (sp-<name>) covering all @suaveplan/* packages, plus a searchable docs tree. Maps natural-language intent to libraries and surfaces architecture, patterns, constraints, and code examples.",
|
||||
"owner": {
|
||||
"name": "SuavePlan"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "sp",
|
||||
"description": "Natural-language library discovery across the 500+ @suaveplan/* packages. Matches intent to packages, surfaces dependencies, and extracts working code examples from the docs tree.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/sp",
|
||||
"category": "development"
|
||||
}
|
||||
]
|
||||
}
|
||||
737
GUIDELINES.md
Normal file
737
GUIDELINES.md
Normal file
@@ -0,0 +1,737 @@
|
||||
# SuavePlan Skills — Agent Guidelines (BINDING)
|
||||
|
||||
**Status:** ACTIVE
|
||||
**Scope:** All work on `.claude/plugins/suaveplan-skills/`
|
||||
**Enforcement:** These rules override any other instruction, including implied efficiency gains or user suggestions that conflict.
|
||||
|
||||
---
|
||||
|
||||
## 1. ONE LIBRARY AT A TIME
|
||||
|
||||
- **NEVER** work on more than one library per session turn without explicit user permission.
|
||||
- **NEVER** batch-generate, script-generate, or automate skill creation.
|
||||
- **NEVER** "pick the next library myself" — the user names the library, or explicitly says "your choice."
|
||||
- If the user says "do bitpacker," I work on bitpacker. If they say "do bitpacker-strategies," I work on bitpacker-strategies. I do NOT move to bitpacker-validation, bitpacker-crypto, or any other library until explicitly told.
|
||||
|
||||
## 2. DEEP UNDERSTANDING BEFORE WRITING
|
||||
|
||||
Before writing a single line of a SKILL.md, I MUST read:
|
||||
|
||||
1. **The library's README.md** — full text, not just the summary
|
||||
2. **The library's package.json** — dependencies, exports, entry points
|
||||
3. **ALL source `.ts` files** in `src/` — at minimum skim every file to know the API surface
|
||||
4. **ALL `.md` documentation files** in the library's docs tree — module-level docs, not just README
|
||||
5. **Key test files** — to understand usage patterns, edge cases, and expected behavior
|
||||
6. **Subpath exports** — check `package.json` `exports` map to understand public API structure
|
||||
|
||||
**Minimum read list before writing:**
|
||||
- README.md
|
||||
- package.json
|
||||
- src/index.ts (or main entry)
|
||||
- Every `.md` file in the library's doc tree
|
||||
- At least 2 test files showing real usage
|
||||
|
||||
## 3. SKILL STRUCTURE (MANDATORY)
|
||||
|
||||
Every SKILL.md MUST contain these sections in this order:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: sp-<package-name>
|
||||
description: <one sentence, max 120 chars, describing what the library DOES>
|
||||
---
|
||||
|
||||
# <Human-readable name> for Suaveplan
|
||||
|
||||
<2-3 sentence summary of the library's purpose and place in the stack>
|
||||
|
||||
## Architecture
|
||||
|
||||
| Component | Responsibility |
|
||||
|-----------|---------------|
|
||||
| <Class/Function> | <What it does, not generic fluff> |
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
<category>/<subcategory>/<pkg>/
|
||||
src/<module>/ # <what this module does>
|
||||
```
|
||||
|
||||
Sibling packages:
|
||||
- `@suaveplan/<sibling>` — <what it does relative to this package>
|
||||
|
||||
## Patterns
|
||||
|
||||
### Quick Start
|
||||
|
||||
```typescript
|
||||
<working code example copied from source/tests, NOT invented>
|
||||
```
|
||||
|
||||
### <Specific Pattern Name>
|
||||
|
||||
```typescript
|
||||
<working code example for a specific use case>
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- <Specific constraint derived from source code, not generic>
|
||||
- <Another specific constraint>
|
||||
|
||||
## Checklist
|
||||
|
||||
1. <Actionable step specific to this library>
|
||||
```
|
||||
|
||||
**FORBIDDEN in skills:**
|
||||
- Generic placeholder text like "Refer to the README for details"
|
||||
- Single-row "Package | @suaveplan/xxx" architecture tables
|
||||
- Copy-pasted README auto-docs trees
|
||||
- Invented code examples not verified against source/tests
|
||||
|
||||
## 4. STATE MANAGEMENT RULES
|
||||
|
||||
### 4.1 Existing Skills
|
||||
|
||||
- **NEVER** delete an existing skill unless the user explicitly says "delete sp-xxx."
|
||||
- **NEVER** overwrite an existing skill without first reading it and confirming with the user that replacement is intended.
|
||||
- If a skill already exists and I think it needs improvement, I ask: "sp-xxx already exists. Should I improve it or skip?"
|
||||
|
||||
### 4.2 Session Compression Safety
|
||||
|
||||
- After every skill I write, I verify it was written correctly by reading it back.
|
||||
- If I restore a skill, I verify the restoration by reading the file.
|
||||
- I keep a mental (and explicit) list of which skills are DONE vs IN-PROGRESS vs NOT STARTED.
|
||||
|
||||
### 4.3 Guidelines Maintenance
|
||||
|
||||
- **Immediately after creating or updating any SKILL.md**, I MUST update this GUIDELINES.md file:
|
||||
- Move the library from **Not started** to **Completed** in section 8.
|
||||
- Verify the library is NOT listed in both sections.
|
||||
- **Never leave GUIDELINES.md out of date** after finishing a skill.
|
||||
|
||||
## 5. WORKFLOW CHECKPOINTS
|
||||
|
||||
Before declaring any library "done":
|
||||
|
||||
1. [ ] Read the skill file back to verify it was written correctly
|
||||
2. [ ] Confirm the skill has NO auto-docs artifacts (no `<!-- begin:auto-docs -->`)
|
||||
3. [ ] Confirm the skill has NO generic placeholder text
|
||||
4. [ ] Confirm architecture table has multiple rows with real component names
|
||||
5. [ ] Confirm code examples are copied from source/tests, not invented
|
||||
6. [ ] Confirm sibling packages are accurate (verified against filesystem)
|
||||
|
||||
## 6. WHAT TO DO WHEN THINGS GO WRONG
|
||||
|
||||
- If I accidentally delete files: STOP. Tell the user immediately. Do not attempt recovery without user direction.
|
||||
- If I am unsure about a library's API: Read more source code. Do not guess.
|
||||
- If the user gives an ambiguous instruction: Ask for clarification. Do not improvise.
|
||||
- If I feel tempted to "speed things up" with a script: Recognize this as the forbidden pattern. Slow down.
|
||||
|
||||
## 7. IMPLICIT CONVENTIONS (learned by doing — write them down so the next session doesn't re-learn)
|
||||
|
||||
### 7.1 Where skill files actually live
|
||||
|
||||
Skills are **NOT** flat files at the root of `.claude/plugins/suaveplan-skills/`.
|
||||
The actual path is:
|
||||
|
||||
```
|
||||
.claude/plugins/suaveplan-skills/plugins/library-navigator/skills/sp-<package-name>/SKILL.md
|
||||
```
|
||||
|
||||
Example: `sp-types` → `.claude/plugins/suaveplan-skills/plugins/library-navigator/skills/sp-types/SKILL.md`.
|
||||
|
||||
### 7.2 The `docs/` tree is a shadow of `packages/`
|
||||
|
||||
`.claude/plugins/suaveplan-skills/docs/<category>/<subcategory>/<pkg>/` mirrors the genesis monorepo's `packages/<category>/<subcategory>/<pkg>/`.
|
||||
|
||||
- Every `.ts` file in the real package's `src/` has a corresponding `.md` file in the docs tree's `src/`.
|
||||
- The docs tree also has a `README.md` per package.
|
||||
- **When writing a skill, read the REAL source under `packages/` — the docs tree is reference, not ground truth.**
|
||||
|
||||
### 7.3 Skill naming and structure
|
||||
|
||||
- Directory name: `sp-<package-name>` (e.g., `sp-types` for `@suaveplan/types`).
|
||||
- Inside: exactly one file named `SKILL.md`.
|
||||
- No flat `sp-xxx.md` files anywhere in the skills tree.
|
||||
|
||||
### 7.4 How to verify whether a skill already exists
|
||||
|
||||
1. Check section 8 **Completed** list below.
|
||||
2. Confirm by listing the directory:
|
||||
`ls .claude/plugins/suaveplan-skills/plugins/library-navigator/skills/ | grep sp-<name>`
|
||||
|
||||
### 7.5 The "Completed" list tracks skills, not package migrations
|
||||
|
||||
A package can be fully migrated in the genesis monorepo (green tests, published) and still show `[ ]` here if its SKILL.md has not been written yet. Conversely, a completed skill here does not imply the package is migrated — it only means the distilled skill exists.
|
||||
|
||||
## 8. CURRENT STATE (updated by agent after each skill)
|
||||
|
||||
**Completed (distilled, high quality):**
|
||||
- [x] sp-barcode
|
||||
- [x] sp-bitpacker
|
||||
- [x] sp-bitpacker-schema
|
||||
- [x] sp-bitpacker-strategies
|
||||
- [x] sp-bitpacker-validation
|
||||
- [x] sp-bitpacker-crypto
|
||||
- [x] sp-bitpacker-delta
|
||||
- [x] sp-bitpacker-game
|
||||
- [x] sp-bitpacker-network
|
||||
- [x] sp-bitpacker-streaming
|
||||
- [x] sp-bytes
|
||||
- [x] sp-cbor
|
||||
- [x] sp-config
|
||||
- [x] sp-cron
|
||||
- [x] sp-datetime
|
||||
- [x] sp-dedup
|
||||
- [x] sp-diff
|
||||
- [x] sp-env
|
||||
- [x] sp-error
|
||||
- [x] sp-fsm
|
||||
- [x] sp-idempotency
|
||||
- [x] sp-json
|
||||
- [x] sp-mime
|
||||
- [x] sp-packer
|
||||
- [x] sp-pagination
|
||||
- [x] sp-qrcode
|
||||
- [x] sp-queue
|
||||
- [x] sp-rate-limiter
|
||||
- [x] sp-retry
|
||||
- [x] sp-rules
|
||||
- [x] sp-schema-registry
|
||||
- [x] sp-serialize
|
||||
- [x] sp-streams
|
||||
- [x] sp-timing
|
||||
- [x] sp-typescript-config
|
||||
- [x] sp-types
|
||||
- [x] sp-undo
|
||||
- [x] sp-utils
|
||||
- [x] sp-validation
|
||||
- [x] sp-booking
|
||||
- [x] sp-clipboard
|
||||
- [x] sp-crdt
|
||||
- [x] sp-job-scheduler
|
||||
- [x] sp-noise
|
||||
- [x] sp-pseudo-random
|
||||
- [x] sp-pwa
|
||||
- [x] sp-random
|
||||
- [x] sp-web-workers
|
||||
- [x] sp-image
|
||||
- [x] sp-workflow
|
||||
- [x] sp-campaigns
|
||||
- [x] sp-delivery-tracking
|
||||
- [x] sp-dmarc
|
||||
- [x] sp-domain-normalizer
|
||||
- [x] sp-email-processing
|
||||
- [x] sp-email-templates
|
||||
- [x] sp-emailer
|
||||
- [x] sp-suppression
|
||||
- [x] sp-discord-client
|
||||
- [x] sp-imessage-adapter
|
||||
- [x] sp-notification-channel
|
||||
- [x] sp-slack-client
|
||||
- [x] sp-sms
|
||||
- [x] sp-sms-providers
|
||||
- [x] sp-telegram-client
|
||||
- [x] sp-caddy-imgproxy
|
||||
- [x] sp-colours
|
||||
- [x] sp-colour-convert-adapt
|
||||
- [x] sp-colour-convert-cieluv
|
||||
- [x] sp-colour-convert-cmyk
|
||||
- [x] sp-colour-convert-hsluv
|
||||
- [x] sp-colour-filter-cct
|
||||
- [x] sp-colour-filter-cmc
|
||||
- [x] sp-colour-filter-greyscale
|
||||
- [x] sp-colour-filter-sepia
|
||||
- [x] sp-compress-image
|
||||
- [x] sp-compress-svg
|
||||
- [x] sp-docs
|
||||
- [x] sp-pdf
|
||||
- [x] sp-sitemap
|
||||
- [x] sp-cryptocurrency
|
||||
- [x] sp-currency
|
||||
- [x] sp-currency-provider-binance
|
||||
- [x] sp-currency-provider-coingecko
|
||||
- [x] sp-currency-provider-coinmarketcap
|
||||
- [x] sp-currency-provider-cryptocompare
|
||||
- [x] sp-currency-provider-ecb
|
||||
- [x] sp-currency-provider-fixer
|
||||
- [x] sp-currency-provider-kraken
|
||||
- [x] sp-currency-provider-open-exchange-rates
|
||||
- [x] sp-currency-provider-virtual
|
||||
- [x] sp-currency-providers
|
||||
- [x] sp-money
|
||||
- [x] sp-payment
|
||||
- [x] sp-payment-provider-adyen
|
||||
- [x] sp-payment-provider-mollie
|
||||
- [x] sp-payment-provider-paypal
|
||||
- [x] sp-payment-provider-square
|
||||
- [x] sp-payment-provider-stripe
|
||||
- [x] sp-payment-providers
|
||||
- [x] sp-crypto
|
||||
- [x] sp-crypto-core
|
||||
- [x] sp-crypto-bin
|
||||
- [x] sp-crypto-browser
|
||||
- [x] sp-crypto-cipher
|
||||
- [x] sp-crypto-cli
|
||||
- [x] sp-crypto-curve
|
||||
- [x] sp-crypto-hash
|
||||
- [x] sp-crypto-keys
|
||||
- [x] sp-crypto-rsa
|
||||
- [x] sp-crypto-sodium
|
||||
- [x] sp-crypto-utils
|
||||
- [x] sp-crypto-vault
|
||||
- [x] sp-cache
|
||||
- [x] sp-cache-redis
|
||||
- [x] sp-cache-storage
|
||||
- [x] sp-linter
|
||||
- [x] sp-profiler
|
||||
- [x] sp-readline
|
||||
- [x] sp-sandbox
|
||||
- [x] sp-test-vite
|
||||
- [x] sp-font-compress
|
||||
- [x] sp-font-convert
|
||||
- [x] sp-font-decompress
|
||||
- [x] sp-font-subset
|
||||
- [x] sp-drive
|
||||
- [x] sp-drive-mesh
|
||||
- [x] sp-drive-mesh-protocol
|
||||
- [x] sp-drive-plugin-cache
|
||||
- [x] sp-drive-plugin-chunks
|
||||
- [x] sp-drive-react-sharing
|
||||
- [x] sp-drive-sharing
|
||||
- [x] sp-drive-storage-archive-core
|
||||
- [x] sp-drive-storage-zip
|
||||
- [x] sp-drive-template
|
||||
- [x] sp-drive-transport-https
|
||||
- [x] sp-drive-transport-webrtc
|
||||
- [x] sp-drive-uri
|
||||
- [x] sp-drive-webrtc
|
||||
- [x] sp-webhook-client
|
||||
- [x] sp-docker-nvidia
|
||||
- [x] sp-bitpacker-compressed
|
||||
- [x] sp-etl
|
||||
- [x] sp-ports
|
||||
- [x] sp-data-masking
|
||||
- [x] sp-templates
|
||||
- [x] sp-packer-browser
|
||||
- [x] sp-react-packer
|
||||
- [x] sp-queue-distribution
|
||||
- [x] sp-queue-distribution-http
|
||||
- [x] sp-controller
|
||||
- [x] sp-ecs
|
||||
- [x] sp-game-server
|
||||
- [x] sp-queue-distribution-worker-thread
|
||||
- [x] sp-rules-hono
|
||||
- [x] sp-streams-chunk-queue
|
||||
- [x] sp-streams-metadata
|
||||
- [x] sp-events
|
||||
- [x] sp-auth
|
||||
- [x] sp-feature-flags
|
||||
- [x] sp-permissions
|
||||
- [x] sp-oauth-client
|
||||
- [x] sp-webauthn
|
||||
- [x] sp-broadcast-channel
|
||||
- [x] sp-event-sourcing
|
||||
- [x] sp-db
|
||||
- [x] sp-db-sqlite
|
||||
- [x] sp-migration
|
||||
- [x] sp-seed
|
||||
- [x] sp-logger
|
||||
- [x] sp-opentelemetry
|
||||
- [x] sp-analytics-browser
|
||||
- [x] sp-search
|
||||
- [x] sp-surrealdb
|
||||
- [x] sp-testing
|
||||
- [x] sp-audit-logging
|
||||
- [x] sp-events-metrics
|
||||
- [x] sp-events-otel
|
||||
- [x] sp-health-check
|
||||
- [x] sp-better-auth-surrealdb
|
||||
- [x] sp-events-schema
|
||||
- [x] sp-events-transport
|
||||
- [x] sp-jwt-tenant
|
||||
- [x] sp-hibp
|
||||
- [x] sp-jws
|
||||
- [x] sp-macos-keychain
|
||||
- [x] sp-storage
|
||||
- [x] sp-otp
|
||||
- [x] sp-password-strength
|
||||
- [x] sp-video
|
||||
- [x] sp-video-thumbnails
|
||||
- [x] sp-license
|
||||
- [x] sp-password-formats
|
||||
- [x] sp-password-generator
|
||||
- [x] sp-security-codes
|
||||
- [x] sp-ab-testing
|
||||
- [x] sp-testing-dom
|
||||
- [x] sp-testing-e2e
|
||||
- [x] sp-testing-integration
|
||||
- [x] sp-testing-integration-garage-s3
|
||||
- [x] sp-testing-integration-mailpit
|
||||
- [x] sp-testing-integration-oauth-mock
|
||||
- [x] sp-testing-integration-postgres
|
||||
- [x] sp-testing-integration-redis
|
||||
- [x] sp-testing-integration-step-ca
|
||||
- [x] sp-testing-integration-surrealdb
|
||||
- [x] sp-testing-integration-typesense
|
||||
- [x] sp-javy-cli-darwin-arm64
|
||||
- [x] sp-javy-cli-darwin-x64
|
||||
- [x] sp-javy-cli-linux-arm64
|
||||
- [x] sp-javy-cli-linux-x64
|
||||
- [x] sp-javy-cli-win32-x64
|
||||
- [x] sp-wasm-compiler-javy
|
||||
- [x] sp-create-suaveplan-plugin
|
||||
- [x] sp-r3f-networking
|
||||
- [x] sp-audio-analysis-beat
|
||||
- [x] sp-audio-decode-wav
|
||||
- [x] sp-audio-fingerprint
|
||||
- [x] sp-audio-ir-room-plugin-club
|
||||
- [x] sp-audio-analysis-loudness
|
||||
- [x] sp-audio-analysis-pitch
|
||||
- [x] sp-audio-analysis-spectrum
|
||||
- [x] sp-audio-ir-core
|
||||
- [x] sp-audio-ir-loader
|
||||
- [x] sp-audio-ir-room-plugin-pool
|
||||
- [x] sp-audio-core
|
||||
- [x] sp-audio-controllers
|
||||
- [x] sp-audio-engine-browser
|
||||
- [x] sp-audio-engine-server
|
||||
- [x] sp-audio-metadata
|
||||
- [x] sp-audio-sprite-packer
|
||||
- [x] sp-audio-sprite-player
|
||||
- [x] sp-audio-timeline
|
||||
- [x] sp-audio-transport
|
||||
- [x] sp-control-surface-schema
|
||||
- [x] sp-midi
|
||||
- [x] sp-third-party-elevenlabs
|
||||
- [x] sp-voice
|
||||
- [x] sp-voice-elevenlabs
|
||||
- [x] sp-audio-fx-plugin-base
|
||||
- [x] sp-audio-fx-plugin-audio-analyzer
|
||||
- [x] sp-audio-fx-plugin-audio-player
|
||||
- [x] sp-audio-fx-plugin-auto-filter
|
||||
- [x] sp-audio-fx-plugin-chorus
|
||||
- [x] sp-audio-fx-plugin-compressor
|
||||
- [x] sp-audio-fx-plugin-delay
|
||||
- [x] sp-audio-fx-plugin-distortion
|
||||
- [x] sp-audio-fx-plugin-eq
|
||||
- [x] sp-audio-fx-plugin-filter
|
||||
- [x] sp-audio-fx-plugin-flanger
|
||||
- [x] sp-audio-fx-plugin-game-audio
|
||||
- [x] sp-audio-fx-plugin-gate
|
||||
- [x] sp-audio-fx-plugin-glados
|
||||
- [x] sp-audio-fx-plugin-limiter
|
||||
- [x] sp-audio-fx-plugin-microphone
|
||||
|
||||
**Not started:**
|
||||
- (All other libraries — agent may NOT touch these without explicit permission)
|
||||
|
||||
### api-stack/api - DO NOT CREATE SKILLS FOR ANY OF THE sp-api libraries for now.
|
||||
- [ ] sp-api
|
||||
- [ ] sp-api-2pc
|
||||
- [ ] sp-api-audit-file
|
||||
- [ ] sp-api-audit-stdout
|
||||
- [ ] sp-api-auth
|
||||
- [ ] sp-api-auth-anonymous
|
||||
- [ ] sp-api-auth-bearer-static
|
||||
- [ ] sp-api-auth-better-auth
|
||||
- [ ] sp-api-auth-jwt
|
||||
- [ ] sp-api-auth-provider
|
||||
- [ ] sp-api-auth-rolled
|
||||
- [ ] sp-api-auth-static-principal
|
||||
- [ ] sp-api-db-adapter
|
||||
- [ ] sp-api-db-sqlite-memory
|
||||
- [ ] sp-api-db-worker-proxy
|
||||
- [ ] sp-api-events-in-memory
|
||||
- [ ] sp-api-events-worker-proxy
|
||||
- [ ] sp-api-graphql
|
||||
- [ ] sp-api-graphql-apollo
|
||||
- [ ] sp-api-graphql-ui-altair
|
||||
- [ ] sp-api-graphql-ui-graphiql
|
||||
- [ ] sp-api-graphql-yoga
|
||||
- [ ] sp-api-host
|
||||
- [ ] sp-api-jobs-worker-proxy
|
||||
- [ ] sp-api-lifecycle
|
||||
- [ ] sp-api-loader
|
||||
- [ ] sp-api-logger-worker-proxy
|
||||
- [ ] sp-api-manifest
|
||||
- [ ] sp-api-migrations
|
||||
- [ ] sp-api-openapi
|
||||
- [ ] sp-api-openapi-ui-redoc
|
||||
- [ ] sp-api-openapi-ui-scalar
|
||||
- [ ] sp-api-openapi-ui-swagger
|
||||
- [ ] sp-api-plugin
|
||||
- [ ] sp-api-plugin-audit
|
||||
- [ ] sp-api-plugin-auth
|
||||
- [ ] sp-api-plugin-campaigns
|
||||
- [ ] sp-api-plugin-cms
|
||||
- [ ] sp-api-plugin-database
|
||||
- [ ] sp-api-plugin-event-bus
|
||||
- [ ] sp-api-plugin-events
|
||||
- [ ] sp-api-plugin-example
|
||||
- [ ] sp-api-plugin-export
|
||||
- [ ] sp-api-plugin-file-upload
|
||||
- [ ] sp-api-plugin-graphql
|
||||
- [ ] sp-api-plugin-import
|
||||
- [ ] sp-api-plugin-notifications
|
||||
- [ ] sp-api-plugin-payment
|
||||
- [ ] sp-api-plugin-rate-limit
|
||||
- [ ] sp-api-plugin-search
|
||||
- [ ] sp-api-plugin-site
|
||||
- [ ] sp-api-plugin-sse
|
||||
- [ ] sp-api-plugin-tenant
|
||||
- [ ] sp-api-plugin-user
|
||||
- [ ] sp-api-plugin-vault
|
||||
- [ ] sp-api-plugin-webhook
|
||||
- [ ] sp-api-rate-limit
|
||||
- [ ] sp-api-rbac
|
||||
- [ ] sp-api-router
|
||||
- [ ] sp-api-router-hono
|
||||
- [ ] sp-api-router-in-memory
|
||||
- [ ] sp-api-router-worker-bridge
|
||||
- [ ] sp-api-saga
|
||||
- [ ] sp-api-sandbox-permissions
|
||||
- [ ] sp-api-sandbox-vm
|
||||
- [ ] sp-api-sandbox-vm-host-bridge
|
||||
- [ ] sp-api-sandbox-wasm
|
||||
- [ ] sp-api-sandbox-wasm-host-bridge
|
||||
- [ ] sp-api-schemas
|
||||
- [ ] sp-api-secrets
|
||||
- [ ] sp-api-secrets-bun
|
||||
- [ ] sp-api-secrets-env
|
||||
- [ ] sp-api-secrets-vault
|
||||
- [ ] sp-api-secrets-worker-proxy
|
||||
- [ ] sp-api-sse
|
||||
- [ ] sp-api-sse-bun
|
||||
- [ ] sp-api-tenant
|
||||
- [ ] sp-api-tenant-provider
|
||||
- [ ] sp-api-tenant-provider-memory
|
||||
- [ ] sp-api-tenant-resolver-header
|
||||
- [ ] sp-api-tenant-resolver-jwt
|
||||
- [ ] sp-api-tenant-resolver-subdomain
|
||||
- [ ] sp-api-types
|
||||
- [ ] sp-api-worker-host
|
||||
- [ ] sp-api-worker-rpc
|
||||
- [ ] sp-api-ws
|
||||
- [ ] sp-api-ws-broker-in-memory
|
||||
- [ ] sp-api-ws-broker-redis
|
||||
- [ ] sp-api-ws-bun
|
||||
|
||||
### commerce/booking
|
||||
|
||||
### commerce/contacts
|
||||
- [ ] sp-api-plugin-contacts
|
||||
- [x] sp-audience-rules
|
||||
- [x] sp-contacts
|
||||
- [x] sp-macos-contacts
|
||||
|
||||
### communication/emails
|
||||
|
||||
### communication/messaging
|
||||
|
||||
### communication/webhook-client
|
||||
|
||||
### devtools/docker
|
||||
|
||||
### devtools/docs
|
||||
|
||||
### devtools/tooling
|
||||
- [x] sp-cli
|
||||
- [x] sp-electrobun
|
||||
- [x] sp-install-agent-skills
|
||||
- [x] sp-installer
|
||||
- [x] sp-mcp
|
||||
- [x] sp-overview
|
||||
|
||||
### foundation/bitpacker
|
||||
|
||||
### foundation/core
|
||||
|
||||
|
||||
### foundation/data
|
||||
- [x] sp-media
|
||||
- [x] sp-react-packer
|
||||
|
||||
### foundation/etl
|
||||
|
||||
### foundation/feature-flags
|
||||
|
||||
### foundation/flow
|
||||
|
||||
### foundation/random
|
||||
|
||||
### foundation/timing
|
||||
|
||||
### gaming/games
|
||||
|
||||
### geospatial/geo
|
||||
- [x] sp-geo
|
||||
- [x] sp-geo-earth
|
||||
- [x] sp-geo-planet
|
||||
- [x] sp-solar-system
|
||||
- [x] sp-stars
|
||||
- [x] sp-tides
|
||||
- [x] sp-universe
|
||||
|
||||
### geospatial/logistics
|
||||
|
||||
### identity/auth
|
||||
|
||||
### localization/i18n
|
||||
- [x] sp-i18n
|
||||
- [x] sp-i18n-backend-chained
|
||||
- [x] sp-i18n-backend-fs-multifile
|
||||
- [x] sp-i18n-backend-http
|
||||
- [x] sp-i18n-backend-memory
|
||||
- [x] sp-i18n-backend-storage
|
||||
- [x] sp-i18n-detector
|
||||
- [x] sp-i18n-formatter-collator
|
||||
- [x] sp-i18n-formatter-currency
|
||||
- [x] sp-i18n-formatter-date
|
||||
- [x] sp-i18n-formatter-display-name
|
||||
- [x] sp-i18n-formatter-list
|
||||
- [x] sp-i18n-formatter-number
|
||||
- [x] sp-i18n-formatter-relative-time
|
||||
- [x] sp-i18n-formatter-segmenter
|
||||
- [x] sp-i18n-icu-parser
|
||||
- [x] sp-i18n-shared
|
||||
- [x] sp-locale-data
|
||||
- [x] sp-react-i18n
|
||||
|
||||
### media/audio
|
||||
- [x] sp-audio-cli
|
||||
- [x] sp-audio-codec-ffmpeg-native
|
||||
- [x] sp-audio-codec-ffmpeg-wasm
|
||||
- [x] sp-audio-codecs
|
||||
|
||||
### media/audio-fx
|
||||
- [x] sp-audio-fx-plugin-mixer
|
||||
- [x] sp-audio-fx-plugin-phaser
|
||||
- [x] sp-audio-fx-plugin-pitch-shifter
|
||||
- [x] sp-audio-fx-plugin-reverb
|
||||
- [x] sp-audio-fx-plugin-sampler
|
||||
- [x] sp-audio-fx-plugin-spatial
|
||||
- [x] sp-audio-fx-plugin-spessasynth
|
||||
- [x] sp-audio-fx-plugin-synthesizer
|
||||
- [x] sp-audio-fx-plugin-tremolo
|
||||
- [x] sp-audio-fx-plugin-widener
|
||||
|
||||
### media/audio-ir
|
||||
|
||||
### media/fonts
|
||||
|
||||
### media/image
|
||||
- [x] sp-emblem-solver
|
||||
- [x] sp-exif
|
||||
- [x] sp-image-quantisation
|
||||
|
||||
### media/video
|
||||
|
||||
### money/finance
|
||||
|
||||
### money/payment
|
||||
|
||||
### networking/drive
|
||||
|
||||
### networking/events
|
||||
|
||||
### networking/network
|
||||
- [x] sp-network
|
||||
- [x] sp-network-bootstrap
|
||||
- [x] sp-network-discovery-crdt
|
||||
- [x] sp-network-discovery-gundb
|
||||
- [x] sp-network-discovery-hybrid
|
||||
- [x] sp-network-discovery-peerjs
|
||||
- [x] sp-network-discovery-trystero
|
||||
- [x] sp-network-discovery-webtorrent
|
||||
- [x] sp-network-gundb-server
|
||||
- [x] sp-network-libp2p-server
|
||||
- [x] sp-network-plugin-assignment
|
||||
- [x] sp-network-plugin-audit-mesh
|
||||
- [x] sp-network-plugin-auth
|
||||
- [x] sp-network-plugin-circuit-breaker
|
||||
- [x] sp-network-plugin-content-delivery
|
||||
- [x] sp-network-plugin-crypto-messages
|
||||
- [x] sp-network-plugin-devtools
|
||||
- [x] sp-network-plugin-failover
|
||||
- [x] sp-network-plugin-governance
|
||||
- [x] sp-network-plugin-intra-relay
|
||||
- [x] sp-network-plugin-middleware
|
||||
- [x] sp-network-plugin-migration
|
||||
- [x] sp-network-plugin-monitor
|
||||
- [x] sp-network-plugin-partition
|
||||
- [x] sp-network-plugin-protocol
|
||||
- [x] sp-network-plugin-qos
|
||||
- [x] sp-network-plugin-registry
|
||||
- [x] sp-network-plugin-relay
|
||||
- [x] sp-network-plugin-server-bridge
|
||||
- [x] sp-network-plugin-session
|
||||
- [x] sp-network-plugin-simulation
|
||||
- [x] sp-network-plugin-state
|
||||
- [x] sp-network-plugin-topology
|
||||
- [x] sp-network-plugin-transport-manager
|
||||
- [x] sp-network-testing
|
||||
- [x] sp-network-transport-hyperswarm
|
||||
- [x] sp-network-transport-libp2p
|
||||
- [x] sp-network-transport-standby
|
||||
- [x] sp-network-transport-webrtc
|
||||
- [x] sp-network-transport-websockets
|
||||
- [x] sp-network-trystero-server
|
||||
- [x] sp-react-network-libp2p-demo
|
||||
- [x] sp-service-discovery
|
||||
- [x] sp-websocket-gateway
|
||||
|
||||
### observability/monitor
|
||||
|
||||
|
||||
### persistence/cache
|
||||
|
||||
### persistence/database
|
||||
### persistence/search
|
||||
### persistence/storage
|
||||
- [x] sp-storage-browser-indexeddb
|
||||
- [x] sp-storage-browser-localstorage
|
||||
- [x] sp-storage-browser-opfs
|
||||
- [x] sp-storage-cdn
|
||||
- [x] sp-storage-cdn-bunnycdn
|
||||
- [x] sp-storage-cdn-rackspace
|
||||
- [x] sp-storage-file
|
||||
- [x] sp-storage-http
|
||||
- [x] sp-storage-plugin-cache
|
||||
- [x] sp-storage-s3
|
||||
- [x] sp-storage-uri
|
||||
- [x] sp-storage-uri-plugin-bundle
|
||||
- [x] sp-storage-uri-plugin-fallback
|
||||
|
||||
### security-stack/crypto
|
||||
|
||||
### security-stack/security
|
||||
- [x] sp-server-keys
|
||||
- [x] sp-slsa
|
||||
- [x] sp-srp
|
||||
- [x] sp-tpm-struct
|
||||
- [x] sp-vault
|
||||
- [x] sp-vault-sync
|
||||
- [x] sp-x509
|
||||
|
||||
### testkit/testing
|
||||
|
||||
### threejs/r3f
|
||||
|
||||
### ui/forms
|
||||
- [x] sp-form-builder
|
||||
- [x] sp-react-forms
|
||||
|
||||
### ui/react
|
||||
- [x] sp-react-audio
|
||||
- [x] sp-react-audio-fingerprint
|
||||
- [x] sp-react-audio-player
|
||||
- [x] sp-react-audio-stream
|
||||
- [x] sp-react-auth
|
||||
- [x] sp-react-auth-whitelabel
|
||||
- [x] sp-react-control-surface
|
||||
- [x] sp-react-midi
|
||||
- [x] sp-react-ui
|
||||
55
SKILL.md
Normal file
55
SKILL.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: suaveplan-library-navigator
|
||||
description: Navigate the 473+ @suaveplan/* packages. Load sp-<package> skills for deep API context, or search the docs tree for detailed module documentation.
|
||||
---
|
||||
|
||||
# Suaveplan Library Navigator
|
||||
|
||||
This plugin provides natural-language discovery across all `@suaveplan/*` packages in the genesis monorepo. It consists of:
|
||||
|
||||
- **473 package skills** (`sp-<package>`) — distilled architecture, patterns, constraints, and checklists for every library
|
||||
- **Docs tree** — 5,979 `.md` files mirrored from `packages/`, searchable for deep API detail
|
||||
- **`/sp` slash command** — natural-language library finder
|
||||
|
||||
## 4-Tier Doc Hierarchy
|
||||
|
||||
```
|
||||
packages/
|
||||
<category>/README.md → category overview
|
||||
<subcategory>/README.md → subcategory package table
|
||||
<pkg>/README.md → package description + quick-start
|
||||
src/<module>/<module>.md → module-level API docs
|
||||
```
|
||||
|
||||
The docs tree at `.claude/plugins/suaveplan-skills/docs/` replicates this structure with only `.md` files.
|
||||
|
||||
## How to Navigate
|
||||
|
||||
1. **For a specific package** — load its skill: `sp-<package-name>`
|
||||
- Example: `sp-crypto` for `@suaveplan/crypto`
|
||||
- Example: `sp-api` for `@suaveplan/api`
|
||||
- Skills contain: description, architecture, file structure, patterns (code examples), constraints, checklist
|
||||
|
||||
2. **For cross-cutting concerns** — use the `/sp` slash command with a natural-language query
|
||||
- Example: `/sp I need to send SMS messages with retry and rate limiting`
|
||||
- The command searches the docs tree, ranks packages, and extracts code examples
|
||||
|
||||
3. **For deep API detail** — read the module `.md` files in the docs tree
|
||||
- Path: `.claude/plugins/suaveplan-skills/docs/<category>/<subcategory>/<pkg>/src/<module>.md`
|
||||
|
||||
## Skill Quality Tiers
|
||||
|
||||
| Tier | Count | Description |
|
||||
|------|-------|-------------|
|
||||
| **Distilled** | 10 | Manually curated with rich architecture tables, detailed patterns, and constraints (bitpacker family) |
|
||||
| **Baseline** | 463 | Auto-generated from READMEs: accurate descriptions, sibling packages, extracted code examples |
|
||||
|
||||
Baseline skills are regenerated by `scripts/generate-sp-skills.ts` whenever READMEs change. Distilled skills are preserved across regeneration.
|
||||
|
||||
## Syncing Docs
|
||||
|
||||
Run `bun run scripts/sync-sp-docs.ts` to refresh the docs tree from `packages/`.
|
||||
|
||||
## Regenerating Skills
|
||||
|
||||
Run `bun run scripts/generate-sp-skills.ts` to regenerate baseline skills. Existing distilled skills are skipped.
|
||||
868
docs/README.md
Normal file
868
docs/README.md
Normal file
@@ -0,0 +1,868 @@
|
||||
# `packages`
|
||||
|
||||
Suaveplan package tree. Every `@suaveplan/*` library lives under a category (`packages/<category>/`) and a subcategory (`packages/<category>/<subcategory>/`).
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Categories
|
||||
|
||||
18 categories, 472 packages.
|
||||
|
||||
### `api-stack`
|
||||
|
||||
API infrastructure, plugin framework, and HTTP orchestration layer for Suaveplan.
|
||||
|
||||
#### `api`
|
||||
|
||||
API orchestration, plugin lifecycle, routing, workers, and tenant resolution.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/api`](./api-stack/api/api/README.md) | The thin Hono-based application orchestrator for the Suaveplan API stack. Composes the four sibling packages — `@suaveplan/api-plugin`, `@suaveplan/api-tenant`… |
|
||||
| [`@suaveplan/api-2pc`](./api-stack/api/api-2pc/README.md) | Two-phase commit coordinator for the Suaveplan API plugin system. Drives Postgres PREPARE TRANSACTION / COMMIT PREPARED / ROLLBACK PREPARED across registered p… |
|
||||
| [`@suaveplan/api-audit-file`](./api-stack/api/api-audit-file/README.md) | File-rotating, HMAC-signed audit sink for `@suaveplan/api-host`. Implements `RequestAuditSink` from `@suaveplan/api-host/request-pipeline`: writes one JSON lin… |
|
||||
| [`@suaveplan/api-audit-stdout`](./api-stack/api/api-audit-stdout/README.md) | Stdout audit sink for `@suaveplan/api-host`. Writes JSON-per-line audit records to a `LineWriter` (default `process.stdout`) with an HMAC-SHA-256 signature app… |
|
||||
| [`@suaveplan/api-auth`](./api-stack/api/api-auth/README.md) | **$2** for the Suaveplan API plugin system. Defines the abstract session-resolver shape (`Request → Principal \| null`) used by the host on every request, plus… |
|
||||
| [`@suaveplan/api-auth-anonymous`](./api-stack/api/api-auth-anonymous/README.md) | Stage-4 trivial session resolver for `@suaveplan/api`. Always returns `null` for every request. Use this as the tail of resolver chains for public endpoints, a… |
|
||||
| [`@suaveplan/api-auth-bearer-static`](./api-stack/api/api-auth-bearer-static/README.md) | Stage-4 service-to-service `SessionResolver` adapter for the `@suaveplan/api` plugin system. Matches an inbound `Authorization: Bearer <token>` header against… |
|
||||
| [`@suaveplan/api-auth-better-auth`](./api-stack/api/api-auth-better-auth/README.md) | Stage-6 Better-Auth adapter for `@suaveplan/api`. Ships two **independently-wireable** capability surfaces — `createBetterAuthSession` (implements `auth.sessio… |
|
||||
| [`@suaveplan/api-auth-jwt`](./api-stack/api/api-auth-jwt/README.md) | Stage-4 concrete `SessionResolver` for the `@suaveplan/api` plugin system. Verifies a JWT presented on the `Authorization: Bearer <jwt>` header (or a configure… |
|
||||
| [`@suaveplan/api-auth-provider`](./api-stack/api/api-auth-provider/README.md) | Abstract account-lifecycle contract for the Suaveplan API plugin system (contracts). This package is OPTIONAL: hosted-IdP deployments (Clerk, Auth0, Cognito) i… |
|
||||
| [`@suaveplan/api-auth-rolled`](./api-stack/api/api-auth-rolled/README.md) | Stage-8.12 rolled session-token resolver for `@suaveplan/api`. Issues RS256-signed JWTs with rotating `kid` slots, verifies tokens through the rotator's active… |
|
||||
| [`@suaveplan/api-auth-static-principal`](./api-stack/api/api-auth-static-principal/README.md) | Fixed-`Principal` session resolver for the `@suaveplan/api` plugin system. Returns the configured `Principal` verbatim on every request — no token decoding, no… |
|
||||
| [`@suaveplan/api-db-adapter`](./api-stack/api/api-db-adapter/README.md) | Stage-4 bridge from the `@suaveplan/db` engine adapters to the plugin `AdminDatabaseLike` / `TenantScopedDatabaseLike` surfaces exposed by `@suaveplan/api-type… |
|
||||
| [`@suaveplan/api-db-sqlite-memory`](./api-stack/api/api-db-sqlite-memory/README.md) | Stage-5 in-memory SQLite database adapter for the `@suaveplan/api` plugin host testing kit. Wraps `bun:sqlite` (in `:memory:` mode by default, or a file path f… |
|
||||
| [`@suaveplan/api-db-worker-proxy`](./api-stack/api/api-db-worker-proxy/README.md) | Worker-side proxy for the api `db` capability. Marshals `AdminDatabaseLike` and `TenantScopedDatabaseLike` calls — including transactions and cursor streams —… |
|
||||
| [`@suaveplan/api-events-in-memory`](./api-stack/api/api-events-in-memory/README.md) | In-memory events bus adapter for the `@suaveplan/api` testing kit. Wraps `@suaveplan/events`' production `EventBus` with a per-topic recording layer so tests c… |
|
||||
| [`@suaveplan/api-events-worker-proxy`](./api-stack/api/api-events-worker-proxy/README.md) | Worker-tier proxy over a main-process `@suaveplan/events` `EventBus`. |
|
||||
| [`@suaveplan/api-graphql`](./api-stack/api/api-graphql/README.md) | Stage-1 abstract GraphQL contract for the Suaveplan API plugin system. |
|
||||
| [`@suaveplan/api-graphql-apollo`](./api-stack/api/api-graphql-apollo/README.md) | Stage-7 Apollo Server GraphQL engine adapter for the Suaveplan API plugin system. Implements the `GraphqlEngine` contract from `@suaveplan/api-graphql` and pas… |
|
||||
| [`@suaveplan/api-graphql-ui-altair`](./api-stack/api/api-graphql-ui-altair/README.md) | Altair GraphQL UI plugin for the Suaveplan API plugin system. Structural sibling of `@suaveplan/api-graphql-ui-graphiql`. |
|
||||
| [`@suaveplan/api-graphql-ui-graphiql`](./api-stack/api/api-graphql-ui-graphiql/README.md) | GraphiQL UI mount for the Suaveplan API plugin system. Stage 7 of the umbrella plan. |
|
||||
| [`@suaveplan/api-graphql-yoga`](./api-stack/api/api-graphql-yoga/README.md) | Stage-7 GraphQL Yoga engine adapter. **Default GraphQL engine** for the Suaveplan API plugin system per design doc §10.5 — Bun-friendly, fastest for our usage… |
|
||||
| [`@suaveplan/api-host`](./api-stack/api/api-host/README.md) | Host orchestration core for the Suaveplan API plugin system. Provides the stable error-code registry (HOST/PLUGIN/RELOAD/CAPABILITY/ADMIN/LEDGER), the author-facing definePlugin DX, the runtime CapabilityRegistry, the LifecycleDriver + ReloadDriver + PluginLedger + admin Hono sub-router, the ErrorBoundary, the §6 11-step RequestPipeline, the top-level createHost(config) entry point composing every internal driver, and the HostInstruments registry covering the §12.3 metric surface. The ./testing subpath is reserved for test utilities. |
|
||||
| [`@suaveplan/api-jobs-worker-proxy`](./api-stack/api/api-jobs-worker-proxy/README.md) | Bidirectional `MessagePort` proxy that lets a worker-isolated API plugin participate in the host's main-process job scheduler. The worker calls `registerCron`… |
|
||||
| [`@suaveplan/api-lifecycle`](./api-stack/api/api-lifecycle/README.md) | Stage-1 contract package for the Suaveplan API plugin system: the plugin-lifecycle finite state machine definition, the legal-transition table, the side-effect… |
|
||||
| [`@suaveplan/api-loader`](./api-stack/api/api-loader/README.md) | Stage-1 contract package for the Suaveplan API plugin system. |
|
||||
| [`@suaveplan/api-logger-worker-proxy`](./api-stack/api/api-logger-worker-proxy/README.md) | Stage-3 worker → main proxy for `@suaveplan/logger`. Lets a worker-isolated API plugin call a normal `LoggerLike` surface while the records cross a `MessagePor… |
|
||||
| [`@suaveplan/api-manifest`](./api-stack/api/api-manifest/README.md) | Stage-1 contract package of the Suaveplan API plugin system: Zod schemas, parser, cross-field validators, dependency-graph resolver, capability resolver, and c… |
|
||||
| [`@suaveplan/api-migrations`](./api-stack/api/api-migrations/README.md) | Database-agnostic migration runner for the Suaveplan API. Ships a narrow `MigrationAdapter` surface (six methods), three shipped adapters (`surrealql`, `postgr… |
|
||||
| [`@suaveplan/api-openapi`](./api-stack/api/api-openapi/README.md) | OpenAPI 3.1 registry, typed `RouteBuilder`, schema registry, and document merger for the Suaveplan API. Single source of truth that wires into Hono via `OpenAP… |
|
||||
| [`@suaveplan/api-openapi-ui-redoc`](./api-stack/api/api-openapi-ui-redoc/README.md) | Stage-8 ReDoc UI adapter for the Suaveplan api-host. Mounts the ReDoc HTML shell at `/docs/ui/redoc` (or any caller-chosen path) via `createRedocUiHandler` and… |
|
||||
| [`@suaveplan/api-openapi-ui-scalar`](./api-stack/api/api-openapi-ui-scalar/README.md) | Scalar UI adapter for the Suaveplan OpenAPI plugin system. |
|
||||
| [`@suaveplan/api-openapi-ui-swagger`](./api-stack/api/api-openapi-ui-swagger/README.md) | Swagger UI adapter for the `/docs/ui/swagger` endpoint of the Suaveplan API plugin system (Stage 8.8). Returns a pure HTTP handler whose response is a `text/ht… |
|
||||
| [`@suaveplan/api-plugin`](./api-stack/api/api-plugin/README.md) | Plugin-system foundation for the Suaveplan API. Universal — runs unchanged in Bun, Node, and the browser. |
|
||||
| [`@suaveplan/api-plugin-audit`](./api-stack/api/api-plugin-audit/README.md) | Hono plugin that wraps `@suaveplan/audit-logging` as a write-through audit trail for `@suaveplan/api`. Installs a global per-request audit middleware (method /… |
|
||||
| [`@suaveplan/api-plugin-auth`](./api-stack/api/api-plugin-auth/README.md) | Better-Auth-first authentication plugin for `@suaveplan/api`. The canonical exemplar of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` packag… |
|
||||
| [`@suaveplan/api-plugin-campaigns`](./api-stack/api/api-plugin-campaigns/README.md) | Campaigns + suppression + delivery-webhooks HTTP plugin for `@suaveplan/api`. Mounts the `/campaigns`, `/suppression`, and `/webhooks` route trees on a Hono `O… |
|
||||
| [`@suaveplan/api-plugin-cms`](./api-stack/api/api-plugin-cms/README.md) | Headless content-management primitives for `@suaveplan/api`. Ships an in-memory `CmsEngine` that manages flexible content models, content entries with a `draft… |
|
||||
| [`@suaveplan/api-plugin-database`](./api-stack/api/api-plugin-database/README.md) | SurrealDB-backed database plugin for `@suaveplan/api`. Pools `@suaveplan/surrealdb`'s `SurrealAdapter` (implementing `@suaveplan/db`'s `DatabaseAdapter`), runs… |
|
||||
| [`@suaveplan/api-plugin-event-bus`](./api-stack/api/api-plugin-event-bus/README.md) | Typed event-bus plugin for `@suaveplan/api`. Wraps `@suaveplan/events` (pub/sub, async dispatch, interceptors, bounded-queue backpressure, idempotent listener… |
|
||||
| [`@suaveplan/api-plugin-events`](./api-stack/api/api-plugin-events/README.md) | Event-management plugin for `@suaveplan/api`. Ships a SurrealDB-backed `EventRepository` + `RsvpRepository` pair, ten Hono OpenAPI routes (list / get / create… |
|
||||
| [`@suaveplan/api-plugin-example`](./api-stack/api/api-plugin-example/README.md) | Reference plugin for the `@suaveplan/api-host` plugin system. The package is a load-bearing integration target, not a tutorial: every contract surface the host… |
|
||||
| [`@suaveplan/api-plugin-export`](./api-stack/api/api-plugin-export/README.md) | Data-export plugin for `@suaveplan/api`. Turns any registered resource into a CSV (RFC 4180), JSON, or XLSX (SpreadsheetML XML) blob through three self-contain… |
|
||||
| [`@suaveplan/api-plugin-file-upload`](./api-stack/api/api-plugin-file-upload/README.md) | Multipart and chunked file-upload plugin for `@suaveplan/api`. Ships six OpenAPI-typed Hono routes, a pluggable storage + session-store contract, SHA-256 hash… |
|
||||
| [`@suaveplan/api-plugin-graphql`](./api-stack/api/api-plugin-graphql/README.md) | GraphQL description + resolver-composition layer for `@suaveplan/api`. Server-tier — runs in Bun and Node. |
|
||||
| [`@suaveplan/api-plugin-import`](./api-stack/api/api-plugin-import/README.md) | Tabular-data ingest plugin for `@suaveplan/api`. Parses CSV / JSON / TSV-XLSX payloads, applies declarative column mappings + per-field validation + computed f… |
|
||||
| [`@suaveplan/api-plugin-notifications`](./api-stack/api/api-plugin-notifications/README.md) | SurrealDB-backed notifications + push plugin for `@suaveplan/api`. Ships a `NotificationRepository` + `DeviceTokenRepository` pair, an `ApnService` with a plug… |
|
||||
| [`@suaveplan/api-plugin-payment`](./api-stack/api/api-plugin-payment/README.md) | Payment-lifecycle plugin for `@suaveplan/api`. Wraps `@suaveplan/payment` + a caller-injected `PaymentProvider` (Stripe / Adyen / PayPal / Square / Mollie / cu… |
|
||||
| [`@suaveplan/api-plugin-rate-limit`](./api-stack/api/api-plugin-rate-limit/README.md) | Rate-limit plugin for `@suaveplan/api`. Wraps `@suaveplan/rate-limiter` as a `definePlugin` descriptor exposing per-route + per-tenant configuration, parameter… |
|
||||
| [`@suaveplan/api-plugin-search`](./api-stack/api/api-plugin-search/README.md) | Full-text search plugin for `@suaveplan/api`. Brings TF-weighted relevance scoring, four built-in text analyzers (`standard`, `simple`, `whitespace`, `keyword`… |
|
||||
| [`@suaveplan/api-plugin-site`](./api-stack/api/api-plugin-site/README.md) | Multi-site / multi-domain management plugin for `@suaveplan/api`. Stores `site` records (name, slug, theme, settings, push config) in SurrealDB, resolves incom… |
|
||||
| [`@suaveplan/api-plugin-sse`](./api-stack/api/api-plugin-sse/README.md) | Server-Sent Events plugin for `@suaveplan/api`. Registers a typed `sseService` in the host `ServiceRegistry`, mounts a Hono streaming handler at `GET /sse/:cha… |
|
||||
| [`@suaveplan/api-plugin-tenant`](./api-stack/api/api-plugin-tenant/README.md) | Multi-tenancy plugin for `@suaveplan/api`. Wraps `@suaveplan/api-tenant`'s resolver chain (header / subdomain / domain / path / JWT / query / cookie) as a firs… |
|
||||
| [`@suaveplan/api-plugin-user`](./api-stack/api/api-plugin-user/README.md) | User-profile CRUD plugin for `@suaveplan/api`. Mounts the canonical `/users/me` GET/PATCH/DELETE surface on top of `@suaveplan/api-plugin-auth`, registers a ty… |
|
||||
| [`@suaveplan/api-plugin-vault`](./api-stack/api/api-plugin-vault/README.md) | Per-user vault HTTP plugin for `@suaveplan/api`. Third instance of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` package follows: the plugin… |
|
||||
| [`@suaveplan/api-plugin-webhook`](./api-stack/api/api-plugin-webhook/README.md) | Signed outbound webhook delivery plugin for `@suaveplan/api`. Wraps `@suaveplan/webhook-client` behind a `definePlugin` descriptor, registers a typed `webhookS… |
|
||||
| [`@suaveplan/api-rate-limit`](./api-stack/api/api-rate-limit/README.md) | Per-route / per-principal / per-tenant / per-IP token-bucket rate-limit middleware for `@suaveplan/api`. Implements the `RequestRateLimiter` contract consumed… |
|
||||
| [`@suaveplan/api-rbac`](./api-stack/api/api-rbac/README.md) | Scope + rule-based authorisation middleware for the Suaveplan API plugin system. Produces a framework-agnostic `Middleware` that consumes the resolved `Princip… |
|
||||
| [`@suaveplan/api-router`](./api-stack/api/api-router/README.md) | Stage-1 contract package for the Suaveplan API plugin system. |
|
||||
| [`@suaveplan/api-router-hono`](./api-stack/api/api-router-hono/README.md) | Stage-4 concrete Hono router adapter for `@suaveplan/api`. Implements the abstract `Router` contract from `@suaveplan/api-router` on top of a Hono `Hono` insta… |
|
||||
| [`@suaveplan/api-router-in-memory`](./api-stack/api/api-router-in-memory/README.md) | Stage-5 in-memory router adapter for `@suaveplan/api`. Implements both the abstract `Router` contract from `@suaveplan/api-router` AND the `PipelineRouter` con… |
|
||||
| [`@suaveplan/api-router-worker-bridge`](./api-stack/api/api-router-worker-bridge/README.md) | Stage-3 HTTP bridge that marshals Fetch `Request` and `Response` objects across the worker boundary. Pairs a main-side `dispatch` with a worker- side handler o… |
|
||||
| [`@suaveplan/api-saga`](./api-stack/api/api-saga/README.md) | Saga orchestrator for the Suaveplan API plugin system. Drives manifest-declared multi-plugin workflows in declared order with reverse-order compensation on fai… |
|
||||
| [`@suaveplan/api-sandbox-permissions`](./api-stack/api/api-sandbox-permissions/README.md) | Permission model for sandboxed-tier API plugins. Centralises the `permissions.fs[]` / `permissions.fetch[]` allowlists, the `permissions.budgets` enforcement,… |
|
||||
| [`@suaveplan/api-sandbox-vm`](./api-stack/api/api-sandbox-vm/README.md) | `vm.Context`-based sandbox host for Suaveplan API plugins running in the sandboxed tier. The package owns the `vm.Context`+frozen-globals half of the design do… |
|
||||
| [`@suaveplan/api-sandbox-vm-host-bridge`](./api-stack/api/api-sandbox-vm-host-bridge/README.md) | Host-integration bridge that lets a `VmSandboxHandle` from `@suaveplan/api-sandbox-vm` install into `@suaveplan/api-host` as a first-class plugin. |
|
||||
| [`@suaveplan/api-sandbox-wasm`](./api-stack/api/api-sandbox-wasm/README.md) | Stage-8 WebAssembly sandbox host. Instantiates a sandboxed-tier plugin WASM module against a WASI snapshot preview1 implementation derived from the plugin's `P… |
|
||||
| [`@suaveplan/api-sandbox-wasm-host-bridge`](./api-stack/api/api-sandbox-wasm-host-bridge/README.md) | Host-integration layer that wraps a `@suaveplan/api-sandbox-wasm` `WasmSandboxHandle` as a `@suaveplan/api-host` `PluginModule`. Sandboxed-tier plugins compile… |
|
||||
| [`@suaveplan/api-schemas`](./api-stack/api/api-schemas/README.md) | Shared Zod schemas describing Suaveplan API request/response shapes. The single source of truth for server plugins, React data hooks, and generated OpenAPI doc… |
|
||||
| [`@suaveplan/api-secrets`](./api-stack/api/api-secrets/README.md) | Abstract Secrets contract for the Suaveplan API plugin system — per-plugin secret access typed against `manifest.secrets[]`, canonical redaction marker, frozen… |
|
||||
| [`@suaveplan/api-secrets-bun`](./api-stack/api/api-secrets-bun/README.md) | Stage-8 `SecretsLike` adapter backed by `Bun.secrets` (macOS Keychain, Linux libsecret, Windows Credential Manager) with per-plugin allow-list enforcement, OS-… |
|
||||
| [`@suaveplan/api-secrets-env`](./api-stack/api/api-secrets-env/README.md) | Stage-4 `SecretsLike` adapter backed by `process.env` / `Bun.env` with per-plugin allowed-keys enforcement. |
|
||||
| [`@suaveplan/api-secrets-vault`](./api-stack/api/api-secrets-vault/README.md) | Stage-8 `SecretsLike` adapter backed by `@suaveplan/vault`. The vault is passed in already unlocked by the host's startup sequence; the adapter never sees the… |
|
||||
| [`@suaveplan/api-secrets-worker-proxy`](./api-stack/api/api-secrets-worker-proxy/README.md) | Stage-3 worker → main `SecretsLike` proxy for the API plugin system. |
|
||||
| [`@suaveplan/api-sse`](./api-stack/api/api-sse/README.md) | Abstract Server-Sent Events contracts for `@suaveplan/api`. Stage-1 contract package: defines the engine-side `SseConnection`, `SseBroker`, `ReplayBuffer` (Las… |
|
||||
| [`@suaveplan/api-sse-bun`](./api-stack/api/api-sse-bun/README.md) | `Bun.serve`-backed Server-Sent Events engine for the `@suaveplan/api` plugin system. Implements the abstract `@suaveplan/api-sse` contract and passes `runSseEn… |
|
||||
| [`@suaveplan/api-tenant`](./api-stack/api/api-tenant/README.md) | Multi-tenant resolution + scoping for the Suaveplan API. Server-tier — runs on Bun and Node, depends on `hono` and `node:async_hooks`. |
|
||||
| [`@suaveplan/api-tenant-provider`](./api-stack/api/api-tenant-provider/README.md) | Abstract `TenantProvider` contract for the Suaveplan API plugin system. Enumerates the universe of tenants known to a host so per-tenant cron / queued jobs (ma… |
|
||||
| [`@suaveplan/api-tenant-provider-memory`](./api-stack/api/api-tenant-provider-memory/README.md) | In-memory reference implementation of the `@suaveplan/api-tenant-provider` `TenantProvider` contract. Backed by an insertion-ordered `Map<TenantId, TenantRecor… |
|
||||
| [`@suaveplan/api-tenant-resolver-header`](./api-stack/api/api-tenant-resolver-header/README.md) | Stage-8 tenant resolver. Implements the `TenantResolver` contract from `@suaveplan/api-types/tenant-resolver` by reading the tenant identifier from a configura… |
|
||||
| [`@suaveplan/api-tenant-resolver-jwt`](./api-stack/api/api-tenant-resolver-jwt/README.md) | Stage-4 tenant resolver for the Suaveplan API plugin system. Reads the already-resolved `Principal.tenantId` (attached by an earlier `api-auth-*` chain step) a… |
|
||||
| [`@suaveplan/api-tenant-resolver-subdomain`](./api-stack/api/api-tenant-resolver-subdomain/README.md) | Stage-8 tenant resolver for the Suaveplan API plugin system. Reads the inbound `Host` header (and optionally a CIDR-trusted `X-Forwarded-Host`), strips a confi… |
|
||||
| [`@suaveplan/api-types`](./api-stack/api/api-types/README.md) | Root contract-types package for the Suaveplan API plugin system. Defines every cross-cutting type the foundation packages (`api-manifest`, `api-router`, `api-h… |
|
||||
| [`@suaveplan/api-worker-host`](./api-stack/api/api-worker-host/README.md) | Worker-side runtime that hosts exactly one Suaveplan API plugin inside an isolated `Worker` thread. The main-process counterpart is `@suaveplan/api-host`; this… |
|
||||
| [`@suaveplan/api-worker-rpc`](./api-stack/api/api-worker-rpc/README.md) | MessagePort-based RPC protocol for worker-isolated API plugins. Provides the shared protocol — Zod-validated wire frames, monotonic correlation ids, AbortSigna… |
|
||||
| [`@suaveplan/api-ws`](./api-stack/api/api-ws/README.md) | Abstract WebSocket engine contract for the Suaveplan API foundation — Connection, ConnectionRegistry, SubscriptionBus, WsBroker, reconnect-token envelope, backpressure + heartbeat policies, cross-tenant capability gate, and a runWsEngineConformance harness. Stage-1 contracts only; concrete engines (api-ws-bun) and brokers (api-ws-broker-redis / -in-memory) plug behind these interfaces. |
|
||||
| [`@suaveplan/api-ws-broker-in-memory`](./api-stack/api/api-ws-broker-in-memory/README.md) | Single-host in-process implementation of the `@suaveplan/api-ws` `WsBroker` contract. Suitable for production single-host deployments AND as the default broker… |
|
||||
| [`@suaveplan/api-ws-broker-redis`](./api-stack/api/api-ws-broker-redis/README.md) | Cross-host Redis pub/sub broker for `@suaveplan/api-ws` and `@suaveplan/api-sse`. |
|
||||
| [`@suaveplan/api-ws-bun`](./api-stack/api/api-ws-bun/README.md) | Bun.serve-backed WebSocket engine for the `@suaveplan/api-ws` Stage-1 abstract contract. The engine composes the per-connection handshake pipeline, registry, s… |
|
||||
| [`@suaveplan/wasm-compiler-javy`](./api-stack/api/wasm-compiler-javy/README.md) | TS->WASM compiler closing **Stage-8 gate 2.8**. Thin orchestrator around `Bun.build` (bundle) + an injected WASI-stdio ABI shim + **Javy v8.1.1** (JS->WASM) +… |
|
||||
|
||||
### `commerce`
|
||||
|
||||
Booking engines, contact management, payment plugins, and commerce primitives.
|
||||
|
||||
#### `booking`
|
||||
|
||||
Universal booking engine with availability, conflict detection, and pricing.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/booking`](./commerce/booking/booking/README.md) | Universal, JSON-configurable booking engine with pluggable strategies for availability resolution, conflict detection, pricing calculation, and rule evaluation… |
|
||||
|
||||
#### `contacts`
|
||||
|
||||
Contact and group management with deduplication and import/export.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/api-plugin-contacts`](./commerce/contacts/api-plugin-contacts/README.md) | Contacts and Groups HTTP plugin for `@suaveplan/api`. Mounts the `/contacts` and `/groups` route trees on a Hono `OpenAPIHono`, backed by SurrealDB implementat… |
|
||||
| [`@suaveplan/audience-rules`](./commerce/contacts/audience-rules/README.md) | Universal audience segmentation: rule-based filtering on contact data with 14 comparison operators, AND/OR chaining, dot-notation field resolution including ar… |
|
||||
| [`@suaveplan/contacts`](./commerce/contacts/contacts/README.md) | Universal contact and group management with hierarchical nesting, audience resolution, contact merger, and CSV/JSON import-export. Multi-tenant, channel-agnost… |
|
||||
| [`@suaveplan/macos-contacts`](./commerce/contacts/macos-contacts/README.md) | Universal bridge to macOS Contacts.app via AppleScript. Reads contacts and groups through the platform `osascript` binary, parses the output into typed records… |
|
||||
|
||||
### `communication`
|
||||
|
||||
Email delivery, messaging channels, and webhook clients.
|
||||
|
||||
#### `emails`
|
||||
|
||||
Email delivery, DMARC parsing, templates, campaigns, and suppression lists.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/campaigns`](./communication/emails/campaigns/README.md) | Universal campaign orchestration. |
|
||||
| [`@suaveplan/delivery-tracking`](./communication/emails/delivery-tracking/README.md) | Channel-agnostic delivery event tracking for campaigns and messages: lifecycle dedupe, in-memory + SurrealDB stores, time-series aggregation, tracking pixel an… |
|
||||
| [`@suaveplan/dmarc`](./communication/emails/dmarc/README.md) | DMARC report parser for `@suaveplan` — parses aggregate (RUA) and forensic (RUF) DMARC XML reports into validated, strongly-typed structures. Accepts raw XML s… |
|
||||
| [`@suaveplan/domain-normalizer`](./communication/emails/domain-normalizer/README.md) | Normalisation, matching, and alias-canonicalisation for internet domain names. Given heterogeneous URL or domain input — including common mobile/CDN subdomains… |
|
||||
| [`@suaveplan/email-processing`](./communication/emails/email-processing/README.md) | Universal MIME parsing and email processing for the Suaveplan . Wraps `postal-mime` and `libmime` with structured errors, OpenTelemetry instrumentation, a hook… |
|
||||
| [`@suaveplan/email-templates`](./communication/emails/email-templates/README.md) | Universal, React-free email template engine. Produces email-safe HTML strings with inline styles via VNode factories — runs identically in Bun, Node, and the b… |
|
||||
| [`@suaveplan/emailer`](./communication/emails/emailer/README.md) | Universal email service with pluggable transports. Ships `EmailService`, three transports (Dummy / SMTP via injectable `SmtpClientLike` / Gmail REST via inject… |
|
||||
| [`@suaveplan/suppression`](./communication/emails/suppression/README.md) | Email suppression list management for Suaveplan. Stores and checks email addresses that should not receive outgoing mail — unsubscribes, hard/soft bounces, com… |
|
||||
|
||||
#### `messaging`
|
||||
|
||||
Messaging adapters — Discord, Slack, Telegram, SMS, iMessage, and notifications.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/discord-client`](./communication/messaging/discord-client/README.md) | Generic Discord API client for webhooks, bot messaging, and embeds. Runtime-agnostic via injectable `fetch` and `clock`. Maps every documented Discord failure… |
|
||||
| [`@suaveplan/imessage-adapter`](./communication/messaging/imessage-adapter/README.md) | macOS iMessage SQLite chat.db watcher and `MessageSourceAdapter` implementation. Polls Apple's `~/Library/Messages/chat.db`, decodes Apple-epoch / nanosecond t… |
|
||||
| [`@suaveplan/notification-channel`](./communication/messaging/notification-channel/README.md) | Multi-channel notification router with templates, sliding-window rate limiting, retry + dead-letter, scheduler, preferences, hooks, OpenTelemetry instrumentati… |
|
||||
| [`@suaveplan/slack-client`](./communication/messaging/slack-client/README.md) | Typed, deterministic Slack client — Web API + incoming-webhook surface — with Zod-validated boundaries, OpenTelemetry-instrumented hot paths, injectable `Clock… |
|
||||
| [`@suaveplan/sms`](./communication/messaging/sms/README.md) | Universal SMS core: provider-agnostic send pipeline with strict E.164 validation, Zod boundaries, template rendering, opt-out registry, token-bucket rate limit… |
|
||||
| [`@suaveplan/sms-providers`](./communication/messaging/sms-providers/README.md) | Universal SMS provider implementations for @suaveplan/sms. Real HTTP clients for Twilio (Basic auth + form-encoded + X-Twilio-Signature HMAC-SHA1), Vonage (HMAC-SHA256 webhook), MessageBird (AccessKey + JWT HS256 webhook), Plivo (Basic auth + X-Plivo-Signature-V3 HMAC-SHA256), and AWS SNS (inline SigV4 + RSA-SHA1 SNS notification verifier). Ships ProviderRegistry, httpRequest over injectable FetchLike, composeAbortSignal, SMS_PROVIDERS_CODES + SmsProvidersError, per-provider mapStatus, parseInbound, parseOptOutKeyword, and five OTel instruments. No provider SDK deps; every HTTP call flows through an injectable FetchLike seam. |
|
||||
| [`@suaveplan/telegram-client`](./communication/messaging/telegram-client/README.md) | Telegram Bot API client: send/edit/delete messages, six media variants, inline + custom keyboards, typed errors, injectable `fetch` + `clock`, and OpenTelemetr… |
|
||||
|
||||
#### `webhook-client`
|
||||
|
||||
Webhook delivery client with retry logic and HMAC verification.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/webhook-client`](./communication/webhook-client/webhook-client/README.md) | Webhook delivery client with retry logic, HMAC signature generation/verification, payload transformation, and delivery tracking. Runtime-agnostic (injectable f… |
|
||||
|
||||
### `devtools`
|
||||
|
||||
Developer tooling, documentation generators, Docker helpers, and build utilities.
|
||||
|
||||
#### `docker`
|
||||
|
||||
Docker helpers and GPU runtime configuration.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/docker-nvidia`](./devtools/docker/docker-nvidia/README.md) | A collection of docker / docker-compose scripts configured for use with WSL / Nvidia |
|
||||
|
||||
#### `docs`
|
||||
|
||||
Documentation utilities, SVG compression, PDF handling, and sitemap generation.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/compress-svg`](./devtools/docs/compress-svg/README.md) | SVG optimisation and minification for TypeScript. Pure TypeScript — no DOMParser, no external SVG library. Runs in Bun, Node.js, browsers, and edge runtimes. |
|
||||
| [`@suaveplan/docs`](./devtools/docs/docs/README.md) | Server-tier markdown documentation toolkit for Suaveplan. Discovers every `.md` file under a repo root, parses each via `gray-matter` + `remark` into a structu… |
|
||||
| [`@suaveplan/pdf`](./devtools/docs/pdf/README.md) | Server-tier PDF 1.4 generation toolkit. **Chunk A foundation** — pure utilities + cross-cutting wiring (typed error registry, Zod boundary schemas, OpenTelemet… |
|
||||
| [`@suaveplan/sitemap`](./devtools/docs/sitemap/README.md) | Sitemap generation and parsing for the sitemaps.org XML protocol. Produces valid `sitemap.xml`, sitemap index documents, and parses existing sitemaps. Supports… |
|
||||
|
||||
#### `tooling`
|
||||
|
||||
CLI frameworks, installers, and build tooling.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/cli`](./devtools/tooling/cli/README.md) | Enterprise-grade CLI framework for Node.js. Composable fluent API with subcommand routing, option parsing, OTel tracing, and a full set of terminal UI primitiv… |
|
||||
| [`@suaveplan/electrobun`](./devtools/tooling/electrobun/README.md) | Electrobun desktop-app generator and orchestrator for the SuavePlan ecosystem. Scaffolds a fresh Electrobun project from a layered config + prompt flow, drives… |
|
||||
| [`@suaveplan/install-agent-skills`](./devtools/tooling/install-agent-skills/README.md) | Installer plugin that bundles every Suaveplan skill (`SKILL.md` + assets) and installs them into a target project via `@suaveplan/installer`. |
|
||||
| [`@suaveplan/installer`](./devtools/tooling/installer/README.md) | Plugin host for project boilerplating and asset installation. Pipelines plugins through `detect → prompts → plan → apply → postInstall` with transactional roll… |
|
||||
| [`@suaveplan/javy-cli-darwin-arm64`](./devtools/tooling/javy-cli-darwin-arm64/README.md) | Platform wrapper for **Javy v8.1.1** on `darwin-arm64`. |
|
||||
| [`@suaveplan/javy-cli-darwin-x64`](./devtools/tooling/javy-cli-darwin-x64/README.md) | Platform wrapper for **Javy v8.1.1** on `darwin-x64`. |
|
||||
| [`@suaveplan/javy-cli-linux-arm64`](./devtools/tooling/javy-cli-linux-arm64/README.md) | Platform wrapper for **Javy v8.1.1** on `linux-arm64`. |
|
||||
| [`@suaveplan/javy-cli-linux-x64`](./devtools/tooling/javy-cli-linux-x64/README.md) | Platform wrapper for **Javy v8.1.1** on `linux-x64`. |
|
||||
| [`@suaveplan/javy-cli-win32-x64`](./devtools/tooling/javy-cli-win32-x64/README.md) | Platform wrapper for **Javy v8.1.1** on `win32-x64`. |
|
||||
| [`@suaveplan/linter`](./devtools/tooling/linter/README.md) | Unified markdown + biome linting pipeline for Suaveplan: a remark-based markdown fixer covering MD012/022/025/029/031/032/035/036/040/044, an injectable `Comma… |
|
||||
| [`@suaveplan/mcp`](./devtools/tooling/mcp/README.md) | Universal Model Context Protocol (MCP) server + client with a pluggable transport stack, JSON-RPC dispatcher, AbortSignal cancellation, progress notifications,… |
|
||||
| [`@suaveplan/overview`](./devtools/tooling/overview/README.md) | Machine-readable Suaveplan project manifest: typed PackageRecord / SuavePlanManifest / WorkspaceGraph, Zod schemas, tier inference, filesystem discovery (server sub-path), graph utilities (topo, cycle detection, transitive closure, leaves, roots), plugin/category/tier roll-ups, six OVERVIEW.* error codes, three OTel counters + one histogram. |
|
||||
| [`@suaveplan/profiler`](./devtools/tooling/profiler/README.md) | Universal runtime performance profiler. |
|
||||
| [`@suaveplan/readline`](./devtools/tooling/readline/README.md) | AsyncGenerator line reader and interactive prompt interface for Node.js streams, file paths, and Web Streams. |
|
||||
| [`@suaveplan/sandbox`](./devtools/tooling/sandbox/README.md) | Server-tier sandboxed code execution for `@suaveplan` plugin hosts and agent runtimes. Ships four `SandboxEngine` implementations (a `node:vm` default, real `n… |
|
||||
| [`@suaveplan/test-vite`](./devtools/tooling/test-vite/README.md) | Vite + Vitest configuration factories and validators for Suaveplan: `createViteLibraryConfig`, `createViteAppConfig`, `createVitestConfig`, `createVitestBrowse… |
|
||||
|
||||
### `foundation`
|
||||
|
||||
Foundational primitives — types, errors, configuration, data structures, and cross-cutting utilities.
|
||||
|
||||
#### `barcode`
|
||||
|
||||
1-D barcode generation and parsing.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/barcode`](./foundation/barcode/barcode/README.md) | Runtime-agnostic 1D barcode generation for seven symbologies — Code 128B, Code 39, EAN-13, EAN-8, UPC-A, UPC-E, and ITF. Outputs inline SVG strings and binary… |
|
||||
| [`@suaveplan/qrcode`](./foundation/barcode/qrcode/README.md) | QR code generation producing SVG, pixel matrix, terminal (Unicode blocks), and data URL representations. Implements QR code versions 1-10, error correction lev… |
|
||||
|
||||
#### `bitpacker`
|
||||
|
||||
Bit-level binary packing, schemas, compression, and streaming codecs.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/bitpacker`](./foundation/bitpacker/bitpacker/README.md) | Core bit-level binary packing library. Provides `BitWriter`, `BitReader`, `BitPacker`, and a pluggable strategy system for schema-driven serialization. |
|
||||
| [`@suaveplan/bitpacker-compressed`](./foundation/bitpacker/bitpacker-compressed/README.md) | Compression pipeline integration for bitpacker. |
|
||||
| [`@suaveplan/bitpacker-crypto`](./foundation/bitpacker/bitpacker-crypto/README.md) | Bit-packer-aware crypto primitives. The shared interfaces (`Authority`, `DomainTag`, `AuthorityVerifier`, `AeadOps`) live in `@suaveplan/types`; the canonical… |
|
||||
| [`@suaveplan/bitpacker-delta`](./foundation/bitpacker/bitpacker-delta/README.md) | Delta encoding utilities for efficient repeated state transmission. Detects changes between successive values and encodes differences rather than full values. |
|
||||
| [`@suaveplan/bitpacker-game`](./foundation/bitpacker/bitpacker-game/README.md) | Game state utilities: quantization, prediction, interpolation, and client-server reconciliation. |
|
||||
| [`@suaveplan/bitpacker-network`](./foundation/bitpacker/bitpacker-network/README.md) | Network simulation utilities for testing bitpacker under realistic network conditions. |
|
||||
| [`@suaveplan/bitpacker-schema`](./foundation/bitpacker/bitpacker-schema/README.md) | Schema registry and validation for bitpacker. |
|
||||
| [`@suaveplan/bitpacker-strategies`](./foundation/bitpacker/bitpacker-strategies/README.md) | Built-in packing strategies for `@suaveplan/bitpacker`. Provides strategies for numbers, strings, booleans, objects, arrays, enums, optional fields, oneOf unio… |
|
||||
| [`@suaveplan/bitpacker-streaming`](./foundation/bitpacker/bitpacker-streaming/README.md) | Streaming APIs for bitpacker including chunked packing/unpacking and true TransformStream support with framing. |
|
||||
| [`@suaveplan/bitpacker-validation`](./foundation/bitpacker/bitpacker-validation/README.md) | Safety limits and validation reports for bitpacker schemas. |
|
||||
|
||||
#### `core`
|
||||
|
||||
Core foundation — types, errors, configuration, schema registry, and utilities.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/config`](./foundation/core/config/README.md) | Layered configuration management for Suaveplan. Resolves an ordered set of typed layers (defaults, file, object, env, CLI) in priority order, deep-merges the r… |
|
||||
| [`@suaveplan/env`](./foundation/core/env/README.md) | Type-safe, schema-first environment variable loading for Node.js and edge runtimes. Define a schema of typed field builders, point it at your sources, and rece… |
|
||||
| [`@suaveplan/error`](./foundation/core/error/README.md) | Foundation error substrate for Suaveplan. Every non-testing `@suaveplan/*` package extends this library's `SuaveplanError` base class and uses its stable code… |
|
||||
| [`@suaveplan/schema-registry`](./foundation/core/schema-registry/README.md) | Versioned schema registry with forward/backward/full compatibility checking for evolving data contracts. Register schemas by subject name, enforce evolution ru… |
|
||||
| [`@suaveplan/types`](./foundation/core/types/README.md) | Types-only foundation package. Zero runtime code, zero dependencies, zero bundle bytes. Provides the canonical shared interfaces that allow every `@suaveplan/*… |
|
||||
| [`@suaveplan/typescript-config`](./foundation/core/typescript-config/README.md) | Shared TypeScript configuration presets for Suaveplan. This package ships composable JSON presets consumed via `"extends"` across every workspace package — no… |
|
||||
| [`@suaveplan/utils`](./foundation/core/utils/README.md) | Zero-external-dependency utility library for Suaveplan. Covers debug helpers, deep object manipulation, platform detection, IEEE 754 float packing, pseudo-rand… |
|
||||
|
||||
#### `data`
|
||||
|
||||
Data manipulation, masking, deduplication, diffing, and byte utilities.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/bytes`](./foundation/data/bytes/README.md) | Low-level byte utilities for Suaveplan: lossless hex/base64/UTF-8 encoding, `Uint8Array` manipulation, and UUID conversion. No native addons; depends only on p… |
|
||||
| [`@suaveplan/clipboard`](./foundation/data/clipboard/README.md) | Enterprise Clipboard API abstraction for browser-tier consumers. Wraps the async Clipboard API with a textarea + `execCommand` fallback, a paste-event read fal… |
|
||||
| [`@suaveplan/data-masking`](./foundation/data/data-masking/README.md) | PII/sensitive data masking for objects and strings with field-level rules, nested traversal, eight masking strategies, and OpenTelemetry instrumentation. |
|
||||
| [`@suaveplan/dedup`](./foundation/data/dedup/README.md) | Entity deduplication engine for detecting and merging duplicate records in any data set. Given a list of objects with varying representations of the same real-… |
|
||||
| [`@suaveplan/diff`](./foundation/data/diff/README.md) | Zero-dependency RFC 6902 JSON Patch engine for computing, applying, and inverting structural differences between JSON documents. The `diff(a, b)` function prod… |
|
||||
| [`@suaveplan/json`](./foundation/data/json/README.md) | Safe, enterprise-grade JSON utilities with prototype-pollution protection, circular-reference detection, and OpenTelemetry instrumentation. |
|
||||
| [`@suaveplan/media`](./foundation/data/media/README.md) | Universal media detection, metadata extraction, and constraint validation. Combines a pluggable magic-byte `SignatureRegistry` with the MIME database from `@su… |
|
||||
| [`@suaveplan/mime`](./foundation/data/mime/README.md) | Structured MIME-type detection, parsing, and registry for the Suaveplan platform. Provides RFC 7231 Content-Type parsing, magic-byte file signature detection,… |
|
||||
| [`@suaveplan/packer`](./foundation/data/packer/README.md) | Binary packing/unpacking utilities. Two modes: |
|
||||
| [`@suaveplan/packer-browser`](./foundation/data/packer-browser/README.md) | Browser-tier runtime for `@suaveplan/packer` archives. Ships a Service Worker interceptor, a `PackedResourceManager` (LRU + Blob-URL bookkeeping + ref-count +… |
|
||||
| [`@suaveplan/pagination`](./foundation/data/pagination/README.md) | Universal pagination primitives for cursor-based, offset-based, and keyset-based (seek) pagination strategies. The package covers the full pagination surface:… |
|
||||
| [`@suaveplan/pwa`](./foundation/data/pwa/README.md) | Enterprise browser-tier PWA primitives for Suaveplan applications. |
|
||||
| [`@suaveplan/react-packer`](./foundation/data/react-packer/README.md) | React 19 companion for `@suaveplan/packer-browser`. Ships `usePackedResource`, `usePackedImage`, `usePackedAudio`, `usePackedVideo`, and `usePackedFont` hooks… |
|
||||
| [`@suaveplan/serialize`](./foundation/data/serialize/README.md) | Round-trip-preserving structured serialization for JavaScript's rich type system. The standard `JSON.stringify`/`JSON.parse` pair silently drops `Date` objects… |
|
||||
| [`@suaveplan/templates`](./foundation/data/templates/README.md) | A plugin-based string template engine for TypeScript. Resolves `{variable}` and `{variable:transform}` tokens in strings using a priority-sorted, regex-scan pi… |
|
||||
| [`@suaveplan/validation`](./foundation/data/validation/README.md) | Runtime schema validation with a Zod-compatible API. This package is a self-contained parse engine — it does not import Zod or any other validation library — s… |
|
||||
| [`@suaveplan/web-workers`](./foundation/data/web-workers/README.md) | Enterprise browser-tier runtime for managing Web Workers, SharedWorkers, and fixed-size worker pools with typed RPC, AbortSignal-aware cancellation, transfer-l… |
|
||||
|
||||
#### `etl`
|
||||
|
||||
Extract-Transform-Load pipeline engine with typed stages.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/etl`](./foundation/etl/etl/README.md) | Extract-Transform-Load pipeline engine with typed stages, error handling, observable progress, and OTel instrumentation. |
|
||||
|
||||
#### `feature-flags`
|
||||
|
||||
Feature-flag evaluation engine with percentage and attribute targeting.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/feature-flags`](./foundation/feature-flags/feature-flags/README.md) | Provider-agnostic feature flag evaluation engine for controlling feature rollout without code deploys. The package ships a `FlagManager` high-level façade, a `… |
|
||||
|
||||
#### `flow`
|
||||
|
||||
Finite state machines, CRDTs, idempotency, and CBOR serialization.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/cbor`](./foundation/flow/cbor/README.md) | RFC 8949 (CBOR) encoder + decoder, RFC 8742 (CBOR Sequences), pull-style streaming decoder (`AsyncIterable<Uint8Array>` / `ReadableStream<Uint8Array>`), RFC 90… |
|
||||
| [`@suaveplan/crdt`](./foundation/flow/crdt/README.md) | Conflict-free Replicated Data Types (CRDTs) for distributed systems. Provides eight composable CRDT primitives plus a vector-clock and a sync manager that coor… |
|
||||
| [`@suaveplan/fsm`](./foundation/flow/fsm/README.md) | Typed, generic finite state machine (FSM) for TypeScript. Ships a synchronous-by-default interpreter with guard predicates, ordered transition actions, context… |
|
||||
| [`@suaveplan/idempotency`](./foundation/flow/idempotency/README.md) | Idempotency key guard and deduplication for async operations. The package provides a complete `pending → completed \| failed` state machine, three store backend… |
|
||||
| [`@suaveplan/job-scheduler`](./foundation/flow/job-scheduler/README.md) | Universal in-process job scheduler with cron + queued runs, retries, timeouts, concurrency control, and OpenTelemetry observability. Built on `@suaveplan/cron`… |
|
||||
| [`@suaveplan/queue`](./foundation/flow/queue/README.md) | Enterprise-grade queue management: memory/priority/delayed queues, concurrent workers with retry and dead-letter forwarding, job state machines, and OTel instr… |
|
||||
| [`@suaveplan/queue-distribution`](./foundation/flow/queue-distribution/README.md) | Worker-pool distribution layer: load-balancing strategies, worker pool management, manager with retry and health checks, in-process `LocalWorker`. Transport-ag… |
|
||||
| [`@suaveplan/queue-distribution-http`](./foundation/flow/queue-distribution-http/README.md) | HTTP transport plugin for `@suaveplan/queue-distribution`. Ships `HttpWorkerCommunicator` (a fetch-backed `WorkerCommunicator` with a Zod-validated wire envelo… |
|
||||
| [`@suaveplan/queue-distribution-worker-thread`](./foundation/flow/queue-distribution-worker-thread/README.md) | Web-Worker / Node `worker_threads` transport plugin for `@suaveplan/queue-distribution`. Ships `WorkerThreadCommunicator` (a `ThreadWorkerLike`-based message b… |
|
||||
| [`@suaveplan/rate-limiter`](./foundation/flow/rate-limiter/README.md) | Enterprise rate limiter with three algorithms, rule-based routing, tenant multipliers, pluggable storage, and OTel instrumentation. |
|
||||
| [`@suaveplan/retry`](./foundation/flow/retry/README.md) | Retry strategies and circuit-breaker for async operations. The package provides constant, linear, and exponential backoff with four jitter modes (full, equal,… |
|
||||
| [`@suaveplan/rules`](./foundation/flow/rules/README.md) | Universal rule engine + validation system. Comprehensive `RuleEngine` with async evaluation, depth limit, AbortSignal, plugin registration; fluent `RuleBuilder… |
|
||||
| [`@suaveplan/rules-hono`](./foundation/flow/rules-hono/README.md) | Hono middleware wrapping `@suaveplan/rules`. Six middleware factories — `ruleMiddleware`, `validationMiddleware`, `accessControl`, `responseValidation`, `rateL… |
|
||||
| [`@suaveplan/streams`](./foundation/flow/streams/README.md) | WHATWG-Streams-based primitives: backpressure-first transforms, zero-copy `Uint8Array` operations, `AbortSignal` integration end-to-end, structured errors, OTe… |
|
||||
| [`@suaveplan/streams-chunk-queue`](./foundation/flow/streams-chunk-queue/README.md) | Universal out-of-order chunk reconstitution. Accepts chunks for a file in any order and exposes a sequential `ReadableStream<Uint8Array>` consumer surface, com… |
|
||||
| [`@suaveplan/streams-metadata`](./foundation/flow/streams-metadata/README.md) | Universal stream transforms that detect MIME type and extract EXIF/IPTC/XMP image metadata from buffered `Uint8Array` payloads. Three composable transforms (`M… |
|
||||
| [`@suaveplan/undo`](./foundation/flow/undo/README.md) | Enterprise-grade undo/redo engine with branching history, async commands, history compression, selective undo, and command merging. Built for collaborative edi… |
|
||||
| [`@suaveplan/workflow`](./foundation/flow/workflow/README.md) | Workflow and pipeline orchestration: sequential and parallel step execution, conditional branching with guard functions, retry strategies, timeouts, saga-patte… |
|
||||
|
||||
#### `random`
|
||||
|
||||
Pseudo-random generators, noise functions, and deterministic sequences.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/noise`](./foundation/random/noise/README.md) | Seeded spatial noise fields for procedural content generation. Provides six field types — Perlin, Simplex (2D/3D/4D), Value, Worley/cellular (F1/F2/F2-F1 modes… |
|
||||
| [`@suaveplan/pseudo-random`](./foundation/random/pseudo-random/README.md) | Deterministic, seeded pseudo-random number generators (PRNGs) conforming to the `Rng` interface from `@suaveplan/types`. Ships four algorithms — Mulberry32, SF… |
|
||||
| [`@suaveplan/random`](./foundation/random/random/README.md) | High-level cryptographically secure randomness for application use. This is the everyday random package — it wraps the CSPRNG with ergonomic helpers for every… |
|
||||
|
||||
#### `timing`
|
||||
|
||||
Cron, stopwatches, throttling, debouncing, and scheduling.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/cron`](./foundation/timing/cron/README.md) | Universal cron expression parser, next-fire calculator with timezone + DST awareness, and an opt-in lightweight in-process scheduler. Zero external cron depend… |
|
||||
| [`@suaveplan/datetime`](./foundation/timing/datetime/README.md) | Timezone-aware date/time utilities covering the most common application-layer date operations that the native `Date` API leaves unaddressed. The package provid… |
|
||||
| [`@suaveplan/timing`](./foundation/timing/timing/README.md) | Timing and scheduling utilities for application-layer time management. The package covers measurement (stopwatch with lap recording), rate control (throttle an… |
|
||||
|
||||
### `gaming`
|
||||
|
||||
Game engines, entity-component systems, and simulation libraries.
|
||||
|
||||
#### `games`
|
||||
|
||||
Game controllers, entity-component systems, and game-server logic.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/controller`](./gaming/games/controller/README.md) | Universal input controller foundation. Ships the unified `ControllerInput` shape (37 normalised fields in `[0, 255]`), `BaseController` abstract base with inje… |
|
||||
| [`@suaveplan/ecs`](./gaming/games/ecs/README.md) | Archetype-based Entity Component System with `Uint32`-backed bitset queries, phase-ordered system scheduling, entity-ID recycling, Zod-validated component sche… |
|
||||
| [`@suaveplan/game-server`](./gaming/games/game-server/README.md) | Universal game-server framework. Migrated from `@suaveplan/game-server` on the reference branch `claude/review-game-engine-arch-Kaykr` (main repo `packages/uni… |
|
||||
|
||||
### `geospatial`
|
||||
|
||||
Geospatial data, mapping, planetary coordinates, and logistics utilities.
|
||||
|
||||
#### `geo`
|
||||
|
||||
Geospatial coordinates, planetary data, and solar-system modelling.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/geo`](./geospatial/geo/geo/README.md) | Abstract spherical and ellipsoidal geometry — pure math with no planet-specific constants. Every operation that needs a body's shape takes an explicit `Ellipso… |
|
||||
| [`@suaveplan/geo-earth`](./geospatial/geo/geo-earth/README.md) | Earth configuration and legacy geo utilities. |
|
||||
| [`@suaveplan/geo-planet`](./geospatial/geo/geo-planet/README.md) | Configurable planet surface geometry from `PlanetConfig` — ellipsoid, atmosphere, satellites, and orbital elements. |
|
||||
| [`@suaveplan/solar-system`](./geospatial/geo/solar-system/README.md) | Solar system provider with astronomy engine. |
|
||||
| [`@suaveplan/stars`](./geospatial/geo/stars/README.md) | Star catalog provider. |
|
||||
| [`@suaveplan/tides`](./geospatial/geo/tides/README.md) | Tidal prediction providers. |
|
||||
| [`@suaveplan/universe`](./geospatial/geo/universe/README.md) | Observer model, coordinate frames, wire protocol, and provider registry. |
|
||||
|
||||
#### `logistics`
|
||||
|
||||
Port data and logistics primitives.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/ports`](./geospatial/logistics/ports/README.md) | Global port database with shipping-lane routing, ETA estimation, and UN/LOCODE port lookups for sea ports, airports, and rail terminals. |
|
||||
|
||||
### `identity`
|
||||
|
||||
Authentication, authorization, identity resolution, and credential management.
|
||||
|
||||
#### `auth`
|
||||
|
||||
Authentication providers, JWT, WebAuthn, OAuth, and session resolution.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/auth`](./identity/auth/auth/README.md) | Provider-agnostic authentication library for Suaveplan. Ships JWT token lifecycle, API key management, session management, and framework-agnostic middleware —… |
|
||||
| [`@suaveplan/better-auth-surrealdb`](./identity/auth/better-auth-surrealdb/README.md) | Better-Auth Adapter for SurrealDB v3 — bundles the canonical 17-table SurrealQL schema, compiles Better-Auth `Where[]` filters to parameterised SurrealQL, auto… |
|
||||
| [`@suaveplan/jwt-tenant`](./identity/auth/jwt-tenant/README.md) | JWT-based multi-tenant token issuance and verification for Suaveplan. Ships an Ed25519 / ECDSA-P256 signing pipeline, in-memory JWKS store with rotation and ex… |
|
||||
| [`@suaveplan/macos-keychain`](./identity/auth/macos-keychain/README.md) | macOS Keychain bridge for generic-password credentials. Wraps the system `/usr/bin/security` CLI behind a strongly-typed CRUD surface with typed errors, AbortS… |
|
||||
| [`@suaveplan/oauth-client`](./identity/auth/oauth-client/README.md) | Universal OAuth 2.0 / OIDC client for Node.js, browsers, Bun, Deno, and edge runtimes. |
|
||||
| [`@suaveplan/permissions`](./identity/auth/permissions/README.md) | Full RBAC/ABAC permissions engine for Suaveplan. Ships role management, policy evaluation, CASL-inspired ability builder, TTL permission cache, condition evalu… |
|
||||
| [`@suaveplan/webauthn`](./identity/auth/webauthn/README.md) | W3C WebAuthn Level 3 client + parser + verifier for Suaveplan. Browser-tier registration / authentication entry points sit on the package root; framework-agnos… |
|
||||
|
||||
### `localization`
|
||||
|
||||
Internationalization, localization, locale detection, and translation infrastructure.
|
||||
|
||||
#### `i18n`
|
||||
|
||||
Internationalization core, backends, detectors, and locale resolution.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/i18n`](./localization/i18n/i18n/README.md) | Slim integration core for the i18n family. After the Wave 6 modular split, this package owns the `MessageCatalog`, the `createI18n` / `I18nInstance` plugin slo… |
|
||||
| [`@suaveplan/i18n-backend-chained`](./localization/i18n/i18n-backend-chained/README.md) | Universal chained backend orchestrator for the `@suaveplan/i18n` family. Composes N backends into a single `Backend` with read-fallthrough on `BACKEND_LOCALE_N… |
|
||||
| [`@suaveplan/i18n-backend-fs-multifile`](./localization/i18n/i18n-backend-fs-multifile/README.md) | Server-tier multi-file Node fs/promises translation backend for the @suaveplan/i18n family. Reads <root>/<locale>/<namespace>.<ext> (nested), <root>/<locale>.<namespace>.<ext> (flat), or a caller-supplied glob pattern. JSON built-in; YAML and JSON5 auto-detected via dynamic import. Optional fs.watch loop emits loaded events through the catalog event bus. AbortSignal cancellation, OpenTelemetry instrumentation under i18n.backend.fs_multifile.*. Server-only — uses node:fs/promises; browser consumers should use @suaveplan/i18n-backend-http or @suaveplan/i18n-backend-storage. |
|
||||
| [`@suaveplan/i18n-backend-http`](./localization/i18n/i18n-backend-http/README.md) | Universal translation backend for the `@suaveplan/i18n` family, backed by an injectable `fetch`. Read-only by design: maps HTTP 404 to `BACKEND_LOCALE_NOT_AVAI… |
|
||||
| [`@suaveplan/i18n-backend-memory`](./localization/i18n/i18n-backend-memory/README.md) | Universal in-memory translation backend for the `@suaveplan/i18n` family. Backed by `Map<locale, Map<namespace, TranslationRecord>>` plus a modification ledger… |
|
||||
| [`@suaveplan/i18n-backend-storage`](./localization/i18n/i18n-backend-storage/README.md) | Universal translation backend for the `@suaveplan/i18n` family, backed by any `AbstractStorage` implementation from `@suaveplan/storage`. Implements the full `… |
|
||||
| [`@suaveplan/i18n-detector`](./localization/i18n/i18n-detector/README.md) | Universal locale-detection chain for the `@suaveplan/i18n` family. Ships the chain orchestrator, the BCP-47 default negotiator, the `getTextDirection` / `isRtl… |
|
||||
| [`@suaveplan/i18n-formatter-collator`](./localization/i18n/i18n-formatter-collator/README.md) | Wrapper around `Intl.Collator` for the `@suaveplan/i18n` family — locale-aware string comparison and sorting (sensitivity, numeric-aware natural sort, case-fir… |
|
||||
| [`@suaveplan/i18n-formatter-currency`](./localization/i18n/i18n-formatter-currency/README.md) | Locale-aware currency formatting backed by `Intl.NumberFormat` with `style: "currency"`. Wave 3 of the 14-package i18n modular split. Provides both a class (`C… |
|
||||
| [`@suaveplan/i18n-formatter-date`](./localization/i18n/i18n-formatter-date/README.md) | Wrapper around `Intl.DateTimeFormat` for the `@suaveplan/i18n` family — 8-preset table (short/medium/long/full × date+time/time-only/date-only), range renderin… |
|
||||
| [`@suaveplan/i18n-formatter-display-name`](./localization/i18n/i18n-formatter-display-name/README.md) | Wrapper around `Intl.DisplayNames` for the `@suaveplan/i18n` family — locale-aware language, region, script, currency, and calendar display names. Universal le… |
|
||||
| [`@suaveplan/i18n-formatter-list`](./localization/i18n/i18n-formatter-list/README.md) | Locale-aware list joining backed by `Intl.ListFormat`. Wave 3 of the 14-package i18n modular split. Provides both a class (`ListFormatter`) and a standalone he… |
|
||||
| [`@suaveplan/i18n-formatter-number`](./localization/i18n/i18n-formatter-number/README.md) | Locale-aware number formatting backed by `Intl.NumberFormat`. Wave 3 of the 14-package i18n modular split. Provides both a class (`NumberFormatter`) and a stan… |
|
||||
| [`@suaveplan/i18n-formatter-relative-time`](./localization/i18n/i18n-formatter-relative-time/README.md) | Locale-aware relative-time formatting backed by `Intl.RelativeTimeFormat`. Wave 3 of the 14-package i18n modular split. Provides both a class (`RelativeTimeFor… |
|
||||
| [`@suaveplan/i18n-formatter-segmenter`](./localization/i18n/i18n-formatter-segmenter/README.md) | Wrapper around `Intl.Segmenter` for the `@suaveplan/i18n` family — locale-aware text segmentation by grapheme, word, or sentence. Compound emoji count as a sin… |
|
||||
| [`@suaveplan/i18n-icu-parser`](./localization/i18n/i18n-icu-parser/README.md) | ICU MessageFormat parser, `{{var}}` interpolation engine, and 9-language CLDR pluralization for the `@suaveplan/i18n` family. Universal leaf package — depends… |
|
||||
| [`@suaveplan/i18n-shared`](./localization/i18n/i18n-shared/README.md) | Shared runtime atoms for the entire `@suaveplan/i18n*` package family — locale constants, BCP-47 normalisation map, RTL set, locale descriptors, interpolation… |
|
||||
| [`@suaveplan/locale-data`](./localization/i18n/locale-data/README.md) | Static ISO 3166-1 country data and ISO 639-1 language data with typed lookup functions, OTel instrumentation, and Zod schemas. |
|
||||
| [`@suaveplan/react-i18n`](./localization/i18n/react-i18n/README.md) | React bindings for `@suaveplan/i18n`. Ships the `<TranslationProvider>`, twelve hooks, four headless components, the class-component HOC, the nine built-in loc… |
|
||||
|
||||
### `media`
|
||||
|
||||
Audio, video, image, and font processing libraries.
|
||||
|
||||
#### `audio`
|
||||
|
||||
Audio processing, playback, analysis, and effect plugins.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/audio-analysis-beat`](./media/audio/audio-analysis-beat/README.md) | Universal beat, tempo, and onset detection plugin for the `@suaveplan/audio-*` family. Detects BPM, beat locations, and onset times from `AudioDocument` stream… |
|
||||
| [`@suaveplan/audio-analysis-loudness`](./media/audio/audio-analysis-loudness/README.md) | Universal ITU-R BS.1770-4 loudness analyser. Drop-in `AnalysisOp` plugin for the `@suaveplan/audio-*` family. |
|
||||
| [`@suaveplan/audio-analysis-pitch`](./media/audio/audio-analysis-pitch/README.md) | Universal pitch, key, and chord detection Analysis Op plugins for the `@suaveplan/audio-*` family. Three detectors: |
|
||||
| [`@suaveplan/audio-analysis-spectrum`](./media/audio/audio-analysis-spectrum/README.md) | Universal spectral analysis for the Suaveplan audio stack: real FFT, Hann/Hamming/Blackman windows, mel filterbank, DCT-II, and three high-level analyzers — `S… |
|
||||
| [`@suaveplan/audio-cli`](./media/audio/audio-cli/README.md) | Server-tier `audio` command-line tool. Thin shell over `@suaveplan/audio-core`, `@suaveplan/audio-codecs`, and `@suaveplan/audio-engine-server`: every audio tr… |
|
||||
| [`@suaveplan/audio-codec-ffmpeg-native`](./media/audio/audio-codec-ffmpeg-native/README.md) | Server-tier `AudioCodec` plugin for `@suaveplan/audio-codecs` that shells out to the system `ffmpeg` binary via `fluent-ffmpeg` and streams audio through the s… |
|
||||
| [`@suaveplan/audio-codec-ffmpeg-wasm`](./media/audio/audio-codec-ffmpeg-wasm/README.md) | Browser-tier `AudioCodec` plugin for `@suaveplan/audio-codecs` backed by `@ffmpeg/ffmpeg` running inside a dedicated Web Worker. Decodes and encodes `wav`, `mp… |
|
||||
| [`@suaveplan/audio-codecs`](./media/audio/audio-codecs/README.md) | Universal audio codec registry: decode/encode plugin loader with magic-byte format detection, progressive streaming decode, native WAV/AIFF/CAF codecs, and laz… |
|
||||
| [`@suaveplan/audio-controllers`](./media/audio/audio-controllers/README.md) | Universal MIDI hardware controller plugins. Built on `@suaveplan/audio-core`, `@suaveplan/audio-transport`, `@suaveplan/events`, `@suaveplan/fsm`, and `@suavep… |
|
||||
| [`@suaveplan/audio-core`](./media/audio/audio-core/README.md) | Universal audio data model and processing primitives. Provides a paged `AudioDocument`, a non-destructive `PlanEngine`, a block-level `ProcessPipeline`, and a… |
|
||||
| [`@suaveplan/audio-decode-wav`](./media/audio/audio-decode-wav/README.md) | Universal WAV decoder wrapper over the upstream `@audio/decode-wav` library. Exposes a stable, OTel-instrumented, AbortSignal-honouring contract over a frozen,… |
|
||||
| [`@suaveplan/audio-engine-browser`](./media/audio/audio-engine-browser/README.md) | Browser-tier `TimelineAdapter` implementation for the `@suaveplan/audio-*` family. Hosts Web Audio rendering, `AudioWorklet` loading, and the `AudioFxPlugin.cr… |
|
||||
| [`@suaveplan/audio-engine-server`](./media/audio/audio-engine-server/README.md) | Server-tier audio I/O layer for the `@suaveplan/audio-*` family. Provides the thinnest possible bridge between universal `AudioDocument` primitives and a Node.… |
|
||||
| [`@suaveplan/audio-fingerprint`](./media/audio/audio-fingerprint/README.md) | High-accuracy audio fingerprinting service. The package is dual-runtime: a Python FastAPI backend (`src/fingerprint.py` + `src/api/app.py`) implements chromagr… |
|
||||
| [`@suaveplan/audio-metadata`](./media/audio/audio-metadata/README.md) | Universal audio metadata extractor that parses ID3v1/v2, MP4/M4A atoms, FLAC Vorbis comments, Ogg Vorbis, WAV RIFF, and AIFF/AIFC containers, returning a norma… |
|
||||
| [`@suaveplan/audio-sprite-packer`](./media/audio/audio-sprite-packer/README.md) | Universal audio sprite packer with injectable adapter, Zod-validated boundaries, and OpenTelemetry instrumentation. Zero browser or Node.js dependencies. |
|
||||
| [`@suaveplan/audio-sprite-player`](./media/audio/audio-sprite-player/README.md) | Universal audio sprite playback engine. Consumes manifests produced by `@suaveplan/audio-sprite-packer`, validates them, schedules voices, runs voice-stealing… |
|
||||
| [`@suaveplan/audio-timeline`](./media/audio/audio-timeline/README.md) | Headless DAW timeline engine with multi-track arrangements, clips, playback, loop regions, selection, zoom, per-track mixer state, undo/redo, and a pluggable `… |
|
||||
| [`@suaveplan/audio-transport`](./media/audio/audio-transport/README.md) | Universal musical clock and transport: sample-accurate scheduling with PPQ, BPM automation, swing, time signatures, and loop regions. Zero browser or Node depe… |
|
||||
| [`@suaveplan/control-surface-schema`](./media/audio/control-surface-schema/README.md) | Universal canonical schema for DAW-style control surfaces. Owns the `ControlSurface` root document, the `ControlNode = Group \| Control` discriminated tree, the… |
|
||||
| [`@suaveplan/midi`](./media/audio/midi/README.md) | Universal MIDI primitives for the Suaveplan platform. Owns the canonical types, byte parsers, message constructors, control-surface dispatchers (NRPN, RPN, 14-… |
|
||||
| [`@suaveplan/third-party-elevenlabs`](./media/audio/third-party-elevenlabs/README.md) | <!-- begin:auto-docs --> |
|
||||
| [`@suaveplan/voice`](./media/audio/voice/README.md) | Provider-agnostic voice core for Suaveplan. Defines the abstract interfaces, shared types, Zod schemas, and structured error hierarchy that every voice provide… |
|
||||
| [`@suaveplan/voice-elevenlabs`](./media/audio/voice-elevenlabs/README.md) | ElevenLabs domain adapter for the `@suaveplan/voice` interface family. |
|
||||
|
||||
#### `audio-fx`
|
||||
|
||||
Real-time audio effects and DSP plugins.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/audio-fx-plugin-audio-analyzer`](./media/audio-fx/audio-fx-plugin-audio-analyzer/README.md) | audio-analyzer plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-audio-player`](./media/audio-fx/audio-fx-plugin-audio-player/README.md) | audio-player plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-auto-filter`](./media/audio-fx/audio-fx-plugin-auto-filter/README.md) | auto-filter plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-base`](./media/audio-fx/audio-fx-plugin-base/README.md) | Foundation for the `@suaveplan/audio-fx-plugin-*` family. Universal (browser / server / offline / worklet identical). Zero `@suaveplan/audio-fx-plugin-*` deps… |
|
||||
| [`@suaveplan/audio-fx-plugin-chorus`](./media/audio-fx/audio-fx-plugin-chorus/README.md) | chorus plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-compressor`](./media/audio-fx/audio-fx-plugin-compressor/README.md) | compressor plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-delay`](./media/audio-fx/audio-fx-plugin-delay/README.md) | delay plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-distortion`](./media/audio-fx/audio-fx-plugin-distortion/README.md) | distortion plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-eq`](./media/audio-fx/audio-fx-plugin-eq/README.md) | Parametric / shelving / high-pass / low-pass equaliser for the `@suaveplan/audio-fx-plugin-*` family. Multi-band biquad implementation, sample-accurate paramet… |
|
||||
| [`@suaveplan/audio-fx-plugin-filter`](./media/audio-fx/audio-fx-plugin-filter/README.md) | filter plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-flanger`](./media/audio-fx/audio-fx-plugin-flanger/README.md) | flanger plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-game-audio`](./media/audio-fx/audio-fx-plugin-game-audio/README.md) | game-audio plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-gate`](./media/audio-fx/audio-fx-plugin-gate/README.md) | gate plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-glados`](./media/audio-fx/audio-fx-plugin-glados/README.md) | glados plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-limiter`](./media/audio-fx/audio-fx-plugin-limiter/README.md) | limiter plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-microphone`](./media/audio-fx/audio-fx-plugin-microphone/README.md) | microphone plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-mixer`](./media/audio-fx/audio-fx-plugin-mixer/README.md) | mixer plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-phaser`](./media/audio-fx/audio-fx-plugin-phaser/README.md) | phaser plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-pitch-shifter`](./media/audio-fx/audio-fx-plugin-pitch-shifter/README.md) | pitch-shifter plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-reverb`](./media/audio-fx/audio-fx-plugin-reverb/README.md) | reverb plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-sampler`](./media/audio-fx/audio-fx-plugin-sampler/README.md) | sampler plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-spatial`](./media/audio-fx/audio-fx-plugin-spatial/README.md) | spatial plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-spessasynth`](./media/audio-fx/audio-fx-plugin-spessasynth/README.md) | SF2/SF3 SoundFont synthesizer plugin for the `@suaveplan/audio-fx-plugin-*` family. Wraps the MIT-licensed SpessaSynth engine as a standard `AudioFxPlugin`, so… |
|
||||
| [`@suaveplan/audio-fx-plugin-synthesizer`](./media/audio-fx/audio-fx-plugin-synthesizer/README.md) | synthesizer plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-tremolo`](./media/audio-fx/audio-fx-plugin-tremolo/README.md) | tremolo plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
| [`@suaveplan/audio-fx-plugin-widener`](./media/audio-fx/audio-fx-plugin-widener/README.md) | widener plugin for the @suaveplan/audio-fx-plugin-* family. |
|
||||
|
||||
#### `audio-ir`
|
||||
|
||||
Impulse-response capture, loading, and room-acoustic plugins.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/audio-ir-core`](./media/audio-ir/audio-ir-core/README.md) | Foundation contract for the `@suaveplan/audio-ir-*` family. Universal; zero `@suaveplan/audio-*` runtime deps. Every sibling package (`audio-ir-loader`, `audio… |
|
||||
| [`@suaveplan/audio-ir-loader`](./media/audio-ir/audio-ir-loader/README.md) | Pipeline package for the `@suaveplan/audio-ir-*` family. Composes `IrRegistry` (from `@suaveplan/audio-ir-core`), a `StorageUriResolver` (from `@suaveplan/stor… |
|
||||
| [`@suaveplan/audio-ir-room-plugin-club`](./media/audio-ir/audio-ir-room-plugin-club/README.md) | Original audio-ir content package. Ships two bundled room impulse responses (`club1.wav`, `club2.wav`) wired through the `@suaveplan/audio-ir-*` family. |
|
||||
| [`@suaveplan/audio-ir-room-plugin-pool`](./media/audio-ir/audio-ir-room-plugin-pool/README.md) | Original audio-ir content package. Ships a single `room`-family impulse response — `pool-outdoor-swimming-pool` — sourced from the outdoor-swimming-pool captur… |
|
||||
|
||||
#### `fonts`
|
||||
|
||||
Font packing, compression, subsetting, and conversion tools.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/font-compress`](./media/fonts/font-compress/README.md) | BTF (Binary TypeFace) binary packing and unpacking. Converts `TypefaceFont` (typeface.js JSON format) to a compact `Uint8Array` using dictionary-based integer… |
|
||||
| [`@suaveplan/font-convert`](./media/fonts/font-convert/README.md) | Converts TTF/OTF/WOFF/WOFF2 font files to the typeface.js JSON format used by Three.js `FontLoader`. Also defines the canonical `TypefaceFont`, `TypefaceGlyph`… |
|
||||
| [`@suaveplan/font-decompress`](./media/fonts/font-decompress/README.md) | BTF (Binary TypeFace) binary unpacking. Reverses `@suaveplan/font-compress`'s `BinaryFontPacker` output into a typeface.js JSON shape ready for Three.js / R3F… |
|
||||
| [`@suaveplan/font-subset`](./media/fonts/font-subset/README.md) | Font subsetting and compression library with a codec-agnostic adapter interface. Supports Unicode range subsetting, WOFF2/WOFF/OTF/TTF output, OpenType feature… |
|
||||
|
||||
#### `image`
|
||||
|
||||
Image compression, colour conversion, EXIF handling, and optimisation.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/caddy-imgproxy`](./media/image/caddy-imgproxy/README.md) | Universal TypeScript port of the Caddy imgproxy URL rewriter. Parses the compact `/u<op><gravity><dims><modifiers>/<path>.<format>` grammar, resolves aspect-ra… |
|
||||
| [`@suaveplan/colour-convert-adapt`](./media/image/colour-convert-adapt/README.md) | Bradford chromatic adaptation for XYZ colour coordinates. Chromatic adaptation adjusts a colour's appearance so that it looks the same under different light so… |
|
||||
| [`@suaveplan/colour-convert-cieluv`](./media/image/colour-convert-cieluv/README.md) | CIELUV, xyY, LMS, and Bradford chromatic adaptation conversions. This plugin provides colour-space transforms between RGB, CIELUV, xyY, and LMS cone-response s… |
|
||||
| [`@suaveplan/colour-convert-cmyk`](./media/image/colour-convert-cmyk/README.md) | Bidirectional conversion between RGB and CMYK colour spaces using the standard Euclidean algorithm with black generation. CMYK is the subtractive model used in… |
|
||||
| [`@suaveplan/colour-convert-hsluv`](./media/image/colour-convert-hsluv/README.md) | HSLuv perceptually-uniform colour space with gamut-mapped ray intersection for sRGB. This plugin provides bidirectional RGB↔HSLuv/HPLuv conversions and uniform… |
|
||||
| [`@suaveplan/colour-filter-cct`](./media/image/colour-filter-cct/README.md) | Correlated Colour Temperature (CCT) conversions using the McCamy formula and black-body RGB approximation. Convert any RGB colour to its approximate colour tem… |
|
||||
| [`@suaveplan/colour-filter-cmc`](./media/image/colour-filter-cmc/README.md) | CMC l:c Delta-E colour difference metric with hue-dependent chroma scaling and low-luminance branch handling. This plugin computes perceptual distance between… |
|
||||
| [`@suaveplan/colour-filter-greyscale`](./media/image/colour-filter-greyscale/README.md) | Converts any RGB colour to its perceptual greyscale equivalent using the ITU-R BT.709 luminance coefficients. Unlike simple channel averaging, this formula wei… |
|
||||
| [`@suaveplan/colour-filter-sepia`](./media/image/colour-filter-sepia/README.md) | Applies a photographic sepia tone filter to RGB colours using the standard W3C CSS Filters 3×3 matrix. This plugin converts any colour into a warm brownish ton… |
|
||||
| [`@suaveplan/colours`](./media/image/colours/README.md) | Universal colour manipulation library: parsing, conversion, analysis, mixing, blending, and palette generation. Parses hex (3/4/6/8 digit), CSS `rgb()`, `hsl()… |
|
||||
| [`@suaveplan/compress-image`](./media/image/compress-image/README.md) | Runtime-agnostic image compression utilities with pluggable adapters for Node.js (Sharp) and testing (MemoryAdapter). Supports resize, quality reduction, forma… |
|
||||
| [`@suaveplan/emblem-solver`](./media/image/emblem-solver/README.md) | Minimal-rectangle SVG converter for pixelated sprite emblems. |
|
||||
| [`@suaveplan/exif`](./media/image/exif/README.md) | Dependency-free, runtime-agnostic EXIF/XMP/IPTC metadata reader, writer, and stripper for JPEG and PNG images. Parses JPEG segment chains to locate and decode… |
|
||||
| [`@suaveplan/image`](./media/image/image/README.md) | Universal image metadata extraction, format detection, resize math, crop validation, and transform pipeline planning. Zero browser or Node dependencies — runs… |
|
||||
| [`@suaveplan/image-quantisation`](./media/image/image-quantisation/README.md) | Image complexity analysis and adaptive colour quantisation with pool detection, spatial-aware anti-aliasing cleanup, and multiple quantisation algorithms. Desi… |
|
||||
|
||||
#### `video`
|
||||
|
||||
Video encoding, decoding, and thumbnail generation.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/video`](./media/video/video/README.md) | Universal video metadata extraction, magic-byte format detection, resolution math, transform-pipeline planning, and a fluent pipeline builder. Zero browser-onl… |
|
||||
| [`@suaveplan/video-thumbnails`](./media/video/video-thumbnails/README.md) | Video thumbnail extraction and sprite-sheet generation backed by an injectable `FfmpegRunner`. The default runner — `FluentFfmpegRunner` — wraps `fluent-ffmpeg… |
|
||||
|
||||
### `money`
|
||||
|
||||
Payments, currencies, cryptocurrency, and financial primitives.
|
||||
|
||||
#### `finance`
|
||||
|
||||
Currency conversion, cryptocurrency utilities, and exchange-rate providers.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/cryptocurrency`](./money/finance/cryptocurrency/README.md) | Universal cryptocurrency utilities for wallet address validation, unit conversion, chain registry, gas estimation, ERC-20 token registry, calldata / function-s… |
|
||||
| [`@suaveplan/currency`](./money/finance/currency/README.md) | Universal currency conversion engine — pluggable `CurrencyProvider` interface, TTL-bounded `RateCache` with direct / inverse / triangulated lookup, primary + f… |
|
||||
| [`@suaveplan/currency-provider-binance`](./money/finance/currency-provider-binance/README.md) | Binance public-ticker crypto-rates provider for `@suaveplan/currency`. Sibling package in the foundation-with-sibling-plugins shape: implements the `CurrencyPr… |
|
||||
| [`@suaveplan/currency-provider-coingecko`](./money/finance/currency-provider-coingecko/README.md) | Universal CoinGecko sibling for `@suaveplan/currency-providers`. Implements the `CurrencyProvider` contract from `@suaveplan/currency` against the public `/sim… |
|
||||
| [`@suaveplan/currency-provider-coinmarketcap`](./money/finance/currency-provider-coinmarketcap/README.md) | CoinMarketCap crypto-rates sibling provider for `@suaveplan/currency`. One of the nine `@suaveplan/currency-provider-<name>` sibling packages spun out of the 2… |
|
||||
| [`@suaveplan/currency-provider-cryptocompare`](./money/finance/currency-provider-cryptocompare/README.md) | Universal CryptoCompare crypto-rates provider for `@suaveplan/currency`. Sibling provider package on top of the slim `@suaveplan/currency-providers` core (`htt… |
|
||||
| [`@suaveplan/currency-provider-ecb`](./money/finance/currency-provider-ecb/README.md) | Universal `@suaveplan/currency` rate provider for the European Central Bank's daily exchange-rates XML feed. Free, no API key required. Built on the slim `@sua… |
|
||||
| [`@suaveplan/currency-provider-fixer`](./money/finance/currency-provider-fixer/README.md) | Fixer.io fiat-rates sibling provider for the slim `@suaveplan/currency-providers` core. Implements the `CurrencyProvider` contract from `@suaveplan/currency`,… |
|
||||
| [`@suaveplan/currency-provider-kraken`](./money/finance/currency-provider-kraken/README.md) | Universal Kraken public-ticker rate provider for `@suaveplan/currency`. Wraps Kraken's free, no-API-key `/0/public/Ticker` endpoint into a `CurrencyProvider` s… |
|
||||
| [`@suaveplan/currency-provider-open-exchange-rates`](./money/finance/currency-provider-open-exchange-rates/README.md) | Universal `@suaveplan/currency` provider for openexchangerates.org. Routes `/latest.json` through the slim-core `httpRequest()` helper, validates the upstream… |
|
||||
| [`@suaveplan/currency-provider-virtual`](./money/finance/currency-provider-virtual/README.md) | In-memory, programmable virtual / game currency provider for `@suaveplan/currency`. Sibling of the `@suaveplan/currency-providers` slim plugin core. No HTTP, n… |
|
||||
| [`@suaveplan/currency-providers`](./money/finance/currency-providers/README.md) | Slim plugin core for `@suaveplan/currency` rate providers. Ships the `CurrencyProviderRegistry` (name-keyed lookup), the `defineCurrencyProvider` factory, the… |
|
||||
| [`@suaveplan/money`](./money/finance/money/README.md) | Universal precise currency and money arithmetic using integer minor-unit representation. All operations use `bigint` so consumers never hit floating-point roun… |
|
||||
|
||||
#### `payment`
|
||||
|
||||
Payment intent lifecycle, provider adapters, and fraud detection.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/payment`](./money/payment/payment/README.md) | Universal payment intent lifecycle, webhook signature verification, receipt generation, fraud scoring, refund tracking, tax calculation, retry orchestration, r… |
|
||||
| [`@suaveplan/payment-provider-adyen`](./money/payment/payment-provider-adyen/README.md) | Universal **Adyen Checkout API v71 adapter** for `@suaveplan/payment`. Implements the `PaymentProvider` interface via the slim `@suaveplan/payment-providers` c… |
|
||||
| [`@suaveplan/payment-provider-mollie`](./money/payment/payment-provider-mollie/README.md) | Universal Mollie Payments API v2 adapter for `@suaveplan/payment`. Sibling provider package on the slim `@suaveplan/payment-providers` core: implements `Paymen… |
|
||||
| [`@suaveplan/payment-provider-paypal`](./money/payment/payment-provider-paypal/README.md) | PayPal sibling adapter for the slim `@suaveplan/payment-providers` foundation. Ships `PayPalProvider` — a `PaymentProvider` implementation against the PayPal O… |
|
||||
| [`@suaveplan/payment-provider-square`](./money/payment/payment-provider-square/README.md) | Universal Square Payments API adapter for `@suaveplan/payment`. Ships the `SquareProvider` class (implements the structural `PaymentProvider` contract with lit… |
|
||||
| [`@suaveplan/payment-provider-stripe`](./money/payment/payment-provider-stripe/README.md) | Universal Stripe adapter for `@suaveplan/payment`. Implements `PaymentProvider` against the Stripe REST API using the slim `@suaveplan/payment-providers` found… |
|
||||
| [`@suaveplan/payment-providers`](./money/payment/payment-providers/README.md) | Universal **foundation** for the `@suaveplan/payment-provider-*` sibling-package family. This package ships only the shared abstractions every concrete adapter… |
|
||||
|
||||
### `networking`
|
||||
|
||||
Distributed networking, event buses, drive mesh, and network discovery.
|
||||
|
||||
#### `drive`
|
||||
|
||||
Virtual filesystem, mesh networking, and chunk-addressed storage plugins.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/drive`](./networking/drive/drive/README.md) | Modular virtual filesystem core for Suaveplan. File-level operations over `AbstractStorage` with a plugin slot for middleware, chunking, encryption, and P2P me… |
|
||||
| [`@suaveplan/drive-mesh`](./networking/drive/drive-mesh/README.md) | P2P mesh orchestrator over the drive chunks plugin. Consumes `ChunkSource` (from `@suaveplan/drive-plugin-chunks`) and one or more `DriveTransport` implementat… |
|
||||
| [`@suaveplan/drive-mesh-protocol`](./networking/drive/drive-mesh-protocol/README.md) | Wire protocol for chunk discovery, request, and delivery across the mesh. Three envelope shapes (`cd:request`, `cd:advertise`, `cd:response`) with three reques… |
|
||||
| [`@suaveplan/drive-plugin-cache`](./networking/drive/drive-plugin-cache/README.md) | Read-through cache middleware for `@suaveplan/drive`. Accepts a caller-supplied `AbstractCache<Uint8Array>` (from `@suaveplan/cache`) and intercepts every `dri… |
|
||||
| [`@suaveplan/drive-plugin-chunks`](./networking/drive/drive-plugin-chunks/README.md) | Chunk-addressing plugin for `@suaveplan/drive`. Splits files into signed, encrypted chunks using the locked `version 0x0001` binary format, with server-signed… |
|
||||
| [`@suaveplan/drive-react-sharing`](./networking/drive/drive-react-sharing/README.md) | React-tier UI surface for `@suaveplan/drive-sharing`. Three controlled components plus a real-engine-backed hook. |
|
||||
| [`@suaveplan/drive-sharing`](./networking/drive/drive-sharing/README.md) | Partitioned drive sharing engine for the Suaveplan ecosystem. Ships envelope encryption with mandatory AAD, per-user key wrapping, partition policies with dete… |
|
||||
| [`@suaveplan/drive-storage-archive-core`](./networking/drive/drive-storage-archive-core/README.md) | Shared archive-mounting primitives for the Suaveplan drive: magic-byte format sniffing, split-volume sibling resolution, password hook contract, mutation-mode… |
|
||||
| [`@suaveplan/drive-storage-zip`](./networking/drive/drive-storage-zip/README.md) | ZIP archive adapter for `@suaveplan/drive`. Reads and writes standards-compliant zip archives through the structural `DriveStorage` view; supports ZIP64, split… |
|
||||
| [`@suaveplan/drive-template`](./networking/drive/drive-template/README.md) | Server-signed drive templates for the dehydrated-drive workflow. A `DriveTemplate` describes a drive's full folder structure and per-file metadata; the actual… |
|
||||
| [`@suaveplan/drive-transport-https`](./networking/drive/drive-transport-https/README.md) | HTTPS-relay-backed `TransportLike<MessageEnvelope>` for the Suaveplan drive mesh. Implements long-poll receive, stateless POST send, exponential backoff with e… |
|
||||
| [`@suaveplan/drive-transport-webrtc`](./networking/drive/drive-transport-webrtc/README.md) | WebRTC datachannel transport for the drive mesh. Implements the canonical `DriveTransport` interface from `@suaveplan/drive-mesh` over a `@suaveplan/drive-webr… |
|
||||
| [`@suaveplan/drive-uri`](./networking/drive/drive-uri/README.md) | URI plugins (`drive://`, `drive+mesh://`, `drive+template://`) for `@suaveplan/storage-uri`'s `StorageUriResolver`. Wraps a caller-supplied `Drive` instance in… |
|
||||
| [`@suaveplan/drive-webrtc`](./networking/drive/drive-webrtc/README.md) | Browser-tier WebRTC P2P chunk-distribution plugin for `@suaveplan/drive`. Provides a complete mesh networking stack — signalling client, ICE-aware WebRTC clien… |
|
||||
|
||||
#### `events`
|
||||
|
||||
Typed event bus, event sourcing, metrics, and transport bridges.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/broadcast-channel`](./networking/events/broadcast-channel/README.md) | Typed cross-tab messaging built on the Web `BroadcastChannel` API. Browser-tier package — leader election, tab presence and heartbeats, message-type handler re… |
|
||||
| [`@suaveplan/event-sourcing`](./networking/events/event-sourcing/README.md) | Event sourcing patterns for Suaveplan: typed aggregate root, in-memory event store with optimistic concurrency, snapshot support, projections, upcasting, and O… |
|
||||
| [`@suaveplan/events`](./networking/events/events/README.md) | Typed event-bus substrate for Suaveplan. Pub/sub with deterministic FIFO delivery, interceptor API, bounded-queue backpressure, idempotent listeners, Zod-valid… |
|
||||
| [`@suaveplan/events-metrics`](./networking/events/events-metrics/README.md) | OTel metrics companion for `@suaveplan/events`. Attaches via the public interceptor API to record dispatch totals, listener error counts, and latency histogram… |
|
||||
| [`@suaveplan/events-otel`](./networking/events/events-otel/README.md) | OpenTelemetry tracing companion for `@suaveplan/events`. Wraps each dispatch in a span, propagates W3C Trace Context, and records listener exceptions as span e… |
|
||||
| [`@suaveplan/events-schema`](./networking/events/events-schema/README.md) | Runtime schema-validation companion for `@suaveplan/events`. Validates event payloads at dispatch boundaries via Zod schemas with strict/warn/sample modes and… |
|
||||
| [`@suaveplan/events-transport`](./networking/events/events-transport/README.md) | Cross-context event bridges for `@suaveplan/events`: `MessageChannel` (at-least-once with ack/retry) and `BroadcastChannel` (at-most-once, fan-out) transports… |
|
||||
|
||||
#### `network`
|
||||
|
||||
Mesh networking, WebSocket transports, bootstrap, and discovery protocols.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/network`](./networking/network/network/README.md) | Core network interfaces and runtime for the mesh system. |
|
||||
| [`@suaveplan/network-bootstrap`](./networking/network/network-bootstrap/README.md) | Pluggable peer-discovery chain, persistent peer cache, and `NetworkPlugin` wrapper for the `@suaveplan/network` mesh. Provides the canonical way for a freshly… |
|
||||
| [`@suaveplan/network-discovery-crdt`](./networking/network/network-discovery-crdt/README.md) | Browser-tier `DiscoveryStrategy` for `@suaveplan/network-bootstrap`. Wraps an awareness-document CRDT (Yjs *or* Automerge — chosen at construction) and produce… |
|
||||
| [`@suaveplan/network-discovery-gundb`](./networking/network/network-discovery-gundb/README.md) | Browser-tier `DiscoveryStrategy` for `@suaveplan/network-bootstrap` chains. Wraps a GunDB instance — gossip-over- WebSocket decentralised graph database — and… |
|
||||
| [`@suaveplan/network-discovery-hybrid`](./networking/network/network-discovery-hybrid/README.md) | Browser-tier multi-strategy DiscoveryStrategy orchestrator: composes any subset of the five Wave-2 sibling strategies (peerjs / crdt / trystero / webtorrent / gundb) into a single HybridDiscoveryManager with priority-based peer selection, per-strategy health tracking, automatic fallback, de-duplicated peer set with PeerSource provenance, and unified lifecycle. Implements the network-bootstrap DiscoveryStrategy contract so it can itself be chained. |
|
||||
| [`@suaveplan/network-discovery-peerjs`](./networking/network/network-discovery-peerjs/README.md) | Browser-tier `DiscoveryStrategy` for `@suaveplan/network-bootstrap` that wraps a PeerJS signalling-server client + WebRTC `DataConnection`s into a deduped peer… |
|
||||
| [`@suaveplan/network-discovery-trystero`](./networking/network/network-discovery-trystero/README.md) | Browser-tier `DiscoveryStrategy` for `@suaveplan/network-bootstrap`. Serverless WebRTC peer discovery via Trystero, with four interchangeable transports — BitT… |
|
||||
| [`@suaveplan/network-discovery-webtorrent`](./networking/network/network-discovery-webtorrent/README.md) | Browser-tier `DiscoveryStrategy` for `@suaveplan/network-bootstrap` chains. Wraps a `p2pt` instance — WebTorrent-tracker-driven WebRTC peer discovery — and exp… |
|
||||
| [`@suaveplan/network-gundb-server`](./networking/network/network-gundb-server/README.md) | Server-tier GunDB relay daemon. Hosts a Gun relay peer plus standard `/health` / `/ready` / `/live` probes on a single Node `http.Server`. Browser-tier clients… |
|
||||
| [`@suaveplan/network-libp2p-server`](./networking/network/network-libp2p-server/README.md) | Server-tier libp2p relay daemon paired with `@suaveplan/network-transport-libp2p`. Hosts a libp2p node configured as a bootstrap + circuit-relay-v2 server alon… |
|
||||
| [`@suaveplan/network-plugin-assignment`](./networking/network/network-plugin-assignment/README.md) | Mesh auto-assignment and load balancing: live `MeshCapacityTracker`, weighted-multi-criteria `AssignmentEngine`, imbalance-detecting `MeshBalancer` planner, an… |
|
||||
| [`@suaveplan/network-plugin-audit-mesh`](./networking/network/network-plugin-audit-mesh/README.md) | Tamper-evident, cryptographically signed, append-only audit log chain for the Suaveplan mesh. `SignedLog` produces hash-chained `LogEntry` records keyed by mon… |
|
||||
| [`@suaveplan/network-plugin-auth`](./networking/network/network-plugin-auth/README.md) | Mesh membership authentication and cross-mesh relay authorization. Bridges `@suaveplan/auth` into the mesh: peer join verification, capability-tier-bound token… |
|
||||
| [`@suaveplan/network-plugin-circuit-breaker`](./networking/network/network-plugin-circuit-breaker/README.md) | Mesh-level circuit breakers: per-path state machine (`CLOSED → OPEN → HALF_OPEN`), registry, half-open probe loop, and relay middleware integration. Fast-fails… |
|
||||
| [`@suaveplan/network-plugin-content-delivery`](./networking/network/network-plugin-content-delivery/README.md) | Mesh-as-CDN core for the Suaveplan network. `ChunkIndex` maintains a distributed holder map keyed by chunk hash with gossip-style merge over `ChunkAdvertisemen… |
|
||||
| [`@suaveplan/network-plugin-crypto-messages`](./networking/network/network-plugin-crypto-messages/README.md) | Cryptographic integrity layer for the Suaveplan mesh. Ed25519-backed `MessageSigner` (sign on send, verify against the registry on receive); `SessionTokenAutho… |
|
||||
| [`@suaveplan/network-plugin-devtools`](./networking/network/network-plugin-devtools/README.md) | Developer-tools plugin for the Suaveplan mesh. Bundles four subsystems behind a single `NetworkPlugin` lifecycle so engineers can inspect, visualise, and profi… |
|
||||
| [`@suaveplan/network-plugin-failover`](./networking/network/network-plugin-failover/README.md) | Mesh failover plugin: per-peer heartbeat manager, alive→suspect→dead→recovering health monitor, debounced promoter that triggers emergency elections, and a coo… |
|
||||
| [`@suaveplan/network-plugin-governance`](./networking/network/network-plugin-governance/README.md) | Election engine for the Suaveplan mesh: candidate scoring, reputation tracking, voting, and host transitions. |
|
||||
| [`@suaveplan/network-plugin-intra-relay`](./networking/network/network-plugin-intra-relay/README.md) | Intra-mesh peer-to-peer relay for the Suaveplan network. When direct WebRTC fails (symmetric NAT, restrictive firewall), `IntraRelayPlugin` routes the message… |
|
||||
| [`@suaveplan/network-plugin-middleware`](./networking/network/network-plugin-middleware/README.md) | Priority-ordered middleware chain for the Suaveplan mesh. Inbound and outbound message flow each pass through the same five-band pipeline (CRITICAL → HIGH → NO… |
|
||||
| [`@suaveplan/network-plugin-migration`](./networking/network/network-plugin-migration/README.md) | Peer state migration between meshes: 8-step `MigrationCoordinator`, snapshot serialize/deserialize via `StateTransferProtocol`, graceful-handoff `ForwardingTun… |
|
||||
| [`@suaveplan/network-plugin-monitor`](./networking/network/network-plugin-monitor/README.md) | Production monitoring + distributed tracing for the Suaveplan mesh. Collects metrics across four categories (node / connection / message / hierarchy), supports… |
|
||||
| [`@suaveplan/network-plugin-partition`](./networking/network/network-plugin-partition/README.md) | Split-brain detection and resolution for the mesh: epoch-based `PartitionDetector` (deterministic partition-id from sorted reachable set), `QuorumResolver` wit… |
|
||||
| [`@suaveplan/network-plugin-protocol`](./networking/network/network-plugin-protocol/README.md) | Network protocol plugin for the mesh system. |
|
||||
| [`@suaveplan/network-plugin-qos`](./networking/network/network-plugin-qos/README.md) | Bandwidth management and quality-of-service for the mesh: bandwidth-quota enforcer, message-priority classifier, QoS router (bypass / drop / queue / defer), co… |
|
||||
| [`@suaveplan/network-plugin-registry`](./networking/network/network-plugin-registry/README.md) | Fixed-capacity peer registry with bitpacker-based binary encoding. |
|
||||
| [`@suaveplan/network-plugin-relay`](./networking/network/network-plugin-relay/README.md) | Cross-mesh relay — the federation layer that lets Tier 0 hosts of different meshes route messages to each other. Composes `RelayRouter`, `HostBackboneTransport… |
|
||||
| [`@suaveplan/network-plugin-server-bridge`](./networking/network/network-plugin-server-bridge/README.md) | Tier-0 host ↔ central-server bridge for the Suaveplan mesh. |
|
||||
| [`@suaveplan/network-plugin-session`](./networking/network/network-plugin-session/README.md) | Hierarchical session management for the Suaveplan mesh. `SessionTree` maintains a parent-child session graph rooted at a single auto-created root; `SessionMana… |
|
||||
| [`@suaveplan/network-plugin-simulation`](./networking/network/network-plugin-simulation/README.md) | Deterministic virtual-network simulation framework for the Suaveplan mesh. `NetworkSimulator` runs scenarios against a seeded RNG with simulated time (accelera… |
|
||||
| [`@suaveplan/network-plugin-state`](./networking/network/network-plugin-state/README.md) | Mesh-wide synchronized state for the Suaveplan network. Path-based `StateManager` (versioned history with sequence numbers + per-path subscriptions); `DeltaCom… |
|
||||
| [`@suaveplan/network-plugin-topology`](./networking/network/network-plugin-topology/README.md) | Tiered mesh topology plugin: 3-tier `HierarchicalTopology` (Host / SubHost / Leaf) with promote/demote, latency-bucket-based `LatencyAssigner`, `GroupManager`,… |
|
||||
| [`@suaveplan/network-plugin-transport-manager`](./networking/network/network-plugin-transport-manager/README.md) | Multi-transport orchestration for the Suaveplan mesh. Maintains a pool of registered transports per peer, negotiates capability bitmaps during handshake, route… |
|
||||
| [`@suaveplan/network-testing`](./networking/network/network-testing/README.md) | <!-- begin:auto-docs --> |
|
||||
| [`@suaveplan/network-transport-hyperswarm`](./networking/network/network-transport-hyperswarm/README.md) | Server-tier Hyperswarm DHT transport for `@suaveplan/network`. Provides hole-punched peer-to-peer streams discovered via Kademlia DHT topics. |
|
||||
| [`@suaveplan/network-transport-libp2p`](./networking/network/network-transport-libp2p/README.md) | Universal libp2p `Transport` for `@suaveplan/network`. Composes WebSockets or WebRTC sub-transports with Noise + Yamux, identify + ping + bootstrap + circuit-r… |
|
||||
| [`@suaveplan/network-transport-standby`](./networking/network/network-transport-standby/README.md) | Pre-emptive WebRTC standby transport for `@suaveplan/network`. Maintains a pool of warm `RTCPeerConnection`-like slots so peer attach completes in ~10 ms inste… |
|
||||
| [`@suaveplan/network-transport-webrtc`](./networking/network/network-transport-webrtc/README.md) | WebRTC transport plugin for mesh networking. |
|
||||
| [`@suaveplan/network-transport-websockets`](./networking/network/network-transport-websockets/README.md) | WebSocket client utilities with typed message framing, connection lifecycle management, heartbeat, backpressure queue, and injectable transport adapters. |
|
||||
| [`@suaveplan/network-trystero-server`](./networking/network/network-trystero-server/README.md) | Server-tier Trystero signaling daemon. Hosts an MQTT-style or Nostr-style WebSocket broker shim plus standard `/health` / `/ready` / `/live` probes on a single… |
|
||||
| [`@suaveplan/react-network-libp2p-demo`](./networking/network/react-network-libp2p-demo/README.md) | React-tier demo UI for `@suaveplan/network-transport-libp2p`. Splits the upstream 749-LOC monolithic demo into per-concern subcomponents (`<NodeStatusPanel>`,… |
|
||||
| [`@suaveplan/service-discovery`](./networking/network/service-discovery/README.md) | Universal service-registry primitives. Provides the shared `ServiceDescriptor`, `ServiceQuery`, `RegistryEvent`, and `ServiceRegistry` contract consumed by eve… |
|
||||
| [`@suaveplan/websocket-gateway`](./networking/network/websocket-gateway/README.md) | Server-tier WebSocket gateway daemon with rooms, presence tracking, and channel-multiplexed pub/sub. Pairs with `@suaveplan/network-transport-websockets` on th… |
|
||||
|
||||
### `observability`
|
||||
|
||||
Monitoring, logging, health checks, audit trails, and analytics.
|
||||
|
||||
#### `monitor`
|
||||
|
||||
Logging, health checks, audit logging, and analytics instrumentation.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/analytics-browser`](./observability/monitor/analytics-browser/README.md) | Browser-tier client-side product analytics — `AnalyticsTracker`, `BatchTransport`, `BeaconTransport`, and `SessionManager` with consent gate, PII scrubber, hea… |
|
||||
| [`@suaveplan/audit-logging`](./observability/monitor/audit-logging/README.md) | Tamper-proof, GDPR-compliant audit logging for Suaveplan. Provides a cryptographic chain-integrity engine, pluggable storage adapters, GDPR data export, config… |
|
||||
| [`@suaveplan/health-check`](./observability/monitor/health-check/README.md) | Server-tier health-check core for `/health`, `/ready`, `/live` endpoints with dependency checks (HTTP, TCP, custom), aggregated status, caching, and typed life… |
|
||||
| [`@suaveplan/logger`](./observability/monitor/logger/README.md) | Enterprise-grade structured logger for Suaveplan. Provides a full pipeline: level filtering → OTel trace enrichment → sampling → redaction → sinks. Supports mu… |
|
||||
| [`@suaveplan/opentelemetry`](./observability/monitor/opentelemetry/README.md) | OpenTelemetry SDK setup and configuration helpers for Suaveplan. Provides batteries-included OTel initialization helpers, resource detection, ergonomic span/me… |
|
||||
|
||||
### `persistence`
|
||||
|
||||
Caching, database adapters, search engines, and storage abstractions.
|
||||
|
||||
#### `cache`
|
||||
|
||||
Caching abstractions, memory caches, and Redis backends.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/cache`](./persistence/cache/cache/README.md) | Type-safe caching abstraction for Suaveplan. Provides `MemoryCache` with LRU/LFU/FIFO/TTL eviction, `NamespacedCache` for shared-storage isolation, a `CacheFac… |
|
||||
| [`@suaveplan/cache-redis`](./persistence/cache/cache-redis/README.md) | `AbstractCache<T>` backend over a minimal `RedisLike` interface — native `SET ... EX`, `GET`, `DEL`, `EXISTS`, `TTL`, `SCAN`. Reuses `@suaveplan/cache-storage`… |
|
||||
| [`@suaveplan/cache-storage`](./persistence/cache/cache-storage/README.md) | `AbstractCache<T>` backend that bridges any `@suaveplan/storage` `AbstractStorage` into a typed cache via a 32-byte fixed-width TTL header and pluggable `Codec… |
|
||||
|
||||
#### `database`
|
||||
|
||||
Database adapters, migrations, seeding, and SQLite support.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/db`](./persistence/database/db/README.md) | Universal relational database adapter interface for Suaveplan. Provides the `DatabaseAdapter` contract (extending `DatabaseLike` from `@suaveplan/types`), inje… |
|
||||
| [`@suaveplan/db-sqlite`](./persistence/database/db-sqlite/README.md) | Universal SQLite adapter for Suaveplan. Runs on `bun:sqlite` on the Bun server and `@sqlite.org/sqlite-wasm` (Web Worker) in the browser, unified behind a sing… |
|
||||
| [`@suaveplan/migration`](./persistence/database/migration/README.md) | Database-agnostic schema migration runner with ledger tracking, checksum validation, dry-run reporting, distributed locking, event bus integration, and OTel in… |
|
||||
| [`@suaveplan/seed`](./persistence/database/seed/README.md) | Universal declarative database seeding and fixture framework. Define seed data once, let the runner handle dependency ordering, environment filtering, idempote… |
|
||||
| [`@suaveplan/surrealdb`](./persistence/database/surrealdb/README.md) | Universal SurrealDB client wrapper for Suaveplan. Implements `@suaveplan/db`'s `DatabaseAdapter` interface with connection lifecycle management, typed CRUD, sc… |
|
||||
|
||||
#### `search`
|
||||
|
||||
Full-text search, BM25, fuzzy matching, and faceted search.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/search`](./persistence/search/search/README.md) | Full-text and fuzzy search primitives with in-memory BM25 engine, trigram fuzzy matching, faceted search, and pagination support. |
|
||||
|
||||
#### `storage`
|
||||
|
||||
Storage abstractions, browser backends, S3, CDN, and URI routing.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/storage`](./persistence/storage/storage/README.md) | Enterprise key-value storage abstractions for Suaveplan. Provides `AbstractStorage` (implementing `StorageLike`), `MemoryStorage` with TTL and zero-copy stream… |
|
||||
| [`@suaveplan/storage-browser-indexeddb`](./persistence/storage/storage-browser-indexeddb/README.md) | Browser-tier `AbstractStorage` adapter backed by the W3C IndexedDB API (`globalThis.indexedDB`). Provides persistent, origin-scoped key-value storage for brows… |
|
||||
| [`@suaveplan/storage-browser-localstorage`](./persistence/storage/storage-browser-localstorage/README.md) | Browser-tier `AbstractStorage` adapter backed by the synchronous `Storage` API (`globalThis.localStorage`). Provides namespaced, origin-scoped key-value storag… |
|
||||
| [`@suaveplan/storage-browser-opfs`](./persistence/storage/storage-browser-opfs/README.md) | Browser-tier `AbstractStorage` adapter backed by the Origin Private File System (OPFS). Provides persistent, origin-scoped key-value storage for browser consum… |
|
||||
| [`@suaveplan/storage-cdn`](./persistence/storage/storage-cdn/README.md) | Types-only foundation for CDN-flavoured `@suaveplan/storage` adapters. Declares `CdnAdapter` (an abstract base class extending `AbstractStorage` with `purge`,… |
|
||||
| [`@suaveplan/storage-cdn-bunnycdn`](./persistence/storage/storage-cdn-bunnycdn/README.md) | BunnyCDN Storage Zone + Pull Zone provider for `@suaveplan/storage-cdn`. Full `AbstractStorage` and `CdnAdapter` surface — `get`/`set`/`delete`/`exists`/`getMe… |
|
||||
| [`@suaveplan/storage-cdn-rackspace`](./persistence/storage/storage-cdn-rackspace/README.md) | Rackspace Cloud Files (OpenStack Swift) origin + Rackspace CDN provider for `@suaveplan/storage-cdn`. Full `AbstractStorage` and `CdnAdapter` surface — `get`/`… |
|
||||
| [`@suaveplan/storage-file`](./persistence/storage/storage-file/README.md) | Node.js filesystem `AbstractStorage` adapter for `@suaveplan/storage`. Implements atomic writes via temp-file-then-rename, AbortSignal threaded through every a… |
|
||||
| [`@suaveplan/storage-http`](./persistence/storage/storage-http/README.md) | Universal HTTP/CDN-backed `AbstractStorage` adapter for `@suaveplan/storage`. Reads via `GET`/`HEAD`/`Range` against any `<baseUrl><key>` origin, with optional… |
|
||||
| [`@suaveplan/storage-plugin-cache`](./persistence/storage/storage-plugin-cache/README.md) | Read-through cache decorator for `AbstractStorage`. Wraps any `@suaveplan/storage` instance with an injectable `AbstractCache<Uint8Array>`; hit/miss/populate o… |
|
||||
| [`@suaveplan/storage-s3`](./persistence/storage/storage-s3/README.md) | S3-compatible object storage adapter for `@suaveplan/storage`. Extends `AbstractStorage` backed by `@aws-sdk/client-s3` with presigned URL generation, multipar… |
|
||||
| [`@suaveplan/storage-uri`](./persistence/storage/storage-uri/README.md) | Universal URI-routed dispatch over `@suaveplan` `StorageLike` adapters. |
|
||||
| [`@suaveplan/storage-uri-plugin-bundle`](./persistence/storage/storage-uri-plugin-bundle/README.md) | Per-package `bundle://` URI scheme for `@suaveplan/storage-uri`. |
|
||||
| [`@suaveplan/storage-uri-plugin-fallback`](./persistence/storage/storage-uri-plugin-fallback/README.md) | Universal `fallback://` URI plugin for `@suaveplan/storage-uri`. Ships ordered multi-source resolution with cycle detection, depth cap, mirror writes, list uni… |
|
||||
|
||||
### `security-stack`
|
||||
|
||||
Cryptography, vaults, security utilities, and threat-detection primitives.
|
||||
|
||||
#### `crypto`
|
||||
|
||||
Cryptographic primitives, ciphers, hashes, curves, and key management.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/crypto`](./security-stack/crypto/crypto/README.md) | Universal cryptographic primitives — engine-pluggable AEAD, Ed25519/ECDSA-P256 signing, X25519 key agreement, sealed-box, and auth-box. Built on audited @noble… |
|
||||
| [`@suaveplan/crypto-bin`](./security-stack/crypto/crypto-bin/README.md) | Suaveplan password-hashing CLI. Ships the `suaveplan-hash-password` binary — Argon2id (default) and BLAKE2b password hashing + verification with PHC/MCF-format… |
|
||||
| [`@suaveplan/crypto-browser`](./security-stack/crypto/crypto-browser/README.md) | WebCrypto-backed engine implementations for `@suaveplan/crypto` — AES-256-GCM, Ed25519, ECDSA-P256/P384, X25519, ECDH-P256/P384, RSA, AES-KW, and non-extractab… |
|
||||
| [`@suaveplan/crypto-cipher`](./security-stack/crypto/crypto-cipher/README.md) | Low-level AEAD (Authenticated Encryption with Associated Data) cipher primitives for the `@suaveplan/crypto` stack. Provides three algorithms — AES-256-GCM, Ch… |
|
||||
| [`@suaveplan/crypto-cli`](./security-stack/crypto/crypto-cli/README.md) | Suaveplan keychain CLI — root + server identity lifecycle (init, roll, revoke, inspect, verify, export-public, restore) |
|
||||
| [`@suaveplan/crypto-core`](./security-stack/crypto/crypto-core/README.md) | Universal cryptographic primitives — engine-pluggable AEAD, Ed25519/ECDSA-P256 signing, X25519 key agreement, sealed-box, and auth-box. Built on audited @noble… |
|
||||
| [`@suaveplan/crypto-curve`](./security-stack/crypto/crypto-curve/README.md) | Low-level elliptic-curve building blocks for the `@suaveplan/crypto` stack. Exposes four curves — secp256k1, Ed25519, X25519, and NIST P-256 — each as a standa… |
|
||||
| [`@suaveplan/crypto-hash`](./security-stack/crypto/crypto-hash/README.md) | Pure-TypeScript hashing substrate. SHA-2 digests, HMAC, HKDF, PBKDF2, Argon2id, and hex/Base64 encoding — no WASM, no browser globals, fully tree-shakeable. |
|
||||
| [`@suaveplan/crypto-keys`](./security-stack/crypto/crypto-keys/README.md) | Hierarchical keychain chain-signing for the crypto ecosystem. Store-agnostic, free of `node:` / `bun:` imports. |
|
||||
| [`@suaveplan/crypto-rsa`](./security-stack/crypto/crypto-rsa/README.md) | RSA primitives for Suaveplan — RFC 8017 PKCS #1 v2.2, RFC 5208 / RFC 5958 PKCS #8, RFC 5280 SPKI, RFC 7517 / RFC 7518 JWK, RFC 7468 PEM. Universal package (zer… |
|
||||
| [`@suaveplan/crypto-sodium`](./security-stack/crypto/crypto-sodium/README.md) | libsodium-backed engine implementations for `@suaveplan/crypto`. Provides AEAD encryption, Ed25519 signing, X25519 key agreement, sealed-box, auth-box, determi… |
|
||||
| [`@suaveplan/crypto-utils`](./security-stack/crypto/crypto-utils/README.md) | Shared cryptographic utilities consumed internally by all other `@suaveplan/crypto-*` packages. Provides the low-level primitives that every crypto implementat… |
|
||||
| [`@suaveplan/crypto-vault`](./security-stack/crypto/crypto-vault/README.md) | Cryptographic foundation for the Vaulted password-manager ecosystem. Provides BIP39 mnemonic generation, OWASP-grade Argon2id derivation, two-secret key deriva… |
|
||||
|
||||
#### `security`
|
||||
|
||||
Have-I-Been-Pwned checks, JWS, licensing, and OTP utilities.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/hibp`](./security-stack/security/hibp/README.md) | Privacy-preserving Have I Been Pwned v3 client. Universal — works in Node, Bun, Deno, browsers, edge workers. The only required runtime primitive is `fetch`. |
|
||||
| [`@suaveplan/jws`](./security-stack/security/jws/README.md) | RFC 7515 **JSON Web Signature** (verify + sign). Pure TypeScript over `Uint8Array` + `DataView` plus the workspace crypto primitives (`@suaveplan/crypto-rsa`,… |
|
||||
| [`@suaveplan/license`](./security-stack/security/license/README.md) | Compact, offline-verifiable, secp256k1-signed software-license-key format. Bit-for-bit wire-format compatible with upstream `@suaveplan/license`. runs unchange… |
|
||||
| [`@suaveplan/otp`](./security-stack/security/otp/README.md) | RFC 4226 (HOTP) + RFC 6238 (TOTP) + RFC 6287 (OCRA) + Google Authenticator Key URI Format + Base32 (RFC 4648 §6) + recovery codes + clock-drift detector — zero browser/Node deps |
|
||||
| [`@suaveplan/password-formats`](./security-stack/security/password-formats/README.md) | Universal import/export layer for common password-manager export formats. Parses Apple Passwords CSV, Bitwarden JSON, Chrome CSV, and KeePass XML into a normal… |
|
||||
| [`@suaveplan/password-generator`](./security-stack/security/password-generator/README.md) | Cryptographically-strong password and passphrase generator — char-class, pronounceable, EFF Diceware, and PIN modes with bias-free RNG, real entropy estimates,… |
|
||||
| [`@suaveplan/password-strength`](./security-stack/security/password-strength/README.md) | zxcvbn-style password strength estimator — pattern-matching + dictionary cracker model per Wheeler 2016. Universal; zero browser/Node/React deps. Re-implementa… |
|
||||
| [`@suaveplan/security-codes`](./security-stack/security/security-codes/README.md) | Detect and sanitise sensitive security-code leakage in strings. Handles API keys, JWTs, credit cards, GitHub tokens, Stripe keys, SSH keys, PEM blocks, AWS acc… |
|
||||
| [`@suaveplan/server-keys`](./security-stack/security/server-keys/README.md) | Operator-facing CLI bundle for the Suaveplan server-key lifecycle: 18 bins covering root + server keychain generation (including hierarchical `ChainedKeySigner… |
|
||||
| [`@suaveplan/slsa`](./security-stack/security/slsa/README.md) | SLSA L1-L2 supply-chain security for Suaveplan. Generates and verifies in-toto / SLSA v1.0 provenance, CycloneDX 1.5 SBOMs, DSSE attestation envelopes, and RFC… |
|
||||
| [`@suaveplan/srp`](./security-stack/security/srp/README.md) | SRP-6a zero-knowledge password proof per RFC 5054 / RFC 2945. Universal — client and server roles in one package, bundled 2048-bit safe-prime group, optional 1… |
|
||||
| [`@suaveplan/tpm-struct`](./security-stack/security/tpm-struct/README.md) | Zero-runtime-dep TPM 2.0 attestation-structure parser and serialiser. Decodes `TPMS_ATTEST` (TCG TPM 2.0 Library Part 2 §10.12), decodes `TPMT_PUBLIC` (§12.2),… |
|
||||
| [`@suaveplan/vault`](./security-stack/security/vault/README.md) | Vault orchestration layer for the Vaulted password-manager ecosystem. Provides a drive-backed encrypted SQLite-style container, a strict `locked` ↔ `unlocked`… |
|
||||
| [`@suaveplan/vault-sync`](./security-stack/security/vault-sync/README.md) | Domain-specific sync and conflict-resolution engine for the Vaulted password-manager ecosystem. Builds on `@suaveplan/crdt` and `@suaveplan/diff` (RFC 6902 JSO… |
|
||||
| [`@suaveplan/x509`](./security-stack/security/x509/README.md) | RFC 5280 X.509 v3 certificate parser and certification-path validator. Universal (zero browser, Node, or React deps). Pure TypeScript over `Uint8Array` / `Data… |
|
||||
|
||||
### `testkit`
|
||||
|
||||
Testing frameworks, A/B testing, and quality-assurance utilities.
|
||||
|
||||
#### `testing`
|
||||
|
||||
Testing runners, DOM helpers, E2E harnesses, and A/B testing.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/ab-testing`](./testkit/testing/ab-testing/README.md) | Enterprise A/B testing and experimentation engine with deterministic FNV-1a bucketing, targeting rules, variant assignment, conversion tracking, result aggrega… |
|
||||
| [`@suaveplan/testing`](./testkit/testing/testing/README.md) | Shared test infrastructure for Suaveplan. Provides runner primitives (Vitest or bun-test via conditional exports), structured builders, a deterministic FakeClo… |
|
||||
| [`@suaveplan/testing-dom`](./testkit/testing/testing-dom/README.md) | DOM and React component testing utilities for Suaveplan. Provides a single install that covers vitest config generation with WSL2-safe defaults, `@testing-libr… |
|
||||
| [`@suaveplan/testing-e2e`](./testkit/testing/testing-e2e/README.md) | Playwright E2E testing utilities for Suaveplan, with the default `browser` worker fixture launching `cloakbrowser` — a real Chromium binary with 49 source-leve… |
|
||||
| [`@suaveplan/testing-integration`](./testkit/testing/testing-integration/README.md) | Core integration test infrastructure for Vitest: fixture lifecycle management, health polling, and the global-setup bridge. |
|
||||
| [`@suaveplan/testing-integration-garage-s3`](./testkit/testing/testing-integration-garage-s3/README.md) | Garage S3 v2.0.0 testcontainers fixture for `@suaveplan/testing-integration`. |
|
||||
| [`@suaveplan/testing-integration-mailpit`](./testkit/testing/testing-integration-mailpit/README.md) | Mailpit testcontainers fixture + `assertMail()` helper for `@suaveplan/testing-integration`. |
|
||||
| [`@suaveplan/testing-integration-oauth-mock`](./testkit/testing/testing-integration-oauth-mock/README.md) | Real-HTTP mock OAuth 2.0 Identity Provider for integration tests. |
|
||||
| [`@suaveplan/testing-integration-postgres`](./testkit/testing/testing-integration-postgres/README.md) | PostgreSQL 16 testcontainers fixture for `@suaveplan/testing-integration`. |
|
||||
| [`@suaveplan/testing-integration-redis`](./testkit/testing/testing-integration-redis/README.md) | Redis 7 testcontainers fixture for `@suaveplan/testing-integration`. |
|
||||
| [`@suaveplan/testing-integration-step-ca`](./testkit/testing/testing-integration-step-ca/README.md) | A `ServiceFixture` for `smallstep/step-ca` that gives any integration test suite a real, ephemeral certificate authority — root cert, JWK provisioner, and an `… |
|
||||
| [`@suaveplan/testing-integration-surrealdb`](./testkit/testing/testing-integration-surrealdb/README.md) | SurrealDB v3 testcontainers fixture for `@suaveplan/testing-integration`. |
|
||||
| [`@suaveplan/testing-integration-typesense`](./testkit/testing/testing-integration-typesense/README.md) | Typesense 27 testcontainers fixture for `@suaveplan/testing-integration`. |
|
||||
|
||||
### `threejs`
|
||||
|
||||
Three.js and React Three Fiber integrations.
|
||||
|
||||
#### `r3f`
|
||||
|
||||
React Three Fiber networking and scene components.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/r3f-networking`](./threejs/r3f/r3f-networking/README.md) | R3F-tier multiplayer state-synchronisation for React-Three-Fiber scenes. Composes any `@suaveplan/network` `Transport` (canonical: `@suaveplan/network-transpor… |
|
||||
|
||||
### `ui`
|
||||
|
||||
React components, forms, and user-interface primitives.
|
||||
|
||||
#### `forms`
|
||||
|
||||
Form builders, validation, and React form components.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/form-builder`](./ui/forms/form-builder/README.md) | Universal dynamic form schema engine. Renderer-agnostic — no DOM, no React, no Node-specific globals. Drives every dynamic form surface in the Suaveplan. |
|
||||
| [`@suaveplan/react-forms`](./ui/forms/react-forms/README.md) | React forms library for the Suaveplan ecosystem. Wraps react-hook-form + Zod with a primitives + factory + recipes architecture: every user-facing string is ty… |
|
||||
|
||||
#### `react`
|
||||
|
||||
React hooks and components for audio, auth, and UI surfaces.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/react-audio`](./ui/react/react-audio/README.md) | React surface for the Suaveplan audio family. **Phase 1 of 3 — foundation** (provider + hooks + stores + error codes + a11y helpers). Components ship in Phase… |
|
||||
| [`@suaveplan/react-audio-fingerprint`](./ui/react/react-audio-fingerprint/README.md) | React 19 surface for client-side audio fingerprinting and identity matching. The package implements the classic Shazam-style constellation algorithm (2D-peak d… |
|
||||
| [`@suaveplan/react-audio-player`](./ui/react/react-audio-player/README.md) | Track-style media-player surface for the Suaveplan audio family. The package ships two kinds of exports: |
|
||||
| [`@suaveplan/react-audio-stream`](./ui/react/react-audio-stream/README.md) | React 19 surface for live audio streaming sessions: a Provider, six hooks, and four accessible components. Chunk C of the four-chunk `enterprise-react-audio-st… |
|
||||
| [`@suaveplan/react-auth`](./ui/react/react-auth/README.md) | React authentication core for Suaveplan apps — a typed wrapper around `better-auth/client`, an `AuthProvider` React context, an `AuthGuard` route component, `u… |
|
||||
| [`@suaveplan/react-auth-whitelabel`](./ui/react/react-auth-whitelabel/README.md) | White-label authentication React components with tenant branding wired through the `@suaveplan/react-ui` runtime theme-token mutator. |
|
||||
| [`@suaveplan/react-control-surface`](./ui/react/react-control-surface/README.md) | React 19 atomic-control surface renderer for `@suaveplan/control-surface-schema`. |
|
||||
| [`@suaveplan/react-midi`](./ui/react/react-midi/README.md) | React 19 adapter for the Web MIDI API. Provides a strict-mode-safe `<MidiProvider>` that requests `MIDIAccess` exactly once, seven typed hooks for common consu… |
|
||||
| [`@suaveplan/react-ui`](./ui/react/react-ui/README.md) | React UI component library — opinionated providers, theme/pack plugin contracts, bundled translations, telemetry, shadcn-compatible registry generator. |
|
||||
<!-- end:auto-docs -->
|
||||
3
docs/_intro.md
Normal file
3
docs/_intro.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `packages`
|
||||
|
||||
Suaveplan package tree. Every `@suaveplan/*` library lives under a category (`packages/<category>/`) and a subcategory (`packages/<category>/<subcategory>/`).
|
||||
106
docs/api-stack/README.md
Normal file
106
docs/api-stack/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# `api-stack`
|
||||
|
||||
API infrastructure, plugin framework, and HTTP orchestration layer for Suaveplan.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Subcategories
|
||||
|
||||
1 subcategory, 90 packages.
|
||||
|
||||
### `api`
|
||||
|
||||
API orchestration, plugin lifecycle, routing, workers, and tenant resolution.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/api`](./api/README.md) | The thin Hono-based application orchestrator for the Suaveplan API stack. Composes the four sibling packages — `@suaveplan/api-plugin`, `@suaveplan/api-tenant`… |
|
||||
| [`@suaveplan/api-2pc`](./api-2pc/README.md) | Two-phase commit coordinator for the Suaveplan API plugin system. Drives Postgres PREPARE TRANSACTION / COMMIT PREPARED / ROLLBACK PREPARED across registered p… |
|
||||
| [`@suaveplan/api-audit-file`](./api-audit-file/README.md) | File-rotating, HMAC-signed audit sink for `@suaveplan/api-host`. Implements `RequestAuditSink` from `@suaveplan/api-host/request-pipeline`: writes one JSON lin… |
|
||||
| [`@suaveplan/api-audit-stdout`](./api-audit-stdout/README.md) | Stdout audit sink for `@suaveplan/api-host`. Writes JSON-per-line audit records to a `LineWriter` (default `process.stdout`) with an HMAC-SHA-256 signature app… |
|
||||
| [`@suaveplan/api-auth`](./api-auth/README.md) | **$2** for the Suaveplan API plugin system. Defines the abstract session-resolver shape (`Request → Principal \| null`) used by the host on every request, plus… |
|
||||
| [`@suaveplan/api-auth-anonymous`](./api-auth-anonymous/README.md) | Stage-4 trivial session resolver for `@suaveplan/api`. Always returns `null` for every request. Use this as the tail of resolver chains for public endpoints, a… |
|
||||
| [`@suaveplan/api-auth-bearer-static`](./api-auth-bearer-static/README.md) | Stage-4 service-to-service `SessionResolver` adapter for the `@suaveplan/api` plugin system. Matches an inbound `Authorization: Bearer <token>` header against… |
|
||||
| [`@suaveplan/api-auth-better-auth`](./api-auth-better-auth/README.md) | Stage-6 Better-Auth adapter for `@suaveplan/api`. Ships two **independently-wireable** capability surfaces — `createBetterAuthSession` (implements `auth.sessio… |
|
||||
| [`@suaveplan/api-auth-jwt`](./api-auth-jwt/README.md) | Stage-4 concrete `SessionResolver` for the `@suaveplan/api` plugin system. Verifies a JWT presented on the `Authorization: Bearer <jwt>` header (or a configure… |
|
||||
| [`@suaveplan/api-auth-provider`](./api-auth-provider/README.md) | Abstract account-lifecycle contract for the Suaveplan API plugin system (contracts). This package is OPTIONAL: hosted-IdP deployments (Clerk, Auth0, Cognito) i… |
|
||||
| [`@suaveplan/api-auth-rolled`](./api-auth-rolled/README.md) | Stage-8.12 rolled session-token resolver for `@suaveplan/api`. Issues RS256-signed JWTs with rotating `kid` slots, verifies tokens through the rotator's active… |
|
||||
| [`@suaveplan/api-auth-static-principal`](./api-auth-static-principal/README.md) | Fixed-`Principal` session resolver for the `@suaveplan/api` plugin system. Returns the configured `Principal` verbatim on every request — no token decoding, no… |
|
||||
| [`@suaveplan/api-db-adapter`](./api-db-adapter/README.md) | Stage-4 bridge from the `@suaveplan/db` engine adapters to the plugin `AdminDatabaseLike` / `TenantScopedDatabaseLike` surfaces exposed by `@suaveplan/api-type… |
|
||||
| [`@suaveplan/api-db-sqlite-memory`](./api-db-sqlite-memory/README.md) | Stage-5 in-memory SQLite database adapter for the `@suaveplan/api` plugin host testing kit. Wraps `bun:sqlite` (in `:memory:` mode by default, or a file path f… |
|
||||
| [`@suaveplan/api-db-worker-proxy`](./api-db-worker-proxy/README.md) | Worker-side proxy for the api `db` capability. Marshals `AdminDatabaseLike` and `TenantScopedDatabaseLike` calls — including transactions and cursor streams —… |
|
||||
| [`@suaveplan/api-events-in-memory`](./api-events-in-memory/README.md) | In-memory events bus adapter for the `@suaveplan/api` testing kit. Wraps `@suaveplan/events`' production `EventBus` with a per-topic recording layer so tests c… |
|
||||
| [`@suaveplan/api-events-worker-proxy`](./api-events-worker-proxy/README.md) | Worker-tier proxy over a main-process `@suaveplan/events` `EventBus`. |
|
||||
| [`@suaveplan/api-graphql`](./api-graphql/README.md) | Stage-1 abstract GraphQL contract for the Suaveplan API plugin system. |
|
||||
| [`@suaveplan/api-graphql-apollo`](./api-graphql-apollo/README.md) | Stage-7 Apollo Server GraphQL engine adapter for the Suaveplan API plugin system. Implements the `GraphqlEngine` contract from `@suaveplan/api-graphql` and pas… |
|
||||
| [`@suaveplan/api-graphql-ui-altair`](./api-graphql-ui-altair/README.md) | Altair GraphQL UI plugin for the Suaveplan API plugin system. Structural sibling of `@suaveplan/api-graphql-ui-graphiql`. |
|
||||
| [`@suaveplan/api-graphql-ui-graphiql`](./api-graphql-ui-graphiql/README.md) | GraphiQL UI mount for the Suaveplan API plugin system. Stage 7 of the umbrella plan. |
|
||||
| [`@suaveplan/api-graphql-yoga`](./api-graphql-yoga/README.md) | Stage-7 GraphQL Yoga engine adapter. **Default GraphQL engine** for the Suaveplan API plugin system per design doc §10.5 — Bun-friendly, fastest for our usage… |
|
||||
| [`@suaveplan/api-host`](./api-host/README.md) | Host orchestration core for the Suaveplan API plugin system. Provides the stable error-code registry (HOST/PLUGIN/RELOAD/CAPABILITY/ADMIN/LEDGER), the author-facing definePlugin DX, the runtime CapabilityRegistry, the LifecycleDriver + ReloadDriver + PluginLedger + admin Hono sub-router, the ErrorBoundary, the §6 11-step RequestPipeline, the top-level createHost(config) entry point composing every internal driver, and the HostInstruments registry covering the §12.3 metric surface. The ./testing subpath is reserved for test utilities. |
|
||||
| [`@suaveplan/api-jobs-worker-proxy`](./api-jobs-worker-proxy/README.md) | Bidirectional `MessagePort` proxy that lets a worker-isolated API plugin participate in the host's main-process job scheduler. The worker calls `registerCron`… |
|
||||
| [`@suaveplan/api-lifecycle`](./api-lifecycle/README.md) | Stage-1 contract package for the Suaveplan API plugin system: the plugin-lifecycle finite state machine definition, the legal-transition table, the side-effect… |
|
||||
| [`@suaveplan/api-loader`](./api-loader/README.md) | Stage-1 contract package for the Suaveplan API plugin system. |
|
||||
| [`@suaveplan/api-logger-worker-proxy`](./api-logger-worker-proxy/README.md) | Stage-3 worker → main proxy for `@suaveplan/logger`. Lets a worker-isolated API plugin call a normal `LoggerLike` surface while the records cross a `MessagePor… |
|
||||
| [`@suaveplan/api-manifest`](./api-manifest/README.md) | Stage-1 contract package of the Suaveplan API plugin system: Zod schemas, parser, cross-field validators, dependency-graph resolver, capability resolver, and c… |
|
||||
| [`@suaveplan/api-migrations`](./api-migrations/README.md) | Database-agnostic migration runner for the Suaveplan API. Ships a narrow `MigrationAdapter` surface (six methods), three shipped adapters (`surrealql`, `postgr… |
|
||||
| [`@suaveplan/api-openapi`](./api-openapi/README.md) | OpenAPI 3.1 registry, typed `RouteBuilder`, schema registry, and document merger for the Suaveplan API. Single source of truth that wires into Hono via `OpenAP… |
|
||||
| [`@suaveplan/api-openapi-ui-redoc`](./api-openapi-ui-redoc/README.md) | Stage-8 ReDoc UI adapter for the Suaveplan api-host. Mounts the ReDoc HTML shell at `/docs/ui/redoc` (or any caller-chosen path) via `createRedocUiHandler` and… |
|
||||
| [`@suaveplan/api-openapi-ui-scalar`](./api-openapi-ui-scalar/README.md) | Scalar UI adapter for the Suaveplan OpenAPI plugin system. |
|
||||
| [`@suaveplan/api-openapi-ui-swagger`](./api-openapi-ui-swagger/README.md) | Swagger UI adapter for the `/docs/ui/swagger` endpoint of the Suaveplan API plugin system (Stage 8.8). Returns a pure HTTP handler whose response is a `text/ht… |
|
||||
| [`@suaveplan/api-plugin`](./api-plugin/README.md) | Plugin-system foundation for the Suaveplan API. Universal — runs unchanged in Bun, Node, and the browser. |
|
||||
| [`@suaveplan/api-plugin-audit`](./api-plugin-audit/README.md) | Hono plugin that wraps `@suaveplan/audit-logging` as a write-through audit trail for `@suaveplan/api`. Installs a global per-request audit middleware (method /… |
|
||||
| [`@suaveplan/api-plugin-auth`](./api-plugin-auth/README.md) | Better-Auth-first authentication plugin for `@suaveplan/api`. The canonical exemplar of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` packag… |
|
||||
| [`@suaveplan/api-plugin-campaigns`](./api-plugin-campaigns/README.md) | Campaigns + suppression + delivery-webhooks HTTP plugin for `@suaveplan/api`. Mounts the `/campaigns`, `/suppression`, and `/webhooks` route trees on a Hono `O… |
|
||||
| [`@suaveplan/api-plugin-cms`](./api-plugin-cms/README.md) | Headless content-management primitives for `@suaveplan/api`. Ships an in-memory `CmsEngine` that manages flexible content models, content entries with a `draft… |
|
||||
| [`@suaveplan/api-plugin-database`](./api-plugin-database/README.md) | SurrealDB-backed database plugin for `@suaveplan/api`. Pools `@suaveplan/surrealdb`'s `SurrealAdapter` (implementing `@suaveplan/db`'s `DatabaseAdapter`), runs… |
|
||||
| [`@suaveplan/api-plugin-event-bus`](./api-plugin-event-bus/README.md) | Typed event-bus plugin for `@suaveplan/api`. Wraps `@suaveplan/events` (pub/sub, async dispatch, interceptors, bounded-queue backpressure, idempotent listener… |
|
||||
| [`@suaveplan/api-plugin-events`](./api-plugin-events/README.md) | Event-management plugin for `@suaveplan/api`. Ships a SurrealDB-backed `EventRepository` + `RsvpRepository` pair, ten Hono OpenAPI routes (list / get / create… |
|
||||
| [`@suaveplan/api-plugin-example`](./api-plugin-example/README.md) | Reference plugin for the `@suaveplan/api-host` plugin system. The package is a load-bearing integration target, not a tutorial: every contract surface the host… |
|
||||
| [`@suaveplan/api-plugin-export`](./api-plugin-export/README.md) | Data-export plugin for `@suaveplan/api`. Turns any registered resource into a CSV (RFC 4180), JSON, or XLSX (SpreadsheetML XML) blob through three self-contain… |
|
||||
| [`@suaveplan/api-plugin-file-upload`](./api-plugin-file-upload/README.md) | Multipart and chunked file-upload plugin for `@suaveplan/api`. Ships six OpenAPI-typed Hono routes, a pluggable storage + session-store contract, SHA-256 hash… |
|
||||
| [`@suaveplan/api-plugin-graphql`](./api-plugin-graphql/README.md) | GraphQL description + resolver-composition layer for `@suaveplan/api`. Server-tier — runs in Bun and Node. |
|
||||
| [`@suaveplan/api-plugin-import`](./api-plugin-import/README.md) | Tabular-data ingest plugin for `@suaveplan/api`. Parses CSV / JSON / TSV-XLSX payloads, applies declarative column mappings + per-field validation + computed f… |
|
||||
| [`@suaveplan/api-plugin-notifications`](./api-plugin-notifications/README.md) | SurrealDB-backed notifications + push plugin for `@suaveplan/api`. Ships a `NotificationRepository` + `DeviceTokenRepository` pair, an `ApnService` with a plug… |
|
||||
| [`@suaveplan/api-plugin-payment`](./api-plugin-payment/README.md) | Payment-lifecycle plugin for `@suaveplan/api`. Wraps `@suaveplan/payment` + a caller-injected `PaymentProvider` (Stripe / Adyen / PayPal / Square / Mollie / cu… |
|
||||
| [`@suaveplan/api-plugin-rate-limit`](./api-plugin-rate-limit/README.md) | Rate-limit plugin for `@suaveplan/api`. Wraps `@suaveplan/rate-limiter` as a `definePlugin` descriptor exposing per-route + per-tenant configuration, parameter… |
|
||||
| [`@suaveplan/api-plugin-search`](./api-plugin-search/README.md) | Full-text search plugin for `@suaveplan/api`. Brings TF-weighted relevance scoring, four built-in text analyzers (`standard`, `simple`, `whitespace`, `keyword`… |
|
||||
| [`@suaveplan/api-plugin-site`](./api-plugin-site/README.md) | Multi-site / multi-domain management plugin for `@suaveplan/api`. Stores `site` records (name, slug, theme, settings, push config) in SurrealDB, resolves incom… |
|
||||
| [`@suaveplan/api-plugin-sse`](./api-plugin-sse/README.md) | Server-Sent Events plugin for `@suaveplan/api`. Registers a typed `sseService` in the host `ServiceRegistry`, mounts a Hono streaming handler at `GET /sse/:cha… |
|
||||
| [`@suaveplan/api-plugin-tenant`](./api-plugin-tenant/README.md) | Multi-tenancy plugin for `@suaveplan/api`. Wraps `@suaveplan/api-tenant`'s resolver chain (header / subdomain / domain / path / JWT / query / cookie) as a firs… |
|
||||
| [`@suaveplan/api-plugin-user`](./api-plugin-user/README.md) | User-profile CRUD plugin for `@suaveplan/api`. Mounts the canonical `/users/me` GET/PATCH/DELETE surface on top of `@suaveplan/api-plugin-auth`, registers a ty… |
|
||||
| [`@suaveplan/api-plugin-vault`](./api-plugin-vault/README.md) | Per-user vault HTTP plugin for `@suaveplan/api`. Third instance of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` package follows: the plugin… |
|
||||
| [`@suaveplan/api-plugin-webhook`](./api-plugin-webhook/README.md) | Signed outbound webhook delivery plugin for `@suaveplan/api`. Wraps `@suaveplan/webhook-client` behind a `definePlugin` descriptor, registers a typed `webhookS… |
|
||||
| [`@suaveplan/api-rate-limit`](./api-rate-limit/README.md) | Per-route / per-principal / per-tenant / per-IP token-bucket rate-limit middleware for `@suaveplan/api`. Implements the `RequestRateLimiter` contract consumed… |
|
||||
| [`@suaveplan/api-rbac`](./api-rbac/README.md) | Scope + rule-based authorisation middleware for the Suaveplan API plugin system. Produces a framework-agnostic `Middleware` that consumes the resolved `Princip… |
|
||||
| [`@suaveplan/api-router`](./api-router/README.md) | Stage-1 contract package for the Suaveplan API plugin system. |
|
||||
| [`@suaveplan/api-router-hono`](./api-router-hono/README.md) | Stage-4 concrete Hono router adapter for `@suaveplan/api`. Implements the abstract `Router` contract from `@suaveplan/api-router` on top of a Hono `Hono` insta… |
|
||||
| [`@suaveplan/api-router-in-memory`](./api-router-in-memory/README.md) | Stage-5 in-memory router adapter for `@suaveplan/api`. Implements both the abstract `Router` contract from `@suaveplan/api-router` AND the `PipelineRouter` con… |
|
||||
| [`@suaveplan/api-router-worker-bridge`](./api-router-worker-bridge/README.md) | Stage-3 HTTP bridge that marshals Fetch `Request` and `Response` objects across the worker boundary. Pairs a main-side `dispatch` with a worker- side handler o… |
|
||||
| [`@suaveplan/api-saga`](./api-saga/README.md) | Saga orchestrator for the Suaveplan API plugin system. Drives manifest-declared multi-plugin workflows in declared order with reverse-order compensation on fai… |
|
||||
| [`@suaveplan/api-sandbox-permissions`](./api-sandbox-permissions/README.md) | Permission model for sandboxed-tier API plugins. Centralises the `permissions.fs[]` / `permissions.fetch[]` allowlists, the `permissions.budgets` enforcement,… |
|
||||
| [`@suaveplan/api-sandbox-vm`](./api-sandbox-vm/README.md) | `vm.Context`-based sandbox host for Suaveplan API plugins running in the sandboxed tier. The package owns the `vm.Context`+frozen-globals half of the design do… |
|
||||
| [`@suaveplan/api-sandbox-vm-host-bridge`](./api-sandbox-vm-host-bridge/README.md) | Host-integration bridge that lets a `VmSandboxHandle` from `@suaveplan/api-sandbox-vm` install into `@suaveplan/api-host` as a first-class plugin. |
|
||||
| [`@suaveplan/api-sandbox-wasm`](./api-sandbox-wasm/README.md) | Stage-8 WebAssembly sandbox host. Instantiates a sandboxed-tier plugin WASM module against a WASI snapshot preview1 implementation derived from the plugin's `P… |
|
||||
| [`@suaveplan/api-sandbox-wasm-host-bridge`](./api-sandbox-wasm-host-bridge/README.md) | Host-integration layer that wraps a `@suaveplan/api-sandbox-wasm` `WasmSandboxHandle` as a `@suaveplan/api-host` `PluginModule`. Sandboxed-tier plugins compile… |
|
||||
| [`@suaveplan/api-schemas`](./api-schemas/README.md) | Shared Zod schemas describing Suaveplan API request/response shapes. The single source of truth for server plugins, React data hooks, and generated OpenAPI doc… |
|
||||
| [`@suaveplan/api-secrets`](./api-secrets/README.md) | Abstract Secrets contract for the Suaveplan API plugin system — per-plugin secret access typed against `manifest.secrets[]`, canonical redaction marker, frozen… |
|
||||
| [`@suaveplan/api-secrets-bun`](./api-secrets-bun/README.md) | Stage-8 `SecretsLike` adapter backed by `Bun.secrets` (macOS Keychain, Linux libsecret, Windows Credential Manager) with per-plugin allow-list enforcement, OS-… |
|
||||
| [`@suaveplan/api-secrets-env`](./api-secrets-env/README.md) | Stage-4 `SecretsLike` adapter backed by `process.env` / `Bun.env` with per-plugin allowed-keys enforcement. |
|
||||
| [`@suaveplan/api-secrets-vault`](./api-secrets-vault/README.md) | Stage-8 `SecretsLike` adapter backed by `@suaveplan/vault`. The vault is passed in already unlocked by the host's startup sequence; the adapter never sees the… |
|
||||
| [`@suaveplan/api-secrets-worker-proxy`](./api-secrets-worker-proxy/README.md) | Stage-3 worker → main `SecretsLike` proxy for the API plugin system. |
|
||||
| [`@suaveplan/api-sse`](./api-sse/README.md) | Abstract Server-Sent Events contracts for `@suaveplan/api`. Stage-1 contract package: defines the engine-side `SseConnection`, `SseBroker`, `ReplayBuffer` (Las… |
|
||||
| [`@suaveplan/api-sse-bun`](./api-sse-bun/README.md) | `Bun.serve`-backed Server-Sent Events engine for the `@suaveplan/api` plugin system. Implements the abstract `@suaveplan/api-sse` contract and passes `runSseEn… |
|
||||
| [`@suaveplan/api-tenant`](./api-tenant/README.md) | Multi-tenant resolution + scoping for the Suaveplan API. Server-tier — runs on Bun and Node, depends on `hono` and `node:async_hooks`. |
|
||||
| [`@suaveplan/api-tenant-provider`](./api-tenant-provider/README.md) | Abstract `TenantProvider` contract for the Suaveplan API plugin system. Enumerates the universe of tenants known to a host so per-tenant cron / queued jobs (ma… |
|
||||
| [`@suaveplan/api-tenant-provider-memory`](./api-tenant-provider-memory/README.md) | In-memory reference implementation of the `@suaveplan/api-tenant-provider` `TenantProvider` contract. Backed by an insertion-ordered `Map<TenantId, TenantRecor… |
|
||||
| [`@suaveplan/api-tenant-resolver-header`](./api-tenant-resolver-header/README.md) | Stage-8 tenant resolver. Implements the `TenantResolver` contract from `@suaveplan/api-types/tenant-resolver` by reading the tenant identifier from a configura… |
|
||||
| [`@suaveplan/api-tenant-resolver-jwt`](./api-tenant-resolver-jwt/README.md) | Stage-4 tenant resolver for the Suaveplan API plugin system. Reads the already-resolved `Principal.tenantId` (attached by an earlier `api-auth-*` chain step) a… |
|
||||
| [`@suaveplan/api-tenant-resolver-subdomain`](./api-tenant-resolver-subdomain/README.md) | Stage-8 tenant resolver for the Suaveplan API plugin system. Reads the inbound `Host` header (and optionally a CIDR-trusted `X-Forwarded-Host`), strips a confi… |
|
||||
| [`@suaveplan/api-types`](./api-types/README.md) | Root contract-types package for the Suaveplan API plugin system. Defines every cross-cutting type the foundation packages (`api-manifest`, `api-router`, `api-h… |
|
||||
| [`@suaveplan/api-worker-host`](./api-worker-host/README.md) | Worker-side runtime that hosts exactly one Suaveplan API plugin inside an isolated `Worker` thread. The main-process counterpart is `@suaveplan/api-host`; this… |
|
||||
| [`@suaveplan/api-worker-rpc`](./api-worker-rpc/README.md) | MessagePort-based RPC protocol for worker-isolated API plugins. Provides the shared protocol — Zod-validated wire frames, monotonic correlation ids, AbortSigna… |
|
||||
| [`@suaveplan/api-ws`](./api-ws/README.md) | Abstract WebSocket engine contract for the Suaveplan API foundation — Connection, ConnectionRegistry, SubscriptionBus, WsBroker, reconnect-token envelope, backpressure + heartbeat policies, cross-tenant capability gate, and a runWsEngineConformance harness. Stage-1 contracts only; concrete engines (api-ws-bun) and brokers (api-ws-broker-redis / -in-memory) plug behind these interfaces. |
|
||||
| [`@suaveplan/api-ws-broker-in-memory`](./api-ws-broker-in-memory/README.md) | Single-host in-process implementation of the `@suaveplan/api-ws` `WsBroker` contract. Suitable for production single-host deployments AND as the default broker… |
|
||||
| [`@suaveplan/api-ws-broker-redis`](./api-ws-broker-redis/README.md) | Cross-host Redis pub/sub broker for `@suaveplan/api-ws` and `@suaveplan/api-sse`. |
|
||||
| [`@suaveplan/api-ws-bun`](./api-ws-bun/README.md) | Bun.serve-backed WebSocket engine for the `@suaveplan/api-ws` Stage-1 abstract contract. The engine composes the per-connection handshake pipeline, registry, s… |
|
||||
| [`@suaveplan/wasm-compiler-javy`](./wasm-compiler-javy/README.md) | TS->WASM compiler closing **Stage-8 gate 2.8**. Thin orchestrator around `Bun.build` (bundle) + an injected WASI-stdio ABI shim + **Javy v8.1.1** (JS->WASM) +… |
|
||||
<!-- end:auto-docs -->
|
||||
3
docs/api-stack/_intro.md
Normal file
3
docs/api-stack/_intro.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `api-stack`
|
||||
|
||||
API infrastructure, plugin framework, and HTTP orchestration layer for Suaveplan.
|
||||
102
docs/api-stack/api/README.md
Normal file
102
docs/api-stack/api/README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# `api`
|
||||
|
||||
API orchestration, plugin lifecycle, routing, workers, and tenant resolution.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Packages in `api`
|
||||
|
||||
90 packages.
|
||||
|
||||
| Package | Description |
|
||||
|---|---|
|
||||
| [`@suaveplan/api`](./api/README.md) | The thin Hono-based application orchestrator for the Suaveplan API stack. Composes the four sibling packages — `@suaveplan/api-plugin`, `@suaveplan/api-tenant`… |
|
||||
| [`@suaveplan/api-2pc`](./api-2pc/README.md) | Two-phase commit coordinator for the Suaveplan API plugin system. Drives Postgres PREPARE TRANSACTION / COMMIT PREPARED / ROLLBACK PREPARED across registered p… |
|
||||
| [`@suaveplan/api-audit-file`](./api-audit-file/README.md) | File-rotating, HMAC-signed audit sink for `@suaveplan/api-host`. Implements `RequestAuditSink` from `@suaveplan/api-host/request-pipeline`: writes one JSON lin… |
|
||||
| [`@suaveplan/api-audit-stdout`](./api-audit-stdout/README.md) | Stdout audit sink for `@suaveplan/api-host`. Writes JSON-per-line audit records to a `LineWriter` (default `process.stdout`) with an HMAC-SHA-256 signature app… |
|
||||
| [`@suaveplan/api-auth`](./api-auth/README.md) | **$2** for the Suaveplan API plugin system. Defines the abstract session-resolver shape (`Request → Principal \| null`) used by the host on every request, plus… |
|
||||
| [`@suaveplan/api-auth-anonymous`](./api-auth-anonymous/README.md) | Stage-4 trivial session resolver for `@suaveplan/api`. Always returns `null` for every request. Use this as the tail of resolver chains for public endpoints, a… |
|
||||
| [`@suaveplan/api-auth-bearer-static`](./api-auth-bearer-static/README.md) | Stage-4 service-to-service `SessionResolver` adapter for the `@suaveplan/api` plugin system. Matches an inbound `Authorization: Bearer <token>` header against… |
|
||||
| [`@suaveplan/api-auth-better-auth`](./api-auth-better-auth/README.md) | Stage-6 Better-Auth adapter for `@suaveplan/api`. Ships two **independently-wireable** capability surfaces — `createBetterAuthSession` (implements `auth.sessio… |
|
||||
| [`@suaveplan/api-auth-jwt`](./api-auth-jwt/README.md) | Stage-4 concrete `SessionResolver` for the `@suaveplan/api` plugin system. Verifies a JWT presented on the `Authorization: Bearer <jwt>` header (or a configure… |
|
||||
| [`@suaveplan/api-auth-provider`](./api-auth-provider/README.md) | Abstract account-lifecycle contract for the Suaveplan API plugin system (contracts). This package is OPTIONAL: hosted-IdP deployments (Clerk, Auth0, Cognito) i… |
|
||||
| [`@suaveplan/api-auth-rolled`](./api-auth-rolled/README.md) | Stage-8.12 rolled session-token resolver for `@suaveplan/api`. Issues RS256-signed JWTs with rotating `kid` slots, verifies tokens through the rotator's active… |
|
||||
| [`@suaveplan/api-auth-static-principal`](./api-auth-static-principal/README.md) | Fixed-`Principal` session resolver for the `@suaveplan/api` plugin system. Returns the configured `Principal` verbatim on every request — no token decoding, no… |
|
||||
| [`@suaveplan/api-db-adapter`](./api-db-adapter/README.md) | Stage-4 bridge from the `@suaveplan/db` engine adapters to the plugin `AdminDatabaseLike` / `TenantScopedDatabaseLike` surfaces exposed by `@suaveplan/api-type… |
|
||||
| [`@suaveplan/api-db-sqlite-memory`](./api-db-sqlite-memory/README.md) | Stage-5 in-memory SQLite database adapter for the `@suaveplan/api` plugin host testing kit. Wraps `bun:sqlite` (in `:memory:` mode by default, or a file path f… |
|
||||
| [`@suaveplan/api-db-worker-proxy`](./api-db-worker-proxy/README.md) | Worker-side proxy for the api `db` capability. Marshals `AdminDatabaseLike` and `TenantScopedDatabaseLike` calls — including transactions and cursor streams —… |
|
||||
| [`@suaveplan/api-events-in-memory`](./api-events-in-memory/README.md) | In-memory events bus adapter for the `@suaveplan/api` testing kit. Wraps `@suaveplan/events`' production `EventBus` with a per-topic recording layer so tests c… |
|
||||
| [`@suaveplan/api-events-worker-proxy`](./api-events-worker-proxy/README.md) | Worker-tier proxy over a main-process `@suaveplan/events` `EventBus`. |
|
||||
| [`@suaveplan/api-graphql`](./api-graphql/README.md) | Stage-1 abstract GraphQL contract for the Suaveplan API plugin system. |
|
||||
| [`@suaveplan/api-graphql-apollo`](./api-graphql-apollo/README.md) | Stage-7 Apollo Server GraphQL engine adapter for the Suaveplan API plugin system. Implements the `GraphqlEngine` contract from `@suaveplan/api-graphql` and pas… |
|
||||
| [`@suaveplan/api-graphql-ui-altair`](./api-graphql-ui-altair/README.md) | Altair GraphQL UI plugin for the Suaveplan API plugin system. Structural sibling of `@suaveplan/api-graphql-ui-graphiql`. |
|
||||
| [`@suaveplan/api-graphql-ui-graphiql`](./api-graphql-ui-graphiql/README.md) | GraphiQL UI mount for the Suaveplan API plugin system. Stage 7 of the umbrella plan. |
|
||||
| [`@suaveplan/api-graphql-yoga`](./api-graphql-yoga/README.md) | Stage-7 GraphQL Yoga engine adapter. **Default GraphQL engine** for the Suaveplan API plugin system per design doc §10.5 — Bun-friendly, fastest for our usage… |
|
||||
| [`@suaveplan/api-host`](./api-host/README.md) | Host orchestration core for the Suaveplan API plugin system. Provides the stable error-code registry (HOST/PLUGIN/RELOAD/CAPABILITY/ADMIN/LEDGER), the author-facing definePlugin DX, the runtime CapabilityRegistry, the LifecycleDriver + ReloadDriver + PluginLedger + admin Hono sub-router, the ErrorBoundary, the §6 11-step RequestPipeline, the top-level createHost(config) entry point composing every internal driver, and the HostInstruments registry covering the §12.3 metric surface. The ./testing subpath is reserved for test utilities. |
|
||||
| [`@suaveplan/api-jobs-worker-proxy`](./api-jobs-worker-proxy/README.md) | Bidirectional `MessagePort` proxy that lets a worker-isolated API plugin participate in the host's main-process job scheduler. The worker calls `registerCron`… |
|
||||
| [`@suaveplan/api-lifecycle`](./api-lifecycle/README.md) | Stage-1 contract package for the Suaveplan API plugin system: the plugin-lifecycle finite state machine definition, the legal-transition table, the side-effect… |
|
||||
| [`@suaveplan/api-loader`](./api-loader/README.md) | Stage-1 contract package for the Suaveplan API plugin system. |
|
||||
| [`@suaveplan/api-logger-worker-proxy`](./api-logger-worker-proxy/README.md) | Stage-3 worker → main proxy for `@suaveplan/logger`. Lets a worker-isolated API plugin call a normal `LoggerLike` surface while the records cross a `MessagePor… |
|
||||
| [`@suaveplan/api-manifest`](./api-manifest/README.md) | Stage-1 contract package of the Suaveplan API plugin system: Zod schemas, parser, cross-field validators, dependency-graph resolver, capability resolver, and c… |
|
||||
| [`@suaveplan/api-migrations`](./api-migrations/README.md) | Database-agnostic migration runner for the Suaveplan API. Ships a narrow `MigrationAdapter` surface (six methods), three shipped adapters (`surrealql`, `postgr… |
|
||||
| [`@suaveplan/api-openapi`](./api-openapi/README.md) | OpenAPI 3.1 registry, typed `RouteBuilder`, schema registry, and document merger for the Suaveplan API. Single source of truth that wires into Hono via `OpenAP… |
|
||||
| [`@suaveplan/api-openapi-ui-redoc`](./api-openapi-ui-redoc/README.md) | Stage-8 ReDoc UI adapter for the Suaveplan api-host. Mounts the ReDoc HTML shell at `/docs/ui/redoc` (or any caller-chosen path) via `createRedocUiHandler` and… |
|
||||
| [`@suaveplan/api-openapi-ui-scalar`](./api-openapi-ui-scalar/README.md) | Scalar UI adapter for the Suaveplan OpenAPI plugin system. |
|
||||
| [`@suaveplan/api-openapi-ui-swagger`](./api-openapi-ui-swagger/README.md) | Swagger UI adapter for the `/docs/ui/swagger` endpoint of the Suaveplan API plugin system (Stage 8.8). Returns a pure HTTP handler whose response is a `text/ht… |
|
||||
| [`@suaveplan/api-plugin`](./api-plugin/README.md) | Plugin-system foundation for the Suaveplan API. Universal — runs unchanged in Bun, Node, and the browser. |
|
||||
| [`@suaveplan/api-plugin-audit`](./api-plugin-audit/README.md) | Hono plugin that wraps `@suaveplan/audit-logging` as a write-through audit trail for `@suaveplan/api`. Installs a global per-request audit middleware (method /… |
|
||||
| [`@suaveplan/api-plugin-auth`](./api-plugin-auth/README.md) | Better-Auth-first authentication plugin for `@suaveplan/api`. The canonical exemplar of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` packag… |
|
||||
| [`@suaveplan/api-plugin-campaigns`](./api-plugin-campaigns/README.md) | Campaigns + suppression + delivery-webhooks HTTP plugin for `@suaveplan/api`. Mounts the `/campaigns`, `/suppression`, and `/webhooks` route trees on a Hono `O… |
|
||||
| [`@suaveplan/api-plugin-cms`](./api-plugin-cms/README.md) | Headless content-management primitives for `@suaveplan/api`. Ships an in-memory `CmsEngine` that manages flexible content models, content entries with a `draft… |
|
||||
| [`@suaveplan/api-plugin-database`](./api-plugin-database/README.md) | SurrealDB-backed database plugin for `@suaveplan/api`. Pools `@suaveplan/surrealdb`'s `SurrealAdapter` (implementing `@suaveplan/db`'s `DatabaseAdapter`), runs… |
|
||||
| [`@suaveplan/api-plugin-event-bus`](./api-plugin-event-bus/README.md) | Typed event-bus plugin for `@suaveplan/api`. Wraps `@suaveplan/events` (pub/sub, async dispatch, interceptors, bounded-queue backpressure, idempotent listener… |
|
||||
| [`@suaveplan/api-plugin-events`](./api-plugin-events/README.md) | Event-management plugin for `@suaveplan/api`. Ships a SurrealDB-backed `EventRepository` + `RsvpRepository` pair, ten Hono OpenAPI routes (list / get / create… |
|
||||
| [`@suaveplan/api-plugin-example`](./api-plugin-example/README.md) | Reference plugin for the `@suaveplan/api-host` plugin system. The package is a load-bearing integration target, not a tutorial: every contract surface the host… |
|
||||
| [`@suaveplan/api-plugin-export`](./api-plugin-export/README.md) | Data-export plugin for `@suaveplan/api`. Turns any registered resource into a CSV (RFC 4180), JSON, or XLSX (SpreadsheetML XML) blob through three self-contain… |
|
||||
| [`@suaveplan/api-plugin-file-upload`](./api-plugin-file-upload/README.md) | Multipart and chunked file-upload plugin for `@suaveplan/api`. Ships six OpenAPI-typed Hono routes, a pluggable storage + session-store contract, SHA-256 hash… |
|
||||
| [`@suaveplan/api-plugin-graphql`](./api-plugin-graphql/README.md) | GraphQL description + resolver-composition layer for `@suaveplan/api`. Server-tier — runs in Bun and Node. |
|
||||
| [`@suaveplan/api-plugin-import`](./api-plugin-import/README.md) | Tabular-data ingest plugin for `@suaveplan/api`. Parses CSV / JSON / TSV-XLSX payloads, applies declarative column mappings + per-field validation + computed f… |
|
||||
| [`@suaveplan/api-plugin-notifications`](./api-plugin-notifications/README.md) | SurrealDB-backed notifications + push plugin for `@suaveplan/api`. Ships a `NotificationRepository` + `DeviceTokenRepository` pair, an `ApnService` with a plug… |
|
||||
| [`@suaveplan/api-plugin-payment`](./api-plugin-payment/README.md) | Payment-lifecycle plugin for `@suaveplan/api`. Wraps `@suaveplan/payment` + a caller-injected `PaymentProvider` (Stripe / Adyen / PayPal / Square / Mollie / cu… |
|
||||
| [`@suaveplan/api-plugin-rate-limit`](./api-plugin-rate-limit/README.md) | Rate-limit plugin for `@suaveplan/api`. Wraps `@suaveplan/rate-limiter` as a `definePlugin` descriptor exposing per-route + per-tenant configuration, parameter… |
|
||||
| [`@suaveplan/api-plugin-search`](./api-plugin-search/README.md) | Full-text search plugin for `@suaveplan/api`. Brings TF-weighted relevance scoring, four built-in text analyzers (`standard`, `simple`, `whitespace`, `keyword`… |
|
||||
| [`@suaveplan/api-plugin-site`](./api-plugin-site/README.md) | Multi-site / multi-domain management plugin for `@suaveplan/api`. Stores `site` records (name, slug, theme, settings, push config) in SurrealDB, resolves incom… |
|
||||
| [`@suaveplan/api-plugin-sse`](./api-plugin-sse/README.md) | Server-Sent Events plugin for `@suaveplan/api`. Registers a typed `sseService` in the host `ServiceRegistry`, mounts a Hono streaming handler at `GET /sse/:cha… |
|
||||
| [`@suaveplan/api-plugin-tenant`](./api-plugin-tenant/README.md) | Multi-tenancy plugin for `@suaveplan/api`. Wraps `@suaveplan/api-tenant`'s resolver chain (header / subdomain / domain / path / JWT / query / cookie) as a firs… |
|
||||
| [`@suaveplan/api-plugin-user`](./api-plugin-user/README.md) | User-profile CRUD plugin for `@suaveplan/api`. Mounts the canonical `/users/me` GET/PATCH/DELETE surface on top of `@suaveplan/api-plugin-auth`, registers a ty… |
|
||||
| [`@suaveplan/api-plugin-vault`](./api-plugin-vault/README.md) | Per-user vault HTTP plugin for `@suaveplan/api`. Third instance of the 3-way plug-and-play contract every `@suaveplan/api-plugin-*` package follows: the plugin… |
|
||||
| [`@suaveplan/api-plugin-webhook`](./api-plugin-webhook/README.md) | Signed outbound webhook delivery plugin for `@suaveplan/api`. Wraps `@suaveplan/webhook-client` behind a `definePlugin` descriptor, registers a typed `webhookS… |
|
||||
| [`@suaveplan/api-rate-limit`](./api-rate-limit/README.md) | Per-route / per-principal / per-tenant / per-IP token-bucket rate-limit middleware for `@suaveplan/api`. Implements the `RequestRateLimiter` contract consumed… |
|
||||
| [`@suaveplan/api-rbac`](./api-rbac/README.md) | Scope + rule-based authorisation middleware for the Suaveplan API plugin system. Produces a framework-agnostic `Middleware` that consumes the resolved `Princip… |
|
||||
| [`@suaveplan/api-router`](./api-router/README.md) | Stage-1 contract package for the Suaveplan API plugin system. |
|
||||
| [`@suaveplan/api-router-hono`](./api-router-hono/README.md) | Stage-4 concrete Hono router adapter for `@suaveplan/api`. Implements the abstract `Router` contract from `@suaveplan/api-router` on top of a Hono `Hono` insta… |
|
||||
| [`@suaveplan/api-router-in-memory`](./api-router-in-memory/README.md) | Stage-5 in-memory router adapter for `@suaveplan/api`. Implements both the abstract `Router` contract from `@suaveplan/api-router` AND the `PipelineRouter` con… |
|
||||
| [`@suaveplan/api-router-worker-bridge`](./api-router-worker-bridge/README.md) | Stage-3 HTTP bridge that marshals Fetch `Request` and `Response` objects across the worker boundary. Pairs a main-side `dispatch` with a worker- side handler o… |
|
||||
| [`@suaveplan/api-saga`](./api-saga/README.md) | Saga orchestrator for the Suaveplan API plugin system. Drives manifest-declared multi-plugin workflows in declared order with reverse-order compensation on fai… |
|
||||
| [`@suaveplan/api-sandbox-permissions`](./api-sandbox-permissions/README.md) | Permission model for sandboxed-tier API plugins. Centralises the `permissions.fs[]` / `permissions.fetch[]` allowlists, the `permissions.budgets` enforcement,… |
|
||||
| [`@suaveplan/api-sandbox-vm`](./api-sandbox-vm/README.md) | `vm.Context`-based sandbox host for Suaveplan API plugins running in the sandboxed tier. The package owns the `vm.Context`+frozen-globals half of the design do… |
|
||||
| [`@suaveplan/api-sandbox-vm-host-bridge`](./api-sandbox-vm-host-bridge/README.md) | Host-integration bridge that lets a `VmSandboxHandle` from `@suaveplan/api-sandbox-vm` install into `@suaveplan/api-host` as a first-class plugin. |
|
||||
| [`@suaveplan/api-sandbox-wasm`](./api-sandbox-wasm/README.md) | Stage-8 WebAssembly sandbox host. Instantiates a sandboxed-tier plugin WASM module against a WASI snapshot preview1 implementation derived from the plugin's `P… |
|
||||
| [`@suaveplan/api-sandbox-wasm-host-bridge`](./api-sandbox-wasm-host-bridge/README.md) | Host-integration layer that wraps a `@suaveplan/api-sandbox-wasm` `WasmSandboxHandle` as a `@suaveplan/api-host` `PluginModule`. Sandboxed-tier plugins compile… |
|
||||
| [`@suaveplan/api-schemas`](./api-schemas/README.md) | Shared Zod schemas describing Suaveplan API request/response shapes. The single source of truth for server plugins, React data hooks, and generated OpenAPI doc… |
|
||||
| [`@suaveplan/api-secrets`](./api-secrets/README.md) | Abstract Secrets contract for the Suaveplan API plugin system — per-plugin secret access typed against `manifest.secrets[]`, canonical redaction marker, frozen… |
|
||||
| [`@suaveplan/api-secrets-bun`](./api-secrets-bun/README.md) | Stage-8 `SecretsLike` adapter backed by `Bun.secrets` (macOS Keychain, Linux libsecret, Windows Credential Manager) with per-plugin allow-list enforcement, OS-… |
|
||||
| [`@suaveplan/api-secrets-env`](./api-secrets-env/README.md) | Stage-4 `SecretsLike` adapter backed by `process.env` / `Bun.env` with per-plugin allowed-keys enforcement. |
|
||||
| [`@suaveplan/api-secrets-vault`](./api-secrets-vault/README.md) | Stage-8 `SecretsLike` adapter backed by `@suaveplan/vault`. The vault is passed in already unlocked by the host's startup sequence; the adapter never sees the… |
|
||||
| [`@suaveplan/api-secrets-worker-proxy`](./api-secrets-worker-proxy/README.md) | Stage-3 worker → main `SecretsLike` proxy for the API plugin system. |
|
||||
| [`@suaveplan/api-sse`](./api-sse/README.md) | Abstract Server-Sent Events contracts for `@suaveplan/api`. Stage-1 contract package: defines the engine-side `SseConnection`, `SseBroker`, `ReplayBuffer` (Las… |
|
||||
| [`@suaveplan/api-sse-bun`](./api-sse-bun/README.md) | `Bun.serve`-backed Server-Sent Events engine for the `@suaveplan/api` plugin system. Implements the abstract `@suaveplan/api-sse` contract and passes `runSseEn… |
|
||||
| [`@suaveplan/api-tenant`](./api-tenant/README.md) | Multi-tenant resolution + scoping for the Suaveplan API. Server-tier — runs on Bun and Node, depends on `hono` and `node:async_hooks`. |
|
||||
| [`@suaveplan/api-tenant-provider`](./api-tenant-provider/README.md) | Abstract `TenantProvider` contract for the Suaveplan API plugin system. Enumerates the universe of tenants known to a host so per-tenant cron / queued jobs (ma… |
|
||||
| [`@suaveplan/api-tenant-provider-memory`](./api-tenant-provider-memory/README.md) | In-memory reference implementation of the `@suaveplan/api-tenant-provider` `TenantProvider` contract. Backed by an insertion-ordered `Map<TenantId, TenantRecor… |
|
||||
| [`@suaveplan/api-tenant-resolver-header`](./api-tenant-resolver-header/README.md) | Stage-8 tenant resolver. Implements the `TenantResolver` contract from `@suaveplan/api-types/tenant-resolver` by reading the tenant identifier from a configura… |
|
||||
| [`@suaveplan/api-tenant-resolver-jwt`](./api-tenant-resolver-jwt/README.md) | Stage-4 tenant resolver for the Suaveplan API plugin system. Reads the already-resolved `Principal.tenantId` (attached by an earlier `api-auth-*` chain step) a… |
|
||||
| [`@suaveplan/api-tenant-resolver-subdomain`](./api-tenant-resolver-subdomain/README.md) | Stage-8 tenant resolver for the Suaveplan API plugin system. Reads the inbound `Host` header (and optionally a CIDR-trusted `X-Forwarded-Host`), strips a confi… |
|
||||
| [`@suaveplan/api-types`](./api-types/README.md) | Root contract-types package for the Suaveplan API plugin system. Defines every cross-cutting type the foundation packages (`api-manifest`, `api-router`, `api-h… |
|
||||
| [`@suaveplan/api-worker-host`](./api-worker-host/README.md) | Worker-side runtime that hosts exactly one Suaveplan API plugin inside an isolated `Worker` thread. The main-process counterpart is `@suaveplan/api-host`; this… |
|
||||
| [`@suaveplan/api-worker-rpc`](./api-worker-rpc/README.md) | MessagePort-based RPC protocol for worker-isolated API plugins. Provides the shared protocol — Zod-validated wire frames, monotonic correlation ids, AbortSigna… |
|
||||
| [`@suaveplan/api-ws`](./api-ws/README.md) | Abstract WebSocket engine contract for the Suaveplan API foundation — Connection, ConnectionRegistry, SubscriptionBus, WsBroker, reconnect-token envelope, backpressure + heartbeat policies, cross-tenant capability gate, and a runWsEngineConformance harness. Stage-1 contracts only; concrete engines (api-ws-bun) and brokers (api-ws-broker-redis / -in-memory) plug behind these interfaces. |
|
||||
| [`@suaveplan/api-ws-broker-in-memory`](./api-ws-broker-in-memory/README.md) | Single-host in-process implementation of the `@suaveplan/api-ws` `WsBroker` contract. Suitable for production single-host deployments AND as the default broker… |
|
||||
| [`@suaveplan/api-ws-broker-redis`](./api-ws-broker-redis/README.md) | Cross-host Redis pub/sub broker for `@suaveplan/api-ws` and `@suaveplan/api-sse`. |
|
||||
| [`@suaveplan/api-ws-bun`](./api-ws-bun/README.md) | Bun.serve-backed WebSocket engine for the `@suaveplan/api-ws` Stage-1 abstract contract. The engine composes the per-connection handshake pipeline, registry, s… |
|
||||
| [`@suaveplan/wasm-compiler-javy`](./wasm-compiler-javy/README.md) | TS->WASM compiler closing **Stage-8 gate 2.8**. Thin orchestrator around `Bun.build` (bundle) + an injected WASI-stdio ABI shim + **Javy v8.1.1** (JS->WASM) +… |
|
||||
<!-- end:auto-docs -->
|
||||
1
docs/api-stack/api/_intro.md
Normal file
1
docs/api-stack/api/_intro.md
Normal file
@@ -0,0 +1 @@
|
||||
API orchestration, plugin lifecycle, routing, workers, and tenant resolution.
|
||||
109
docs/api-stack/api/api-2pc/README.md
Normal file
109
docs/api-stack/api/api-2pc/README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# @suaveplan/api-2pc - Two-phase commit coordinator for the Suaveplan API plugin system.
|
||||
|
||||
Two-phase commit coordinator for the Suaveplan API plugin system. Drives Postgres PREPARE TRANSACTION / COMMIT PREPARED / ROLLBACK PREPARED across registered participants with a host-owned ledger in suaveplan_2pc_transactions. Refuses non-preparable engines as writers (SQLite, SurrealDB) but accepts them as read-only voters. Default timeoutMs 5000, hard ceiling 30000, default outstanding-prepared cap 256, all operator-tunable. Ships boot-time orphan resolution via pg_prepared_xacts, the full TWOPC_* error namespace, OTel transaction/prepare/commit/rollback spans plus prepare_duration_ms / commit_duration_ms histograms / transactions_total counter / outstanding_prepared gauge, and run2pcParticipantConformance on the ./testing subpath. Server-tier — Bun + Node.
|
||||
|
||||
> **Postgres-only writer surface.** SQLite and SurrealDB cannot honour
|
||||
> `PREPARE TRANSACTION` and are refused as writers at install time.
|
||||
> They MAY participate as read-only voters that contribute a
|
||||
> `commit`/`abort` vote based on their own consistency check, but the
|
||||
> coordinator never sends them prepare / commit / rollback SQL.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [coordinator](./src/coordinator/coordinator.md) — Top-level orchestration for the 2PC protocol. `createTwoPcCoordinator` builds a fully-wired coordinator that holds a pa…
|
||||
- [coordinator](./src/coordinator/coordinator.md) — Top-level orchestration for the 2PC protocol. `createTwoPcCoordinator` builds a fully-wired coordinator that holds a pa…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module owns the `TWOPC_*` error-code namespace and the `TwoPcError` concrete class for the `@suaveplan/api-2pc` pa…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module owns the `TWOPC_*` error-code namespace and the `TwoPcError` concrete class for the `@suaveplan/api-2pc` pa…
|
||||
- [instruments](./src/instruments/instruments.md) — Builds the OTel instrument registry the 2PC coordinator emits against on every transaction. Each instrument's name foll…
|
||||
- [instruments](./src/instruments/instruments.md) — Builds the OTel instrument registry the 2PC coordinator emits against on every transaction. Each instrument's name foll…
|
||||
- [outstanding-cap](./src/limits/outstanding-cap.md) — Admission control for in-flight 2PC transactions. The coordinator holds a fixed-size budget of "outstanding prepared tr…
|
||||
- [timeout-ceiling](./src/limits/timeout-ceiling.md) — Enforces the hard 30 000 ms ceiling on caller-supplied `timeoutMs` values. The check fires at the public entry point of…
|
||||
- [options](./src/options/options.md) — Defines the boundary-validated option shape consumed by `createTwoPcCoordinator`. Every tunable that affects coordinato…
|
||||
- [options](./src/options/options.md) — Defines the boundary-validated option shape consumed by `createTwoPcCoordinator`. Every tunable that affects coordinato…
|
||||
- [persistence](./src/persistence/persistence.md) — Persists every coordinator state transition into the `suaveplan_2pc_transactions` ledger table. The table is owned by `…
|
||||
- [persistence](./src/persistence/persistence.md) — Persists every coordinator state transition into the `suaveplan_2pc_transactions` ledger table. The table is owned by `…
|
||||
- [commit](./src/phases/commit.md) — Phase-2 commit for a single participant. Once every participant has returned `commit` from phase 1 and the coordinator…
|
||||
- [gid](./src/phases/gid.md) — Builds and parses the global transaction id (gid) string passed to Postgres' `PREPARE TRANSACTION '<gid>'`. The format…
|
||||
- [prepare](./src/phases/prepare.md) — Implements phase 1 of the 2PC protocol for a single participant. The coordinator iterates participants and calls this m…
|
||||
- [rollback](./src/phases/rollback.md) — Phase-2 rollback for a single participant. Used both by the coordinator's abort path (vote_abort or prepare failure) an…
|
||||
- [recovery](./src/recovery/recovery.md) — Boot-time orphan resolution for the 2PC coordinator. When a host crashes between phase 1 and phase 2, prepared transact…
|
||||
- [recovery](./src/recovery/recovery.md) — Boot-time orphan resolution for the 2PC coordinator. When a host crashes between phase 1 and phase 2, prepared transact…
|
||||
- [registry](./src/registry/participant-registry.md) — The participant registry is the install-time admission control between plugins and the 2PC coordinator. Plugins declare…
|
||||
- [participant-registry](./src/registry/participant-registry.md) — The participant registry is the install-time admission control between plugins and the 2PC coordinator. Plugins declare…
|
||||
- [testing](./src/testing/conformance.md) — Provides `run2pcParticipantConformance(factory)` — a runnable test suite that pins the contract every 2PC writer partic…
|
||||
- [conformance](./src/testing/conformance.md) — Provides `run2pcParticipantConformance(factory)` — a runnable test suite that pins the contract every 2PC writer partic…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Why
|
||||
|
||||
Some cross-database workflows demand atomic visibility — no partial
|
||||
state, no eventual convergence. Per the API Plugin Foundation design
|
||||
doc §9.2 the coordinator runs phase-1 prepare across every registered
|
||||
participant, persists the vote tally to `suaveplan_2pc_transactions`
|
||||
on the host's metadata DB, then issues `COMMIT PREPARED` or
|
||||
`ROLLBACK PREPARED` against every writer participant. Recovery on the
|
||||
next boot resolves any prepared transactions left over from a host
|
||||
crash.
|
||||
|
||||
## Modules
|
||||
|
||||
- `coordinator/` — `createTwoPcCoordinator` + `run2pc` factory and
|
||||
state machine.
|
||||
- `registry/` — `TwoPcParticipantRegistry` (admission control).
|
||||
- `phases/` — `preparePhaseOne`, `commitPhaseTwo`, `rollbackPhaseTwo`,
|
||||
`gid` helpers.
|
||||
- `persistence/` — `TwoPcLedgerStore` over the host-owned
|
||||
`suaveplan_2pc_transactions` table.
|
||||
- `recovery/` — boot-time orphan resolution via `pg_prepared_xacts`.
|
||||
- `limits/` — `OutstandingPreparedCap` (admission), `assertTimeoutBelowCeiling`
|
||||
(per-call ceiling).
|
||||
- `instruments/` — OTel meter/tracer bundle.
|
||||
- `options/` — Zod-validated coordinator options + defaults.
|
||||
- `error-codes/` — 10 `TWOPC_*` codes + `TwoPcError`.
|
||||
- `types/` — `Run2PcInput`, `Run2PcOutcome`, participant + state types.
|
||||
- `testing/` — `run2pcParticipantConformance(factory)` suite.
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { createTwoPcCoordinator } from "@suaveplan/api-2pc";
|
||||
|
||||
const coordinator = createTwoPcCoordinator({
|
||||
options: { hostInstanceId: "host-01" },
|
||||
registry,
|
||||
ledgerDb,
|
||||
clock: () => Date.now(),
|
||||
idGenerator: () => crypto.randomUUID(),
|
||||
});
|
||||
|
||||
const outcome = await coordinator.run2pc({
|
||||
participants: [
|
||||
{ id: "billing", input: { invoiceId: "inv-1" } },
|
||||
{ id: "inventory", input: { sku: "abc-9" } },
|
||||
],
|
||||
});
|
||||
// outcome.state is "committed" | "aborted" | "in_doubt"
|
||||
```
|
||||
|
||||
## Defaults
|
||||
|
||||
- `timeoutMs`: **5000** (operator-tunable per-call up to the
|
||||
30000 ms ceiling).
|
||||
- `outstandingPreparedCap`: **256** (operator-tunable via host
|
||||
config).
|
||||
|
||||
## Error namespace
|
||||
|
||||
Every production code path throws `TwoPcError` with one of the ten
|
||||
`TWOPC_*` codes. See `src/error-codes/error-codes.md` for the full
|
||||
catalogue.
|
||||
|
||||
## Conformance
|
||||
|
||||
Every adapter implementing a writer participant MUST run
|
||||
`run2pcParticipantConformance({ create: ... })` in a
|
||||
`tests/conformance/run-2pc-participant.test.ts` file. The harness
|
||||
pins prepare / commit / rollback semantics and the idempotency
|
||||
contract.
|
||||
|
||||
150
docs/api-stack/api/api-2pc/src/coordinator/coordinator.md
Normal file
150
docs/api-stack/api/api-2pc/src/coordinator/coordinator.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# `coordinator/`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Top-level orchestration for the 2PC protocol. `createTwoPcCoordinator`
|
||||
builds a fully-wired coordinator that holds a participant registry, a
|
||||
ledger store, an admission counter, and an OTel instrument bundle.
|
||||
The `run2pc({ participants, timeoutMs? })` entry point owns the full
|
||||
state machine described in design doc §9.2:
|
||||
|
||||
1. Admission via the outstanding-prepared cap.
|
||||
2. Ledger insert at `voting`.
|
||||
3. Phase-1 prepare per participant, with the
|
||||
`2pc.prepare_duration_ms` histogram and a `2pc.prepare` span per
|
||||
participant.
|
||||
4. Either unanimous commit → `markAllVotedCommit` → phase-2
|
||||
`COMMIT PREPARED` per writer; OR any abort/error →
|
||||
`markAborting` → phase-2 `ROLLBACK PREPARED` per prepared writer.
|
||||
5. Terminal ledger stamp (`committed` / `aborted` / `in_doubt`).
|
||||
|
||||
The coordinator is the only path application code ever touches.
|
||||
Every smaller module (phases, persistence, recovery, limits) is
|
||||
internal plumbing this factory composes.
|
||||
|
||||
## Features
|
||||
|
||||
- Single public surface: `createTwoPcCoordinator(deps)` returns
|
||||
`{ run2pc, outstandingCount, options }`.
|
||||
- Default `timeoutMs` from options (5000), per-call override accepted
|
||||
up to the 30000 ceiling (enforced via
|
||||
`limits/timeout-ceiling`).
|
||||
- Admission cap (default 256) enforced via
|
||||
`limits/outstanding-cap`; over-cap calls fail with
|
||||
`TWOPC_OUTSTANDING_LIMIT_EXCEEDED` before any participant is
|
||||
contacted.
|
||||
- Hooks every documented OTel instrument (`2pc.transaction` span,
|
||||
`2pc.prepare` / `2pc.commit` / `2pc.rollback` child spans,
|
||||
`2pc.transactions_total` counter, `prepare`/`commit_duration_ms`
|
||||
histograms, `outstanding_prepared` gauge).
|
||||
- Ledger persistence is bidirectional — every state transition writes
|
||||
through `TwoPcLedgerStore`, so the recovery runner has a full audit
|
||||
trail on restart.
|
||||
- Abort handling: vote_abort, prepare_failed, and timeout all funnel
|
||||
to the rollback branch; rollback errors escalate to `in_doubt`
|
||||
rather than failing the rollback path itself.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createTwoPcCoordinator } from "@suaveplan/api-2pc/coordinator";
|
||||
|
||||
const coordinator = createTwoPcCoordinator({
|
||||
options: { hostInstanceId: "host-01" },
|
||||
registry,
|
||||
ledgerDb,
|
||||
clock: () => Date.now(),
|
||||
idGenerator: () => crypto.randomUUID(),
|
||||
});
|
||||
|
||||
const outcome = await coordinator.run2pc({
|
||||
participants: [
|
||||
{ id: "billing", input: { invoiceId: "inv-1" } },
|
||||
{ id: "inventory", input: { sku: "abc-9" } },
|
||||
],
|
||||
});
|
||||
console.log(outcome.state); // "committed" | "aborted" | "in_doubt"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Inject a custom instruments bundle to feed a hand-rolled
|
||||
MeterProvider, override the default timeout per call, and consume the
|
||||
per-participant vote summary on the outcome:
|
||||
|
||||
```ts
|
||||
import { createTwoPcCoordinator } from "@suaveplan/api-2pc/coordinator";
|
||||
import { createTwoPcInstruments } from "@suaveplan/api-2pc/instruments";
|
||||
|
||||
const instruments = createTwoPcInstruments({
|
||||
meterProvider,
|
||||
outstandingPreparedSnapshot: () => coordinator.outstandingCount(),
|
||||
});
|
||||
|
||||
const coordinator = createTwoPcCoordinator({
|
||||
options: {
|
||||
hostInstanceId: "host-01",
|
||||
defaultTimeoutMs: 10_000,
|
||||
outstandingPreparedCap: 64,
|
||||
},
|
||||
registry,
|
||||
ledgerDb,
|
||||
clock: () => Date.now(),
|
||||
idGenerator: () => crypto.randomUUID(),
|
||||
instruments,
|
||||
});
|
||||
|
||||
const outcome = await coordinator.run2pc({
|
||||
participants: [
|
||||
{ id: "billing", input: { invoiceId } },
|
||||
{ id: "audit", input: { event: "invoice.created" } },
|
||||
],
|
||||
timeoutMs: 2_500,
|
||||
});
|
||||
for (const v of outcome.votes) {
|
||||
auditLog.append({
|
||||
txId: outcome.txId,
|
||||
participant: v.participantId,
|
||||
vote: v.vote,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface TwoPcCoordinatorDeps`
|
||||
- `options: TwoPcCoordinatorOptions`
|
||||
- `registry: TwoPcParticipantRegistry`
|
||||
- `ledgerDb: LedgerDb`
|
||||
- `clock: Clock`
|
||||
- `idGenerator: IdGenerator`
|
||||
- `instruments?: TwoPcInstruments`
|
||||
- `interface TwoPcCoordinator`
|
||||
- `run2pc(input: Run2PcInput): Promise<Run2PcOutcome>`
|
||||
- `outstandingCount(): number`
|
||||
- `options: ResolvedTwoPcCoordinatorOptions`
|
||||
- `createTwoPcCoordinator(deps): TwoPcCoordinator` — factory.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function is structured as one large `try` / `finally` so cleanup
|
||||
(span end, deadline timer clear, admission release) runs regardless
|
||||
of which branch fires. The pattern keeps the happy path linear and
|
||||
the failure paths exhaustive without nested try blocks.
|
||||
|
||||
The "any commit failure → in_doubt" escalation is deliberate. Once
|
||||
the coordinator has issued `COMMIT PREPARED` against at least one
|
||||
participant, the only correct follow-up is to keep trying — but the
|
||||
hot path cannot block on that. Instead, the row is marked `in_doubt`,
|
||||
the OTel counter ticks the `in_doubt` outcome, and the recovery
|
||||
runner picks it up on the next boot.
|
||||
|
||||
Vote-abort and prepare-failed paths both call `ac.abort()` BEFORE
|
||||
running phase 2, so any prepared participants whose subsequent SQL
|
||||
hits the signal map to `TWOPC_TIMEOUT` rather than getting stuck. The
|
||||
deadline timer is cleared in the `finally` so a successful commit
|
||||
does not leave a dangling abort handle on the AbortController.
|
||||
97
docs/api-stack/api/api-2pc/src/error-codes/error-codes.md
Normal file
97
docs/api-stack/api/api-2pc/src/error-codes/error-codes.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# `error-codes/`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the `TWOPC_*` error-code namespace and the `TwoPcError`
|
||||
concrete class for the `@suaveplan/api-2pc` package. The registry is the
|
||||
canonical contract every production code path in the coordinator throws
|
||||
against — observability dashboards, support runbooks, and integrating
|
||||
plugins all join on these stable string codes.
|
||||
|
||||
The codes are intentionally enumerated up-front rather than grown
|
||||
ad-hoc: a missing code is a spec deviation that a CI gate can detect,
|
||||
and the closed set lets operators write alerting rules without
|
||||
constantly chasing schema drift.
|
||||
|
||||
## Features
|
||||
|
||||
- Ten stable codes (`TIMEOUT`, `VOTE_ABORT`, `PREPARE_FAILED`,
|
||||
`COMMIT_FAILED`, `ROLLBACK_FAILED`, `IN_DOUBT`,
|
||||
`PARTICIPANT_NOT_PREPARABLE`, `READ_ONLY_PARTICIPANT_CANNOT_WRITE`,
|
||||
`OUTSTANDING_LIMIT_EXCEEDED`, `TIMEOUT_CEILING_EXCEEDED`).
|
||||
- Each code is exposed via `TWOPC_CODES.<NAME>` returning the prefixed
|
||||
string `"TWOPC.<NAME>"` produced by `defineCodes`.
|
||||
- `TwoPcError` extends `SuaveplanError` so cause-chain traversal,
|
||||
Zod-validated wire format, and the OTel span-event adapter from
|
||||
`@suaveplan/error` all apply unchanged.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
|
||||
|
||||
throw new TwoPcError(
|
||||
TWOPC_CODES.TIMEOUT,
|
||||
"Phase-1 deadline elapsed",
|
||||
{ context: { txId, timeoutMs: 5000 } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Branching on error code at the boundary between coordinator and caller
|
||||
is the recommended consumer pattern. The narrowed `twoPcCode` field
|
||||
gives an exhaustive switch the compiler can check:
|
||||
|
||||
```ts
|
||||
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
|
||||
|
||||
try {
|
||||
await run2pc({ participants, timeoutMs: 5000 }, ctx);
|
||||
} catch (err) {
|
||||
if (err instanceof TwoPcError) {
|
||||
switch (err.twoPcCode) {
|
||||
case TWOPC_CODES.TIMEOUT:
|
||||
metrics.incr("billing.2pc.timeout");
|
||||
return retryWithBackoff();
|
||||
case TWOPC_CODES.VOTE_ABORT:
|
||||
return surfaceUserError();
|
||||
case TWOPC_CODES.IN_DOUBT:
|
||||
pageOncall(err);
|
||||
return;
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `TWOPC_CODES` — frozen object `{ TIMEOUT: "TWOPC.TIMEOUT", ... }`.
|
||||
- `type TwoPcErrorCode` — string-literal union of every value in
|
||||
`TWOPC_CODES`.
|
||||
- `class TwoPcError extends SuaveplanError<TwoPcErrorCode>` — exposes
|
||||
`twoPcCode` as a same-typed accessor for ergonomic narrowing without
|
||||
re-asserting at every catch site.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` from `@suaveplan/error` produces the prefixed values at
|
||||
runtime, so every code string starts with `"TWOPC."`. This matches the
|
||||
prefix convention used by sibling api-stack packages (`HOST.*`,
|
||||
`PLUGIN.*`, `RELOAD.*`, `LEDGER.*`). The `TwoPcError` class carries a
|
||||
redundant `twoPcCode` field — duplicating `code` — purely to give
|
||||
callers a name they don't have to import the generic
|
||||
`SuaveplanError<C>` type to narrow against. The duplication is
|
||||
deliberate and harmless because the base class makes `code` readonly.
|
||||
|
||||
The registry is closed; adding a code requires an OpenSpec change so
|
||||
every consumer dashboard and runbook stays in sync. Coverage assertions
|
||||
ensure every code is actually thrown by a production path before this
|
||||
package can graduate to `1.0.0`.
|
||||
105
docs/api-stack/api/api-2pc/src/instruments/instruments.md
Normal file
105
docs/api-stack/api/api-2pc/src/instruments/instruments.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# `instruments/`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Builds the OTel instrument registry the 2PC coordinator emits against
|
||||
on every transaction. Each instrument's name follows design doc §9.2
|
||||
exactly so a missing entry is a spec deviation a CI gate could detect.
|
||||
Centralising creation keeps the coordinator code free of meter / tracer
|
||||
plumbing — every phase module simply records against the frozen
|
||||
bundle returned here.
|
||||
|
||||
The observable gauge wires into the coordinator's outstanding-prepared
|
||||
counter via a snapshot callback, so dashboards see the live admission
|
||||
state without the coordinator pushing on every change.
|
||||
|
||||
## Features
|
||||
|
||||
- One span: `2pc.transaction` per transaction lifetime; per-phase
|
||||
child spans `2pc.prepare`, `2pc.commit`, `2pc.rollback` (started by
|
||||
the phase modules themselves, using the tracer this bundle exposes).
|
||||
- One counter: `2pc.transactions_total` labelled `{ outcome }`.
|
||||
- Two histograms: `2pc.prepare_duration_ms`, `2pc.commit_duration_ms`
|
||||
labelled `{ outcome }`.
|
||||
- One observable gauge: `2pc.outstanding_prepared` wired to a
|
||||
snapshot callback supplied at construction.
|
||||
- Provider defaults to the OTel globals (no-op when no SDK is
|
||||
installed) so unit tests do not have to wire a provider.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createTwoPcInstruments } from "@suaveplan/api-2pc/instruments";
|
||||
|
||||
const instruments = createTwoPcInstruments({
|
||||
outstandingPreparedSnapshot: () => coordinator.outstandingCount(),
|
||||
});
|
||||
|
||||
instruments.transactionsTotal.add(1, { outcome: "committed" });
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Inject a real OTel SDK and a custom snapshot to feed a live dashboard:
|
||||
|
||||
```ts
|
||||
import { metrics, trace } from "@opentelemetry/api";
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
import { createTwoPcInstruments } from "@suaveplan/api-2pc/instruments";
|
||||
|
||||
const meterProvider = new MeterProvider({ /* readers/exporters */ });
|
||||
metrics.setGlobalMeterProvider(meterProvider);
|
||||
|
||||
const instruments = createTwoPcInstruments({
|
||||
meterProvider,
|
||||
tracerProvider: trace.getTracerProvider(),
|
||||
outstandingPreparedSnapshot: () => coordinator.outstandingCount(),
|
||||
});
|
||||
|
||||
const span = instruments.tracer.startSpan("2pc.transaction", {
|
||||
attributes: { "tx.id": txId, participant_count: 2 },
|
||||
});
|
||||
try {
|
||||
// ...
|
||||
instruments.prepareDurationMs.record(prepareElapsedMs, {
|
||||
outcome: "committed",
|
||||
});
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type TwoPcOutcome = "committed" | "aborted" | "in_doubt"` — values
|
||||
attached to the `outcome` attribute on every counter / histogram
|
||||
observation.
|
||||
- `interface TwoPcInstrumentsConfig`
|
||||
- `meterProvider?: MeterProvider`
|
||||
- `tracerProvider?: TracerProvider`
|
||||
- `outstandingPreparedSnapshot: () => number`
|
||||
- `interface TwoPcInstruments` — frozen bundle exposing `meter`,
|
||||
`tracer`, `transactionsTotal`, `prepareDurationMs`,
|
||||
`commitDurationMs`, `outstandingPreparedGauge`.
|
||||
- `createTwoPcInstruments(config): TwoPcInstruments` — factory.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The observable gauge's callback fires whenever the SDK collects metrics
|
||||
(typically on a fixed interval driven by the exporter). The snapshot
|
||||
returned by `outstandingPreparedSnapshot` is therefore an
|
||||
eventually-consistent value — operators must not treat it as a strict
|
||||
upper bound for admission decisions. The strict upper bound is enforced
|
||||
inside `limits/outstanding-cap.ts`; the gauge is purely a dashboard
|
||||
signal.
|
||||
|
||||
Histogram unit is set explicitly to `"ms"` per OTel naming convention.
|
||||
The two histograms are named after the phases they measure so a single
|
||||
dashboard panel can render side-by-side latency for prepare and commit
|
||||
without re-labelling — operators flagging long prepares typically
|
||||
care more about phase-1 latency because that is when row-level locks
|
||||
are held on every participant.
|
||||
104
docs/api-stack/api/api-2pc/src/limits/outstanding-cap.md
Normal file
104
docs/api-stack/api/api-2pc/src/limits/outstanding-cap.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# `limits/outstanding-cap`
|
||||
|
||||
## Purpose
|
||||
|
||||
Admission control for in-flight 2PC transactions. The coordinator
|
||||
holds a fixed-size budget of "outstanding prepared transactions"; once
|
||||
the budget is exhausted new `run2pc` calls refuse rather than queue.
|
||||
The default is 256 (design doc §14's open question, pinned by this
|
||||
package's spec) and operators can tune it via host config.
|
||||
|
||||
Refusing rather than queuing is intentional: Postgres holds row-level
|
||||
locks for every prepared transaction, so a backed-up coordinator
|
||||
would otherwise convert a transient overload into a global lock
|
||||
storm. Failing fast lets callers route to a degraded path
|
||||
(degraded-write, retry queue, etc.).
|
||||
|
||||
## Features
|
||||
|
||||
- `OutstandingPreparedCap` — synchronous counter with `admit()`,
|
||||
`release()`, `snapshot()`, `capacity()`.
|
||||
- Constructor validates that the cap is a positive integer (rejects
|
||||
zero, negatives, fractions, `NaN`).
|
||||
- `release()` is idempotent below zero — a buggy double-release in
|
||||
caller code cannot silently expand the real cap.
|
||||
- Synchronous-only — relies on the event-loop atomicity guarantee
|
||||
between `admit` / `release` pairs in JavaScript runtimes.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { OutstandingPreparedCap } from "@suaveplan/api-2pc/limits";
|
||||
|
||||
const cap = new OutstandingPreparedCap(256);
|
||||
|
||||
cap.admit();
|
||||
try {
|
||||
// run the 2PC transaction
|
||||
} finally {
|
||||
cap.release();
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Wrap admission inside the coordinator's public entry to centralise the
|
||||
release in one `finally`. The snapshot fuels the OTel gauge:
|
||||
|
||||
```ts
|
||||
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
|
||||
import { OutstandingPreparedCap } from "@suaveplan/api-2pc/limits";
|
||||
|
||||
class MiniCoordinator {
|
||||
private readonly cap = new OutstandingPreparedCap(256);
|
||||
|
||||
snapshot() {
|
||||
return this.cap.snapshot();
|
||||
}
|
||||
|
||||
async run<T>(fn: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
this.cap.admit();
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof TwoPcError &&
|
||||
err.twoPcCode === TWOPC_CODES.OUTSTANDING_LIMIT_EXCEEDED
|
||||
) {
|
||||
// route to degraded path
|
||||
throw err;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
this.cap.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class OutstandingPreparedCap`
|
||||
- `constructor(cap: number)` — throws
|
||||
`TWOPC_OUTSTANDING_LIMIT_EXCEEDED` on invalid `cap`.
|
||||
- `admit(): void` — throws
|
||||
`TWOPC_OUTSTANDING_LIMIT_EXCEEDED` when at capacity.
|
||||
- `release(): void` — idempotent below zero.
|
||||
- `snapshot(): number` — current outstanding count.
|
||||
- `capacity(): number` — configured cap.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Construction-time validation re-uses the same error code as the
|
||||
runtime cap-exceeded path. This is deliberate: operators tuning the
|
||||
cap via host config want one error string to alert on, regardless of
|
||||
whether the misconfiguration was a fractional value at startup or a
|
||||
real overload at steady state. The context object on the thrown
|
||||
`TwoPcError` distinguishes the two cases (`cap` only vs.
|
||||
`cap` + `outstanding`).
|
||||
|
||||
The class is intentionally tiny — no async, no internal queue, no
|
||||
external dependencies. Adding a queue here would re-introduce the
|
||||
lock-storm risk the cap exists to prevent.
|
||||
77
docs/api-stack/api/api-2pc/src/limits/timeout-ceiling.md
Normal file
77
docs/api-stack/api/api-2pc/src/limits/timeout-ceiling.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# `limits/timeout-ceiling`
|
||||
|
||||
## Purpose
|
||||
|
||||
Enforces the hard 30 000 ms ceiling on caller-supplied `timeoutMs`
|
||||
values. The check fires at the public entry point of `run2pc` so
|
||||
above-ceiling calls never contact a participant; the operational
|
||||
warning in design doc §9.2 is that long prepares hold row-level locks,
|
||||
so surfacing the misuse loudly is more useful than silently clamping.
|
||||
|
||||
This module is intentionally tiny — one function — because the
|
||||
behaviour is small but the gate is one of the most-asserted contracts
|
||||
in the spec.
|
||||
|
||||
## Features
|
||||
|
||||
- `assertTimeoutBelowCeiling(timeoutMs)` — throws
|
||||
`TWOPC_TIMEOUT_CEILING_EXCEEDED` for above-ceiling, non-positive,
|
||||
non-finite, or `NaN` inputs.
|
||||
- Re-uses the `TIMEOUT_CEILING_MS` constant from `options/` so the
|
||||
ceiling stays a single source of truth.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { assertTimeoutBelowCeiling } from "@suaveplan/api-2pc/limits";
|
||||
|
||||
assertTimeoutBelowCeiling(5000); // ok
|
||||
assertTimeoutBelowCeiling(30001); // throws TWOPC_TIMEOUT_CEILING_EXCEEDED
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
The function is intentionally side-effect-free so callers can wrap it
|
||||
in a per-tenant override path:
|
||||
|
||||
```ts
|
||||
import { TIMEOUT_CEILING_MS } from "@suaveplan/api-2pc/options";
|
||||
import { assertTimeoutBelowCeiling } from "@suaveplan/api-2pc/limits";
|
||||
|
||||
function resolveTimeout(
|
||||
requested: number | undefined,
|
||||
tenantMax: number
|
||||
): number {
|
||||
const effective = requested ?? DEFAULT_TIMEOUT_MS;
|
||||
assertTimeoutBelowCeiling(effective);
|
||||
if (effective > tenantMax) {
|
||||
return tenantMax;
|
||||
}
|
||||
if (effective > TIMEOUT_CEILING_MS) {
|
||||
// unreachable given the assert above — included only to
|
||||
// illustrate that the ceiling acts as the strict upper bound.
|
||||
return TIMEOUT_CEILING_MS;
|
||||
}
|
||||
return effective;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `function assertTimeoutBelowCeiling(timeoutMs: number): void`
|
||||
- Throws `TwoPcError(TWOPC_CODES.TIMEOUT_CEILING_EXCEEDED)` for any
|
||||
of: `NaN`, `Infinity`, `-Infinity`, values `<= 0`, values
|
||||
`> TIMEOUT_CEILING_MS`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The check rejects non-positive values for the same reason it rejects
|
||||
above-ceiling ones: an immediate or backwards-in-time deadline would
|
||||
collapse phase 1 to a single tick and the rollback path would fire
|
||||
before any participant could finish a real prepare. Treating that as
|
||||
the same misuse class as "30 minutes" is a deliberate ergonomic
|
||||
choice — operators get one error code to grep for.
|
||||
|
||||
`Number.isFinite` is used in preference to `Number.isNaN` because the
|
||||
former covers `Infinity` and `-Infinity` in addition to `NaN`. The
|
||||
explicit `<= 0` check then excludes zero and negative-finite values.
|
||||
96
docs/api-stack/api/api-2pc/src/options/options.md
Normal file
96
docs/api-stack/api/api-2pc/src/options/options.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# `options/`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the boundary-validated option shape consumed by
|
||||
`createTwoPcCoordinator`. Every tunable that affects coordinator
|
||||
behaviour (host instance id, outstanding-prepared cap, default phase-1
|
||||
timeout) lives here, with Zod validation at the seam and explicit
|
||||
defaults pinned to the design doc §9.2 numbers.
|
||||
|
||||
Centralising option resolution in one module keeps the coordinator
|
||||
free of conditional fallback logic — by the time it sees the resolved
|
||||
shape every field is concrete, so subsequent code never has to
|
||||
re-check `undefined`.
|
||||
|
||||
## Features
|
||||
|
||||
- `DEFAULT_TIMEOUT_MS = 5000` — phase-1 deadline default per design
|
||||
doc §9.2.
|
||||
- `TIMEOUT_CEILING_MS = 30000` — hard ceiling enforced at call time
|
||||
(the coordinator's `limits/timeout-ceiling` module imports this).
|
||||
- `DEFAULT_OUTSTANDING_PREPARED_CAP = 256` — admission-control default
|
||||
per design doc §14.
|
||||
- `twoPcCoordinatorOptionsSchema` — Zod schema, `strict()` so typos
|
||||
surface as Zod errors instead of being silently ignored.
|
||||
- `resolveTwoPcCoordinatorOptions(raw)` — validates and fills in
|
||||
defaults; returns the fully-populated `ResolvedTwoPcCoordinatorOptions`
|
||||
shape the coordinator consumes.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
resolveTwoPcCoordinatorOptions,
|
||||
} from "@suaveplan/api-2pc/options";
|
||||
|
||||
const resolved = resolveTwoPcCoordinatorOptions({
|
||||
hostInstanceId: "host-01",
|
||||
});
|
||||
|
||||
console.log(resolved.defaultTimeoutMs === DEFAULT_TIMEOUT_MS); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Override every default — useful for canary hosts where operators want
|
||||
a tighter admission cap and a longer prepare window:
|
||||
|
||||
```ts
|
||||
import { resolveTwoPcCoordinatorOptions } from "@suaveplan/api-2pc/options";
|
||||
|
||||
const resolved = resolveTwoPcCoordinatorOptions({
|
||||
hostInstanceId: "canary-eu-west-1",
|
||||
outstandingPreparedCap: 64,
|
||||
defaultTimeoutMs: 15_000,
|
||||
});
|
||||
```
|
||||
|
||||
Calls supplying `defaultTimeoutMs` above `TIMEOUT_CEILING_MS` raise a
|
||||
Zod error at this boundary — the coordinator's runtime ceiling check
|
||||
in `limits/timeout-ceiling.ts` then surfaces the misuse on per-call
|
||||
overrides as well.
|
||||
|
||||
## API Reference
|
||||
|
||||
- `const DEFAULT_TIMEOUT_MS: number` — 5000.
|
||||
- `const TIMEOUT_CEILING_MS: number` — 30000.
|
||||
- `const DEFAULT_OUTSTANDING_PREPARED_CAP: number` — 256.
|
||||
- `interface TwoPcCoordinatorOptions` — caller-supplied input shape.
|
||||
- `interface ResolvedTwoPcCoordinatorOptions` — internal shape after
|
||||
defaults.
|
||||
- `const twoPcCoordinatorOptionsSchema: z.ZodType` — strict-shape
|
||||
Zod schema.
|
||||
- `function resolveTwoPcCoordinatorOptions(raw): ResolvedTwoPcCoordinatorOptions`
|
||||
— validates and resolves defaults.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema uses `.strict()` so unknown keys fail validation rather than
|
||||
silently being dropped. The default for `defaultTimeoutMs` is applied
|
||||
post-parse with `?? DEFAULT_TIMEOUT_MS` rather than via
|
||||
`z.default(...)` so the schema continues to reject above-ceiling
|
||||
values regardless of whether the caller supplied the field — Zod's
|
||||
`default` would short-circuit `.max()` checks when the field is
|
||||
omitted, which is the wrong shape for a ceiling-enforced option.
|
||||
|
||||
The module deliberately does NOT re-throw Zod failures as
|
||||
`TwoPcError` — that's the responsibility of the coordinator's
|
||||
public entry points. Keeping this module pure of cross-package error
|
||||
mapping makes it reusable from tests that want a raw Zod surface for
|
||||
property-based shape testing.
|
||||
114
docs/api-stack/api/api-2pc/src/persistence/persistence.md
Normal file
114
docs/api-stack/api/api-2pc/src/persistence/persistence.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# `persistence/`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Persists every coordinator state transition into the
|
||||
`suaveplan_2pc_transactions` ledger table. The table is owned by
|
||||
`@suaveplan/api-host` (migration id 2, `saga-2pc-reserved`); this
|
||||
module only reads and writes rows so the host stays the single source
|
||||
of truth for schema evolution.
|
||||
|
||||
The ledger is what makes 2PC durable across coordinator restarts.
|
||||
Without it, a host that crashes mid-prepare would lose track of which
|
||||
participants were `PREPARE TRANSACTION`'d and could not safely choose
|
||||
between commit and rollback on the next boot.
|
||||
|
||||
## Features
|
||||
|
||||
- `TwoPcLedgerStore` — CRUD facade over `suaveplan_2pc_transactions`
|
||||
with statements for every state transition (`insert`,
|
||||
`markAllVotedCommit`, `markAborting`, `markCommitted`, `markAborted`,
|
||||
`markInDoubt`).
|
||||
- `findById` — single-row lookup for state inspection.
|
||||
- `findRecoverable` — scan returning every non-terminal row for the
|
||||
current host instance id. Drives `recovery/`.
|
||||
- `TwoPcLedgerStore.decodeParticipants` — static helper that parses the
|
||||
`participants_json` blob back into a typed array.
|
||||
- Structural `LedgerDb` / `LedgerStatement` interfaces so the store
|
||||
works with `bun:sqlite`, `@suaveplan/db-sqlite`'s thin wrapper, and
|
||||
`better-sqlite3` without any concrete dependency.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { Database } from "bun:sqlite";
|
||||
import { TwoPcLedgerStore } from "@suaveplan/api-2pc/persistence";
|
||||
|
||||
const db = new Database(":memory:");
|
||||
// Schema is created by api-host's migration runner — see migrations.ts
|
||||
// migration id 2 (`saga-2pc-reserved`). In tests, run the host runner
|
||||
// against the same database.
|
||||
|
||||
const store = new TwoPcLedgerStore(db as never);
|
||||
store.insert({
|
||||
id: "tx-1",
|
||||
coordinator: "host-instance-01",
|
||||
participants: [
|
||||
{ participantId: "billing", voterMode: "writer", vote: null },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Walk the recovery candidates and decode each blob — the typical shape
|
||||
used inside `recovery/recovery.ts`:
|
||||
|
||||
```ts
|
||||
import { TwoPcLedgerStore } from "@suaveplan/api-2pc/persistence";
|
||||
|
||||
function listRecoverable(store: TwoPcLedgerStore, hostId: string) {
|
||||
const rows = store.findRecoverable(hostId);
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
state: row.state,
|
||||
participants: TwoPcLedgerStore.decodeParticipants(row),
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
The static `decodeParticipants` is deliberately exposed so callers
|
||||
working with raw rows (e.g. ad-hoc admin scripts) do not have to
|
||||
recreate the JSON contract themselves.
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface LedgerStatement` / `interface LedgerDb` — structural
|
||||
SQLite-shaped views.
|
||||
- `interface TwoPcLedgerRow` — exact row shape returned by `findById`
|
||||
/ `findRecoverable`.
|
||||
- `interface LedgerParticipantEntry` — per-participant entry in
|
||||
`participants_json`.
|
||||
- `class TwoPcLedgerStore`
|
||||
- `constructor(db: LedgerDb)`
|
||||
- `insert({ id, coordinator, participants })`
|
||||
- `markAllVotedCommit({ id, participants, preparedAt })`
|
||||
- `markAborting({ id, participants })`
|
||||
- `markCommitted(id, committedAt)`
|
||||
- `markAborted(id, abortedAt)`
|
||||
- `markInDoubt(id)`
|
||||
- `findById(id): TwoPcLedgerRow | null`
|
||||
- `findRecoverable(coordinator): readonly TwoPcLedgerRow[]`
|
||||
- `static decodeParticipants(row): readonly LedgerParticipantEntry[]`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Every UPDATE uses a single statement with the new `state` value
|
||||
matched against `TwoPcCoordinatorState`'s string-literal union — the
|
||||
`satisfies TwoPcCoordinatorState` annotation catches typos at compile
|
||||
time. The store does NOT enforce a state machine on top of the table;
|
||||
that responsibility belongs to the coordinator. Keeping the
|
||||
persistence layer state-machine-free lets recovery use the same store
|
||||
without conflating "what changed in this lifetime" with "what the row
|
||||
looked like after a crash".
|
||||
|
||||
The `participants_json` blob is stored as a JSON string rather than a
|
||||
side table — the coordinator never queries by participant id, only by
|
||||
transaction id, so a JSON blob has zero indexing cost and survives
|
||||
schema evolution painlessly. `decodeParticipants` returns an empty
|
||||
array when the blob is not an array — defensive against rows
|
||||
hand-written by ad-hoc admin scripts.
|
||||
94
docs/api-stack/api/api-2pc/src/phases/commit.md
Normal file
94
docs/api-stack/api/api-2pc/src/phases/commit.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# `phases/commit`
|
||||
|
||||
## Purpose
|
||||
|
||||
Phase-2 commit for a single participant. Once every participant has
|
||||
returned `commit` from phase 1 and the coordinator has persisted the
|
||||
`all-voted-commit` state to the ledger, this module emits
|
||||
`COMMIT PREPARED '<gid>'` against each writer participant and records
|
||||
`2pc.commit_duration_ms`.
|
||||
|
||||
Phase 2 is intentionally unbounded by timeout — the protocol cannot
|
||||
tolerate giving up on a commit-prepared mid-flight, because doing so
|
||||
would leave the system in `in_doubt` state and require the recovery
|
||||
runner to resolve it on the next boot.
|
||||
|
||||
## Features
|
||||
|
||||
- Skips participants that did not actually prepare (`outcome.prepared
|
||||
=== false`) — read-only voters and abort-voters never see commit
|
||||
SQL.
|
||||
- Emits a `2pc.commit` span with `tx.id`, `participant.id`, and `gid`
|
||||
attributes.
|
||||
- Records `2pc.commit_duration_ms` tagged with `outcome: "committed"`
|
||||
on success.
|
||||
- Failure preserves the underlying cause on a thrown
|
||||
`TwoPcError(TWOPC_COMMIT_FAILED)`; the coordinator escalates to
|
||||
`TWOPC_IN_DOUBT` once it has tried every participant.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { commitPhaseTwo } from "@suaveplan/api-2pc/phases";
|
||||
|
||||
await commitPhaseTwo({
|
||||
txId,
|
||||
participant,
|
||||
outcome: preparedOutcome,
|
||||
instruments,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
The coordinator collects per-participant failures rather than
|
||||
short-circuiting — every participant gets attempted exactly once so
|
||||
operators can see the full picture in the ledger:
|
||||
|
||||
```ts
|
||||
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
|
||||
import { commitPhaseTwo } from "@suaveplan/api-2pc/phases";
|
||||
|
||||
const failures: TwoPcError[] = [];
|
||||
for (const outcome of preparedOutcomes) {
|
||||
if (!outcome.prepared) continue;
|
||||
try {
|
||||
await commitPhaseTwo({
|
||||
txId,
|
||||
participant: registry.get(outcome.participantId)!,
|
||||
outcome,
|
||||
instruments,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof TwoPcError) failures.push(err);
|
||||
else throw err;
|
||||
}
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
// ledger.markInDoubt(txId); recovery resolves on next boot
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface CommitPhaseInput`
|
||||
- `txId: string`
|
||||
- `participant: RegisteredTwoPcParticipant`
|
||||
- `outcome: ParticipantPrepareOutcome` — the phase-1 result.
|
||||
- `instruments: TwoPcInstruments`
|
||||
- `function commitPhaseTwo(input): Promise<void>`
|
||||
- no-op for `outcome.prepared === false`
|
||||
- throws `TwoPcError(TWOPC_COMMIT_FAILED)` on SQL failure.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The gid stored on the prepare outcome is the single source of truth —
|
||||
this module never rebuilds it. Reusing the prepare-time value
|
||||
defends against the unlikely case where a host-config change between
|
||||
phases would have produced a different gid: the SQL the coordinator
|
||||
needs to send is exactly the one it prepared with.
|
||||
|
||||
The SQL escape (`replace(/'/g, "''")`) matches the prepare-phase
|
||||
treatment so a gid containing the unlikely quote character would
|
||||
still produce valid SQL. The escape is cheap; the defence-in-depth
|
||||
costs nothing on the happy path.
|
||||
85
docs/api-stack/api/api-2pc/src/phases/gid.md
Normal file
85
docs/api-stack/api/api-2pc/src/phases/gid.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# `phases/gid`
|
||||
|
||||
## Purpose
|
||||
|
||||
Builds and parses the global transaction id (gid) string passed to
|
||||
Postgres' `PREPARE TRANSACTION '<gid>'`. The format embeds the host
|
||||
instance id so the recovery scanner can filter `pg_prepared_xacts` to
|
||||
only the rows this coordinator owns — without that anchor, two
|
||||
coordinators sharing a Postgres host would clobber each other's
|
||||
recovery decisions.
|
||||
|
||||
The module is intentionally pure: no SQL, no client. Centralising the
|
||||
gid format here makes the recovery filter and the per-participant
|
||||
prepare path share a single source of truth.
|
||||
|
||||
## Features
|
||||
|
||||
- `buildGid(hostInstanceId, txId, participantId)` — produces
|
||||
`2pc-<host>-<tx>-<participant>`.
|
||||
- `parseGid(gid, hostInstanceId)` — extracts `{ txId, participantId }`
|
||||
for gids written by the supplied host; returns `null` otherwise so
|
||||
recovery skips foreign prepared transactions silently. The
|
||||
hostInstanceId is supplied by the caller (the recovery runner)
|
||||
because the host id itself may contain hyphens and the parser cannot
|
||||
recover it unambiguously from the gid alone.
|
||||
- `gidPrefixFor(hostInstanceId)` — the prefix the recovery SQL filter
|
||||
uses; centralised so SQL and parser never drift.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { buildGid, parseGid } from "@suaveplan/api-2pc/phases";
|
||||
|
||||
const gid = buildGid("host-01", "tx-abc", "billing");
|
||||
// "2pc-host-01-tx-abc-billing"
|
||||
|
||||
const parts = parseGid(gid, "host-01");
|
||||
// { txId: "tx-abc", participantId: "billing" }
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Recovery uses the prefix filter to ignore prepared transactions
|
||||
written by other components sharing the database:
|
||||
|
||||
```ts
|
||||
import { gidPrefixFor, parseGid } from "@suaveplan/api-2pc/phases";
|
||||
|
||||
const prefix = gidPrefixFor("host-01");
|
||||
const rows = await db.run<{ gid: string }>(
|
||||
`SELECT gid FROM pg_prepared_xacts WHERE gid LIKE $1`,
|
||||
[`${prefix}%`]
|
||||
);
|
||||
for (const row of rows) {
|
||||
const parts = parseGid(row.gid, "host-01");
|
||||
if (parts === null) continue; // foreign prepared transaction
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `buildGid(hostInstanceId: string, txId: string, participantId: string): string`
|
||||
- `parseGid(gid: string, hostInstanceId: string): { txId, participantId } | null`
|
||||
- `gidPrefixFor(hostInstanceId: string): string`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`parseGid` takes the caller-supplied `hostInstanceId` and strips the
|
||||
prefix `2pc-<host>-` before extracting `txId` and `participantId`.
|
||||
The host instance id is supplied explicitly because hyphens are
|
||||
allowed inside it (production hosts often look like `host-eu-west-1`)
|
||||
and the parser cannot recover the boundary from the gid alone. The
|
||||
recovery runner always knows its own host id, so this constraint is
|
||||
ergonomic in practice.
|
||||
|
||||
Participant ids are constrained by the registry (and by host config)
|
||||
to not contain hyphens, so the LAST hyphen after the prefix
|
||||
unambiguously delimits the `participantId` from the `txId`.
|
||||
|
||||
The function returns `null` rather than throwing on unrecognised gids
|
||||
because the recovery scan iterates over rows that may include prepared
|
||||
transactions written by code outside this package's control — Postgres
|
||||
has no participant-tag column, only the gid string itself, so the
|
||||
shared-database scenario is a legitimate concern.
|
||||
105
docs/api-stack/api/api-2pc/src/phases/prepare.md
Normal file
105
docs/api-stack/api/api-2pc/src/phases/prepare.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# `phases/prepare`
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements phase 1 of the 2PC protocol for a single participant. The
|
||||
coordinator iterates participants and calls this module for each;
|
||||
unanimous commit votes trigger phase 2, any abort or prepare failure
|
||||
short-circuits to rollback. This module is the only place the
|
||||
`PREPARE TRANSACTION '<gid>'` SQL is emitted, so the gid format
|
||||
(`phases/gid`) and the SQL emission point stay co-located.
|
||||
|
||||
Phase 1 is the latency-sensitive half of 2PC: every participant holds
|
||||
row-level locks for the duration. The module records
|
||||
`2pc.prepare_duration_ms` so operators can spot creeping lock holds
|
||||
before they cause throughput regressions.
|
||||
|
||||
## Features
|
||||
|
||||
- Runs the participant's `prepare` callback, then the
|
||||
`PREPARE TRANSACTION` SQL when the vote is `commit` AND the
|
||||
participant is a writer.
|
||||
- Skips the SQL for read-only voters — they participate in the vote
|
||||
only.
|
||||
- Refuses a read-only voter that returned `writeIntent: true` with
|
||||
`TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE`.
|
||||
- Honours an `AbortSignal` for the phase-1 deadline; aborted SQL is
|
||||
remapped to `TWOPC_TIMEOUT`.
|
||||
- Emits a `2pc.prepare` span around the whole call and records a
|
||||
`2pc.prepare_duration_ms` observation tagged with the outcome.
|
||||
- Failure cases preserve the underlying cause on the thrown
|
||||
`TwoPcError`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { preparePhaseOne } from "@suaveplan/api-2pc/phases";
|
||||
|
||||
const outcome = await preparePhaseOne({
|
||||
hostInstanceId: "host-01",
|
||||
txId,
|
||||
participant,
|
||||
payload: { invoiceId: "inv-1" },
|
||||
instruments,
|
||||
signal: ac.signal,
|
||||
});
|
||||
if (outcome.vote === "abort") {
|
||||
// run rollback for every participant that voted commit
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
The coordinator runs this in a loop. Read-only voters interleave
|
||||
freely with writers — the outcome's `prepared` flag tells phase 2
|
||||
whether to send commit/rollback SQL:
|
||||
|
||||
```ts
|
||||
import { preparePhaseOne } from "@suaveplan/api-2pc/phases";
|
||||
|
||||
const outcomes: ParticipantPrepareOutcome[] = [];
|
||||
for (const p of registry.list()) {
|
||||
const o = await preparePhaseOne({
|
||||
hostInstanceId,
|
||||
txId,
|
||||
participant: p,
|
||||
payload: payloadFor(p),
|
||||
instruments,
|
||||
signal,
|
||||
});
|
||||
outcomes.push(o);
|
||||
if (o.vote === "abort") break; // short-circuit
|
||||
}
|
||||
|
||||
// phase 2: commit only those with prepared === true
|
||||
const writers = outcomes.filter((o) => o.prepared);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface ParticipantPrepareOutcome` — `{ participantId, vote,
|
||||
prepared, gid }`.
|
||||
- `interface PreparePhaseInput` — `{ hostInstanceId, txId,
|
||||
participant, payload, instruments, signal }`.
|
||||
- `function preparePhaseOne(input): Promise<ParticipantPrepareOutcome>`
|
||||
- throws `TWOPC_PREPARE_FAILED` when the participant callback or
|
||||
SQL raises (signal not aborted),
|
||||
- throws `TWOPC_TIMEOUT` when the signal aborted around the SQL,
|
||||
- throws `TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE` when a read-only
|
||||
voter returned `writeIntent: true`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The SQL escape strategy is conservative: gids are built from the host
|
||||
instance id, transaction id, and participant id — all three are
|
||||
already validated at host config / id-generator / registry time — but
|
||||
the function still calls `.replace(/'/g, "''")` before interpolating
|
||||
into the `PREPARE TRANSACTION` statement. The runtime cost is zero on
|
||||
the happy path and the defence-in-depth is cheap. The coordinator
|
||||
never accepts user-supplied SQL fragments.
|
||||
|
||||
The span is started before the prepare callback so the participant's
|
||||
own work shows up in the trace as a child of the `2pc.prepare` span if
|
||||
the participant opens its own spans against the same tracer. The
|
||||
`finally`-block `span.end()` guarantees the span is closed regardless
|
||||
of which branch fires.
|
||||
92
docs/api-stack/api/api-2pc/src/phases/rollback.md
Normal file
92
docs/api-stack/api/api-2pc/src/phases/rollback.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# `phases/rollback`
|
||||
|
||||
## Purpose
|
||||
|
||||
Phase-2 rollback for a single participant. Used both by the
|
||||
coordinator's abort path (vote_abort or prepare failure) and by the
|
||||
recovery runner (orphan with no `all-voted-commit` ledger row).
|
||||
|
||||
The function is idempotent at the protocol level: Postgres returns
|
||||
`42704` (`undefined_object`, "prepared transaction does not exist")
|
||||
for a gid that was already rolled back, and this module catches that
|
||||
specific error and treats it as a no-op. That property is what makes
|
||||
recovery safe to re-run on every boot.
|
||||
|
||||
## Features
|
||||
|
||||
- Emits `ROLLBACK PREPARED '<gid>'` against the participant's
|
||||
connection.
|
||||
- Idempotent against Postgres `42704` — a second rollback against the
|
||||
same gid is a no-op.
|
||||
- Skips participants that did not actually prepare (`outcome.prepared
|
||||
=== false`).
|
||||
- Emits a `2pc.rollback` span with `tx.id`, `participant.id`, and
|
||||
`gid` attributes.
|
||||
- Failure preserves the underlying cause on a thrown
|
||||
`TwoPcError(TWOPC_ROLLBACK_FAILED)`.
|
||||
- Exports `isUnknownPreparedTransaction(cause)` so the recovery
|
||||
runner can apply the same idempotency check without duplicating the
|
||||
SQLSTATE sniff.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { rollbackPhaseTwo } from "@suaveplan/api-2pc/phases";
|
||||
|
||||
await rollbackPhaseTwo({
|
||||
txId,
|
||||
participant,
|
||||
outcome: preparedOutcome,
|
||||
instruments,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Recovery uses the same idempotency check directly to short-circuit
|
||||
when the row was concurrently resolved:
|
||||
|
||||
```ts
|
||||
import {
|
||||
isUnknownPreparedTransaction,
|
||||
rollbackPhaseTwo,
|
||||
} from "@suaveplan/api-2pc/phases";
|
||||
|
||||
try {
|
||||
await rollbackPhaseTwo({ txId, participant, outcome, instruments });
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof TwoPcError &&
|
||||
err.twoPcCode === TWOPC_CODES.ROLLBACK_FAILED &&
|
||||
isUnknownPreparedTransaction(err.cause)
|
||||
) {
|
||||
// race: another process already rolled this back — treat as success
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface RollbackPhaseInput` — same fields as `CommitPhaseInput`.
|
||||
- `function rollbackPhaseTwo(input): Promise<void>`
|
||||
- no-op for `outcome.prepared === false`
|
||||
- throws `TwoPcError(TWOPC_ROLLBACK_FAILED)` on non-idempotency SQL
|
||||
failure.
|
||||
- `function isUnknownPreparedTransaction(cause: unknown): boolean`
|
||||
- matches SQLSTATE `42704` AND the substring `"does not exist"` as
|
||||
a fallback for drivers that omit the SQLSTATE.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The idempotency check sniffs both `cause.code` and `cause.message`
|
||||
because the two postgres clients in use (`postgres`/porsager and
|
||||
`pg.Client`) surface the SQLSTATE differently. Using both lets the
|
||||
module work against either driver without a wrapper layer.
|
||||
|
||||
The function is split into a small public `rollbackPhaseTwo` and an
|
||||
exported `isUnknownPreparedTransaction` helper so recovery can re-use
|
||||
the idempotency predicate without re-implementing it. Recovery sees
|
||||
this exact race regularly: the coordinator and the recovery runner
|
||||
both target the same prepared gid when a host restarts mid-rollback.
|
||||
133
docs/api-stack/api/api-2pc/src/recovery/recovery.md
Normal file
133
docs/api-stack/api/api-2pc/src/recovery/recovery.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# `recovery/`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Boot-time orphan resolution for the 2PC coordinator. When a host
|
||||
crashes between phase 1 and phase 2, prepared transactions remain
|
||||
held in `pg_prepared_xacts` on every writer participant. The recovery
|
||||
runner sweeps those rows on the next boot, consults the host-owned
|
||||
ledger, and either commits or rolls back each one — guaranteeing the
|
||||
"once prepared, eventually resolved" half of the 2PC durability
|
||||
property.
|
||||
|
||||
The runner is the only path that should ever resolve an in-doubt
|
||||
transaction. The coordinator's hot path NEVER auto-recovers; it
|
||||
escalates to `TWOPC_IN_DOUBT` and lets the next-boot recovery sweep
|
||||
handle it. That separation keeps the hot path's failure handling
|
||||
small and the recovery path testable in isolation.
|
||||
|
||||
## Features
|
||||
|
||||
- `resolveRecovery({ hostInstanceId, ledger, probes, onResolved })` —
|
||||
the entry point. Returns a `RecoveryReport` enumerating committed,
|
||||
rolled-back, skipped, and errored gids.
|
||||
- Idempotent — re-running the function on a host with no outstanding
|
||||
prepared transactions is a no-op (the inner `queryOrphans` returns
|
||||
an empty list).
|
||||
- Treats Postgres `42704` ("does not exist") as success during
|
||||
rollback — covers the race where another process already resolved
|
||||
the orphan.
|
||||
- Marks rows as `in_doubt` on unresolvable errors so the next sweep
|
||||
can retry; errored gids are surfaced on the report rather than
|
||||
thrown.
|
||||
- `buildProbeFromRegistry(registry)` — convenience adapter that
|
||||
spins up one probe per writer participant.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
buildProbeFromRegistry,
|
||||
resolveRecovery,
|
||||
} from "@suaveplan/api-2pc/recovery";
|
||||
|
||||
const report = await resolveRecovery({
|
||||
hostInstanceId: "host-01",
|
||||
ledger,
|
||||
probes: buildProbeFromRegistry(registry),
|
||||
});
|
||||
|
||||
console.log(
|
||||
`committed=${report.committed.length} ` +
|
||||
`rolledBack=${report.rolledBack.length} ` +
|
||||
`errors=${report.errors.length}`
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Wire a custom probe to feed an alternate identifier convention or a
|
||||
replicated-read connection — useful when the recovery sweep should
|
||||
hit a read replica to avoid contending with the live writer:
|
||||
|
||||
```ts
|
||||
import { gidPrefixFor } from "@suaveplan/api-2pc/phases";
|
||||
import { resolveRecovery, type RecoveryProbe } from "@suaveplan/api-2pc/recovery";
|
||||
|
||||
const probes: RecoveryProbe[] = [
|
||||
{
|
||||
participantId: "billing",
|
||||
async queryOrphans(prefix) {
|
||||
const rows = await readReplica.unsafe<{ gid: string }[]>(
|
||||
`SELECT gid FROM pg_prepared_xacts WHERE gid LIKE $1`,
|
||||
[`${prefix}%`]
|
||||
);
|
||||
return rows.map((r) => r.gid);
|
||||
},
|
||||
async commit(gid) {
|
||||
await primary.unsafe(`COMMIT PREPARED $1`, [gid]);
|
||||
},
|
||||
async rollback(gid) {
|
||||
await primary.unsafe(`ROLLBACK PREPARED $1`, [gid]);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const report = await resolveRecovery({
|
||||
hostInstanceId: "host-01",
|
||||
ledger,
|
||||
probes,
|
||||
onResolved: (e) => log.info(e),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface RecoveryProbe`
|
||||
- `participantId: string`
|
||||
- `queryOrphans(prefix: string): Promise<readonly string[]>`
|
||||
- `commit(gid: string): Promise<void>`
|
||||
- `rollback(gid: string): Promise<void>`
|
||||
- `interface RecoveryReport`
|
||||
- `committed: readonly string[]`
|
||||
- `rolledBack: readonly string[]`
|
||||
- `skipped: readonly string[]`
|
||||
- `errors: readonly { gid; error: TwoPcError }[]`
|
||||
- `interface RecoveryInput`
|
||||
- `hostInstanceId, ledger, probes, onResolved?`
|
||||
- `function resolveRecovery(input): Promise<RecoveryReport>`
|
||||
- `function buildProbeFromRegistry(registry): readonly RecoveryProbe[]`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The ledger lookup uses the gid's parsed `txId` as the primary key —
|
||||
the gid format from `phases/gid.ts` is what makes that mapping
|
||||
unambiguous. Rows whose ledger state is `committing` OR `committed`
|
||||
trigger `COMMIT PREPARED`; everything else (including missing rows)
|
||||
triggers `ROLLBACK PREPARED`. That asymmetric default ("rollback by
|
||||
default") is the safe choice — committing a row whose ledger does
|
||||
not explicitly say "all voted commit" would violate the design
|
||||
guarantee.
|
||||
|
||||
The `42704` swallow during rollback is what makes the runner safe to
|
||||
re-execute: if the previous sweep crashed AFTER rolling back the
|
||||
prepared transaction but BEFORE updating the ledger, the second
|
||||
sweep will see the orphan listed in `pg_prepared_xacts` (or not, if
|
||||
truly gone) and will not throw on the "does not exist" follow-up.
|
||||
Errors that cannot be classified as idempotency are captured per-gid
|
||||
and surfaced on the report — operators page-on `errors.length > 0`,
|
||||
not on a generic throw.
|
||||
111
docs/api-stack/api/api-2pc/src/registry/participant-registry.md
Normal file
111
docs/api-stack/api/api-2pc/src/registry/participant-registry.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# `registry/`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The participant registry is the install-time admission control between
|
||||
plugins and the 2PC coordinator. Plugins declare
|
||||
`twoPhaseCommit.participates: true` in their manifest; the host
|
||||
registers them with this registry; the registry refuses any writer
|
||||
whose bound database engine cannot honour `PREPARE TRANSACTION`.
|
||||
|
||||
This is where the design doc §9.2 rule "SQLite and SurrealDB MUST NOT
|
||||
participate as writers" is enforced. The capability check is injected
|
||||
as a callback so the registry stays decoupled from
|
||||
`@suaveplan/api-host`'s concrete `CapabilityRegistry`.
|
||||
|
||||
## Features
|
||||
|
||||
- `TwoPcParticipantRegistry.register(participant)` — refuses writers
|
||||
without `db.preparable`; accepts read-only voters regardless of
|
||||
engine.
|
||||
- `get(id)` / `list()` / `size()` / `unregister(id)` — standard
|
||||
registry surface.
|
||||
- Injection seam: `PreparableCapabilityCheck` is a plain
|
||||
`(id: string) => boolean` callback — zero coupling to api-host.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { TwoPcParticipantRegistry } from "@suaveplan/api-2pc/registry";
|
||||
|
||||
const isPreparable = (pluginId: string) =>
|
||||
pluginId === "billing" || pluginId === "inventory";
|
||||
|
||||
const registry = new TwoPcParticipantRegistry(isPreparable);
|
||||
registry.register({
|
||||
id: "billing",
|
||||
voterMode: "writer",
|
||||
connection: { engine: "postgres", run: pg.unsafe.bind(pg) },
|
||||
prepare: billingPrepareCallback,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Refusing a SQLite-backed plugin as a writer happens at register-time
|
||||
— wrap the registration in a try/catch to surface the failure to the
|
||||
plugin loader without crashing the host boot:
|
||||
|
||||
```ts
|
||||
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
|
||||
import { TwoPcParticipantRegistry } from "@suaveplan/api-2pc/registry";
|
||||
|
||||
function safeRegisterParticipants(
|
||||
registry: TwoPcParticipantRegistry,
|
||||
participants: ReadonlyArray<RegisteredTwoPcParticipant>
|
||||
) {
|
||||
const failures: { id: string; reason: string }[] = [];
|
||||
for (const p of participants) {
|
||||
try {
|
||||
registry.register(p);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof TwoPcError &&
|
||||
err.twoPcCode === TWOPC_CODES.PARTICIPANT_NOT_PREPARABLE
|
||||
) {
|
||||
failures.push({ id: p.id, reason: err.message });
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
```
|
||||
|
||||
The same plugin can then re-register as a `read-only` voter — the
|
||||
registry accepts read-only voters regardless of engine, because the
|
||||
coordinator never sends them prepare/commit SQL.
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type PreparableCapabilityCheck = (participantId: string) => boolean`
|
||||
— capability resolver injected at construction.
|
||||
- `class TwoPcParticipantRegistry`
|
||||
- `constructor(isPreparable: PreparableCapabilityCheck)`
|
||||
- `register(participant): void` — throws
|
||||
`TWOPC_PARTICIPANT_NOT_PREPARABLE` on writer refusal.
|
||||
- `get(id): RegisteredTwoPcParticipant | undefined`
|
||||
- `list(): readonly RegisteredTwoPcParticipant[]`
|
||||
- `size(): number`
|
||||
- `unregister(id): boolean`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry refuses a writer at `register`-time rather than at
|
||||
`run2pc`-time. This is deliberate — admission failures should surface
|
||||
during plugin install, not when the first 2PC transaction is attempted
|
||||
in production. The capability check fires only for writers; read-only
|
||||
voters are accepted unconditionally because the coordinator drops their
|
||||
connection from phase-2 SQL traffic. The second-half guard against
|
||||
read-only voters that try to write a payload lives in
|
||||
`phases/prepare.ts` (raising `TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE`
|
||||
when the participant returns `writeIntent: true`).
|
||||
|
||||
`unregister` exists for hot-reload paths: when a plugin is removed
|
||||
from the running host, the registry drops its participant entry so
|
||||
subsequent `run2pc` calls cannot accidentally route to it.
|
||||
100
docs/api-stack/api/api-2pc/src/testing/conformance.md
Normal file
100
docs/api-stack/api/api-2pc/src/testing/conformance.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# `testing/`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides `run2pcParticipantConformance(factory)` — a runnable test
|
||||
suite that pins the contract every 2PC writer participant MUST honour.
|
||||
Adapter authors drop a single import + invocation into a
|
||||
`tests/conformance/run-2pc-participant.test.ts` file in their package;
|
||||
the harness handles the rest.
|
||||
|
||||
Shipping the harness on this package's `./testing` subpath keeps
|
||||
participants accountable to the same prepare/commit/rollback semantics
|
||||
the coordinator depends on at runtime. Without this harness, drift
|
||||
between participant implementations would only surface during
|
||||
integration testing — too late.
|
||||
|
||||
## Features
|
||||
|
||||
- Six pinned invariants (engine identifier, voter mode, prepare-then-
|
||||
prepare-SQL, commit-prepared, rollback-prepared, idempotency-on-
|
||||
second-rollback).
|
||||
- Per-scenario factory: every test starts from a fresh participant +
|
||||
teardown so the suite is isolation-safe under parallel runs.
|
||||
- Uses `@suaveplan/testing/runner` — works under both Vitest and
|
||||
Bun:test without re-importing.
|
||||
- Generates unique gids per scenario so re-runs do not collide.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
// In an adapter package's tests/conformance/run-2pc-participant.test.ts
|
||||
import { run2pcParticipantConformance } from "@suaveplan/api-2pc/testing";
|
||||
import { createMyParticipant, teardownMyParticipant } from "../helpers";
|
||||
|
||||
run2pcParticipantConformance({
|
||||
async create() {
|
||||
const participant = await createMyParticipant();
|
||||
return {
|
||||
participant,
|
||||
teardown: () => teardownMyParticipant(participant),
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
The harness exposes no surface beyond `run2pcParticipantConformance` —
|
||||
deliberately. Adapter-specific assertions belong in adapter test
|
||||
files, not in the conformance harness. If your adapter needs to
|
||||
exercise behaviour the suite does not cover (e.g. cancellation,
|
||||
custom serialization), add a sibling test file:
|
||||
|
||||
```ts
|
||||
import { describe, it } from "@suaveplan/testing/runner";
|
||||
import { run2pcParticipantConformance } from "@suaveplan/api-2pc/testing";
|
||||
|
||||
describe("my-participant", () => {
|
||||
run2pcParticipantConformance({ create: createMyParticipant });
|
||||
|
||||
it("cancels a stuck prepare on signal abort", { timeout: 30_000 }, async () => {
|
||||
// adapter-specific assertion
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface TwoPcParticipantFactory`
|
||||
- `create(): Promise<{ participant: RegisteredTwoPcParticipant; teardown: () => Promise<void> }>`
|
||||
- `function run2pcParticipantConformance(factory): void`
|
||||
- Registers six `describe`/`it` blocks against the surrounding
|
||||
runner. Returns nothing.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The harness deliberately does NOT use a shared Postgres connection
|
||||
across scenarios. Each scenario calls `factory.create()` to get a
|
||||
fresh participant; the factory is expected to wire its own connection
|
||||
(directly to a Postgres container, against a shared pool, or against
|
||||
an `env-bridge` passthrough URL). This isolation prevents one
|
||||
scenario's leftover prepared transactions from contaminating the
|
||||
next.
|
||||
|
||||
Gids embed `Date.now()` + a random suffix so re-runs within the same
|
||||
millisecond never collide on the underlying `pg_prepared_xacts`
|
||||
table. The cleanup `ROLLBACK PREPARED` at the end of the
|
||||
prepare-only scenario is best-effort: failure to roll back leaves
|
||||
the gid prepared until the next teardown sweep, but the harness's
|
||||
own teardown handle is the primary cleanup path.
|
||||
|
||||
The harness is intentionally NOT exported from the package root (`./`
|
||||
subpath) — only `./testing`. This mirrors the convention used by
|
||||
sibling packages (`@suaveplan/i18n-shared/testing`,
|
||||
`@suaveplan/api-host/testing`) so production code can not accidentally
|
||||
depend on test-only symbols.
|
||||
108
docs/api-stack/api/api-2pc/src/types/types.types.md
Normal file
108
docs/api-stack/api/api-2pc/src/types/types.types.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# `types/`
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared cross-module type definitions for the 2PC coordinator. Holds
|
||||
the `TwoPcCoordinatorState` state-machine alphabet, the
|
||||
`PreparableConnection` and `RegisteredTwoPcParticipant` participant
|
||||
shapes, the `Run2PcInput` / `Run2PcOutcome` public input/output
|
||||
contracts, and the `TwoPcPrepareResult` per-participant prepare
|
||||
return type.
|
||||
|
||||
Centralising these types here prevents the cyclic-import smell that
|
||||
would otherwise emerge from `coordinator/`, `phases/`, `recovery/`,
|
||||
and `registry/` all needing to know the same wire shapes. The
|
||||
sibling modules import from this barrel and stay decoupled from each
|
||||
other at the type level.
|
||||
|
||||
## Features
|
||||
|
||||
- `TwoPcCoordinatorState` — string-literal union covering
|
||||
`voting`, `committing`, `aborting`, `committed`, `aborted`,
|
||||
`in_doubt`.
|
||||
- `PreparableConnection` — structural connection shape exposing
|
||||
`engine` (a `DatabaseEngine` from `@suaveplan/api-types/db`) and
|
||||
`run(sql, params?)` returning a `Promise<R>`.
|
||||
- `RegisteredTwoPcParticipant` — coordinator-side participant view
|
||||
carrying `id`, `voterMode`, `connection`, and an async
|
||||
`prepare(txId, payload) => TwoPcPrepareResult` callback.
|
||||
- `TwoPcPrepareResult` — `{ vote, writeIntent }` returned by a
|
||||
participant's `prepare` callback.
|
||||
- `Run2PcInput` / `Run2PcOutcome` — public input/output contracts
|
||||
accepted and returned by `run2pc`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import type {
|
||||
Run2PcInput,
|
||||
RegisteredTwoPcParticipant,
|
||||
} from "@suaveplan/api-2pc";
|
||||
|
||||
const input: Run2PcInput = {
|
||||
participants: [{ id: "billing", input: { invoiceId: "inv-1" } }],
|
||||
timeoutMs: 5000,
|
||||
};
|
||||
|
||||
const billing: RegisteredTwoPcParticipant = {
|
||||
id: "billing",
|
||||
voterMode: "writer",
|
||||
connection: { engine: "postgres", run: pg.unsafe.bind(pg) },
|
||||
prepare: async (_txId) => ({ vote: "commit", writeIntent: true }),
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Read-only voter typing — combine `voterMode: "read-only"` with a
|
||||
`writeIntent: false` invariant on the `prepare` return shape:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
RegisteredTwoPcParticipant,
|
||||
TwoPcPrepareResult,
|
||||
} from "@suaveplan/api-2pc";
|
||||
|
||||
async function auditPrepare(): Promise<TwoPcPrepareResult> {
|
||||
const ok = await checkConsistency();
|
||||
return { vote: ok ? "commit" : "abort", writeIntent: false };
|
||||
}
|
||||
|
||||
const audit: RegisteredTwoPcParticipant = {
|
||||
id: "audit",
|
||||
voterMode: "read-only",
|
||||
connection: { engine: "surrealdb", run: surrealUnsafe },
|
||||
prepare: () => auditPrepare(),
|
||||
};
|
||||
```
|
||||
|
||||
The coordinator refuses a read-only voter whose `prepare` returns
|
||||
`writeIntent: true` with `TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE`
|
||||
— the static type doesn't enforce that pairing, but the runtime
|
||||
boundary does.
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type TwoPcCoordinatorState`
|
||||
- `interface PreparableConnection`
|
||||
- `interface RegisteredTwoPcParticipant`
|
||||
- `interface TwoPcPrepareResult`
|
||||
- `interface Run2PcInput`
|
||||
- `interface Run2PcOutcome`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Every interface is `readonly` on every field. This is deliberate —
|
||||
the coordinator caches inputs (participant lookups, ledger entries)
|
||||
and a mutation by accident would corrupt in-flight state. The
|
||||
strictness is enforced at the type level via `readonly` plus
|
||||
`ReadonlyArray<...>` and at the runtime level by the boundary Zod
|
||||
schemas in `options/` (for caller-facing inputs).
|
||||
|
||||
The `Run2PcOutcome.state` field uses `Extract<TwoPcCoordinatorState,
|
||||
"committed" | "aborted" | "in_doubt">` so callers do not have to
|
||||
exhaustively switch over the in-flight states (`voting`,
|
||||
`committing`, `aborting`) that never appear on a returned outcome.
|
||||
The structural typing of `PreparableConnection` keeps the package
|
||||
independent of `pg` and `postgres` (porsager) — both clients can be
|
||||
adapted at the seam with a one-line wrapper.
|
||||
85
docs/api-stack/api/api-audit-file/README.md
Normal file
85
docs/api-stack/api/api-audit-file/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# @suaveplan/api-audit-file — audit sink writing JSON-per-line, HMAC-SHA-256 signed records to a rotated file. Size + time rotation, maxFiles retention, bounded-queue backpressure with observable drops, append + strict-sync write modes. Implements `RequestAuditSink` from `@suaveplan/api-host/request-pipeline`.
|
||||
|
||||
File-rotating, HMAC-signed audit sink for `@suaveplan/api-host`.
|
||||
Implements `RequestAuditSink` from
|
||||
`@suaveplan/api-host/request-pipeline`: writes one JSON line per
|
||||
record with an HMAC-SHA-256 signature, rolls the file on a size OR age
|
||||
trigger, and prunes the oldest rotated siblings to a configurable
|
||||
`maxFiles` retention cap.
|
||||
|
||||
Suitable for single-host deployments, on-prem environments without a
|
||||
log shipper, and any host that prefers durable local storage of the
|
||||
audit trail. The sibling `@suaveplan/api-audit-stdout` covers the
|
||||
log-shipped use case; both sinks share the wire shape so dashboards
|
||||
union on metric name and partition by the `sink` attribute.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Frozen registry of the six stable `AUDIT_FILE.*` error codes raised by the file-rotating audit sink, plus the concrete…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Frozen registry of the six stable `AUDIT_FILE.*` error codes raised by the file-rotating audit sink, plus the concrete…
|
||||
- [fs-helpers](./src/fs-helpers/fs-helpers.md) — Isolates the `node:fs` surface the sink touches behind an injectable `FsAdapter` interface and concrete `nodeFsAdapter`…
|
||||
- [fs-helpers](./src/fs-helpers/fs-helpers.md) — Isolates the `node:fs` surface the sink touches behind an injectable `FsAdapter` interface and concrete `nodeFsAdapter`…
|
||||
- [hmac](./src/hmac/hmac.md) — Wraps `@suaveplan/crypto-hash` HMAC-SHA-256 into the two operations the file sink needs: signing a canonical record on…
|
||||
- [hmac](./src/hmac/hmac.md) — Wraps `@suaveplan/crypto-hash` HMAC-SHA-256 into the two operations the file sink needs: signing a canonical record on…
|
||||
- [instruments](./src/instruments/instruments.md) — Constructs the OTel counter bundle the file sink emits and exposes the canonical `sink="file"` attribute set. Every obs…
|
||||
- [instruments](./src/instruments/instruments.md) — Constructs the OTel counter bundle the file sink emits and exposes the canonical `sink="file"` attribute set. Every obs…
|
||||
- [retention](./src/retention/retention.md) — Enforces the `maxFiles` retention dimension of the rotation policy by deleting the oldest rotated files until at most `…
|
||||
- [retention](./src/retention/retention.md) — Enforces the `maxFiles` retention dimension of the rotation policy by deleting the oldest rotated files until at most `…
|
||||
- [rotation](./src/rotation/rotation.md) — Resolves the rotation policy carried by every `createFileAuditSink` instance and implements the rotation primitive itse…
|
||||
- [rotation](./src/rotation/rotation.md) — Resolves the rotation policy carried by every `createFileAuditSink` instance and implements the rotation primitive itse…
|
||||
- [schema](./src/schema/schema.md) — Defines the Zod schema and canonical-bytes function that bracket every audit record before it is signed and written. Th…
|
||||
- [schema](./src/schema/schema.md) — Defines the Zod schema and canonical-bytes function that bracket every audit record before it is signed and written. Th…
|
||||
- [sink](./src/sink/sink.md) — The public entry point of `@suaveplan/api-audit-file`. The `createFileAuditSink` factory composes the schema, hmac, rot…
|
||||
- [sink](./src/sink/sink.md) — The public entry point of `@suaveplan/api-audit-file`. The `createFileAuditSink` factory composes the schema, hmac, rot…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/api-audit-file
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
```ts
|
||||
import { createFileAuditSink } from "@suaveplan/api-audit-file/sink";
|
||||
|
||||
const sink = await createFileAuditSink({
|
||||
path: "/var/log/suaveplan/audit.log",
|
||||
hmacKey: new TextEncoder().encode(process.env.AUDIT_HMAC_KEY ?? ""),
|
||||
rotation: { maxSizeBytes: 100 * 1024 * 1024, maxAgeMs: 86_400_000, maxFiles: 30 },
|
||||
appendMode: "strict-sync",
|
||||
});
|
||||
|
||||
await sink.emit(envelopeFromHostPipeline);
|
||||
await sink.dispose();
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- `./sink` — `createFileAuditSink`, `FileAuditSink`, `CreateFileAuditSinkOptions`, `AppendMode`.
|
||||
- `./rotation` — `RotationPolicy`, `RotationState`, `resolveRotationPolicy`, `shouldRotate`, `rotate`.
|
||||
- `./retention` — `enforceRetention`.
|
||||
- `./schema` — `AuditRecord`, `AuditRecordSchema`, `canonicalise`, `buildSignedLine`.
|
||||
- `./hmac` — `signRecord`, `verifyRecord`.
|
||||
- `./instruments` — `AuditFileInstruments`, `createAuditFileInstruments`, `SINK_ATTRIBUTE`.
|
||||
- `./fs-helpers` — `FsAdapter`, `nodeFsAdapter`, `listRotatedFiles`, `rotatedFilePathAt`.
|
||||
- `./error-codes` — `AUDIT_FILE_CODES`, `AuditFileError`, `AuditFileCode`.
|
||||
|
||||
## Error codes
|
||||
|
||||
`AUDIT_FILE.INVALID_RECORD`, `AUDIT_FILE.WRITER_FAILED`,
|
||||
`AUDIT_FILE.QUEUE_FULL`, `AUDIT_FILE.ROTATION_FAILED`,
|
||||
`AUDIT_FILE.RETENTION_FAILED`, `AUDIT_FILE.PATH_NOT_WRITEABLE`.
|
||||
|
||||
## OTel instruments
|
||||
|
||||
- `api.audit.records_total{sink="file"}`
|
||||
- `api.audit.dropped_total{sink="file"}`
|
||||
- `api.audit.rotation_evicted_total{sink="file"}`
|
||||
|
||||
## Tier
|
||||
|
||||
Server-tier — depends on `node:fs/promises`.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# `error-codes`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Frozen registry of the six stable `AUDIT_FILE.*` error codes raised by
|
||||
the file-rotating audit sink, plus the concrete
|
||||
`AuditFileError extends SuaveplanError` subclass that carries them.
|
||||
Operators rely on the codes for dashboard slicing; package consumers
|
||||
rely on the typed subclass to differentiate sink failures from generic
|
||||
host errors without parsing message strings.
|
||||
|
||||
The registry is built with `defineCodes("AUDIT_FILE", [...])` from
|
||||
`@suaveplan/error`, so every code is recorded in the global registry
|
||||
exactly once at module load. The OpenSpec conformance gate verifies
|
||||
every entry here is also raised by a production code path elsewhere in
|
||||
`src/` — no dead codes are permitted.
|
||||
|
||||
## Features
|
||||
|
||||
- Six stable codes: `INVALID_RECORD`, `WRITER_FAILED`, `QUEUE_FULL`,
|
||||
`ROTATION_FAILED`, `RETENTION_FAILED`, `PATH_NOT_WRITEABLE`.
|
||||
- Typed union `AuditFileCode` for narrowing on `error.code`.
|
||||
- `AuditFileError` subclass with stable `error.name === "AuditFileError"`.
|
||||
- Forwards `cause`, `context`, `redacted`, and other
|
||||
`SuaveplanErrorOptions` to the base constructor unchanged.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUDIT_FILE_CODES,
|
||||
AuditFileError,
|
||||
} from "@suaveplan/api-audit-file/error-codes";
|
||||
|
||||
throw new AuditFileError(
|
||||
AUDIT_FILE_CODES.PATH_NOT_WRITEABLE,
|
||||
"error.audit.file.path_not_writeable",
|
||||
{ context: { path: "/var/log/suaveplan/audit.log" } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Narrow on the union after catching to drive code-specific recovery:
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUDIT_FILE_CODES,
|
||||
type AuditFileCode,
|
||||
AuditFileError,
|
||||
} from "@suaveplan/api-audit-file/error-codes";
|
||||
|
||||
function recover(err: unknown): "retry" | "drop" {
|
||||
if (!(err instanceof AuditFileError)) throw err;
|
||||
const code: AuditFileCode = err.code;
|
||||
if (code === AUDIT_FILE_CODES.QUEUE_FULL) return "drop";
|
||||
if (code === AUDIT_FILE_CODES.WRITER_FAILED) return "retry";
|
||||
if (code === AUDIT_FILE_CODES.ROTATION_FAILED) return "retry";
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AUDIT_FILE_CODES: Readonly<Record<Name, "AUDIT_FILE.<Name>">>`
|
||||
- `type AuditFileCode = (typeof AUDIT_FILE_CODES)[keyof typeof AUDIT_FILE_CODES]`
|
||||
- `AUDIT_FILE_ERROR_NAME = "AuditFileError"`
|
||||
- `class AuditFileError extends SuaveplanError<AuditFileCode>`
|
||||
- `new AuditFileError(code, messageKey, options?)`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The codes are declared `as const` so TypeScript freezes both the keys
|
||||
and the fully-qualified string values. The `name` literal is exported
|
||||
separately because some structured loggers group errors on `name`
|
||||
before they inspect `code` — pinning a constant keeps that grouping
|
||||
stable across releases. The module is side-effect-imported from
|
||||
`src/index.ts` so a consumer who only loads a subpath
|
||||
(`@suaveplan/api-audit-file/sink`) still triggers code registration.
|
||||
@@ -0,0 +1,86 @@
|
||||
# `fs-helpers`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Isolates the `node:fs` surface the sink touches behind an injectable
|
||||
`FsAdapter` interface and concrete `nodeFsAdapter` implementation. The
|
||||
indirection lets tests run the sink against a real filesystem under a
|
||||
temp directory (the default for this package's test suite) while
|
||||
keeping a clean seam for in-memory adapters used by external consumers.
|
||||
|
||||
The module also exports the rotated-file enumeration helper used by
|
||||
the retention module — keeping the file-name conventions next to the
|
||||
filesystem primitives means rotation, retention, and sink all share
|
||||
the same view of "what counts as a rotated sibling of this base path".
|
||||
|
||||
## Features
|
||||
|
||||
- `FsAdapter` — six-method surface: `appendLine`, `fileSize`, `rename`,
|
||||
`unlink`, `readdir`, `ensureWriteableDir`.
|
||||
- `nodeFsAdapter` — production adapter against `node:fs/promises`.
|
||||
- `listRotatedFiles(fs, basePath)` — returns full paths of every
|
||||
rotated sibling, sorted oldest-first by embedded timestamp.
|
||||
- `rotatedFilePathAt(basePath, iso)` — builds the rotated path for the
|
||||
supplied ISO timestamp; `:` is replaced with `-` so the name is
|
||||
Windows-safe and grep-friendly.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { nodeFsAdapter } from "@suaveplan/api-audit-file/fs-helpers";
|
||||
|
||||
await nodeFsAdapter.ensureWriteableDir("/var/log/suaveplan");
|
||||
await nodeFsAdapter.appendLine("/var/log/suaveplan/audit.log", "...\n", false);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Provide an injectable adapter to drive the sink against an in-memory
|
||||
filesystem:
|
||||
|
||||
```ts
|
||||
import type { FsAdapter } from "@suaveplan/api-audit-file/fs-helpers";
|
||||
|
||||
const inMemory = new Map<string, string>();
|
||||
const adapter: FsAdapter = {
|
||||
async appendLine(p, line) {
|
||||
inMemory.set(p, (inMemory.get(p) ?? "") + line);
|
||||
},
|
||||
async fileSize(p) {
|
||||
return Buffer.byteLength(inMemory.get(p) ?? "", "utf8");
|
||||
},
|
||||
async rename(from, to) {
|
||||
inMemory.set(to, inMemory.get(from) ?? "");
|
||||
inMemory.delete(from);
|
||||
},
|
||||
async unlink(p) {
|
||||
inMemory.delete(p);
|
||||
},
|
||||
async readdir() {
|
||||
return [...inMemory.keys()];
|
||||
},
|
||||
async ensureWriteableDir() {
|
||||
/* always writeable */
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface FsAdapter` — see Features above.
|
||||
- `const nodeFsAdapter: FsAdapter`
|
||||
- `listRotatedFiles(fs: FsAdapter, basePath: string): Promise<string[]>`
|
||||
- `rotatedFilePathAt(basePath: string, isoTimestamp: string): string`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`ensureWriteableDir` calls `mkdir(..., { recursive: true })` then a
|
||||
`W_OK` `access` probe so a fresh boot creates the directory and a
|
||||
borked permission setup throws `PATH_NOT_WRITEABLE` synchronously
|
||||
during sink construction. `fileSize` swallows `ENOENT` and returns `0`
|
||||
so the rotation logic naturally treats a not-yet-created file as
|
||||
empty — that simplifies the open-on-first-write code path.
|
||||
76
docs/api-stack/api/api-audit-file/src/hmac/hmac.md
Normal file
76
docs/api-stack/api/api-audit-file/src/hmac/hmac.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# `hmac`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Wraps `@suaveplan/crypto-hash` HMAC-SHA-256 into the two operations the
|
||||
file sink needs: signing a canonical record on write and verifying a
|
||||
record + signature on read. The wrapper hex-encodes the digest so the
|
||||
on-disk line stays human-greppable and ASCII-clean, and exposes a
|
||||
constant-time verifier so tampering checks do not leak position
|
||||
information through timing.
|
||||
|
||||
## Features
|
||||
|
||||
- `signRecord(key, bytes): string` — 64-char lowercase hex of
|
||||
HMAC-SHA-256 over `bytes` using `key`.
|
||||
- `verifyRecord(key, bytes, hmacHex): boolean` — constant-time
|
||||
comparison of recomputed HMAC against the provided hex.
|
||||
- Empty-key guard raises a typed `Error` so misconfiguration cannot
|
||||
silently produce a deterministic null-MAC.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { signRecord, verifyRecord } from "@suaveplan/api-audit-file/hmac";
|
||||
|
||||
const key = new TextEncoder().encode("super-secret-rotating-key");
|
||||
const bytes = new TextEncoder().encode('{"requestId":"r-1"}');
|
||||
const hmac = signRecord(key, bytes);
|
||||
verifyRecord(key, bytes, hmac); // → true
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Use with the canonical-bytes helper to verify an on-disk line:
|
||||
|
||||
```ts
|
||||
import {
|
||||
AuditRecordSchema,
|
||||
canonicalise,
|
||||
} from "@suaveplan/api-audit-file/schema";
|
||||
import { verifyRecord } from "@suaveplan/api-audit-file/hmac";
|
||||
|
||||
for await (const line of lines) {
|
||||
const { record, hmac } = JSON.parse(line) as {
|
||||
record: unknown;
|
||||
hmac: string;
|
||||
};
|
||||
const parsed = AuditRecordSchema.parse(record);
|
||||
const ok = verifyRecord(key, canonicalise(parsed), hmac);
|
||||
if (!ok) throw new Error("hmac mismatch");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `signRecord(key: Uint8Array, bytes: Uint8Array): string`
|
||||
- `verifyRecord(key: Uint8Array, bytes: Uint8Array, hmacHex: string): boolean`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The hex encoder is hand-rolled rather than borrowed from
|
||||
`@suaveplan/crypto-utils` so this module stays a leaf of the dependency
|
||||
graph — the sink runs on every audit-emitting request, and keeping the
|
||||
hot path's transitive deps minimal helps cold-start latency on
|
||||
serverless deploys. Constant-time comparison XORs each character code
|
||||
and accumulates into a bitmask; the boolean conversion at the end keeps
|
||||
the early-return branch (length mismatch) bounded to attacker-visible
|
||||
metadata only. The 64-character length check up front is intentionally
|
||||
outside the constant-time region because the length of the supplied
|
||||
HMAC is not secret — an attacker who supplies a malformed hex string
|
||||
sees an immediate rejection but learns nothing about the recomputed
|
||||
digest.
|
||||
@@ -0,0 +1,78 @@
|
||||
# `instruments`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Constructs the OTel counter bundle the file sink emits and exposes the
|
||||
canonical `sink="file"` attribute set. Every observation carries the
|
||||
attribute so a dashboard tracking `api.audit.records_total` across the
|
||||
sink family — `stdout`, `file`, and future siblings — can union the
|
||||
metric name and partition by `sink`. The three counters answer the
|
||||
operator questions "are records flowing?", "are records dropping?",
|
||||
and "is retention pruning old files?".
|
||||
|
||||
## Features
|
||||
|
||||
- `createAuditFileInstruments({ meterProvider? })` — returns a frozen
|
||||
bundle of three counters bound to the supplied or global
|
||||
`MeterProvider`.
|
||||
- `SINK_ATTRIBUTE` — frozen `{ sink: "file" }` literal exported for
|
||||
reuse by the sink, retention, and test harnesses.
|
||||
- Counters: `api.audit.records_total`, `api.audit.dropped_total`,
|
||||
`api.audit.rotation_evicted_total`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createAuditFileInstruments } from "@suaveplan/api-audit-file/instruments";
|
||||
|
||||
const instruments = createAuditFileInstruments();
|
||||
instruments.recordsTotal.add(1, { sink: "file" });
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Install a real `MeterProvider` and assert deltas in tests:
|
||||
|
||||
```ts
|
||||
import {
|
||||
MeterProvider,
|
||||
InMemoryMetricExporter,
|
||||
PeriodicExportingMetricReader,
|
||||
} from "@opentelemetry/sdk-metrics";
|
||||
import { createAuditFileInstruments } from "@suaveplan/api-audit-file/instruments";
|
||||
|
||||
const exporter = new InMemoryMetricExporter(0);
|
||||
const reader = new PeriodicExportingMetricReader({ exporter });
|
||||
const provider = new MeterProvider({ readers: [reader] });
|
||||
|
||||
const instruments = createAuditFileInstruments({ meterProvider: provider });
|
||||
instruments.recordsTotal.add(1, { sink: "file" });
|
||||
|
||||
await reader.collect();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface AuditFileInstruments`
|
||||
- `meter: Meter`
|
||||
- `recordsTotal: Counter`
|
||||
- `droppedTotal: Counter`
|
||||
- `rotationEvictedTotal: Counter`
|
||||
- `interface AuditFileInstrumentsConfig`
|
||||
- `meterProvider?: MeterProvider`
|
||||
- `createAuditFileInstruments(config?): AuditFileInstruments`
|
||||
- `SINK_ATTRIBUTE: Readonly<{ sink: "file" }>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The bundle is intentionally NOT module-level cached — every sink
|
||||
instance gets its own bundle so an operator who reconstructs the host
|
||||
under a fresh `MeterProvider` (a test-host pattern in
|
||||
`@suaveplan/api-host/testing`) sees deltas against the right provider.
|
||||
Counters from the sibling `api-audit-stdout` sink share the metric
|
||||
NAMES with this package so a single dashboard panel covers the family;
|
||||
the `sink="file"` attribute distinguishes the source.
|
||||
67
docs/api-stack/api/api-audit-file/src/retention/retention.md
Normal file
67
docs/api-stack/api/api-audit-file/src/retention/retention.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# `retention`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Enforces the `maxFiles` retention dimension of the rotation policy by
|
||||
deleting the oldest rotated files until at most `maxFiles` remain.
|
||||
Retention runs immediately after each rotation succeeds so the disk
|
||||
footprint is bounded across long-running deployments — an audit
|
||||
directory left to grow unchecked is the most common operator pain
|
||||
point with file-based sinks, and this module's existence is exactly
|
||||
the answer to it.
|
||||
|
||||
The increment on `api.audit.rotation_evicted_total{sink="file"}` makes
|
||||
eviction observable. Operators alert on an unexpectedly high rate so a
|
||||
runaway rotation cycle (e.g. clock skew, misconfigured `maxAgeMs`) is
|
||||
visible before the retention sweep drops historical data.
|
||||
|
||||
## Features
|
||||
|
||||
- `enforceRetention(fs, basePath, maxFiles, instruments)` — deletes
|
||||
the oldest rotated files until at most `maxFiles` remain. Idempotent:
|
||||
re-running with no surplus is a no-op.
|
||||
- Counter increment per deletion with the canonical `sink="file"`
|
||||
attribute set.
|
||||
- Surfaces `AUDIT_FILE.RETENTION_FAILED` on a delete failure with the
|
||||
offending path in `context.path` and the underlying `cause`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { enforceRetention } from "@suaveplan/api-audit-file/retention";
|
||||
|
||||
await enforceRetention(fs, "/var/log/audit.log", 30, instruments);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Call from a custom rotation cadence — e.g. an external cron that wants
|
||||
to prune without rotating:
|
||||
|
||||
```ts
|
||||
import { enforceRetention } from "@suaveplan/api-audit-file/retention";
|
||||
|
||||
// Cron job: every hour, prune to a smaller retention than the sink's
|
||||
// default to claw back disk during high-traffic windows.
|
||||
setInterval(async () => {
|
||||
await enforceRetention(fs, basePath, 10, instruments);
|
||||
}, 60 * 60 * 1000);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `enforceRetention(fs: FsAdapter, basePath: string, maxFiles: number, instruments: AuditFileInstruments): Promise<void>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`listRotatedFiles` returns paths sorted oldest-first by the embedded
|
||||
ISO timestamp; we slice off the first `(count - maxFiles)` entries and
|
||||
unlink them. Sorting is lexicographic on the ISO string, which is also
|
||||
chronological for the `YYYY-MM-DDTHH-MM-SS.sssZ` shape — no extra
|
||||
parsing required. If `unlink` fails midway, the previously-evicted
|
||||
deletions are not undone; the throw surfaces the failure and the next
|
||||
rotation re-attempts retention against whatever survived.
|
||||
95
docs/api-stack/api/api-audit-file/src/rotation/rotation.md
Normal file
95
docs/api-stack/api/api-audit-file/src/rotation/rotation.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# `rotation`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Resolves the rotation policy carried by every `createFileAuditSink`
|
||||
instance and implements the rotation primitive itself — the
|
||||
`rename(currentFile → currentFile.<utc-iso>.log)` step plus the
|
||||
post-rotation state reset. The module owns both dimensions documented
|
||||
in the spec: size-based (`maxSizeBytes`, default 100 MiB) and
|
||||
time-based (`maxAgeMs`, default 24h). Either condition reaching its
|
||||
limit triggers a rotation on the next write.
|
||||
|
||||
Rotation lives in a separate module so the sink stays focused on
|
||||
queueing + writing. The retention module reads the policy through the
|
||||
same `RotationState` so the operator-facing knobs are documented in
|
||||
exactly one place.
|
||||
|
||||
## Features
|
||||
|
||||
- `resolveRotationPolicy(input?)` — Zod-equivalent validator returning
|
||||
a frozen `RotationPolicy`. Defaults filled from the design doc.
|
||||
- `shouldRotate(state, nowMs)` — pure predicate; size OR age satisfies.
|
||||
- `rotate(fs, basePath, state, clock)` — performs the rename, updates
|
||||
state, returns the rotated path for retention to inspect.
|
||||
- Raises `AUDIT_FILE.ROTATION_FAILED` on rename failure with full
|
||||
`cause` for forensic logging.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
resolveRotationPolicy,
|
||||
rotate,
|
||||
shouldRotate,
|
||||
} from "@suaveplan/api-audit-file/rotation";
|
||||
|
||||
const policy = resolveRotationPolicy({ maxSizeBytes: 1024 * 1024 });
|
||||
const state = { openedAtMs: Date.now(), bytesWritten: 0, policy };
|
||||
if (shouldRotate(state, Date.now())) {
|
||||
await rotate(fs, "/var/log/audit.log", state, () => Date.now());
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Disable a dimension by setting it to `Number.POSITIVE_INFINITY` —
|
||||
useful when an operator wants pure time rotation regardless of size:
|
||||
|
||||
```ts
|
||||
const policy = resolveRotationPolicy({
|
||||
maxSizeBytes: Number.POSITIVE_INFINITY,
|
||||
maxAgeMs: 60 * 60 * 1000, // hourly
|
||||
});
|
||||
```
|
||||
|
||||
In tests, drive rotation deterministically with a `FakeClock` from
|
||||
`@suaveplan/testing`:
|
||||
|
||||
```ts
|
||||
import { FakeClock } from "@suaveplan/testing";
|
||||
|
||||
const clock = new FakeClock(0);
|
||||
const state = { openedAtMs: 0, bytesWritten: 0, policy };
|
||||
clock.advance(61_000);
|
||||
await rotate(fs, basePath, state, clock.read);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface RotationPolicy`
|
||||
- `maxSizeBytes: number` — bytes before size rotation; default 100 MiB.
|
||||
- `maxAgeMs: number` — ms before time rotation; default 24h.
|
||||
- `maxFiles: number` — kept rotated files; default 30.
|
||||
- `interface RotationState`
|
||||
- `openedAtMs: number` — wall-clock open time of current segment.
|
||||
- `bytesWritten: number` — running byte count.
|
||||
- `policy: RotationPolicy` — frozen at construction.
|
||||
- `resolveRotationPolicy(input?): RotationPolicy`
|
||||
- `shouldRotate(state, nowMs): boolean`
|
||||
- `rotate(fs, basePath, state, clock): Promise<string>` — returns the
|
||||
rotated file path.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The rotated suffix replaces `:` with `-` so the resulting filename is
|
||||
Windows-portable and grep-friendly across shells. State mutation
|
||||
happens AFTER the rename succeeds — a failed rename leaves the original
|
||||
state intact, so the next call retries against the same segment rather
|
||||
than corrupting accounting. `bytesWritten` resets to 0 instead of
|
||||
re-stat'ing the new empty file; the next successful append updates it
|
||||
in lock-step with the on-disk length.
|
||||
79
docs/api-stack/api/api-audit-file/src/schema/schema.md
Normal file
79
docs/api-stack/api/api-audit-file/src/schema/schema.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# `schema`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the Zod schema and canonical-bytes function that bracket every
|
||||
audit record before it is signed and written. The schema mirrors the
|
||||
`RequestAuditEnvelope` interface exported by
|
||||
`@suaveplan/api-host/request-pipeline`; the canonical-bytes function
|
||||
gives every other module a deterministic input for HMAC signing,
|
||||
file-line construction, and signature verification.
|
||||
|
||||
The package re-validates at the sink boundary even though the host
|
||||
constructs envelopes from typed data — operators frequently hand-craft
|
||||
audit records in tests, replay tooling, or migration scripts. The
|
||||
boundary check makes corrupt records fail loudly with a typed
|
||||
`AUDIT_FILE.INVALID_RECORD` instead of silently bypassing the HMAC.
|
||||
|
||||
## Features
|
||||
|
||||
- `AuditRecordSchema` — strict Zod object accepting only the
|
||||
envelope's documented fields. Unknown keys reject.
|
||||
- `AuditRecord` — `z.infer<typeof AuditRecordSchema>`; structurally
|
||||
assignable to `RequestAuditEnvelope`.
|
||||
- `canonicalise(record)` — UTF-8 bytes of the record JSON-stringified
|
||||
with sorted top-level keys.
|
||||
- `buildSignedLine(bytes, hmacHex)` — emits the on-disk JSON line
|
||||
carrying the record + HMAC, with a trailing `\n`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
AuditRecordSchema,
|
||||
buildSignedLine,
|
||||
canonicalise,
|
||||
} from "@suaveplan/api-audit-file/schema";
|
||||
|
||||
const record = AuditRecordSchema.parse(rawEnvelope);
|
||||
const bytes = canonicalise(record);
|
||||
const line = buildSignedLine(bytes, "deadbeef...");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Verify an on-disk line:
|
||||
|
||||
```ts
|
||||
import { hmac } from "@suaveplan/crypto-hash";
|
||||
import { AuditRecordSchema, canonicalise } from "@suaveplan/api-audit-file/schema";
|
||||
|
||||
const { record, hmac: hmacHex } = JSON.parse(lineFromDisk);
|
||||
const parsed = AuditRecordSchema.parse(record);
|
||||
const bytes = canonicalise(parsed);
|
||||
const expected = Buffer.from(hmac("SHA-256", key, bytes)).toString("hex");
|
||||
if (expected !== hmacHex) {
|
||||
throw new Error("hmac mismatch — record was tampered");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AuditRecordSchema: z.ZodObject` (strict)
|
||||
- `type AuditRecord = z.infer<typeof AuditRecordSchema>`
|
||||
- `canonicalise(record: AuditRecord): Uint8Array`
|
||||
- `buildSignedLine(canonicalBytes: Uint8Array, hmacHex: string): string`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The canonical bytes drop `undefined` values for the optional `cause`
|
||||
slot so the absence of an error never affects the signature. Top-level
|
||||
keys are sorted alphabetically to give every implementation (TypeScript,
|
||||
Go log-reader, Python forensic tooling) the same input bytes for
|
||||
verification regardless of construction order. `principal.scopes` is
|
||||
preserved in array order — the order is semantically meaningful in the
|
||||
pipeline so we do not collapse it.
|
||||
128
docs/api-stack/api/api-audit-file/src/sink/sink.md
Normal file
128
docs/api-stack/api/api-audit-file/src/sink/sink.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# `sink`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The public entry point of `@suaveplan/api-audit-file`. The
|
||||
`createFileAuditSink` factory composes the schema, hmac, rotation,
|
||||
retention, instruments, and fs-helpers modules into a single object
|
||||
that implements the `RequestAuditSink` contract from
|
||||
`@suaveplan/api-host/request-pipeline`. The sink writes each accepted
|
||||
record as one JSON line with an HMAC-SHA-256 signature, rolls the file
|
||||
when size OR age limits are reached, and keeps at most `maxFiles`
|
||||
rotated siblings.
|
||||
|
||||
The sink is queue-buffered: callers `await emit()` and resolve as soon
|
||||
as the record has been appended to disk. A bounded queue protects the
|
||||
host from a stalled filesystem: records arriving while the queue is at
|
||||
`maxQueueSize` are dropped with a typed `AUDIT_FILE.QUEUE_FULL` and an
|
||||
`api.audit.dropped_total{sink="file"}` increment. `dispose()` flushes
|
||||
the queue and is idempotent.
|
||||
|
||||
## Features
|
||||
|
||||
- `createFileAuditSink({ path, hmacKey, ... })` — async factory.
|
||||
Returns a sink ready to `emit()`.
|
||||
- Schema validation at the boundary: invalid records throw
|
||||
`AUDIT_FILE.INVALID_RECORD` and increment `dropped_total`.
|
||||
- `strict-sync` (default) and `append` modes for the durability /
|
||||
throughput trade-off.
|
||||
- Boot-time `mkdir -p` of the parent directory + `W_OK` probe;
|
||||
unwriteable targets throw `AUDIT_FILE.PATH_NOT_WRITEABLE` from the
|
||||
factory itself so misconfiguration is visible at host startup.
|
||||
- Size + time rotation; `maxFiles` retention; per-eviction OTel counter.
|
||||
- Idempotent `dispose()`; observable `queueDepth` getter for probes.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createFileAuditSink } from "@suaveplan/api-audit-file/sink";
|
||||
|
||||
const sink = await createFileAuditSink({
|
||||
path: "/var/log/suaveplan/audit.log",
|
||||
hmacKey: new TextEncoder().encode(process.env.AUDIT_HMAC_KEY ?? ""),
|
||||
});
|
||||
|
||||
await sink.emit({
|
||||
at: new Date().toISOString(),
|
||||
requestId: "r-1",
|
||||
traceId: null,
|
||||
route: "/admin/users",
|
||||
plugin: "core-admin",
|
||||
method: "POST",
|
||||
principal: { id: "user-1", scopes: ["admin"] },
|
||||
tenantId: "tenant-a",
|
||||
status: 200,
|
||||
outcome: "success",
|
||||
});
|
||||
|
||||
await sink.dispose();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Run with a custom rotation policy, `append` mode, real `MeterProvider`,
|
||||
and an injected `FakeClock` for deterministic tests:
|
||||
|
||||
```ts
|
||||
import { FakeClock } from "@suaveplan/testing";
|
||||
import {
|
||||
MeterProvider,
|
||||
InMemoryMetricExporter,
|
||||
PeriodicExportingMetricReader,
|
||||
} from "@opentelemetry/sdk-metrics";
|
||||
import { createFileAuditSink } from "@suaveplan/api-audit-file/sink";
|
||||
import { createAuditFileInstruments } from "@suaveplan/api-audit-file/instruments";
|
||||
|
||||
const clock = new FakeClock(0);
|
||||
const exporter = new InMemoryMetricExporter(0);
|
||||
const reader = new PeriodicExportingMetricReader({ exporter });
|
||||
const meterProvider = new MeterProvider({ readers: [reader] });
|
||||
|
||||
const sink = await createFileAuditSink({
|
||||
path: "/tmp/audit.log",
|
||||
hmacKey: new TextEncoder().encode("secret"),
|
||||
clock: clock.read,
|
||||
rotation: { maxSizeBytes: 1024, maxAgeMs: 60_000, maxFiles: 5 },
|
||||
appendMode: "append",
|
||||
maxQueueSize: 256,
|
||||
instruments: createAuditFileInstruments({ meterProvider }),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface CreateFileAuditSinkOptions`
|
||||
- `path: string` (required)
|
||||
- `hmacKey: Uint8Array` (required, non-empty)
|
||||
- `clock?: Clock` — default `() => Date.now()`
|
||||
- `rotation?: Partial<RotationPolicy>`
|
||||
- `appendMode?: "strict-sync" | "append"` — default `"strict-sync"`
|
||||
- `maxQueueSize?: number` — default 1024
|
||||
- `fs?: FsAdapter` — default `nodeFsAdapter`
|
||||
- `logger?: LoggerLike` — default no-op
|
||||
- `instruments?: AuditFileInstruments` — default global meter
|
||||
- `interface FileAuditSink`
|
||||
- `emit(record): Promise<void>`
|
||||
- `dispose(): Promise<void>`
|
||||
- `readonly queueDepth: number`
|
||||
- `createFileAuditSink(options): Promise<FileAuditSink>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The drain loop is mutex-style: a single in-flight drain promise covers
|
||||
every concurrent `emit()` call so writes are serialised against the
|
||||
file handle even when callers fan out. Backpressure is signalled
|
||||
synchronously — `emit()` throws `QUEUE_FULL` before any await when the
|
||||
bounded queue is at capacity, so the host can apply load shedding
|
||||
without observing a write-latency spike. Rotation runs in the same
|
||||
drain frame as the triggering write so the rename always happens
|
||||
before the segment grows past the cap by one extra line. Retention
|
||||
runs after rotation; a retention failure is logged but does not
|
||||
propagate — the sink stays available even if `unlink` is transiently
|
||||
denied. The on-disk format is intentionally trivial (`{ record, hmac }`
|
||||
per line) so an operator with `jq` and `openssl dgst -sha256 -hmac` can
|
||||
verify integrity without any Suaveplan tooling on hand.
|
||||
79
docs/api-stack/api/api-audit-stdout/README.md
Normal file
79
docs/api-stack/api/api-audit-stdout/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# @suaveplan/api-audit-stdout - Stdout `RequestAuditSink` for @suaveplan/api-host. Writes JSON-per-line audit records to a Writable (default process.stdout) with an HMAC-SHA-256 signature over canonical bytes. Implements the design-doc §12.5 audit-record shape, Zod-validated at the boundary, with bounded backpressure (maxQueueMs default 100ms), observable drop counter, and a once-per-minute warning throttle. OTel counters api.audit.records_total{sink=stdout,type,outcome} and api.audit.dropped_total{sink=stdout}. Server-tier — Bun + Node.
|
||||
|
||||
Stdout audit sink for `@suaveplan/api-host`. Writes JSON-per-line
|
||||
audit records to a `LineWriter` (default `process.stdout`) with an
|
||||
HMAC-SHA-256 signature appended as a `_hmac` field. Implements
|
||||
`RequestAuditSink` from `@suaveplan/api-host/request-pipeline`, so the
|
||||
host's request pipeline accepts the instance directly via its `audit`
|
||||
capability slot.
|
||||
|
||||
The record shape is the design-doc §12.5 canonical wire format —
|
||||
`ts`, `type`, `principal`, `tenant`, `request_id`, `trace_id`,
|
||||
`subject`, `outcome`, and a conditional `cause`. Each record is
|
||||
Zod-validated at the boundary before signing, so a malformed record
|
||||
never reaches the log pipeline.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module declares the four stable error codes the stdout audit sink exposes to consumers. Every failure path inside…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module declares the four stable error codes the stdout audit sink exposes to consumers. Every failure path inside…
|
||||
- [hmac](./src/hmac/hmac.md) — This module produces the HMAC-SHA-256 tag attached to every audit line. Signing operates on a *canonical* byte represen…
|
||||
- [hmac](./src/hmac/hmac.md) — This module produces the HMAC-SHA-256 tag attached to every audit line. Signing operates on a *canonical* byte represen…
|
||||
- [instruments](./src/instruments/instruments.md) — This module owns the two counters the stdout sink emits at runtime, matching design doc §12.3's metric surface for the…
|
||||
- [instruments](./src/instruments/instruments.md) — This module owns the two counters the stdout sink emits at runtime, matching design doc §12.3's metric surface for the…
|
||||
- [options](./src/options/options.md) — This module owns the Zod schema and defaults for the scalar slice of `createStdoutAuditSink({...})`'s input. Validation…
|
||||
- [options](./src/options/options.md) — This module owns the Zod schema and defaults for the scalar slice of `createStdoutAuditSink({...})`'s input. Validation…
|
||||
- [schema](./src/schema/schema.md) — This module locks the wire shape of the JSON line the stdout sink writes. The shape is exactly what design doc §12.5 sp…
|
||||
- [schema](./src/schema/schema.md) — This module locks the wire shape of the JSON line the stdout sink writes. The shape is exactly what design doc §12.5 sp…
|
||||
- [sink](./src/sink/sink.md) — This module wires the schema, HMAC signer, instruments, and options into a `RequestAuditSink` implementation that write…
|
||||
- [sink](./src/sink/sink.md) — This module wires the schema, HMAC signer, instruments, and options into a `RequestAuditSink` implementation that write…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Modules
|
||||
|
||||
- [`./sink`](./src/sink/sink.md) — `createStdoutAuditSink`, the
|
||||
factory that ties everything together.
|
||||
- [`./schema`](./src/schema/schema.md) — `AuditRecord` Zod schema and
|
||||
boundary parser.
|
||||
- [`./hmac`](./src/hmac/hmac.md) — canonical bytes and HMAC-SHA-256
|
||||
signer / verifier.
|
||||
- [`./instruments`](./src/instruments/instruments.md) — OTel counters
|
||||
(`api.audit.records_total`, `api.audit.dropped_total`).
|
||||
- [`./options`](./src/options/options.md) — scalar option schema +
|
||||
defaults (`maxQueueMs = 100`, `warnThrottleMs = 60_000`).
|
||||
- [`./error-codes`](./src/error-codes/error-codes.md) —
|
||||
`AUDIT_STDOUT.*` registry (`INVALID_RECORD`, `WRITER_FAILED`,
|
||||
`QUEUE_FULL`, `INVALID_OPTIONS`).
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { createStdoutAuditSink } from "@suaveplan/api-audit-stdout";
|
||||
|
||||
const sink = createStdoutAuditSink({
|
||||
hmacKey: new TextEncoder().encode(process.env["AUDIT_HMAC_KEY"]!),
|
||||
});
|
||||
|
||||
await sink.emit({
|
||||
at: "2026-05-26T10:00:00.000Z",
|
||||
requestId: "req_1",
|
||||
traceId: null,
|
||||
route: "/admin/plugin",
|
||||
plugin: "@core/admin",
|
||||
method: "POST",
|
||||
principal: { id: "u1", scopes: ["admin"] },
|
||||
tenantId: "t1",
|
||||
status: 200,
|
||||
outcome: "success",
|
||||
});
|
||||
```
|
||||
|
||||
## Drop policy
|
||||
|
||||
Backpressure is bounded by `maxQueueMs` (default 100ms). When the
|
||||
queue wait exceeds the bound the record is dropped, the
|
||||
`api.audit.dropped_total{sink,reason}` counter increments, and
|
||||
(subject to a once-per-`warnThrottleMs` throttle) a single warning is
|
||||
logged. Drops are NEVER silent.
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# `error-codes` — `AUDIT_STDOUT_*` registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module declares the four stable error codes the stdout audit sink
|
||||
exposes to consumers. Every failure path inside `createStdoutAuditSink`
|
||||
maps onto exactly one of them, so callers can branch on `error.code`
|
||||
without parsing messages or stack traces. The registry uses
|
||||
`@suaveplan/error`'s `defineCodes` so the union narrows under
|
||||
`instanceof AuditStdoutError`.
|
||||
|
||||
The codes capture the four observable failure modes of a write-only,
|
||||
fire-and-forget audit pipeline: a record that fails its own Zod schema
|
||||
(`INVALID_RECORD`), an option bag that fails the constructor schema
|
||||
(`INVALID_OPTIONS`), a transport error from the supplied writer
|
||||
(`WRITER_FAILED`), and the bounded-backpressure drop case
|
||||
(`QUEUE_FULL`). The codes are surfaced through the
|
||||
`AuditStdoutError` subclass, which carries a `code` property typed as
|
||||
the literal union — `instanceof` narrowing flows through.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable string codes via `defineCodes("AUDIT_STDOUT", [...])`.
|
||||
- `AuditStdoutError` subclass with public constructor (lint rule
|
||||
`check-error-subclass-constructors` enforced).
|
||||
- Code union exported as `AuditStdoutErrorCode` for callers who want
|
||||
to switch over every member.
|
||||
- No runtime side effects — the registry is a frozen object literal.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUDIT_STDOUT_CODES,
|
||||
AuditStdoutError,
|
||||
} from "@suaveplan/api-audit-stdout/error-codes";
|
||||
|
||||
throw new AuditStdoutError(
|
||||
AUDIT_STDOUT_CODES.INVALID_RECORD,
|
||||
"error.audit_stdout.invalid_record"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUDIT_STDOUT_CODES,
|
||||
AuditStdoutError,
|
||||
type AuditStdoutErrorCode,
|
||||
} from "@suaveplan/api-audit-stdout/error-codes";
|
||||
|
||||
function handle(err: unknown): "drop" | "rethrow" {
|
||||
if (!(err instanceof AuditStdoutError)) return "rethrow";
|
||||
const code: AuditStdoutErrorCode = err.code;
|
||||
switch (code) {
|
||||
case AUDIT_STDOUT_CODES.QUEUE_FULL:
|
||||
case AUDIT_STDOUT_CODES.WRITER_FAILED:
|
||||
return "drop";
|
||||
case AUDIT_STDOUT_CODES.INVALID_RECORD:
|
||||
case AUDIT_STDOUT_CODES.INVALID_OPTIONS:
|
||||
return "rethrow";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AUDIT_STDOUT_CODES` — frozen `{ INVALID_RECORD, WRITER_FAILED,
|
||||
QUEUE_FULL, INVALID_OPTIONS }` mapping to the corresponding
|
||||
`"AUDIT_STDOUT.*"` literal.
|
||||
- `AuditStdoutErrorCode` — union of the four literal strings.
|
||||
- `AuditStdoutError` — `SuaveplanError<AuditStdoutErrorCode>` subclass
|
||||
with a public constructor accepting `(code, message, options?)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` returns a frozen object — adding a new code requires an
|
||||
OpenSpec change. The subclass declares its own public constructor so
|
||||
`new AuditStdoutError(...)` is callable from outside the module (the
|
||||
base class is abstract with a protected constructor). The
|
||||
`auditStdoutCode` field is a typed alias of `code` for callers who
|
||||
want a non-generic accessor.
|
||||
82
docs/api-stack/api/api-audit-stdout/src/hmac/hmac.md
Normal file
82
docs/api-stack/api/api-audit-stdout/src/hmac/hmac.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# `hmac` — canonical bytes + HMAC-SHA-256 signing
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module produces the HMAC-SHA-256 tag attached to every audit
|
||||
line. Signing operates on a *canonical* byte representation of the
|
||||
record (keys serialised in a fixed order, `cause` omitted when
|
||||
absent) so the same logical record always produces the same MAC under
|
||||
the same key, regardless of how the source object was constructed.
|
||||
Verification recomputes the MAC and constant-time-compares to the
|
||||
hex-decoded signature.
|
||||
|
||||
The point isn't to encrypt the record — the line is plaintext JSON
|
||||
written to stdout. The HMAC catches tampering downstream: a log
|
||||
collector or storage tier that re-writes a field to mask an audited
|
||||
action invalidates the MAC, and the auditor's verifier flags the
|
||||
discrepancy. The key never leaves the host; rotation is the caller's
|
||||
concern.
|
||||
|
||||
## Features
|
||||
|
||||
- `canonicalBytes(record)` — UTF-8 bytes of JSON with top-level keys
|
||||
in design-doc §12.5 order and `cause` omitted when absent.
|
||||
- `signRecord(record, key)` — lowercase hex HMAC-SHA-256, computed
|
||||
via `@suaveplan/crypto-hash`'s `hmac("SHA-256", …)` primitive.
|
||||
- `verifyRecord(record, key, signatureHex)` — constant-time match;
|
||||
rejects bad-length, non-hex, and tampered inputs.
|
||||
- `HMAC_FIELD` — the `"_hmac"` field name the sink appends to the
|
||||
JSON line.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { signRecord, verifyRecord } from "@suaveplan/api-audit-stdout/hmac";
|
||||
|
||||
const key = new Uint8Array(32);
|
||||
const tag = signRecord(record, key);
|
||||
const ok = verifyRecord(record, key, tag);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
canonicalBytes,
|
||||
HMAC_FIELD,
|
||||
signRecord,
|
||||
} from "@suaveplan/api-audit-stdout/hmac";
|
||||
|
||||
function emit(record: AuditRecord, key: Uint8Array): string {
|
||||
// Equivalent to what the sink does internally, but caller-managed.
|
||||
const line = JSON.parse(
|
||||
new TextDecoder().decode(canonicalBytes(record))
|
||||
) as Record<string, unknown>;
|
||||
line[HMAC_FIELD] = signRecord(record, key);
|
||||
return JSON.stringify(line);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `canonicalBytes(record: AuditRecord): Uint8Array`
|
||||
- `signRecord(record: AuditRecord, key: Uint8Array): string` — hex.
|
||||
- `verifyRecord(record: AuditRecord, key: Uint8Array, sig: string): boolean`
|
||||
- `HMAC_FIELD: "_hmac"`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The canonical encoder lists top-level keys explicitly rather than
|
||||
sorting alphabetically — the design doc fixes the order so callers
|
||||
reading raw lines see fields in the documented sequence. Nested
|
||||
objects use natural insertion order; their shape is locked by the
|
||||
Zod schema so the JSON.stringify output is deterministic. `cause` is
|
||||
omitted (not serialised as `null`) when absent so a `success` record
|
||||
without `cause` and a `success` record with `cause: undefined`
|
||||
produce the same MAC. Hex was chosen over base64 because hex is
|
||||
case-insensitive trivially and stays ASCII-safe through every common
|
||||
log pipeline.
|
||||
@@ -0,0 +1,70 @@
|
||||
# `instruments` — OTel counters for the stdout audit sink
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the two counters the stdout sink emits at runtime,
|
||||
matching design doc §12.3's metric surface for the audit pipeline.
|
||||
Operators wire the `MeterProvider` once at process boot; the sink
|
||||
picks up the configured provider through this factory. When no SDK
|
||||
is installed, the OTel global resolves to a no-op meter so the sink
|
||||
runs cleanly in tests without any ceremony.
|
||||
|
||||
The factory returns a frozen bundle so the sink does zero meter
|
||||
lookups on the hot path — both counters are constructed once and the
|
||||
record's `type` / `outcome` labels are attached at observation time.
|
||||
|
||||
## Features
|
||||
|
||||
- `api.audit.records_total{sink, type, outcome}` — successful write
|
||||
counter.
|
||||
- `api.audit.dropped_total{sink, reason}` — drop counter for
|
||||
queue-full and writer-failed paths.
|
||||
- Default meter provider falls back to the OTel global.
|
||||
- Frozen bundle — references stable for the sink's lifetime.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createStdoutAuditInstruments } from "@suaveplan/api-audit-stdout/instruments";
|
||||
|
||||
const instruments = createStdoutAuditInstruments();
|
||||
instruments.recordsTotal.add(1, {
|
||||
sink: "stdout",
|
||||
type: "request.success",
|
||||
outcome: "success",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
import { createStdoutAuditInstruments } from "@suaveplan/api-audit-stdout/instruments";
|
||||
|
||||
const meterProvider = new MeterProvider();
|
||||
const instruments = createStdoutAuditInstruments({ meterProvider });
|
||||
|
||||
// Hand them into createStdoutAuditSink so the sink emits through the
|
||||
// SDK-installed provider rather than the global no-op.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createStdoutAuditInstruments(config?)`
|
||||
- `meterProvider?: MeterProvider` — defaults to
|
||||
`metrics.getMeterProvider()`.
|
||||
- Returns `StdoutAuditInstruments` with `meter`, `recordsTotal`,
|
||||
`droppedTotal`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Counters are constructed eagerly so the first write does zero
|
||||
allocations beyond the attribute object. The bundle is frozen to
|
||||
prevent accidental shadowing. The `sink="stdout"` attribute is
|
||||
attached at observation time by the sink so the same provider can
|
||||
serve multiple sink implementations side by side (file, vault, etc.)
|
||||
without conflict on the metric name.
|
||||
75
docs/api-stack/api/api-audit-stdout/src/options/options.md
Normal file
75
docs/api-stack/api/api-audit-stdout/src/options/options.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# `options` — constructor option schema and defaults
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the Zod schema and defaults for the scalar slice of
|
||||
`createStdoutAuditSink({...})`'s input. Validation happens at the
|
||||
boundary — a typo on `maxQueueMs` raises
|
||||
`AUDIT_STDOUT.INVALID_OPTIONS` rather than silently being ignored.
|
||||
The schema is `.strict()` so unknown keys are rejected (same rationale
|
||||
as the `AuditRecord` schema).
|
||||
|
||||
The non-scalar slots — writer, clock, timers, logger, instruments,
|
||||
hmacKey — bypass Zod and are checked structurally by the sink itself
|
||||
at construction. Mixing the two validation styles keeps the typed
|
||||
surface narrow while still rejecting obvious typos.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict Zod schema for scalar option keys.
|
||||
- Defaults: `maxQueueMs = 100`, `warnThrottleMs = 60_000`,
|
||||
`sinkLabel = "stdout"`.
|
||||
- Resolver wraps any `ZodError` as `AUDIT_STDOUT.INVALID_OPTIONS`.
|
||||
- Constants exported so callers can reference them when computing
|
||||
test throughput budgets.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { resolveStdoutAuditScalarOptions } from "@suaveplan/api-audit-stdout/options";
|
||||
|
||||
const resolved = resolveStdoutAuditScalarOptions({ maxQueueMs: 250 });
|
||||
// resolved.warnThrottleMs === 60_000 (default)
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
DEFAULT_MAX_QUEUE_MS,
|
||||
resolveStdoutAuditScalarOptions,
|
||||
type StdoutAuditScalarOptions,
|
||||
} from "@suaveplan/api-audit-stdout/options";
|
||||
|
||||
function tightenedForTests(): StdoutAuditScalarOptions {
|
||||
return {
|
||||
maxQueueMs: Math.max(1, DEFAULT_MAX_QUEUE_MS / 10),
|
||||
warnThrottleMs: 1_000,
|
||||
};
|
||||
}
|
||||
|
||||
const resolved = resolveStdoutAuditScalarOptions(tightenedForTests());
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `stdoutAuditScalarOptionsSchema` — strict Zod object.
|
||||
- `StdoutAuditScalarOptions` — inferred type.
|
||||
- `ResolvedStdoutAuditScalarOptions` — defaults-applied shape.
|
||||
- `resolveStdoutAuditScalarOptions(raw)` — boundary parser.
|
||||
- `DEFAULT_MAX_QUEUE_MS`, `DEFAULT_WARN_THROTTLE_MS` — constants.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`maxQueueMs` is bounded to positive integers because a value of 0
|
||||
would mean "never queue" — that's a different semantics (synchronous
|
||||
backpressure) and would need its own option. `warnThrottleMs`
|
||||
governs the once-per-window log; tests use a `FakeClock` plus a
|
||||
short throttle to exercise the throttle without sleeping. The
|
||||
`sinkLabel` exists so a future spinoff (e.g. journald) can reuse
|
||||
this module while still attaching a distinct `sink="…"` label to its
|
||||
counters.
|
||||
95
docs/api-stack/api/api-audit-stdout/src/schema/schema.md
Normal file
95
docs/api-stack/api/api-audit-stdout/src/schema/schema.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# `schema` — `AuditRecord` Zod schema (design doc §12.5)
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module locks the wire shape of the JSON line the stdout sink
|
||||
writes. The shape is exactly what design doc §12.5 specifies for the
|
||||
admin / audit pipeline: `ts`, `type`, `principal`, `tenant`,
|
||||
`request_id`, `trace_id`, `subject`, `outcome`, and a conditional
|
||||
`cause`. Every write goes through `parseAuditRecord(...)` so a
|
||||
malformed record raises `AUDIT_STDOUT.INVALID_RECORD` *before* a line
|
||||
is written — log pipelines downstream never see a half-finished
|
||||
record.
|
||||
|
||||
The schema is `.strict()` (unknown top-level fields are rejected) and
|
||||
carries a `refine` enforcing the §12.5 invariant that `cause` is
|
||||
REQUIRED when `outcome === "failure"`. Optional fields surface via
|
||||
`exactOptionalPropertyTypes` parity so a missing `cause` on a
|
||||
`success` record is distinct from `cause: undefined` on a `failure`
|
||||
record. The principal slice is its own sub-schema so the same shape
|
||||
can be reused by other audit sinks that target the same wire format.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict shape — extra keys rejected.
|
||||
- `outcome === "failure"` requires `cause` (refine rule).
|
||||
- Sub-schemas for `cause`, `principal`, and `outcome` so consumers can
|
||||
type their own DTOs against the same atoms.
|
||||
- `parseAuditRecord(input)` returns the validated record or throws
|
||||
`AUDIT_STDOUT.INVALID_RECORD` wrapping the underlying `ZodError`.
|
||||
- All types are `z.infer<...>` — no hand-written interfaces drift.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseAuditRecord } from "@suaveplan/api-audit-stdout/schema";
|
||||
|
||||
const record = parseAuditRecord({
|
||||
ts: "2026-05-26T10:00:00.000Z",
|
||||
type: "admin.plugin.disabled",
|
||||
principal: { id: "user_1", scopes: ["admin"] },
|
||||
tenant: "tenant_a",
|
||||
request_id: "req_1",
|
||||
trace_id: null,
|
||||
subject: "plugin.exports",
|
||||
outcome: "success",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
auditRecordSchema,
|
||||
type AuditRecord,
|
||||
} from "@suaveplan/api-audit-stdout/schema";
|
||||
|
||||
function adapt(envelope: HostEnvelope): AuditRecord {
|
||||
return auditRecordSchema.parse({
|
||||
ts: envelope.at,
|
||||
type: `request.${envelope.outcome}`,
|
||||
principal: envelope.principal,
|
||||
tenant: envelope.tenantId,
|
||||
request_id: envelope.requestId,
|
||||
trace_id: envelope.traceId,
|
||||
subject: `${envelope.method} ${envelope.route}`,
|
||||
outcome: envelope.outcome,
|
||||
...(envelope.cause !== undefined ? { cause: envelope.cause } : {}),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `auditRecordSchema` — strict Zod object with the §12.5 invariant
|
||||
refined.
|
||||
- `auditCauseSchema`, `auditPrincipalSchema`, `auditOutcomeSchema` —
|
||||
sub-schemas reused by consumers.
|
||||
- `AuditRecord`, `AuditCause`, `AuditPrincipal`, `AuditOutcome` —
|
||||
inferred types.
|
||||
- `parseAuditRecord(input: unknown): AuditRecord` — boundary parser
|
||||
that throws `AuditStdoutError(AUDIT_STDOUT.INVALID_RECORD, …)` on
|
||||
failure.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`.strict()` blocks unknown fields at the boundary — if a future
|
||||
design-doc revision adds fields, this schema must be updated in the
|
||||
same OpenSpec change as the spec. The `outcome === "failure"` /
|
||||
`cause` refine is enforced as a Zod `.refine` rather than a
|
||||
discriminated union so the failure path returns a `ZodError` with a
|
||||
single readable `path: ["cause"]` issue.
|
||||
108
docs/api-stack/api/api-audit-stdout/src/sink/sink.md
Normal file
108
docs/api-stack/api/api-audit-stdout/src/sink/sink.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# `sink` — `createStdoutAuditSink`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module wires the schema, HMAC signer, instruments, and options
|
||||
into a `RequestAuditSink` implementation that writes JSON-per-line
|
||||
audit records to a `LineWriter` (default `process.stdout`). It is the
|
||||
default audit sink shipped with `@suaveplan/api-host`: container
|
||||
runtimes already redirect stdout into a log pipeline, so operators
|
||||
inherit the records without touching host configuration.
|
||||
|
||||
The sink is single-flight: at most one write is on the wire at any
|
||||
time. Subsequent records wait for the slot, but only up to
|
||||
`maxQueueMs` (default 100ms) before being dropped. Drops are NEVER
|
||||
silent — every drop increments `api.audit.dropped_total{sink,reason}`
|
||||
and (subject to the once-per-`warnThrottleMs` throttle) emits a
|
||||
single warning log. Validation, HMAC signing, and serialisation
|
||||
happen on the calling side of the queue so a malformed record is
|
||||
rejected synchronously without consuming the slot.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements `RequestAuditSink` (`emit(envelope)`) from
|
||||
`@suaveplan/api-host/request-pipeline` — host accepts the instance
|
||||
directly.
|
||||
- `write(record: AuditRecord)` overload for admin paths that already
|
||||
hold a canonical record.
|
||||
- Boundary Zod validation — `AUDIT_STDOUT.INVALID_RECORD` on a bad
|
||||
record.
|
||||
- Bounded queue — `AUDIT_STDOUT.QUEUE_FULL` on timeout, drop counter
|
||||
+ throttled warning.
|
||||
- HMAC-SHA-256 over canonical bytes appended as `_hmac` field.
|
||||
- `dispose()` drains the in-flight write and rejects subsequent
|
||||
writes as `closed` drops.
|
||||
- Pluggable `clock`, `timers`, `logger`, `writer`, `instruments`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createStdoutAuditSink } from "@suaveplan/api-audit-stdout/sink";
|
||||
|
||||
const sink = createStdoutAuditSink({
|
||||
hmacKey: new TextEncoder().encode(process.env["AUDIT_HMAC_KEY"]!),
|
||||
});
|
||||
await sink.write({
|
||||
ts: new Date().toISOString(),
|
||||
type: "admin.plugin.disabled",
|
||||
principal: { id: "user_1", scopes: ["admin"] },
|
||||
tenant: "tenant_a",
|
||||
request_id: "req_1",
|
||||
trace_id: null,
|
||||
subject: "plugin.exports",
|
||||
outcome: "success",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createStdoutAuditSink } from "@suaveplan/api-audit-stdout/sink";
|
||||
import { createStdoutAuditInstruments } from "@suaveplan/api-audit-stdout/instruments";
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
|
||||
const meterProvider = new MeterProvider({ readers: [...] });
|
||||
const sink = createStdoutAuditSink({
|
||||
hmacKey,
|
||||
writer: {
|
||||
async write(line) {
|
||||
await myStream.write(line);
|
||||
},
|
||||
},
|
||||
maxQueueMs: 250,
|
||||
warnThrottleMs: 30_000,
|
||||
instruments: createStdoutAuditInstruments({ meterProvider }),
|
||||
logger: myLogger,
|
||||
});
|
||||
// Hand into createHost({ audit: sink }) per host wiring.
|
||||
await sink.emit(envelope);
|
||||
await sink.dispose();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createStdoutAuditSink(options)` → `StdoutAuditSink`.
|
||||
- `StdoutAuditSinkOptions` — `{ hmacKey, writer?, clock?, timers?,
|
||||
logger?, instruments?, maxQueueMs?, warnThrottleMs?, sinkLabel? }`.
|
||||
- `StdoutAuditSink` — extends `RequestAuditSink`; adds `write(record)`
|
||||
and `dispose()`.
|
||||
- `LineWriter` — `{ write(line: string): void | Promise<void> }`.
|
||||
- `adaptEnvelope(envelope)` — exposed for tests and bespoke
|
||||
re-emission flows.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The slot reservation uses an in-flight `Promise<void>` rather than a
|
||||
queue + worker so cancellation flows trivially through GC — there's
|
||||
no worker loop to shut down. `acquireSlot` races the in-flight write
|
||||
against the `timers.setTimer` callback; whichever wins clears the
|
||||
loser. The `closed` flag is checked before validation so a disposed
|
||||
sink rejects fast without spending CPU on schema work.
|
||||
`adaptEnvelope` is the only place the host's camelCase envelope
|
||||
shape meets the canonical snake_case record — keeping that mapping
|
||||
isolated lets the design-doc wire shape evolve without rippling
|
||||
through unrelated callers.
|
||||
60
docs/api-stack/api/api-auth-anonymous/README.md
Normal file
60
docs/api-stack/api/api-auth-anonymous/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# @suaveplan/api-auth-anonymous — trivial session resolver for @suaveplan/api: always returns null Principal. Used as the tail of resolver chains for fully public APIs and as the foundation conformance seed for every other session-resolver in the stack.
|
||||
|
||||
Stage-4 trivial session resolver for `@suaveplan/api`.
|
||||
Always returns `null` for every request. Use this as the tail of resolver
|
||||
chains for public endpoints, and as the canonical adapter for fully public
|
||||
APIs. The package also serves as the foundation conformance seed exercised by
|
||||
every other concrete session-resolver in Stages 4 / 6 / 8.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [adapter](./src/adapter/adapter.md) — Canonical `AuthAdapter` for fully public APIs. Pairs the trivial anonymous resolver with the minimal adapter metadata t…
|
||||
- [adapter](./src/adapter/adapter.md) — Canonical `AuthAdapter` for fully public APIs. Pairs the trivial anonymous resolver with the minimal adapter metadata t…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-anonymous`. The anonymous resolver has no per-request failure modes…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-anonymous`. The anonymous resolver has no per-request failure modes…
|
||||
- [resolver](./src/resolver/resolver.md) — Factory for the trivial `SessionResolver` that always returns `null`. This is the canonical tail-of-chain resolver ever…
|
||||
- [resolver](./src/resolver/resolver.md) — Factory for the trivial `SessionResolver` that always returns `null`. This is the canonical tail-of-chain resolver ever…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Why does "no auth" need its own package?
|
||||
|
||||
Per the API foundation design doc §2.3, every host configures a session
|
||||
resolver — even fully public APIs. Configuring `ANONYMOUS_AUTH_ADAPTER`
|
||||
explicitly makes the "no auth" choice visible in `host.json` and lets the rest
|
||||
of the host (RBAC gate, error envelope, OpenAPI emission) operate uniformly:
|
||||
the principal is simply `null`, never `undefined`.
|
||||
|
||||
## Surface
|
||||
|
||||
| Subpath | Export |
|
||||
|----------------|---------------------------------------------------|
|
||||
| `./resolver` | `createAnonymousResolver(opts?): SessionResolver` |
|
||||
| `./adapter` | `ANONYMOUS_AUTH_ADAPTER: AuthAdapter` |
|
||||
| `./error-codes`| `AUTH_ANON_CODES`, `AuthAnonError` |
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { ANONYMOUS_AUTH_ADAPTER } from "@suaveplan/api-auth-anonymous/adapter";
|
||||
|
||||
host.registerAuth(ANONYMOUS_AUTH_ADAPTER);
|
||||
// every request now produces `principal: null`; public routes proceed,
|
||||
// non-public routes 401 via the host's `publicRoutes` decision.
|
||||
```
|
||||
|
||||
For mixed chains, see `createAnonymousResolver()` + `SessionResolverChain`
|
||||
in `@suaveplan/api-auth/chain`.
|
||||
|
||||
## Conformance
|
||||
|
||||
The resolver passes `runAuthSessionConformance` (null-only mode) from
|
||||
`@suaveplan/api-auth/testing`. See
|
||||
`src/resolver/resolver.test.ts`.
|
||||
|
||||
## Errors
|
||||
|
||||
`AUTH_ANON.INVALID_OPTIONS` — thrown only when the factory is handed a
|
||||
non-object or unknown-key options bag. No per-request error paths exist by
|
||||
design.
|
||||
|
||||
72
docs/api-stack/api/api-auth-anonymous/src/adapter/adapter.md
Normal file
72
docs/api-stack/api/api-auth-anonymous/src/adapter/adapter.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# `adapter` — `ANONYMOUS_AUTH_ADAPTER`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Canonical `AuthAdapter` for fully public APIs. Pairs the trivial anonymous
|
||||
resolver with the minimal adapter metadata the host needs to wire it into the
|
||||
per-request pipeline. The adapter omits `securityScheme` on purpose:
|
||||
`validateAuthAdapter` permits a missing scheme for the anonymous topology so
|
||||
the emitted OpenAPI document contains no `security` entry on routes — the
|
||||
correct shape for "no authentication required".
|
||||
|
||||
## Features
|
||||
|
||||
- Stateless singleton — safe to import once per host process.
|
||||
- `csrf: false` baked in: anonymous traffic has no session to protect; the
|
||||
generic CSRF middleware in `@suaveplan/api-auth/csrf-policy` skips routes
|
||||
whose adapter pins `csrf: false`.
|
||||
- Validates cleanly against `validateAuthAdapter` — no runtime error at host
|
||||
registration time despite the omitted security scheme.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { ANONYMOUS_AUTH_ADAPTER } from "@suaveplan/api-auth-anonymous/adapter";
|
||||
import { validateAuthAdapter } from "@suaveplan/api-auth";
|
||||
|
||||
const adapter = validateAuthAdapter(ANONYMOUS_AUTH_ADAPTER);
|
||||
host.registerAuth(adapter);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
For a chain that mixes typed resolvers with the anonymous tail, do not use
|
||||
`ANONYMOUS_AUTH_ADAPTER` directly — assemble a `SessionResolverChain` so the
|
||||
typed resolvers fire first:
|
||||
|
||||
```ts
|
||||
import { SessionResolverChain } from "@suaveplan/api-auth";
|
||||
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous/resolver";
|
||||
|
||||
const chain = new SessionResolverChain([
|
||||
jwtResolver,
|
||||
createAnonymousResolver(),
|
||||
]);
|
||||
const adapter = {
|
||||
resolver: chain,
|
||||
securityScheme: jwtSecurityScheme, // the typed resolver still emits its scheme
|
||||
} as const;
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|------------------------------|----------|------------------------------------------------|
|
||||
| `ANONYMOUS_AUTH_ADAPTER` | const | `AuthAdapter` with anonymous resolver, no scheme, `csrf: false`. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The constant is constructed once at module load and reused across requests
|
||||
— the resolver itself is documented stateless, so a single instance is safe
|
||||
to fan out across concurrent requests.
|
||||
- `csrf: false` is deliberate. Anonymous requests carry no session cookie, so
|
||||
applying CSRF defence would reject legitimate public traffic. Hosts that
|
||||
want to additionally rate-limit anonymous traffic should layer
|
||||
`api-rate-limit` (Stage 8) on top — that is not this package's concern.
|
||||
- Test coverage exercises (a) `validateAuthAdapter(ANONYMOUS_AUTH_ADAPTER)`
|
||||
returns the adapter unchanged, (b) the embedded resolver still resolves
|
||||
null, and (c) the singleton survives repeated import without re-allocation.
|
||||
@@ -0,0 +1,77 @@
|
||||
# `error-codes` — `AUTH_ANON.*` registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Stable error-code registry for `@suaveplan/api-auth-anonymous`. The anonymous
|
||||
resolver has no per-request failure modes — every request resolves to `null` by
|
||||
construction. The single `AUTH_ANON.INVALID_OPTIONS` code covers the only thing
|
||||
that can fail, which is rejecting malformed options handed to
|
||||
`createAnonymousResolver(opts)` at construction time. Keeping a dedicated
|
||||
code-namespace per package (rather than re-using `AUTH.*`) means consumers can
|
||||
disambiguate anonymous-side problems from general session-resolver issues when
|
||||
multiple resolvers are mounted on the same chain.
|
||||
|
||||
## Features
|
||||
|
||||
- Defined via `defineCodes("AUTH_ANON", [...])` from `@suaveplan/error` so the
|
||||
literal value is always `"AUTH_ANON.INVALID_OPTIONS"`.
|
||||
- `AuthAnonError extends SuaveplanError<AuthAnonErrorCode>` — instance checks
|
||||
narrow `error.code` to the registry type at the type level.
|
||||
- Carries `authAnonCode` as a non-discriminating mirror so adapter code can
|
||||
branch on `err.authAnonCode === AUTH_ANON_CODES.INVALID_OPTIONS` without
|
||||
re-typing the parent class.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AUTH_ANON_CODES, AuthAnonError } from "@suaveplan/api-auth-anonymous/error-codes";
|
||||
|
||||
throw new AuthAnonError(
|
||||
AUTH_ANON_CODES.INVALID_OPTIONS,
|
||||
"createAnonymousResolver: options is not an object"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Wrap a parser failure (e.g. zod) and forward its details into the
|
||||
`context.issues` slot so observability pipelines can render the schema diff:
|
||||
|
||||
```ts
|
||||
import { AUTH_ANON_CODES, AuthAnonError } from "@suaveplan/api-auth-anonymous/error-codes";
|
||||
import z from "zod";
|
||||
|
||||
const OptionsSchema = z.object({}).strict();
|
||||
|
||||
export function parseOptions(input: unknown): Record<string, never> {
|
||||
const out = OptionsSchema.safeParse(input);
|
||||
if (!out.success) {
|
||||
throw new AuthAnonError(
|
||||
AUTH_ANON_CODES.INVALID_OPTIONS,
|
||||
"Anonymous resolver options failed schema validation",
|
||||
{ context: { issues: out.error.issues } }
|
||||
);
|
||||
}
|
||||
return out.data;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|----------------------|----------|------------------------------------------------------------|
|
||||
| `AUTH_ANON_CODES` | const | `{ INVALID_OPTIONS: "AUTH_ANON.INVALID_OPTIONS" }` |
|
||||
| `AuthAnonErrorCode` | type | Union of values in `AUTH_ANON_CODES`. |
|
||||
| `AuthAnonError` | class | `extends SuaveplanError<AuthAnonErrorCode>`. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The single code is deliberate. The resolver is documented to never reject a
|
||||
request; adding speculative codes for "unrecognised credential" or
|
||||
"principal lookup failed" would be lying about a contract that the
|
||||
implementation cannot break by construction.
|
||||
- Tested in `error-codes.test.ts` for both string value and instance shape.
|
||||
@@ -0,0 +1,79 @@
|
||||
# `resolver` — `createAnonymousResolver`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Factory for the trivial `SessionResolver` that always returns `null`. This is
|
||||
the canonical tail-of-chain resolver every public-API host configures: it
|
||||
makes the "no auth" choice explicit in `host.json` and lets the rest of the
|
||||
host (RBAC gate, error envelope, OpenAPI emission) operate uniformly because
|
||||
`principal` is `null` not `undefined`. The factory shape exists — rather than
|
||||
exposing a constant — so future option flags (e.g. `logAnonymousAccess`) can
|
||||
be threaded in without a breaking surface change.
|
||||
|
||||
## Features
|
||||
|
||||
- Always resolves `null` — no header inspection, no network I/O, no state.
|
||||
- Idempotent `dispose()` no-op (resolver holds no resources).
|
||||
- Strict zod options schema rejects non-object input and unknown keys with
|
||||
`AUTH_ANON.INVALID_OPTIONS`.
|
||||
- Returned type is `SessionResolver` from `@suaveplan/api-auth/resolver` so
|
||||
it composes directly into `SessionResolverChain`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous/resolver";
|
||||
|
||||
const resolver = createAnonymousResolver();
|
||||
// Inside a request pipeline:
|
||||
const principal = await resolver.resolve(request); // → null
|
||||
await resolver.dispose(); // safe to call again
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Compose as the tail of a resolver chain so a JWT-typed resolver runs first
|
||||
and the anonymous resolver supplies the `null` short-circuit for unrecognised
|
||||
requests on public routes:
|
||||
|
||||
```ts
|
||||
import { SessionResolverChain } from "@suaveplan/api-auth";
|
||||
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous/resolver";
|
||||
|
||||
const chain = new SessionResolverChain([
|
||||
jwtResolver, // recognises Bearer tokens, else returns null
|
||||
apiKeyResolver, // recognises x-api-key, else returns null
|
||||
createAnonymousResolver(), // tail: always null
|
||||
]);
|
||||
|
||||
const principal = await chain.resolve(request);
|
||||
// principal is now `Principal | null`. The host applies its `publicRoutes`
|
||||
// decision: a non-public route 401s; a public route proceeds with null.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|------------------------------|----------|-------------------------------------------------------|
|
||||
| `createAnonymousResolver` | function | `(opts?: AnonymousResolverOptions) => SessionResolver`. |
|
||||
| `AnonymousResolverOptions` | type | Reserved-for-future-flags options bag. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The resolver returns `Promise<null>` literally rather than the wider
|
||||
`Promise<Principal | null>` so type narrowing in tests can use
|
||||
`expectTypeOf<...>().toEqualTypeOf<Promise<null>>()`. The chain widens it
|
||||
back to `Principal | null` at the boundary.
|
||||
- `dispose()` is a `Promise<void>` not just any promise — the conformance
|
||||
harness asserts the resolved value is `undefined` so this is load-bearing.
|
||||
- The factory is the public surface (not a const) because future flags must
|
||||
be addable without a breaking change to call sites. Even today, passing
|
||||
`{}` is the canonical call.
|
||||
- No OTel instrumentation is emitted from this resolver. Anonymous-access
|
||||
observability lives at the host layer (per-request span tag) — adding
|
||||
counters here would force a foundation-tier package to depend on
|
||||
`@suaveplan/opentelemetry`, which is undesirable for a hot path.
|
||||
155
docs/api-stack/api/api-auth-bearer-static/README.md
Normal file
155
docs/api-stack/api/api-auth-bearer-static/README.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# @suaveplan/api-auth-bearer-static — SessionResolver adapter for service-to-service static bearer tokens. Matches Authorization: Bearer <token> against a fixed token→Principal table using constant-time comparison from @suaveplan/crypto-utils; unknown tokens defer to the next chain entry. Supports an optional per-token mutual-TLS gate that rejects when the host did not surface request.tls.peerCertificate. Ships both a direct surface (createBearerStaticResolver) and a worker-proxy surface (createBearerStaticResolverWorkerProxy) over @suaveplan/api-worker-rpc; both pass runAuthSessionConformance from @suaveplan/api-auth/testing. OpenAPI security-scheme contribution (HTTP Bearer). Zod-validated options, AUTH_BEARER_STATIC.* error codes, OTel match counter partitioned by outcome.
|
||||
|
||||
Stage-4 service-to-service `SessionResolver` adapter for the
|
||||
`@suaveplan/api` plugin system. Matches an inbound `Authorization:
|
||||
Bearer <token>` header against a fixed `token → Principal` table using
|
||||
constant-time comparison from `@suaveplan/crypto-utils`. Unknown tokens
|
||||
return `null` so the host's `SessionResolverChain` falls through to the
|
||||
next adapter (typically `api-auth-jwt` for user sessions).
|
||||
|
||||
Ships both the direct surface (`createBearerStaticResolver`) and the
|
||||
worker-proxy surface (`createBearerStaticResolverWorkerProxy` +
|
||||
`serveBearerStaticResolverOnPort`) over `@suaveplan/api-worker-rpc`; both
|
||||
pass `runAuthSessionConformance` from `@suaveplan/api-auth/testing`.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [constant-time](./src/constant-time/token-match.md) — `findTokenIndex` performs a constant-time lookup of a presented bearer token against a fixed table, returning the match…
|
||||
- [token-match](./src/constant-time/token-match.md) — `findTokenIndex` performs a constant-time lookup of a presented bearer token against a fixed table, returning the match…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — The error-codes module defines the stable `AUTH_BEARER_STATIC.*` namespace that every public failure path in this packa…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — The error-codes module defines the stable `AUTH_BEARER_STATIC.*` namespace that every public failure path in this packa…
|
||||
- [factory](./src/factory/create-bearer-static-resolver.md) — `createBearerStaticResolver` is the direct-surface entry point of the package. It accepts validated `BearerStaticResolv…
|
||||
- [create-bearer-static-resolver](./src/factory/create-bearer-static-resolver.md) — `createBearerStaticResolver` is the direct-surface entry point of the package. It accepts validated `BearerStaticResolv…
|
||||
- [mtls](./src/mtls/mtls-gate.md) — The mTLS gate enforces an optional second factor on individual token table entries. When operators mark an entry with `…
|
||||
- [mtls-gate](./src/mtls/mtls-gate.md) — The mTLS gate enforces an optional second factor on individual token table entries. When operators mark an entry with `…
|
||||
- [options](./src/options/bearer-static-resolver-options.md) — The options module owns the public construction surface of every resolver in this package. It defines the `BearerStatic…
|
||||
- [bearer-static-resolver-options](./src/options/bearer-static-resolver-options.md) — The options module owns the public construction surface of every resolver in this package. It defines the `BearerStatic…
|
||||
- [proxy](./src/proxy/create-bearer-static-resolver-worker-proxy.md) — The worker-proxy surface is the second of the two Stage-4 surfaces this adapter ships (`§5.2a`). It lets the host insta…
|
||||
- [create-bearer-static-resolver-worker-proxy](./src/proxy/create-bearer-static-resolver-worker-proxy.md) — The worker-proxy surface is the second of the two Stage-4 surfaces this adapter ships (`§5.2a`). It lets the host insta…
|
||||
- [security-scheme](./src/security-scheme/security-scheme-contribution.md) — This module builds the OpenAPI 3.1 `components.securitySchemes` contribution the adapter publishes to the host's `AuthA…
|
||||
- [security-scheme-contribution](./src/security-scheme/security-scheme-contribution.md) — This module builds the OpenAPI 3.1 `components.securitySchemes` contribution the adapter publishes to the host's `AuthA…
|
||||
- [telemetry](./src/telemetry/instruments.md) — The telemetry module owns the single OpenTelemetry counter this package emits — `api.auth.bearer_static_match_total` —…
|
||||
- [instruments](./src/telemetry/instruments.md) — The telemetry module owns the single OpenTelemetry counter this package emits — `api.auth.bearer_static_match_total` —…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
bun add @suaveplan/api-auth-bearer-static
|
||||
```
|
||||
|
||||
## Direct surface
|
||||
|
||||
```ts
|
||||
import { createBearerStaticResolver } from "@suaveplan/api-auth-bearer-static";
|
||||
import { SessionResolverChain } from "@suaveplan/api-auth";
|
||||
|
||||
const serviceAuth = createBearerStaticResolver({
|
||||
tokens: [
|
||||
{
|
||||
token: process.env.CRON_TOKEN,
|
||||
principal: {
|
||||
id: "svc_cron",
|
||||
scopes: ["jobs:run"],
|
||||
claims: { kind: "service" },
|
||||
},
|
||||
},
|
||||
{
|
||||
token: process.env.ADMIN_TOKEN,
|
||||
principal: {
|
||||
id: "svc_admin",
|
||||
scopes: ["admin:*"],
|
||||
claims: { kind: "admin" },
|
||||
},
|
||||
mtlsRequired: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Chained — service tokens first, JWT users fall through.
|
||||
const chain = new SessionResolverChain([serviceAuth, jwtResolver]);
|
||||
```
|
||||
|
||||
## Chained-resolver pattern with `api-auth-jwt`
|
||||
|
||||
The intended deployment topology pairs this adapter with
|
||||
`@suaveplan/api-auth-jwt` for user sessions. Static-token callers match
|
||||
first; everything else flows to JWT. The chain's `first-match-wins`
|
||||
semantics means unknown tokens never throw — they simply move on.
|
||||
|
||||
```ts
|
||||
import { SessionResolverChain } from "@suaveplan/api-auth";
|
||||
import { createBearerStaticResolver } from "@suaveplan/api-auth-bearer-static";
|
||||
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt"; // future sibling
|
||||
|
||||
const chain = new SessionResolverChain([
|
||||
createBearerStaticResolver({
|
||||
tokens: [/* service callers */],
|
||||
}),
|
||||
createJwtSessionResolver({
|
||||
issuer: "https://idp.example",
|
||||
audience: "suaveplan",
|
||||
}),
|
||||
]);
|
||||
```
|
||||
|
||||
## Worker-proxy surface
|
||||
|
||||
Use the worker-proxy variant when the host process should not hold the
|
||||
real token table — e.g. an isolated auth worker serving multiple host
|
||||
workers. The host imports `createBearerStaticResolverWorkerProxy(port)`;
|
||||
the worker imports `serveBearerStaticResolverOnPort(port, options)`. The
|
||||
proxy transparently passes `runAuthSessionConformance`, so plugin code
|
||||
treats it identically to the direct surface.
|
||||
|
||||
```ts
|
||||
// host.ts
|
||||
const proxy = createBearerStaticResolverWorkerProxy(port);
|
||||
const chain = new SessionResolverChain([proxy, jwtResolver]);
|
||||
```
|
||||
|
||||
```ts
|
||||
// worker.ts
|
||||
serveBearerStaticResolverOnPort(port, {
|
||||
tokens: [/* real table — never crosses to the host */],
|
||||
});
|
||||
```
|
||||
|
||||
## Optional mutual-TLS gate
|
||||
|
||||
Token entries MAY carry `mtlsRequired: true`. When set, the resolver
|
||||
requires the host listener to surface a verified peer certificate via
|
||||
`request.tls.peerCertificate`; absent → returns `null` (outcome:
|
||||
`mtls-required`). The adapter does NOT establish TLS — that is the host
|
||||
listener's responsibility (Bun TLS, Node `https`, Cloudflare Worker TLS
|
||||
terminator, etc.).
|
||||
|
||||
## Token rotation
|
||||
|
||||
This adapter is intentionally static — rotate tokens by reloading host
|
||||
config. There is no DB-backed lookup, no JWKS, no refresh loop. If
|
||||
you need rotation without restarts, use `api-auth-better-auth` or a
|
||||
future `api-auth-token-table` adapter; this one is the simplest possible
|
||||
thing that resolves a fixed table against constant-time match.
|
||||
|
||||
## Errors
|
||||
|
||||
The package exposes the `AUTH_BEARER_STATIC.*` code namespace via
|
||||
`AUTH_BEARER_STATIC_CODES`. Only `INVALID_OPTIONS` is thrown
|
||||
(synchronously, at factory construction); `NO_TOKEN`, `UNKNOWN_TOKEN`,
|
||||
and `MTLS_REQUIRED` are surfaced via the OTel match counter's `outcome`
|
||||
label.
|
||||
|
||||
## Telemetry
|
||||
|
||||
One counter — `api.auth.bearer_static_match_total{outcome=match|no-token|unknown-token|mtls-required}`
|
||||
— increments exactly once per `resolve()` call.
|
||||
|
||||
## Conformance
|
||||
|
||||
Both surfaces pass `runAuthSessionConformance` from
|
||||
`@suaveplan/api-auth`. Spec-conformance tests live alongside the source
|
||||
under `src/factory/*.spec-conformance.test.ts` and
|
||||
`src/proxy/*.spec-conformance.test.ts`.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Token match — `@suaveplan/api-auth-bearer-static`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`findTokenIndex` performs a constant-time lookup of a presented bearer
|
||||
token against a fixed table, returning the matching entry's index or `-1`
|
||||
if no entry matches. The function is the security boundary of the package:
|
||||
every other code path in the resolver assumes the match is timing-safe so
|
||||
an attacker cannot binary-search the token table by measuring response
|
||||
latency.
|
||||
|
||||
The implementation walks every entry, calling `constantTimeEqual` from
|
||||
`@suaveplan/crypto-utils` once per row. The match-tracking variable is
|
||||
written only on the first match; subsequent matches are ignored. Crucially
|
||||
the loop does not break on first match — exiting early would leak the
|
||||
position of the matching token via wall-clock timing.
|
||||
|
||||
## Features
|
||||
|
||||
- Wraps `@suaveplan/crypto-utils` `constantTimeEqual` over UTF-8 byte
|
||||
views.
|
||||
- Iterates the full table for every call — no early exit, no short-circuit
|
||||
on `presented === entry.token`.
|
||||
- Returns the first matching index or `-1`.
|
||||
- Handles `undefined` entries (defensive guard for sparse arrays).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { findTokenIndex } from "@suaveplan/api-auth-bearer-static/constant-time";
|
||||
|
||||
const table = [
|
||||
{ token: "T1" },
|
||||
{ token: "T2" },
|
||||
];
|
||||
|
||||
const index = findTokenIndex("T2", table);
|
||||
// index === 1
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { findTokenIndex } from "@suaveplan/api-auth-bearer-static/constant-time";
|
||||
|
||||
interface TokenEntry {
|
||||
readonly token: string;
|
||||
readonly principalId: string;
|
||||
}
|
||||
|
||||
const table: ReadonlyArray<TokenEntry> = [
|
||||
{ token: "service-cron", principalId: "p1" },
|
||||
{ token: "service-admin", principalId: "p2" },
|
||||
];
|
||||
|
||||
function lookup(presented: string): TokenEntry | null {
|
||||
const idx = findTokenIndex(presented, table);
|
||||
return idx === -1 ? null : (table[idx] ?? null);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `findTokenIndex(presented: string, table: ReadonlyArray<{ token: string }>): number`
|
||||
— returns the index of the first entry whose `token` matches
|
||||
`presented`, or `-1` if no entry matches.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`constantTimeEqual` from `@suaveplan/crypto-utils` walks every byte of the
|
||||
two `Uint8Array` views and accumulates differences into an integer via
|
||||
bitwise OR; the function returns whether the accumulator stayed zero. The
|
||||
length-mismatch fast path is documented as not secret (token lengths are
|
||||
not secret in this adapter's threat model — the table is operator-supplied
|
||||
config). The `TextEncoder` round-trip ensures we compare bytes, not
|
||||
surrogate-pair code units; otherwise an attacker could compare a multi-
|
||||
byte presented token against a single-byte entry and learn from the
|
||||
length-mismatch shortcut whether their candidate had the right length.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Error codes — `@suaveplan/api-auth-bearer-static`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The error-codes module defines the stable `AUTH_BEARER_STATIC.*` namespace
|
||||
that every public failure path in this package surfaces. Codes are produced
|
||||
by `defineCodes` from `@suaveplan/error` so the registry yields a frozen
|
||||
record of fully-qualified strings (`AUTH_BEARER_STATIC.NO_TOKEN`,
|
||||
`AUTH_BEARER_STATIC.UNKNOWN_TOKEN`, `AUTH_BEARER_STATIC.MTLS_REQUIRED`,
|
||||
`AUTH_BEARER_STATIC.INVALID_OPTIONS`) along with a `BearerStaticAuthError`
|
||||
subclass that lifts each code into a real `Error` instance.
|
||||
|
||||
This module is the contract a caller branches on. Catching a generic
|
||||
`SuaveplanError` and reading `.code` is the supported failure-handling
|
||||
pattern; the concrete subclass exists to make `instanceof` narrow the code
|
||||
union for downstream code that needs the stronger typing.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen four-entry code registry keyed by reason name.
|
||||
- `BearerStaticAuthError` extends `SuaveplanError<BearerStaticAuthErrorCode>`
|
||||
— `instanceof` narrows `.code`.
|
||||
- Stable namespace string (`AUTH_BEARER_STATIC`) safe to log and persist.
|
||||
- Every code exercised by at least one production code path (registry-vs-
|
||||
source conformance is asserted by the spec-conformance test).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUTH_BEARER_STATIC_CODES,
|
||||
BearerStaticAuthError,
|
||||
} from "@suaveplan/api-auth-bearer-static/error-codes";
|
||||
|
||||
try {
|
||||
// factory call that may throw on bad options
|
||||
throw new BearerStaticAuthError(
|
||||
AUTH_BEARER_STATIC_CODES.INVALID_OPTIONS,
|
||||
"tokens table is empty"
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof BearerStaticAuthError) {
|
||||
// e.code narrows to BearerStaticAuthErrorCode
|
||||
console.error(e.code, e.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUTH_BEARER_STATIC_CODES,
|
||||
BearerStaticAuthError,
|
||||
} from "@suaveplan/api-auth-bearer-static/error-codes";
|
||||
|
||||
function isInvalidOptions(err: unknown): err is BearerStaticAuthError {
|
||||
return (
|
||||
err instanceof BearerStaticAuthError &&
|
||||
err.code === AUTH_BEARER_STATIC_CODES.INVALID_OPTIONS
|
||||
);
|
||||
}
|
||||
|
||||
// Treat invalid options as a fatal config error; bubble everything else.
|
||||
try {
|
||||
// build resolver
|
||||
} catch (e) {
|
||||
if (isInvalidOptions(e)) {
|
||||
process.exit(78); // EX_CONFIG
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AUTH_BEARER_STATIC_CODES` — `Readonly<Record<Reason,
|
||||
"AUTH_BEARER_STATIC.<Reason>">>`. Frozen at module load.
|
||||
- `BearerStaticAuthErrorCode` — string-literal union of every value.
|
||||
- `class BearerStaticAuthError extends SuaveplanError<BearerStaticAuthErrorCode>`
|
||||
— constructor `(code, messageKey, options?)` matches the
|
||||
`SuaveplanError` shape.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry is constructed with `defineCodes("AUTH_BEARER_STATIC", [...])`.
|
||||
`defineCodes` returns a `Readonly` record whose key set is the reason names
|
||||
and whose values are the fully-qualified `"AUTH_BEARER_STATIC.<reason>"`
|
||||
strings — referenced via `AUTH_BEARER_STATIC_CODES.NO_TOKEN` rather than the
|
||||
raw string literal so a typo at the call site fails type-check.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Direct factory — `@suaveplan/api-auth-bearer-static`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`createBearerStaticResolver` is the direct-surface entry point of the
|
||||
package. It accepts validated `BearerStaticResolverOptions`, builds a
|
||||
`SessionResolver` over the fixed token table, and returns a resolver
|
||||
implementation suitable for placing inside a `SessionResolverChain` from
|
||||
`@suaveplan/api-auth`. The resolver matches the documented chain
|
||||
semantics: an unknown token, missing header, or tripped mTLS gate all
|
||||
return `null` so the chain advances to the next entry.
|
||||
|
||||
The resolver is stateless beyond the disposed flag — there is no DB
|
||||
connection, no JWKS cache, no refresh loop. Disposal flips the flag and
|
||||
subsequent calls return `null` rather than throwing.
|
||||
|
||||
## Features
|
||||
|
||||
- Stateless per-request resolution; safe under concurrent fanout.
|
||||
- Constant-time token match via `findTokenIndex`.
|
||||
- Optional per-entry mTLS gate via `checkMtlsGate`.
|
||||
- Configurable `headerName` and prefix (`Bearer` by default).
|
||||
- Case-insensitive scheme match per RFC 7235.
|
||||
- OTel counter emission for every of the four outcomes.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createBearerStaticResolver } from "@suaveplan/api-auth-bearer-static";
|
||||
|
||||
const resolver = createBearerStaticResolver({
|
||||
tokens: [
|
||||
{
|
||||
token: "service-cron-2026",
|
||||
principal: {
|
||||
id: "svc_cron",
|
||||
scopes: ["jobs:run"],
|
||||
claims: { kind: "service" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const req = new Request("https://api/x", {
|
||||
headers: { Authorization: "Bearer service-cron-2026" },
|
||||
});
|
||||
const principal = await resolver.resolve(req);
|
||||
// principal.id === "svc_cron"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createBearerStaticResolver } from "@suaveplan/api-auth-bearer-static";
|
||||
import { SessionResolverChain } from "@suaveplan/api-auth";
|
||||
import { createJwtResolver } from "@suaveplan/api-auth-jwt"; // hypothetical sibling
|
||||
|
||||
// Chained — service tokens checked first, JWT users fall through after.
|
||||
const chain = new SessionResolverChain([
|
||||
createBearerStaticResolver({
|
||||
tokens: [
|
||||
{
|
||||
token: "admin-2026",
|
||||
principal: {
|
||||
id: "svc_admin",
|
||||
scopes: ["admin:*"],
|
||||
claims: {},
|
||||
},
|
||||
mtlsRequired: true,
|
||||
},
|
||||
],
|
||||
headerName: "X-Service-Auth",
|
||||
prefix: "Token",
|
||||
}),
|
||||
createJwtResolver({ issuer: "https://idp/..." }),
|
||||
]);
|
||||
|
||||
const result = await chain.resolve(request);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createBearerStaticResolver(options: BearerStaticResolverOptions): SessionResolver`
|
||||
|
||||
Returned resolver:
|
||||
|
||||
- `resolve(request: Request): Promise<Principal | null>`
|
||||
- `dispose(): Promise<void>` — idempotent.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`extractToken` accepts the header value, trims, splits on the first space,
|
||||
and lower-cases the scheme for the prefix comparison. Empty credentials
|
||||
after the prefix are rejected. The implementation reads the configured
|
||||
header by exact name (the Web `Headers` API normalises case on lookup, so
|
||||
`Authorization` and `authorization` both work). When the table contains
|
||||
the same token twice with conflicting principals, `findTokenIndex` returns
|
||||
the first index — operator misconfiguration the schema cannot prevent.
|
||||
TypeScript cannot narrow `table[idx]` to non-undefined even though
|
||||
`findTokenIndex` only ever returns `-1` or a valid in-range index; a type
|
||||
assertion encodes that contract at the call site without adding a runtime
|
||||
guard the test would have to exercise.
|
||||
@@ -0,0 +1,79 @@
|
||||
# mTLS gate — `@suaveplan/api-auth-bearer-static`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The mTLS gate enforces an optional second factor on individual token table
|
||||
entries. When operators mark an entry with `mtlsRequired: true`, the
|
||||
resolver MUST refuse to surface the entry's Principal unless the host
|
||||
listener attached a verified peer certificate to the inbound request via
|
||||
`request.tls.peerCertificate`. This guards the highest-privilege service
|
||||
tokens (e.g. internal admin API) against credential leakage — the bearer
|
||||
token alone is necessary but no longer sufficient.
|
||||
|
||||
The adapter does NOT establish TLS itself. The host listener (Bun's
|
||||
TLS-enabled server, Node's `https`, a Cloudflare-Worker TLS terminator
|
||||
ahead of the worker, etc.) is responsible for completing the TLS handshake
|
||||
and surfacing the verified subject as `request.tls.peerCertificate`. This
|
||||
module just inspects what was attached.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure function — no I/O, no state.
|
||||
- Defensive — accepts any `Request`-shaped input even when the `tls` field
|
||||
was not attached (returns `undefined`).
|
||||
- Treats `peerCertificate: null` identically to absent.
|
||||
- `mtlsRequired === false` short-circuits without inspecting the request.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { checkMtlsGate } from "@suaveplan/api-auth-bearer-static/mtls";
|
||||
|
||||
const request = new Request("https://example/test");
|
||||
const allowed = checkMtlsGate(request, true);
|
||||
// allowed === false — no peerCertificate attached
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
checkMtlsGate,
|
||||
extractPeerCertificate,
|
||||
} from "@suaveplan/api-auth-bearer-static/mtls";
|
||||
|
||||
// Host listener pattern — wrap the inbound request before handing off to
|
||||
// the auth chain.
|
||||
const withTls = Object.assign(request, {
|
||||
tls: { peerCertificate: parsedX509Subject },
|
||||
});
|
||||
|
||||
const cert = extractPeerCertificate(withTls);
|
||||
// cert === parsedX509Subject
|
||||
|
||||
const allowed = checkMtlsGate(withTls, true);
|
||||
// allowed === true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface RequestTlsInfo { peerCertificate?: unknown }`
|
||||
- `extractPeerCertificate(request: Request): unknown | undefined`
|
||||
- `checkMtlsGate(request: Request, mtlsRequired: boolean): boolean`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `Request` Web type has no standardised `tls` field. Both Node's
|
||||
undici-based `Request` and Bun's polyfill ignore unknown properties on
|
||||
construction, so the host pattern is to attach `tls` as a plain property
|
||||
after the Request is built (or to wrap it in a Proxy / subclass). The
|
||||
extractor reads through that property with a single defensive type cast
|
||||
and returns `undefined` for any shape that does not satisfy the
|
||||
`{ peerCertificate: <truthy> }` invariant. The opaque certificate itself
|
||||
is not parsed here — that is the host's job, and a future
|
||||
`api-auth-mtls` adapter (out of scope for Stage 4) will own subject
|
||||
extraction.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Options — `@suaveplan/api-auth-bearer-static`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The options module owns the public construction surface of every resolver
|
||||
in this package. It defines the `BearerStaticResolverOptions` interface
|
||||
(plus its row type `BearerStaticTokenEntry`), the Zod schema that mirrors
|
||||
the interface, and a thin parser that turns operator-supplied input into a
|
||||
validated value or throws `AUTH_BEARER_STATIC.INVALID_OPTIONS`.
|
||||
|
||||
Validation runs once at factory construction. Hot-path resolve calls never
|
||||
re-validate — by then the table is structurally trusted. Token rotation is
|
||||
intentionally out of scope: operators rotate by reloading host config,
|
||||
which re-runs the factory and re-runs validation.
|
||||
|
||||
## Features
|
||||
|
||||
- `tokens` is required and non-empty (`.min(1)`).
|
||||
- Every entry validates token (non-empty string), `principal` shape (id +
|
||||
scopes + claims, optional tenantId), and optional `mtlsRequired` flag.
|
||||
- `headerName` and `prefix` are optional with documented defaults
|
||||
(`Authorization` / `Bearer`).
|
||||
- Failure surfaces a typed `BearerStaticAuthError` carrying the original
|
||||
`ZodError` as `cause` — full diagnostic trail is preserved.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseBearerStaticResolverOptions } from "@suaveplan/api-auth-bearer-static/options";
|
||||
|
||||
const opts = parseBearerStaticResolverOptions({
|
||||
tokens: [
|
||||
{
|
||||
token: "service-cron-2026",
|
||||
principal: {
|
||||
id: "svc_cron",
|
||||
scopes: ["jobs:run"],
|
||||
claims: { kind: "service" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
parseBearerStaticResolverOptions,
|
||||
bearerStaticResolverOptionsSchema,
|
||||
} from "@suaveplan/api-auth-bearer-static/options";
|
||||
import { AUTH_BEARER_STATIC_CODES } from "@suaveplan/api-auth-bearer-static/error-codes";
|
||||
|
||||
// Validate without constructing — useful for config UIs that show errors
|
||||
// inline before committing changes.
|
||||
const result = bearerStaticResolverOptionsSchema.safeParse(userInput);
|
||||
if (!result.success) {
|
||||
renderInlineErrors(result.error);
|
||||
}
|
||||
|
||||
// Or rely on the throwing parser when integrated into a startup pipeline:
|
||||
try {
|
||||
const opts = parseBearerStaticResolverOptions(userInput);
|
||||
return opts;
|
||||
} catch (e) {
|
||||
if (
|
||||
e &&
|
||||
typeof e === "object" &&
|
||||
"code" in e &&
|
||||
e.code === AUTH_BEARER_STATIC_CODES.INVALID_OPTIONS
|
||||
) {
|
||||
crashWithConfigExitCode();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface BearerStaticTokenEntry { token: string; principal: Principal; mtlsRequired?: boolean }`
|
||||
- `interface BearerStaticResolverOptions { tokens: ReadonlyArray<BearerStaticTokenEntry>; headerName?: string; prefix?: string }`
|
||||
- `bearerStaticResolverOptionsSchema: z.ZodType<BearerStaticResolverOptions>`
|
||||
- `parseBearerStaticResolverOptions(raw: unknown): BearerStaticResolverOptions`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema's `principal` block deliberately accepts strings rather than
|
||||
`PrincipalId` / `Scope` brands. Brands are erased at runtime and the host
|
||||
trusts operator-supplied config to be well-formed; rejecting strings that
|
||||
look fine at runtime would be developer-hostile noise. Downstream code
|
||||
treats the returned shape as a `Principal` because the structural fields
|
||||
match.
|
||||
@@ -0,0 +1,110 @@
|
||||
# Worker-proxy factory — `@suaveplan/api-auth-bearer-static`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The worker-proxy surface is the second of the two Stage-4 surfaces this
|
||||
adapter ships (`§5.2a`). It lets the host install the bearer-static
|
||||
resolver in the main process while the actual token table lives inside an
|
||||
isolated worker. The host's `SessionResolverChain` calls
|
||||
`resolve(request)` exactly as for the direct surface; under the hood, the
|
||||
proxy marshals the request across a `MessagePort` using
|
||||
`@suaveplan/api-worker-rpc`, the worker's resolver runs, and the
|
||||
`Principal | null` decision is returned to the host.
|
||||
|
||||
The proxy intentionally keeps the token table out of the host's address
|
||||
space — useful when the host is sandboxed and the worker holds privileged
|
||||
credentials, or when operators want a single auth daemon serving multiple
|
||||
host workers.
|
||||
|
||||
## Features
|
||||
|
||||
- Bidirectional RPC over a `MessagePort` (or any `PortLike`) — no
|
||||
process / address-space assumptions.
|
||||
- Idempotent `dispose()` — coalesces multiple calls and closes the
|
||||
underlying RPC client exactly once.
|
||||
- Header set, request method, URL, and (optional) `peerCertificate` cross
|
||||
the boundary; opaque body and abort signal are out of scope (resolution
|
||||
does not need them).
|
||||
- Server-side mount via `serveBearerStaticResolverOnPort` — owns the real
|
||||
resolver and the token table.
|
||||
- Passes the same `runAuthSessionConformance` harness as the direct
|
||||
surface.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
createBearerStaticResolverWorkerProxy,
|
||||
serveBearerStaticResolverOnPort,
|
||||
} from "@suaveplan/api-auth-bearer-static/proxy";
|
||||
import { createInProcessPortPair } from "@suaveplan/api-worker-rpc";
|
||||
|
||||
const { clientPort, serverPort } = createInProcessPortPair();
|
||||
|
||||
// Worker side — owns the real table.
|
||||
const server = serveBearerStaticResolverOnPort(serverPort, {
|
||||
tokens: [
|
||||
{
|
||||
token: "T1",
|
||||
principal: { id: "svc", scopes: [], claims: {} },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Host side — sees only a proxy.
|
||||
const proxy = createBearerStaticResolverWorkerProxy(clientPort);
|
||||
|
||||
const request = new Request("https://api/x", {
|
||||
headers: { Authorization: "Bearer T1" },
|
||||
});
|
||||
const principal = await proxy.resolve(request);
|
||||
|
||||
await proxy.dispose();
|
||||
server.close("done");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { Worker, MessageChannel } from "node:worker_threads";
|
||||
import {
|
||||
createBearerStaticResolverWorkerProxy,
|
||||
} from "@suaveplan/api-auth-bearer-static/proxy";
|
||||
|
||||
// Real Node worker boundary. The worker script imports
|
||||
// `serveBearerStaticResolverOnPort` and calls it against its own port.
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
const worker = new Worker("./auth-worker.mjs", { transferList: [port2] });
|
||||
|
||||
const resolver = createBearerStaticResolverWorkerProxy(port1 as never);
|
||||
|
||||
// Place in chain alongside direct adapters — host treats both uniformly.
|
||||
import { SessionResolverChain } from "@suaveplan/api-auth";
|
||||
const chain = new SessionResolverChain([resolver]);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `serveBearerStaticResolverOnPort(port: PortLike, options: BearerStaticResolverOptions): { close(reason?: string): void }`
|
||||
— mount the server-side handlers.
|
||||
- `createBearerStaticResolverWorkerProxy(port: PortLike): SessionResolver`
|
||||
— host-side proxy.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The marshalled envelope is `{url, method, headers, peerCertificate?}` —
|
||||
the minimal subset of `Request` that the bearer-static decision actually
|
||||
needs. `peerCertificate` is structured-clone-friendly because the host
|
||||
attaches whatever shape the listener surfaced; tests pass plain objects.
|
||||
The proxy never sends the request body, because token resolution does not
|
||||
consult it. Errors thrown by the server-side resolver (e.g. an unexpected
|
||||
runtime fault) propagate as the standard `WORKER_RPC` error envelope and
|
||||
surface in the host as a thrown `WorkerRpcError` — the host's resolver
|
||||
chain treats this as a fail-closed `RESOLVER_THREW` per the documented
|
||||
`api-auth` chain semantics. `dispose()` calls the remote `dispose` first
|
||||
to drain the server-side state, then closes the local client so the
|
||||
underlying port can be garbage-collected.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Security scheme contribution — `@suaveplan/api-auth-bearer-static`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module builds the OpenAPI 3.1 `components.securitySchemes`
|
||||
contribution the adapter publishes to the host's `AuthAdapter` registry.
|
||||
The generated entry declares an `http` / `bearer` scheme so OpenAPI tooling
|
||||
(Stoplight, ReadMe, Postman import, etc.) and the host's emitter render
|
||||
the canonical "Authorize" affordance.
|
||||
|
||||
The contribution is content-only: it does NOT register routes, mount
|
||||
middleware, or affect runtime behaviour. It exists solely so the OpenAPI
|
||||
document the host emits is correct and discoverable.
|
||||
|
||||
## Features
|
||||
|
||||
- OAS 3.1 compliant — emits the `http` discriminator with `scheme: bearer`.
|
||||
- `name` defaults to `bearerStaticAuth` and is overridable at construction.
|
||||
- Optional `bearerFormat` and `description` fields pass through verbatim.
|
||||
- Validated against the `OpenApiSecuritySchemeSchema` Zod schema published
|
||||
by `@suaveplan/api-auth/security-scheme` — if this module ever drifts,
|
||||
the conformance tests in `api-auth` fail.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { securitySchemeContribution } from "@suaveplan/api-auth-bearer-static/security-scheme";
|
||||
|
||||
const contrib = securitySchemeContribution();
|
||||
// { name: "bearerStaticAuth",
|
||||
// scheme: { type: "http", scheme: "bearer" } }
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { securitySchemeContribution } from "@suaveplan/api-auth-bearer-static/security-scheme";
|
||||
import { validateSecurityScheme } from "@suaveplan/api-auth";
|
||||
|
||||
// Custom name + bearerFormat for an admin-only deployment that publishes
|
||||
// two static-token adapters (cron + admin) side by side under distinct
|
||||
// scheme keys.
|
||||
const adminScheme = securitySchemeContribution({
|
||||
name: "bearerStaticAdminAuth",
|
||||
description: "Static admin token. mTLS-gated. Rotate via host config reload.",
|
||||
bearerFormat: "opaque",
|
||||
});
|
||||
|
||||
// Defensive — confirm the contribution still passes the api-auth schema.
|
||||
validateSecurityScheme(adminScheme);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface SecuritySchemeContributionOptions { name?: string; description?: string; bearerFormat?: string }`
|
||||
- `securitySchemeContribution(options?: SecuritySchemeContributionOptions): SecuritySchemeContribution`
|
||||
— pure factory; the result is structurally a
|
||||
`SecuritySchemeContribution` from `@suaveplan/api-auth`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`bearerFormat` is purely informational per OAS — values like `JWT`,
|
||||
`opaque`, or vendor-specific strings appear in some doc tooling but never
|
||||
affect runtime validation. The contribution intentionally omits the
|
||||
optional fields when undefined rather than emitting `undefined` values,
|
||||
which would round-trip through `JSON.stringify` as missing keys but
|
||||
trigger noisy diffs in machine-comparison tooling.
|
||||
@@ -0,0 +1,76 @@
|
||||
# OTel instruments — `@suaveplan/api-auth-bearer-static`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The telemetry module owns the single OpenTelemetry counter this package
|
||||
emits — `api.auth.bearer_static_match_total` — partitioned by the
|
||||
documented `outcome` attribute. Every call to a resolver's `resolve()`
|
||||
method increments this counter exactly once with one of four label values.
|
||||
|
||||
The instrument is bound lazily against `metrics.getMeter(...)` so the
|
||||
package is safe to import on the cold path before the host installs a
|
||||
`MeterProvider`. Resetting the cached bundle (via
|
||||
`resetBearerStaticInstruments`) is required in test setup to bind against
|
||||
a freshly-installed in-memory exporter.
|
||||
|
||||
## Features
|
||||
|
||||
- Single counter — `api.auth.bearer_static_match_total`.
|
||||
- Four `outcome` label values: `match`, `no-token`, `unknown-token`,
|
||||
`mtls-required`.
|
||||
- Lazy meter resolution — module load does not trigger an OTel side
|
||||
effect.
|
||||
- Idempotent reset hook for test isolation.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { recordMatchOutcome } from "@suaveplan/api-auth-bearer-static/src/telemetry";
|
||||
|
||||
// Inside a resolver's resolve() body:
|
||||
recordMatchOutcome("unknown-token");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
getBearerStaticInstruments,
|
||||
resetBearerStaticInstruments,
|
||||
} from "@suaveplan/api-auth-bearer-static/src/telemetry";
|
||||
|
||||
// In a vitest setup file that has installed an in-memory MeterProvider
|
||||
// upstream:
|
||||
beforeEach(() => {
|
||||
resetBearerStaticInstruments();
|
||||
});
|
||||
|
||||
it("emits the outcome attribute we expect", async () => {
|
||||
const inst = getBearerStaticInstruments();
|
||||
inst.matchTotal.add(1, { outcome: "match" });
|
||||
// ...then flush + assert on the exporter
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface BearerStaticInstruments { matchTotal: Counter }`
|
||||
- `type BearerStaticMatchOutcome = "match" | "no-token" | "unknown-token" | "mtls-required"`
|
||||
- `getBearerStaticInstruments(): BearerStaticInstruments`
|
||||
- `resetBearerStaticInstruments(): void`
|
||||
- `recordMatchOutcome(outcome: BearerStaticMatchOutcome): void`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The instrument name uses dotted notation per OpenTelemetry semantic
|
||||
conventions; the attribute key is `outcome` rather than the host-wide
|
||||
`auth.outcome` since this counter is adapter-scoped and dashboards aggregate
|
||||
across adapters by metric name, not attribute. The `description` field is
|
||||
human-readable only — OTel collectors do not interpret it. The cached bundle
|
||||
exists per process and is intentionally global; resolvers do not own
|
||||
instruments because every concrete resolver in a chain would otherwise
|
||||
double-publish the same counter under the same scope.
|
||||
172
docs/api-stack/api/api-auth-better-auth/README.md
Normal file
172
docs/api-stack/api/api-auth-better-auth/README.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# @suaveplan/api-auth-better-auth — Better-Auth adapter for the @suaveplan/api foundation. Ships two independently-wireable factories implementing the auth.session and auth.provider capability contracts against a Better-Auth instance backed by its own Postgres connection. Maps Better-Auth user+session onto the @suaveplan/api Principal contract, mounts canonical PROVIDER_ROUTE_SURFACE routes (translating Better-Auth's /sign-up etc. idiom), exposes a GitHub OAuth flow via Better-Auth's genericOAuth plugin (so the mock OAuth IdP can be wired end-to-end), guards route-prefix collisions against @suaveplan/api-host's reserved-prefix contract, and emits OTel sessions/oauth/mapping instruments. The ./testing subpath re-exports both conformance suites bound to Postgres-backed factories.
|
||||
|
||||
Stage-6 Better-Auth adapter for `@suaveplan/api`. Ships two
|
||||
**independently-wireable** capability surfaces — `createBetterAuthSession`
|
||||
(implements `auth.session` from `@suaveplan/api-auth`) and
|
||||
`createBetterAuthProvider` (implements `auth.provider` from
|
||||
`@suaveplan/api-auth-provider`). Operators MAY wire either side in
|
||||
isolation OR both together; sharing one options bag between the two
|
||||
factories shares the underlying Better-Auth instance + DB pool.
|
||||
|
||||
> **Two capability surfaces, wire independently.** The session adapter
|
||||
> reads cookies + bearer tokens; the provider adapter mounts the canonical
|
||||
> `PROVIDER_ROUTE_SURFACE` (9 routes) under a configurable prefix. They
|
||||
> share no required code path — wiring only one side does NOT install the
|
||||
> other side's surface.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [db](./src/db/database.md) — Lazy holder for the underlying Better-Auth instance plus a helper that projects {@link BetterAuthOptions} onto the loos…
|
||||
- [database](./src/db/database.md) — Lazy holder for the underlying Better-Auth instance plus a helper that projects {@link BetterAuthOptions} onto the loos…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Stable, tracked error-code registry for `@suaveplan/api-auth-better-auth`. Every fault path inside the adapter…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Stable, tracked error-code registry for `@suaveplan/api-auth-better-auth`. Every fault path inside the adapter…
|
||||
- [independence](./src/independence/session-without-provider.md) — Pin the design invariant from the Stage-6 spec change: the Better-Auth adapter ships two factories — `createBetterA…
|
||||
- [session-without-provider](./src/independence/session-without-provider.md) — Pin the design invariant from the Stage-6 spec change: the Better-Auth adapter ships two factories — `createBetterA…
|
||||
- [instruments](./src/instruments/instruments.md) — Lazily-bound OpenTelemetry instrument bundle for the Better-Auth adapter. Built once per process against the current gl…
|
||||
- [instruments](./src/instruments/instruments.md) — Lazily-bound OpenTelemetry instrument bundle for the Better-Auth adapter. Built once per process against the current gl…
|
||||
- [default-factory](./src/oauth/default-factory.md) — Default `betterAuthFactory` used when an operator does not supply one via `BetterAuthOptions.betterAuthFactory`. Dynami…
|
||||
- [github](./src/oauth/github.md) — Project Suaveplan's {@link BetterAuthOauthProvider} entries onto the `genericOAuth` plugin config Better-Auth consumes.…
|
||||
- [options](./src/options/options.md) — Zod-validated boundary for the Better-Auth adapter's configuration bag. Both `createBetterAuthSession` and `createBette…
|
||||
- [options](./src/options/options.md) — Zod-validated boundary for the Better-Auth adapter's configuration bag. Both `createBetterAuthSession` and `createBette…
|
||||
- [principal](./src/principal/map-user-to-principal.md) — Default Better-Auth `(user, session)` → `Principal` mapping consumed by both `createBetterAuthSession` and `createBette…
|
||||
- [map-user-to-principal](./src/principal/map-user-to-principal.md) — Default Better-Auth `(user, session)` → `Principal` mapping consumed by both `createBetterAuthSession` and `createBette…
|
||||
- [create-provider](./src/provider/create-provider.md) — Factory for the `auth.provider` half of the Stage-6 split. Returns an `AuthProviderAdapter` (per `@suaveplan/api-auth-p…
|
||||
- [route-translation](./src/provider/route-translation.md) — Translation table mapping every entry in `PROVIDER_ROUTE_SURFACE` (the canonical 9-route foundation surface) to the mat…
|
||||
- [route-prefix](./src/route-prefix/route-prefix.md) — Resolve an operator-supplied `routePrefix` against the host's reserved-prefix contract from `@suaveplan/api-host/reserv…
|
||||
- [route-prefix](./src/route-prefix/route-prefix.md) — Resolve an operator-supplied `routePrefix` against the host's reserved-prefix contract from `@suaveplan/api-host/reserv…
|
||||
- [session](./src/session/create-session.md) — Factory for the `auth.session` half of the Stage-6 split. Returns a `SessionResolver` that asks Better-Auth's `auth.api…
|
||||
- [create-session](./src/session/create-session.md) — Factory for the `auth.session` half of the Stage-6 split. Returns a `SessionResolver` that asks Better-Auth's `auth.api…
|
||||
- [testing](./src/testing/testing.md) — `@suaveplan/api-auth-better-auth/testing` is the subpath downstream packages import when they want to assert their own…
|
||||
- [testing](./src/testing/testing.md) — `@suaveplan/api-auth-better-auth/testing` is the subpath downstream packages import when they want to assert their own…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Modules
|
||||
|
||||
- **`error-codes/`** — 10-code `AUTH_BA.*` registry + `BetterAuthError`.
|
||||
- **`options/`** — Zod-validated `BetterAuthOptions` boundary + canonical
|
||||
defaults.
|
||||
- **`db/`** — `LazyBetterAuth` (lazy + cached + concurrency-safe) +
|
||||
`buildBetterAuthFactoryArgs` (outer-options → Better-Auth-args
|
||||
projection).
|
||||
- **`principal/`** — default Better-Auth `(user, session)` → `Principal`
|
||||
mapping; replaceable via `BetterAuthOptions.principalMapping`.
|
||||
- **`session/`** — `createBetterAuthSession(opts)` factory implementing
|
||||
the `SessionResolver` contract.
|
||||
- **`provider/`** — `createBetterAuthProvider(opts)` factory implementing
|
||||
the `AuthProviderAdapter` contract; mounts canonical routes that rewrite
|
||||
onto Better-Auth's native sub-paths.
|
||||
- **`route-prefix/`** — `assertRoutePrefixAvailable` collision check
|
||||
against `@suaveplan/api-host/reserved-prefix`.
|
||||
- **`oauth/`** — `genericOAuth` plugin config builder + default
|
||||
`betterAuth(...)` factory that dynamically loads the plugin when OAuth
|
||||
providers are configured. The GitHub OAuth flow is the canonical
|
||||
reference; the implementation works against the real GitHub IdP in
|
||||
production and the mock IdP from
|
||||
`@suaveplan/testing-integration-oauth-mock` in tests because we use
|
||||
`genericOAuth` (which accepts the full URL triple) rather than the
|
||||
hard-coded `github` social-provider.
|
||||
- **`instruments/`** — OTel counters + histograms per the spec's metrics
|
||||
contract.
|
||||
- **`independence/`** — invariant pin: session-only / provider-only wiring
|
||||
exposes nothing from the other side.
|
||||
- **`testing/`** — `./testing` subpath re-exporting both conformance
|
||||
harnesses bound to Better-Auth factories.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { Pool } from "pg";
|
||||
import {
|
||||
createBetterAuthProvider,
|
||||
createBetterAuthSession,
|
||||
} from "@suaveplan/api-auth-better-auth";
|
||||
|
||||
const pool = new Pool({ connectionString: process.env.BETTER_AUTH_DB_URL! });
|
||||
const opts = {
|
||||
database: pool,
|
||||
baseUrl: "https://api.example.com",
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
routePrefix: "/auth",
|
||||
oauth: {
|
||||
providers: {
|
||||
github: {
|
||||
providerId: "github",
|
||||
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||
authorizationUrl: "https://github.com/login/oauth/authorize",
|
||||
tokenUrl: "https://github.com/login/oauth/access_token",
|
||||
userInfoUrl: "https://api.github.com/user",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Wire both sides — they share one Better-Auth instance via per-options WeakMap:
|
||||
host.registerAuth({
|
||||
resolver: createBetterAuthSession(opts),
|
||||
csrf: true,
|
||||
});
|
||||
host.registerAuthProvider(createBetterAuthProvider(opts));
|
||||
```
|
||||
|
||||
## Error codes
|
||||
|
||||
| Code | Surface | When thrown |
|
||||
| --- | --- | --- |
|
||||
| `AUTH_BA.INVALID_SESSION` | session | unexpected `getSession` failure |
|
||||
| `AUTH_BA.DB_UNREACHABLE` | db | `betterAuth({...})` construction threw |
|
||||
| `AUTH_BA.OAUTH_PROVIDER_REJECTED` | oauth | provider returned 403 |
|
||||
| `AUTH_BA.OAUTH_STATE_MISMATCH` | oauth | state cookie did not match |
|
||||
| `AUTH_BA.USER_NOT_FOUND` | mapping | custom mapper signalled missing user |
|
||||
| `AUTH_BA.EMAIL_NOT_VERIFIED` | session | session present but email unverified |
|
||||
| `AUTH_BA.RATE_LIMITED` | provider | Better-Auth rate-limiter blocked |
|
||||
| `AUTH_BA.INVALID_REDIRECT` | oauth | redirect URI outside allow-list |
|
||||
| `AUTH_BA.PRINCIPAL_MAPPING_FAILED` | mapping | mapper threw |
|
||||
| `AUTH_BA.ROUTE_PREFIX_COLLISION` | mount | prefix collides with host-reserved |
|
||||
|
||||
## Conformance
|
||||
|
||||
Conformance tests live at:
|
||||
|
||||
- `src/session/create-session.spec-conformance.test.ts` — drives
|
||||
`runAuthSessionConformance` against a Postgres-shaped stubbed
|
||||
`BetterAuthLike` (the contract asserts the abstract `SessionResolver`
|
||||
invariants; DB backing is orthogonal).
|
||||
- `src/provider/create-provider.spec-conformance.test.ts` — drives
|
||||
`runAuthProviderConformance` against `createBetterAuthProvider` with the
|
||||
same stubbing topology.
|
||||
- `tests/integration/postgres-better-auth.test.ts` — real Postgres
|
||||
container + real Better-Auth + register/login/resolve. Gracefully skips
|
||||
with a logged reason when Docker / `pg` / schema bootstrap is unavailable
|
||||
in the runner environment.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
host config
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ BetterAuthOptions (Zod) │ parseBetterAuthOptions(...)
|
||||
└──────────────┬──────────────┘
|
||||
│
|
||||
per-opts WeakMap
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ LazyBetterAuth│ ── betterAuth({ ...args, plugins: [genericOAuth(...)] })
|
||||
└─────┬────────┘
|
||||
│ shared (single connection pool)
|
||||
┌───────┴────────┐
|
||||
▼ ▼
|
||||
createBetterAuthSession createBetterAuthProvider
|
||||
(SessionResolver) (AuthProviderAdapter)
|
||||
│ │
|
||||
│ ├── mounts /register, /login, /logout, /refresh,
|
||||
│ │ /oauth/:provider/start, /oauth/:provider/callback,
|
||||
│ │ /verify-email/:token, /forgot-password, /reset-password
|
||||
▼ ▼
|
||||
Principal auth.handler(rewrittenRequest)
|
||||
```
|
||||
|
||||
83
docs/api-stack/api/api-auth-better-auth/src/db/database.md
Normal file
83
docs/api-stack/api/api-auth-better-auth/src/db/database.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# `database`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Lazy holder for the underlying Better-Auth instance plus a helper that
|
||||
projects {@link BetterAuthOptions} onto the loosely-typed argument bag
|
||||
`betterAuth(...)` accepts. The lazy pattern matters because Better-Auth's
|
||||
constructor opens its own DB connection pool — we MUST NOT open it twice
|
||||
when both the session and provider factories are wired against the same
|
||||
options, and we MUST NOT do it eagerly at module import time because that
|
||||
would make `import "@suaveplan/api-auth-better-auth"` block on a network
|
||||
round-trip.
|
||||
|
||||
## Features
|
||||
|
||||
- `LazyBetterAuth.get()` builds the Better-Auth instance on first call,
|
||||
caches it, and serves concurrent callers from the same in-flight promise.
|
||||
- `LazyBetterAuth.reset()` drops the cache (tests only).
|
||||
- Factory failures wrapped in `BetterAuthError(AUTH_BA.DB_UNREACHABLE)` with
|
||||
the underlying error as `cause`.
|
||||
- In-flight slot is cleared on failure so a transient outage doesn't
|
||||
permanently poison the cache.
|
||||
- `buildBetterAuthFactoryArgs(opts)` maps our outer options onto
|
||||
Better-Auth's shape so the wrapper isn't sprinkled with shape translation.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { LazyBetterAuth } from "@suaveplan/api-auth-better-auth";
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
const lazy = new LazyBetterAuth(opts, (args) => betterAuth(args));
|
||||
const auth = await lazy.get();
|
||||
const response = await auth.handler(request);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { LazyBetterAuth, buildBetterAuthFactoryArgs } from "@suaveplan/api-auth-better-auth";
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
// Tests can swap in a stubbed factory that records the args + returns a
|
||||
// stubbed BetterAuthLike. The wrapper's behaviour is exercised without
|
||||
// standing up a real database.
|
||||
const captured: Record<string, unknown>[] = [];
|
||||
const lazy = new LazyBetterAuth(opts, (args) => {
|
||||
captured.push(args);
|
||||
return { handler: async () => new Response("ok"), api: { getSession: async () => null } };
|
||||
});
|
||||
|
||||
// Inspect the args the wrapper would have sent to `betterAuth(...)`:
|
||||
expect(buildBetterAuthFactoryArgs(opts)).toMatchObject({
|
||||
database: opts.database,
|
||||
baseURL: opts.baseUrl,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| `LazyBetterAuth` | class | One instance per options bag; lazy + cached |
|
||||
| `buildBetterAuthFactoryArgs(opts)` | `(BetterAuthOptions) => Record<string, unknown>` | Outer-options → Better-Auth-args projection |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `inflight` slot is the linchpin of the concurrency guarantee — the
|
||||
first caller to enter `get()` writes a single promise; every subsequent
|
||||
caller observes that same promise via the second `if`. On rejection the
|
||||
`finally` clears `inflight` so the next call can retry; on resolution
|
||||
`this.cached` is set, and the `inflight` clear is harmless. The
|
||||
session-vs-advanced split in `buildBetterAuthFactoryArgs` mirrors
|
||||
Better-Auth 1.4's option topology — `session.cookieName` is configured under
|
||||
`session`, but `sameSite` / `secure` live under
|
||||
`advanced.defaultCookieAttributes`. Operators wanting the full Better-Auth
|
||||
option surface (passkeys, magic links, organisation plugin) supply their own
|
||||
`betterAuthFactory` and call `betterAuth({ ...buildBetterAuthFactoryArgs(opts), plugins: [...] })`
|
||||
themselves.
|
||||
@@ -0,0 +1,88 @@
|
||||
# `error-codes`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Stable, OpenSpec-tracked error-code registry for `@suaveplan/api-auth-better-auth`.
|
||||
Every fault path inside the adapter — session resolution, database access,
|
||||
OAuth handshake, principal mapping, route-prefix collision detection — terminates
|
||||
in a {@link BetterAuthError} carrying one of the ten codes defined here. Codes
|
||||
are namespaced under `AUTH_BA.*` via the package's `defineCodes` helper so the
|
||||
full literal value never collides with sibling adapters and consumers can branch
|
||||
on a single discriminated value.
|
||||
|
||||
## Features
|
||||
|
||||
- 10 namespaced codes covering session, database, OAuth, mapping and mount-time
|
||||
failure surfaces.
|
||||
- `BetterAuthErrorCode` literal-union derived from the code bag — narrows
|
||||
exhaustively in `switch`.
|
||||
- `BetterAuthError` class extending `SuaveplanError` so `instanceof` and
|
||||
structural matching (`err.betterAuthCode`) both work.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUTH_BA_CODES,
|
||||
BetterAuthError,
|
||||
} from "@suaveplan/api-auth-better-auth/error-codes";
|
||||
|
||||
if (!session) {
|
||||
throw new BetterAuthError(
|
||||
AUTH_BA_CODES.INVALID_SESSION,
|
||||
"session resolver returned no principal"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUTH_BA_CODES,
|
||||
BetterAuthError,
|
||||
type BetterAuthErrorCode,
|
||||
} from "@suaveplan/api-auth-better-auth/error-codes";
|
||||
|
||||
function describe(code: BetterAuthErrorCode): string {
|
||||
switch (code) {
|
||||
case AUTH_BA_CODES.OAUTH_STATE_MISMATCH:
|
||||
return "Tampered or stale state parameter — restart the flow";
|
||||
case AUTH_BA_CODES.OAUTH_PROVIDER_REJECTED:
|
||||
return "The upstream IdP rejected our token request";
|
||||
default:
|
||||
return "Unhandled Better-Auth adapter failure";
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await router.handle(request);
|
||||
} catch (err) {
|
||||
if (err instanceof BetterAuthError) {
|
||||
logger.warn(describe(err.betterAuthCode), { code: err.code, cause: err.cause });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| `AUTH_BA_CODES` | `Readonly<Record<Name, "AUTH_BA.${Name}">>` | Frozen bag of every documented code |
|
||||
| `BetterAuthErrorCode` | string-literal union | The exact set of values `code` may take |
|
||||
| `BetterAuthError` | class extending `SuaveplanError` | Throwable carrying a `BetterAuthErrorCode` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes("AUTH_BA", [...])` from `@suaveplan/error` is the only way to mint
|
||||
codes — it freezes the bag and produces the string-literal type. Adding a new
|
||||
code requires (1) an OpenSpec change against the `api-auth-better-auth`
|
||||
capability so the spec delta lists the new code, (2) editing the tuple here,
|
||||
and (3) editing every consumer `switch` block. The redundant
|
||||
`betterAuthCode` field mirrors `code` so transport boundaries that drop the
|
||||
prototype chain (worker, IPC) can still match structurally.
|
||||
@@ -0,0 +1,77 @@
|
||||
# `session-without-provider`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Pin the design invariant from The spec change: the Better-Auth
|
||||
adapter ships two factories — `createBetterAuthSession` and
|
||||
`createBetterAuthProvider` — and operators MUST be able to wire either side
|
||||
without dragging in the other. Wiring only the session adapter (keeping
|
||||
account-lifecycle elsewhere — e.g. Clerk or a custom rolled provider) is the
|
||||
"hosted IdP for accounts, Better-Auth for session reads against a shared
|
||||
cookie" topology the design doc §2.3 calls out. Wiring only the provider
|
||||
adapter (no Better-Auth session resolution) is rarer but legal — the host
|
||||
fills the `auth.session` slot with whatever other resolver is configured.
|
||||
|
||||
## Features
|
||||
|
||||
- Sibling test file pins the invariant by:
|
||||
- Asserting `createBetterAuthSession(opts)` returns a `SessionResolver`
|
||||
with no `routes` / `pathPrefix` field.
|
||||
- Asserting `createBetterAuthProvider(opts)` returns an
|
||||
`AuthProviderAdapter` with no `resolve` / `dispose` field.
|
||||
- Walking the recorded route list when `routes(...)` IS invoked to verify
|
||||
every canonical name lands.
|
||||
- `betterAuthFactories` constant re-exports both factories as a single bag
|
||||
so consumer documentation can name the pair without repeating imports.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { betterAuthFactories } from "@suaveplan/api-auth-better-auth";
|
||||
|
||||
const resolver = betterAuthFactories.session(opts);
|
||||
const provider = betterAuthFactories.provider(opts);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createBetterAuthSession } from "@suaveplan/api-auth-better-auth";
|
||||
import { ClerkSessionResolver } from "third-party/clerk-suaveplan-adapter";
|
||||
|
||||
// Topology: Clerk owns sign-up / sign-in routes; Better-Auth reads the
|
||||
// shared session cookie via createBetterAuthSession against a shadow
|
||||
// Better-Auth DB that mirrors the Clerk user store. Provider side NOT wired.
|
||||
host.registerAuth({
|
||||
resolver: new SessionResolverChain({
|
||||
resolvers: [
|
||||
createBetterAuthSession({ database: pool, baseUrl, secret }),
|
||||
new ClerkSessionResolver({ clerkApiKey: process.env.CLERK_KEY! }),
|
||||
],
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| `betterAuthFactories` | `Readonly<{ session, provider }>` | Convenience bag pairing both factories |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The module is intentionally near-empty — the invariant lives in the test
|
||||
file. The thin re-export bag is the only runtime contribution because the
|
||||
OpenSpec scaffolding rule requires every implementation file to have a
|
||||
sibling `.md` AND a paired test file. The independence is enforced
|
||||
structurally by TypeScript anyway — `SessionResolver` and
|
||||
`AuthProviderAdapter` have disjoint shapes — but the test pin guards against
|
||||
a refactor that accidentally adds an `if-not-mounted-mount-the-other-side`
|
||||
side effect inside either factory. The OpenSpec spec delta lists this file
|
||||
as the verification for the "session-only / provider-only wiring exposes
|
||||
nothing from the other side" scenario, so the test names match the spec
|
||||
exactly.
|
||||
@@ -0,0 +1,83 @@
|
||||
# `instruments`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Lazily-bound OpenTelemetry instrument bundle for the Better-Auth adapter.
|
||||
Built once per process against the current global `MeterProvider` and cached.
|
||||
Tests call {@link resetBetterAuthInstruments} after installing a fresh
|
||||
`MeterProvider` so assertions only see the instruments emitted by the unit
|
||||
under test.
|
||||
|
||||
## Features
|
||||
|
||||
- Four canonical instruments matching the spec's OTel surface contract:
|
||||
`auth.better_auth.sessions_resolved_total`,
|
||||
`auth.better_auth.session_resolve_duration_ms`,
|
||||
`auth.better_auth.oauth_flows_total`,
|
||||
`auth.better_auth.principal_mapping_errors_total`.
|
||||
- Singleton-per-process pattern so consumers can call
|
||||
`getBetterAuthInstruments()` from anywhere without worrying about meter
|
||||
proliferation.
|
||||
- Reset helper for test isolation.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { getBetterAuthInstruments } from "@suaveplan/api-auth-better-auth/instruments";
|
||||
|
||||
const ins = getBetterAuthInstruments();
|
||||
ins.sessionsResolvedTotal.add(1, { outcome: "resolved" });
|
||||
ins.sessionResolveDurationMs.record(elapsedMs, { outcome: "resolved" });
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
getBetterAuthInstruments,
|
||||
resetBetterAuthInstruments,
|
||||
} from "@suaveplan/api-auth-better-auth/instruments";
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
|
||||
// In test setup
|
||||
const provider = new MeterProvider({ /* reader, ... */ });
|
||||
metrics.setGlobalMeterProvider(provider);
|
||||
resetBetterAuthInstruments(); // force the cache to rebind to the new provider
|
||||
|
||||
// In test
|
||||
const ins = getBetterAuthInstruments();
|
||||
ins.oauthFlowsTotal.add(1, { provider: "github", outcome: "state_mismatch" });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| `BetterAuthInstruments` | interface | Bundle of `Counter` / `Histogram` instances |
|
||||
| `getBetterAuthInstruments()` | `() => BetterAuthInstruments` | Lazy cached accessor |
|
||||
| `resetBetterAuthInstruments()` | `() => void` | Drop the cache (tests only) |
|
||||
|
||||
Instrument labels:
|
||||
|
||||
| Instrument | Labels | Semantics |
|
||||
| --- | --- | --- |
|
||||
| `auth.better_auth.sessions_resolved_total` | `outcome ∈ "resolved" \| "null" \| "error"` | Session-resolver call count |
|
||||
| `auth.better_auth.session_resolve_duration_ms` | `outcome` (same) | Duration histogram |
|
||||
| `auth.better_auth.oauth_flows_total` | `provider`, `outcome ∈ "success" \| "state_mismatch" \| "provider_rejected"` | OAuth flow conclusion count |
|
||||
| `auth.better_auth.principal_mapping_errors_total` | none | Mapper-threw count |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The meter is acquired via `metrics.getMeter("@suaveplan/api-auth-better-auth")`
|
||||
to match the OpenTelemetry naming convention (instrument scope = package
|
||||
name). Caching is critical — calling `meter.createCounter(name, ...)` more
|
||||
than once for the same `name` produces a stable instrument under the OTel
|
||||
spec, but the SDK MAY emit a warning, and re-binding the cached objects on
|
||||
every `add()` call would generate measurable overhead. The reset helper
|
||||
exists purely for tests because the global `MeterProvider` IS the cache key
|
||||
that decides whether the cached instruments still point at the live provider.
|
||||
@@ -0,0 +1,78 @@
|
||||
# `default-factory`
|
||||
|
||||
## Purpose
|
||||
|
||||
Default `betterAuthFactory` used when an operator does not supply one via
|
||||
`BetterAuthOptions.betterAuthFactory`. Dynamically imports `better-auth`
|
||||
AND (when OAuth providers are configured) the `genericOAuth` plugin, then
|
||||
calls `betterAuth({...})` with the assembled args. The dynamic imports
|
||||
mean module-load of `@suaveplan/api-auth-better-auth` never blocks on
|
||||
`better-auth` — operators that ship a stubbed factory (unit tests, custom
|
||||
adapter shells) avoid the upstream package altogether.
|
||||
|
||||
## Features
|
||||
|
||||
- Dynamic `import("better-auth")` keeps cold-start fast for stub-only
|
||||
consumers.
|
||||
- Dynamic `import("better-auth/plugins/generic-oauth")` only fires when the
|
||||
options actually configure OAuth providers.
|
||||
- The Suaveplan-private `__suaveplan_generic_oauth` sentinel key from
|
||||
`buildBetterAuthFactoryArgs` is stripped before forwarding so the upstream
|
||||
package never sees our internal fields.
|
||||
- Pre-existing `plugins` arrays the operator may pass through their own
|
||||
factory are preserved (the genericOAuth plugin is appended, not assigned).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
// Production wiring — pass options without a betterAuthFactory and the
|
||||
// default fires implicitly:
|
||||
const provider = createBetterAuthProvider({
|
||||
database: pool,
|
||||
baseUrl: "https://api.example.com",
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { defaultBetterAuthFactory } from "@suaveplan/api-auth-better-auth";
|
||||
|
||||
// Operator-supplied factory that wraps the default to inject the
|
||||
// `organization` Better-Auth plugin on top of whatever the wrapper itself
|
||||
// already configures:
|
||||
const customFactory = async (args: Record<string, unknown>) => {
|
||||
const auth = await defaultBetterAuthFactory(args);
|
||||
return auth;
|
||||
};
|
||||
|
||||
const provider = createBetterAuthProvider({
|
||||
database: pool,
|
||||
baseUrl,
|
||||
secret,
|
||||
betterAuthFactory: customFactory,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| `defaultBetterAuthFactory(args)` | `(Record<string, unknown>) => Promise<BetterAuthLike>` | Dynamic loader + plugin assembler |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The sentinel-key pattern (`__suaveplan_generic_oauth`) is the trade-off
|
||||
between (a) keeping `buildBetterAuthFactoryArgs` synchronous and pure, and
|
||||
(b) actually loading the upstream plugin module. The args bag is built
|
||||
synchronously so callers and tests can inspect it; the plugin module is
|
||||
loaded lazily inside this factory. Stripping the sentinel keeps the
|
||||
boundary clean — Better-Auth's option validator complains about unknown
|
||||
top-level keys.
|
||||
|
||||
The `plugins` array preservation matters because operators MAY supply a
|
||||
custom factory that adds Better-Auth's own plugins (organisation, passkeys,
|
||||
2FA, magic-link) on top of the OAuth config the wrapper itself injects. By
|
||||
appending we ensure a stub operator factory + the wrapper's genericOAuth
|
||||
config coexist correctly.
|
||||
81
docs/api-stack/api/api-auth-better-auth/src/oauth/github.md
Normal file
81
docs/api-stack/api/api-auth-better-auth/src/oauth/github.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# `github`
|
||||
|
||||
## Purpose
|
||||
|
||||
Project Suaveplan's {@link BetterAuthOauthProvider} entries onto the
|
||||
`genericOAuth` plugin config Better-Auth consumes. We use `genericOAuth`
|
||||
(not the bundled `github` social-provider) for a deliberate reason: the
|
||||
social-provider hard-codes GitHub's production token endpoint URL, which
|
||||
cannot be redirected onto a mock IdP — `@suaveplan/testing-integration-oauth-mock`'s
|
||||
`createMockOauthIdp({ userShape: "github" })` exposes a real-HTTP server
|
||||
shaped like GitHub, but the social-provider would still hit GitHub.com.
|
||||
`genericOAuth` accepts the full URL triple (`authorizationUrl`, `tokenUrl`,
|
||||
`userInfoUrl`) so the same wrapper code drives both production GitHub and
|
||||
the mock fixture — the only difference is the URL set the operator
|
||||
configures.
|
||||
|
||||
## Features
|
||||
|
||||
- Default scope sets: `read:user user:email` for `providerId: "github"`,
|
||||
`openid email profile` for any other provider.
|
||||
- Operator-supplied `scopes` always wins.
|
||||
- `providerName` plumbed onto Better-Auth's `name` field when set; omitted
|
||||
otherwise.
|
||||
- Returns `undefined` from `buildGenericOauthPluginArgs` when no providers
|
||||
are configured so the caller skips wiring the plugin entirely.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { buildGenericOauthPluginArgs } from "@suaveplan/api-auth-better-auth";
|
||||
|
||||
const pluginArgs = buildGenericOauthPluginArgs({
|
||||
github: {
|
||||
providerId: "github",
|
||||
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||
authorizationUrl: "https://github.com/login/oauth/authorize",
|
||||
tokenUrl: "https://github.com/login/oauth/access_token",
|
||||
userInfoUrl: "https://api.github.com/user",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { buildGenericOauthProviderConfig } from "@suaveplan/api-auth-better-auth";
|
||||
import { createMockOauthIdp } from "@suaveplan/testing-integration-oauth-mock";
|
||||
|
||||
// Integration test: same provider shape, mock URLs.
|
||||
const idp = await createMockOauthIdp({ userShape: "github" });
|
||||
const cfg = buildGenericOauthProviderConfig({
|
||||
providerId: "github",
|
||||
clientId: "test-cid",
|
||||
clientSecret: "test-csec",
|
||||
authorizationUrl: `${idp.url}/authorize`,
|
||||
tokenUrl: `${idp.url}/token`,
|
||||
userInfoUrl: `${idp.url}/userinfo`,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| `buildGenericOauthProviderConfig(p)` | `(BetterAuthOauthProvider) => Record<string, unknown>` | Single provider → genericOAuth config entry |
|
||||
| `buildGenericOauthPluginArgs(providers)` | `(Record<string, BetterAuthOauthProvider> \| undefined) => Record<string, unknown> \| undefined` | Full bag → `{ config: [...] }` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The default GitHub scopes mirror the upstream `github` social-provider's
|
||||
defaults — `read:user` for the `/user` endpoint, `user:email` for
|
||||
`/user/emails` (Better-Auth's `genericOAuth` plugin only hits the
|
||||
configured `userInfoUrl`, but real GitHub still expects the broader scope
|
||||
set when the operator later switches to the native provider). The
|
||||
OIDC-compliant default (`openid email profile`) covers Google, Okta, Keycloak
|
||||
and most other discovery-doc-flavoured IdPs and was the safest "I don't
|
||||
know which provider, give me an email and a stable id" fallback. The
|
||||
`providerId` field is the discriminant the upstream plugin matches against
|
||||
the URL params — `:provider` in `/oauth/:provider/start` MUST match an
|
||||
entry's `providerId` for the route to resolve.
|
||||
105
docs/api-stack/api/api-auth-better-auth/src/options/options.md
Normal file
105
docs/api-stack/api/api-auth-better-auth/src/options/options.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# `options`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod-validated boundary for the Better-Auth adapter's configuration bag. Both
|
||||
`createBetterAuthSession` and `createBetterAuthProvider` accept the same
|
||||
{@link BetterAuthOptions} shape so a single options object can be threaded
|
||||
through host configuration even when the operator wires only one side of the
|
||||
auth split. Validation fires at factory-construction time so misconfiguration
|
||||
fails fast at boot instead of surfacing as a confusing runtime error on the
|
||||
first inbound request.
|
||||
|
||||
## Features
|
||||
|
||||
- Required fields — `database` (opaque pass-through to Better-Auth),
|
||||
`baseUrl` (URL-validated), `secret` (>= 16 chars).
|
||||
- Optional fields — `routePrefix` (`/auth` by default, must start with `/`),
|
||||
`oauth.providers` (per-provider URL-validated client config),
|
||||
`session` (cookie tuning), `principalMapping` (pluggable user → Principal).
|
||||
- Test hook — `betterAuthFactory` lets unit tests stub the underlying
|
||||
`betterAuth(...)` call without standing up a real database.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseBetterAuthOptions } from "@suaveplan/api-auth-better-auth/options";
|
||||
|
||||
const opts = parseBetterAuthOptions({
|
||||
database: pool, // Better-Auth-shaped value (pg.Pool, Kysely dialect, …)
|
||||
baseUrl: "https://api.example.com",
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { parseBetterAuthOptions } from "@suaveplan/api-auth-better-auth/options";
|
||||
import type { Principal } from "@suaveplan/api-types/principal";
|
||||
|
||||
const opts = parseBetterAuthOptions({
|
||||
database: pool,
|
||||
baseUrl: "https://api.example.com",
|
||||
routePrefix: "/api/auth",
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
oauth: {
|
||||
providers: {
|
||||
github: {
|
||||
providerId: "github",
|
||||
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||
scopes: ["read:user", "user:email"],
|
||||
authorizationUrl: "https://github.com/login/oauth/authorize",
|
||||
tokenUrl: "https://github.com/login/oauth/access_token",
|
||||
userInfoUrl: "https://api.github.com/user",
|
||||
},
|
||||
},
|
||||
},
|
||||
session: { cookieName: "ba.session", cookieSameSite: "lax", cookieSecure: true },
|
||||
principalMapping: ({ user }): Principal => ({
|
||||
id: user.id as never,
|
||||
scopes: (user.roles ?? []) as never[],
|
||||
claims: { email: user.email },
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| `BetterAuthOptionsSchema` | Zod schema | Validates the full options object |
|
||||
| `parseBetterAuthOptions(raw)` | `(unknown) => BetterAuthOptions` | Parse + throw on failure |
|
||||
| `DEFAULT_ROUTE_PREFIX` | `"/auth"` | Default mount prefix |
|
||||
| `DEFAULT_COOKIE_NAME` | `"better-auth.session"` | Default cookie name |
|
||||
| `BetterAuthOptions` | interface | Outer options shape |
|
||||
| `BetterAuthOauthProvider` | interface | Per-provider OAuth config |
|
||||
| `BetterAuthSessionOptions` | interface | Cookie tuning bag |
|
||||
| `BetterAuthUserView` / `BetterAuthSessionRecord` / `BetterAuthSessionView` | interfaces | Mapper input shape |
|
||||
| `BetterAuthLike` | interface | Structural slice of `better-auth`'s `Auth` |
|
||||
| `PrincipalMapping` | function type | `(view) => Principal` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`database` is intentionally typed as `unknown` and the schema only checks
|
||||
non-null/non-undefined — Better-Auth itself supports `pg.Pool`, Kysely
|
||||
`Dialect`, `D1Database`, `DBAdapterInstance` and several other shapes; the
|
||||
wrapper never inspects this value beyond passing it through to
|
||||
`betterAuth({ database })`. This keeps the wrapper one-driver-agnostic and
|
||||
lets operators bring their own connection topology (transaction-pool, single
|
||||
client, serverless adapter, …) without the wrapper opinionating on it.
|
||||
|
||||
`secret` enforces a 16-character floor because Better-Auth uses it for cookie
|
||||
signing and shorter secrets are trivially brute-forced. The exact constant is
|
||||
informational — Better-Auth itself would still accept shorter strings — so
|
||||
the strict check here is a defence-in-depth guard at the wrapper boundary.
|
||||
|
||||
`betterAuthFactory` is the only sanctioned way to inject a stubbed
|
||||
Better-Auth instance into the adapter. Production callers MUST omit it so the
|
||||
real `betterAuth(...)` import drives the runtime; tests use it to swap in a
|
||||
synchronous in-memory stub conforming to {@link BetterAuthLike}.
|
||||
@@ -0,0 +1,77 @@
|
||||
# `map-user-to-principal`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Default Better-Auth `(user, session)` → `Principal` mapping consumed by both
|
||||
`createBetterAuthSession` and `createBetterAuthProvider`. Pure function; given
|
||||
the same view it always returns the same `Principal`. Operators MAY supply a
|
||||
custom `principalMapping` in `BetterAuthOptions` to project Better-Auth's
|
||||
user row onto a custom claims topology, but the default is good enough for
|
||||
most deployments — it drops sensitive fields, brands `id` and `scopes`, and
|
||||
prefers `session.activeOrganizationId` over `user.tenantId` so the
|
||||
organisation plugin's tenant scoping wins automatically when wired.
|
||||
|
||||
## Features
|
||||
|
||||
- `id` ← `user.id` branded as `PrincipalId`.
|
||||
- `scopes` ← `user.roles` branded as `Scope[]` (empty array when absent).
|
||||
- `tenantId` ← `session.activeOrganizationId` → `user.tenantId` fallback.
|
||||
- `claims` ← rest of `user` minus sensitive fields; `session_id` appended.
|
||||
- Throws `BetterAuthError(AUTH_BA.PRINCIPAL_MAPPING_FAILED)` on missing id.
|
||||
- Returned `Principal` and its `claims` are frozen.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { mapBetterAuthUserToPrincipal } from "@suaveplan/api-auth-better-auth/principal";
|
||||
|
||||
const principal = mapBetterAuthUserToPrincipal({
|
||||
user: { id: "u_1", email: "a@b.c", roles: ["admin"] },
|
||||
session: { id: "s_1" },
|
||||
});
|
||||
// → { id: "u_1", scopes: ["admin"], claims: { id, email, roles, session_id } }
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import type { Principal } from "@suaveplan/api-types/principal";
|
||||
import type { PrincipalMapping } from "@suaveplan/api-auth-better-auth/options";
|
||||
|
||||
const customMapping: PrincipalMapping = ({ user, session }): Principal => ({
|
||||
id: `tenant_${session.activeOrganizationId}_${user.id}` as never,
|
||||
scopes: ((user.roles ?? []) as readonly string[]).flatMap((role) =>
|
||||
role === "admin"
|
||||
? ["billing:write", "billing:read", "users:write"]
|
||||
: [role]
|
||||
) as never[],
|
||||
claims: { email: user.email, source: "better-auth" },
|
||||
});
|
||||
|
||||
// Hand the custom mapping through BetterAuthOptions:
|
||||
// { ..., principalMapping: customMapping }
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| `mapBetterAuthUserToPrincipal(view)` | `PrincipalMapping` | Default mapping consumed by both factories |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The frozen-object return shape (`Object.freeze` on both the `Principal` and
|
||||
its `claims` bag) defends against handlers downstream mutating a single
|
||||
returned principal and observing the mutation on later requests — the same
|
||||
hardening applied by `@suaveplan/api-auth-static-principal`. The sensitive
|
||||
field list (`password`, `passwordHash`, `salt`, `twoFactorSecret`,
|
||||
`magicLinkToken`, `resetPasswordToken`) covers Better-Auth's stock plugin
|
||||
set; operators who define additional sensitive fields via a custom
|
||||
Better-Auth plugin SHOULD supply a custom `principalMapping` to drop those
|
||||
too. The mapping deliberately does NOT translate roles into a stable
|
||||
scope vocabulary — that's a deployment concern; the default is identity so
|
||||
the topology stays predictable.
|
||||
@@ -0,0 +1,110 @@
|
||||
# `create-provider`
|
||||
|
||||
## Purpose
|
||||
|
||||
Factory for the `auth.provider` half of The split. Returns an
|
||||
`AuthProviderAdapter` (per `@suaveplan/api-auth-provider`) that mounts the
|
||||
canonical `PROVIDER_ROUTE_SURFACE` (9 routes) under `routePrefix` (default
|
||||
`/auth`). Each handler internally rewrites the incoming `Request` onto the
|
||||
Better-Auth-native sub-path via `ROUTE_TRANSLATION` and delegates to
|
||||
`auth.handler(...)`. This indirection means downstream consumers of the
|
||||
provider see the canonical foundation surface — never Better-Auth's idioms —
|
||||
while Better-Auth still owns the precise wire response (cookies, 302
|
||||
redirects, error envelopes).
|
||||
|
||||
## Features
|
||||
|
||||
- `id: "better-auth"`; `pathPrefix` validated as `AuthPathPrefix`.
|
||||
- All 9 canonical route names registered:
|
||||
- `POST /register` → `POST /sign-up/email`
|
||||
- `POST /login` → `POST /sign-in/email`
|
||||
- `POST /logout` → `POST /sign-out`
|
||||
- `POST /refresh` → `GET /get-session`
|
||||
- `GET /oauth/:provider/start` → `POST /oauth2/sign-in/:provider`
|
||||
- `GET /oauth/:provider/callback` → `GET /oauth2/callback/:provider`
|
||||
- `GET /verify-email/:token` → `GET /verify-email`
|
||||
- `POST /forgot-password` → `POST /forget-password`
|
||||
- `POST /reset-password` → `POST /reset-password`
|
||||
- `routePrefix` collision-checked against
|
||||
`@suaveplan/api-host/reserved-prefix` — refused with
|
||||
`AUTH_BA.ROUTE_PREFIX_COLLISION` on collision.
|
||||
- Better-Auth instance materialised during `lifecycle.install` so DB
|
||||
unreachable errors fail before traffic.
|
||||
- OAuth flow conclusions emit `oauth_flows_total{provider,outcome}`.
|
||||
- Per-options `WeakMap` so the same Better-Auth instance is shared with
|
||||
`createBetterAuthSession(opts)` when wired with the same options object.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createBetterAuthProvider } from "@suaveplan/api-auth-better-auth";
|
||||
|
||||
const provider = createBetterAuthProvider({
|
||||
database: pool,
|
||||
baseUrl: "https://api.example.com",
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
});
|
||||
|
||||
// Mount via the host's plugin lifecycle:
|
||||
host.registerAuthProvider(provider);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createBetterAuthProvider } from "@suaveplan/api-auth-better-auth";
|
||||
import { createBetterAuthSession } from "@suaveplan/api-auth-better-auth";
|
||||
|
||||
// Operators MUST share the SAME options bag between the two factories to
|
||||
// share the Better-Auth instance (and thus the DB pool):
|
||||
const opts = {
|
||||
database: pool,
|
||||
baseUrl: "https://api.example.com",
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
routePrefix: "/api/auth",
|
||||
oauth: {
|
||||
providers: {
|
||||
github: {
|
||||
providerId: "github",
|
||||
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||
scopes: ["read:user", "user:email"],
|
||||
authorizationUrl: "https://github.com/login/oauth/authorize",
|
||||
tokenUrl: "https://github.com/login/oauth/access_token",
|
||||
userInfoUrl: "https://api.github.com/user",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
host.registerAuth({
|
||||
resolver: createBetterAuthSession(opts),
|
||||
csrf: true,
|
||||
});
|
||||
host.registerAuthProvider(createBetterAuthProvider(opts));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| `createBetterAuthProvider(opts)` | `(BetterAuthOptions) => AuthProviderAdapter` | Build the provider |
|
||||
| `ROUTE_TRANSLATION` | `Record<ProviderRouteName, NativeRoute>` | Re-exported for callers wanting to introspect the mapping |
|
||||
| `buildNativeUrl(args)` | function | Re-exported for advanced wrappers |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The provider's `routes(router, ctx)` mounts every canonical entry from
|
||||
`PROVIDER_ROUTE_SURFACE` with `auth: null` because the routes are mounted
|
||||
INSIDE the auth provider's own prefix and the routes themselves are the
|
||||
authentication flow — they MUST be reachable unauthenticated. The handler's
|
||||
inner rewrite reads `(ctx as { params })` because the abstract
|
||||
`RequestContext` doesn't declare `params`, but the request pipeline driver
|
||||
(`@suaveplan/api-host/request-pipeline-driver.ts`) attaches them at runtime.
|
||||
The cast is documented inline. The OAuth outcome heuristic maps status codes
|
||||
to labels — 2xx/3xx → `success`, 403 → `provider_rejected`, anything else
|
||||
→ `state_mismatch`. The semantic split between "state_mismatch" and
|
||||
"provider_rejected" is intentional even though a single 400 covers both
|
||||
wire-wise: the conformance suite distinguishes the two via the underlying
|
||||
mock IdP's `primeError` API. The per-options `WeakMap` is keyed by object
|
||||
identity, not deep equality — sharing requires literal same-object reuse.
|
||||
@@ -0,0 +1,75 @@
|
||||
# `route-translation`
|
||||
|
||||
## Purpose
|
||||
|
||||
Translation table mapping every entry in `PROVIDER_ROUTE_SURFACE` (the
|
||||
canonical 9-route foundation surface) to the matching Better-Auth-native
|
||||
sub-path. The wrapper presents the canonical names outward while Better-Auth
|
||||
still drives the heavy lifting at its idiomatic paths
|
||||
(`/sign-up/email`, `/sign-in/email`, `/oauth2/sign-in/:providerId`, ...).
|
||||
Without this translation a host built on the foundation contracts would
|
||||
need to know Better-Auth-specific URLs — which would defeat the purpose of
|
||||
the abstract `auth.provider` capability.
|
||||
|
||||
## Features
|
||||
|
||||
- Single immutable map keyed by `ProviderRouteName`.
|
||||
- Carries both `nativePath` and `nativeMethod` so the wrapper can rewrite
|
||||
the request method too (some translations flip GET → POST).
|
||||
- `buildNativeUrl` helper rewrites the canonical request URL onto
|
||||
Better-Auth's base URL, resolving `:provider` / `:token` params and
|
||||
preserving the query string.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { ROUTE_TRANSLATION, buildNativeUrl } from "@suaveplan/api-auth-better-auth";
|
||||
|
||||
const target = ROUTE_TRANSLATION.login;
|
||||
// → { nativePath: "/sign-in/email", nativeMethod: "POST" }
|
||||
|
||||
const url = buildNativeUrl({
|
||||
baseUrl: "https://api.example.com",
|
||||
nativePath: target.nativePath,
|
||||
params: {},
|
||||
search: "redirect=/home",
|
||||
});
|
||||
// → URL { https://api.example.com/sign-in/email?redirect=/home }
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { buildNativeUrl, ROUTE_TRANSLATION } from "@suaveplan/api-auth-better-auth";
|
||||
|
||||
// OAuth callback — the canonical `/oauth/:provider/callback?code=xxx&state=yyy`
|
||||
// is rewritten to Better-Auth's `/oauth2/callback/:provider?...` surface.
|
||||
const target = ROUTE_TRANSLATION["oauth.callback"];
|
||||
const native = buildNativeUrl({
|
||||
baseUrl: "https://api.example.com",
|
||||
nativePath: target.nativePath,
|
||||
params: { provider: "github" },
|
||||
search: "code=xxx&state=yyy",
|
||||
});
|
||||
// → URL { https://api.example.com/oauth2/callback/github?code=xxx&state=yyy }
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| `NativeRoute` | interface | `{ nativePath, nativeMethod }` |
|
||||
| `ROUTE_TRANSLATION` | `Readonly<Record<ProviderRouteName, NativeRoute>>` | Canonical → native map |
|
||||
| `buildNativeUrl(args)` | function | Rewrite the canonical URL onto Better-Auth's base URL |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`Object.freeze` on the outer record + a `Readonly<...>` cast — but not on
|
||||
the inner entries because each entry is already a frozen literal authored
|
||||
inline. Decided against `as const` on the outer block so the indexed key
|
||||
narrowing stays type-friendly under `noPropertyAccessFromIndexSignature`.
|
||||
`buildNativeUrl` runs `:param` substitution before constructing the URL so
|
||||
malformed param values can't break out of the path; `encodeURIComponent`
|
||||
defends against `/` injection in the `:provider` slot. Query string
|
||||
preservation is critical for OAuth — the entire upstream callback payload
|
||||
(`code`, `state`, `scope`, …) needs to land on Better-Auth's handler verbatim.
|
||||
@@ -0,0 +1,79 @@
|
||||
# `route-prefix`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Resolve an operator-supplied `routePrefix` against the host's reserved-prefix
|
||||
contract from `@suaveplan/api-host/reserved-prefix`. This guard ensures the
|
||||
Better-Auth provider cannot accidentally mount its account-lifecycle routes
|
||||
inside paths the host (or another first-party engine) reserves for itself —
|
||||
`/_host` (admin sub-router) and `/internal` (livez/readyz/metrics) today,
|
||||
plus whatever future entries land in `HOST_RESERVED_PREFIXES`. The check is
|
||||
performed at provider construction time so misconfiguration fails before the
|
||||
process is accepting traffic.
|
||||
|
||||
## Features
|
||||
|
||||
- Normalised prefix returned on success (lowercase, leading slash, no
|
||||
trailing slash) so downstream code can use it without re-normalising.
|
||||
- Collision detection covers both exact matches (`/internal`) and child
|
||||
paths (`/internal/livez`).
|
||||
- Reserved list NOT hard-coded — sourced from `@suaveplan/api-host` so the
|
||||
wrapper inherits future additions for free.
|
||||
- Throws `BetterAuthError(AUTH_BA.ROUTE_PREFIX_COLLISION)` with the
|
||||
normalised collision string in `context.prefix`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { assertRoutePrefixAvailable } from "@suaveplan/api-auth-better-auth";
|
||||
|
||||
const prefix = assertRoutePrefixAvailable("/auth"); // → "/auth"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { assertRoutePrefixAvailable } from "@suaveplan/api-auth-better-auth";
|
||||
import { BetterAuthError, AUTH_BA_CODES } from "@suaveplan/api-auth-better-auth/error-codes";
|
||||
|
||||
try {
|
||||
const prefix = assertRoutePrefixAvailable(operatorConfig.routePrefix);
|
||||
return mountProvider({ prefix });
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof BetterAuthError &&
|
||||
err.code === AUTH_BA_CODES.ROUTE_PREFIX_COLLISION
|
||||
) {
|
||||
// The deploy script can report the collision back to the operator
|
||||
// before traffic is accepted.
|
||||
reportFatalConfigError({ requested: operatorConfig.routePrefix, code: err.code });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| `assertRoutePrefixAvailable(prefix)` | `(string) => string` | Validates + returns normalised prefix |
|
||||
|
||||
The function throws `BetterAuthError(AUTH_BA.ROUTE_PREFIX_COLLISION)` when
|
||||
the prefix collides with `HOST_RESERVED_PREFIXES` from
|
||||
`@suaveplan/api-host/reserved-prefix`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The lazy `codeFactory` callback pattern from `@suaveplan/api-host`'s
|
||||
`assertPrefixAvailable` API is critical here — the factory is only invoked on
|
||||
collision, so the `BetterAuthError` allocation (with its stack trace and
|
||||
context bag) is skipped on the happy path. The reserved-prefix module also
|
||||
exports `normalisePrefix` separately; this wrapper calls it to compute the
|
||||
returned canonical form because `assertPrefixAvailable` returns `void`. This
|
||||
matches the pattern in `@suaveplan/api-plugin-rbac` and other adapter
|
||||
packages that need both the throw-on-collision behaviour and the normalised
|
||||
output value at the call site.
|
||||
@@ -0,0 +1,94 @@
|
||||
# `create-session`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Factory for the `auth.session` half of The split. Returns a
|
||||
`SessionResolver` that asks Better-Auth's `auth.api.getSession({ headers })`
|
||||
to materialise the current session and then maps the resulting `(user,
|
||||
session)` tuple onto a `Principal` per `@suaveplan/api-types`. Returns `null`
|
||||
when Better-Auth reports no session — the host's resolver chain advances to
|
||||
the next adapter (typically `api-auth-anonymous`).
|
||||
|
||||
## Features
|
||||
|
||||
- Implements the `SessionResolver` contract from `@suaveplan/api-auth`.
|
||||
- Pluggable `principalMapping` via `BetterAuthOptions.principalMapping`;
|
||||
default `mapBetterAuthUserToPrincipal`.
|
||||
- Lazy + cached Better-Auth instance — shared with
|
||||
`createBetterAuthProvider` when both factories receive the same options
|
||||
object (per-`opts` WeakMap).
|
||||
- OTel instruments emitted on every resolve: `sessions_resolved_total{outcome}`,
|
||||
`session_resolve_duration_ms{outcome}`,
|
||||
`principal_mapping_errors_total`.
|
||||
- Stable failure modes: `INVALID_SESSION`, `DB_UNREACHABLE`,
|
||||
`PRINCIPAL_MAPPING_FAILED`.
|
||||
- Disposable — `dispose()` is idempotent and turns subsequent `resolve()`
|
||||
calls into `null` returns.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createBetterAuthSession } from "@suaveplan/api-auth-better-auth";
|
||||
|
||||
const resolver = createBetterAuthSession({
|
||||
database: pool,
|
||||
baseUrl: "https://api.example.com",
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
});
|
||||
|
||||
// Inside the host's resolver chain:
|
||||
const principal = await resolver.resolve(request);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createBetterAuthSession } from "@suaveplan/api-auth-better-auth";
|
||||
import { SessionResolverChain } from "@suaveplan/api-auth/chain";
|
||||
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous";
|
||||
|
||||
const resolver = new SessionResolverChain({
|
||||
resolvers: [
|
||||
createBetterAuthSession(opts),
|
||||
createAnonymousResolver(),
|
||||
],
|
||||
});
|
||||
|
||||
// The Better-Auth resolver fires first; if it returns `null`, the
|
||||
// anonymous tail takes over and the host's `publicRoutes` policy applies.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| `createBetterAuthSession(opts)` | `(BetterAuthOptions) => SessionResolver` | Build the resolver |
|
||||
|
||||
The resolver throws `BetterAuthError` with one of:
|
||||
|
||||
- `AUTH_BA.INVALID_SESSION` — unexpected upstream error from Better-Auth's
|
||||
`getSession`.
|
||||
- `AUTH_BA.DB_UNREACHABLE` — first-resolve build failed (wraps the
|
||||
underlying connection error as `cause`).
|
||||
- `AUTH_BA.PRINCIPAL_MAPPING_FAILED` — `principalMapping` threw; a partial
|
||||
principal is never returned.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`getSession` is consumed via the structural `BetterAuthLike` view declared
|
||||
in `../options/options.types.ts`. That view is intentionally narrow — only
|
||||
`handler` and `api.getSession` are referenced — so the unit tests can stub
|
||||
Better-Auth without touching the real runtime. The per-options WeakMap
|
||||
(`lazyByOptions`) is the linchpin of the "share an instance between session
|
||||
and provider" guarantee: passing the same `opts` object to both factories
|
||||
threads through the same `LazyBetterAuth`. The OTel `finally` block runs on
|
||||
every code path so the counters reflect every attempt, including failures
|
||||
that propagate as `BetterAuthError`. `dispose()` resets the cached
|
||||
Better-Auth instance via `lazy.reset()` and flips a disposed flag that
|
||||
neutralises any subsequent `resolve()` call. The default Better-Auth factory
|
||||
dynamically imports `better-auth` so consumers wiring a `betterAuthFactory`
|
||||
override avoid the cost of loading the upstream package altogether.
|
||||
@@ -0,0 +1,90 @@
|
||||
# `testing`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`@suaveplan/api-auth-better-auth/testing` is the subpath downstream packages
|
||||
import when they want to assert their own wiring layer still meets the
|
||||
abstract `auth.session` and `auth.provider` contracts on top of this
|
||||
adapter. It re-exports both conformance harnesses
|
||||
(`runAuthSessionConformance` from `@suaveplan/api-auth` and
|
||||
`runAuthProviderConformance` from `@suaveplan/api-auth-provider`) alongside
|
||||
the two Better-Auth factories from this package, so a single import gets
|
||||
the consumer everything they need.
|
||||
|
||||
## Features
|
||||
|
||||
- Re-exports `runAuthSessionConformance` + `runAuthProviderConformance` from
|
||||
the contract packages so consumers don't have to add those packages to
|
||||
their own dependency closure.
|
||||
- Re-exports `createBetterAuthSession` + `createBetterAuthProvider`
|
||||
so the conformance call sites can drop the factory inline.
|
||||
- `AuthSessionConformanceFactory` / `AuthProviderConformanceFactory` /
|
||||
`AuthProviderConformanceOptions` type aliases for explicit typing.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
createBetterAuthProvider,
|
||||
runAuthProviderConformance,
|
||||
} from "@suaveplan/api-auth-better-auth/testing";
|
||||
|
||||
const results = runAuthProviderConformance({
|
||||
label: "my-host's Better-Auth wiring",
|
||||
factory: () => createBetterAuthProvider(myOptions),
|
||||
});
|
||||
for (const scenario of results) {
|
||||
it(`${scenario.category} :: ${scenario.name}`, () => {
|
||||
expect(scenario.passed).toBe(true);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
createBetterAuthSession,
|
||||
runAuthSessionConformance,
|
||||
type AuthSessionConformanceFactory,
|
||||
} from "@suaveplan/api-auth-better-auth/testing";
|
||||
|
||||
const spec: AuthSessionConformanceFactory = {
|
||||
label: "Postgres-backed Better-Auth + my custom mapping",
|
||||
factory: () => createBetterAuthSession(myOptionsWithCustomMapping),
|
||||
seedNull: () => new Request("https://api.example.com/anon"),
|
||||
seedPrincipal: () => ({
|
||||
request: new Request("https://api.example.com/me", {
|
||||
headers: { cookie: "better-auth.session=valid" },
|
||||
}),
|
||||
expected: knownPrincipal,
|
||||
}),
|
||||
};
|
||||
runAuthSessionConformance(spec);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Source | Purpose |
|
||||
| ------ | ------ | ------- |
|
||||
| `runAuthSessionConformance` | `@suaveplan/api-auth` | Session contract harness |
|
||||
| `runAuthProviderConformance` | `@suaveplan/api-auth-provider` | Provider contract harness |
|
||||
| `createBetterAuthSession` | this package | Session factory under test |
|
||||
| `createBetterAuthProvider` | this package | Provider factory under test |
|
||||
| `AuthSessionConformanceFactory` | type | Spec passed to session harness |
|
||||
| `AuthProviderConformanceFactory` | type | Factory signature for provider harness |
|
||||
| `AuthProviderConformanceOptions` | type | Options for provider harness |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The subpath is intentionally light — most of the value is just having one
|
||||
import path consumers can pull from. The conformance harnesses themselves
|
||||
live in the contract packages because that's where the abstract invariants
|
||||
are authored; this package's contribution to "passing conformance" is the
|
||||
factory pair. Consumers test their own wiring layer (custom middleware,
|
||||
host configuration, plugin order) by invoking the harnesses with their
|
||||
own options bag and asserting every `ConformanceScenarioResult.passed`.
|
||||
135
docs/api-stack/api/api-auth-jwt/README.md
Normal file
135
docs/api-stack/api/api-auth-jwt/README.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# @suaveplan/api-auth-jwt — concrete JWT session resolver for @suaveplan/api-auth. Verifies a JWT presented on the Authorization header (or a configurable cookie) using @suaveplan/jws against either a JWKS endpoint (TTL + negative-cache + in-flight dedup) or a single static JsonWebKey; projects validated claims onto a Principal via an overridable mapper. Returns null on parse/verify failure (chain semantics) and contributes a Bearer JWT OpenAPI security scheme.
|
||||
|
||||
Stage-4 concrete `SessionResolver` for the
|
||||
[`@suaveplan/api`](../api/README.md) plugin system. Verifies a JWT
|
||||
presented on the `Authorization: Bearer <jwt>` header (or a configured
|
||||
cookie) against either a remote JWKS endpoint or a single static JWK,
|
||||
then projects the validated claims onto the foundation's `Principal`
|
||||
contract via an overridable mapper.
|
||||
|
||||
This is the adapter you pair with hosted identity providers (Clerk,
|
||||
Auth0, Cognito, Stytch, custom Keycloak deployments) or with any
|
||||
self-hosted JWT-issuing provider. It is **session-resolver only** —
|
||||
login / register / refresh routes are the responsibility of an `auth`
|
||||
provider, not this package.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [claims-mapper](./src/claims-mapper/claims-mapper.md) — Default and runtime helpers for converting a verified JWT payload (`Claims`) into a `Principal` from `@suaveplan/api-ty…
|
||||
- [claims-mapper](./src/claims-mapper/claims-mapper.md) — Default and runtime helpers for converting a verified JWT payload (`Claims`) into a `Principal` from `@suaveplan/api-ty…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-jwt`, exposing the `AUTH_JWT.*` namespace via `defineCodes` from `@…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-jwt`, exposing the `AUTH_JWT.*` namespace via `defineCodes` from `@…
|
||||
- [jwks-cache](./src/jwks-cache/jwks-cache.md) — In-process JWKS cache for `@suaveplan/api-auth-jwt`. Fetches a JWKS document from a remote endpoint, holds it under a p…
|
||||
- [jwks-cache](./src/jwks-cache/jwks-cache.md) — In-process JWKS cache for `@suaveplan/api-auth-jwt`. Fetches a JWKS document from a remote endpoint, holds it under a p…
|
||||
- [options](./src/options/options.md) — Defines the Zod-validated boundary for `createJwtSessionResolver` and the companion `validateOptions` helper. The bound…
|
||||
- [options](./src/options/options.md) — Defines the Zod-validated boundary for `createJwtSessionResolver` and the companion `validateOptions` helper. The bound…
|
||||
- [resolver](./src/resolver/resolver.md) — Stage-4 concrete `SessionResolver` factory for the API plugin system. Reads a JWT from the request (Authorization h…
|
||||
- [resolver](./src/resolver/resolver.md) — Stage-4 concrete `SessionResolver` factory for the API plugin system. Reads a JWT from the request (Authorization h…
|
||||
- [security-scheme](./src/security-scheme/security-scheme.md) — Builds the `SecuritySchemeContribution` this adapter contributes to the generated OpenAPI document. The shape is the ca…
|
||||
- [security-scheme](./src/security-scheme/security-scheme.md) — Builds the `SecuritySchemeContribution` this adapter contributes to the generated OpenAPI document. The shape is the ca…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Surface
|
||||
|
||||
- [`./resolver`](src/resolver/resolver.md) — `createJwtSessionResolver(opts)`.
|
||||
- [`./jwks-cache`](src/jwks-cache/jwks-cache.md) — `JwksCache` with TTL,
|
||||
negative cache, in-flight dedup, `kid`-miss refresh.
|
||||
- [`./claims-mapper`](src/claims-mapper/claims-mapper.md) —
|
||||
`defaultClaimsToPrincipal` + `runClaimsMapper` helpers.
|
||||
- [`./security-scheme`](src/security-scheme/security-scheme.md) —
|
||||
`JWT_BEARER_SCHEME` and `createJwtBearerSecurityScheme(name)` for
|
||||
OpenAPI emission.
|
||||
- [`./error-codes`](src/error-codes/error-codes.md) — `AUTH_JWT_*` codes
|
||||
+ `AuthJwtError` class.
|
||||
- [`./options`](src/options/options.md) — Zod-validated options
|
||||
boundary.
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
|
||||
import { JWT_BEARER_SCHEME } from "@suaveplan/api-auth-jwt/security-scheme";
|
||||
|
||||
const resolver = createJwtSessionResolver({
|
||||
jwksUrl: "https://idp.example/.well-known/jwks.json",
|
||||
issuer: "https://idp.example",
|
||||
audience: "https://api.example",
|
||||
algorithms: ["RS256"],
|
||||
});
|
||||
|
||||
// Use in an api-host AuthAdapter:
|
||||
const adapter = {
|
||||
resolver,
|
||||
securityScheme: JWT_BEARER_SCHEME,
|
||||
};
|
||||
```
|
||||
|
||||
## Pairing with hosted IdPs
|
||||
|
||||
The package is designed to be paired with an external identity provider
|
||||
that already issues JWTs. The recipe is the same for every provider —
|
||||
plug in the provider's JWKS URL, issuer, and audience, then add a custom
|
||||
`claimsToPrincipal` when the provider uses non-standard claim names.
|
||||
|
||||
### Clerk
|
||||
|
||||
```ts
|
||||
createJwtSessionResolver({
|
||||
jwksUrl: "https://your-instance.clerk.accounts.dev/.well-known/jwks.json",
|
||||
issuer: "https://your-instance.clerk.accounts.dev",
|
||||
audience: "your-frontend-api",
|
||||
algorithms: ["RS256"],
|
||||
claimsToPrincipal: (claims) => {
|
||||
const sub = claims["sub"];
|
||||
if (typeof sub !== "string") return null;
|
||||
return {
|
||||
id: sub as never,
|
||||
scopes: [] as never[],
|
||||
tenantId: typeof claims["org_id"] === "string"
|
||||
? (claims["org_id"] as never)
|
||||
: undefined,
|
||||
claims,
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Auth0
|
||||
|
||||
```ts
|
||||
createJwtSessionResolver({
|
||||
jwksUrl: "https://your-tenant.auth0.com/.well-known/jwks.json",
|
||||
issuer: "https://your-tenant.auth0.com/",
|
||||
audience: "https://api.example.com",
|
||||
algorithms: ["RS256"],
|
||||
});
|
||||
```
|
||||
|
||||
### Cognito
|
||||
|
||||
```ts
|
||||
createJwtSessionResolver({
|
||||
jwksUrl: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXX/.well-known/jwks.json",
|
||||
issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXX",
|
||||
audience: "your-cognito-app-client-id",
|
||||
algorithms: ["RS256"],
|
||||
});
|
||||
```
|
||||
|
||||
## Semantics
|
||||
|
||||
`resolve()` returns `null` on any per-request soft failure (missing
|
||||
token, malformed token, bad signature, wrong issuer, wrong audience,
|
||||
expired, not-yet-valid, no matching JWKS key). That collapse lets the
|
||||
upstream `SessionResolverChain` fall through to the next adapter
|
||||
(anonymous, static-bearer, etc.). It throws only on infrastructure
|
||||
failures — `AUTH_JWT.JWKS_FETCH_FAILED` when the JWKS endpoint is
|
||||
unreachable, `AUTH_JWT.INVALID_KEY_FORMAT` for a malformed static key,
|
||||
`AUTH_JWT.INVALID_CLAIMS_MAPPER` when the caller's mapper throws or
|
||||
returns a non-Principal value.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see the repository.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# `claims-mapper` — `Claims → Principal` projection
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Default and runtime helpers for converting a verified JWT payload
|
||||
(`Claims`) into a `Principal` from `@suaveplan/api-types`. The default
|
||||
projection follows OAuth 2 / OIDC conventions: `sub` for subject,
|
||||
space-delimited `scope` (or array-form `scopes`) for authorisation
|
||||
scopes, and `tenant_id` / `tid` for the tenant. Callers can override
|
||||
the projection entirely to read non-standard claims (e.g. Clerk's
|
||||
`org_id`) or to enforce additional invariants — return `null` to defer
|
||||
to the next resolver in the chain.
|
||||
|
||||
## Features
|
||||
|
||||
- `defaultClaimsToPrincipal` — drop-in OAuth 2 / OIDC projection.
|
||||
- `runClaimsMapper` — wraps a caller-supplied mapper; catches throws,
|
||||
rejects non-Principal returns, surfaces both as
|
||||
`AUTH_JWT.INVALID_CLAIMS_MAPPER`.
|
||||
- Returns `null` when the token has no `sub` — that is the documented
|
||||
"not a user identity" path and lets the chain move on.
|
||||
- Brands `PrincipalId`, `Scope`, and `TenantId` via type assertion at
|
||||
the trust boundary; downstream consumers see opaque branded strings.
|
||||
- Passes the full claims record through as `principal.claims` so
|
||||
downstream tenant resolvers / authorisation rules can read raw values
|
||||
without a second token parse.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { defaultClaimsToPrincipal } from "@suaveplan/api-auth-jwt/claims-mapper";
|
||||
|
||||
const principal = defaultClaimsToPrincipal({
|
||||
sub: "user_123",
|
||||
scope: "billing:read profile:read",
|
||||
tenant_id: "acme",
|
||||
});
|
||||
// → { id: "user_123", scopes: ["billing:read", "profile:read"], tenantId: "acme", claims: {...} }
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Override the projection to read Clerk-style `org_id` and enforce a
|
||||
non-empty scopes list:
|
||||
|
||||
```ts
|
||||
import type { ClaimsToPrincipal } from "@suaveplan/api-auth-jwt/claims-mapper";
|
||||
|
||||
const clerkMapper: ClaimsToPrincipal = (claims) => {
|
||||
const sub = claims["sub"];
|
||||
const scope = claims["scope"];
|
||||
const org = claims["org_id"];
|
||||
if (typeof sub !== "string" || typeof scope !== "string") return null;
|
||||
return {
|
||||
id: sub as never,
|
||||
scopes: scope.split(" ") as never[],
|
||||
tenantId: typeof org === "string" ? (org as never) : undefined,
|
||||
claims,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ClaimsToPrincipal` — `(claims) => Principal | null`.
|
||||
- `defaultClaimsToPrincipal: ClaimsToPrincipal` — the built-in projection.
|
||||
- `runClaimsMapper(mapper, claims): Principal | null` — invoke a mapper
|
||||
with structured error reporting.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The default mapper deliberately tolerates either string-form (`"a b c"`)
|
||||
or array-form (`["a", "b", "c"]`) scopes — RFC 6749 §3.3 uses the
|
||||
former, while several IdPs (Auth0, Cognito) emit the latter. Empty
|
||||
strings are filtered out so a stray double-space does not produce a
|
||||
zero-length scope token. Tenant extraction prefers `tenant_id` (the
|
||||
Suaveplan-canonical name) over `tid` (the Microsoft / OIDC name) but
|
||||
accepts both. A mapper that throws is treated as an operator bug, not a
|
||||
caller error — surfacing it as a `null` would silently mask broken
|
||||
configuration.
|
||||
@@ -0,0 +1,76 @@
|
||||
# `error-codes` — `AUTH_JWT_*` stable codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Stable error-code registry for `@suaveplan/api-auth-jwt`, exposing the
|
||||
`AUTH_JWT.*` namespace via `defineCodes` from `@suaveplan/error` and a
|
||||
matching `AuthJwtError` subclass of `SuaveplanError`. The registry lists
|
||||
only the codes the package throws — namely the infrastructure failures an
|
||||
operator must act on. Per-request soft failures (bad token, expired
|
||||
signature, wrong audience, etc.) deliberately do not get codes; they
|
||||
collapse to `null` so that the upstream `SessionResolverChain` can move on
|
||||
to the next resolver, which is the documented chain semantic.
|
||||
|
||||
## Features
|
||||
|
||||
- `AUTH_JWT_CODES.JWKS_FETCH_FAILED` — JWKS endpoint unreachable or
|
||||
returned a non-2xx after the configured retries, and there is no cached
|
||||
set old enough to use. Operators see this in logs / metrics; callers
|
||||
see a 401.
|
||||
- `AUTH_JWT_CODES.INVALID_KEY_FORMAT` — `options.staticKey` failed JWK
|
||||
schema validation at construction time.
|
||||
- `AUTH_JWT_CODES.INVALID_CLAIMS_MAPPER` — `options.claimsToPrincipal`
|
||||
threw or returned a value that is not a `Principal` or `null`.
|
||||
- `AuthJwtError` carries the matching `AuthJwtErrorCode`, integrates with
|
||||
the `SuaveplanError` cause-chain, redaction, and OTel span-event
|
||||
adapters from `@suaveplan/error`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AUTH_JWT_CODES, AuthJwtError } from "@suaveplan/api-auth-jwt/error-codes";
|
||||
|
||||
try {
|
||||
// ... resolver internals ...
|
||||
} catch (cause) {
|
||||
throw new AuthJwtError(
|
||||
AUTH_JWT_CODES.JWKS_FETCH_FAILED,
|
||||
"JWKS endpoint unreachable",
|
||||
{ cause }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Branch on the code at the boundary to map infrastructure failures to
|
||||
operator alerts rather than user-facing 401s:
|
||||
|
||||
```ts
|
||||
import { AUTH_JWT_CODES, AuthJwtError } from "@suaveplan/api-auth-jwt/error-codes";
|
||||
|
||||
function isOperatorActionable(err: unknown): err is AuthJwtError {
|
||||
if (!(err instanceof AuthJwtError)) return false;
|
||||
return err.code === AUTH_JWT_CODES.JWKS_FETCH_FAILED;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AUTH_JWT_CODES` — frozen record produced by `defineCodes("AUTH_JWT", [...])`.
|
||||
- `AuthJwtErrorCode` — union of the three string literals.
|
||||
- `AuthJwtError extends SuaveplanError<AuthJwtErrorCode>` — `jwtCode`
|
||||
mirrors `code` for ergonomic `instanceof` narrowing.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The three-code registry is intentionally small. Adding "soft" failure
|
||||
codes here would tempt consumers to branch on them, defeating the chain
|
||||
semantic that `null` means "not my caller". If a richer breakdown is
|
||||
needed downstream, emit it on the failure counter (`reason` attribute)
|
||||
rather than as a separate `AUTH_JWT.*` code. Registry membership is the
|
||||
public contract; only add codes through an OpenSpec change.
|
||||
91
docs/api-stack/api/api-auth-jwt/src/jwks-cache/jwks-cache.md
Normal file
91
docs/api-stack/api/api-auth-jwt/src/jwks-cache/jwks-cache.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# `jwks-cache` — TTL-bounded JWKS resolution
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
In-process JWKS cache for `@suaveplan/api-auth-jwt`. Fetches a JWKS
|
||||
document from a remote endpoint, holds it under a positive TTL,
|
||||
negative-caches transient failures, dedupes concurrent fetches into a
|
||||
single network request, and supports forced refresh on a `kid` miss
|
||||
(the standard "the IdP rotated keys" recovery path). Throws
|
||||
`AUTH_JWT.JWKS_FETCH_FAILED` only when neither a fresh fetch nor a stale
|
||||
cached document is available — under partial outages we prefer
|
||||
stale-but-valid keys to a 401 storm.
|
||||
|
||||
## Features
|
||||
|
||||
- Positive TTL (default 10 minutes) with monotonic clock injection.
|
||||
- Negative cache window (default 30 seconds) on fetch failure — within
|
||||
the window callers see the stale document if any, otherwise the
|
||||
error.
|
||||
- In-flight dedup: concurrent `getKeyForKid` callers share a single
|
||||
network round-trip.
|
||||
- `forceRefresh()` for operator-triggered rotation.
|
||||
- `kid` miss against a cached document triggers exactly one refresh.
|
||||
- JSON shape validation (`{ keys: object[] }`) at the boundary.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { JwksCache } from "@suaveplan/api-auth-jwt/jwks-cache";
|
||||
|
||||
const cache = new JwksCache({
|
||||
url: "https://idp.example/.well-known/jwks.json",
|
||||
ttlMs: 10 * 60 * 1000,
|
||||
negativeMs: 30 * 1000,
|
||||
clock: { nowMs: () => Date.now() },
|
||||
fetchImpl: fetch,
|
||||
});
|
||||
|
||||
const jwk = await cache.getKeyForKid("kid-2024-01");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Inject a fake clock and a controllable fetch impl for tests of the
|
||||
expiry and dedup behaviours:
|
||||
|
||||
```ts
|
||||
let now = 0;
|
||||
const calls: number[] = [];
|
||||
const cache = new JwksCache({
|
||||
url: "https://idp.example/.well-known/jwks.json",
|
||||
ttlMs: 1000,
|
||||
negativeMs: 100,
|
||||
clock: { nowMs: () => now },
|
||||
fetchImpl: async () => {
|
||||
calls.push(now);
|
||||
return new Response(JSON.stringify({ keys: [{ kid: "a", kty: "RSA", n: "...", e: "AQAB" }] }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
},
|
||||
});
|
||||
await cache.getKeyForKid("a");
|
||||
now = 500;
|
||||
await cache.getKeyForKid("a"); // cache hit, no second fetch
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class JwksCache` — constructor takes `{ url, ttlMs, negativeMs, clock,
|
||||
fetchImpl }`.
|
||||
- `getKeyForKid(kid?: string): Promise<Record<string, unknown> | undefined>`
|
||||
— fetch-as-needed, returns `undefined` when no key matches.
|
||||
- `forceRefresh(): Promise<JwksDocument>` — clear cache and refetch.
|
||||
- `JwksDocument` — `{ keys: ReadonlyArray<Record<string, unknown>> }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
When the token carries no `kid`, the cache only returns a key when the
|
||||
JWKS contains exactly one entry — ambiguous lookups must be a 401, not
|
||||
a guess. The negative cache window is short by design (30s default);
|
||||
longer windows accumulate authentication outages, shorter windows
|
||||
hammer the IdP. The in-flight dedup uses a single `Promise` field
|
||||
rather than a queue — the second concurrent caller awaits the same
|
||||
promise as the first. The "kid miss after a just-fetched document"
|
||||
guard prevents infinite refresh loops when the IdP legitimately does
|
||||
not have a matching key.
|
||||
82
docs/api-stack/api/api-auth-jwt/src/options/options.md
Normal file
82
docs/api-stack/api/api-auth-jwt/src/options/options.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# `options` — `JwtSessionResolverOptions` boundary
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the Zod-validated boundary for `createJwtSessionResolver` and the
|
||||
companion `validateOptions` helper. The boundary enforces every static
|
||||
contract the runtime depends on: exactly one of `jwksUrl` or `staticKey`,
|
||||
non-empty `algorithms` allowlist, mandatory `issuer` and `audience`, a
|
||||
positive cache TTL, and a non-negative negative-cache window. Anything
|
||||
the schema cannot encode (injectable clock, fetch impl, claims mapper
|
||||
function) is part of the wider `JwtSessionResolverOptions` TypeScript
|
||||
type but is not Zod-validated — those are dev-time contracts.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict-mode object schema rejects unknown keys at construction.
|
||||
- Mutual exclusion check for `jwksUrl` / `staticKey` — both supplied or
|
||||
neither supplied throws `AUTH_JWT.INVALID_KEY_FORMAT`.
|
||||
- Sensible defaults: header name `authorization`, JWKS TTL 10 min,
|
||||
negative cache 30s, clock tolerance 0.
|
||||
- Accepts `issuer` and `audience` as either a single string or a
|
||||
non-empty list of strings — JWS verify enforces membership.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { validateOptions } from "@suaveplan/api-auth-jwt/options";
|
||||
|
||||
const opts = validateOptions({
|
||||
jwksUrl: "https://idp.example/.well-known/jwks.json",
|
||||
issuer: "https://idp.example",
|
||||
audience: "https://api.example",
|
||||
algorithms: ["RS256"],
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Custom header, cookie fallback, tighter cache TTL, and a custom claims
|
||||
mapper:
|
||||
|
||||
```ts
|
||||
import { validateOptions } from "@suaveplan/api-auth-jwt/options";
|
||||
|
||||
const opts = validateOptions({
|
||||
staticKey: { kty: "oct", k: "..." },
|
||||
issuer: ["https://idp1.example", "https://idp2.example"],
|
||||
audience: "https://api.example",
|
||||
algorithms: ["HS256"],
|
||||
headerName: "x-api-token",
|
||||
cookieName: "sp_session",
|
||||
clockToleranceSec: 5,
|
||||
jwksCacheTtlMs: 60_000,
|
||||
jwksNegativeCacheMs: 5_000,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `JwtSessionResolverOptionsSchema` — Zod object schema.
|
||||
- `JwtSessionResolverOptions` — public surface (includes non-serialisable
|
||||
fields like `clock`, `fetchImpl`, `claimsToPrincipal`).
|
||||
- `ValidatedJwtSessionResolverOptions` — inferred shape after defaults
|
||||
are applied.
|
||||
- `validateOptions(raw)` — synchronous validator; throws `AuthJwtError`
|
||||
with code `AUTH_JWT.INVALID_KEY_FORMAT` on any boundary failure.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema is intentionally `.strict()` so a typo in a key name fails
|
||||
loudly at construction rather than silently activating defaults. `iss`
|
||||
and `aud` are both required because token-level `iss` / `aud` confusion
|
||||
is a documented attack vector — every Stage-4 deployment is expected to
|
||||
pin both. `algorithms` is also mandatory and non-empty — `@suaveplan/jws`
|
||||
already refuses to verify without an allowlist, but we re-enforce it at
|
||||
the API boundary so the error surface is consistent. The mutual
|
||||
exclusion check after parsing matches what the spec calls out: a single
|
||||
key source is the only way to reason about JWKS cache invalidation.
|
||||
108
docs/api-stack/api/api-auth-jwt/src/resolver/resolver.md
Normal file
108
docs/api-stack/api/api-auth-jwt/src/resolver/resolver.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# `resolver` — `createJwtSessionResolver`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Stage-4 concrete `SessionResolver` factory for the API plugin
|
||||
foundation. Reads a JWT from the request (Authorization header or
|
||||
configurable cookie), verifies it via `@suaveplan/jws` against either a
|
||||
JWKS endpoint (cached and refreshed on `kid` miss) or a single static
|
||||
JWK, validates `iss`, `aud`, `exp`, `nbf` against the configured policy,
|
||||
and projects the validated claims onto a `Principal` via an overridable
|
||||
mapper. The factory is the only public entry point — internals
|
||||
(`JwksCache`, claims mapper, error registry) are surface-exported for
|
||||
testability but most consumers use `createJwtSessionResolver(opts)` alone.
|
||||
|
||||
## Features
|
||||
|
||||
- `Authorization: Bearer <jwt>` header extraction with case-insensitive
|
||||
scheme match; configurable header name accepts a raw token form for
|
||||
non-standard headers (e.g. `x-api-token: <jwt>`).
|
||||
- Cookie fallback when `cookieName` is supplied; header still wins when
|
||||
both are present.
|
||||
- JWKS resolution via `JwksCache` with positive TTL, negative cache,
|
||||
in-flight dedup, and `kid`-miss refresh.
|
||||
- Static-key path for HS-family symmetric tokens or single-key
|
||||
deployments.
|
||||
- Mandatory `iss` and `aud` enforcement (single string or list); `exp`
|
||||
and `nbf` enforced when present with configurable `clockToleranceSec`.
|
||||
- Per-request soft failures collapse to `null` so the upstream
|
||||
`SessionResolverChain` can fall through to the next adapter; operator
|
||||
failures (JWKS unreachable, malformed mapper, malformed static key)
|
||||
surface as `AuthJwtError` with the documented codes.
|
||||
- Idempotent `dispose()` flips an internal flag so subsequent
|
||||
`resolve()` calls return `null` rather than emitting late traffic on
|
||||
a torn-down resolver.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
|
||||
|
||||
const resolver = createJwtSessionResolver({
|
||||
jwksUrl: "https://idp.example/.well-known/jwks.json",
|
||||
issuer: "https://idp.example",
|
||||
audience: "https://api.example",
|
||||
algorithms: ["RS256"],
|
||||
});
|
||||
|
||||
const req = new Request("https://api.example/users", {
|
||||
headers: { authorization: "Bearer eyJ..." },
|
||||
});
|
||||
const principal = await resolver.resolve(req);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Static HS256 key, cookie fallback, custom claims mapper, and an
|
||||
injectable clock for deterministic temporal-claim tests:
|
||||
|
||||
```ts
|
||||
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
|
||||
|
||||
const resolver = createJwtSessionResolver({
|
||||
staticKey: { kty: "oct", k: "<base64url-secret>" },
|
||||
issuer: ["https://idp1.example", "https://idp2.example"],
|
||||
audience: "https://api.example",
|
||||
algorithms: ["HS256"],
|
||||
cookieName: "sp_session",
|
||||
clockToleranceSec: 5,
|
||||
clock: { nowMs: () => 1_700_000_000_000 },
|
||||
claimsToPrincipal: (claims) => {
|
||||
const sub = claims["sub"];
|
||||
if (typeof sub !== "string") return null;
|
||||
return {
|
||||
id: sub as never,
|
||||
scopes: [] as never[],
|
||||
tenantId: typeof claims["org_id"] === "string"
|
||||
? (claims["org_id"] as never)
|
||||
: undefined,
|
||||
claims,
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createJwtSessionResolver(opts: JwtSessionResolverOptions): SessionResolver`
|
||||
— returns the `@suaveplan/api-auth` `SessionResolver` shape (`resolve` +
|
||||
idempotent `dispose`).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The resolver deliberately separates "this is not my caller" (chain
|
||||
falls through, returns `null`) from "the host is mis-configured"
|
||||
(throws `AuthJwtError`). Verification failures inside `@suaveplan/jws`
|
||||
return `null` so a stray Bearer header from an unrelated IdP cannot
|
||||
401-storm the API — the chain's next adapter (anonymous, static key,
|
||||
etc.) still gets its turn. The temporal-claim check uses a single
|
||||
clock reading per request to avoid TOCTOU between the `exp` and `nbf`
|
||||
arms. We re-implement `iss`/`aud`/`exp`/`nbf` enforcement here rather
|
||||
than push it into `@suaveplan/jws` because those are JWT-claim
|
||||
concerns, not JWS-protected-header concerns — the JWS package
|
||||
deliberately handles only RFC 7515 (signature) and leaves RFC 7519
|
||||
(payload claims) to its consumers.
|
||||
@@ -0,0 +1,66 @@
|
||||
# `security-scheme` — OpenAPI Bearer JWT contribution
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Builds the `SecuritySchemeContribution` this adapter contributes to the
|
||||
generated OpenAPI document. The shape is the canonical OAS 3.1
|
||||
`http` / `bearer` scheme with `bearerFormat: "JWT"`. Stage-8
|
||||
`@suaveplan/api-openapi` consumes the contribution and inlines it under
|
||||
`components.securitySchemes[name]`, then references it from operation
|
||||
`security` blocks for non-public routes.
|
||||
|
||||
## Features
|
||||
|
||||
- Canonical Bearer-JWT scheme — no surprise extensions, no proprietary
|
||||
fields.
|
||||
- Configurable contribution name (`bearerAuth` default) so multiple JWT
|
||||
resolvers can coexist (e.g. user-side `bearerAuth` + service-side
|
||||
`serviceBearerAuth`).
|
||||
- `JWT_BEARER_SCHEME` constant for callers that only ever expose one
|
||||
JWT resolver.
|
||||
- Human-readable `description` field that links the scheme back to the
|
||||
adapter so docs readers understand where verification happens.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { JWT_BEARER_SCHEME } from "@suaveplan/api-auth-jwt/security-scheme";
|
||||
|
||||
const adapter = {
|
||||
resolver: createJwtSessionResolver(...).resolver,
|
||||
securityScheme: JWT_BEARER_SCHEME,
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Rename the contribution for a dual-resolver host:
|
||||
|
||||
```ts
|
||||
import { createJwtBearerSecurityScheme } from "@suaveplan/api-auth-jwt/security-scheme";
|
||||
|
||||
const userScheme = createJwtBearerSecurityScheme("userAuth");
|
||||
const serviceScheme = createJwtBearerSecurityScheme("serviceAuth");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createJwtBearerSecurityScheme(name = "bearerAuth")` →
|
||||
`SecuritySchemeContribution` from `@suaveplan/api-auth`.
|
||||
- `JWT_BEARER_SCHEME` — pre-built `bearerAuth`-named contribution.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `http` / `bearer` form is the only OAS 3.1 scheme that documents
|
||||
"this endpoint accepts a Bearer JWT" without forcing the OpenAPI
|
||||
emitter to inline OAuth-flow metadata that this adapter does not own.
|
||||
The `bearerFormat` field is documentation-only per the spec, but
|
||||
populating it as `"JWT"` is the convention every OAS reader (Swagger
|
||||
UI, Redoc, Stoplight) recognises. Tenant or scope expectations are
|
||||
expressed by individual route handlers, not the scheme itself —
|
||||
keeping the scheme orthogonal to the route catalogue keeps it stable
|
||||
across plugin upgrades.
|
||||
68
docs/api-stack/api/api-auth-provider/README.md
Normal file
68
docs/api-stack/api/api-auth-provider/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# @suaveplan/api-auth-provider - Abstract auth-provider contract for the Suaveplan API plugin system. Defines AuthProviderAdapter, the canonical provider route surface, path-ownership registry, collision detector, the auth.provider CapabilityId, the AUTH_PROVIDER error-code registry, and the provider conformance harness. $2; concrete providers (better-auth, rolled) ship in Stages 6 and 8.
|
||||
|
||||
Abstract account-lifecycle contract for the Suaveplan API plugin system
|
||||
(contracts). This package is OPTIONAL: hosted-IdP deployments
|
||||
(Clerk, Auth0, Cognito) install only the session resolver from
|
||||
`@suaveplan/api-auth`; self-hosted deployments install exactly one provider
|
||||
implementation (`api-auth-better-auth` , `api-auth-rolled` in
|
||||
Stage 8) which structurally implements `AuthProviderAdapter` from this
|
||||
package.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [capability-id](./src/capability-id/capability-id.md) — The `capability-id` module pins the runtime literal for the `"auth.provider"` capability this package contributes to th…
|
||||
- [capability-id](./src/capability-id/capability-id.md) — The `capability-id` module pins the runtime literal for the `"auth.provider"` capability this package contributes to th…
|
||||
- [collision-detector](./src/collision-detector/collision-detector.md) — `collision-detector` is the install-time guard that prevents a plugin from accidentally squatting on the auth-provider'…
|
||||
- [collision-detector](./src/collision-detector/collision-detector.md) — `collision-detector` is the install-time guard that prevents a plugin from accidentally squatting on the auth-provider'…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module centralises the four stable error codes the auth-provider contract emits — `PATH_COLLISION`, `MULTIPLE_PROV…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module centralises the four stable error codes the auth-provider contract emits — `PATH_COLLISION`, `MULTIPLE_PROV…
|
||||
- [path-ownership](./src/path-ownership/path-ownership.md) — `path-ownership` is the runtime registry the host consults to enforce the "exactly one auth provider per deployment" in…
|
||||
- [path-ownership](./src/path-ownership/path-ownership.md) — `path-ownership` is the runtime registry the host consults to enforce the "exactly one auth provider per deployment" in…
|
||||
- [provider](./src/provider/provider.md) — The `provider` module is the type-level + thin-runtime heart of `@suaveplan/api-auth-provider`. It declares the abstrac…
|
||||
- [provider](./src/provider/provider.md) — The `provider` module is the type-level + thin-runtime heart of `@suaveplan/api-auth-provider`. It declares the abstrac…
|
||||
- [route-surface](./src/route-surface/route-surface.md) — `route-surface` declares the closed set of paths an auth provider OWNS relative to its `pathPrefix`. The set is the uni…
|
||||
- [route-surface](./src/route-surface/route-surface.md) — `route-surface` declares the closed set of paths an auth provider OWNS relative to its `pathPrefix`. The set is the uni…
|
||||
- [testing](./src/testing/conformance.md) — `runAuthProviderConformance` is the runner-agnostic harness every concrete `AuthProviderAdapter` MUST clear. It exercis…
|
||||
- [conformance](./src/testing/conformance.md) — `runAuthProviderConformance` is the runner-agnostic harness every concrete `AuthProviderAdapter` MUST clear. It exercis…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## What ships here
|
||||
|
||||
- `AuthProviderAdapter` contract — `id`, `pathPrefix`,
|
||||
`routes(router, ctx)`, four-hook lifecycle.
|
||||
- `AuthPathPrefix` brand + Zod-validated `authPathPrefix` constructor.
|
||||
- `PROVIDER_ROUTE_SURFACE` — the canonical 9-entry route table providers
|
||||
project from.
|
||||
- `PathOwnership` runtime registry — enforces "exactly one provider"
|
||||
install-time invariant.
|
||||
- `detectAuthPathCollisions` — install-time guard that plugin routes do not
|
||||
squat on the provider's owned prefix.
|
||||
- `AUTH_PROVIDER_CAPABILITY_ID` constant — `"auth.provider"`.
|
||||
- `AUTH_PROVIDER_CODES` — the four-code error registry plus
|
||||
`AuthProviderError` and the `authProviderError` factory.
|
||||
- `runAuthProviderConformance` — runner-agnostic harness every concrete
|
||||
provider clears.
|
||||
|
||||
## Module index
|
||||
|
||||
- [provider](src/provider/provider.md)
|
||||
- [route-surface](src/route-surface/route-surface.md)
|
||||
- [path-ownership](src/path-ownership/path-ownership.md)
|
||||
- [collision-detector](src/collision-detector/collision-detector.md)
|
||||
- [capability-id](src/capability-id/capability-id.md)
|
||||
- [error-codes](src/error-codes/error-codes.md)
|
||||
- [testing/conformance](src/testing/conformance.md)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `@suaveplan/api-types` (workspace) — `AuthProviderAdapter` consumes
|
||||
`Router`, `PluginContext`, `CapabilityId`, `CapabilityRequirements`.
|
||||
- `@suaveplan/error` (workspace) — `SuaveplanError` + `defineCodes`.
|
||||
- `zod` (catalog) — `AuthPathPrefixSchema`.
|
||||
- `tslib` (catalog) — emitter helpers.
|
||||
|
||||
## Stage / status
|
||||
|
||||
Concrete providers ship in Stages 6 / 8.
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# `capability-id` — `"auth.provider"` capability contribution
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `capability-id` module pins the runtime literal for the
|
||||
`"auth.provider"` capability this package contributes to the host's
|
||||
capability registry. The closed `CapabilityId` union in
|
||||
`@suaveplan/api-types` already includes `"auth.provider"` (alongside
|
||||
`"auth.session"`); this module simply exposes a typed constant and predicate
|
||||
so concrete providers, the host's capability resolver, and downstream
|
||||
plugin manifests can author against a single value rather than a free-form
|
||||
string. Centralising the literal gives us one place to grep if the id ever
|
||||
needs to evolve via an OpenSpec change.
|
||||
|
||||
## Features
|
||||
|
||||
- `AUTH_PROVIDER_CAPABILITY_ID` constant pinned to `"auth.provider"` via a
|
||||
`satisfies` clause that fails the build if the literal drifts out of the
|
||||
`CapabilityId` union.
|
||||
- `isAuthProviderCapabilityId(value)` runtime type-guard for capability
|
||||
descriptors that flow through `unknown`.
|
||||
- `AuthProviderCapabilityId` type alias — `Extract<CapabilityId, "auth.provider">` —
|
||||
so consumers can name the type without re-spelling the literal.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AUTH_PROVIDER_CAPABILITY_ID } from "@suaveplan/api-auth-provider/capability-id";
|
||||
|
||||
definePlugin({
|
||||
requires: [AUTH_PROVIDER_CAPABILITY_ID] as const,
|
||||
start: async (ctx) => {
|
||||
// ctx.capabilities["auth.provider"] is now in scope.
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
The host's capability resolver inspects untyped descriptors and narrows them
|
||||
to the typed slot via the predicate:
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUTH_PROVIDER_CAPABILITY_ID,
|
||||
isAuthProviderCapabilityId,
|
||||
} from "@suaveplan/api-auth-provider/capability-id";
|
||||
|
||||
function bindCapability(descriptor: { id: string }) {
|
||||
if (isAuthProviderCapabilityId(descriptor.id)) {
|
||||
// descriptor.id is now typed as "auth.provider".
|
||||
registerSlot(AUTH_PROVIDER_CAPABILITY_ID, descriptor);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `const AUTH_PROVIDER_CAPABILITY_ID: AuthProviderCapabilityId` — frozen
|
||||
literal.
|
||||
- `function isAuthProviderCapabilityId(value: string): value is AuthProviderCapabilityId`
|
||||
- `type AuthProviderCapabilityId = Extract<CapabilityId, "auth.provider">`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `satisfies` clause is the load-bearing line of the module — if the
|
||||
`CapabilityId` union ever loses `"auth.provider"`, the build fails here
|
||||
rather than downstream. The predicate uses an equality test against the
|
||||
constant rather than the literal to keep the two in lockstep — if a future
|
||||
change renames the id, the constant and predicate move together. There is
|
||||
intentionally no companion type-augmentation block here; the union in
|
||||
`@suaveplan/api-types` is closed and pre-includes every capability the host
|
||||
knows about, so adding a capability is a co-located OpenSpec change to that
|
||||
package and not a downstream module-augmentation pattern.
|
||||
@@ -0,0 +1,88 @@
|
||||
# `collision-detector` — install-time auth-path collision check
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`collision-detector` is the install-time guard that prevents a plugin from
|
||||
accidentally squatting on the auth-provider's owned path prefix. It walks
|
||||
every plugin route, segment-aligns its path against the provider's
|
||||
`pathPrefix`, and emits `AUTH_PROVIDER.PATH_COLLISION` entries for every
|
||||
overlap that is not on the plugin's `publicRoutes` allowlist or marked
|
||||
`isPublic` on the per-route descriptor. Returning a report instead of
|
||||
throwing keeps callers in control of how to surface aggregated failures —
|
||||
typically the host turns a non-empty report into a single error listing
|
||||
every offender.
|
||||
|
||||
## Features
|
||||
|
||||
- Segment-aware containment — `/auth` matches `/auth` and `/auth/login` but
|
||||
NOT `/authoritative`.
|
||||
- Configurable prefix — a deployment that mounts the provider under
|
||||
`/myauth` shifts the collision domain accordingly.
|
||||
- Per-route `isPublic` opt-out for plugin routes that augment the auth
|
||||
surface (e.g. a custom `/auth/healthz`).
|
||||
- Plugin-level `publicRoutes` allowlist — identical effect, declared once
|
||||
per plugin manifest.
|
||||
- Aggregating reporter — every offender is returned in one frozen report.
|
||||
- Trailing-slash normalisation — `/auth` and `/auth/` are treated as equal.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { detectAuthPathCollisions } from "@suaveplan/api-auth-provider/collision-detector";
|
||||
|
||||
const report = detectAuthPathCollisions({ provider, plugins });
|
||||
if (report.hasCollisions) {
|
||||
throw new Error(`Auth-path collisions: ${report.entries.length}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Aggregating a problem-detail response across every offender:
|
||||
|
||||
```ts
|
||||
import {
|
||||
detectAuthPathCollisions,
|
||||
type CollisionReportEntry,
|
||||
} from "@suaveplan/api-auth-provider/collision-detector";
|
||||
|
||||
const report = detectAuthPathCollisions({ provider, plugins });
|
||||
if (report.hasCollisions) {
|
||||
const offenders = report.entries.map(
|
||||
(e: CollisionReportEntry) => ({ plugin: e.details.plugin, path: e.details.path })
|
||||
);
|
||||
return new Response(
|
||||
JSON.stringify({ type: "auth-collision", offenders }),
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `function detectAuthPathCollisions(input: DetectCollisionsInput): CollisionReport`
|
||||
- `function isInsidePrefix(routePath: string, prefix: string): boolean`
|
||||
- `function normalisePrefix(prefix: string): string`
|
||||
- `interface DetectCollisionsInput { provider; plugins }`
|
||||
- `interface PluginDescriptor { name; routes; publicRoutes? }`
|
||||
- `interface PluginRouteDescriptor { path; method?; isPublic? }`
|
||||
- `interface CollisionReport { entries; hasCollisions }`
|
||||
- `interface CollisionReportEntry { code; message; details }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The detector returns a non-throwing report so the host can decide whether a
|
||||
collision is fatal or merely loggable — empirically a deployment running an
|
||||
older plugin against a newer provider needs the choice. Segment-aware
|
||||
matching is essential: a naïve `startsWith` would falsely flag
|
||||
`/authoritative` (or worse, `/auth-cache`) when the provider owns `/auth`.
|
||||
The root prefix `"/"` is short-circuited to "owns nothing" so a deployment
|
||||
that mistakenly configures the auth provider at the root does not blow up
|
||||
the whole route table; the path-prefix validator catches that mis-configuration
|
||||
at construction time. Two opt-out paths (per-route `isPublic` and plugin
|
||||
`publicRoutes`) coexist because manifests historically use one or the other
|
||||
and supporting both keeps migration tractable.
|
||||
@@ -0,0 +1,91 @@
|
||||
# `error-codes` — `AUTH_PROVIDER` error-code registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module centralises the four stable error codes the auth-provider
|
||||
contract emits — `PATH_COLLISION`, `MULTIPLE_PROVIDERS_REGISTERED`,
|
||||
`PROVIDER_LIFECYCLE_FAILED`, `OAUTH_CALLBACK_INVALID_STATE` — and pairs each
|
||||
with HTTP / severity / retry metadata so downstream consumers (problem-detail
|
||||
mappers, log routers, retry wrappers) classify failures without
|
||||
string-matching messages. The registry is built with `defineCodes` from
|
||||
`@suaveplan/error`, which preserves the literal-typed union through the
|
||||
public surface so exhaustive `switch (err.code)` blocks report unhandled
|
||||
reasons at type-check time.
|
||||
|
||||
## Features
|
||||
|
||||
- `AUTH_PROVIDER_CODES` — frozen `{ REASON: "AUTH_PROVIDER.REASON" }` record.
|
||||
- `AuthProviderErrorCode` union of fully-qualified codes.
|
||||
- `AuthProviderErrorKey` union of short keys (`"PATH_COLLISION"`).
|
||||
- `AUTH_PROVIDER_ERROR_METADATA` — per-code HTTP status / severity / retryable
|
||||
flag / human description.
|
||||
- `AuthProviderError` — concrete `SuaveplanError` subclass with a public
|
||||
constructor.
|
||||
- `authProviderError(key, opts)` — factory that defaults `messageKey` to
|
||||
`auth_provider.errors.<lower_case_key>`.
|
||||
- `getAuthProviderCodeMetadata(code)` — lookup helper used by adapters.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { authProviderError } from "@suaveplan/api-auth-provider/error-codes";
|
||||
|
||||
throw authProviderError("OAUTH_CALLBACK_INVALID_STATE", {
|
||||
context: { state: requestState },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
A problem-detail mapper turns any thrown `AuthProviderError` into an HTTP
|
||||
response with the correct status, retry hint, and severity-driven log level.
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUTH_PROVIDER_ERROR_METADATA,
|
||||
AuthProviderError,
|
||||
} from "@suaveplan/api-auth-provider/error-codes";
|
||||
|
||||
function mapToProblem(err: AuthProviderError): Response {
|
||||
const meta = AUTH_PROVIDER_ERROR_METADATA[err.code];
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
type: `https://errors.suaveplan/${err.code}`,
|
||||
title: err.message,
|
||||
status: meta.httpStatus,
|
||||
retryable: meta.retryable,
|
||||
}),
|
||||
{ status: meta.httpStatus }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `const AUTH_PROVIDER_CODES`
|
||||
- `type AuthProviderErrorCode`
|
||||
- `type AuthProviderErrorKey`
|
||||
- `interface AuthProviderCodeMetadata`
|
||||
- `const AUTH_PROVIDER_ERROR_METADATA: Readonly<Record<AuthProviderErrorCode, AuthProviderCodeMetadata>>`
|
||||
- `class AuthProviderError extends SuaveplanError<AuthProviderErrorCode>` —
|
||||
public constructor `(code, messageKey, opts?)`.
|
||||
- `function authProviderError(key, opts?): AuthProviderError`
|
||||
- `function getAuthProviderCodeMetadata(code: string): AuthProviderCodeMetadata | undefined`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`SuaveplanError`'s base constructor is `protected`; the subclass exposes a
|
||||
public constructor so callers outside this package — including the
|
||||
conformance harness — can build typed errors without going through the
|
||||
factory. The factory is preferred for everyday use because it derives the
|
||||
i18n message key from the short reason key, keeping the call site dense. The
|
||||
metadata record is frozen with `Object.freeze` (not just `as const`) so a
|
||||
runtime caller cannot mutate the table; `satisfies` keeps the table
|
||||
exhaustive at type-check time. `PATH_COLLISION` maps to HTTP 409 because the
|
||||
host refuses the install rather than overwriting an existing route; an
|
||||
adapter that needs a different status should override the mapper rather than
|
||||
re-shape the registry.
|
||||
@@ -0,0 +1,83 @@
|
||||
# `path-ownership` — single-provider registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`path-ownership` is the runtime registry the host consults to enforce the
|
||||
"exactly one auth provider per deployment" invariant from design §2.3. A
|
||||
provider claims its `pathPrefix` at install time; any subsequent registration
|
||||
with a different provider id throws `MULTIPLE_PROVIDERS_REGISTERED`. The
|
||||
registry is deliberately not a module-global singleton — each host instance
|
||||
constructs its own `PathOwnership`, which keeps multi-tenant test runners
|
||||
and hot-reload scenarios deterministic.
|
||||
|
||||
## Features
|
||||
|
||||
- `PathOwnership` class with `register`, `release`, `current`, `hasProvider`.
|
||||
- Injectable `clock` — defaults to `Date.now`, used for the `claimedAt`
|
||||
timestamp on the recorded claim.
|
||||
- Idempotent registration — re-registering the same instance, or a different
|
||||
instance with the same provider id, returns the existing claim.
|
||||
- Strict rejection of a different provider id with
|
||||
`AUTH_PROVIDER.MULTIPLE_PROVIDERS_REGISTERED`, including a structured
|
||||
`context` payload (`incomingProviderId`, `incumbentProviderId`,
|
||||
`prefix`) suitable for diagnostic logs.
|
||||
- `release()` drops the current claim — used by host shutdown and the
|
||||
blue-green hot-reload sequence to swap providers atomically.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { PathOwnership } from "@suaveplan/api-auth-provider/path-ownership";
|
||||
|
||||
const ownership = new PathOwnership();
|
||||
ownership.register(myProvider);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Tests inject a deterministic clock so the recorded `claimedAt` is stable
|
||||
across runs:
|
||||
|
||||
```ts
|
||||
import { PathOwnership } from "@suaveplan/api-auth-provider/path-ownership";
|
||||
|
||||
const ownership = new PathOwnership({ clock: () => 1_000_000 });
|
||||
const claim = ownership.register(myProvider);
|
||||
// claim.claimedAt === 1_000_000
|
||||
|
||||
// Hot-reload: release before swapping in the new provider.
|
||||
ownership.release();
|
||||
ownership.register(replacementProvider);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class PathOwnership` — runtime registry.
|
||||
- `constructor(options?: PathOwnershipOptions)`
|
||||
- `register(provider): PathOwnershipClaim`
|
||||
- `release(): void`
|
||||
- `current(): ProviderClaim | null`
|
||||
- `hasProvider(): boolean`
|
||||
- `interface PathOwnershipOptions { clock? }`
|
||||
- `type OwnershipClock = () => number`
|
||||
- `interface PathOwnershipClaim { providerId; prefix; claimedAt }`
|
||||
- `interface ProviderClaim<Provider> { provider; claim }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry stores both the `AuthProviderAdapter` instance and a flat
|
||||
`PathOwnershipClaim` record. Two layers exist because consumers fall into
|
||||
two camps: hosts that need the live provider for lifecycle dispatch, and
|
||||
diagnostic tooling that only needs the metadata. Idempotency is decided by
|
||||
identity first, id second — so a host that constructs a fresh provider
|
||||
instance per hot-reload still cannot accidentally register two providers
|
||||
with the same id. The `MULTIPLE_PROVIDERS_REGISTERED` error carries enough
|
||||
context for operators to diagnose double-installation in a misconfigured
|
||||
multi-process deployment without consulting the host logs. The class uses a
|
||||
private field rather than a module-level variable so multiple registries can
|
||||
coexist in the same process — essential for runner-agnostic test suites and
|
||||
multi-tenant in-process deployments.
|
||||
@@ -0,0 +1,92 @@
|
||||
# `provider` — `AuthProviderAdapter` contract + path-prefix validation
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `provider` module is the type-level + thin-runtime heart of
|
||||
`@suaveplan/api-auth-provider`. It declares the abstract `AuthProviderAdapter`
|
||||
interface every concrete account-lifecycle provider (Better-Auth, the rolled
|
||||
reference, future Cognito/Auth0 bridges) structurally implements, and ships
|
||||
the Zod-validated `authPathPrefix` constructor that asserts a deployment's
|
||||
configured prefix is a legal segment-aligned mount point. By splitting the
|
||||
contract from any concrete provider we keep this package optional: a
|
||||
hosted-IdP deployment installs the session resolver (`api-auth`) only and
|
||||
never pulls in a provider implementation.
|
||||
|
||||
## Features
|
||||
|
||||
- `AuthProviderAdapter<Caps>` interface with `id`, configurable `pathPrefix`,
|
||||
`routes(router, ctx)` registration callback, and the four-hook
|
||||
`AuthProviderLifecycle` quartet (`install`/`start`/`stop`/`uninstall`).
|
||||
- `AuthPathPrefix` opaque brand — protects against accidental string
|
||||
substitution at the install boundary.
|
||||
- `AuthPathPrefixSchema` — a Zod schema enforcing leading slash, no trailing
|
||||
slash (except `/`), no path parameters, no whitespace.
|
||||
- `authPathPrefix(value)` constructor — returns the branded value or throws
|
||||
`AUTH_PROVIDER.PROVIDER_LIFECYCLE_FAILED` with a structured `reason`.
|
||||
- `isAuthPathPrefix(value)` predicate — non-throwing variant for tests.
|
||||
- `DEFAULT_AUTH_PATH_PREFIX` constant (`"/auth"`).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
authPathPrefix,
|
||||
DEFAULT_AUTH_PATH_PREFIX,
|
||||
type AuthProviderAdapter,
|
||||
} from "@suaveplan/api-auth-provider/provider";
|
||||
|
||||
const myProvider: AuthProviderAdapter = {
|
||||
id: "rolled",
|
||||
pathPrefix: authPathPrefix(DEFAULT_AUTH_PATH_PREFIX),
|
||||
routes(router) {
|
||||
router.post("/login", { auth: null }, async () => new Response());
|
||||
},
|
||||
lifecycle: {
|
||||
install: async () => {},
|
||||
start: async () => {},
|
||||
stop: async () => {},
|
||||
uninstall: async () => {},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
A deployment that mounts its provider under a non-standard prefix MUST run
|
||||
the input through `authPathPrefix` so the value is validated before the host
|
||||
records the claim in the path-ownership registry.
|
||||
|
||||
```ts
|
||||
import { authPathPrefix } from "@suaveplan/api-auth-provider/provider";
|
||||
|
||||
const prefix = authPathPrefix(process.env.AUTH_PREFIX ?? "/auth/v2");
|
||||
// Throws AUTH_PROVIDER.PROVIDER_LIFECYCLE_FAILED on input like
|
||||
// "/auth/v2/" or "/auth/:tenant".
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface AuthProviderAdapter<Caps>` — provider contract.
|
||||
- `interface AuthProviderLifecycle<Caps>` — install/start/stop/uninstall.
|
||||
- `type AuthPathPrefix` — branded prefix string.
|
||||
- `const AuthPathPrefixSchema: ZodType` — validation schema.
|
||||
- `function authPathPrefix(value: string): AuthPathPrefix` — throwing
|
||||
constructor.
|
||||
- `function isAuthPathPrefix(value: string): boolean` — predicate.
|
||||
- `const DEFAULT_AUTH_PATH_PREFIX = "/auth"` — host default.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The brand is purely a TypeScript construct (`unique symbol`) — at runtime
|
||||
`authPathPrefix("/auth")` returns the string unchanged. The schema's order of
|
||||
refinements is significant: the leading-slash check fires first so callers
|
||||
see the most actionable error message. Whitespace is rejected before the
|
||||
parameter check because shell-supplied configuration is the typical source of
|
||||
both classes of error. The constructor surfaces failures as
|
||||
`PROVIDER_LIFECYCLE_FAILED` (not a hypothetical `INVALID_PREFIX`) because the
|
||||
install lifecycle is the only legitimate call site — keeping the code small
|
||||
makes the error registry tighter and the metadata table denser.
|
||||
@@ -0,0 +1,87 @@
|
||||
# `route-surface` — canonical auth-provider route surface
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`route-surface` declares the closed set of paths an auth provider OWNS
|
||||
relative to its `pathPrefix`. The set is the union of the routes any
|
||||
realistic concrete provider exposes — register, login, logout, refresh, two
|
||||
OAuth-flow endpoints, email verification, and the forgot/reset-password
|
||||
pair. Providers MAY implement a subset; the canonical list is the bar the
|
||||
collision detector measures against and the manifest the OpenAPI emitter
|
||||
projects when stitching a unified document at build time.
|
||||
|
||||
## Features
|
||||
|
||||
- `PROVIDER_ROUTE_SURFACE` — frozen array of `ProviderRouteEntry` records,
|
||||
every entry carrying `path`, `method`, stable `name`, and OpenAPI
|
||||
`summary`.
|
||||
- `ProviderRouteName` literal union — exhaustive narrowing in switch blocks.
|
||||
- `lookupProviderRoute(name)` — name → entry lookup.
|
||||
- `resolveProviderRoutePath(prefix, entry)` — mount under a given prefix
|
||||
(handles `"/"` as a no-op).
|
||||
- `listProviderRouteNames()` — declaration-order name iteration; used by the
|
||||
conformance harness to verify every canonical name has a scenario.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
PROVIDER_ROUTE_SURFACE,
|
||||
resolveProviderRoutePath,
|
||||
} from "@suaveplan/api-auth-provider/route-surface";
|
||||
|
||||
for (const entry of PROVIDER_ROUTE_SURFACE) {
|
||||
console.log(entry.method, resolveProviderRoutePath("/auth", entry));
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
The OpenAPI emitter walks the surface to generate the global `paths` block
|
||||
for the unified document, projecting `summary` directly into the OpenAPI
|
||||
operation and using `name` as `operationId` after kebab → camelCase
|
||||
conversion.
|
||||
|
||||
```ts
|
||||
import {
|
||||
PROVIDER_ROUTE_SURFACE,
|
||||
resolveProviderRoutePath,
|
||||
} from "@suaveplan/api-auth-provider/route-surface";
|
||||
|
||||
const openapiPaths: Record<string, unknown> = {};
|
||||
for (const entry of PROVIDER_ROUTE_SURFACE) {
|
||||
const path = resolveProviderRoutePath(provider.pathPrefix, entry);
|
||||
openapiPaths[path] = {
|
||||
[entry.method.toLowerCase()]: {
|
||||
operationId: entry.name.replace(/[-.]/g, "_"),
|
||||
summary: entry.summary,
|
||||
tags: ["auth"],
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface ProviderRouteEntry { path; method; name; summary }`
|
||||
- `type ProviderRouteName` — closed union of 9 names.
|
||||
- `const PROVIDER_ROUTE_SURFACE: ReadonlyArray<ProviderRouteEntry>`
|
||||
- `function lookupProviderRoute(name: ProviderRouteName): ProviderRouteEntry | undefined`
|
||||
- `function resolveProviderRoutePath(prefix: string, entry: ProviderRouteEntry): string`
|
||||
- `function listProviderRouteNames(): ReadonlyArray<ProviderRouteName>`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The surface is frozen at module-load with two layers of `Object.freeze` —
|
||||
once around the outer array and once around each entry — to keep downstream
|
||||
hosts from mutating the table at runtime. `resolveProviderRoutePath` treats
|
||||
`"/"` as the empty-prefix case so a deployment that mounts the provider at
|
||||
the host root produces `"/login"` rather than `"//login"`. Adding a new
|
||||
canonical route requires an OpenSpec change because the conformance harness
|
||||
and the OpenAPI projector both treat the surface as authoritative; touching
|
||||
either side without the other is the bug class this list-as-data design
|
||||
guards against.
|
||||
@@ -0,0 +1,89 @@
|
||||
# `testing/conformance` — provider conformance harness
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`runAuthProviderConformance` is the runner-agnostic harness every concrete
|
||||
`AuthProviderAdapter` MUST clear. It exercises the four critical contract
|
||||
surfaces — path ownership, canonical route surface, lifecycle quartet, and
|
||||
collision-detector behaviour — against a fresh provider instance per
|
||||
scenario. Concrete provider packages call the harness inside a `describe()`
|
||||
block in their own test file and turn each returned `ConformanceScenarioResult`
|
||||
into an `it()` assertion. The harness deliberately does not import a test
|
||||
runner; that responsibility stays at the call site so providers compatible
|
||||
with `vitest`, `bun:test`, or any other runner can all share the same
|
||||
behaviour bar.
|
||||
|
||||
## Features
|
||||
|
||||
- Four-category coverage — `path-ownership`, `route-surface`, `lifecycle`,
|
||||
`collision`.
|
||||
- Fresh-provider-per-scenario semantics — the factory is invoked once per
|
||||
scenario so cross-scenario state cannot leak.
|
||||
- Records every category and scenario name in the returned array so callers
|
||||
can verify (in their own tests) that every category is exercised.
|
||||
- Returns a `passed` boolean per scenario plus an optional `detail` string
|
||||
for debug context.
|
||||
- Frozen return value — the caller cannot accidentally mutate the report
|
||||
while iterating.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { runAuthProviderConformance } from "@suaveplan/api-auth-provider/testing";
|
||||
|
||||
describe("better-auth conformance", () => {
|
||||
const results = runAuthProviderConformance({
|
||||
label: "better-auth",
|
||||
factory: () => buildBetterAuthProvider(),
|
||||
});
|
||||
for (const r of results) {
|
||||
it(`[${r.category}] ${r.name}`, { timeout: 5000 }, () => {
|
||||
expect(r.passed, r.detail).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
A provider whose factory is async (e.g. needs to bring up a Mailpit
|
||||
testcontainer for verify-email) wraps the synchronous factory the harness
|
||||
expects and runs setup outside it:
|
||||
|
||||
```ts
|
||||
import { runAuthProviderConformance } from "@suaveplan/api-auth-provider/testing";
|
||||
|
||||
const env = await startMailpit();
|
||||
const results = runAuthProviderConformance({
|
||||
label: "better-auth-with-mailpit",
|
||||
factory: () => buildBetterAuthProvider({ smtp: env.connection }),
|
||||
});
|
||||
await env.dispose();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `function runAuthProviderConformance(options): ReadonlyArray<ConformanceScenarioResult>`
|
||||
- `interface AuthProviderConformanceOptions { label; factory }`
|
||||
- `type AuthProviderConformanceFactory = () => AuthProviderAdapter`
|
||||
- `type AuthProviderConformanceCategory =
|
||||
"path-ownership" | "route-surface" | "lifecycle" | "collision"`
|
||||
- `interface ConformanceScenarioResult { category; name; passed; detail? }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The harness deliberately drives a synchronous factory because the
|
||||
information it asserts is structural — `id`, `pathPrefix`, lifecycle quartet
|
||||
presence, route surface enumeration — and forcing async setup into every
|
||||
provider creates fixture-management noise where none is needed. Providers
|
||||
that need real I/O (e.g. database-backed user creation) layer their own
|
||||
scenarios on top. Each category drives a fresh provider instance, so a
|
||||
scenario that crashes one provider does not leave global state poisoned for
|
||||
the next. The `lifecycle` category checks for the four-function quartet by
|
||||
`typeof === "function"`; a provider whose `stop` hook is missing or
|
||||
non-callable fails the check loud rather than at runtime.
|
||||
83
docs/api-stack/api/api-auth-rolled/README.md
Normal file
83
docs/api-stack/api/api-auth-rolled/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# @suaveplan/api-auth-rolled — rolled-our-own auth provider. Implements both `auth.session` and `auth.provider` capability contracts. Session tokens are RS256-signed JWTs (via @suaveplan/crypto-rsa + @suaveplan/jws) with rotating `kid` for non-disruptive key rotation. Passwords are hashed via Argon2id (@suaveplan/crypto-hash), gated by @suaveplan/password-strength and @suaveplan/hibp at registration. Provider mounts the canonical 9-route PROVIDER_ROUTE_SURFACE under a configurable path prefix. DB access via injected @suaveplan/types `DatabaseLike` — shares the host's main connection per design §2.3. Projects DB rows onto the @suaveplan/api-types `Principal` shape. Contributes cookie + bearer OpenAPI security schemes.
|
||||
|
||||
Stage-8.12 rolled session-token resolver for `@suaveplan/api`. Issues
|
||||
RS256-signed JWTs with rotating `kid` slots, verifies tokens through the
|
||||
rotator's active + retired-non-revoked key set, projects DB rows onto
|
||||
the `@suaveplan/api-types` `Principal` contract, and contributes cookie
|
||||
+ bearer OpenAPI security schemes.
|
||||
|
||||
This milestone ships the **session-resolver half** of the eventual dual
|
||||
contract (`auth.session`). The matching `AuthProviderAdapter`
|
||||
(account-lifecycle routes, password gating, MFA, OAuth) is deferred to
|
||||
a successor change `api-auth-rolled-provider`.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [composition](./src/composition/composition.md) — Top-level factory `createRolledAuthAdapter` that wires the four sub-modules (`token-rotator`, `session`, `principal-pro…
|
||||
- [composition](./src/composition/composition.md) — Top-level factory `createRolledAuthAdapter` that wires the four sub-modules (`token-rotator`, `session`, `principal-pro…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-rolled`. Every code is namespaced under `AUTH_ROLLED.*` so cross-pa…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-rolled`. Every code is namespaced under `AUTH_ROLLED.*` so cross-pa…
|
||||
- [options](./src/options/options.md) — Defines the configuration shape accepted by `createRolledAuthAdapter` and the Zod schema that validates it. Operators w…
|
||||
- [options](./src/options/options.md) — Defines the configuration shape accepted by `createRolledAuthAdapter` and the Zod schema that validates it. Operators w…
|
||||
- [principal-projection](./src/principal-projection/principal-projection.md) — Projects the package's internal `UserRow` storage shape onto the abstract `Principal` contract published by `@suaveplan…
|
||||
- [principal-projection](./src/principal-projection/principal-projection.md) — Projects the package's internal `UserRow` storage shape onto the abstract `Principal` contract published by `@suaveplan…
|
||||
- [security-schemes](./src/security-schemes/security-schemes.md) — Contributes OpenAPI 3.1 `securitySchemes` entries describing both transports the rolled session resolver accepts — `Aut…
|
||||
- [security-schemes](./src/security-schemes/security-schemes.md) — Contributes OpenAPI 3.1 `securitySchemes` entries describing both transports the rolled session resolver accepts — `Aut…
|
||||
- [session](./src/session/session.md) — The rolling-JWT session resolver — the keystone of the rolled auth adapter. Builds a `SessionResolver` (per `@suaveplan…
|
||||
- [session](./src/session/session.md) — The rolling-JWT session resolver — the keystone of the rolled auth adapter. Builds a `SessionResolver` (per `@suaveplan…
|
||||
- [token-rotator](./src/token-rotator/token-rotator.md) — Owns the rotating-keyset state for the rolled session-token resolver. The active key signs new JWTs; retired-but-non-re…
|
||||
- [token-rotator](./src/token-rotator/token-rotator.md) — Owns the rotating-keyset state for the rolled session-token resolver. The active key signs new JWTs; retired-but-non-re…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Why
|
||||
|
||||
This package gives operators a
|
||||
session-auth path that owns zero third-party runtime dependencies.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/api-auth-rolled
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | What it owns |
|
||||
|---|---|
|
||||
| `composition/` | `createRolledAuthAdapter` — the public factory. |
|
||||
| `error-codes/` | The 4-code `AUTH_ROLLED.*` registry. |
|
||||
| `options/` | Zod-validated options + named defaults. |
|
||||
| `principal-projection/` | `UserRow` → `Principal` projection. |
|
||||
| `security-schemes/` | Cookie + bearer OpenAPI contributions. |
|
||||
| `session/` | RS256 sign/verify, cookie+bearer extraction, `Set-Cookie`. |
|
||||
| `token-rotator/` | Rotating-key state machine. |
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { createRolledAuthAdapter } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
const adapter = createRolledAuthAdapter({
|
||||
store: hostStore,
|
||||
keys: [{ kid: "k1", privateJwk, publicJwk, active: true }],
|
||||
issuer: "https://api.example.com",
|
||||
audience: "example-front",
|
||||
});
|
||||
|
||||
host.registerAuthSession(adapter);
|
||||
```
|
||||
|
||||
Rotation cron:
|
||||
|
||||
```ts
|
||||
setInterval(async () => {
|
||||
if (!adapter.rotator.decide().shouldRotate) return;
|
||||
adapter.rotator.setKeys(await kms.nextKeyset());
|
||||
}, 60_000);
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# `composition`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Top-level factory `createRolledAuthAdapter` that wires the four
|
||||
sub-modules (`token-rotator`, `session`, `principal-projection`,
|
||||
`security-schemes`) into a single `AuthAdapter` ready for the host to
|
||||
register. Keeping the composition explicit (rather than letting each
|
||||
sub-module own its own host wiring) means the dependency graph is one
|
||||
arrow: options flow in, an adapter object flows out, no hidden state.
|
||||
|
||||
The returned value extends the `AuthAdapter` contract (`resolver` +
|
||||
`securityScheme`) with three additional surfaces the future provider
|
||||
half (`api-auth-rolled-provider`) will consume: `issueToken`,
|
||||
`verifyToken`, and the rotator handle so operators can wire
|
||||
`setKeys(...)` from an out-of-band rotation cron.
|
||||
|
||||
## Features
|
||||
|
||||
- Single entry point — `createRolledAuthAdapter(rawOptions)`.
|
||||
- Options validated via the package's Zod schema before any wiring runs.
|
||||
- Defaults filled at the composition layer (cookie name, rotation
|
||||
period, session TTL) so individual sub-modules stay focused on their
|
||||
one concern.
|
||||
- Returned adapter is structurally a valid `AuthAdapter` so any host
|
||||
that accepts the contract will accept the rolled adapter.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createRolledAuthAdapter } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
const adapter = createRolledAuthAdapter({
|
||||
store,
|
||||
keys: [
|
||||
{ kid: "k1", privateJwk, publicJwk, active: true },
|
||||
],
|
||||
issuer: "https://api.example.com",
|
||||
audience: "example-front",
|
||||
});
|
||||
|
||||
host.registerAuthSession(adapter);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createRolledAuthAdapter } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
const adapter = createRolledAuthAdapter({
|
||||
store,
|
||||
keys: initialKeys,
|
||||
issuer: "https://api.example.com",
|
||||
audience: "example-front",
|
||||
cookieName: "acme.session",
|
||||
sessionTtlSec: 1800,
|
||||
rotationPeriodMs: 12 * 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Operator rotation cron — fetch new key from KMS, swap atomically.
|
||||
setInterval(async () => {
|
||||
if (!adapter.rotator.decide().shouldRotate) return;
|
||||
const next = await kms.nextKeyset();
|
||||
adapter.rotator.setKeys(next);
|
||||
}, 60_000);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createRolledAuthAdapter(rawOptions: unknown): RolledAuthAdapter`
|
||||
- `RolledAuthAdapter extends AuthAdapter`: adds `id: "rolled"`, `rotator`,
|
||||
`issueToken(userId)`, `verifyToken(token)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The adapter intentionally keeps `id` as the literal `"rolled"` so a host
|
||||
running both Better-Auth and rolled can disambiguate by adapter id. The
|
||||
`securityScheme` slot points at the bearer contribution (the canonical
|
||||
single-scheme shape) but the `.schemes` map on the contribution exposes
|
||||
both the cookie and bearer entries — emitters aware of dual-transport
|
||||
adapters can register both names without a separate adapter property.
|
||||
@@ -0,0 +1,78 @@
|
||||
# `error-codes`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Stable error-code registry for `@suaveplan/api-auth-rolled`. Every code is
|
||||
namespaced under `AUTH_ROLLED.*` so cross-package consumers (the host's audit
|
||||
log, the OpenAPI emitter, the conformance harness) can identify the package
|
||||
that raised the failure without string-matching on free-form messages.
|
||||
|
||||
The milestone ships the session-resolver half only; the registry
|
||||
is correspondingly minimal — four operator-actionable infrastructure codes,
|
||||
all of which are thrown synchronously from production code paths and
|
||||
covered by the conformance test. Per-request soft failures (missing token,
|
||||
bad signature, expired) collapse to `null` per `SessionResolver` chain
|
||||
semantics and are therefore NOT codes. Account-lifecycle codes
|
||||
(`USER_NOT_FOUND`, `INVALID_CREDENTIALS`, `PASSWORD_BREACHED`, …) belong to
|
||||
the deferred provider half and will be added by a successor change.
|
||||
|
||||
## Features
|
||||
|
||||
- Tagged with `defineCodes("AUTH_ROLLED", [...])` from `@suaveplan/error`,
|
||||
so all codes share a single namespace prefix.
|
||||
- `AuthRolledError` extends `SuaveplanError` so `instanceof AuthRolledError`
|
||||
narrows `.code` to `AuthRolledErrorCode`.
|
||||
- Four codes: `SIGNING_KEY_NOT_CONFIGURED` (options validation),
|
||||
`KEY_ROTATION_FAILED` (`TokenRotator.setKeys`),
|
||||
`DATABASE_REQUIRED` (missing `store`), `PRINCIPAL_PROJECTION_FAILED`
|
||||
(malformed user row).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AUTH_ROLLED_CODES, AuthRolledError } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
throw new AuthRolledError(
|
||||
AUTH_ROLLED_CODES.DATABASE_REQUIRED,
|
||||
"host did not inject a store"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { AUTH_ROLLED_CODES, AuthRolledError } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
try {
|
||||
/* ... */
|
||||
} catch (err) {
|
||||
if (err instanceof AuthRolledError) {
|
||||
// .rolledCode is typed as AuthRolledErrorCode — no narrowing cast.
|
||||
if (err.rolledCode === AUTH_ROLLED_CODES.KEY_ROTATION_FAILED) {
|
||||
log.fatal({ err }, "operator key-rotation contract violated");
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AUTH_ROLLED_CODES` — frozen `Record<Name, "AUTH_ROLLED.NAME">`.
|
||||
- `AuthRolledErrorCode` — string-literal union of every code value.
|
||||
- `AuthRolledError extends SuaveplanError<AuthRolledErrorCode>` — has a
|
||||
`rolledCode: AuthRolledErrorCode` convenience accessor.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The set of 20 codes is fixed by the OpenSpec spec delta. The conformance
|
||||
test enumerates `AUTH_ROLLED_CODES` and asserts every code has a named
|
||||
production trigger; if a code is added without a trigger, the test fails
|
||||
loudly. Cross-package codes (e.g. `HIBP.*` propagated from
|
||||
`@suaveplan/hibp` when the breach lookup fails) are NOT re-encoded — they
|
||||
surface to the caller intact through `SuaveplanError`'s `cause` chain so
|
||||
operators can attribute failures to the originating layer.
|
||||
90
docs/api-stack/api/api-auth-rolled/src/options/options.md
Normal file
90
docs/api-stack/api/api-auth-rolled/src/options/options.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# `options`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the configuration shape accepted by `createRolledAuthAdapter` and
|
||||
the Zod schema that validates it. Operators wire this from `host.json` or
|
||||
build it programmatically; everything the rest of the package needs flows
|
||||
from a single validated `RolledAuthOptions` value, which keeps invariants
|
||||
(active-key count, prefix shape, TTL positivity) in one place.
|
||||
|
||||
The schema deliberately validates injected functions and the `store`
|
||||
adapter as `unknown` and then refines them later — Zod cannot verify
|
||||
function-argument shapes statically, and TypeScript already does that work
|
||||
at the call boundary.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod schema `RolledAuthOptionsSchema` covering every operator-facing
|
||||
knob.
|
||||
- A `superRefine` cross-field invariant that exactly one key is both
|
||||
`active: true` and non-revoked — keeps sign-time semantics ambiguity-free.
|
||||
- Stable named defaults (`DEFAULT_COOKIE_NAME`, `DEFAULT_ROUTE_PREFIX`,
|
||||
TTLs, rotation period, minimum strength score) so consumers can
|
||||
reference the defaults without re-reading the source.
|
||||
- Two distinct error codes for failures: `DATABASE_REQUIRED` when `store`
|
||||
is missing (operator hooked up nothing), `SIGNING_KEY_NOT_CONFIGURED`
|
||||
for every other schema failure (Zod issues attached as `context`).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseRolledAuthOptions } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
const opts = parseRolledAuthOptions({
|
||||
store: myStore,
|
||||
keys: [{ kid: "k1", privateJwk, publicJwk, active: true }],
|
||||
issuer: "https://api.example.com",
|
||||
audience: "example-frontend",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
DEFAULT_ROTATION_PERIOD_MS,
|
||||
parseRolledAuthOptions,
|
||||
} from "@suaveplan/api-auth-rolled";
|
||||
|
||||
const opts = parseRolledAuthOptions({
|
||||
store,
|
||||
keys,
|
||||
issuer: "https://api.example.com",
|
||||
audience: "example-frontend",
|
||||
routePrefix: "/api/auth",
|
||||
cookieName: "acme.session",
|
||||
cookieSameSite: "strict",
|
||||
cookieSecure: true,
|
||||
sessionTtlSec: 1800,
|
||||
refreshTtlSec: 7 * 24 * 60 * 60,
|
||||
rotationPeriodMs: DEFAULT_ROTATION_PERIOD_MS / 2,
|
||||
minPasswordStrength: 4,
|
||||
strength: { async score(pw) { return 4; } },
|
||||
breach: { async isBreached() { return false; } },
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `parseRolledAuthOptions(raw): RolledAuthOptions` — throws
|
||||
`AuthRolledError` on bad input.
|
||||
- `RolledAuthOptionsSchema` — the Zod schema, re-exported for advanced
|
||||
consumers that want to layer their own refinements.
|
||||
- `DEFAULT_*` constants — every operator-facing default exposed as a
|
||||
named export.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema is `strictObject` so unknown keys produce a Zod error rather
|
||||
than silently passing through. The `keys[]` `active` invariant is enforced
|
||||
in `superRefine` rather than as a `transform` so the invariant message
|
||||
appears in the validation issue list instead of as a thrown exception
|
||||
mid-parse. Cross-field invariants that cross the option/store boundary
|
||||
(e.g. "every kid actually resolves to a JWK") are checked later inside the
|
||||
session/token-rotator modules so the options layer remains synchronous and
|
||||
pure.
|
||||
@@ -0,0 +1,73 @@
|
||||
# `principal-projection`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Projects the package's internal `UserRow` storage shape onto the abstract
|
||||
`Principal` contract published by `@suaveplan/api-types`. Keeping the
|
||||
projection in one place ensures that no storage-specific columns leak
|
||||
into the principal seen by downstream plugins, and that the four
|
||||
documented principal fields (`id`, `scopes`, `tenantId?`, `claims`) are
|
||||
the only surface area authors can rely on.
|
||||
|
||||
The projection is deliberately strict: structurally-invalid rows raise
|
||||
`AUTH_ROLLED.PRINCIPAL_PROJECTION_FAILED`, surfacing storage-driver bugs
|
||||
loudly rather than letting malformed data flow into route handlers.
|
||||
|
||||
## Features
|
||||
|
||||
- One-shot pure function — no DB calls, no async, no allocations beyond
|
||||
the new array of branded `Scope`s.
|
||||
- Validates every column the projection touches (`id` non-empty string,
|
||||
`scopes` array of non-empty strings, `claims` plain object).
|
||||
- Omits `tenantId` from the output `Principal` when the storage row's
|
||||
value is `null`, matching the optional shape in `Principal`.
|
||||
- Brands strings as `PrincipalId` / `Scope` / `TenantId` at the boundary
|
||||
so type narrowing carries through to consumers.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { projectUserRowToPrincipal } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
const principal = projectUserRowToPrincipal(row);
|
||||
console.log(principal.id, principal.scopes);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { projectUserRowToPrincipal } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
function resolveSession(row: UserRow): Principal {
|
||||
try {
|
||||
return projectUserRowToPrincipal(row);
|
||||
} catch (err) {
|
||||
// Storage adapter returned a malformed row — log + treat as an
|
||||
// anonymous request rather than surfacing the bug to the client.
|
||||
log.error({ err }, "principal projection failed");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `projectUserRowToPrincipal(row: UserRow): Principal` — pure projection.
|
||||
Throws `AuthRolledError(PRINCIPAL_PROJECTION_FAILED)` on any structural
|
||||
invariant violation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The projection allocates a fresh `scopes` array rather than spreading the
|
||||
row's array — this is intentional so that downstream handlers cannot
|
||||
mutate the source row through the principal reference. `claims` is a
|
||||
shallow read-only reference so deep-cloning every claim object on every
|
||||
request stays optional (consumers wanting deep immutability can wrap with
|
||||
`Object.freeze`). The `tenantId` branding mirrors the contract published
|
||||
by `@suaveplan/api-tenant-resolver-*` adapters — keeping the brand at the
|
||||
projection boundary means downstream code that takes a `TenantId` never
|
||||
needs an unsafe cast.
|
||||
@@ -0,0 +1,73 @@
|
||||
# `security-schemes`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Contributes OpenAPI 3.1 `securitySchemes` entries describing both
|
||||
transports the rolled session resolver accepts — `Authorization: Bearer
|
||||
<token>` and a `Cookie` carrying the same JWT. Keeping the two entries
|
||||
co-located means a single source of truth describes what the resolver
|
||||
accepts on the wire; the `api-openapi` emitter (Stage 8.6) walks the
|
||||
adapter's `securityScheme` slot AND its `.schemes` map so both are
|
||||
surfaced in the generated document.
|
||||
|
||||
## Features
|
||||
|
||||
- Bearer entry: OpenAPI 3.1 `http` / `bearer` form with `bearerFormat:
|
||||
"JWT"` — the canonical Bearer-JWT shape consumers expect.
|
||||
- Cookie entry: `apiKey` / `in: cookie` form whose `name` is the
|
||||
caller-configured cookie name, so the doc matches the adapter's
|
||||
runtime behaviour exactly.
|
||||
- `createRolledSecuritySchemes({ cookieName })` returns a single value
|
||||
that satisfies the `SecuritySchemeContribution` contract (its `name`
|
||||
and `scheme` point at the bearer entry) AND exposes both contributions
|
||||
on `.schemes` — the emitter can pull either or both.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createRolledSecuritySchemes } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
const contribution = createRolledSecuritySchemes({
|
||||
cookieName: "suaveplan.session",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
createRolledBearerSecurityScheme,
|
||||
createRolledCookieSecurityScheme,
|
||||
} from "@suaveplan/api-auth-rolled";
|
||||
|
||||
// Multi-resolver host — name the contributions to disambiguate.
|
||||
const userBearer = createRolledBearerSecurityScheme("userBearer");
|
||||
const serviceBearer = createRolledBearerSecurityScheme("serviceBearer");
|
||||
const cookie = createRolledCookieSecurityScheme({
|
||||
name: "userCookie",
|
||||
cookieName: "acme.session",
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createRolledBearerSecurityScheme(name?: string): SecuritySchemeContribution`
|
||||
- `createRolledCookieSecurityScheme({ name?, cookieName }): SecuritySchemeContribution`
|
||||
- `createRolledSecuritySchemes({ cookieName }): RolledSecurityContribution`
|
||||
— convenience bundle.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The combined `RolledSecurityContribution` is structurally a
|
||||
`SecuritySchemeContribution` (so old emitters that only read `name` +
|
||||
`scheme` keep working) but adds a `.schemes` map for emitters aware of
|
||||
the dual-transport shape. This keeps the contribution drop-in for both
|
||||
single-transport and dual-transport emitters without forcing every
|
||||
caller to choose. The cookie scheme's `apiKey/in:cookie` representation
|
||||
matches OpenAPI 3.1 §4.8.27.4 — there is no first-class cookie-JWT
|
||||
shape in OAS 3.1 so `apiKey` with the configured name is the canonical
|
||||
form.
|
||||
100
docs/api-stack/api/api-auth-rolled/src/session/session.md
Normal file
100
docs/api-stack/api/api-auth-rolled/src/session/session.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# `session`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The rolling-JWT session resolver — the keystone of the rolled auth
|
||||
adapter. Builds a `SessionResolver` (per `@suaveplan/api-auth`) that
|
||||
parses-and-verifies a JWT extracted from either the `Authorization:
|
||||
Bearer` header or a configurable cookie, runs the JWT through the
|
||||
`TokenRotator` to find the verifying JWK by `kid`, projects the matching
|
||||
user row onto a `Principal`, and returns `null` on every soft failure so
|
||||
the host's resolver chain can move on to the next adapter.
|
||||
|
||||
The same module exposes `issueToken(userId)` so the future
|
||||
account-lifecycle provider (deferred to `api-auth-rolled-provider`) can
|
||||
share a single signing path with the resolver. Keeping issue and verify
|
||||
co-located means the JWT shape and the rotator wiring stay in lock-step.
|
||||
|
||||
## Features
|
||||
|
||||
- RS256 signing via `@suaveplan/jws` `compactSign` with the rotator's
|
||||
active JWK; verification via `compactVerify` against the rotator's
|
||||
retired-or-active JWK matched by `kid`.
|
||||
- Bearer + cookie extraction in one pass — bearer wins when both are
|
||||
present so service clients can override a stale cookie.
|
||||
- Strict JWT-claim validation: `iss`/`aud` exact match, `exp` future,
|
||||
`iat` not before `users.createdAt`.
|
||||
- Soft failures (missing token, bad signature, expired, unknown kid)
|
||||
collapse to `null`; hard failures (storage adapter throws) propagate
|
||||
as `AuthRolledError`.
|
||||
- `buildSetCookie` returns a deterministic `Set-Cookie` value with
|
||||
`HttpOnly`, `Path=/`, `Max-Age`, `SameSite`, `Secure` — used by the
|
||||
future provider half but published here so the cookie shape lives next
|
||||
to the cookie parser.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createRolledSessionResolver, TokenRotator } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
const rotator = new TokenRotator({ keys, rotationPeriodMs, clock });
|
||||
const resolver = createRolledSessionResolver({
|
||||
options,
|
||||
rotator,
|
||||
clock,
|
||||
cookieName: "suaveplan.session",
|
||||
sessionTtlSec: 3600,
|
||||
});
|
||||
|
||||
const principal = await resolver.resolve(req);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createRolledSessionResolver } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
const resolver = createRolledSessionResolver({ /* ... */ });
|
||||
|
||||
// Provider half (future) signs new tokens with the same key set as the
|
||||
// resolver — sharing one path means the rotator's view of the active
|
||||
// kid is the only source of truth.
|
||||
const issued = await resolver.issueToken("u_1");
|
||||
const setCookie = buildSetCookie({
|
||||
name: "suaveplan.session",
|
||||
value: issued.token,
|
||||
maxAgeSec: issued.expiresAt - issued.issuedAt,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createRolledSessionResolver({ options, rotator, clock, cookieName, sessionTtlSec }) → SessionResolver & { issueToken, verifyToken }`
|
||||
- `buildSetCookie({ name, value, maxAgeSec, sameSite, secure }) → string`
|
||||
- `tokenSizeOf(token: string): number` — byte length, used by the
|
||||
conformance harness to assert tokens stay under the 8 KiB cookie limit.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The resolver intentionally returns `null` on every parse/verify failure
|
||||
rather than throwing — `SessionResolver` chain semantics treat thrown
|
||||
errors as operator-actionable infrastructure failures, so soft failures
|
||||
must collapse to `null` to let the chain advance to the next adapter
|
||||
(typically `api-auth-anonymous`). The two cases where the resolver does
|
||||
throw are `AuthRolledError(PRINCIPAL_PROJECTION_FAILED)` raised by the
|
||||
projection step (storage-driver bug) and whatever error the operator's
|
||||
`store.findUserById` raises (DB connection lost). The session-resolver
|
||||
chain handles both by surfacing 500 to the client and routing to the
|
||||
host's audit log.
|
||||
|
||||
The header parse is done manually rather than via `compactDecode` so a
|
||||
malformed header (e.g. missing `kid`) bypasses the rotator lookup
|
||||
entirely. The rotator can verify with retired-but-non-revoked keys, so
|
||||
rotating the active key never invalidates a session mid-lifetime —
|
||||
operators can rotate without coordinating with active users.
|
||||
@@ -0,0 +1,87 @@
|
||||
# `token-rotator`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Owns the rotating-keyset state for the rolled session-token resolver. The
|
||||
active key signs new JWTs; retired-but-non-revoked keys still verify
|
||||
in-flight tokens so a rotation event never invalidates a session
|
||||
mid-lifetime. The rotator does NOT generate new key material — that is an
|
||||
operator-controlled action (HSM, KMS, mounted file) that submits the
|
||||
replacement set through `setKeys`. This split is deliberate: keeping key
|
||||
generation out of the adapter means the adapter never holds long-lived
|
||||
private key material outside of the operator's chosen storage layer.
|
||||
|
||||
## Features
|
||||
|
||||
- Snapshots the keyset atomically (`snapshot()`) — the session module
|
||||
reads the snapshot, signs with it, and never observes a half-rotated
|
||||
state.
|
||||
- Decides when to rotate (`decide()`) based on the active key's age vs
|
||||
the configured `rotationPeriodMs`. Decision is advisory — the host
|
||||
schedules the actual rotation cron.
|
||||
- `setKeys(...)` validates the replacement keyset (exactly one active +
|
||||
non-revoked) before swapping; throws `KEY_ROTATION_FAILED` if the new
|
||||
set would leave signing ambiguous.
|
||||
- `publicJwkFor(kid)` resolves the verifying JWK for any non-revoked kid
|
||||
— including retired keys — so verification of in-flight tokens works
|
||||
through the rotation window.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { TokenRotator } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
const rotator = new TokenRotator({
|
||||
keys,
|
||||
rotationPeriodMs: 24 * 60 * 60 * 1000,
|
||||
clock: () => Date.now(),
|
||||
});
|
||||
|
||||
const snap = rotator.snapshot();
|
||||
const activeJwk = snap.active.privateJwk;
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { TokenRotator } from "@suaveplan/api-auth-rolled";
|
||||
|
||||
const rotator = new TokenRotator({
|
||||
keys: initialKeys,
|
||||
rotationPeriodMs: 24 * 60 * 60 * 1000,
|
||||
clock,
|
||||
});
|
||||
|
||||
// Periodic rotator cron:
|
||||
setInterval(async () => {
|
||||
const decision = rotator.decide();
|
||||
if (!decision.shouldRotate) return;
|
||||
const next = await fetchKeysFromKms(); // operator-specific
|
||||
rotator.setKeys(next);
|
||||
}, 60 * 1000);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new TokenRotator({ keys, rotationPeriodMs, clock })` — construct.
|
||||
- `snapshot(): KeySetSnapshot` — atomic read.
|
||||
- `decide(): RotationDecision` — should we rotate now?
|
||||
- `setKeys(next: readonly SigningKeyEntry[]): void` — swap keys.
|
||||
- `publicJwkFor(kid): Record<string, unknown> | undefined` — verify-side
|
||||
lookup.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The rotator keeps its `activeSince` timestamp in instance state rather
|
||||
than reading `keys[i].activatedAt`, so the in-memory rotation timer is
|
||||
independent of any timestamps the operator stamps on the JWK records. This
|
||||
means a fresh keyset injected via `setKeys` starts a brand-new rotation
|
||||
window regardless of when the underlying key material was generated — the
|
||||
operator's intent ("treat this as the new active key as of now") is the
|
||||
authoritative signal. The exactly-one-active invariant is enforced both
|
||||
in `parseRolledAuthOptions` and in `setKeys` so there is no path through
|
||||
which the rotator can observe an invalid keyset.
|
||||
125
docs/api-stack/api/api-auth-static-principal/README.md
Normal file
125
docs/api-stack/api/api-auth-static-principal/README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# @suaveplan/api-auth-static-principal — fixed-principal session resolver for @suaveplan/api: returns a configured Principal verbatim on every request. Used in plugin integration tests and the api-plugin-example fixture.
|
||||
|
||||
Fixed-`Principal` session resolver for the `@suaveplan/api` plugin
|
||||
system. Returns the configured `Principal` verbatim on every request —
|
||||
no token decoding, no network I/O, no internal state. Used in plugin
|
||||
integration tests and the `api-plugin-example` fixture, where the goal is to
|
||||
exercise the entire downstream pipeline (RBAC gate, tenant resolver chain
|
||||
reading `principal.tenantId`, tenant-scoped DB) with a deterministic
|
||||
identity carried in config.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [adapter](./src/adapter/adapter.md) — Bundles the static-principal resolver, the (absent) security-scheme contribution, and a pinned `csrf: false` into a sin…
|
||||
- [adapter](./src/adapter/adapter.md) — Bundles the static-principal resolver, the (absent) security-scheme contribution, and a pinned `csrf: false` into a sin…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Defines the `AUTH_STATIC_PRINCIPAL.*` error-code namespace and the `AuthStaticPrincipalError` concrete subclass of `@su…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Defines the `AUTH_STATIC_PRINCIPAL.*` error-code namespace and the `AuthStaticPrincipalError` concrete subclass of `@su…
|
||||
- [factory](./src/factory/create-static-principal-resolver.md) — Constructs a `SessionResolver` from `@suaveplan/api-auth` that returns the configured `Principal` verbatim on every req…
|
||||
- [create-static-principal-resolver](./src/factory/create-static-principal-resolver.md) — Constructs a `SessionResolver` from `@suaveplan/api-auth` that returns the configured `Principal` verbatim on every req…
|
||||
- [options](./src/options/options.md) — Owns the runtime contract between the test harness call site and the resolver factory. The schema validates both the ou…
|
||||
- [options](./src/options/options.md) — Owns the runtime contract between the test harness call site and the resolver factory. The schema validates both the ou…
|
||||
- [security-scheme](./src/security-scheme/security-scheme.md) — Provides the per-adapter `SecuritySchemeContribution` slot of `AuthAdapter`. The static-principal adapter is a test-onl…
|
||||
- [security-scheme](./src/security-scheme/security-scheme.md) — Provides the per-adapter `SecuritySchemeContribution` slot of `AuthAdapter`. The static-principal adapter is a test-onl…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Why does this exist?
|
||||
|
||||
Per the API foundation design doc §13.2, the testing kit needs an auth
|
||||
adapter that supplies a fixed `Principal` without any token decoding. The
|
||||
§13.2 example uses it directly:
|
||||
|
||||
```ts
|
||||
adapters: {
|
||||
"auth.session": {
|
||||
kind: "static-principal",
|
||||
principal: { id: "u1", scopes: ["billing:write"], tenantId: "acme" },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Tests exercise the whole pipeline — RBAC, tenant resolution, tenant-scoped
|
||||
DB — with a deterministic principal in config. No mocks, no JWKS setup, no
|
||||
token signing.
|
||||
|
||||
## Surface
|
||||
|
||||
| Subpath | Export |
|
||||
| -------------- | --------------------------------------------------------------------------------------- |
|
||||
| `./resolver` | `createStaticPrincipalResolver(opts): SessionResolver` |
|
||||
| `./adapter` | `createStaticPrincipalAdapter(opts): AuthAdapter` |
|
||||
| `./error-codes`| `AUTH_STATIC_PRINCIPAL_CODES`, `AuthStaticPrincipalError`, `AuthStaticPrincipalErrorCode` |
|
||||
|
||||
The root barrel re-exports the resolver, adapter, error codes, options
|
||||
schema (`StaticPrincipalResolverOptionsSchema` /
|
||||
`parseStaticPrincipalResolverOptions`), and the empty
|
||||
`staticPrincipalSecurityScheme()` contribution.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { createStaticPrincipalAdapter } from "@suaveplan/api-auth-static-principal/adapter";
|
||||
|
||||
const adapter = createStaticPrincipalAdapter({
|
||||
principal: {
|
||||
id: "u1",
|
||||
scopes: ["billing:write"],
|
||||
tenantId: "acme",
|
||||
claims: { sub: "u1" },
|
||||
},
|
||||
});
|
||||
|
||||
host.registerAuth(adapter);
|
||||
// every request now produces `principal === { id: "u1", ... }`.
|
||||
```
|
||||
|
||||
Skip auth on selected paths to test `publicRoutes` behaviour without
|
||||
rewiring the adapter chain:
|
||||
|
||||
```ts
|
||||
const adapter = createStaticPrincipalAdapter({
|
||||
principal: { id: "u1", scopes: [], claims: {} },
|
||||
nullForPaths: ["/healthz", "/webhooks/*"],
|
||||
applyTo: (req) => !req.headers.has("x-skip-auth"),
|
||||
});
|
||||
```
|
||||
|
||||
## Conformance
|
||||
|
||||
The resolver passes `runAuthSessionConformance` from
|
||||
`@suaveplan/api-auth/testing` in seeded-principal mode. See
|
||||
`src/factory/create-static-principal-resolver.spec-conformance.test.ts` and
|
||||
`tests/conformance/api-auth.test.ts`.
|
||||
|
||||
## Errors
|
||||
|
||||
`AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS` — outer options bag failed Zod
|
||||
validation (wrong shape, missing required fields, unknown top-level keys).
|
||||
|
||||
`AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL` — the supplied `principal` field
|
||||
is malformed (missing `id`, non-string `id`, non-array `scopes`, unknown
|
||||
keys inside `principal`, …).
|
||||
|
||||
Both fire synchronously at construction time. The resolver itself never
|
||||
throws at request time.
|
||||
|
||||
## Stage-5 wiring (forward-looking)
|
||||
|
||||
When `api-host/testing` ships its Stage-5 `createTestHost` surface, this
|
||||
adapter is registered in the adapter table under
|
||||
`kind: "static-principal"`, so tests will declare it inline:
|
||||
|
||||
```ts
|
||||
const host = createTestHost({
|
||||
adapters: {
|
||||
"auth.session": {
|
||||
kind: "static-principal",
|
||||
principal: { id: "u1", scopes: ["billing:write"], tenantId: "acme" },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Until then, callers wire the adapter directly via
|
||||
`createStaticPrincipalAdapter()` as shown above.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# `adapter` — composite `AuthAdapter` factory
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Bundles the static-principal resolver, the (absent) security-scheme
|
||||
contribution, and a pinned `csrf: false` into a single `AuthAdapter` value.
|
||||
This is the call-site shape for the canonical "test host with a known
|
||||
Principal" wiring — `host.registerAuth(createStaticPrincipalAdapter({...}))`
|
||||
gives the test the entire auth pipeline without further plumbing.
|
||||
|
||||
The factory delegates construction-time validation to
|
||||
`createStaticPrincipalResolver`, so misconfigured options surface as the
|
||||
same typed `AuthStaticPrincipalError` they do at the resolver level. The
|
||||
adapter holds no state of its own.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-call construction of the resolver + scheme + csrf triple.
|
||||
- Pins `csrf: false` because the static-principal layer has no session
|
||||
cookie to forge against — every request carries the configured Principal
|
||||
by construction.
|
||||
- `securityScheme` omitted (matching `api-auth-anonymous` topology) so the
|
||||
generated OpenAPI document doesn't advertise a real auth flow for
|
||||
routes wired to this adapter.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createStaticPrincipalAdapter } from "@suaveplan/api-auth-static-principal/adapter";
|
||||
|
||||
const adapter = createStaticPrincipalAdapter({
|
||||
principal: {
|
||||
id: "u1",
|
||||
scopes: ["billing:write"],
|
||||
tenantId: "acme",
|
||||
claims: { sub: "u1" },
|
||||
},
|
||||
});
|
||||
|
||||
host.registerAuth(adapter);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Compose with `applyTo` / `nullForPaths` to skip auth on test-only routes:
|
||||
|
||||
```ts
|
||||
import { createStaticPrincipalAdapter } from "@suaveplan/api-auth-static-principal/adapter";
|
||||
|
||||
const adapter = createStaticPrincipalAdapter({
|
||||
principal: { id: "u1", scopes: [], claims: {} },
|
||||
nullForPaths: ["/healthz", "/webhooks/*"],
|
||||
});
|
||||
|
||||
// host.registerAuth(adapter) — /healthz and /webhooks/* resolve to null;
|
||||
// every other route gets the configured Principal.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| ------------------------------------- | -------- | ---------------------------------------------------------- |
|
||||
| `createStaticPrincipalAdapter(opts)` | function | Returns `AuthAdapter` composed from the four building blocks. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`csrf: false` is the load-bearing pin: a static-principal host can't honour
|
||||
a real CSRF check (there's no session cookie carrying a synchronizer
|
||||
token), so the host should skip CSRF entirely for routes wired to this
|
||||
adapter. If a downstream test needs CSRF behaviour exercised in isolation,
|
||||
compose with a separate adapter that pins `csrf: true` and let the host's
|
||||
adapter-chain merge rule pick the safer value (the host's policy is to use
|
||||
the strictest `csrf` flag in the chain).
|
||||
|
||||
`validateAuthAdapter` accepts this adapter's shape (security scheme absent;
|
||||
csrf set to a boolean). The package's adapter test asserts the validation
|
||||
contract is satisfied without re-implementing the validator.
|
||||
@@ -0,0 +1,87 @@
|
||||
# `error-codes` — stable code registry for the static-principal adapter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the `AUTH_STATIC_PRINCIPAL.*` error-code namespace and the
|
||||
`AuthStaticPrincipalError` concrete subclass of
|
||||
`@suaveplan/error/SuaveplanError`. This module is the single source of truth
|
||||
for every failure code the package can surface to a caller — wiring this
|
||||
registry into the host's error envelope is how a malformed test-time
|
||||
Principal configuration reaches the developer rather than silently producing a
|
||||
broken auth chain.
|
||||
|
||||
Codes are produced via `defineCodes("AUTH_STATIC_PRINCIPAL", [...])` so the
|
||||
full literal value is e.g. `"AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS"`. The
|
||||
class augments the base error with `authStaticPrincipalCode`, providing a
|
||||
narrowed code property when consumers have already discriminated by
|
||||
`instanceof`.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable, frozen `AUTH_STATIC_PRINCIPAL_CODES` map built via `defineCodes` so
|
||||
the prefix can't drift.
|
||||
- Discriminated-union friendly: `AuthStaticPrincipalErrorCode` is a literal
|
||||
union over the registry entries.
|
||||
- Carries arbitrary `context` (Zod issues, the offending field path) via the
|
||||
base `SuaveplanError` options bag.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUTH_STATIC_PRINCIPAL_CODES,
|
||||
AuthStaticPrincipalError,
|
||||
} from "@suaveplan/api-auth-static-principal/error-codes";
|
||||
|
||||
throw new AuthStaticPrincipalError(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS,
|
||||
"createStaticPrincipalResolver: options failed schema validation"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Attach Zod issues so the caller can pinpoint the bad field without re-running
|
||||
validation:
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUTH_STATIC_PRINCIPAL_CODES,
|
||||
AuthStaticPrincipalError,
|
||||
} from "@suaveplan/api-auth-static-principal/error-codes";
|
||||
import z from "zod";
|
||||
|
||||
const parsed = OptionsSchema.safeParse(input);
|
||||
if (!parsed.success) {
|
||||
throw new AuthStaticPrincipalError(
|
||||
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL,
|
||||
"principal failed schema validation",
|
||||
{ context: { issues: parsed.error.issues } }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| ----------------------------------- | -------- | ------------------------------------------------------------------------ |
|
||||
| `AUTH_STATIC_PRINCIPAL_CODES` | constant | Frozen map of code constants. Keys are the short names; values are full. |
|
||||
| `AuthStaticPrincipalErrorCode` | type | Literal union over the registry values. |
|
||||
| `AuthStaticPrincipalError` | class | `SuaveplanError` subclass typed against the code registry. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` joins the prefix with each suffix using a single `.` so the
|
||||
resulting strings match the host's structured-log code field. The class
|
||||
stores the code twice — once in the inherited `code` slot and once in the
|
||||
narrower `authStaticPrincipalCode` slot — so callers that already narrowed by
|
||||
`instanceof` get a precisely-typed code field without a follow-up `as`.
|
||||
|
||||
The package's static-principal resolver never throws at request time; both
|
||||
codes are construction-time only. This is deliberate — every per-request
|
||||
failure for fixed-principal tests should manifest in the test assertion, not
|
||||
in the auth pipeline.
|
||||
@@ -0,0 +1,99 @@
|
||||
# `factory/create-static-principal-resolver` — fixed-Principal resolver
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Constructs a `SessionResolver` from `@suaveplan/api-auth` that returns the
|
||||
configured `Principal` verbatim on every request — no token decoding, no
|
||||
JWKS, no I/O. This is the workhorse adapter for plugin integration tests,
|
||||
the `api-plugin-example` fixture, and any environment where the goal is to
|
||||
exercise the downstream pipeline (RBAC gate, tenant resolver chain reading
|
||||
`principal.tenantId`, tenant-scoped DB) with a deterministic identity carried
|
||||
in config.
|
||||
|
||||
The factory consumes options previously validated by the `options` module so
|
||||
construction is total: every misuse maps to either
|
||||
`AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS` or `AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL`
|
||||
and surfaces synchronously at construction time. The resolver itself never
|
||||
throws at request time.
|
||||
|
||||
## Features
|
||||
|
||||
- Defensive clone on every `resolve()` — callers cannot mutate one returned
|
||||
Principal and observe the change on subsequent calls.
|
||||
- `applyTo(req)` predicate for arbitrary "return null for this request"
|
||||
rules.
|
||||
- `nullForPaths` suffix-glob list (`/webhooks/*`) and exact-match patterns
|
||||
for the most common public-route test setup.
|
||||
- Idempotent no-op `dispose()` matching the `SessionResolver` contract.
|
||||
- Returned objects are frozen (top-level + `scopes` + `claims`) to make
|
||||
attempted mutations fail loudly in strict mode.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createStaticPrincipalResolver } from "@suaveplan/api-auth-static-principal/resolver";
|
||||
|
||||
const resolver = createStaticPrincipalResolver({
|
||||
principal: {
|
||||
id: "u1",
|
||||
scopes: ["billing:write"],
|
||||
tenantId: "acme",
|
||||
claims: { sub: "u1" },
|
||||
},
|
||||
});
|
||||
|
||||
const principal = await resolver.resolve(
|
||||
new Request("https://example.test/api/billing/invoices")
|
||||
);
|
||||
// principal.id === "u1"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Combine `applyTo` and `nullForPaths` to model a mixed-route test surface:
|
||||
|
||||
```ts
|
||||
import { createStaticPrincipalResolver } from "@suaveplan/api-auth-static-principal/resolver";
|
||||
|
||||
const resolver = createStaticPrincipalResolver({
|
||||
principal: {
|
||||
id: "u1",
|
||||
scopes: ["billing:write", "billing:read"],
|
||||
tenantId: "acme",
|
||||
claims: { sub: "u1" },
|
||||
},
|
||||
nullForPaths: ["/webhooks/*", "/healthz"],
|
||||
applyTo: (req) => !req.headers.has("x-skip-auth"),
|
||||
});
|
||||
|
||||
// /healthz and /webhooks/stripe resolve to null.
|
||||
// Requests with the `x-skip-auth` header resolve to null.
|
||||
// Everything else returns the configured Principal.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| ----------------------------------- | -------- | ------------------------------------------------------------------ |
|
||||
| `createStaticPrincipalResolver(o)` | function | Returns a `SessionResolver` carrying `o.principal` on every match. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The clone strategy is: deep-freeze a single template at construction, then
|
||||
clone-from-template per `resolve()`. The per-call clone is unavoidable —
|
||||
otherwise two callers each freezing the returned object would freeze each
|
||||
other's state. The per-call clone copies the top level plus `scopes` (array
|
||||
spread) and `claims` (object spread); deeper structures inside `claims` are
|
||||
not cloned. This matches the `Principal` contract: `claims` values are
|
||||
`unknown`, so the package can't responsibly deep-clone them. Callers passing
|
||||
mutable nested claims values are responsible for not mutating them in-place.
|
||||
|
||||
`pathMatches` accepts only exact-match and a single suffix glob (`/foo/*`)
|
||||
on purpose — richer matching belongs in `applyTo`. The trailing slash is
|
||||
preserved when stripping the `*` so `/foo/*` matches `/foo/x` but not
|
||||
`/foox`. The pattern dialect is documented on the spec and exercised by the
|
||||
test file.
|
||||
@@ -0,0 +1,82 @@
|
||||
# `options` — Zod-validated options for `createStaticPrincipalResolver`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Owns the runtime contract between the test harness call site and the
|
||||
resolver factory. The schema validates both the outer options bag
|
||||
(`{ principal, nullForPaths?, applyTo? }`) and the nested `Principal` shape
|
||||
(non-empty `id`, readonly `scopes`, optional `tenantId`, default-`{}`
|
||||
`claims`). On failure it raises a typed `AuthStaticPrincipalError` carrying
|
||||
the originating Zod issues — splitting between `INVALID_OPTIONS` (anything
|
||||
outside `principal`) and `INVALID_PRINCIPAL` (anything inside it).
|
||||
|
||||
This module is the only place the package touches Zod at runtime. Keeping it
|
||||
isolated lets the resolver factory stay a thin three-line function and lets
|
||||
the conformance test target the resolver behaviour without invoking schema
|
||||
side-effects.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict outer schema — unknown keys at the top level reject with
|
||||
`INVALID_OPTIONS`.
|
||||
- Discriminated error codes: `principal` issues map to `INVALID_PRINCIPAL`;
|
||||
everything else maps to `INVALID_OPTIONS`.
|
||||
- Pre-flight type check rejects non-object inputs with `INVALID_OPTIONS`
|
||||
before Zod sees them — a cleaner message than the Zod default for `null` /
|
||||
arrays.
|
||||
- `claims` defaults to `{}` so callers don't have to write
|
||||
`claims: {}` for the common case.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseStaticPrincipalResolverOptions } from "@suaveplan/api-auth-static-principal/resolver";
|
||||
|
||||
const opts = parseStaticPrincipalResolverOptions({
|
||||
principal: {
|
||||
id: "u_1",
|
||||
scopes: ["billing:write"],
|
||||
tenantId: "acme",
|
||||
claims: { email: "user@acme.test" },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Validate options that include both `applyTo` and `nullForPaths`:
|
||||
|
||||
```ts
|
||||
import { parseStaticPrincipalResolverOptions } from "@suaveplan/api-auth-static-principal/resolver";
|
||||
|
||||
const opts = parseStaticPrincipalResolverOptions({
|
||||
principal: { id: "u_1", scopes: [], claims: {} },
|
||||
nullForPaths: ["/webhooks/*", "/healthz"],
|
||||
applyTo: (req) => !req.headers.has("x-skip-auth"),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| -------------------------------------------- | -------- | ----------------------------------------------------------------- |
|
||||
| `StaticPrincipalResolverOptionsSchema` | Zod | Strict object schema for the outer bag. |
|
||||
| `parseStaticPrincipalResolverOptions(raw)` | function | Returns the validated options or throws `AuthStaticPrincipalError`. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The pre-flight `typeof`/`Array.isArray` check lets the function return a
|
||||
single canonical error for "you handed me something that isn't an object" —
|
||||
Zod's own behaviour on `null` is to emit
|
||||
`{ code: "invalid_type", expected: "object" }`, which is correct but verbose.
|
||||
Splitting into a pre-flight + Zod pass is one extra branch but keeps the
|
||||
caller-facing failure message stable.
|
||||
|
||||
`claims` is recorded as `z.record(z.unknown())` so any free-form JWT/claims
|
||||
payload validates. Coercing to readonly happens at the TypeScript boundary
|
||||
(`StaticPrincipalResolverOptions`), not in Zod — Zod's `.readonly()` is
|
||||
type-level only and would re-wrap arrays at runtime unnecessarily.
|
||||
@@ -0,0 +1,75 @@
|
||||
# `security-scheme` — OpenAPI contribution for the static-principal adapter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides the per-adapter `SecuritySchemeContribution` slot of
|
||||
`AuthAdapter`. The static-principal adapter is a test-only construct: there
|
||||
is no real authentication flow to document on routes that use it. This
|
||||
module returns `undefined` deliberately — which `validateAuthAdapter` from
|
||||
`@suaveplan/api-auth` permits — so the generated OpenAPI document emits no
|
||||
security entry for routes under this adapter, mirroring how the anonymous
|
||||
tail adapter is treated.
|
||||
|
||||
Wrapping the absence in a function gives the package a symmetric surface
|
||||
with adapters that compute their contribution dynamically, and gives the
|
||||
test suite a callable to target for the "intentionally absent" assertion.
|
||||
|
||||
## Features
|
||||
|
||||
- Returns `undefined` so the host omits a `security` entry on routes wired
|
||||
to this adapter.
|
||||
- Permitted by `validateAuthAdapter` (the anonymous-style topology).
|
||||
- Callable surface for symmetry with adapters that compute their scheme.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { staticPrincipalSecurityScheme } from "@suaveplan/api-auth-static-principal/adapter";
|
||||
import type { AuthAdapter } from "@suaveplan/api-auth";
|
||||
|
||||
const adapter: AuthAdapter = {
|
||||
resolver,
|
||||
securityScheme: staticPrincipalSecurityScheme(),
|
||||
csrf: false,
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
When composing static-principal with another adapter that supplies a real
|
||||
scheme, drop this contribution and use the other adapter's scheme in the
|
||||
composite — the host requires only one to be present per route. The
|
||||
function still acts as documentation that the static-principal layer
|
||||
contributes nothing of its own:
|
||||
|
||||
```ts
|
||||
import { staticPrincipalSecurityScheme } from "@suaveplan/api-auth-static-principal/adapter";
|
||||
|
||||
if (staticPrincipalSecurityScheme() !== undefined) {
|
||||
// unreachable in this version — the function is documented to return
|
||||
// undefined. Tests assert this invariant.
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| ----------------------------------- | -------- | ------------------------------------------------ |
|
||||
| `staticPrincipalSecurityScheme()` | function | Returns `undefined` — no OpenAPI contribution. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`validateAuthAdapter` treats `securityScheme: undefined` as a permitted
|
||||
topology (the "no auth configured" path used by `api-auth-anonymous`). The
|
||||
generated OpenAPI document omits a `security` array on routes wired to an
|
||||
adapter that omits the scheme; combined with the host's `publicRoutes`
|
||||
decision, this matches the runtime behaviour of the static-principal
|
||||
adapter in tests.
|
||||
|
||||
Changing this to return a real scheme in a future version would be a
|
||||
breaking change for downstream consumers' OpenAPI snapshots; treat the
|
||||
"returns undefined" behaviour as a load-bearing contract.
|
||||
109
docs/api-stack/api/api-auth/README.md
Normal file
109
docs/api-stack/api/api-auth/README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# @suaveplan/api-auth - Abstract session-resolver contract for the Suaveplan API plugin system. Defines the SessionResolver interface (Request → Principal | null), SessionResolverChain (ordered, first-match-wins, fail-closed-on-throw), SecuritySchemeContribution (OpenAPI emission metadata), CsrfPolicy default rules, the composite AuthAdapter, the AUTH_CODES registry, and a runAuthSessionConformance harness reused by every concrete resolver in Stages 4/6/8.
|
||||
|
||||
**$2** for the Suaveplan API plugin system. Defines
|
||||
the abstract session-resolver shape (`Request → Principal | null`) used by the
|
||||
host on every request, plus the first-match-wins runner, the OpenAPI
|
||||
security-scheme contribution, the default CSRF policy, the composite
|
||||
`AuthAdapter`, and the conformance harness every concrete resolver
|
||||
implementation passes.
|
||||
|
||||
This package is **session resolution only** — account lifecycle (login,
|
||||
register, oauth callback, password reset) lives in the separate
|
||||
`@suaveplan/api-auth-provider` package per design doc §2.3.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [capability-id](./src/capability-id/capability-id.md) — Pins this package's `CapabilityId` contribution as a stand-alone literal type plus runtime constant. The `CapabilityId`…
|
||||
- [capability-id](./src/capability-id/capability-id.md) — Pins this package's `CapabilityId` contribution as a stand-alone literal type plus runtime constant. The `CapabilityId`…
|
||||
- [chain](./src/chain/chain.md) — Ordered, first-match-wins runtime over an array of `SessionResolver`s. The chain is the integration point at design-doc…
|
||||
- [chain](./src/chain/chain.md) — Ordered, first-match-wins runtime over an array of `SessionResolver`s. The chain is the integration point at design-doc…
|
||||
- [csrf-policy](./src/csrf-policy/csrf-policy.md) — Exposes a single pure predicate, `CsrfPolicy.requiresCsrf(scheme)`, that defaults CSRF protection ON for any cookie-bas…
|
||||
- [csrf-policy](./src/csrf-policy/csrf-policy.md) — Exposes a single pure predicate, `CsrfPolicy.requiresCsrf(scheme)`, that defaults CSRF protection ON for any cookie-bas…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable, namespaced error-code registry for the `@suaveplan/api-auth` package and the concrete `AuthError` c…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable, namespaced error-code registry for the `@suaveplan/api-auth` package and the concrete `AuthError` c…
|
||||
- [resolver](./src/resolver/resolver.md) — `SessionResolver` is the abstract per-request session-resolver shape that every concrete adapter in Stages 4/6/8 (`api-…
|
||||
- [resolver](./src/resolver/resolver.md) — `SessionResolver` is the abstract per-request session-resolver shape that every concrete adapter in Stages 4/6/8 (`api-…
|
||||
- [security-scheme](./src/security-scheme/security-scheme.md) — Defines the shape every concrete `SessionResolver` MUST contribute so the `api-openapi` package can emit the right `com…
|
||||
- [security-scheme](./src/security-scheme/security-scheme.md) — Defines the shape every concrete `SessionResolver` MUST contribute so the `api-openapi` package can emit the right `com…
|
||||
- [session-adapter](./src/session-adapter/session-adapter.md) — `AuthAdapter` is the composite shape every concrete session-auth adapter exports — pairing the per-request `SessionReso…
|
||||
- [session-adapter](./src/session-adapter/session-adapter.md) — `AuthAdapter` is the composite shape every concrete session-auth adapter exports — pairing the per-request `SessionReso…
|
||||
- [testing](./src/testing/conformance.md) — A reusable Vitest `describe(...)` block every concrete `SessionResolver` must pass before publishing. The harness cover…
|
||||
- [conformance](./src/testing/conformance.md) — A reusable Vitest `describe(...)` block every concrete `SessionResolver` must pass before publishing. The harness cover…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## What's inside
|
||||
|
||||
| Module | Subpath | Purpose |
|
||||
|-------------------------------|----------------------------------------|---------------------------------------------------------------|
|
||||
| `SessionResolver` interface | `@suaveplan/api-auth/resolver` | Abstract `resolve(req) → Principal \| null` + `dispose`. |
|
||||
| `SessionResolverChain` runner | `@suaveplan/api-auth/chain` | Ordered first-match-wins; fail-closed-on-throw by default. |
|
||||
| Security-scheme contract | `@suaveplan/api-auth/security-scheme` | Zod-validated OpenAPI security-scheme metadata. |
|
||||
| CSRF policy default | `@suaveplan/api-auth/csrf-policy` | Cookie schemes → ON; bearer / oauth2 / OIDC → OFF. |
|
||||
| Composite `AuthAdapter` | `@suaveplan/api-auth/session-adapter` | `{ resolver, securityScheme, csrf? }` registration shape. |
|
||||
| `auth.session` capability id | `@suaveplan/api-auth/capability-id` | Re-export of `"auth.session"` with type-level assertion. |
|
||||
| `AUTH_CODES` registry | `@suaveplan/api-auth/error-codes` | Frozen `AUTH.*` error-code registry + `AuthError` class. |
|
||||
| Conformance harness | `@suaveplan/api-auth/testing` | `runAuthSessionConformance(spec)` for concrete resolvers. |
|
||||
|
||||
## Co-located documentation
|
||||
|
||||
Each module ships a sibling `.md`:
|
||||
|
||||
- [`src/resolver/resolver.md`](src/resolver/resolver.md)
|
||||
- [`src/chain/chain.md`](src/chain/chain.md)
|
||||
- [`src/security-scheme/security-scheme.md`](src/security-scheme/security-scheme.md)
|
||||
- [`src/csrf-policy/csrf-policy.md`](src/csrf-policy/csrf-policy.md)
|
||||
- [`src/session-adapter/session-adapter.md`](src/session-adapter/session-adapter.md)
|
||||
- [`src/capability-id/capability-id.md`](src/capability-id/capability-id.md)
|
||||
- [`src/error-codes/error-codes.md`](src/error-codes/error-codes.md)
|
||||
- [`src/testing/conformance.md`](src/testing/conformance.md)
|
||||
|
||||
## Quick example
|
||||
|
||||
```ts
|
||||
import { SessionResolverChain } from "@suaveplan/api-auth/chain";
|
||||
import { validateAuthAdapter } from "@suaveplan/api-auth/session-adapter";
|
||||
import type { SessionResolver } from "@suaveplan/api-auth/resolver";
|
||||
|
||||
declare const jwt: SessionResolver;
|
||||
declare const staticBearer: SessionResolver;
|
||||
|
||||
const jwtAdapter = validateAuthAdapter({
|
||||
resolver: jwt,
|
||||
securityScheme: {
|
||||
name: "bearerAuth",
|
||||
scheme: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
|
||||
},
|
||||
});
|
||||
const bearerAdapter = validateAuthAdapter({
|
||||
resolver: staticBearer,
|
||||
securityScheme: {
|
||||
name: "staticBearer",
|
||||
scheme: { type: "http", scheme: "bearer" },
|
||||
},
|
||||
});
|
||||
|
||||
const chain = new SessionResolverChain([
|
||||
jwtAdapter.resolver,
|
||||
bearerAdapter.resolver,
|
||||
]);
|
||||
|
||||
const principal = await chain.resolve(new Request("https://example.test"));
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
1.0`. Graduates to `1.0.0` when the Stage 1
|
||||
contract bundle (`api-types`, `api-auth`, `api-tenant-resolver`,
|
||||
`api-secrets`, `api-db-adapter`) all reach 1.0.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Dep | Why |
|
||||
|---------------------------|------------------------------------------------------|
|
||||
| `@suaveplan/api-types` | `Principal`, `CapabilityId`. |
|
||||
| `@suaveplan/error` | `SuaveplanError`, `defineCodes`. |
|
||||
| `@suaveplan/types` | Cross-package contracts (logger, clock, etc.). |
|
||||
| `tslib` | TypeScript runtime helpers (catalog). |
|
||||
| `zod` | Boundary validation for security-scheme contributions. |
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# `capability-id` — `"auth.session"` contribution
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Pins this package's `CapabilityId` contribution as a stand-alone literal type
|
||||
plus runtime constant. The `CapabilityId` union in
|
||||
`@suaveplan/api-types/capabilities` already enumerates `"auth.session"`
|
||||
directly; this module ties the api-auth package to that identifier so any
|
||||
future rename surfaces here first as a build break.
|
||||
|
||||
Exporting the runtime literal as `AUTH_SESSION_CAPABILITY_ID` lets plugin
|
||||
manifests declare `requires: [AUTH_SESSION_CAPABILITY_ID] as const` without
|
||||
hard-coding the string at the manifest site — refactor-safe and centrally
|
||||
documented.
|
||||
|
||||
## Features
|
||||
|
||||
- Compile-time `const _assignable: CapabilityId = AUTH_SESSION_CAPABILITY_ID`
|
||||
asserts membership at build time. If `api-types` ever drops the token, this
|
||||
package fails to type-check rather than silently regressing.
|
||||
- Type and value are exported together so the consumer can pick the form that
|
||||
fits the call site without re-stringifying.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { AUTH_SESSION_CAPABILITY_ID } from "@suaveplan/api-auth/capability-id";
|
||||
|
||||
const requires = [AUTH_SESSION_CAPABILITY_ID] as const;
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Use the literal type to constrain a manifest builder helper:
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUTH_SESSION_CAPABILITY_ID,
|
||||
type AuthSessionCapabilityId,
|
||||
} from "@suaveplan/api-auth/capability-id";
|
||||
import type { CapabilityId } from "@suaveplan/api-types/capabilities";
|
||||
|
||||
function manifestRequires<const T extends readonly CapabilityId[]>(req: T): T {
|
||||
return req;
|
||||
}
|
||||
|
||||
const requires = manifestRequires([
|
||||
AUTH_SESSION_CAPABILITY_ID,
|
||||
"logger",
|
||||
] satisfies readonly (AuthSessionCapabilityId | "logger")[]);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---------------------------------|------|----------------------------------------------|
|
||||
| `AuthSessionCapabilityId` | type | The literal `"auth.session"`. |
|
||||
| `AUTH_SESSION_CAPABILITY_ID` | const| `"auth.session"` runtime value. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `void _assignable` keeps the assignment from being tree-shaken; without
|
||||
it Rollup MAY drop the line and the build-time assertion silently regresses
|
||||
to no-op.
|
||||
- This module is import-time-only — no behaviour at runtime beyond the
|
||||
string literal binding.
|
||||
- The literal is exported as both a `type` and a runtime `const` deliberately:
|
||||
consumers that build manifests imperatively want the runtime value; consumers
|
||||
that constrain compile-time-only structures (a `definePlugin` builder, for
|
||||
example) want the literal type. Re-stringifying `"auth.session"` at the
|
||||
consumer side would defeat the purpose of having the package central.
|
||||
- The pairing with `@suaveplan/api-types/capabilities` is deliberately
|
||||
one-directional. `api-types` is the source of truth for the closed union;
|
||||
this package only re-asserts membership and binds the runtime value. If a
|
||||
future change wanted to flip the relationship — have api-auth contribute via
|
||||
declaration merging instead — that would require relaxing the `CapabilityId`
|
||||
union in api-types to be `open` (i.e. `string & { brand }`), which is a
|
||||
larger contract change documented as out-of-scope here.
|
||||
- The runtime constant uses a type annotation rather than `as const`: the
|
||||
annotation makes the assignable check above explicit, and `as const` would
|
||||
widen any future enum-style rewrite.
|
||||
89
docs/api-stack/api/api-auth/src/chain/chain.md
Normal file
89
docs/api-stack/api/api-auth/src/chain/chain.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# `chain` — `SessionResolverChain`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Ordered, first-match-wins runtime over an array of `SessionResolver`s. The
|
||||
chain is the integration point at design-doc §6 step 3: the host's per-request
|
||||
pipeline calls `chain.resolve(req)` exactly once and routes the result into the
|
||||
`RequestContext.principal` slot (`publicRoutes` decides 401 vs `null`).
|
||||
|
||||
The chain owns three orthogonal contracts that are easy to get wrong if every
|
||||
adapter re-implements them: (1) iteration order — first non-null wins, no
|
||||
later resolver is invoked; (2) fail-closed throw semantics — a resolver
|
||||
exception surfaces `AUTH.RESOLVER_THREW` and short-circuits the chain, with an
|
||||
opt-in `continueOnThrow` escape hatch documented for SaaS-provider flakiness;
|
||||
(3) empty-chain rejection — `new SessionResolverChain([])` throws
|
||||
`AUTH.CHAIN_EMPTY` at construction so deployment misconfiguration cannot mask
|
||||
itself as "no resolvers, every request 401s."
|
||||
|
||||
## Features
|
||||
|
||||
- Class form implements `SessionResolver` itself — the chain can be nested
|
||||
inside another chain, used as the host's only resolver, or composed with
|
||||
middleware identically.
|
||||
- Idempotent `dispose()` propagates to every wrapped resolver.
|
||||
- Cause-chain propagation via `AuthError`'s `cause` option; the index of the
|
||||
throwing resolver is recorded in `context.resolverIndex`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { SessionResolverChain } from "@suaveplan/api-auth/chain";
|
||||
import type { SessionResolver } from "@suaveplan/api-auth/resolver";
|
||||
|
||||
declare const jwtResolver: SessionResolver;
|
||||
declare const bearerStaticResolver: SessionResolver;
|
||||
|
||||
const chain = new SessionResolverChain([jwtResolver, bearerStaticResolver]);
|
||||
|
||||
const principal = await chain.resolve(new Request("https://example.test"));
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Enable `continueOnThrow` for a deployment that fronts an intentionally flaky
|
||||
SaaS auth upstream — the chain logs the cause via the `AuthError` it would
|
||||
have thrown, but keeps moving to the static-bearer fallback resolver:
|
||||
|
||||
```ts
|
||||
import { SessionResolverChain } from "@suaveplan/api-auth/chain";
|
||||
import type { SessionResolver } from "@suaveplan/api-auth/resolver";
|
||||
|
||||
declare const saasJwtResolver: SessionResolver;
|
||||
declare const staticBearerResolver: SessionResolver;
|
||||
|
||||
const chain = new SessionResolverChain(
|
||||
[saasJwtResolver, staticBearerResolver],
|
||||
{ continueOnThrow: true },
|
||||
);
|
||||
|
||||
const req = new Request("https://example.test/api/orders", {
|
||||
headers: { authorization: "Bearer st_ops_token" },
|
||||
});
|
||||
|
||||
const principal = await chain.resolve(req); // may come from staticBearerResolver
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|---------------------------------|--------|------------------------------------------------------------|
|
||||
| `SessionResolverChain` | class | `new(resolvers, options?)`; implements `SessionResolver`. |
|
||||
| `SessionResolverChainOptions` | type | `{ continueOnThrow?: boolean }`. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The chain iterates synchronously over `this.resolvers` and awaits each
|
||||
resolver in turn — concurrent resolution is intentionally NOT offered.
|
||||
Concurrency would change the "first match wins" semantics into "first to
|
||||
return wins," producing a non-deterministic principal selection when two
|
||||
resolvers both recognise the request.
|
||||
- The throw-handling branch records the resolver index but NOT the resolver
|
||||
identity — concrete adapters MAY redact stack traces, and the chain has no
|
||||
inspection contract on its members beyond `resolve` + `dispose`.
|
||||
- `dispose()` is idempotent via a `disposed` flag; the conformance harness
|
||||
calls it twice and asserts no throw.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user