@steve31415/baselib 3.0.2 → 3.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 CHANGED
@@ -6,7 +6,7 @@ What it provides and why: `docs/SPEC.md`. How it's put together:
6
6
  `~/migration/research/base-services-design.md` (step 5).
7
7
 
8
8
  Server subpath exports: `config`, `log`, `auth`, `s2s`, `db`, `http`, `sync`,
9
- `app-update`, and `llm`. Browser exports: `log-browser`, `rum`, `sync-browser`, and
9
+ `app-update`, `llm`, and `email`. Browser exports: `log-browser`, `rum`, `sync-browser`, and
10
10
  `app-update-browser`.
11
11
 
12
12
  `sync` provides the Postgres event-log and server protocol primitives;
@@ -23,6 +23,10 @@ generic Yjs or service-worker coordinator.
23
23
  registry, automatic cross-provider failover, portable JSON-schema output.
24
24
  Usage guide: `~/plasticine-way/docs/LLM.md`.
25
25
 
26
+ `email` is the fleet's transactional email path: `sendEmail` through Resend
27
+ from a named `NAME@snewman.net` sender, no outbox — a failed send is an
28
+ ERROR log (`~/plasticine-way/docs/SECURITY.md`, "Resend API key").
29
+
26
30
  Bins: `check-test-owners` — the fleet's structural test-coverage gate; every
27
31
  app runs it from `npm run verify`.
28
32
 
@@ -0,0 +1,34 @@
1
+ import { type Logger } from './log-core.js';
2
+ export declare const RESEND_API_URL = "https://api.resend.com/emails";
3
+ /** An email to send. At least one of `text` / `html` must be present. */
4
+ export interface EmailMessage {
5
+ /** RFC 5322 from address, e.g. `Watchdog <watchdog@snewman.net>`. */
6
+ from: string;
7
+ to: string;
8
+ subject: string;
9
+ text?: string;
10
+ html?: string;
11
+ }
12
+ export interface SendEmailResult {
13
+ ok: boolean;
14
+ /** Resend's message id, when the send succeeded and the response parsed. */
15
+ emailId?: string;
16
+ /** Human-readable failure description, present when `ok` is false. */
17
+ error?: string;
18
+ }
19
+ export interface EmailDeps {
20
+ /** Resend API key. Falsy means "email not configured": logged at ERROR with
21
+ * the message, never sent. */
22
+ apiKey: string | undefined;
23
+ logger: Logger;
24
+ /** Defaults to the global `fetch`. Injectable for tests. */
25
+ fetchImpl?: typeof fetch;
26
+ /** Merged into every log line so callers can attribute the send. */
27
+ context?: Record<string, unknown>;
28
+ }
29
+ /**
30
+ * Send one email through Resend. Never throws: every failure is an ERROR
31
+ * log (with enough of the message to redo the send by hand) and an
32
+ * `{ ok: false, error }` result the caller surfaces where it matters.
33
+ */
34
+ export declare function sendEmail(msg: EmailMessage, deps: EmailDeps): Promise<SendEmailResult>;
package/dist/email.js ADDED
@@ -0,0 +1,80 @@
1
+ // Transactional email through Resend — the fleet's one email path for
2
+ // system-generated mail (Steve's ruling 2026-09-03, migration decision log):
3
+ // named senders on the verified snewman.net domain (`Watchdog
4
+ // <watchdog@snewman.net>`, `Lurch <steve@snewman.net>`, …), key from Secret
5
+ // Manager `shared--resend-api-key`. Gmail via Mirror2's token broker stays
6
+ // only for mail that is genuinely Steve-to-Steve (Digest2).
7
+ //
8
+ // Ported from the old-world package @steve31415/resend-mailer 1.0.0
9
+ // (plasticine-apps/resend-mailer@5148649) minus its D1 outbox: the new-world
10
+ // rule is no outbox — a failed send is an ERROR log carrying the message,
11
+ // so the daily triage sees it and the send can be redone by hand. The
12
+ // request shape and error handling are the package's, kept verbatim: they
13
+ // ran against the live Resend API for months (TESTING.md, "Assumptions
14
+ // about external systems"): `POST https://api.resend.com/emails` with
15
+ // `{from, to: [to], subject, text?, html?}` answers 200 `{id}`; failures are
16
+ // 4xx/5xx with a JSON body.
17
+ import { serializeError, truncate } from './log-core.js';
18
+ export const RESEND_API_URL = 'https://api.resend.com/emails';
19
+ /**
20
+ * Send one email through Resend. Never throws: every failure is an ERROR
21
+ * log (with enough of the message to redo the send by hand) and an
22
+ * `{ ok: false, error }` result the caller surfaces where it matters.
23
+ */
24
+ export async function sendEmail(msg, deps) {
25
+ const { logger } = deps;
26
+ const fetchImpl = deps.fetchImpl ?? fetch;
27
+ const base = {
28
+ to: msg.to,
29
+ subject: truncate(msg.subject, 200),
30
+ textLength: msg.text?.length ?? 0,
31
+ htmlLength: msg.html?.length ?? 0,
32
+ ...deps.context,
33
+ };
34
+ const preview = { text: truncate(msg.text ?? '', 500), html: truncate(msg.html ?? '', 500) };
35
+ if (!msg.text && !msg.html) {
36
+ logger.error('email_rejected_no_body', base);
37
+ return { ok: false, error: 'sendEmail requires a non-empty `text` or `html` body' };
38
+ }
39
+ if (!deps.apiKey) {
40
+ // Nothing else will record what we failed to send: log the whole message.
41
+ logger.error('email_not_configured', { ...base, hasKey: false, ...preview });
42
+ return { ok: false, error: 'Email not configured: missing Resend API key' };
43
+ }
44
+ logger.info('email_sending', base);
45
+ const start = Date.now();
46
+ try {
47
+ const res = await fetchImpl(RESEND_API_URL, {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${deps.apiKey}` },
50
+ body: JSON.stringify({
51
+ from: msg.from,
52
+ to: [msg.to],
53
+ subject: msg.subject,
54
+ ...(msg.text !== undefined ? { text: msg.text } : {}),
55
+ ...(msg.html !== undefined ? { html: msg.html } : {}),
56
+ }),
57
+ });
58
+ const elapsedMs = Date.now() - start;
59
+ const body = await res.text().catch(() => '');
60
+ if (!res.ok) {
61
+ const error = `Resend API ${res.status}: ${truncate(body, 500)}`;
62
+ logger.error('email_send_failed', { ...base, status: res.status, body: truncate(body, 500), elapsedMs, ...preview });
63
+ return { ok: false, error };
64
+ }
65
+ let emailId;
66
+ try {
67
+ emailId = JSON.parse(body).id;
68
+ }
69
+ catch {
70
+ // Non-JSON success body: unusual, but the send did succeed.
71
+ }
72
+ logger.info('email_sent', { ...base, status: res.status, emailId, elapsedMs });
73
+ return { ok: true, emailId };
74
+ }
75
+ catch (err) {
76
+ const elapsedMs = Date.now() - start;
77
+ logger.error('email_send_error', { ...base, error: serializeError(err), elapsedMs, ...preview });
78
+ return { ok: false, error: `Network error: ${err instanceof Error ? err.message : String(err)}` };
79
+ }
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve31415/baselib",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "Plasticine new-world shared platform library: logging, auth, service-to-service auth, db, HTTP, sync, app updates",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,6 +64,10 @@
64
64
  "./llm": {
65
65
  "types": "./dist/llm/index.d.ts",
66
66
  "default": "./dist/llm/index.js"
67
+ },
68
+ "./email": {
69
+ "types": "./dist/email.d.ts",
70
+ "default": "./dist/email.js"
67
71
  }
68
72
  },
69
73
  "bin": {