@yaghmori/notification-service 0.1.1

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 @@
1
+ {"version":3,"file":"nest.d.ts","sourceRoot":"","sources":["../src/nest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAEnE,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAGpE,MAAM,MAAM,4BAA4B,GAAG,eAAe,GAAG;IAC3D,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB,CAAC;AAEF,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,qBAAqB,CAOvB"}
package/dist/nest.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.notificationTcpClient = notificationTcpClient;
4
+ const microservices_1 = require("@nestjs/microservices");
5
+ const config_1 = require("./config");
6
+ const generated_1 = require("./generated");
7
+ function notificationTcpClient(options = {}) {
8
+ const { host, port } = (0, config_1.resolveTcpEndpoint)(options);
9
+ return {
10
+ name: options.name ?? generated_1.INJECTION_TOKEN,
11
+ transport: microservices_1.Transport.TCP,
12
+ options: { host, port },
13
+ };
14
+ }
15
+ //# sourceMappingURL=nest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nest.js","sourceRoot":"","sources":["../src/nest.ts"],"names":[],"mappings":";;AASA,sDASC;AAjBD,yDAAkD;AAClD,qCAAoE;AACpE,2CAA8C;AAM9C,SAAgB,qBAAqB,CACnC,UAAwC,EAAE;IAE1C,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,IAAA,2BAAkB,EAAC,OAAO,CAAC,CAAC;IACnD,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,2BAAe;QACrC,SAAS,EAAE,yBAAS,CAAC,GAAG;QACxB,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;KACxB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,118 @@
1
+ import { z } from 'zod';
2
+ /** Payload for Kafka `notification.send.requested` / TCP create. */
3
+ export declare const sendNotificationRequestSchema: z.ZodObject<{
4
+ userId: z.ZodString;
5
+ eventKey: z.ZodString;
6
+ title: z.ZodOptional<z.ZodString>;
7
+ body: z.ZodOptional<z.ZodString>;
8
+ channels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
+ emailTemplate: z.ZodOptional<z.ZodString>;
10
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
11
+ actionUrl: z.ZodOptional<z.ZodString>;
12
+ tenantId: z.ZodOptional<z.ZodString>;
13
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
+ }, "strip", z.ZodTypeAny, {
15
+ userId: string;
16
+ eventKey: string;
17
+ title?: string | undefined;
18
+ body?: string | undefined;
19
+ channels?: string[] | undefined;
20
+ emailTemplate?: string | undefined;
21
+ data?: Record<string, unknown> | undefined;
22
+ actionUrl?: string | undefined;
23
+ tenantId?: string | undefined;
24
+ metadata?: Record<string, unknown> | undefined;
25
+ }, {
26
+ userId: string;
27
+ eventKey: string;
28
+ title?: string | undefined;
29
+ body?: string | undefined;
30
+ channels?: string[] | undefined;
31
+ emailTemplate?: string | undefined;
32
+ data?: Record<string, unknown> | undefined;
33
+ actionUrl?: string | undefined;
34
+ tenantId?: string | undefined;
35
+ metadata?: Record<string, unknown> | undefined;
36
+ }>;
37
+ export type SendNotificationRequest = z.infer<typeof sendNotificationRequestSchema>;
38
+ export declare const listNotificationsRequestSchema: z.ZodObject<{
39
+ userId: z.ZodOptional<z.ZodString>;
40
+ limit: z.ZodOptional<z.ZodNumber>;
41
+ offset: z.ZodOptional<z.ZodNumber>;
42
+ unreadOnly: z.ZodOptional<z.ZodBoolean>;
43
+ }, "strip", z.ZodTypeAny, {
44
+ userId?: string | undefined;
45
+ limit?: number | undefined;
46
+ offset?: number | undefined;
47
+ unreadOnly?: boolean | undefined;
48
+ }, {
49
+ userId?: string | undefined;
50
+ limit?: number | undefined;
51
+ offset?: number | undefined;
52
+ unreadOnly?: boolean | undefined;
53
+ }>;
54
+ export type ListNotificationsRequest = z.infer<typeof listNotificationsRequestSchema>;
55
+ export declare const getNotificationsRequestSchema: z.ZodObject<{
56
+ userId: z.ZodOptional<z.ZodString>;
57
+ limit: z.ZodOptional<z.ZodNumber>;
58
+ offset: z.ZodOptional<z.ZodNumber>;
59
+ unreadOnly: z.ZodOptional<z.ZodBoolean>;
60
+ }, "strip", z.ZodTypeAny, {
61
+ userId?: string | undefined;
62
+ limit?: number | undefined;
63
+ offset?: number | undefined;
64
+ unreadOnly?: boolean | undefined;
65
+ }, {
66
+ userId?: string | undefined;
67
+ limit?: number | undefined;
68
+ offset?: number | undefined;
69
+ unreadOnly?: boolean | undefined;
70
+ }>;
71
+ export type GetNotificationsRequest = ListNotificationsRequest;
72
+ export declare const markAsReadRequestSchema: z.ZodObject<{
73
+ id: z.ZodString;
74
+ userId: z.ZodOptional<z.ZodString>;
75
+ }, "strip", z.ZodTypeAny, {
76
+ id: string;
77
+ userId?: string | undefined;
78
+ }, {
79
+ id: string;
80
+ userId?: string | undefined;
81
+ }>;
82
+ export type MarkAsReadRequest = z.infer<typeof markAsReadRequestSchema>;
83
+ export declare const createNotificationRequestSchema: z.ZodObject<{
84
+ userId: z.ZodString;
85
+ eventKey: z.ZodString;
86
+ title: z.ZodOptional<z.ZodString>;
87
+ body: z.ZodOptional<z.ZodString>;
88
+ channels: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
89
+ emailTemplate: z.ZodOptional<z.ZodString>;
90
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
91
+ actionUrl: z.ZodOptional<z.ZodString>;
92
+ tenantId: z.ZodOptional<z.ZodString>;
93
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
94
+ }, "strip", z.ZodTypeAny, {
95
+ userId: string;
96
+ eventKey: string;
97
+ title?: string | undefined;
98
+ body?: string | undefined;
99
+ channels?: string[] | undefined;
100
+ emailTemplate?: string | undefined;
101
+ data?: Record<string, unknown> | undefined;
102
+ actionUrl?: string | undefined;
103
+ tenantId?: string | undefined;
104
+ metadata?: Record<string, unknown> | undefined;
105
+ }, {
106
+ userId: string;
107
+ eventKey: string;
108
+ title?: string | undefined;
109
+ body?: string | undefined;
110
+ channels?: string[] | undefined;
111
+ emailTemplate?: string | undefined;
112
+ data?: Record<string, unknown> | undefined;
113
+ actionUrl?: string | undefined;
114
+ tenantId?: string | undefined;
115
+ metadata?: Record<string, unknown> | undefined;
116
+ }>;
117
+ export type CreateNotificationRequest = SendNotificationRequest;
118
+ //# sourceMappingURL=schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,oEAAoE;AACpE,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;EAKzC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAEtF,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;EAAiC,CAAC;AAC5E,MAAM,MAAM,uBAAuB,GAAG,wBAAwB,CAAC;AAE/D,eAAO,MAAM,uBAAuB;;;;;;;;;EAGlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAgC,CAAC;AAC7E,MAAM,MAAM,yBAAyB,GAAG,uBAAuB,CAAC"}
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createNotificationRequestSchema = exports.markAsReadRequestSchema = exports.getNotificationsRequestSchema = exports.listNotificationsRequestSchema = exports.sendNotificationRequestSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const nonEmptyStringSchema = zod_1.z.string().min(1);
6
+ const dataObjectSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.unknown());
7
+ /** Payload for Kafka `notification.send.requested` / TCP create. */
8
+ exports.sendNotificationRequestSchema = zod_1.z.object({
9
+ userId: nonEmptyStringSchema,
10
+ eventKey: nonEmptyStringSchema,
11
+ title: zod_1.z.string().optional(),
12
+ body: zod_1.z.string().optional(),
13
+ channels: zod_1.z.array(zod_1.z.string()).optional(),
14
+ emailTemplate: zod_1.z.string().optional(),
15
+ data: dataObjectSchema.optional(),
16
+ actionUrl: zod_1.z.string().optional(),
17
+ tenantId: zod_1.z.string().optional(),
18
+ metadata: dataObjectSchema.optional(),
19
+ });
20
+ exports.listNotificationsRequestSchema = zod_1.z.object({
21
+ userId: nonEmptyStringSchema.optional(),
22
+ limit: zod_1.z.number().int().min(1).max(100).optional(),
23
+ offset: zod_1.z.number().int().min(0).optional(),
24
+ unreadOnly: zod_1.z.boolean().optional(),
25
+ });
26
+ exports.getNotificationsRequestSchema = exports.listNotificationsRequestSchema;
27
+ exports.markAsReadRequestSchema = zod_1.z.object({
28
+ id: nonEmptyStringSchema,
29
+ userId: nonEmptyStringSchema.optional(),
30
+ });
31
+ exports.createNotificationRequestSchema = exports.sendNotificationRequestSchema;
32
+ //# sourceMappingURL=schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAExB,MAAM,oBAAoB,GAAG,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC/C,MAAM,gBAAgB,GAAG,OAAC,CAAC,MAAM,CAAC,OAAC,CAAC,MAAM,EAAE,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,CAAC;AAE3D,oEAAoE;AACvD,QAAA,6BAA6B,GAAG,OAAC,CAAC,MAAM,CAAC;IACpD,MAAM,EAAE,oBAAoB;IAC5B,QAAQ,EAAE,oBAAoB;IAC9B,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,QAAQ,EAAE,OAAC,CAAC,KAAK,CAAC,OAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACxC,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACpC,IAAI,EAAE,gBAAgB,CAAC,QAAQ,EAAE;IACjC,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,QAAQ,EAAE,gBAAgB,CAAC,QAAQ,EAAE;CACtC,CAAC,CAAC;AAGU,QAAA,8BAA8B,GAAG,OAAC,CAAC,MAAM,CAAC;IACrD,MAAM,EAAE,oBAAoB,CAAC,QAAQ,EAAE;IACvC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IAClD,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC1C,UAAU,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;CACnC,CAAC,CAAC;AAGU,QAAA,6BAA6B,GAAG,sCAA8B,CAAC;AAG/D,QAAA,uBAAuB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC9C,EAAE,EAAE,oBAAoB;IACxB,MAAM,EAAE,oBAAoB,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAGU,QAAA,+BAA+B,GAAG,qCAA6B,CAAC"}
@@ -0,0 +1,8 @@
1
+ # Best practices — notification-service client
2
+
3
+ - **HTTP** for inbox list / mark-read / health / SSE from apps with `x-api-key`.
4
+ - **TCP** for internal Nest↔Nest or .NET↔Nest (`NotificationServiceTcpClient`).
5
+ - **Kafka** for async delivery (`NotificationService.Topics.SendRequested` → sent/failed).
6
+ - Configure with `NOTIFICATION_SERVICE_*` env or `NotificationServiceOptions` — any host/port.
7
+ - Keep TCP private; do not expose `TCP_PORT` publicly.
8
+ - Prefer Kafka for fan-out delivery; use TCP/HTTP for synchronous inbox ops.
@@ -0,0 +1,20 @@
1
+ # Publish .NET SDK to nuget.org (Trusted Publishing)
2
+
3
+ No long-lived API key required.
4
+
5
+ → [Trusted Publishing on nuget.org](https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing)
6
+
7
+ ## Quick setup
8
+
9
+ 1. nuget.org → **Trusted Publishing** → policy:
10
+ - Repository owner: `yaghmori`
11
+ - Repository: `notification-service`
12
+ - Workflow: `cd-main.yml` (filename only)
13
+ 2. GitHub → Settings → Secrets and variables → Actions → **Variables** → `NUGET_USER` = nuget.org **username** (not email, not API key).
14
+ 3. CD uses `permissions.id-token: write` + `NuGet/login@v1` → temp key → `dotnet nuget push`.
15
+
16
+ Private repos: one successful login within 7 days permanently activates the policy.
17
+
18
+ After it works, revoke any old nuget.org API keys / `NUGET_TOKEN`.
19
+
20
+ Package id: `Yaghmori.NotificationService`.
@@ -0,0 +1,208 @@
1
+ # Multi-protocol client guide (notification-service)
2
+
3
+ Integration guide for calling **notification-service** from **Node.js** and **.NET** over **HTTP**, **NestJS TCP**, and **Kafka**. Scope is notification inbox + multi-channel delivery.
4
+
5
+ | Artifact | Location |
6
+ |----------|----------|
7
+ | Truth file | `packages/client/contracts.json` |
8
+ | Node SDK | `@yaghmori/notification-service` (`packages/client`) |
9
+ | .NET SDK | `Yaghmori.NotificationService` (`sdk/dotnet/Yaghmori.NotificationService`) |
10
+ | Docker | `ghcr.io/yaghmori/notification-service` |
11
+
12
+ ```bash
13
+ cd packages/client && pnpm run codegen
14
+ ```
15
+
16
+ Emits TS (`src/generated.ts`) and C# (`NotificationService.g.cs`).
17
+
18
+ ---
19
+
20
+ ## 1. Protocol selection
21
+
22
+ | Protocol | Sync? | Auth on wire | Typical callers | Network |
23
+ |----------|-------|--------------|-----------------|---------|
24
+ | **HTTP** | Yes | `x-api-key` / Bearer JWT | Apps, gateways, inbox UI | Public or private; authenticate unless `AUTH_DISABLED` |
25
+ | **NestJS TCP** | Request/response | None (trust network) | Nest / .NET mesh | Private VPC / mesh only |
26
+ | **Kafka** | Async | Cluster SASL/SSL | Fan-out delivery, retries, audit | Private Kafka; idempotent producers |
27
+
28
+ 1. External clients → **HTTP + API key** (list / mark-read / health; SSE for live inbox).
29
+ 2. Internal Nest / .NET → **TCP** for create / get / mark-read / archive.
30
+ 3. “Deliver this notification eventually” → **Kafka** `notification.send.requested` (+ consume `sent` / `failed`).
31
+
32
+ Do **not** expose Nest TCP publicly. Use `x-api-key`, not `Authorization: Bearer <api-key>`.
33
+
34
+ ---
35
+
36
+ ## 2. Address resolution
37
+
38
+ Package defaults (`ports.tcp = 4004`, `ports.http = 3000`) are **local compose suggestions**.
39
+
40
+ **HTTP base URL:** options `baseUrl` → `NOTIFICATION_SERVICE_URL` → `http://{HOST}:{HTTP_PORT|3000}`
41
+
42
+ **TCP:** options → `NOTIFICATION_SERVICE_HOST` + `NOTIFICATION_SERVICE_TCP_PORT` → `127.0.0.1:4004`
43
+
44
+ **Kafka:** options → `KAFKA_BROKERS` / `KAFKA_BOOTSTRAP_SERVERS` → `localhost:9092`
45
+
46
+ | Role | Variable |
47
+ |------|----------|
48
+ | Service HTTP listen | `PORT` |
49
+ | Service TCP listen | `TCP_PORT` |
50
+ | Client HTTP | `NOTIFICATION_SERVICE_URL` or host + `NOTIFICATION_SERVICE_HTTP_PORT` |
51
+ | Client TCP | `NOTIFICATION_SERVICE_HOST` + `NOTIFICATION_SERVICE_TCP_PORT` |
52
+ | Client auth | `NOTIFICATION_SERVICE_API_KEY` / `NOTIFICATION_SERVICE_BEARER` |
53
+
54
+ ```bash
55
+ docker run --rm -p 8080:8080 -p 5004:5004 \
56
+ -e PORT=8080 -e TCP_PORT=5004 \
57
+ -e AUTH_API_KEYS=dev-key-1 \
58
+ ghcr.io/yaghmori/notification-service:latest
59
+
60
+ export NOTIFICATION_SERVICE_URL=http://127.0.0.1:8080
61
+ export NOTIFICATION_SERVICE_HOST=127.0.0.1
62
+ export NOTIFICATION_SERVICE_TCP_PORT=5004
63
+ export NOTIFICATION_SERVICE_API_KEY=dev-key-1
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 3. Authentication (HTTP)
69
+
70
+ | Mode | Config |
71
+ |------|--------|
72
+ | API keys | Admin keys or `AUTH_API_KEYS=k1,k2` → `x-api-key` / `Authorization: ApiKey …` |
73
+ | JWT | `JWT_SECRET` → `Authorization: Bearer <jwt>` |
74
+ | Disabled | `AUTH_DISABLED=true` — trusted mesh only |
75
+ | Public | `@Public()` e.g. `GET /health` |
76
+
77
+ TCP/Kafka use network trust + broker SASL, not `x-api-key`.
78
+
79
+ ---
80
+
81
+ ## 4. HTTP
82
+
83
+ | Constant | Path | Purpose |
84
+ |----------|------|---------|
85
+ | `LIST` | `GET /notifications` | Inbox list (query: userId, limit, offset, unreadOnly) |
86
+ | `STREAM` | `GET /notifications/stream` | SSE live feed |
87
+ | `MARK_READ` | `POST /notifications/{id}/read` | Mark one read |
88
+ | `MARK_READ_ALL` | `POST /notifications/read-all` | Mark all read |
89
+ | `ARCHIVE` / `ARCHIVE_ALL` | archive paths | Soft-remove from inbox |
90
+ | `DELETE` | `DELETE /notifications/{id}` | Delete |
91
+ | `HEALTH` | `GET /health` | Liveness |
92
+
93
+ Node SDK exposes `list`, `markRead`, `health`. .NET: `ListAsync`, `MarkReadAsync`, `HealthAsync`.
94
+
95
+ Timeouts: 10–30s for inbox APIs; health &lt; 5s. Retry 408/429/5xx only.
96
+
97
+ ---
98
+
99
+ ## 5. NestJS TCP
100
+
101
+ Framing: `<utf8-byte-length>#<json>`
102
+
103
+ Request: `{ "pattern": "notification.create_notification", "data": { … }, "id": "<uuid>" }`
104
+ Response: `{ "err": null, "response": { … }, "id": "<uuid>" }`
105
+
106
+ | Constant | Pattern |
107
+ |----------|---------|
108
+ | `GET_NOTIFICATIONS` | `notification.get_notifications` |
109
+ | `GET_NOTIFICATION_BY_ID` | `notification.get_notification_by_id` |
110
+ | `CREATE_NOTIFICATION` | `notification.create_notification` |
111
+ | `MARK_AS_READ` | `notification.mark_as_read` |
112
+ | `MARK_ALL_AS_READ` | `notification.mark_all_as_read` |
113
+ | `ARCHIVE_NOTIFICATION` | `notification.archive_notification` |
114
+ | `DELETE_NOTIFICATION` | `notification.delete_notification` |
115
+ | `HEALTH_CHECK` | `health.check` |
116
+
117
+ .NET helpers: `CreateAsync` / `GetNotificationsAsync` on `NotificationServiceTcpClient`. Prefer private networks; set connect + read timeouts.
118
+
119
+ ---
120
+
121
+ ## 6. Kafka
122
+
123
+ | Role | Topic constant | Topic | Event type |
124
+ |------|----------------|-------|------------|
125
+ | Command | `SEND_REQUESTED` | `notification.send.requested` | `evt.notification.message.send.v1` |
126
+ | Lifecycle | `SENT` | `notification.sent` | `evt.notification.message.sent.v1` |
127
+ | Lifecycle | `FAILED` | `notification.failed` | `evt.notification.message.failed.v1` |
128
+
129
+ **Send payload** (camelCase JSON):
130
+
131
+ ```json
132
+ {
133
+ "userId": "user-1",
134
+ "eventKey": "order.shipped",
135
+ "title": "Order shipped",
136
+ "body": "Your order is on the way.",
137
+ "channels": ["in-app", "email"],
138
+ "emailTemplate": "optional-template",
139
+ "data": { "orderId": "1" },
140
+ "actionUrl": "https://app.example.com/orders/1",
141
+ "tenantId": "optional",
142
+ "metadata": {}
143
+ }
144
+ ```
145
+
146
+ Producer defaults: `acks=all`, idempotence, retries ≥ 3, headers `content-type` + `x-event-type`.
147
+ Consumer: manual commit after side effects; dedicated `group.id`.
148
+
149
+ Kafka env: `KAFKA_BROKERS`, `KAFKA_CLIENT_ID`, `KAFKA_SSL`, `KAFKA_SASL_*` (aliases `KAFKA_USERNAME` / `KAFKA_PASSWORD`).
150
+
151
+ ---
152
+
153
+ ## 7. Node.js
154
+
155
+ ```ts
156
+ import {
157
+ createNotificationHttpClient,
158
+ notificationTcpClient,
159
+ NotificationKafka,
160
+ NotificationService,
161
+ } from '@yaghmori/notification-service';
162
+
163
+ const http = createNotificationHttpClient({
164
+ baseUrl: process.env.NOTIFICATION_SERVICE_URL,
165
+ auth: { apiKey: process.env.NOTIFICATION_SERVICE_API_KEY! },
166
+ });
167
+ await http.list({ userId: 'u1' });
168
+ await http.markRead(id);
169
+ await http.health();
170
+
171
+ // Nest: ClientsModule.register([notificationTcpClient()])
172
+ // Kafka: NotificationKafka.sendRequested + NotificationKafka.connection()
173
+ ```
174
+
175
+ ---
176
+
177
+ ## 8. .NET
178
+
179
+ ```csharp
180
+ services.AddNotificationServiceClient(o =>
181
+ {
182
+ o.ApiKey = Environment.GetEnvironmentVariable("NOTIFICATION_SERVICE_API_KEY");
183
+ });
184
+
185
+ using var http = new NotificationServiceHttpClient(new NotificationServiceOptions());
186
+ await http.ListAsync(userId: "u1");
187
+ await http.MarkReadAsync(id);
188
+ await http.HealthAsync();
189
+
190
+ await using var tcp = new NotificationServiceTcpClient(new NotificationServiceOptions());
191
+ await tcp.CreateAsync(new SendNotificationRequest
192
+ {
193
+ UserId = "u1",
194
+ EventKey = "order.shipped",
195
+ Title = "Order shipped",
196
+ Body = "On the way",
197
+ });
198
+ await tcp.GetNotificationsAsync(new { userId = "u1" });
199
+
200
+ using var kafka = new NotificationKafkaProducer(new NotificationKafkaOptions());
201
+ await kafka.PublishSendRequestedAsync(new SendNotificationRequest
202
+ {
203
+ UserId = "u1",
204
+ EventKey = "order.shipped",
205
+ });
206
+ ```
207
+
208
+ Constants live in generated `NotificationService.g.cs` (`Patterns`, `HttpPaths`, `Topics`, `EventTypes`).
package/docs/USAGE.md ADDED
@@ -0,0 +1,153 @@
1
+ # Full SDK usage — @yaghmori/notification-service
2
+
3
+ HTTP inbox + TCP RPC + Kafka send topics + auth. Deploy on **any host/port**; configure the SDK.
4
+
5
+ ## Install / run
6
+
7
+ ```bash
8
+ pnpm add @yaghmori/notification-service zod
9
+ docker pull ghcr.io/yaghmori/notification-service:latest
10
+ ```
11
+
12
+ ```bash
13
+ docker run --rm -p 3000:3000 -p 4004:4004 \
14
+ -e PORT=3000 -e TCP_PORT=4004 \
15
+ -e AUTH_API_KEYS=dev-key \
16
+ -e DATABASE_URL=... \
17
+ ghcr.io/yaghmori/notification-service:latest
18
+ ```
19
+
20
+ ## Configure address / ports
21
+
22
+ | | Client options | Client env | Service env |
23
+ |--|----------------|------------|-------------|
24
+ | HTTP | `baseUrl` / `host`+`port` | `NOTIFICATION_SERVICE_URL` or `HOST`+`HTTP_PORT` | `PORT` |
25
+ | TCP | `notificationTcpClient({ host, port })` | `NOTIFICATION_SERVICE_HOST`, `NOTIFICATION_SERVICE_TCP_PORT` | `TCP_PORT` |
26
+ | Kafka | `NotificationKafka.connection()` | `KAFKA_BROKERS`, SASL_*, SSL | same |
27
+
28
+ ## Auth (HTTP)
29
+
30
+ | Mode | Service config | Client |
31
+ |------|----------------|--------|
32
+ | API key | `AUTH_API_KEYS=k1,k2` | `auth: { apiKey }` → `x-api-key` |
33
+ | JWT | `JWT_SECRET` + Bearer with `serviceName` | `auth: { bearerToken }` |
34
+ | Open | `AUTH_DISABLED=true` | omit auth |
35
+ | Health | always public | `GET /health` |
36
+
37
+ TCP: private network. Kafka: broker SASL/SSL via env (not HTTP API keys).
38
+
39
+ ```ts
40
+ import { createNotificationHttpClient } from '@yaghmori/notification-service';
41
+
42
+ const notifications = createNotificationHttpClient({
43
+ baseUrl: process.env.NOTIFICATION_SERVICE_URL,
44
+ auth: { apiKey: process.env.NOTIFICATION_SERVICE_API_KEY! },
45
+ });
46
+ ```
47
+
48
+ ## HTTP
49
+
50
+ ```ts
51
+ await notifications.list({ userId: 'user-1', unreadOnly: true, limit: 20 });
52
+ await notifications.markRead(notificationId);
53
+ await notifications.health();
54
+ ```
55
+
56
+ Paths: `NotificationService.httpPaths.LIST` → `/notifications`, etc. SSE stream is `STREAM` → `/notifications/stream` (wire your own EventSource; HTTP client focuses on list/mark-read/health).
57
+
58
+ ## NestJS TCP
59
+
60
+ ```ts
61
+ import { Module, Inject, Injectable } from '@nestjs/common';
62
+ import { ClientsModule, ClientProxy } from '@nestjs/microservices';
63
+ import {
64
+ notificationTcpClient,
65
+ NotificationService,
66
+ PATTERNS,
67
+ } from '@yaghmori/notification-service';
68
+ import { firstValueFrom } from 'rxjs';
69
+
70
+ @Module({
71
+ imports: [
72
+ ClientsModule.register([
73
+ notificationTcpClient({
74
+ host: process.env.NOTIFICATION_SERVICE_HOST,
75
+ port: Number(process.env.NOTIFICATION_SERVICE_TCP_PORT) || 4004,
76
+ }),
77
+ ]),
78
+ ],
79
+ })
80
+ export class AppModule {}
81
+
82
+ @Injectable()
83
+ export class NotifyFacade {
84
+ constructor(@Inject(NotificationService.token) private readonly client: ClientProxy) {}
85
+
86
+ create(payload: {
87
+ userId: string;
88
+ eventKey: string;
89
+ title?: string;
90
+ body?: string;
91
+ }) {
92
+ return firstValueFrom(
93
+ this.client.send(PATTERNS.CREATE_NOTIFICATION, payload),
94
+ );
95
+ }
96
+
97
+ list(userId: string) {
98
+ return firstValueFrom(
99
+ this.client.send(PATTERNS.GET_NOTIFICATIONS, { userId }),
100
+ );
101
+ }
102
+ }
103
+ ```
104
+
105
+ ## Kafka — request a send
106
+
107
+ ```ts
108
+ import { Kafka } from 'kafkajs';
109
+ import {
110
+ NotificationKafka,
111
+ sendNotificationRequestSchema,
112
+ } from '@yaghmori/notification-service';
113
+
114
+ const conn = NotificationKafka.connection();
115
+ const kafka = new Kafka(conn);
116
+ const producer = kafka.producer();
117
+ await producer.connect();
118
+
119
+ const payload = sendNotificationRequestSchema.parse({
120
+ userId: 'user-1',
121
+ eventKey: 'order.shipped',
122
+ title: 'Order shipped',
123
+ body: 'Your order is on the way.',
124
+ channels: ['in-app', 'email'],
125
+ actionUrl: 'https://app.example.com/orders/1',
126
+ });
127
+
128
+ await producer.send({
129
+ topic: NotificationKafka.sendRequested, // notification.send.requested
130
+ messages: [
131
+ {
132
+ key: payload.userId,
133
+ value: JSON.stringify(payload),
134
+ headers: {
135
+ 'content-type': 'application/json',
136
+ 'x-event-type': NotificationKafka.eventTypes.SEND,
137
+ },
138
+ },
139
+ ],
140
+ });
141
+ ```
142
+
143
+ Lifecycle topics: `notification.sent`, `notification.failed`.
144
+
145
+ ## Constants
146
+
147
+ ```ts
148
+ import { NotificationService } from '@yaghmori/notification-service';
149
+
150
+ NotificationService.ports; // { tcp: 4004, http: 3000 }
151
+ NotificationService.patterns.CREATE_NOTIFICATION;
152
+ NotificationService.topics.SEND_REQUESTED;
153
+ ```
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@yaghmori/notification-service",
3
+ "version": "0.1.1",
4
+ "description": "Client SDK for notification-service — inbox HTTP/SSE, Nest TCP patterns, Kafka send topics, Zod DTOs",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./nest": {
15
+ "types": "./dist/nest.d.ts",
16
+ "default": "./dist/nest.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "contracts.json",
22
+ "README.md",
23
+ "docs"
24
+ ],
25
+ "dependencies": {
26
+ "zod": "^3.23.8"
27
+ },
28
+ "peerDependencies": {
29
+ "@nestjs/common": "^11.0.0",
30
+ "@nestjs/microservices": "^11.0.0",
31
+ "rxjs": "^7.8.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "@nestjs/common": {
35
+ "optional": true
36
+ },
37
+ "@nestjs/microservices": {
38
+ "optional": true
39
+ },
40
+ "rxjs": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "devDependencies": {
45
+ "@nestjs/common": "^11.0.0",
46
+ "@nestjs/microservices": "^11.0.0",
47
+ "@types/node": "^22.15.3",
48
+ "rxjs": "^7.8.0",
49
+ "typescript": "^5.9.3"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "registry": "https://registry.npmjs.org/"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/yaghmori/notification-service.git",
58
+ "directory": "packages/client"
59
+ },
60
+ "engines": {
61
+ "node": ">=18.0.0"
62
+ },
63
+ "scripts": {
64
+ "codegen": "node scripts/codegen.cjs",
65
+ "build": "pnpm run codegen && tsc -p tsconfig.json",
66
+ "check-types": "tsc -p tsconfig.json --noEmit"
67
+ }
68
+ }