pipework 0.61.0 → 0.63.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/REFERENCE.md +53 -0
  3. package/dist/REFERENCE.md +67 -3
  4. package/dist/auth/auth/index.d.ts +4 -0
  5. package/dist/auth/auth/index.d.ts.map +1 -1
  6. package/dist/auth/auth/index.js +2 -0
  7. package/dist/auth/auth/index.js.map +1 -1
  8. package/dist/auth/auth/namespace.d.ts +6 -0
  9. package/dist/auth/auth/namespace.d.ts.map +1 -1
  10. package/dist/auth/auth/namespace.js +6 -0
  11. package/dist/auth/auth/namespace.js.map +1 -1
  12. package/dist/auth/auth/passwords.d.ts +39 -0
  13. package/dist/auth/auth/passwords.d.ts.map +1 -0
  14. package/dist/auth/auth/passwords.js +76 -0
  15. package/dist/auth/auth/passwords.js.map +1 -0
  16. package/dist/auth/auth/schema.d.ts +27 -0
  17. package/dist/auth/auth/schema.d.ts.map +1 -1
  18. package/dist/auth/auth/schema.js +10 -1
  19. package/dist/auth/auth/schema.js.map +1 -1
  20. package/dist/auth/auth/sign-in.d.ts +64 -0
  21. package/dist/auth/auth/sign-in.d.ts.map +1 -0
  22. package/dist/auth/auth/sign-in.js +135 -0
  23. package/dist/auth/auth/sign-in.js.map +1 -0
  24. package/dist/core/config/namespace.d.ts +2 -0
  25. package/dist/core/config/namespace.d.ts.map +1 -1
  26. package/dist/core/config/schema.d.ts +2 -0
  27. package/dist/core/config/schema.d.ts.map +1 -1
  28. package/dist/core/config/schema.js +4 -0
  29. package/dist/core/config/schema.js.map +1 -1
  30. package/dist/core/errors/http.d.ts +6 -0
  31. package/dist/core/errors/http.d.ts.map +1 -1
  32. package/dist/core/errors/http.js +9 -0
  33. package/dist/core/errors/http.js.map +1 -1
  34. package/dist/core/errors/index.d.ts +1 -1
  35. package/dist/core/errors/index.d.ts.map +1 -1
  36. package/dist/core/errors/index.js +1 -1
  37. package/dist/core/errors/index.js.map +1 -1
  38. package/dist/core/errors/namespace.d.ts +3 -1
  39. package/dist/core/errors/namespace.d.ts.map +1 -1
  40. package/dist/core/errors/namespace.js +3 -1
  41. package/dist/core/errors/namespace.js.map +1 -1
  42. package/dist/data/migrate/generate.d.ts.map +1 -1
  43. package/dist/data/migrate/generate.js +2 -1
  44. package/dist/data/migrate/generate.js.map +1 -1
  45. package/dist/data/migrate/internal-definitions.d.ts.map +1 -1
  46. package/dist/data/migrate/internal-definitions.js +9 -0
  47. package/dist/data/migrate/internal-definitions.js.map +1 -1
  48. package/dist/index.d.ts +5 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +2 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/infra/email/audit.d.ts +3 -0
  53. package/dist/infra/email/audit.d.ts.map +1 -0
  54. package/dist/infra/email/audit.js +25 -0
  55. package/dist/infra/email/audit.js.map +1 -0
  56. package/dist/infra/email/index.d.ts +5 -0
  57. package/dist/infra/email/index.d.ts.map +1 -0
  58. package/dist/infra/email/index.js +5 -0
  59. package/dist/infra/email/index.js.map +1 -0
  60. package/dist/infra/email/mail.d.ts +69 -0
  61. package/dist/infra/email/mail.d.ts.map +1 -0
  62. package/dist/infra/email/mail.js +144 -0
  63. package/dist/infra/email/mail.js.map +1 -0
  64. package/dist/infra/email/namespace.d.ts +15 -0
  65. package/dist/infra/email/namespace.d.ts.map +1 -0
  66. package/dist/infra/email/namespace.js +15 -0
  67. package/dist/infra/email/namespace.js.map +1 -0
  68. package/dist/infra/email/providers.d.ts +42 -0
  69. package/dist/infra/email/providers.d.ts.map +1 -0
  70. package/dist/infra/email/providers.js +71 -0
  71. package/dist/infra/email/providers.js.map +1 -0
  72. package/dist/infra/email/schema.d.ts +74 -0
  73. package/dist/infra/email/schema.d.ts.map +1 -0
  74. package/dist/infra/email/schema.js +39 -0
  75. package/dist/infra/email/schema.js.map +1 -0
  76. package/dist/infra/email/sigv4.d.ts +24 -0
  77. package/dist/infra/email/sigv4.d.ts.map +1 -0
  78. package/dist/infra/email/sigv4.js +55 -0
  79. package/dist/infra/email/sigv4.js.map +1 -0
  80. package/dist/infra/email/sns.d.ts +32 -0
  81. package/dist/infra/email/sns.d.ts.map +1 -0
  82. package/dist/infra/email/sns.js +173 -0
  83. package/dist/infra/email/sns.js.map +1 -0
  84. package/dist/request/http/server.d.ts.map +1 -1
  85. package/dist/request/http/server.js +3 -0
  86. package/dist/request/http/server.js.map +1 -1
  87. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../../src/infra/email/audit.ts"],"names":[],"mappings":"AAMA,0GAA0G;AAC1G,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAevH"}
@@ -0,0 +1,25 @@
1
+ import { sql } from '../../data/db/sql.js';
2
+ import { pipeSystem } from '../../data/db/db.js';
3
+ import { getContext } from '../../request/context/store.js';
4
+ import { resolveActorId } from '../../data/write/audit/actor.js';
5
+ import { getBaseLogger } from '../../core/logging/index.js';
6
+ /** Writes an `Email` audit record under the Flow's open trace step; does nothing when no step is open. */
7
+ export async function auditEmail(verb, entityId, details) {
8
+ const flow = getContext();
9
+ const trace = flow?.trace;
10
+ const stepId = trace?.buffer.openStepId ?? null;
11
+ if (flow === undefined || trace === undefined || trace === null || stepId === null)
12
+ return;
13
+ try {
14
+ await pipeSystem().execute(sql `
15
+ INSERT INTO audit_record
16
+ (trace_day, trace_id, trace_step_id, tenant_id, actor_id, entity, verb, entity_id, after, occurred_at)
17
+ VALUES (${trace.openedDay}, ${trace.id}, ${stepId}, ${flow.tenant}, ${resolveActorId(flow)},
18
+ 'Email', ${verb}, ${entityId}, ${JSON.stringify(details)}::jsonb, now())
19
+ `);
20
+ }
21
+ catch (err) {
22
+ getBaseLogger().error({ err, traceId: trace.id }, `Failed to write the email ${verb} audit record`);
23
+ }
24
+ }
25
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/infra/email/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAA;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAA;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAE3D,0GAA0G;AAC1G,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY,EAAE,QAAuB,EAAE,OAAgC;IACtG,MAAM,IAAI,GAAG,UAAU,EAAE,CAAA;IACzB,MAAM,KAAK,GAAG,IAAI,EAAE,KAAK,CAAA;IACzB,MAAM,MAAM,GAAG,KAAK,EAAE,MAAM,CAAC,UAAU,IAAI,IAAI,CAAA;IAC/C,IAAI,IAAI,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI;QAAE,OAAM;IAC1F,IAAI,CAAC;QACH,MAAM,UAAU,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA;;;gBAGlB,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,EAAE,KAAK,MAAM,KAAK,IAAI,CAAC,MAAM,KAAK,cAAc,CAAC,IAAI,CAAC;mBAC7E,IAAI,KAAK,QAAQ,KAAK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;KAC3D,CAAC,CAAA;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,aAAa,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,EAAE,6BAA6B,IAAI,eAAe,CAAC,CAAA;IACrG,CAAC;AACH,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { createMail, normalizeAddress, SEND_JOB, type Mail, type MailConfig, type SendInput, type SendResult, type RenderedEmail, type EmailTemplates } from './mail.js';
2
+ export { ses, devStandIn, type MailProvider, type OutgoingMessage, type SesConfig } from './providers.js';
3
+ export { snsWebhook, type SnsWebhookConfig } from './sns.js';
4
+ export { signV4, type SigV4Request, type AwsCredentials } from './sigv4.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/infra/email/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EAAE,KAAK,IAAI,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAC1G,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AAC5D,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AACzG,OAAO,EAAE,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC5D,OAAO,EAAE,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,5 @@
1
+ export { createMail, normalizeAddress, SEND_JOB } from './mail.js';
2
+ export { ses, devStandIn } from './providers.js';
3
+ export { snsWebhook } from './sns.js';
4
+ export { signV4 } from './sigv4.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/infra/email/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EACJ,MAAM,WAAW,CAAA;AAC5D,OAAO,EAAE,GAAG,EAAE,UAAU,EAA2D,MAAM,gBAAgB,CAAA;AACzG,OAAO,EAAE,UAAU,EAAyB,MAAM,UAAU,CAAA;AAC5D,OAAO,EAAE,MAAM,EAA0C,MAAM,YAAY,CAAA"}
@@ -0,0 +1,69 @@
1
+ import type { DB } from '../../data/db/index.js';
2
+ import type { Queue } from '../../async/jobs/queue.js';
3
+ import type { Handler } from '../../request/di/types.js';
4
+ import type { MailProvider } from './providers.js';
5
+ /** What a template renders: the subject and the text body, optionally an HTML body. */
6
+ export interface RenderedEmail {
7
+ readonly subject: string;
8
+ readonly text: string;
9
+ readonly html?: string;
10
+ }
11
+ /** The app's templates: a function per name from the data to the rendered message. */
12
+ export type EmailTemplates = Record<string, (data: any) => RenderedEmail>;
13
+ export interface MailConfig<T extends EmailTemplates> {
14
+ readonly provider: MailProvider;
15
+ readonly templates: T;
16
+ /** The From address, e.g. `Verity <no-reply@mail.verity.pub>`. */
17
+ readonly from: string;
18
+ /** The app's job queue; its worker surface must run `mail.jobs`. */
19
+ readonly queue: Queue;
20
+ readonly configurationSet?: string;
21
+ /** The app's environment. `devStandIn` is refused when this is `production`. */
22
+ readonly environment?: string;
23
+ /** Seconds after which a message left `sending` by a crashed run may be sent again. Default 600. */
24
+ readonly sendingStaleSeconds?: number;
25
+ }
26
+ export interface SendInput<T extends EmailTemplates, K extends keyof T & string> {
27
+ readonly to: string;
28
+ readonly template: K;
29
+ readonly data: Parameters<T[K]>[0];
30
+ /** Idempotency key: a second send with the same key sends nothing. */
31
+ readonly key: string;
32
+ }
33
+ export interface SendResult {
34
+ readonly messageId: string;
35
+ /** False when the key was already used; `messageId` is then the earlier message. */
36
+ readonly queued: boolean;
37
+ }
38
+ export interface Mail<T extends EmailTemplates> {
39
+ /**
40
+ * Records the message and enqueues its send in `db`, so the caller's rollback drops both.
41
+ * The same `key` twice gives one message, one job and one delivery.
42
+ */
43
+ send<K extends keyof T & string>(db: DB, input: SendInput<T, K>): Promise<SendResult>;
44
+ /** Job handlers for the send job; spread into the worker surface's `jobs`. */
45
+ readonly jobs: readonly Handler<Record<string, unknown>, unknown>[];
46
+ }
47
+ export declare const SEND_JOB = "pipework.email.send";
48
+ export declare function normalizeAddress(address: string): string;
49
+ /**
50
+ * Creates the transactional email interface.
51
+ *
52
+ * `send(db, { to, template, data, key })` writes a `pipework_email_messages` row and
53
+ * enqueues a `pipework.email.send` job in the caller's transaction. The job renders the
54
+ * app's template and sends through the provider, at most once per key, skipping an
55
+ * address on the suppression list (status `suppressed`, no provider call). The
56
+ * provider call is a trace step and `Email:sent` / `Email:suppressed` / `Email:failed`
57
+ * audit records are written under it, in the trace of the flow that called `send`.
58
+ *
59
+ * Needs blueprint `email: {}` (tables `pipework_email_messages`, `pipework_email_suppressions`).
60
+ * A provider error returns the message to `queued` with `error` set and rethrows, so the
61
+ * queue retries (set `maxAttempts` on the queue); if the queue gives up the row keeps the error.
62
+ * A run claims the message with a conditional update (`status` `queued`, or `sending` for longer than
63
+ * `sendingStaleSeconds`), so a re-run of a job whose first run is still in flight does not call the provider.
64
+ * Residual window: a crash mid-send leaves the message `sending` for the stale window, after which a retry
65
+ * may send it once more; likewise if the provider accepted it and the process died before the row was marked `sent`. SES v2 has no idempotency token to close it.
66
+ * Message bodies and template data are never logged.
67
+ */
68
+ export declare function createMail<T extends EmailTemplates>(config: MailConfig<T>): Mail<T>;
69
+ //# sourceMappingURL=mail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mail.d.ts","sourceRoot":"","sources":["../../../src/infra/email/mail.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,wBAAwB,CAAA;AAOhD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAA;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAGxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD,uFAAuF;AACvF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,sFAAsF;AAEtF,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,aAAa,CAAC,CAAA;AAEzE,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,cAAc;IAClD,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAA;IACrB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAClC,gFAAgF;IAChF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,oGAAoG;IACpG,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CACtC;AAED,MAAM,WAAW,SAAS,CAAC,CAAC,SAAS,cAAc,EAAE,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM;IAC7E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAClC,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,IAAI,CAAC,CAAC,SAAS,cAAc;IAC5C;;;OAGG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IACrF,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,CAAA;CACpE;AAED,eAAO,MAAM,QAAQ,wBAAwB,CAAA;AAG7C,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,cAAc,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAqHnF"}
@@ -0,0 +1,144 @@
1
+ import { sql } from '../../data/db/sql.js';
2
+ import { pipeSystem } from '../../data/db/db.js';
3
+ import { schema } from '../../data/schema/namespace.js';
4
+ import { fitting } from '../../request/di/index.js';
5
+ import { getContext } from '../../request/context/store.js';
6
+ import { ValidationError } from '../../core/errors/index.js';
7
+ import { getBaseLogger } from '../../core/logging/index.js';
8
+ import { __flowStep } from '../../trace/runtime/flow-step.js';
9
+ import { auditEmail } from './audit.js';
10
+ export const SEND_JOB = 'pipework.email.send';
11
+ const ADDRESS_RE = /^[^\s@<>,;"]+@[^\s@<>,;"]+$/;
12
+ export function normalizeAddress(address) {
13
+ return address.trim().toLowerCase();
14
+ }
15
+ /**
16
+ * Creates the transactional email interface.
17
+ *
18
+ * `send(db, { to, template, data, key })` writes a `pipework_email_messages` row and
19
+ * enqueues a `pipework.email.send` job in the caller's transaction. The job renders the
20
+ * app's template and sends through the provider, at most once per key, skipping an
21
+ * address on the suppression list (status `suppressed`, no provider call). The
22
+ * provider call is a trace step and `Email:sent` / `Email:suppressed` / `Email:failed`
23
+ * audit records are written under it, in the trace of the flow that called `send`.
24
+ *
25
+ * Needs blueprint `email: {}` (tables `pipework_email_messages`, `pipework_email_suppressions`).
26
+ * A provider error returns the message to `queued` with `error` set and rethrows, so the
27
+ * queue retries (set `maxAttempts` on the queue); if the queue gives up the row keeps the error.
28
+ * A run claims the message with a conditional update (`status` `queued`, or `sending` for longer than
29
+ * `sendingStaleSeconds`), so a re-run of a job whose first run is still in flight does not call the provider.
30
+ * Residual window: a crash mid-send leaves the message `sending` for the stale window, after which a retry
31
+ * may send it once more; likewise if the provider accepted it and the process died before the row was marked `sent`. SES v2 has no idempotency token to close it.
32
+ * Message bodies and template data are never logged.
33
+ */
34
+ export function createMail(config) {
35
+ if (config.provider.name === 'dev-stand-in' && config.environment === 'production') {
36
+ throw new Error('[pipework] mail.devStandIn is refused in production.\n\n' +
37
+ ' The stand-in writes messages to files instead of sending them.\n' +
38
+ ' Configure a real provider, e.g. mail.ses({ region, credentials }).\n');
39
+ }
40
+ const staleSeconds = config.sendingStaleSeconds ?? 600;
41
+ const deliver = __flowStep({ ref: 'pipework/email:deliver' }, async (messageId) => {
42
+ const db = pipeSystem();
43
+ const rows = await db.execute(sql `SELECT * FROM pipework_email_messages WHERE id = ${messageId}::uuid`);
44
+ const row = rows[0];
45
+ if (row === undefined)
46
+ throw new Error(`[pipework] Email message ${messageId} does not exist.`);
47
+ const status = row['status'];
48
+ if (status === 'sent' || status === 'suppressed' || status === 'failed')
49
+ return { status };
50
+ const environment = getContext()?.instance.config.environment;
51
+ if (config.provider.name === 'dev-stand-in' && environment === 'production') {
52
+ throw new Error('[pipework] mail.devStandIn is refused in production.');
53
+ }
54
+ const to = row['to_address'];
55
+ const suppressed = await db.execute(sql `SELECT reason FROM pipework_email_suppressions WHERE address = ${normalizeAddress(to)}`);
56
+ if (suppressed.length > 0) {
57
+ await db.execute(sql `UPDATE pipework_email_messages SET status = 'suppressed' WHERE id = ${messageId}::uuid`);
58
+ await auditEmail('suppressed', messageId, { reason: suppressed[0]['reason'] });
59
+ getBaseLogger().info({ event: 'email suppressed send', messageId }, 'email skipped: address is suppressed');
60
+ return { status: 'suppressed' };
61
+ }
62
+ const templateName = row['template'];
63
+ const template = config.templates[templateName];
64
+ let rendered = null;
65
+ let renderError = '';
66
+ if (template === undefined) {
67
+ renderError = `no template named "${templateName}"`;
68
+ }
69
+ else {
70
+ try {
71
+ rendered = template(row['data']);
72
+ }
73
+ catch (err) {
74
+ renderError = `template "${templateName}" threw: ${err instanceof Error ? err.message : String(err)}`;
75
+ }
76
+ }
77
+ if (rendered === null) {
78
+ await db.execute(sql `UPDATE pipework_email_messages SET status = 'failed', error = ${renderError} WHERE id = ${messageId}::uuid`);
79
+ await auditEmail('failed', messageId, { error: renderError });
80
+ getBaseLogger().error({ event: 'email failed', messageId }, `email not sent: ${renderError}`);
81
+ return { status: 'failed' };
82
+ }
83
+ const claimed = await db.execute(sql `
84
+ UPDATE pipework_email_messages SET status = 'sending', sending_at = now()
85
+ WHERE id = ${messageId}::uuid
86
+ AND (status = 'queued' OR (status = 'sending' AND sending_at < now() - make_interval(secs => ${staleSeconds})))
87
+ RETURNING id`);
88
+ if (claimed.length === 0) {
89
+ getBaseLogger().info({ event: 'email send held elsewhere', messageId }, 'email not sent: another run holds the message or it is finished');
90
+ return { status: 'held' };
91
+ }
92
+ try {
93
+ const sent = await config.provider.send({
94
+ messageId, from: config.from, to,
95
+ subject: rendered.subject, text: rendered.text,
96
+ ...(rendered.html !== undefined ? { html: rendered.html } : {}),
97
+ ...(config.configurationSet !== undefined ? { configurationSet: config.configurationSet } : {}),
98
+ });
99
+ await db.execute(sql `
100
+ UPDATE pipework_email_messages
101
+ SET status = 'sent', sending_at = NULL, provider_message_id = ${sent.providerMessageId}, sent_at = now(), error = NULL
102
+ WHERE id = ${messageId}::uuid`);
103
+ await auditEmail('sent', messageId, { provider: config.provider.name, providerMessageId: sent.providerMessageId });
104
+ return { status: 'sent' };
105
+ }
106
+ catch (err) {
107
+ const message = err instanceof Error ? err.message : String(err);
108
+ await db.execute(sql `UPDATE pipework_email_messages SET status = 'queued', sending_at = NULL, error = ${message} WHERE id = ${messageId}::uuid`);
109
+ await auditEmail('failed', messageId, { error: message, willRetry: true });
110
+ throw err;
111
+ }
112
+ });
113
+ const sendJob = fitting.use().job(SEND_JOB)
114
+ .input(schema.check.object({ messageId: schema.check.string() }))
115
+ .fit(async ({ input }) => deliver(input.messageId));
116
+ return {
117
+ async send(db, input) {
118
+ if (config.templates[input.template] === undefined) {
119
+ throw new ValidationError({ template: [`no template named "${input.template}" (have: ${Object.keys(config.templates).join(', ')})`] }, `mail.send: unknown template "${input.template}".`);
120
+ }
121
+ if (!ADDRESS_RE.test(input.to.trim())) {
122
+ throw new ValidationError({ to: ['is not an email address'] }, 'mail.send: "to" is not an email address.');
123
+ }
124
+ if (input.key.trim() === '') {
125
+ throw new ValidationError({ key: ['must not be empty'] }, 'mail.send: "key" must not be empty.');
126
+ }
127
+ const traceId = getContext()?.trace?.id ?? null;
128
+ const inserted = await db.execute(sql `
129
+ INSERT INTO pipework_email_messages (key, to_address, template, data, trace_id)
130
+ VALUES (${input.key}, ${input.to.trim()}, ${input.template}, ${JSON.stringify(input.data ?? null)}::jsonb, ${traceId}::uuid)
131
+ ON CONFLICT (key) DO NOTHING
132
+ RETURNING id`);
133
+ if (inserted.length === 0) {
134
+ const existing = await db.execute(sql `SELECT id FROM pipework_email_messages WHERE key = ${input.key}`);
135
+ return { messageId: existing[0]['id'], queued: false };
136
+ }
137
+ const messageId = inserted[0]['id'];
138
+ await config.queue.enqueue(db, { type: SEND_JOB, payload: { messageId }, continueFlow: true });
139
+ return { messageId, queued: true };
140
+ },
141
+ jobs: [sendJob],
142
+ };
143
+ }
144
+ //# sourceMappingURL=mail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mail.js","sourceRoot":"","sources":["../../../src/infra/email/mail.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { createMail } from './mail.js';
2
+ import { ses, devStandIn } from './providers.js';
3
+ import { snsWebhook } from './sns.js';
4
+ /** Mail namespace — transactional email through a job queue, SES v2 or a dev stand-in, bounce and complaint suppression. */
5
+ export declare const mail: {
6
+ /** Creates the mail interface: `send(db, { to, template, data, key })` in the caller's transaction, and the `jobs` that deliver. */
7
+ readonly create: typeof createMail;
8
+ /** Amazon SES v2 provider, SigV4-signed without the AWS SDK. */
9
+ readonly ses: typeof ses;
10
+ /** Development provider that writes each message to a file instead of sending; refused in production. */
11
+ readonly devStandIn: typeof devStandIn;
12
+ /** Inbound handler for SES bounce and complaint events delivered through SNS; suppresses the address. */
13
+ readonly snsWebhook: typeof snsWebhook;
14
+ };
15
+ //# sourceMappingURL=namespace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"namespace.d.ts","sourceRoot":"","sources":["../../../src/infra/email/namespace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACtC,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAErC,4HAA4H;AAC5H,eAAO,MAAM,IAAI;IACf,oIAAoI;;IAEpI,gEAAgE;;IAEhE,yGAAyG;;IAEzG,yGAAyG;;CAEjG,CAAA"}
@@ -0,0 +1,15 @@
1
+ import { createMail } from './mail.js';
2
+ import { ses, devStandIn } from './providers.js';
3
+ import { snsWebhook } from './sns.js';
4
+ /** Mail namespace — transactional email through a job queue, SES v2 or a dev stand-in, bounce and complaint suppression. */
5
+ export const mail = {
6
+ /** Creates the mail interface: `send(db, { to, template, data, key })` in the caller's transaction, and the `jobs` that deliver. */
7
+ create: createMail,
8
+ /** Amazon SES v2 provider, SigV4-signed without the AWS SDK. */
9
+ ses,
10
+ /** Development provider that writes each message to a file instead of sending; refused in production. */
11
+ devStandIn,
12
+ /** Inbound handler for SES bounce and complaint events delivered through SNS; suppresses the address. */
13
+ snsWebhook,
14
+ };
15
+ //# sourceMappingURL=namespace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"namespace.js","sourceRoot":"","sources":["../../../src/infra/email/namespace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACtC,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAErC,4HAA4H;AAC5H,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,oIAAoI;IACpI,MAAM,EAAE,UAAU;IAClB,gEAAgE;IAChE,GAAG;IACH,yGAAyG;IACzG,UAAU;IACV,yGAAyG;IACzG,UAAU;CACF,CAAA"}
@@ -0,0 +1,42 @@
1
+ import { type AwsCredentials } from './sigv4.js';
2
+ /** One rendered message handed to a provider. */
3
+ export interface OutgoingMessage {
4
+ /** The message row id; unique per message. */
5
+ readonly messageId: string;
6
+ readonly from: string;
7
+ readonly to: string;
8
+ readonly subject: string;
9
+ readonly text: string;
10
+ readonly html?: string;
11
+ readonly configurationSet?: string;
12
+ }
13
+ /** Sends one rendered message. A throw means it was not accepted and may be retried. */
14
+ export interface MailProvider {
15
+ readonly name: string;
16
+ send(message: OutgoingMessage): Promise<{
17
+ providerMessageId: string;
18
+ }>;
19
+ }
20
+ export interface SesConfig {
21
+ readonly region: string;
22
+ /** Called for every send, so short-lived credentials can be refreshed by the app. */
23
+ readonly credentials: () => Promise<AwsCredentials>;
24
+ readonly configurationSet?: string;
25
+ /** Base URL instead of `https://email.<region>.amazonaws.com`; for tests against a local server. */
26
+ readonly endpoint?: string;
27
+ }
28
+ /**
29
+ * Amazon SES v2 provider. Posts to `/v2/email/outbound-emails`, signed with SigV4
30
+ * (no AWS SDK). Credentials come from the app, never from the environment.
31
+ * A non-2xx answer throws with the status and AWS's message.
32
+ */
33
+ export declare function ses(config: SesConfig): MailProvider;
34
+ /**
35
+ * Development stand-in: writes each rendered message to `<dir>/<messageId>.json`
36
+ * (from, to, subject, text, html) and logs one line, sending nothing.
37
+ * `mail.create` refuses it when the environment is production.
38
+ */
39
+ export declare function devStandIn(config: {
40
+ readonly dir: string;
41
+ }): MailProvider;
42
+ //# sourceMappingURL=providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../../src/infra/email/providers.ts"],"names":[],"mappings":"AAGA,OAAO,EAAU,KAAK,cAAc,EAAE,MAAM,YAAY,CAAA;AAExD,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CACnC;AAED,wFAAwF;AACxF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,IAAI,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,iBAAiB,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACvE;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,qFAAqF;IACrF,QAAQ,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,CAAA;IACnD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAClC,oGAAoG;IACpG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,MAAM,EAAE,SAAS,GAAG,YAAY,CAsCnD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,YAAY,CAYzE"}
@@ -0,0 +1,71 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { getBaseLogger } from '../../core/logging/index.js';
4
+ import { signV4 } from './sigv4.js';
5
+ /**
6
+ * Amazon SES v2 provider. Posts to `/v2/email/outbound-emails`, signed with SigV4
7
+ * (no AWS SDK). Credentials come from the app, never from the environment.
8
+ * A non-2xx answer throws with the status and AWS's message.
9
+ */
10
+ export function ses(config) {
11
+ const base = (config.endpoint ?? `https://email.${config.region}.amazonaws.com`).replace(/\/+$/, '');
12
+ return {
13
+ name: 'ses',
14
+ async send(message) {
15
+ const set = message.configurationSet ?? config.configurationSet;
16
+ const body = JSON.stringify({
17
+ FromEmailAddress: message.from,
18
+ Destination: { ToAddresses: [message.to] },
19
+ Content: {
20
+ Simple: {
21
+ Subject: { Data: message.subject, Charset: 'UTF-8' },
22
+ Body: {
23
+ Text: { Data: message.text, Charset: 'UTF-8' },
24
+ ...(message.html !== undefined ? { Html: { Data: message.html, Charset: 'UTF-8' } } : {}),
25
+ },
26
+ },
27
+ },
28
+ ...(set !== undefined ? { ConfigurationSetName: set } : {}),
29
+ });
30
+ const url = `${base}/v2/email/outbound-emails`;
31
+ const headers = signV4({
32
+ method: 'POST', url, headers: { 'content-type': 'application/json' }, body,
33
+ region: config.region, service: 'ses', credentials: await config.credentials(),
34
+ });
35
+ const res = await fetch(url, { method: 'POST', headers, body });
36
+ const raw = await res.text();
37
+ let parsed = {};
38
+ try {
39
+ parsed = JSON.parse(raw);
40
+ }
41
+ catch { /* not JSON */ }
42
+ if (!res.ok) {
43
+ const detail = String(parsed['message'] ?? parsed['Message'] ?? raw.slice(0, 200));
44
+ throw new Error(`[pipework] SES refused the message: HTTP ${res.status} ${res.headers.get('x-amzn-errortype') ?? ''} ${detail}`.replace(/ +/g, ' '));
45
+ }
46
+ const id = parsed['MessageId'];
47
+ if (typeof id !== 'string')
48
+ throw new Error('[pipework] SES answered without a MessageId.');
49
+ return { providerMessageId: id };
50
+ },
51
+ };
52
+ }
53
+ /**
54
+ * Development stand-in: writes each rendered message to `<dir>/<messageId>.json`
55
+ * (from, to, subject, text, html) and logs one line, sending nothing.
56
+ * `mail.create` refuses it when the environment is production.
57
+ */
58
+ export function devStandIn(config) {
59
+ return {
60
+ name: 'dev-stand-in',
61
+ async send(message) {
62
+ await mkdir(config.dir, { recursive: true });
63
+ const file = join(config.dir, `${message.messageId}.json`);
64
+ const { from, to, subject, text, html } = message;
65
+ await writeFile(file, JSON.stringify({ from, to, subject, text, ...(html !== undefined ? { html } : {}) }, null, 2));
66
+ getBaseLogger().info({ event: 'email dev stand-in', to, file }, 'email written to file instead of sent');
67
+ return { providerMessageId: `dev-${message.messageId}` };
68
+ },
69
+ };
70
+ }
71
+ //# sourceMappingURL=providers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.js","sourceRoot":"","sources":["../../../src/infra/email/providers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAC3D,OAAO,EAAE,MAAM,EAAuB,MAAM,YAAY,CAAA;AA6BxD;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,MAAiB;IACnC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,IAAI,iBAAiB,MAAM,CAAC,MAAM,gBAAgB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACpG,OAAO;QACL,IAAI,EAAE,KAAK;QACX,KAAK,CAAC,IAAI,CAAC,OAAO;YAChB,MAAM,GAAG,GAAG,OAAO,CAAC,gBAAgB,IAAI,MAAM,CAAC,gBAAgB,CAAA;YAC/D,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;gBAC1B,gBAAgB,EAAE,OAAO,CAAC,IAAI;gBAC9B,WAAW,EAAE,EAAE,WAAW,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE;gBAC1C,OAAO,EAAE;oBACP,MAAM,EAAE;wBACN,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE;wBACpD,IAAI,EAAE;4BACJ,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE;4BAC9C,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;yBAC1F;qBACF;iBACF;gBACD,GAAG,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,oBAAoB,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5D,CAAC,CAAA;YACF,MAAM,GAAG,GAAG,GAAG,IAAI,2BAA2B,CAAA;YAC9C,MAAM,OAAO,GAAG,MAAM,CAAC;gBACrB,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,EAAE,IAAI;gBAC1E,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC,WAAW,EAAE;aAC/E,CAAC,CAAA;YACF,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;YAC/D,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;YAC5B,IAAI,MAAM,GAA4B,EAAE,CAAA;YACxC,IAAI,CAAC;gBAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAA;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,cAAc,CAAC,CAAC;YACpF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;gBAClF,MAAM,IAAI,KAAK,CAAC,4CAA4C,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,EAAE,IAAI,MAAM,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAA;YACtJ,CAAC;YACD,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,CAAA;YAC9B,IAAI,OAAO,EAAE,KAAK,QAAQ;gBAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAA;YAC3F,OAAO,EAAE,iBAAiB,EAAE,EAAE,EAAE,CAAA;QAClC,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,MAAgC;IACzD,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,KAAK,CAAC,IAAI,CAAC,OAAO;YAChB,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,OAAO,CAAC,CAAA;YAC1D,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,OAAO,CAAA;YACjD,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;YACpH,aAAa,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,uCAAuC,CAAC,CAAA;YACxG,OAAO,EAAE,iBAAiB,EAAE,OAAO,OAAO,CAAC,SAAS,EAAE,EAAE,CAAA;QAC1D,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,74 @@
1
+ export declare const EMAIL_TABLE_NAMES: ReadonlySet<string>;
2
+ export declare const EMAIL_STATUSES: readonly ["queued", "sending", "sent", "suppressed", "failed"];
3
+ export declare const SUPPRESSION_REASONS: readonly ["bounce", "complaint"];
4
+ export declare const emailMessages: import("../../data/domain/types.js").DefinedTable<{
5
+ id: import("../../data/domain/types.js").FieldDescriptor<string, {
6
+ defaultRandom: true;
7
+ hasDefault: true;
8
+ } & {
9
+ primaryKey: true;
10
+ }> & import("../../data/domain/field.js").FieldModifiers<string, {
11
+ defaultRandom: true;
12
+ hasDefault: true;
13
+ } & {
14
+ primaryKey: true;
15
+ }>;
16
+ key: import("../../data/domain/types.js").FieldDescriptor<string, {}> & import("../../data/domain/field.js").FieldModifiers<string, {}>;
17
+ toAddress: import("../../data/domain/types.js").FieldDescriptor<string, {}> & import("../../data/domain/field.js").FieldModifiers<string, {}>;
18
+ template: import("../../data/domain/types.js").FieldDescriptor<string, {}> & import("../../data/domain/field.js").FieldModifiers<string, {}>;
19
+ data: import("../../data/domain/types.js").FieldDescriptor<import("zod/v4").JSONType, {}> & import("../../data/domain/field.js").FieldModifiers<import("zod/v4").JSONType, {}>;
20
+ status: import("../../data/domain/types.js").FieldDescriptor<"failed" | "sent" | "queued" | "sending" | "suppressed", {
21
+ hasDefault: true;
22
+ }> & import("../../data/domain/field.js").FieldModifiers<"failed" | "sent" | "queued" | "sending" | "suppressed", {
23
+ hasDefault: true;
24
+ }>;
25
+ providerMessageId: import("../../data/domain/types.js").FieldDescriptor<string | null, {
26
+ nullable: true;
27
+ }> & import("../../data/domain/field.js").FieldModifiers<string | null, {
28
+ nullable: true;
29
+ }>;
30
+ error: import("../../data/domain/types.js").FieldDescriptor<string | null, {
31
+ nullable: true;
32
+ }> & import("../../data/domain/field.js").FieldModifiers<string | null, {
33
+ nullable: true;
34
+ }>;
35
+ traceId: import("../../data/domain/types.js").FieldDescriptor<string | null, {
36
+ nullable: true;
37
+ }> & import("../../data/domain/field.js").FieldModifiers<string | null, {
38
+ nullable: true;
39
+ }>;
40
+ createdAt: import("../../data/domain/types.js").FieldDescriptor<Date, {
41
+ hasDefault: true;
42
+ }> & import("../../data/domain/field.js").FieldModifiers<Date, {
43
+ hasDefault: true;
44
+ }>;
45
+ sentAt: import("../../data/domain/types.js").FieldDescriptor<Date | null, {
46
+ nullable: true;
47
+ }> & import("../../data/domain/field.js").FieldModifiers<Date | null, {
48
+ nullable: true;
49
+ }>;
50
+ sendingAt: import("../../data/domain/types.js").FieldDescriptor<Date | null, {
51
+ nullable: true;
52
+ }> & import("../../data/domain/field.js").FieldModifiers<Date | null, {
53
+ nullable: true;
54
+ }>;
55
+ }, "pipework_email_messages">;
56
+ export declare const emailSuppressions: import("../../data/domain/types.js").DefinedTable<{
57
+ address: import("../../data/domain/types.js").FieldDescriptor<string, {
58
+ primaryKey: true;
59
+ }> & import("../../data/domain/field.js").FieldModifiers<string, {
60
+ primaryKey: true;
61
+ }>;
62
+ reason: import("../../data/domain/types.js").FieldDescriptor<"bounce" | "complaint", {}> & import("../../data/domain/field.js").FieldModifiers<"bounce" | "complaint", {}>;
63
+ detail: import("../../data/domain/types.js").FieldDescriptor<import("zod/v4").JSONType, {
64
+ nullable: true;
65
+ }> & import("../../data/domain/field.js").FieldModifiers<import("zod/v4").JSONType, {
66
+ nullable: true;
67
+ }>;
68
+ createdAt: import("../../data/domain/types.js").FieldDescriptor<Date, {
69
+ hasDefault: true;
70
+ }> & import("../../data/domain/field.js").FieldModifiers<Date, {
71
+ hasDefault: true;
72
+ }>;
73
+ }, "pipework_email_suppressions">;
74
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/infra/email/schema.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAEhD,CAAA;AAEF,eAAO,MAAM,cAAc,gEAAiE,CAAA;AAC5F,eAAO,MAAM,mBAAmB,kCAAmC,CAAA;AAEnE,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAkBxB,CAAA;AAEF,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;iCAK5B,CAAA"}
@@ -0,0 +1,39 @@
1
+ // The two tables behind `mail`. Enabled with `email: {}` in the blueprint;
2
+ // `pipework generate` then emits them into the default database's migrations,
3
+ // IF NOT EXISTS. Not tenant-scoped: a bounce notification arrives with no
4
+ // tenant, and a suppressed address is suppressed for every tenant.
5
+ import { define } from '../../data/domain/index.js';
6
+ import { field } from '../../data/domain/field.js';
7
+ import { schema } from '../../data/schema/namespace.js';
8
+ import { index, unique } from '../../data/db/idx/index.js';
9
+ export const EMAIL_TABLE_NAMES = new Set([
10
+ 'pipework_email_messages', 'pipework_email_suppressions',
11
+ ]);
12
+ export const EMAIL_STATUSES = ['queued', 'sending', 'sent', 'suppressed', 'failed'];
13
+ export const SUPPRESSION_REASONS = ['bounce', 'complaint'];
14
+ export const emailMessages = define('pipework_email_messages', {
15
+ id: field.uuid().defaultRandom().primaryKey(),
16
+ key: field.text(),
17
+ toAddress: field.text(),
18
+ template: field.text(),
19
+ data: field.jsonb(schema.check.json()),
20
+ status: field.enum(EMAIL_STATUSES).default('queued'),
21
+ providerMessageId: field.text().nullable(),
22
+ error: field.text().nullable(),
23
+ traceId: field.uuid().nullable(),
24
+ createdAt: field.timestamp().default('now'),
25
+ sentAt: field.timestamp().nullable(),
26
+ sendingAt: field.timestamp().nullable(),
27
+ }, {
28
+ indexes: (t) => ({
29
+ byKey: unique('pipework_email_messages_key_key').on(t['key']),
30
+ byProviderId: index('idx_pipework_email_messages_provider_id').on(t['providerMessageId']),
31
+ }),
32
+ });
33
+ export const emailSuppressions = define('pipework_email_suppressions', {
34
+ address: field.text().primaryKey(),
35
+ reason: field.enum(SUPPRESSION_REASONS),
36
+ detail: field.jsonb(schema.check.json()).nullable(),
37
+ createdAt: field.timestamp().default('now'),
38
+ });
39
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/infra/email/schema.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,8EAA8E;AAC9E,0EAA0E;AAC1E,mEAAmE;AAEnE,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,gCAAgC,CAAA;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAA;AAE1D,MAAM,CAAC,MAAM,iBAAiB,GAAwB,IAAI,GAAG,CAAC;IAC5D,yBAAyB,EAAE,6BAA6B;CACzD,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,CAAU,CAAA;AAC5F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,QAAQ,EAAE,WAAW,CAAU,CAAA;AAEnE,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,yBAAyB,EAAE;IAC7D,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,aAAa,EAAE,CAAC,UAAU,EAAE;IAC7C,GAAG,EAAE,KAAK,CAAC,IAAI,EAAE;IACjB,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE;IACvB,QAAQ,EAAE,KAAK,CAAC,IAAI,EAAE;IACtB,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACtC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;IACpD,iBAAiB,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAC1C,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAC9B,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IAChC,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAC3C,MAAM,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,QAAQ,EAAE;IACpC,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,QAAQ,EAAE;CACxC,EAAE;IACD,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACf,KAAK,EAAE,MAAM,CAAC,iCAAiC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC7D,YAAY,EAAE,KAAK,CAAC,yCAAyC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC;KAC1F,CAAC;CACH,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC,6BAA6B,EAAE;IACrE,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,UAAU,EAAE;IAClC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC;IACvC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE;IACnD,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CAC5C,CAAC,CAAA"}
@@ -0,0 +1,24 @@
1
+ export interface AwsCredentials {
2
+ readonly accessKeyId: string;
3
+ readonly secretAccessKey: string;
4
+ readonly sessionToken?: string;
5
+ }
6
+ export interface SigV4Request {
7
+ readonly method: string;
8
+ readonly url: string;
9
+ /** Headers to sign and send, other than host and x-amz-date, which are added. */
10
+ readonly headers: Readonly<Record<string, string>>;
11
+ readonly body: string | Buffer;
12
+ readonly region: string;
13
+ readonly service: string;
14
+ readonly credentials: AwsCredentials;
15
+ readonly now?: Date;
16
+ }
17
+ /**
18
+ * Signs a request with AWS Signature Version 4 and returns the headers to send:
19
+ * the given ones, `x-amz-date`, `x-amz-security-token` when the credentials carry
20
+ * a session token, and `authorization`. `host` is signed but not returned; the
21
+ * HTTP client sets it from the URL.
22
+ */
23
+ export declare function signV4(req: SigV4Request): Record<string, string>;
24
+ //# sourceMappingURL=sigv4.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sigv4.d.ts","sourceRoot":"","sources":["../../../src/infra/email/sigv4.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,cAAc,CAAA;IACpC,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAA;CACpB;AAkBD;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwChE"}