Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,213 @@
# add-api-plugin-notifications
**Package**: `@suaveplan/api-plugin-notifications`
**Status**: PROPOSED
**Type**: new-package
**Design**: (none — straightforward migration; see this proposal + spec delta)
**Spec delta**: ./specs/api-plugin-notifications/spec.md
## Why
Genesis ships the foundational pieces of `@suaveplan/api` (database,
search, events, auth, site, etc.) but is missing the
**notifications** plugin — in-app notification CRUD, device-token
registration, and APN push delivery. Without this plugin, downstream
apps that need lock-screen pushes or in-app notification feeds have
to hand-roll the surface. Upstream
`enterprise/api-plugin-notifications.md` documents the audit (4
`*.test.ts` files, two `.spec.ts` Playwright integration files, the
APN mock smell, the mock middleware no-op, and the
`autoCleanOldDays` abandoned config flag).
This change brings the package across at full functional parity,
adapted to the genesis `@suaveplan/api` plugin shape
(`PluginDescriptor`, `ctx.getService(...)`), the genesis
`DatabaseAdapter` positional-param surface, and the
`ctx.eventBus` cross-plugin emission pattern. The local-emitter
wart and the APN mock are both replaced — the APN service now
shapes payloads against a **pluggable transport interface**
(`ApnTransport`) so the plugin never depends on a concrete APN
client library. Production hosts inject a `node-apn` adapter; the
plugin ships a deterministic `MemoryApnTransport` that records every
send for hermetic tests.
## What changes
- ADDED: `createNotificationsPlugin(options?)` — factory returning a
`PluginDescriptor<NotificationsPluginConfig>` for `@suaveplan/api`.
- ADDED: `NotificationsPluginOptionsSchema` / `NotificationsPluginConfig`
/ `NotificationsPluginConfigInput` — Zod-validated config
(basePath, pushBasePath, enablePush, apn, autoCleanOldDays,
maxBulkRecipients).
- ADDED: `ApnConfigSchema` / `ApnConfig` — Zod validation of the
APN config bundle (cert/key/pfx/token + topic + production +
network knobs).
- ADDED: `NotificationRepository` + `INotificationRepository` —
SurrealDB-backed notification CRUD repository over the genesis
`SurrealAdapter` service slot.
- ADDED: `DeviceTokenRepository` + `IDeviceTokenRepository` —
SurrealDB-backed device-token CRUD repository.
- ADDED: `createNotificationsRoutes` — Hono OpenAPI route module
with 11 endpoints (list / unread-count / create / get / patch /
read / read-many / read-all / dismiss / delete / grouped collapses
into list).
- ADDED: `createPushRoutes` — Hono OpenAPI route module with 5
endpoints (register / unregister / list tokens / send / send-bulk).
- ADDED: `ApnService` + `createApnService` — APN delivery service
with a pluggable transport.
- ADDED: `MemoryApnTransport` + `createMemoryApnTransport` +
`ApnTransport` / `ApnTransportNotification` /
`ApnTransportResponse` / `ApnTransportFactory` — the transport
contract and a deterministic default implementation.
- ADDED: `NOTIFICATIONS_PLUGIN_CODES` registry +
`NotificationsPluginError` class — 18 stable codes spanning
configuration, lifecycle, repository, device-token, route, and APN
failure modes.
- ADDED: OTel instruments — 6 counters
(`notificationsCreatedTotal`, `notificationsReadTotal`,
`notificationsDeletedTotal`, `pushSentTotal{platform}`,
`pushFailedTotal{reason}`, `deviceTokensRegisteredTotal{platform}`).
- ADDED: ServiceRegistry augmentation — `notificationRepository`,
`deviceTokenRepository`, `apnService` slots become available to
downstream plugins via typed `ctx.getService(...)`.
- ADDED: Local Zod schemas for notification and push surfaces
(`NotificationSchema`, `CreateNotificationSchema`,
`UpdateNotificationSchema`, `NotificationQuerySchema`,
`NotificationListResponseSchema`, `MarkManyAsReadSchema`,
`DeviceTokenSchema`, `RegisterDeviceTokenSchema`,
`SendPushNotificationSchema`, `SendBulkPushNotificationSchema`,
etc.) — `@suaveplan/api-schemas` on genesis does not yet ship a
`notification` subpath, so the plugin owns the boundary schemas.
- ADDED: `surql/tables/notification.surql` +
`surql/tables/device_token.surql` — slimmed migrations covering
only the columns the repositories read/write. The upstream
cross-plugin permission rules, stored helper functions
(`fn::notification::*`, `fn::device::token::*`,
`fn::push::queue::*`, `fn::push::history::*`), bitwise-typed
notification-type constants, and `push_notification_queue` /
`push_notification_history` tables are excluded (see below).
- ADDED: `NotificationsPluginEvents` event-map keys —
`notifications:initialized`, `notifications:ready`,
`notification:created`, `notification:read`,
`notification:dismissed`, `notification:deleted`,
`notification:push:sent`, `notification:push:failed`,
`notification:device:registered`,
`notification:device:unregistered`.
## Excluded from this migration
These items from `enterprise/api-plugin-notifications.md`
"Required enterprise-quality improvements" and "Feature-rich
enhancements" are deferred because each requires cross-package deps
that don't yet exist on genesis (the same scope-out pattern that
`add-api-plugin-events` used for `fn::notification`, `fn::bitwise`,
`fn::group` references):
- Real `@parse/node-apn` integration in the production codepath —
the catalog deliberately omits `@parse/node-apn`. The migration
ships an `ApnTransport` injection contract instead; production
hosts add the library to their application dependencies and
inject a `node-apn`-backed transport. The deterministic
`MemoryApnTransport` ships with the plugin and is the default in
tests + dev.
- FCM / Web Push / Email / SMS / Slack channels — depend on
`@suaveplan/notification-channel`,
`@suaveplan/emailer`, `@suaveplan/sms-providers`,
`@suaveplan/slack-client` which are not yet on genesis. Deferred
to `add-api-plugin-notifications-channels`.
- `autoCleanOldDays` scheduled cleanup — `deleteOldRead(daysOld)`
is shipped on the repository and is callable; wiring it through
`@suaveplan/job-scheduler` on `onReady` waits for the job-scheduler
plugin (concurrent migration). Deferred to
`add-api-plugin-notifications-cleanup-job`.
- Preference center, quiet hours, templates, suppression lists,
delivery tracking, batching+backoff queue, rich media, digests,
in-app SSE/WS — every one of these requires a sibling plugin that
doesn't exist on genesis yet (`@suaveplan/templates`,
`@suaveplan/suppression`, `@suaveplan/delivery-tracking`,
`@suaveplan/queue`, `@suaveplan/api-plugin-sse`,
`@suaveplan/websocket-gateway`, `@suaveplan/datetime`,
`@suaveplan/i18n`). Each lands in its own follow-up change.
- Auth gating via `@suaveplan/api-plugin-auth` — this plugin reads
`c.get("user")` (set by the host's auth middleware) and raises
`UNAUTHENTICATED` when missing; explicit `requireAuth()`
composition is a host-level concern.
- Audit-log emission, idempotency, rate-limit — depend on
`@suaveplan/api-plugin-audit`, `@suaveplan/idempotency`,
`@suaveplan/api-plugin-rate-limit` (concurrent migrations); will
be composed at the host level once they land.
- Upstream `push_notification_queue` / `push_notification_history`
tables, `fn::push::queue::*`, `fn::push::history::*`,
`fn::notification::create` / `should_create` / `friend_request` /
`post_like` / `comment` SurrealQL helpers — all reference
`record<site>` / `record<post>` / `record<comment>` /
`record<group>` types and call `fn::bitwise::has_bits` /
`fn::settings::get` / `fn::session::is_root` which only exist
inside sibling plugins (`api-plugin-site`,
`api-plugin-bitwise`, `api-plugin-groups`). To avoid hand-rolling
four downstream plugins as a side-quest, the migration ships a
slimmer schema covering only the columns the repositories read /
write; queue + history + helper functions migrate when their
dependent plugins land.
## Source mapping
This change derives from
`../suaveplan/enterprise/api-plugin-notifications.md` and the upstream
source at `../suaveplan/packages/server/api-plugin-notifications/`.
| Requirement | Source lines | Notes |
|---|---|---|
| Plugin factory `createNotificationsPlugin(config?)` | enterprise.md L17-L29; src/index.ts L64-L130 | reworded — genesis uses `PluginDescriptor` not `Plugin<NotificationConfig>` |
| Config schema (basePath, pushBasePath, enablePush, apn, autoCleanOldDays) | enterprise.md L13, L27-L56; src/index.ts L27-L56 | extended with `pushBasePath` and `maxBulkRecipients` for genesis route-mount + bulk cap |
| Plugin dependencies: `["database"]` | enterprise.md L18; src/index.ts L75 | verbatim |
| `migrationsPath` removed in genesis | enterprise.md L86; src/index.ts L86 | the database plugin owns migration application in genesis |
| NotificationRepository: create/findById/findByRecipient/getUnreadCount/update/markAsRead/markManyAsRead/markAllAsRead/dismiss/delete/findGroupedByType/deleteOldRead | src/repository/notification.repository/notification.repository.ts | reworded — typed `db.create/select/update/delete` replaced by positional SurQL (api-plugin-events Path-A pattern); upstream `getById` → `findById`, `getByRecipient` → `findByRecipient`, `getGroupedByType` → `findGroupedByType`, `deleteOldNotifications` → `deleteOldRead` |
| DeviceTokenRepository: register/findById/findByToken/findActiveForUser/deactivate/deactivateByToken/touchLastUsed/cleanup | upstream surql/tables/device_token.surql helpers | new — upstream put device-token CRUD in stored functions; genesis pulls them up into a TS repository so the plugin can run without the surql helpers |
| createNotificationsRoutes (11 endpoints) | src/routes/notifications/notifications.ts | verbatim — `ctx.eventBus` replaces local `EventEmitter`; `grouped` collapses into list filter |
| createPushRoutes (5 endpoints) | src/routes/push/push.ts | reworded — `device_registration` table folded into `device_token`; bulk send capped by `maxBulkRecipients` schema bound |
| APNService (initialize/sendNotification/sendBulkNotifications/shutdown/isInitialized) | src/services/apn/apn.service.ts | reworded — `apn.Provider` swapped for an injectable `ApnTransport`; same payload shape, same `validateConfig` rules, same `mapResponse` semantics |
| MemoryApnTransport (deterministic in-memory delivery) | n/a (genesis-original) | required because the genesis catalog omits `@parse/node-apn` |
| `notification` SurrealQL table | upstream surql/tables/notification.surql | slimmed — see `## Excluded from this migration` |
| `device_token` SurrealQL table | upstream surql/tables/device_token.surql | slimmed |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| 100% test / branch / function / line coverage | Genesis-wide policy (CLAUDE.md "Quality", AGENTS.md §7). |
| `defineCodes("NOTIFICATIONS_PLUGIN", [...])` namespace | Genesis convention; avoids collision with `EVENTS` (`@suaveplan/events`) and `EVENTS_PLUGIN` (`@suaveplan/api-plugin-events`). |
| OTel instruments via `metrics.getMeter("@suaveplan/api-plugin-notifications")` | Genesis-wide observability policy; emitted from production paths and asserted with a real in-memory `MeterProvider` (AGENTS.md §13). |
| Use `ctx.eventBus` from `@suaveplan/api-plugin` context, not a local `EventEmitter` | Fixes the upstream smell where notifications fired into a private `EventEmitter` invisible to siblings. |
| Subpath exports per feature module | AGENTS.md §6 — required so consumers can tree-shake. |
| Schemas owned locally | `@suaveplan/api-schemas` on genesis lacks a `notification` subpath; the plugin's schemas live inside `src/schemas/` until upstream is augmented. |
| Positional `$p0`-style params instead of named `$id` | Genesis `DatabaseAdapter.query(sql, params: readonly unknown[])` wraps params as `{p0: …, p1: …}`. Path-A escape valve documented in CLAUDE.md. |
| Injectable `ApnTransport` | Genesis catalog omits `@parse/node-apn` (network-touching libraries are injected by the host). Matches the genesis convention for transport adapters and means the plugin's test suite is hermetic. |
## Verification approach
- `src/error-codes/error-codes.test.ts` — pins every code is reachable from a real call site.
- `src/otel/instruments.test.ts` — real `MeterProvider`, asserts instrument shape + emission.
- `src/schemas/notification-schemas.test.ts` + `src/schemas/push-schemas.test.ts` + `src/schemas/options.test.ts` — Zod boundary tests.
- `src/repositories/notification-repository/notification-repository.test.ts` — full repository surface against a stub `SurrealAdapter`.
- `src/repositories/device-token-repository/device-token-repository.test.ts` — same coverage shape for device-tokens.
- `src/transport/apn-transport/apn-transport.test.ts` — `MemoryApnTransport` behaviour including failure plans.
- `src/services/apn-service/apn-service.test.ts` — full APN service surface, every error branch, transport-injection and factory pathways.
- `src/routes/notifications-routes/notifications-routes.test.ts` — every route, every status code, ownership check, auth guard, event-bus emission, OTel emission.
- `src/routes/push-routes/push-routes.test.ts` — register / unregister / list / send / send-bulk against an in-memory APN service.
- `src/plugin/plugin.test.ts` — lifecycle: onInit registers repos + APN, double-mount guard, missing-dep failure, emits initialized/ready.
- `src/api-plugin-notifications.spec-conformance.test.ts` — pins every public symbol re-exported by the package root.
- `tests/integration/full-boot.test.ts` — real SurrealDB testcontainer, applies the two surql migrations, then full plugin boot + CRUD roundtrip on both repositories.
## Migration impact
None — additive only. This is a brand-new package on genesis.
Consumers will opt-in via `createNotificationsPlugin()` once they
need notification routes.
## Impact
### Affected dependents
None on genesis today — package is additive. Downstream apps that
previously had no notifications capability gain access.