@steve31415/baselib 3.5.0 → 3.7.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/dist/db.d.ts CHANGED
@@ -20,10 +20,27 @@ export interface PoolOptions {
20
20
  * default is to wait forever, which turns pool starvation into a silent
21
21
  * hang instead of an error). */
22
22
  connectionTimeoutMillis?: number;
23
+ /** Server-side cap on any single SQL statement, sent in each connection's
24
+ * startup packet (default DEFAULT_STATEMENT_TIMEOUT_MS; 0 disables). */
25
+ statementTimeoutMillis?: number;
23
26
  /** Receives idle-client errors (see logPoolErrors). Without one they go to
24
27
  * stderr — on the Cloud Logging backstop, but not in Axiom. */
25
28
  logger?: Logger;
26
29
  }
30
+ /** Why a fleet-wide statement timeout (mirror, 2026-09-08): a transaction an
31
+ * app deliberately holds open across an application await (mirror's job
32
+ * claims) can sit in a lock cycle whose closing edge lives in Node —
33
+ * invisible to Postgres's deadlock detector, so it is never broken, and
34
+ * every statement queued behind it waits forever (mirror's twitter
35
+ * ingestion was silently down ~12 h). With the cap, blocked statements
36
+ * abort (SQLSTATE 57014), their transactions roll back, and the pile-up
37
+ * self-heals. 10 minutes is deliberately far above any healthy statement in
38
+ * the fleet (milliseconds to seconds): never cancel legitimate work, just
39
+ * bound the pathological case (Steve, 2026-09-08). A migration that
40
+ * legitimately needs longer (huge index build) should start with
41
+ * `set local statement_timeout = 0` — migrate() runs each file in its own
42
+ * transaction, so the override scopes to that migration. */
43
+ export declare const DEFAULT_STATEMENT_TIMEOUT_MS: number;
27
44
  /** pg-pool re-emits an idle client's socket error as its own 'error' event —
28
45
  * in production "Connection terminated unexpectedly" when Cloud SQL closes an
29
46
  * idle connection. Unhandled, that event throws and takes the process down
package/dist/db.js CHANGED
@@ -30,6 +30,20 @@ export async function cloudSqlClientOptions(instance, auth) {
30
30
  });
31
31
  return { options, connector };
32
32
  }
33
+ /** Why a fleet-wide statement timeout (mirror, 2026-09-08): a transaction an
34
+ * app deliberately holds open across an application await (mirror's job
35
+ * claims) can sit in a lock cycle whose closing edge lives in Node —
36
+ * invisible to Postgres's deadlock detector, so it is never broken, and
37
+ * every statement queued behind it waits forever (mirror's twitter
38
+ * ingestion was silently down ~12 h). With the cap, blocked statements
39
+ * abort (SQLSTATE 57014), their transactions roll back, and the pile-up
40
+ * self-heals. 10 minutes is deliberately far above any healthy statement in
41
+ * the fleet (milliseconds to seconds): never cancel legitimate work, just
42
+ * bound the pathological case (Steve, 2026-09-08). A migration that
43
+ * legitimately needs longer (huge index build) should start with
44
+ * `set local statement_timeout = 0` — migrate() runs each file in its own
45
+ * transaction, so the override scopes to that migration. */
46
+ export const DEFAULT_STATEMENT_TIMEOUT_MS = 10 * 60_000;
33
47
  /** pg-pool re-emits an idle client's socket error as its own 'error' event —
34
48
  * in production "Connection terminated unexpectedly" when Cloud SQL closes an
35
49
  * idle connection. Unhandled, that event throws and takes the process down
@@ -51,6 +65,10 @@ export async function createPool(opts = {}) {
51
65
  return pool;
52
66
  }
53
67
  async function newPool(opts) {
68
+ // pg sends statement_timeout in the startup packet of every connection —
69
+ // server-enforced, no per-connection SET. A 0 is falsy, so pg omits it and
70
+ // the server default (no timeout) applies.
71
+ const statement_timeout = opts.statementTimeoutMillis ?? DEFAULT_STATEMENT_TIMEOUT_MS;
54
72
  const instance = process.env.CLOUD_SQL_INSTANCE;
55
73
  if (instance) {
56
74
  const { options: clientOpts } = await cloudSqlClientOptions(instance);
@@ -60,12 +78,18 @@ async function newPool(opts) {
60
78
  database: opts.database ?? requireEnv('DB_NAME'),
61
79
  max: opts.max ?? 3, // shared instance: default max_connections is low
62
80
  connectionTimeoutMillis: opts.connectionTimeoutMillis,
81
+ statement_timeout,
63
82
  });
64
83
  }
65
84
  const url = new URL(requireEnv('DATABASE_URL'));
66
85
  if (opts.database)
67
86
  url.pathname = `/${opts.database}`;
68
- return new pg.Pool({ connectionString: url.toString(), max: opts.max ?? 5, connectionTimeoutMillis: opts.connectionTimeoutMillis });
87
+ return new pg.Pool({
88
+ connectionString: url.toString(),
89
+ max: opts.max ?? 5,
90
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
91
+ statement_timeout,
92
+ });
69
93
  }
70
94
  /** End a pool and resolve only once every client's connection has actually
71
95
  * closed. pg-pool's own `end()` resolves as soon as it has *asked* its idle
package/dist/email.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { type Logger } from './log-core.js';
2
2
  export declare const RESEND_API_URL = "https://api.resend.com/emails";
3
+ /** An attachment in Resend's raw-API shape. `content` is standard Base64. */
4
+ export interface EmailAttachment {
5
+ filename: string;
6
+ content: string;
7
+ contentType?: string;
8
+ }
3
9
  /** An email to send. At least one of `text` / `html` must be present. */
4
10
  export interface EmailMessage {
5
11
  /** RFC 5322 from address, e.g. `Watchdog <watchdog@snewman.net>`. */
@@ -8,6 +14,7 @@ export interface EmailMessage {
8
14
  subject: string;
9
15
  text?: string;
10
16
  html?: string;
17
+ attachments?: EmailAttachment[];
11
18
  }
12
19
  export interface SendEmailResult {
13
20
  ok: boolean;
package/dist/email.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // Transactional email through Resend — the fleet's one email path for
2
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).
3
+ // app-specific senders on the verified snewman.net domain (`Watchdog
4
+ // <watchdog@snewman.net>`, `Lurch <lurch@snewman.net>`, …), key from Secret
5
+ // Manager `shared--resend-api-key`. Gmail is reserved for messages initiated
6
+ // directly by the user (for example, Radar's Reply action).
7
7
  //
8
8
  // Ported from the old-world package @steve31415/resend-mailer 1.0.0
9
9
  // (plasticine-apps/resend-mailer@5148649) minus its D1 outbox: the new-world
@@ -29,6 +29,7 @@ export async function sendEmail(msg, deps) {
29
29
  subject: truncate(msg.subject, 200),
30
30
  textLength: msg.text?.length ?? 0,
31
31
  htmlLength: msg.html?.length ?? 0,
32
+ attachmentCount: msg.attachments?.length ?? 0,
32
33
  ...deps.context,
33
34
  };
34
35
  const preview = { text: truncate(msg.text ?? '', 500), html: truncate(msg.html ?? '', 500) };
@@ -53,6 +54,15 @@ export async function sendEmail(msg, deps) {
53
54
  subject: msg.subject,
54
55
  ...(msg.text !== undefined ? { text: msg.text } : {}),
55
56
  ...(msg.html !== undefined ? { html: msg.html } : {}),
57
+ ...(msg.attachments?.length
58
+ ? {
59
+ attachments: msg.attachments.map((attachment) => ({
60
+ filename: attachment.filename,
61
+ content: attachment.content,
62
+ ...(attachment.contentType ? { content_type: attachment.contentType } : {}),
63
+ })),
64
+ }
65
+ : {}),
56
66
  }),
57
67
  });
58
68
  const elapsedMs = Date.now() - start;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve31415/baselib",
3
- "version": "3.5.0",
3
+ "version": "3.7.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",