Initial Commit
This commit is contained in:
90
openspec/archive/2026-05-18-add-api-plugin-user/proposal.md
Normal file
90
openspec/archive/2026-05-18-add-api-plugin-user/proposal.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Add @suaveplan/api-plugin-user — user CRUD on top of api-plugin-auth (3-way plug-and-play)
|
||||
|
||||
**Package**: `@suaveplan/api-plugin-user` (NEW, server-tier)
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
**Position**: depends on `add-api-plugin-auth`
|
||||
|
||||
- **Spec delta**: ./specs/api-plugin-user/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
`@suaveplan/api-plugin-auth` (sibling) owns identity: it ships the `user` table (via the bundled Better-Auth schema), the `Principal` type, the session resolver, and the account-lifecycle routes (sign-up / sign-in / oauth). What it deliberately does NOT ship is **user-profile CRUD** for the authenticated principal — operators inevitably need `/users/me` for profile reads and updates and `DELETE /users/me` for account-erasure flows.
|
||||
|
||||
This package adds exactly that — three routes plus a `userService` for downstream plugins to look up users by id. It is the second instance of the **3-way plug-and-play contract**: its `.surql` schema extends Better-Auth's `user` table with profile fields, its Zod schemas validate every body, and its OpenAPI route definitions register through the same `definePluginRoutes` path the host serves at `/openapi.json`.
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package at `0.1.0`. ADDED:
|
||||
|
||||
### Plugin factory + descriptor
|
||||
|
||||
- `createUserPlugin(options): PluginDescriptor<UserPluginConfig>` — `definePlugin` consumer. `options` Zod-validated; `dependencies: ["auth"]` so the registry topo-sorts after `api-plugin-auth`.
|
||||
- `UserPluginOptions` (Zod-validated): `{ routePrefix?: string (default "/users"); allowSelfDelete?: boolean (default true); profileFields?: readonly string[] (default ["displayName", "avatarUrl", "locale", "timezone"]); applySchemaOnInit?: boolean (default true); logger?: LoggerLike }`.
|
||||
|
||||
### Routes (3)
|
||||
|
||||
All built with `RouteBuilder` from `@suaveplan/api-openapi`; all gated by `requireAuth()` from `@suaveplan/api-plugin-auth`:
|
||||
|
||||
- `GET /users/me` — response: `UserProfileResponseSchema` (Principal + profile fields)
|
||||
- `PATCH /users/me` — body: `UpdateUserProfileBodySchema` (subset of profile fields); response: `UserProfileResponseSchema`
|
||||
- `DELETE /users/me` — empty body; response: `{ deleted: true }`. Disabled when `allowSelfDelete: false` → 403 `USER_SELF_DELETE_DISABLED`. When enabled, invokes Better-Auth's user-delete API via `authService.getBetterAuthInstance()` so all sessions/accounts cascade.
|
||||
|
||||
### userService
|
||||
|
||||
- `userService` registered via `ctx.registerService("userService", ...)`. Surface: `{ getById(id: string): Promise<UserProfile | null>; getCurrent(c: Context): UserProfile | null; updateProfile(id: string, patch: Partial<UserProfile>): Promise<UserProfile> }`.
|
||||
- ServiceRegistry module augmentation: `declare module "@suaveplan/api-plugin/service-registry" { interface ServiceRegistry { userService: UserService } }`.
|
||||
|
||||
### .surql schema (plug-and-play point 1)
|
||||
|
||||
- `./schema` exports `USER_PROFILE_SCHEMA` (frozen string) — DDL extending Better-Auth's `user` table with the configured `profileFields` (string typed, optional). DDL uses `DEFINE FIELD OVERWRITE` for idempotency. Genesis-original (no upstream port — Better-Auth's schema covers identity, not profile).
|
||||
- `applyUserProfileSchema(adapter, profileFields): Promise<void>` convenience helper. `onInit` applies via `migrationRunner` when registered (preferred) or `db` fallback.
|
||||
|
||||
### Zod schemas (plug-and-play point 2) — exposed at `./schemas/*`
|
||||
|
||||
- `./schemas/user-profile` — `UserProfileSchema`, `UserProfileResponseSchema`, `UpdateUserProfileBodySchema` (Zod). `UserProfile` extends `Principal` (re-imported from `@suaveplan/api-plugin-auth/schemas/principal`) with `displayName?`, `avatarUrl?`, `locale?`, `timezone?`. Operators with custom `profileFields` extend via `UserProfileSchema.extend({...})`.
|
||||
- `./schemas/options` — `UserPluginOptionsSchema`.
|
||||
|
||||
### Errors + observability
|
||||
|
||||
- `UserError extends SuaveplanError<UserErrorCode>` via `defineCodes("USER", [...])`. Codes: `USER_NOT_FOUND`, `USER_INVALID_INPUT`, `USER_SELF_DELETE_DISABLED`, `USER_PROFILE_UPDATE_FAILED`, `USER_SERVICE_NOT_AVAILABLE`, `USER_SCHEMA_APPLY_FAILED`, `USER_ROUTE_PREFIX_COLLISION`.
|
||||
- OTel: counter `user.profile_reads_total`, counter `user.profile_updates_total{outcome}`, counter `user.self_deletes_total`.
|
||||
|
||||
### Subpath exports
|
||||
|
||||
`.`, `./plugin/plugin`, `./services/user-service`, `./schemas/user-profile`, `./schemas/options`, `./routes/routes`, `./schema/schema`, `./error-codes/error-codes`, `./otel/instruments`, `./types/types`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Admin-side user listing / search / role-edit endpoints.** Belong in a separate `api-plugin-admin` follow-up.
|
||||
- **Avatar upload + resizing.** The plugin stores `avatarUrl` as a string; resize/upload pipelines live in a separate `api-plugin-media` follow-up.
|
||||
- **GDPR export.** Belongs in `api-plugin-gdpr` follow-up (`DELETE /users/me` triggers Better-Auth's user-delete cascade; the export side is a separate flow).
|
||||
- **Organisations / team membership CRUD.** Better-Auth ships an `organization` plugin; surfacing those routes is `api-plugin-organizations` (follow-up).
|
||||
- **Custom-shape profile fields beyond the operator-supplied flat-string list.** Operators who need nested objects or relations augment `UserProfileSchema` in their own plugin layer.
|
||||
|
||||
## Source mapping
|
||||
|
||||
This package is **genesis-original**. Design intent draws from:
|
||||
|
||||
- The 3-way plug-and-play contract established by `add-api-plugin-auth` (sibling).
|
||||
- `@suaveplan/api-plugin-auth/schemas/principal` re-export pattern.
|
||||
- The legacy `enterprise-api-plugin-user.md` proposal's route surface (verbatim: `/users/me` GET/PATCH/DELETE).
|
||||
|
||||
| Requirement | Source |
|
||||
|---|---|
|
||||
| `definePlugin` consumer with `dependencies: ["auth"]` | `@suaveplan/api-plugin` 4-phase lifecycle |
|
||||
| `requireAuth()` middleware | `@suaveplan/api-plugin-auth` (sibling) |
|
||||
| `RouteBuilder` end-to-end Zod inference | `@suaveplan/api-openapi` shipped 99a213ed |
|
||||
| `userService` ServiceRegistry slot pattern | api-plugin-auth's `authService` pattern (verbatim) |
|
||||
| Better-Auth user-delete cascade on `DELETE /users/me` | Better-Auth `auth.api.removeUser` (or equivalent) |
|
||||
| Schema extension via `DEFINE FIELD OVERWRITE` | genesis target schema convention (`/mnt/f/surrealdb/surql/`) |
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
None today — net-new. Will be consumed by `add-api-plugin-vault` (vaults belong to a user — the vault plugin reads the principal's id from `authService.getCurrentPrincipal(c)` and writes vault items scoped to it).
|
||||
|
||||
### Catalog additions
|
||||
|
||||
None — every dep already in catalogs from sibling packages.
|
||||
Reference in New Issue
Block a user