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