first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
# `services/apn-service`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Apple Push Notification delivery service. Validates an `ApnConfig` at
construction-time, builds a pluggable `ApnTransport` (defaulting to
the deterministic `MemoryApnTransport`), translates the public
`PushNotificationPayload` shape into the transport's library-agnostic
notification format, and maps the transport's response into a
`PushNotificationResult` envelope. Per-device delivery failures NEVER
throw — they land in the result's `errors[]` array so callers can
branch on them without try/catch noise.
## Features
- Three stable failure modes: `APN_CONFIG_MISSING` (no auth
material), `APN_CONFIG_INVALID` (partial token auth),
`APN_NOT_INITIALIZED` (send called after `shutdown()`), plus
transport-level codes (`APN_TRANSPORT_INIT_FAILED`,
`APN_TRANSPORT_SHUTDOWN_FAILED`).
- Auto-initialise on first send — `sendNotification(...)` calls
`initialize()` if the transport isn't live yet.
- Bulk send batches in a single transport call so the underlying
HTTP/2 connection is reused.
- `isInitialized()` returns the live state so route handlers can
short-circuit to `503` when push is disabled.
- Default `pushType` resolution mirrors APN's contract: `"alert"` when
the payload carries title or body, `"background"` when
`contentAvailable` is set, otherwise `"alert"`. Callers may
override.
## Usage: Basic
```ts
import {
ApnService,
createMemoryApnTransport,
} from "@suaveplan/api-plugin-notifications";
const service = new ApnService({
config: {
token: { key: "/keys/AuthKey_ABC.p8", keyId: "ABC", teamId: "TEAM" },
production: false,
defaultTopic: "com.example.app",
},
transportFactory: createMemoryApnTransport,
});
await service.initialize();
const result = await service.sendNotification({
deviceToken: "deadbeef",
alert: { title: "Hi", body: "Welcome" },
badge: 1,
});
console.log(result.sent, result.failed);
```
## Usage: Advanced
Inject a custom transport (e.g. a real `node-apn` adapter) and let the
service own the validation + payload translation:
```ts
import { ApnService, type ApnTransport } from "@suaveplan/api-plugin-notifications";
class NodeApnTransport implements ApnTransport {
async initialize() { /* … */ }
async send(n, tokens) { /* … */ }
async shutdown() { /* … */ }
}
const service = new ApnService({
config: parsedApnConfig,
transport: new NodeApnTransport(),
});
const result = await service.sendBulkNotifications(
["tok1", "tok2", "tok3"],
{ alert: { title: "Bulk", body: "Send" } }
);
```
## API Reference
- `ApnService` — class with `initialize` / `sendNotification` /
`sendBulkNotifications` / `shutdown` / `isInitialized`.
- `ApnServiceOptions` — `{ config, transportFactory?, transport? }`.
- `PushNotificationPayload` — single-message payload.
- `PushNotificationFailure` — per-device failure (`deviceToken`,
`error`, optional `status` / `reason`).
- `PushNotificationResult` — `{ sent, failed, errors? }`.
- `createApnService(options)` — thin factory matching the upstream
surface.
## Implementation Notes
The service never imports `@parse/node-apn` directly — that library
is deliberately absent from the catalog (network-touching
adapters are injected by the host). Tests use the
`MemoryApnTransport` with optional failure plans so every error
branch is exercised without touching the network. Production hosts
write a thin `NodeApnTransport` adapter against their existing
`apn.Provider` and pass it in via `options.transport`.

View File

@@ -0,0 +1,62 @@
# `services/registry-augmentation`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Module-augmentation file that declares three new service slots on
`@suaveplan/api-plugin/service-registry`'s `ServiceRegistry`
interface: `notificationRepository`, `deviceTokenRepository`, and
`apnService`. Without this augmentation the typed `ctx.getService(...)`
helper would return `unknown` for these keys; with it, downstream
plugins resolve the slots with no casts.
## Features
- Cumulative module augmentation — the file only declares the slots
this plugin owns. Host slots (`app`, `db`, `logger`, `eventBus`,
`clock`) are declared by `@suaveplan/api`'s own augmentation; the
TypeScript compiler merges every declaration in the program into a
single `ServiceRegistry` shape.
- Side-effect import — the package barrel re-imports this module via
`import "./services/registry-augmentation.js"` so any consumer
that pulls a single public symbol from `@suaveplan/api-plugin-notifications`
gets the type-only augmentation automatically.
## Usage: Basic
```ts
import "@suaveplan/api-plugin-notifications";
const repo = ctx.getService("notificationRepository");
// repo is INotificationRepository | undefined — no cast.
```
## Usage: Advanced
When using a downstream plugin that depends on the notifications
plugin's typed registry, the registry import flows transitively
through the side-effect import:
```ts
import { createMyDownstreamPlugin } from "@my-org/api-plugin-downstream";
import "@suaveplan/api-plugin-notifications";
// Downstream plugin's own augmentation merges with this one — both
// `myService` and `notificationRepository` resolve typed.
```
## API Reference
There is no runtime export — the file only declares
`declare module "@suaveplan/api-plugin/service-registry"`.
## Implementation Notes
The file deliberately re-declares the host slots `app` and `db` so
intellisense never claims they are absent in a context that imports
only this plugin. Module augmentation is cumulative, so the
re-declaration merges with `@suaveplan/api`'s own augmentation rather
than overriding it.