@12-apps/notifications 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -3
- package/src/manifest/index.ts +49 -0
- package/src/manifest/server.ts +78 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/notifications",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Plug-and-play notification system (12-15): an always-on in-app inbox, per-user × per-category channel preferences, and email / SMS / WhatsApp / web-push transports behind vendor DRIVERS so a second provider is a config entry. Framework-free core (.), host-mounted backend surface (./server: inbox / preferences / push-subscription endpoints, the channel router with delivery records + retry sweep, the permission fan-out, duck-typed Prisma seam), Hono adapter (./hono), React surface (./react: bell + badge, inbox drawer, preferences screen), VAPID sender (./web-push) and the package-owned Prisma partial + migrations. Standardized adoption contract in ADOPTING.md.",
|
|
6
6
|
"exports": {
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"types": "./dist/web-push/index.d.ts",
|
|
25
25
|
"default": "./dist/web-push/index.js"
|
|
26
26
|
},
|
|
27
|
+
"./manifest": "./src/manifest/index.ts",
|
|
28
|
+
"./manifest/server": "./src/manifest/server.ts",
|
|
27
29
|
"./package.json": "./package.json"
|
|
28
30
|
},
|
|
29
31
|
"scripts": {
|
|
@@ -38,9 +40,10 @@
|
|
|
38
40
|
"prisma:sync:check": "node scripts/sync-notifications-schema.mjs --check"
|
|
39
41
|
},
|
|
40
42
|
"dependencies": {
|
|
41
|
-
"@12-apps/ui": "^6.
|
|
43
|
+
"@12-apps/ui": "^6.2.0"
|
|
42
44
|
},
|
|
43
45
|
"peerDependencies": {
|
|
46
|
+
"@12-apps/wiring": ">=1.3.0",
|
|
44
47
|
"hono": ">=4.0.0",
|
|
45
48
|
"react": ">=19.0.0",
|
|
46
49
|
"react-dom": ">=19.0.0",
|
|
@@ -63,6 +66,7 @@
|
|
|
63
66
|
"devDependencies": {
|
|
64
67
|
"@12-apps/eslint-config": "^1.21.1",
|
|
65
68
|
"@12-apps/typescript-config": "^1.20.1",
|
|
69
|
+
"@12-apps/wiring": "^1.10.0",
|
|
66
70
|
"@testing-library/react": "^16.1.0",
|
|
67
71
|
"@types/react": "19.2.2",
|
|
68
72
|
"@types/react-dom": "19.2.2",
|
|
@@ -108,5 +112,11 @@
|
|
|
108
112
|
"!**/*.stories.*",
|
|
109
113
|
"!**/*.test-story.*",
|
|
110
114
|
"!**/test-helpers.*"
|
|
111
|
-
]
|
|
115
|
+
],
|
|
116
|
+
"wiring": {
|
|
117
|
+
"db": {
|
|
118
|
+
"partial": "prisma/notifications.prisma",
|
|
119
|
+
"migrations": "prisma/migrations"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
112
122
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@12-apps/notifications/manifest` — the SHARED wiring manifest.
|
|
3
|
+
*
|
|
4
|
+
* Identity, the Prisma contribution (the four owned models) and the runtime
|
|
5
|
+
* inventory: `http` on the server. Three narrowings are deliberate:
|
|
6
|
+
*
|
|
7
|
+
* - **No `notifications` blueprints.** This package is the notification
|
|
8
|
+
* MECHANISM — the inbox, the preference matrix, the transports, the retry
|
|
9
|
+
* sweep — not an author of alerts. The blueprints belong to whichever
|
|
10
|
+
* package raises the alert (`@12-apps/product-research`'s budget warning
|
|
11
|
+
* ships as a factory over host copy for exactly this reason), and this one
|
|
12
|
+
* takes them through the `generators` seam at mount.
|
|
13
|
+
* - **No `email` capability.** The transports are host-supplied config
|
|
14
|
+
* (`config.transports`, `config.drivers`), so what would be declared here
|
|
15
|
+
* is a seam the host already fills — and declaring it would oblige an
|
|
16
|
+
* adopter to bind a mailer this package never owns.
|
|
17
|
+
* - **No `web` inventory**, though `./react` ships the bell and the
|
|
18
|
+
* preference screens. Listing it would oblige every SERVER host adopting
|
|
19
|
+
* this manifest to answer for a React surface it never mounts —
|
|
20
|
+
* `assemble()` refuses a declared-but-unanswered capability, so the
|
|
21
|
+
* inventory must not overstate.
|
|
22
|
+
*
|
|
23
|
+
* ON THE `db` DECLARATION. The origin host already composes
|
|
24
|
+
* `prisma/notifications.prisma` into its schema — but by STRUCTURAL
|
|
25
|
+
* DISCOVERY, the assembler's fallback for a package that declares nothing.
|
|
26
|
+
* That fallback is why the gap was invisible: four tables reached a host's
|
|
27
|
+
* database with no declaration behind them, and the contract's whole claim
|
|
28
|
+
* is that a package's models arrive because it said so. Declaring changes no
|
|
29
|
+
* host behaviour (the assembler reads the declaration where it used to scan)
|
|
30
|
+
* and closes the one case where composition was happening by accident.
|
|
31
|
+
*
|
|
32
|
+
* `@12-apps/wiring` is a TYPE-ONLY devDependency (the report-builder move):
|
|
33
|
+
* the manifest is a plain `satisfies`-checked value, and the producer
|
|
34
|
+
* factories' runtime assertions run in this package's own test suite.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { PackageManifest } from '@12-apps/wiring';
|
|
38
|
+
|
|
39
|
+
export const notificationsManifest = {
|
|
40
|
+
name: '@12-apps/notifications',
|
|
41
|
+
contract: 1,
|
|
42
|
+
db: { partial: 'prisma/notifications.prisma', migrations: 'prisma/migrations' },
|
|
43
|
+
/**
|
|
44
|
+
* Mandatory for runtime manifests since wiring 1.3.0: a delivery that
|
|
45
|
+
* exhausts its attempts files under `notifications`, not nowhere.
|
|
46
|
+
*/
|
|
47
|
+
observability: { namespace: 'notifications' },
|
|
48
|
+
server: ['http'],
|
|
49
|
+
} as const satisfies PackageManifest;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@12-apps/notifications/manifest/server` — the server capabilities.
|
|
3
|
+
*
|
|
4
|
+
* `http.create` wraps `createApiNotifications` in a WIRE VIEW, and the reason
|
|
5
|
+
* is one field. `NotificationsRequest` carries `headers` — the contract's
|
|
6
|
+
* `WireRequest` does not, because headers are the adapter's business
|
|
7
|
+
* everywhere else — and exactly one descriptor reads it: push-subscribe takes
|
|
8
|
+
* `user-agent` as the DEVICE HINT it labels a subscription with. Without the
|
|
9
|
+
* view the field would simply be absent at runtime while still type-checking,
|
|
10
|
+
* and every saved device would come back unnamed: a silent quality loss, the
|
|
11
|
+
* failure mode the wiring contract exists to convert into a loud one.
|
|
12
|
+
*
|
|
13
|
+
* So the view derives `headers` from the raw request the contract already
|
|
14
|
+
* carries for the handlers `params`/`query`/`body` cannot serve. A host whose
|
|
15
|
+
* adapter leaves `request` unset still gets a working surface — every route
|
|
16
|
+
* answers, the subscription saves — with an unnamed device, which is the
|
|
17
|
+
* honest degradation for a hint. `@12-apps/notifications/hono` populates it.
|
|
18
|
+
*
|
|
19
|
+
* Everything else rides beside the mapped routes on the aggregate unchanged:
|
|
20
|
+
* `notify`, `notifyByPermission`, `dispatchDeliveries`, `drainPending`, the
|
|
21
|
+
* three stores, `registerGenerator` and the transports registry. A host still
|
|
22
|
+
* calls those directly — being mounted does not make the emit front door stop
|
|
23
|
+
* being a library.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { AnyServerManifest, WireRequest } from '@12-apps/wiring';
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
createApiNotifications,
|
|
30
|
+
type ApiNotifications,
|
|
31
|
+
type NotificationsRoute,
|
|
32
|
+
type NotificationsServerConfig,
|
|
33
|
+
} from '../server';
|
|
34
|
+
|
|
35
|
+
/** The header names this surface reads — the device hint, and nothing else. */
|
|
36
|
+
const READ_HEADERS = ['user-agent'] as const;
|
|
37
|
+
|
|
38
|
+
/** The headers the package expects, taken off the raw request when there is one. */
|
|
39
|
+
function headersOf(request: WireRequest<never>): Record<string, string | undefined> {
|
|
40
|
+
const raw = request.request;
|
|
41
|
+
if (!raw) return {};
|
|
42
|
+
return Object.fromEntries(
|
|
43
|
+
READ_HEADERS.map((name) => [name, raw.headers.get(name) ?? undefined]),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** One `NotificationsRoute` as the wiring contract reads it. */
|
|
48
|
+
function asWireRoute(route: NotificationsRoute): {
|
|
49
|
+
method: NotificationsRoute['method'];
|
|
50
|
+
path: string;
|
|
51
|
+
handle(request: WireRequest<never>): Promise<{ status: number; body: unknown }>;
|
|
52
|
+
} {
|
|
53
|
+
return {
|
|
54
|
+
method: route.method,
|
|
55
|
+
path: route.path,
|
|
56
|
+
handle: (request) =>
|
|
57
|
+
route.handle({
|
|
58
|
+
actor: request.actor,
|
|
59
|
+
params: request.params,
|
|
60
|
+
query: request.query,
|
|
61
|
+
body: request.body,
|
|
62
|
+
headers: headersOf(request),
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `createApiNotifications`, its routes re-shaped for the aggregate. */
|
|
68
|
+
export function createWireApiNotifications(
|
|
69
|
+
config: NotificationsServerConfig,
|
|
70
|
+
): Omit<ApiNotifications, 'routes'> & { routes: ReturnType<typeof asWireRoute>[] } {
|
|
71
|
+
const api = createApiNotifications(config);
|
|
72
|
+
return { ...api, routes: api.routes.map(asWireRoute) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const notificationsServerManifest = {
|
|
76
|
+
name: '@12-apps/notifications',
|
|
77
|
+
http: { create: createWireApiNotifications },
|
|
78
|
+
} as const satisfies AnyServerManifest;
|