@devmonks/notify 0.0.0-stage → 0.4.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.
@@ -0,0 +1,130 @@
1
+ /** What a notification says, independent of how it is delivered. Channels pick what they need. */
2
+ export interface Content {
3
+ title: string;
4
+ body: string;
5
+ /** Where tapping it goes (app route or absolute URL). */
6
+ url?: string;
7
+ /** Extra string data (push payload, inbox row). */
8
+ data?: Record<string, string>;
9
+ /**
10
+ * Used by the email entry (`./email`). `template` is a React Email component there (see EmailTemplate);
11
+ * it is typed loosely here so the core does not depend on react.
12
+ */
13
+ email?: {
14
+ subject?: string;
15
+ template?: (props: any) => unknown;
16
+ props?: unknown;
17
+ };
18
+ /** Per-message push options; they override the adapter's `defaults`. */
19
+ push?: PushOptions;
20
+ sms?: {
21
+ text?: string;
22
+ };
23
+ }
24
+ /** Push options, mapped to FCM HTTP v1 fields by `fcmPush`. */
25
+ export interface PushOptions {
26
+ /**
27
+ * Send no visible notification: the app gets `data` (with `title` and `body` added to it) and
28
+ * decides what to show. iOS gets a background push (`content-available`).
29
+ */
30
+ dataOnly?: boolean;
31
+ /** App icon badge (iOS `aps.badge`, Android `notification_count`). */
32
+ badge?: number;
33
+ /** iOS plays "default" when unset. Android 8+ takes the sound from the channel. */
34
+ sound?: string;
35
+ /** Groups notifications (iOS `thread-id`); on Android a new one replaces the one with the same tag. */
36
+ threadId?: string;
37
+ /** Android notification channel (`channel_id`). The app must create it; a high-importance channel shows heads-up banners. */
38
+ channelId?: string;
39
+ /** Defaults to "high" (Android `HIGH`, APNs 10). "normal" may be delayed to save battery. */
40
+ priority?: 'high' | 'normal';
41
+ /** How long the provider keeps an undelivered message. 0 = deliver now or drop. Unset = provider default. */
42
+ ttlSeconds?: number;
43
+ /** A newer message with the same key replaces an undelivered or displayed older one (`collapse_key`, `apns-collapse-id`). */
44
+ collapseKey?: string;
45
+ /** Escape hatch: raw FCM `AndroidConfig` / `ApnsConfig` / `WebpushConfig` fields, deep-merged over the generated message. */
46
+ raw?: {
47
+ android?: Record<string, unknown>;
48
+ apns?: Record<string, unknown>;
49
+ webpush?: Record<string, unknown>;
50
+ };
51
+ }
52
+ export type SendResult = {
53
+ ok: true;
54
+ providerId?: string;
55
+ } | {
56
+ ok: false;
57
+ /** True when trying again later may work (429, 5xx, network). */
58
+ retryable: boolean;
59
+ error: string;
60
+ /** The address is permanently bad (e.g. an unregistered push token): drop it. */
61
+ deadAddress?: boolean;
62
+ };
63
+ /** Per-send context passed to the channel. */
64
+ export interface SendContext {
65
+ type?: string;
66
+ /** Stable per logical delivery; adapters forward it to providers that support idempotency. */
67
+ idempotencyKey?: string;
68
+ }
69
+ export interface Channel {
70
+ name: string;
71
+ send(to: string, content: Content, ctx?: SendContext): Promise<SendResult>;
72
+ }
73
+ export interface Logger {
74
+ info(message: string, meta?: unknown): void;
75
+ }
76
+ export interface Recipient {
77
+ id: string;
78
+ /** BCP 47, e.g. "en". */
79
+ locale: string;
80
+ /** IANA time zone; drives quiet hours. */
81
+ timeZone: string;
82
+ email?: string;
83
+ phone?: string;
84
+ /** Push-capable installs. One push delivery per device. */
85
+ devices: {
86
+ id: string;
87
+ token: string;
88
+ platform?: string;
89
+ }[];
90
+ /** Defaults to true. */
91
+ inAppEnabled?: boolean;
92
+ /** prefs[category][channel] === false switches it off; anything else is on. */
93
+ prefs: Record<string, Partial<Record<string, boolean>>>;
94
+ /** Local wall-clock window, may cross midnight (e.g. 22:00 to 07:00). */
95
+ quietHours?: {
96
+ from: string;
97
+ to: string;
98
+ };
99
+ }
100
+ export interface Delivery {
101
+ /** Unique per planned delivery (random). Use `idempotencyKey` for dedupe. */
102
+ id: string;
103
+ type: string;
104
+ channel: string;
105
+ to: string;
106
+ content: Content;
107
+ recipientId: string;
108
+ /** Set for push deliveries: the device the token belongs to. */
109
+ deviceId?: string;
110
+ idempotencyKey: string;
111
+ }
112
+ export interface NotificationType<D = any> {
113
+ type: string;
114
+ /** Preference key: `recipient.prefs[category][channel]`. Use the type name for per-type switches. */
115
+ category: string;
116
+ channels: string[];
117
+ /** Always send: ignores preferences and quiet hours (sign-in codes, security, moderation notices). */
118
+ transactional?: boolean;
119
+ render(data: D, opts: {
120
+ locale: string;
121
+ }): Content;
122
+ }
123
+ export interface PlanOptions {
124
+ /**
125
+ * Identifies the event. Same type + recipient + channel + address + dedupeKey = same idempotencyKey.
126
+ * Omit only for "once ever" notifications; pass an event id for anything repeatable.
127
+ */
128
+ dedupeKey?: string;
129
+ now?: Date;
130
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/util.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare const errorMessage: (e: unknown) => string;
2
+ /** HTTP statuses worth trying again later. */
3
+ export declare const retryableStatus: (status: number) => boolean;
4
+ /** Parsed JSON, or undefined when [raw] is not JSON (provider error bodies are sometimes HTML). */
5
+ export declare function tryJson<T>(raw: string): T | undefined;
package/dist/util.js ADDED
@@ -0,0 +1,12 @@
1
+ export const errorMessage = (e) => (e instanceof Error ? e.message : String(e));
2
+ /** HTTP statuses worth trying again later. */
3
+ export const retryableStatus = (status) => status === 429 || status >= 500;
4
+ /** Parsed JSON, or undefined when [raw] is not JSON (provider error bodies are sometimes HTML). */
5
+ export function tryJson(raw) {
6
+ try {
7
+ return JSON.parse(raw);
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ }
package/package.json CHANGED
@@ -1,6 +1,89 @@
1
1
  {
2
2
  "name": "@devmonks/notify",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.0",
4
+ "description": "Plan-then-send notifications with an FCM HTTP v1 push adapter. No DB, no queue; the core entry has no dependencies. Optional email (Resend + React Email) and Drizzle entries.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "fcm",
8
+ "firebase",
9
+ "push",
10
+ "notifications",
11
+ "drizzle",
12
+ "resend"
13
+ ],
14
+ "type": "module",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/devmonks-co/notify.git"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "sideEffects": false,
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ },
28
+ "./email": {
29
+ "types": "./dist/email/index.d.ts",
30
+ "import": "./dist/email/index.js"
31
+ },
32
+ "./drizzle": {
33
+ "types": "./dist/drizzle/index.d.ts",
34
+ "import": "./dist/drizzle/index.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
47
+ "typecheck": "tsc --noEmit",
48
+ "test": "vitest run",
49
+ "prepublishOnly": "bun run typecheck && bun run test && bun run build"
50
+ },
51
+ "peerDependencies": {
52
+ "@react-email/components": ">=1",
53
+ "@react-email/render": ">=2",
54
+ "react": ">=18",
55
+ "react-dom": ">=18",
56
+ "drizzle-orm": ">=0.40"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "@react-email/components": {
60
+ "optional": true
61
+ },
62
+ "@react-email/render": {
63
+ "optional": true
64
+ },
65
+ "react": {
66
+ "optional": true
67
+ },
68
+ "react-dom": {
69
+ "optional": true
70
+ },
71
+ "drizzle-orm": {
72
+ "optional": true
73
+ }
74
+ },
75
+ "devDependencies": {
76
+ "@electric-sql/pglite": "^0.5.16",
77
+ "@react-email/components": "1.0.12",
78
+ "@react-email/render": "2.1.0",
79
+ "@types/node": "24",
80
+ "@types/react": "19.3.0",
81
+ "@types/react-dom": "19.3.0",
82
+ "drizzle-kit": "^0.31.11",
83
+ "drizzle-orm": "0.45.2",
84
+ "react": "19.3.0",
85
+ "react-dom": "19.3.0",
86
+ "typescript": "5.9.3",
87
+ "vitest": "3.2.4"
88
+ }
89
+ }