@pya-platform/email 0.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/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @pya/email
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a9ca6bf: Initial release of the Pya platform packages. Extracted from `pyaeats-app`, consumed by `pyaeats-app` (food delivery) and `pyaserv` (services classifieds).
8
+
9
+ Each package exposes a Hono router factory (auth/cms/reviews/comments) or a typed helper (email/audit/cf) parameterised over Cloudflare D1 + KV bindings. UI primitives ship as Lit web components on top of `@pya/tokens` (CSS custom properties). See `ROADMAP.md` and `docs/phase-6-rollout.md` for the consumer cutover plan.
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@pya-platform/email",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "registry": "https://registry.npmjs.org",
7
+ "access": "public"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/undeadliner/pya-platform.git"
12
+ },
13
+ "type": "module",
14
+ "description": "Resend wrapper — single sendEmail entry, audit-log on failure, never throws.",
15
+ "exports": {
16
+ ".": "./src/index.ts"
17
+ },
18
+ "scripts": {
19
+ "type-check": "tsc --noEmit",
20
+ "test": "echo '@pya/email has no tests yet'"
21
+ },
22
+ "peerDependencies": {
23
+ "@cloudflare/workers-types": "^4.20240909.0"
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,87 @@
1
+ // @pya-platform/email — Resend wrapper.
2
+ //
3
+ // Single entry: `sendEmail()`. Never throws — notification failure
4
+ // must not block whatever caller transition already succeeded.
5
+ // Logs to the `audit` stream on failure so it's still observable.
6
+
7
+ export interface PyaEmailBindings {
8
+ readonly RESEND_API_KEY?: string
9
+ readonly EMAIL_DOMAIN?: string
10
+ }
11
+
12
+ export interface SendEmailParams {
13
+ readonly env: PyaEmailBindings
14
+ readonly to: string
15
+ readonly subject: string
16
+ readonly text: string
17
+ readonly html?: string
18
+ /** Brand name shown in the `From:` header. Defaults to "Pya". */
19
+ readonly brandName?: string
20
+ /** Mailbox local-part. Defaults to "noreply". */
21
+ readonly fromLocal?: string
22
+ }
23
+
24
+ const senderAddress = (env: PyaEmailBindings, brand: string, local: string): string => {
25
+ if (env.EMAIL_DOMAIN !== undefined && env.EMAIL_DOMAIN !== '') {
26
+ return `${brand} <${local}@${env.EMAIL_DOMAIN}>`
27
+ }
28
+ // Resend's free sandbox sender — usable until the domain is verified.
29
+ return `${brand} <onboarding@resend.dev>`
30
+ }
31
+
32
+ export const sendEmail = async (params: SendEmailParams): Promise<{ readonly ok: boolean }> => {
33
+ const { env, to, subject, text, html, brandName = 'Pya', fromLocal = 'noreply' } = params
34
+ const apiKey = env.RESEND_API_KEY ?? ''
35
+ if (apiKey === '') {
36
+ console.error(
37
+ JSON.stringify({
38
+ stream: 'audit',
39
+ event: 'email.send_skipped',
40
+ reason: 'RESEND_API_KEY unset',
41
+ to,
42
+ subject,
43
+ }),
44
+ )
45
+ return { ok: false }
46
+ }
47
+ const res = await fetch('https://api.resend.com/emails', {
48
+ method: 'POST',
49
+ headers: {
50
+ Authorization: `Bearer ${apiKey}`,
51
+ 'Content-Type': 'application/json',
52
+ },
53
+ body: JSON.stringify({
54
+ from: senderAddress(env, brandName, fromLocal),
55
+ to,
56
+ subject,
57
+ text,
58
+ ...(html !== undefined ? { html } : {}),
59
+ }),
60
+ })
61
+ if (!res.ok) {
62
+ const body = await res.text().catch(() => '')
63
+ console.error(
64
+ JSON.stringify({
65
+ stream: 'audit',
66
+ event: 'email.send_failed',
67
+ provider: 'resend',
68
+ status: res.status,
69
+ to,
70
+ subject,
71
+ body: body.slice(0, 500),
72
+ }),
73
+ )
74
+ return { ok: false }
75
+ }
76
+ return { ok: true }
77
+ }
78
+
79
+ /** Minimal HTML escape for embedding user-supplied text in templates.
80
+ * Use ONLY where the consumer is the email client, not the browser DOM. */
81
+ export const escapeHtml = (s: string): string =>
82
+ s
83
+ .replaceAll('&', '&amp;')
84
+ .replaceAll('<', '&lt;')
85
+ .replaceAll('>', '&gt;')
86
+ .replaceAll('"', '&quot;')
87
+ .replaceAll("'", '&#39;')
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "noEmit": true,
7
+ "types": ["@cloudflare/workers-types"]
8
+ },
9
+ "include": ["src/**/*.ts"]
10
+ }