@elqnt/notifications 1.0.0 → 1.1.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/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @elqnt/notifications
2
+
3
+ Outbound notification primitives for the Eloquent platform. Today: sending a
4
+ pre-rendered notification email through the API Gateway. Fully typed, zero `any`.
5
+
6
+ > **Building a custom app / using an AI coding agent?** Read [`SKILL.md`](./SKILL.md)
7
+ > — the full agent guide: the one-way architecture (intent → app hook → `useEmail`
8
+ > → gateway → integrations service), the gateway-token flow, and the exact spec of
9
+ > the hook (`useEmail` → `send`). It ships in the package, so the contract is this
10
+ > package's own `.d.ts` — your code type-checks against it.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pnpm add @elqnt/notifications
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```typescript
21
+ import { useEmail } from "@elqnt/notifications/hooks";
22
+
23
+ const { loading, error, send } = useEmail({ baseUrl: apiGatewayUrl, orgId });
24
+
25
+ const res = await send({
26
+ to: ["user@example.com"],
27
+ subject: "Your report is ready",
28
+ body: "<p>Hi — your weekly report is attached.</p>", // already rendered
29
+ });
30
+ // res: SendEmailResponse | null (null on error → check `error`)
31
+ ```
32
+
33
+ ## Hook
34
+
35
+ ### `useEmail(options)`
36
+
37
+ ```typescript
38
+ const {
39
+ loading, // boolean
40
+ error, // string | null
41
+ send, // (request: SendEmailRequest) => Promise<SendEmailResponse | null>
42
+ } = useEmail({ baseUrl, orgId });
43
+ ```
44
+
45
+ `send` posts to `POST /api/v1/email/send` (gateway → integrations service). It
46
+ **never throws**: on failure it returns `null` and sets `error`.
47
+
48
+ ## Types
49
+
50
+ ```typescript
51
+ import type { UseEmailOptions, UseEmailReturn } from "@elqnt/notifications/hooks";
52
+ import type { SendEmailRequest, SendEmailResponse } from "@elqnt/notifications/models";
53
+ ```
54
+
55
+ The root `@elqnt/notifications` re-exports the model types only (for SSR/type
56
+ imports). The hook is reached **only** via `@elqnt/notifications/hooks`.
package/SKILL.md ADDED
@@ -0,0 +1,328 @@
1
+ ---
2
+ name: notifications
3
+ description: Send notification emails from a custom frontend on the Eloquent platform using the @elqnt/notifications hooks. Covers the one true path (custom app → useEmail hook → API Gateway → integrations Go service), the gateway-token/secret flow, and the EXACT input/output spec of the hook (useEmail — send) plus all types.
4
+ ---
5
+
6
+ # Notifications — Sending Email from a Custom Frontend
7
+
8
+ Eloquent's **notifications** package wraps the platform's outbound notification
9
+ surface. Today it exposes **one** thing: sending a **pre-rendered notification
10
+ email** through the API Gateway to the integrations service. This skill is only
11
+ about driving that through the `@elqnt/notifications` package.
12
+
13
+ > Package is `@elqnt/notifications`. It exports exactly one hook: `useEmail`.
14
+ > The body you send is **already rendered** — `{ to, subject, body }`. The
15
+ > package does not template, queue, schedule, or fan-out; for bulk/newsletter
16
+ > flows the gateway has separate routes (`/api/v1/email/bulk-send`,
17
+ > `/api/v1/newsletter/...`) not yet wrapped by this package.
18
+
19
+ ---
20
+
21
+ ## ⛔ The contract — read this before writing any code
22
+
23
+ This skill ships **inside the package**, so the contract is not a separate file
24
+ to keep in sync — it **is** the package's own published type declarations
25
+ (`@elqnt/notifications` → `dist/**/*.d.ts`). Because your app *imports the real
26
+ hook*, the TypeScript compiler enforces the contract for you: a wrong param or a
27
+ misused return value won't compile.
28
+
29
+ The single source of truth, in order:
30
+
31
+ 1. `import { useEmail } from "@elqnt/notifications/hooks"` — the only hook.
32
+ 2. `import type { UseEmailOptions, UseEmailReturn } from "@elqnt/notifications/hooks"`
33
+ — the **named, exact** surface of the hook (every method, its params, its
34
+ return type). The table below is a human-readable mirror of these.
35
+ 3. `import type { SendEmailRequest, SendEmailResponse } from "@elqnt/notifications/models"`
36
+ — the request/response DTOs. (Also re-exported from the root
37
+ `@elqnt/notifications` for type-only/SSR imports.)
38
+
39
+ **Rules (do not drift):**
40
+
41
+ 1. **Use only the exported hook** and only the method it exposes (`send`). Do
42
+ **not** invent hooks, methods, params, or return shapes — if it's not on
43
+ `UseEmailReturn`, it doesn't exist.
44
+ 2. **Let the compiler check you.** Build with `tsc`; never `as any` /
45
+ `@ts-ignore` your way around the hook's types.
46
+ 3. **Never bypass the hook.** No `fetch`/`axios` to `/api/v1/email/...`, no NATS,
47
+ no direct service calls.
48
+ 4. **Wrap, don't scatter.** The hook is imported in exactly one app file
49
+ (`hooks/use-<domain>.ts`); see ["The domain layer"](#the-domain-layer).
50
+ 5. **`body` is already rendered.** The package sends what you give it; rendering
51
+ HTML/markdown is the app's job.
52
+
53
+ The prose/table below explains the method; the package's shipped `.d.ts` is what
54
+ your code type-checks against.
55
+
56
+ ---
57
+
58
+ ## Architecture — the one and only path
59
+
60
+ A component **never** calls the package hook directly. It only sees your **domain
61
+ intent** (e.g. "notify these users"), served by your **app hook**, which is the
62
+ *only* place that wraps `useEmail`.
63
+
64
+ ```
65
+ ┌──────────────────────────────────────────────────────────────────────────────┐
66
+ │ YOUR CUSTOM APP (Next.js / React) │
67
+ │ │
68
+ │ SSR layout: read API_GATEWAY_URL_PUBLIC (request-time) + ORG_ID │
69
+ │ └─► AppConfigProvider { apiGatewayUrl, orgId } │
70
+ │ │
71
+ │ components/* ── trigger "send a notification" intents │
72
+ │ │ │
73
+ │ ▼ │
74
+ │ hooks/use-notify.ts ── app hook: the only file importing @elqnt/notifications│
75
+ │ │ │
76
+ │ ▼ │
77
+ │ useEmail (@elqnt/notifications/hooks) │
78
+ │ │ │
79
+ │ ▼ │
80
+ │ browserApiRequest (@elqnt/api-client/browser) │
81
+ │ │ getGatewayToken() ⇒ GET /api/gateway-token (mint HS256 JWT, JWT_SECRET)│
82
+ │ │ attaches: Authorization: Bearer <jwt>, X-Org-ID, X-User-ID, X-Product│
83
+ └─────────┼──────────────────────────────────────────────────────────────────────┘
84
+
85
+ ┌─────────────────┐ verify JWT, stamp X-Org-ID/X-Product, route match
86
+ │ API GATEWAY │ POST /api/v1/email/send → integrations svc
87
+ └───────┬─────────┘
88
+
89
+ ┌─────────────────┐ renders nothing — sends the pre-rendered body
90
+ │ integrations(Go)│ via the configured email provider
91
+ └─────────────────┘
92
+ ```
93
+
94
+ Rules: the frontend **never** calls the integrations service directly and
95
+ **never** touches NATS. Every call is HTTP through the gateway carrying an org id
96
+ + gateway token. And **components never import `@elqnt/notifications`** — only the
97
+ app hook does.
98
+
99
+ ---
100
+
101
+ ## The gateway token (the secret) — how the custom app gets it
102
+
103
+ Every request to the gateway needs `Authorization: Bearer <token>`: a short-lived
104
+ **HS256 JWT** signed with the shared **gateway secret**. You never hardcode it.
105
+
106
+ ### Browser flow (what the hook uses)
107
+
108
+ The hook → `browserApiRequest` → `getGatewayToken()` internally. You do **not**
109
+ pass a token to the hook. `getGatewayToken()` (from `@elqnt/api-client/browser`)
110
+ by default does:
111
+
112
+ ```
113
+ fetch("/api/gateway-token") ⇒ { token, expiresIn }
114
+ ```
115
+
116
+ So your custom app must expose a **`/api/gateway-token` route** that mints the JWT
117
+ server-side (the secret stays on the server, never reaches the browser). The
118
+ client caches the token and refreshes ~5 min before expiry.
119
+
120
+ ```ts
121
+ // app/api/gateway-token/route.ts (Next.js route handler — server only)
122
+ import { NextResponse } from "next/server";
123
+ import * as jose from "jose";
124
+
125
+ export async function GET() {
126
+ const secret = new TextEncoder().encode(process.env.JWT_SECRET!); // SAME secret the gateway validates with
127
+ const token = await new jose.SignJWT({
128
+ org_id: process.env.ORG_ID!,
129
+ user_id: "system",
130
+ email: "system@my-app.com",
131
+ role: "system",
132
+ scopes: ["write:notifications"], // OR-matched against the route's required scopes
133
+ product: "my-product", // gateway resolves product from THIS claim first
134
+ })
135
+ .setProtectedHeader({ alg: "HS256" })
136
+ .setIssuedAt()
137
+ .setIssuer("eloquent-gateway") // must match gateway JWT_ISSUER
138
+ .setAudience("eloquent-api") // must match gateway JWT_AUDIENCE
139
+ .setExpirationTime("1h")
140
+ .sign(secret);
141
+
142
+ return NextResponse.json({ token, expiresIn: 3600 });
143
+ }
144
+ ```
145
+
146
+ > The gateway re-stamps `X-Org-ID` / `X-User-ID` / `X-Product` from the **signed
147
+ > JWT claims** before proxying, so a forged header can't override the token — the
148
+ > JWT is authoritative.
149
+
150
+ Override the token source (mobile/native, or a non-default URL) once at startup:
151
+
152
+ ```ts
153
+ import { configureAuth } from "@elqnt/api-client/browser";
154
+ configureAuth(async () => myTokenProvider()); // URL string or async () => token|null
155
+ ```
156
+
157
+ ### Server flow (server actions / SSR)
158
+
159
+ The `useEmail` hook is **browser-only** (`"use client"`). For SSR/server actions,
160
+ send via `@elqnt/api-client/server`'s `createServerClient` (mints the JWT itself
161
+ with `JWT_SECRET` — no `/api/gateway-token` hop) and POST `/api/v1/email/send`
162
+ directly.
163
+
164
+ ```ts
165
+ import { createServerClient } from "@elqnt/api-client/server";
166
+
167
+ const client = createServerClient({
168
+ gatewayUrl: process.env.API_GATEWAY_URL_INTERNAL!, // in-cluster gateway URL (server-side)
169
+ jwtSecret: process.env.JWT_SECRET!, // the shared gateway secret
170
+ defaultProduct: "my-product", // gateway reads product from the JWT claim first
171
+ defaultScopes: ["read", "write"],
172
+ });
173
+
174
+ await client.post("/api/v1/email/send",
175
+ { to: ["user@example.com"], subject: "Welcome", body: "<p>Hi</p>" },
176
+ { orgId });
177
+ ```
178
+
179
+ ### Env vars
180
+
181
+ | Var | Used by | Purpose |
182
+ |---|---|---|
183
+ | `JWT_SECRET` | `/api/gateway-token` route + `createServerClient` | sign the gateway JWT (same value the gateway validates with) |
184
+ | `API_GATEWAY_URL_INTERNAL` | `createServerClient` `gatewayUrl` | in-cluster gateway URL (server) |
185
+ | `API_GATEWAY_URL_PUBLIC` | SSR layout → `AppConfigProvider` → browser `baseUrl` | public gateway URL, read **at request time** (not `NEXT_PUBLIC_*`) |
186
+ | `ORG_ID` | token route / app config | the org all requests are scoped to |
187
+
188
+ ### Headers the API layer sets per request
189
+
190
+ | From hook option | Header |
191
+ |---|---|
192
+ | auto token | `Authorization: Bearer <token>` |
193
+ | `orgId` | `X-Org-ID` |
194
+ | `userId` | `X-User-ID` |
195
+ | `userEmail` | `X-User-Email` |
196
+ | `product` (default `"eloquent"`) | `X-Product` |
197
+
198
+ ---
199
+
200
+ ## Hook options
201
+
202
+ There is **no provider/context** in this package. You pass options into the hook
203
+ call. `UseEmailOptions` is the shared `ApiClientOptions`:
204
+
205
+ ```ts
206
+ interface ApiClientOptions {
207
+ baseUrl: string; // API Gateway base URL — required
208
+ orgId: string; // required → X-Org-ID
209
+ userId?: string; // → X-User-ID
210
+ userEmail?: string; // → X-User-Email
211
+ product?: string; // → X-Product, defaults to "eloquent"
212
+ headers?: Record<string, string>;
213
+ }
214
+
215
+ type UseEmailOptions = ApiClientOptions;
216
+ ```
217
+
218
+ > **Imperative, not auto-fetching.** The hook does **not** send anything on
219
+ > mount. You call `send(...)` on demand. The hook exposes `loading` / `error`
220
+ > flags. On failure `send` returns `null` and sets `error` — it does **not**
221
+ > throw.
222
+
223
+ ---
224
+
225
+ ## Hook: `useEmail`
226
+
227
+ ```ts
228
+ import { useEmail } from "@elqnt/notifications/hooks";
229
+
230
+ const email = useEmail({ baseUrl, orgId, product: "my-product" });
231
+ ```
232
+
233
+ Returns `{ loading, error, send }` (`UseEmailReturn`):
234
+
235
+ | Member | Signature | Resolves to | Endpoint |
236
+ |---|---|---|---|
237
+ | `loading` | `boolean` | — | — |
238
+ | `error` | `string \| null` | — | — |
239
+ | `send` | `(request: SendEmailRequest) => Promise<SendEmailResponse \| null>` | `null` on error (sets `error`) | `POST /api/v1/email/send` |
240
+
241
+ ```ts
242
+ const res = await email.send({
243
+ to: ["user@example.com", "ops@example.com"],
244
+ subject: "Your report is ready",
245
+ body: "<p>Hi — your weekly report is attached.</p>", // already rendered
246
+ });
247
+ if (!res || !res.success) {
248
+ // email.error is set; surface it
249
+ }
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Types (`@elqnt/notifications/models`)
255
+
256
+ > Generated from Go via `tygo generate`. Never edit `models/email.ts` manually.
257
+
258
+ ```ts
259
+ /** Request for sending a pre-rendered notification email. */
260
+ interface SendEmailRequest {
261
+ to: string[]; // one or more recipient addresses
262
+ subject: string;
263
+ body: string; // already-rendered HTML/text
264
+ }
265
+
266
+ /** Response from sending an email. */
267
+ interface SendEmailResponse {
268
+ success: boolean;
269
+ message?: string; // provider/error detail when present
270
+ }
271
+ ```
272
+
273
+ ---
274
+
275
+ ## The domain layer
276
+
277
+ Wrap the hook in **one** app file; components speak your intent, not
278
+ `SendEmailRequest`.
279
+
280
+ ```ts
281
+ // hooks/use-notify.ts
282
+ "use client";
283
+ import { useEmail } from "@elqnt/notifications/hooks";
284
+ import { useAppConfig } from "@/contexts/app-config-context";
285
+
286
+ export function useNotify() {
287
+ const { apiGatewayUrl, orgId } = useAppConfig();
288
+ const email = useEmail({ baseUrl: apiGatewayUrl, orgId, product: "my-product" });
289
+
290
+ return {
291
+ loading: email.loading,
292
+ error: email.error,
293
+ async notifyReportReady(to: string[], reportName: string): Promise<boolean> {
294
+ const res = await email.send({
295
+ to,
296
+ subject: `Your report "${reportName}" is ready`,
297
+ body: renderReportEmail(reportName), // your app renders the body
298
+ });
299
+ return !!res?.success;
300
+ },
301
+ };
302
+ }
303
+
304
+ export type UseNotifyReturn = ReturnType<typeof useNotify>;
305
+ ```
306
+
307
+ `{ apiGatewayUrl, orgId }` come from an `AppConfigProvider` fed by SSR
308
+ (`API_GATEWAY_URL_PUBLIC` read at request time — never a `NEXT_PUBLIC_*` var).
309
+ See the `@elqnt/entity` SKILL.md for the full `AppConfigProvider` pattern.
310
+
311
+ ---
312
+
313
+ ## Gotchas
314
+
315
+ - **One hook, one method.** Only `useEmail().send`. Bulk-send and newsletter
316
+ flows exist on the gateway but are **not** wrapped here yet — don't invent
317
+ `sendBulk`/`useNewsletter`.
318
+ - **`body` is pre-rendered.** The package/service does not template. Render the
319
+ HTML/text in your app before calling `send`.
320
+ - **`send` doesn't throw.** It returns `null` and sets `error` on failure (and a
321
+ non-null result may still have `success: false` — check it).
322
+ - **Hook is browser-only** (`"use client"`). For SSR/server actions, POST
323
+ `/api/v1/email/send` via `createServerClient` — not the hook.
324
+ - **Root import is models-only.** `@elqnt/notifications` re-exports only the
325
+ types; the hook is reached **only** via `@elqnt/notifications/hooks`. Importing
326
+ the hook from the root would drag `"use client"` into server components.
327
+ - **`product` matters.** The gateway resolves product from the **JWT claim**
328
+ (server) and `X-Product` (browser). It scopes which org/provider config is used.
@@ -1,11 +1,31 @@
1
+ import { ApiClientOptions } from '@elqnt/api-client/browser';
1
2
  import { SendEmailRequest, SendEmailResponse } from '../models/index.mjs';
2
3
 
3
- interface UseEmailOptions {
4
- baseUrl: string;
5
- orgId: string;
6
- }
7
- declare function useEmail(options: UseEmailOptions): {
4
+ /**
5
+ * Options for {@link useEmail}. Extends the shared gateway client options
6
+ * (`baseUrl`, `orgId`, `product`, …) from `@elqnt/api-client`.
7
+ */
8
+ type UseEmailOptions = ApiClientOptions;
9
+ /**
10
+ * The exact, named contract of {@link useEmail}. Consumers type-check against
11
+ * this published interface — it can't drift from the implementation.
12
+ *
13
+ * Per the SDK hook contract: `{ loading, error, ...methods }`; methods are
14
+ * `async (...args) => Promise<Result>`, call on demand (no fetch-on-mount), and
15
+ * never throw — on failure they resolve to a documented default and set `error`.
16
+ */
17
+ interface UseEmailReturn {
18
+ /** True while a `send` request is in flight. */
19
+ loading: boolean;
20
+ /** Last error message, or `null`. Cleared at the start of each `send`. */
21
+ error: string | null;
22
+ /**
23
+ * Send a pre-rendered notification email.
24
+ * `POST /api/v1/email/send` (gateway → integrations service).
25
+ * Resolves to the response on success, or `null` on failure (sets `error`).
26
+ */
8
27
  send: (request: SendEmailRequest) => Promise<SendEmailResponse | null>;
9
- };
28
+ }
29
+ declare function useEmail(options: UseEmailOptions): UseEmailReturn;
10
30
 
11
- export { useEmail };
31
+ export { type UseEmailOptions, type UseEmailReturn, useEmail };
@@ -1,11 +1,31 @@
1
+ import { ApiClientOptions } from '@elqnt/api-client/browser';
1
2
  import { SendEmailRequest, SendEmailResponse } from '../models/index.js';
2
3
 
3
- interface UseEmailOptions {
4
- baseUrl: string;
5
- orgId: string;
6
- }
7
- declare function useEmail(options: UseEmailOptions): {
4
+ /**
5
+ * Options for {@link useEmail}. Extends the shared gateway client options
6
+ * (`baseUrl`, `orgId`, `product`, …) from `@elqnt/api-client`.
7
+ */
8
+ type UseEmailOptions = ApiClientOptions;
9
+ /**
10
+ * The exact, named contract of {@link useEmail}. Consumers type-check against
11
+ * this published interface — it can't drift from the implementation.
12
+ *
13
+ * Per the SDK hook contract: `{ loading, error, ...methods }`; methods are
14
+ * `async (...args) => Promise<Result>`, call on demand (no fetch-on-mount), and
15
+ * never throw — on failure they resolve to a documented default and set `error`.
16
+ */
17
+ interface UseEmailReturn {
18
+ /** True while a `send` request is in flight. */
19
+ loading: boolean;
20
+ /** Last error message, or `null`. Cleared at the start of each `send`. */
21
+ error: string | null;
22
+ /**
23
+ * Send a pre-rendered notification email.
24
+ * `POST /api/v1/email/send` (gateway → integrations service).
25
+ * Resolves to the response on success, or `null` on failure (sets `error`).
26
+ */
8
27
  send: (request: SendEmailRequest) => Promise<SendEmailResponse | null>;
9
- };
28
+ }
29
+ declare function useEmail(options: UseEmailOptions): UseEmailReturn;
10
30
 
11
- export { useEmail };
31
+ export { type UseEmailOptions, type UseEmailReturn, useEmail };
@@ -1,9 +1,38 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});"use client";
2
- "use client";
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }"use client";
3
2
 
3
+ // hooks/use-email.ts
4
+ var _react = require('react');
5
+ var _browser = require('@elqnt/api-client/browser');
6
+ function useEmail(options) {
7
+ const [loading, setLoading] = _react.useState.call(void 0, false);
8
+ const [error, setError] = _react.useState.call(void 0, null);
9
+ const apiOptions = _react.useMemo.call(void 0, () => ({ ...options }), [options]);
10
+ const send = _react.useCallback.call(void 0,
11
+ async (request) => {
12
+ setLoading(true);
13
+ setError(null);
14
+ try {
15
+ const response = await _browser.browserApiRequest.call(void 0,
16
+ "/api/v1/email/send",
17
+ {
18
+ method: "POST",
19
+ body: request,
20
+ ...apiOptions
21
+ }
22
+ );
23
+ return _nullishCoalesce(response.data, () => ( null));
24
+ } catch (e) {
25
+ setError(e instanceof Error ? e.message : "Failed to send email");
26
+ return null;
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ },
31
+ [apiOptions]
32
+ );
33
+ return { loading, error, send };
34
+ }
4
35
 
5
- var _chunkE6ORNZJXjs = require('../chunk-E6ORNZJX.js');
6
36
 
7
-
8
- exports.useEmail = _chunkE6ORNZJXjs.useEmail;
37
+ exports.useEmail = useEmail;
9
38
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/eloquent-packages/eloquent-packages/packages/notifications/dist/hooks/index.js"],"names":[],"mappings":"AAAA,qFAAY;AACZ,YAAY;AACZ;AACE;AACF,uDAA6B;AAC7B;AACE;AACF,6CAAC","file":"/home/runner/work/eloquent-packages/eloquent-packages/packages/notifications/dist/hooks/index.js"}
1
+ {"version":3,"sources":["/home/runner/work/eloquent/eloquent/packages/@elqnt/notifications/dist/hooks/index.js","../../hooks/use-email.ts"],"names":[],"mappings":"AAAA,yLAAY;AACZ;AACA;ACAA,8BAA+C;AAC/C,oDAAkC;AA+B3B,SAAS,QAAA,CAAS,OAAA,EAA0C;AACjE,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,EAAA,EAAI,6BAAA,KAAc,CAAA;AAC5C,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,EAAA,EAAI,6BAAA,IAA4B,CAAA;AAEtD,EAAA,MAAM,WAAA,EAAa,4BAAA,CAA0B,EAAA,GAAA,CAAO,EAAE,GAAG,QAAQ,CAAA,CAAA,EAAI,CAAC,OAAO,CAAC,CAAA;AAE9E,EAAA,MAAM,KAAA,EAAO,gCAAA;AAAA,IACX,MAAA,CAAO,OAAA,EAAA,GAAiE;AACtE,MAAA,UAAA,CAAW,IAAI,CAAA;AACf,MAAA,QAAA,CAAS,IAAI,CAAA;AACb,MAAA,IAAI;AACF,QAAA,MAAM,SAAA,EAAW,MAAM,wCAAA;AAAA,UACrB,oBAAA;AAAA,UACA;AAAA,YACE,MAAA,EAAQ,MAAA;AAAA,YACR,IAAA,EAAM,OAAA;AAAA,YACN,GAAG;AAAA,UACL;AAAA,QACF,CAAA;AACA,QAAA,wBAAO,QAAA,CAAS,IAAA,UAAQ,MAAA;AAAA,MAC1B,EAAA,MAAA,CAAS,CAAA,EAAG;AACV,QAAA,QAAA,CAAS,EAAA,WAAa,MAAA,EAAQ,CAAA,CAAE,QAAA,EAAU,sBAAsB,CAAA;AAChE,QAAA,OAAO,IAAA;AAAA,MACT,EAAA,QAAE;AACA,QAAA,UAAA,CAAW,KAAK,CAAA;AAAA,MAClB;AAAA,IACF,CAAA;AAAA,IACA,CAAC,UAAU;AAAA,EACb,CAAA;AAEA,EAAA,OAAO,EAAE,OAAA,EAAS,KAAA,EAAO,KAAK,CAAA;AAChC;AD/BA;AACE;AACF,4BAAC","file":"/home/runner/work/eloquent/eloquent/packages/@elqnt/notifications/dist/hooks/index.js","sourcesContent":[null,"\"use client\";\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport { browserApiRequest } from \"@elqnt/api-client/browser\";\nimport type { ApiClientOptions } from \"@elqnt/api-client/browser\";\nimport type { SendEmailRequest, SendEmailResponse } from \"../models\";\n\n/**\n * Options for {@link useEmail}. Extends the shared gateway client options\n * (`baseUrl`, `orgId`, `product`, …) from `@elqnt/api-client`.\n */\nexport type UseEmailOptions = ApiClientOptions;\n\n/**\n * The exact, named contract of {@link useEmail}. Consumers type-check against\n * this published interface — it can't drift from the implementation.\n *\n * Per the SDK hook contract: `{ loading, error, ...methods }`; methods are\n * `async (...args) => Promise<Result>`, call on demand (no fetch-on-mount), and\n * never throw — on failure they resolve to a documented default and set `error`.\n */\nexport interface UseEmailReturn {\n /** True while a `send` request is in flight. */\n loading: boolean;\n /** Last error message, or `null`. Cleared at the start of each `send`. */\n error: string | null;\n /**\n * Send a pre-rendered notification email.\n * `POST /api/v1/email/send` (gateway → integrations service).\n * Resolves to the response on success, or `null` on failure (sets `error`).\n */\n send: (request: SendEmailRequest) => Promise<SendEmailResponse | null>;\n}\n\nexport function useEmail(options: UseEmailOptions): UseEmailReturn {\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const apiOptions = useMemo<ApiClientOptions>(() => ({ ...options }), [options]);\n\n const send = useCallback(\n async (request: SendEmailRequest): Promise<SendEmailResponse | null> => {\n setLoading(true);\n setError(null);\n try {\n const response = await browserApiRequest<SendEmailResponse>(\n \"/api/v1/email/send\",\n {\n method: \"POST\",\n body: request,\n ...apiOptions,\n }\n );\n return response.data ?? null;\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Failed to send email\");\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apiOptions]\n );\n\n return { loading, error, send };\n}\n"]}
@@ -1,8 +1,37 @@
1
1
  "use client";
2
- "use client";
3
- import {
4
- useEmail
5
- } from "../chunk-OPXXF4IO.mjs";
2
+
3
+ // hooks/use-email.ts
4
+ import { useCallback, useMemo, useState } from "react";
5
+ import { browserApiRequest } from "@elqnt/api-client/browser";
6
+ function useEmail(options) {
7
+ const [loading, setLoading] = useState(false);
8
+ const [error, setError] = useState(null);
9
+ const apiOptions = useMemo(() => ({ ...options }), [options]);
10
+ const send = useCallback(
11
+ async (request) => {
12
+ setLoading(true);
13
+ setError(null);
14
+ try {
15
+ const response = await browserApiRequest(
16
+ "/api/v1/email/send",
17
+ {
18
+ method: "POST",
19
+ body: request,
20
+ ...apiOptions
21
+ }
22
+ );
23
+ return response.data ?? null;
24
+ } catch (e) {
25
+ setError(e instanceof Error ? e.message : "Failed to send email");
26
+ return null;
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ },
31
+ [apiOptions]
32
+ );
33
+ return { loading, error, send };
34
+ }
6
35
  export {
7
36
  useEmail
8
37
  };
@@ -1 +1 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
1
+ {"version":3,"sources":["../../hooks/use-email.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport { browserApiRequest } from \"@elqnt/api-client/browser\";\nimport type { ApiClientOptions } from \"@elqnt/api-client/browser\";\nimport type { SendEmailRequest, SendEmailResponse } from \"../models\";\n\n/**\n * Options for {@link useEmail}. Extends the shared gateway client options\n * (`baseUrl`, `orgId`, `product`, …) from `@elqnt/api-client`.\n */\nexport type UseEmailOptions = ApiClientOptions;\n\n/**\n * The exact, named contract of {@link useEmail}. Consumers type-check against\n * this published interface — it can't drift from the implementation.\n *\n * Per the SDK hook contract: `{ loading, error, ...methods }`; methods are\n * `async (...args) => Promise<Result>`, call on demand (no fetch-on-mount), and\n * never throw — on failure they resolve to a documented default and set `error`.\n */\nexport interface UseEmailReturn {\n /** True while a `send` request is in flight. */\n loading: boolean;\n /** Last error message, or `null`. Cleared at the start of each `send`. */\n error: string | null;\n /**\n * Send a pre-rendered notification email.\n * `POST /api/v1/email/send` (gateway → integrations service).\n * Resolves to the response on success, or `null` on failure (sets `error`).\n */\n send: (request: SendEmailRequest) => Promise<SendEmailResponse | null>;\n}\n\nexport function useEmail(options: UseEmailOptions): UseEmailReturn {\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const apiOptions = useMemo<ApiClientOptions>(() => ({ ...options }), [options]);\n\n const send = useCallback(\n async (request: SendEmailRequest): Promise<SendEmailResponse | null> => {\n setLoading(true);\n setError(null);\n try {\n const response = await browserApiRequest<SendEmailResponse>(\n \"/api/v1/email/send\",\n {\n method: \"POST\",\n body: request,\n ...apiOptions,\n }\n );\n return response.data ?? null;\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Failed to send email\");\n return null;\n } finally {\n setLoading(false);\n }\n },\n [apiOptions]\n );\n\n return { loading, error, send };\n}\n"],"mappings":";;;AAEA,SAAS,aAAa,SAAS,gBAAgB;AAC/C,SAAS,yBAAyB;AA+B3B,SAAS,SAAS,SAA0C;AACjE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AAEtD,QAAM,aAAa,QAA0B,OAAO,EAAE,GAAG,QAAQ,IAAI,CAAC,OAAO,CAAC;AAE9E,QAAM,OAAO;AAAA,IACX,OAAO,YAAiE;AACtE,iBAAW,IAAI;AACf,eAAS,IAAI;AACb,UAAI;AACF,cAAM,WAAW,MAAM;AAAA,UACrB;AAAA,UACA;AAAA,YACE,QAAQ;AAAA,YACR,MAAM;AAAA,YACN,GAAG;AAAA,UACL;AAAA,QACF;AACA,eAAO,SAAS,QAAQ;AAAA,MAC1B,SAAS,GAAG;AACV,iBAAS,aAAa,QAAQ,EAAE,UAAU,sBAAsB;AAChE,eAAO;AAAA,MACT,UAAE;AACA,mBAAW,KAAK;AAAA,MAClB;AAAA,IACF;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,SAAO,EAAE,SAAS,OAAO,KAAK;AAChC;","names":[]}
package/dist/index.d.mts CHANGED
@@ -1,2 +1 @@
1
1
  export { SendEmailRequest, SendEmailResponse } from './models/index.mjs';
2
- export { useEmail } from './hooks/index.mjs';
package/dist/index.d.ts CHANGED
@@ -1,2 +1 @@
1
1
  export { SendEmailRequest, SendEmailResponse } from './models/index.js';
2
- export { useEmail } from './hooks/index.js';
package/dist/index.js CHANGED
@@ -1,8 +1 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});"use client";
2
-
3
-
4
- var _chunkE6ORNZJXjs = require('./chunk-E6ORNZJX.js');
5
-
6
-
7
- exports.useEmail = _chunkE6ORNZJXjs.useEmail;
8
- //# sourceMappingURL=index.js.map
1
+ "use strict";//# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/eloquent-packages/eloquent-packages/packages/notifications/dist/index.js"],"names":[],"mappings":"AAAA,qFAAY;AACZ;AACE;AACF,sDAA4B;AAC5B;AACE;AACF,6CAAC","file":"/home/runner/work/eloquent-packages/eloquent-packages/packages/notifications/dist/index.js"}
1
+ {"version":3,"sources":["/home/runner/work/eloquent/eloquent/packages/@elqnt/notifications/dist/index.js"],"names":[],"mappings":"AAAA","file":"/home/runner/work/eloquent/eloquent/packages/@elqnt/notifications/dist/index.js"}
package/dist/index.mjs CHANGED
@@ -1,8 +1 @@
1
- "use client";
2
- import {
3
- useEmail
4
- } from "./chunk-OPXXF4IO.mjs";
5
- export {
6
- useEmail
7
- };
8
1
  //# sourceMappingURL=index.mjs.map
@@ -1,2 +1 @@
1
- "use strict";"use client";
2
- //# sourceMappingURL=index.js.map
1
+ "use strict";//# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/eloquent-packages/eloquent-packages/packages/notifications/dist/models/index.js"],"names":[],"mappings":"AAAA,yBAAY","file":"/home/runner/work/eloquent-packages/eloquent-packages/packages/notifications/dist/models/index.js"}
1
+ {"version":3,"sources":["/home/runner/work/eloquent/eloquent/packages/@elqnt/notifications/dist/models/index.js"],"names":[],"mappings":"AAAA","file":"/home/runner/work/eloquent/eloquent/packages/@elqnt/notifications/dist/models/index.js"}
@@ -1,2 +1 @@
1
- "use client";
2
1
  //# sourceMappingURL=index.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elqnt/notifications",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Notification infrastructure for Eloquent platform (email, push, SMS)",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -23,30 +23,31 @@
23
23
  }
24
24
  },
25
25
  "files": [
26
- "dist"
26
+ "dist",
27
+ "SKILL.md"
27
28
  ],
28
- "scripts": {
29
- "build": "tsup",
30
- "dev": "tsup --watch",
31
- "clean": "rm -rf dist",
32
- "typecheck": "tsc --noEmit"
33
- },
34
29
  "repository": {
35
30
  "type": "git",
36
31
  "url": "git+https://github.com/Blazi-Commerce/eloquent-packages.git",
37
32
  "directory": "packages/notifications"
38
33
  },
39
34
  "dependencies": {
40
- "@elqnt/api-client": "^1.0.0"
35
+ "@elqnt/api-client": "2.2.0"
41
36
  },
42
37
  "peerDependencies": {
43
38
  "react": "^18.0.0 || ^19.0.0"
44
39
  },
45
40
  "devDependencies": {
46
- "@elqnt/api-client": "^1.0.0",
47
41
  "@types/react": "^19.0.0",
48
42
  "react": "^19.0.0",
49
43
  "tsup": "^8.0.0",
50
- "typescript": "^5.0.0"
44
+ "typescript": "^5.0.0",
45
+ "@elqnt/api-client": "2.2.0"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "dev": "tsup --watch",
50
+ "clean": "rm -rf dist",
51
+ "typecheck": "tsc --noEmit"
51
52
  }
52
- }
53
+ }
@@ -1,36 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }"use client";
2
-
3
- // hooks/use-email.ts
4
- var _react = require('react');
5
- var _browser = require('@elqnt/api-client/browser');
6
- function useEmail(options) {
7
- const apiOptions = _react.useMemo.call(void 0,
8
- () => ({ baseUrl: options.baseUrl, orgId: options.orgId }),
9
- [options.baseUrl, options.orgId]
10
- );
11
- const send = _react.useCallback.call(void 0,
12
- async (request) => {
13
- try {
14
- const response = await _browser.browserApiRequest.call(void 0,
15
- "/api/v1/email/send",
16
- {
17
- method: "POST",
18
- body: request,
19
- ...apiOptions
20
- }
21
- );
22
- return _nullishCoalesce(response.data, () => ( null));
23
- } catch (e) {
24
- console.warn("Failed to send email notification");
25
- return null;
26
- }
27
- },
28
- [apiOptions]
29
- );
30
- return { send };
31
- }
32
-
33
-
34
-
35
- exports.useEmail = useEmail;
36
- //# sourceMappingURL=chunk-E6ORNZJX.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/eloquent-packages/eloquent-packages/packages/notifications/dist/chunk-E6ORNZJX.js","../hooks/use-email.ts"],"names":[],"mappings":"AAAA,yLAAY;AACZ;AACA;ACAA,8BAAqC;AACrC,oDAAkC;AAS3B,SAAS,QAAA,CAAS,OAAA,EAA0B;AACjD,EAAA,MAAM,WAAA,EAAa,4BAAA;AAAA,IACjB,CAAA,EAAA,GAAA,CAAO,EAAE,OAAA,EAAS,OAAA,CAAQ,OAAA,EAAS,KAAA,EAAO,OAAA,CAAQ,MAAM,CAAA,CAAA;AAAA,IACxD,CAAC,OAAA,CAAQ,OAAA,EAAS,OAAA,CAAQ,KAAK;AAAA,EACjC,CAAA;AAEA,EAAA,MAAM,KAAA,EAAO,gCAAA;AAAA,IACX,MAAA,CAAO,OAAA,EAAA,GAAiE;AACtE,MAAA,IAAI;AACF,QAAA,MAAM,SAAA,EAAW,MAAM,wCAAA;AAAA,UACrB,oBAAA;AAAA,UACA;AAAA,YACE,MAAA,EAAQ,MAAA;AAAA,YACR,IAAA,EAAM,OAAA;AAAA,YACN,GAAG;AAAA,UACL;AAAA,QACF,CAAA;AACA,QAAA,wBAAO,QAAA,CAAS,IAAA,UAAQ,MAAA;AAAA,MAC1B,EAAA,UAAQ;AACN,QAAA,OAAA,CAAQ,IAAA,CAAK,mCAAmC,CAAA;AAChD,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,IACF,CAAA;AAAA,IACA,CAAC,UAAU;AAAA,EACb,CAAA;AAEA,EAAA,OAAO,EAAE,KAAK,CAAA;AAChB;ADRA;AACA;AACE;AACF,4BAAC","file":"/home/runner/work/eloquent-packages/eloquent-packages/packages/notifications/dist/chunk-E6ORNZJX.js","sourcesContent":[null,"\"use client\";\n\nimport { useCallback, useMemo } from \"react\";\nimport { browserApiRequest } from \"@elqnt/api-client/browser\";\nimport type { ApiClientOptions } from \"@elqnt/api-client/browser\";\nimport type { SendEmailRequest, SendEmailResponse } from \"../models\";\n\ninterface UseEmailOptions {\n baseUrl: string;\n orgId: string;\n}\n\nexport function useEmail(options: UseEmailOptions) {\n const apiOptions = useMemo<ApiClientOptions>(\n () => ({ baseUrl: options.baseUrl, orgId: options.orgId }),\n [options.baseUrl, options.orgId]\n );\n\n const send = useCallback(\n async (request: SendEmailRequest): Promise<SendEmailResponse | null> => {\n try {\n const response = await browserApiRequest<SendEmailResponse>(\n \"/api/v1/email/send\",\n {\n method: \"POST\",\n body: request,\n ...apiOptions,\n }\n );\n return response.data ?? null;\n } catch {\n console.warn(\"Failed to send email notification\");\n return null;\n }\n },\n [apiOptions]\n );\n\n return { send };\n}\n"]}
@@ -1,36 +0,0 @@
1
- "use client";
2
-
3
- // hooks/use-email.ts
4
- import { useCallback, useMemo } from "react";
5
- import { browserApiRequest } from "@elqnt/api-client/browser";
6
- function useEmail(options) {
7
- const apiOptions = useMemo(
8
- () => ({ baseUrl: options.baseUrl, orgId: options.orgId }),
9
- [options.baseUrl, options.orgId]
10
- );
11
- const send = useCallback(
12
- async (request) => {
13
- try {
14
- const response = await browserApiRequest(
15
- "/api/v1/email/send",
16
- {
17
- method: "POST",
18
- body: request,
19
- ...apiOptions
20
- }
21
- );
22
- return response.data ?? null;
23
- } catch {
24
- console.warn("Failed to send email notification");
25
- return null;
26
- }
27
- },
28
- [apiOptions]
29
- );
30
- return { send };
31
- }
32
-
33
- export {
34
- useEmail
35
- };
36
- //# sourceMappingURL=chunk-OPXXF4IO.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../hooks/use-email.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo } from \"react\";\nimport { browserApiRequest } from \"@elqnt/api-client/browser\";\nimport type { ApiClientOptions } from \"@elqnt/api-client/browser\";\nimport type { SendEmailRequest, SendEmailResponse } from \"../models\";\n\ninterface UseEmailOptions {\n baseUrl: string;\n orgId: string;\n}\n\nexport function useEmail(options: UseEmailOptions) {\n const apiOptions = useMemo<ApiClientOptions>(\n () => ({ baseUrl: options.baseUrl, orgId: options.orgId }),\n [options.baseUrl, options.orgId]\n );\n\n const send = useCallback(\n async (request: SendEmailRequest): Promise<SendEmailResponse | null> => {\n try {\n const response = await browserApiRequest<SendEmailResponse>(\n \"/api/v1/email/send\",\n {\n method: \"POST\",\n body: request,\n ...apiOptions,\n }\n );\n return response.data ?? null;\n } catch {\n console.warn(\"Failed to send email notification\");\n return null;\n }\n },\n [apiOptions]\n );\n\n return { send };\n}\n"],"mappings":";;;AAEA,SAAS,aAAa,eAAe;AACrC,SAAS,yBAAyB;AAS3B,SAAS,SAAS,SAA0B;AACjD,QAAM,aAAa;AAAA,IACjB,OAAO,EAAE,SAAS,QAAQ,SAAS,OAAO,QAAQ,MAAM;AAAA,IACxD,CAAC,QAAQ,SAAS,QAAQ,KAAK;AAAA,EACjC;AAEA,QAAM,OAAO;AAAA,IACX,OAAO,YAAiE;AACtE,UAAI;AACF,cAAM,WAAW,MAAM;AAAA,UACrB;AAAA,UACA;AAAA,YACE,QAAQ;AAAA,YACR,MAAM;AAAA,YACN,GAAG;AAAA,UACL;AAAA,QACF;AACA,eAAO,SAAS,QAAQ;AAAA,MAC1B,QAAQ;AACN,gBAAQ,KAAK,mCAAmC;AAChD,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,SAAO,EAAE,KAAK;AAChB;","names":[]}