@pattern-js/mod-email 0.4.0 → 0.5.0-main.79cf721
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 +5 -2
- package/dist/alerts.d.ts +21 -0
- package/dist/alerts.d.ts.map +1 -0
- package/dist/alerts.js +67 -0
- package/dist/alerts.js.map +1 -0
- package/dist/app.d.ts +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +1 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +4 -2
- package/dist/mod.js.map +1 -1
- package/dist/ops/account.d.ts.map +1 -1
- package/dist/ops/account.js +3 -2
- package/dist/ops/account.js.map +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +19 -6
- package/dist/service.js.map +1 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +16 -2
- package/dist/settings.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/docs/guides/delivery.md +2 -2
- package/docs/index.md +20 -1
- package/docs/ops/email.account.md +1 -1
- package/docs/ops/email.account.test.md +12 -0
- package/docs/ops/email.accounts.delete.md +7 -0
- package/docs/ops/email.accounts.read.md +6 -0
- package/docs/ops/email.accounts.write.md +13 -0
- package/docs/ops/email.providers.list.md +9 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ npm install @pattern-js/mod-email @pattern-js/mod-email-resend
|
|
|
19
19
|
|
|
20
20
|
Reach for it when your app sends email — sign-in links, notifications, agent
|
|
21
21
|
tools — and you want the sender to be swappable config, not code: accounts
|
|
22
|
-
live in admin → **
|
|
22
|
+
live in admin → **Resources → Email**, credentials in the vault or env vars, and
|
|
23
23
|
re-pointing an account re-targets every workflow using it. Skip it if your app
|
|
24
24
|
never sends email; if you only need dev sign-in, the magic-link console
|
|
25
25
|
fallback already covers you with zero mods.
|
|
@@ -31,11 +31,14 @@ fallback already covers you with zero mods.
|
|
|
31
31
|
`html`/`text`. Attachments from media bytes, blob refs, or literal files.
|
|
32
32
|
- **`email.account`** — resolve a named account to a ref (like `ai.alias` for
|
|
33
33
|
models); `required: false` turns it into a probe to branch on.
|
|
34
|
-
- **Admin →
|
|
34
|
+
- **Admin → Resources → Email** — accounts CRUD with per-driver fields, secrets
|
|
35
35
|
from the vault or env, and a Test button that sends a REAL email.
|
|
36
36
|
- **`email.deliver-token`** — a visible, forkable workflow subscribed to
|
|
37
37
|
identity's `identity.deliverToken` hook: sign-in links email themselves once
|
|
38
38
|
a `default` account exists.
|
|
39
|
+
- **`email.alert-failed-run`** — failure alerts: set `PATTERN_ALERTS_TO` and any
|
|
40
|
+
failed run emails that address a deep link to the run (an ordinary workflow on
|
|
41
|
+
the `run.failed` event, so you can open and change it).
|
|
39
42
|
- A tiny **driver SPI** — `registerDriver({ id, label, secrets, options, send })`
|
|
40
43
|
in your mod's `ready()`; the admin form generates itself from the field specs.
|
|
41
44
|
|
package/dist/alerts.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-email — the packaged failure-alert workflow.
|
|
3
|
+
*
|
|
4
|
+
* The engine emits one `run.failed` event per failed top-level run (0.5); this
|
|
5
|
+
* workflow turns it into an email to `PATTERN_ALERTS_TO`. Zero-config posture:
|
|
6
|
+
* with the env unset it gates out silently, and with no email account
|
|
7
|
+
* configured the send console-logs — so it ships on, costs nothing, and starts
|
|
8
|
+
* alerting the moment both exist. `internal: false`: open it in the editor to
|
|
9
|
+
* reword the copy, filter workflows, or reroute to another channel entirely.
|
|
10
|
+
*
|
|
11
|
+
* (A failing run that was itself triggered by `run.failed` never re-emits —
|
|
12
|
+
* the engine's recursion guard — so a broken alert can't alert about itself.)
|
|
13
|
+
*
|
|
14
|
+
* A `core.flow.throttle` keyed by workflow id caps the mail at one per failing
|
|
15
|
+
* workflow per 10 minutes: a route failing on every request (or someone
|
|
16
|
+
* hammering it) costs one email, not one per request. Raise or drop it in the
|
|
17
|
+
* editor.
|
|
18
|
+
*/
|
|
19
|
+
import type { Workflow } from "@pattern-js/core";
|
|
20
|
+
export declare function alertFailedRunWorkflow(): Workflow;
|
|
21
|
+
//# sourceMappingURL=alerts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alerts.d.ts","sourceRoot":"","sources":["../src/alerts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,wBAAgB,sBAAsB,IAAI,QAAQ,CAiDjD"}
|
package/dist/alerts.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-email — the packaged failure-alert workflow.
|
|
3
|
+
*
|
|
4
|
+
* The engine emits one `run.failed` event per failed top-level run (0.5); this
|
|
5
|
+
* workflow turns it into an email to `PATTERN_ALERTS_TO`. Zero-config posture:
|
|
6
|
+
* with the env unset it gates out silently, and with no email account
|
|
7
|
+
* configured the send console-logs — so it ships on, costs nothing, and starts
|
|
8
|
+
* alerting the moment both exist. `internal: false`: open it in the editor to
|
|
9
|
+
* reword the copy, filter workflows, or reroute to another channel entirely.
|
|
10
|
+
*
|
|
11
|
+
* (A failing run that was itself triggered by `run.failed` never re-emits —
|
|
12
|
+
* the engine's recursion guard — so a broken alert can't alert about itself.)
|
|
13
|
+
*
|
|
14
|
+
* A `core.flow.throttle` keyed by workflow id caps the mail at one per failing
|
|
15
|
+
* workflow per 10 minutes: a route failing on every request (or someone
|
|
16
|
+
* hammering it) costs one email, not one per request. Raise or drop it in the
|
|
17
|
+
* editor.
|
|
18
|
+
*/
|
|
19
|
+
export function alertFailedRunWorkflow() {
|
|
20
|
+
return {
|
|
21
|
+
id: "email.alert-failed-run",
|
|
22
|
+
name: "Email · alert on failed runs (run.failed)",
|
|
23
|
+
description: "Emails PATTERN_ALERTS_TO whenever a top-level run fails, with the error and a deep link to the " +
|
|
24
|
+
"trace; at most one email per failing workflow every 10 minutes (the throttle node). Unset the env " +
|
|
25
|
+
"(or edit this workflow) to change the posture.",
|
|
26
|
+
source: "code",
|
|
27
|
+
internal: false,
|
|
28
|
+
nodes: [
|
|
29
|
+
{ id: "in", op: "boundary.event", config: { event: "run.failed" } },
|
|
30
|
+
// Recipient(s): unset/empty → the gate stops everything, silently.
|
|
31
|
+
{ id: "to", op: "core.env", config: { name: "PATTERN_ALERTS_TO", default: "" } },
|
|
32
|
+
{ id: "wanted", op: "core.cast.toBoolean" },
|
|
33
|
+
{ id: "gate", op: "core.flow.gate" },
|
|
34
|
+
// One email per failing workflow per window; the rest are dropped.
|
|
35
|
+
{ id: "which", op: "core.object.get", config: { path: "workflowId" } },
|
|
36
|
+
{ id: "throttle", op: "core.flow.throttle", config: { max: 1, window: "10m" } },
|
|
37
|
+
// The admin deep link needs a public origin; localhost is the honest dev default.
|
|
38
|
+
{ id: "origin", op: "core.env", config: { name: "PATTERN_PUBLIC_URL", default: "http://localhost:3000" } },
|
|
39
|
+
{ id: "info", op: "core.object.set", config: { path: "origin" } },
|
|
40
|
+
{ id: "subj", op: "core.string.template", config: { template: "Run failed: {{workflowId}}" } },
|
|
41
|
+
{
|
|
42
|
+
id: "body",
|
|
43
|
+
op: "core.string.template",
|
|
44
|
+
config: {
|
|
45
|
+
template: "# Run failed: {{workflowId}}\n\n> {{error.message}}\n\nInspect the trace (durable runs can resume from the failing node):\n\n{{origin}}/admin/runs/{{runId}}\n\nYou get this because PATTERN_ALERTS_TO is set — edit the \"email.alert-failed-run\" workflow to reword or reroute it.",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{ id: "send", op: "email.send" },
|
|
49
|
+
],
|
|
50
|
+
edges: [
|
|
51
|
+
{ from: { node: "to", port: "out" }, to: { node: "wanted", port: "value" } },
|
|
52
|
+
{ from: { node: "wanted", port: "out" }, to: { node: "gate", port: "condition" } },
|
|
53
|
+
{ from: { node: "in", port: "payload" }, to: { node: "info", port: "object" } },
|
|
54
|
+
{ from: { node: "origin", port: "out" }, to: { node: "info", port: "value" } },
|
|
55
|
+
{ from: { node: "in", port: "payload" }, to: { node: "subj", port: "data" } },
|
|
56
|
+
{ from: { node: "info", port: "out" }, to: { node: "body", port: "data" } },
|
|
57
|
+
{ from: { node: "to", port: "out" }, to: { node: "send", port: "to" } },
|
|
58
|
+
{ from: { node: "subj", port: "out" }, to: { node: "send", port: "subject" } },
|
|
59
|
+
{ from: { node: "body", port: "out" }, to: { node: "send", port: "markdown" } },
|
|
60
|
+
{ from: { node: "in", port: "payload" }, to: { node: "which", port: "object" } },
|
|
61
|
+
{ from: { node: "which", port: "out" }, to: { node: "throttle", port: "key" } },
|
|
62
|
+
{ from: { node: "gate", port: "out" }, to: { node: "throttle", port: "in" } },
|
|
63
|
+
{ from: { node: "throttle", port: "out" }, to: { node: "send", port: "in" } },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=alerts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alerts.js","sourceRoot":"","sources":["../src/alerts.ts"],"names":[],"mappings":"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"}
|
package/dist/app.d.ts
CHANGED
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
* email so the operator sees end-to-end delivery, not a wiring check. The
|
|
10
10
|
* "default" account is what the packaged sign-in delivery workflow uses.
|
|
11
11
|
*/
|
|
12
|
-
export declare const REMOTE = "\nconst { React, api, ui } = globalThis.__PATTERN_ADMIN__;\nconst { GlassPanel, NeonButton, Badge, Modal } = ui;\nconst h = React.createElement;\nconst inputCls = \"glass w-full rounded-lg px-3 py-2 text-sm\";\n\nfunction Field({ label, hint, children }) {\n return h(\"label\", { className: \"block text-sm space-y-1\" },\n h(\"span\", { className: \"text-muted\" }, label),\n children,\n hint && h(\"span\", { className: \"block text-xs text-muted/70\" }, hint));\n}\n\n// Normalize an api.call result that may be the array directly or { key: [...] }.\nfunction arr(r, key) { return Array.isArray(r) ? r : ((r && r[key]) || []); }\n\n// One secret field: a vault|env source toggle + the matching key picker/input.\nfunction SecretRow({ field, refValue, secrets, onChange }) {\n const src = (refValue && refValue.source) || \"vault\";\n const key = (refValue && refValue.key) || \"\";\n const set = (patch) => onChange({ source: src, key, ...patch });\n const vaultPicker = h(\"select\", { className: inputCls, value: key, onChange: (e) => set({ key: e.target.value }) },\n h(\"option\", { value: \"\" }, \"\u2014 pick a vault secret \u2014\"),\n secrets.map((s) => h(\"option\", { key: s, value: s }, s)),\n key && secrets.indexOf(key) < 0 ? h(\"option\", { key: key, value: key }, key + \" (missing)\") : null);\n const envInput = h(\"input\", { className: inputCls, value: key, placeholder: \"ENV_VAR_NAME\", onChange: (e) => set({ key: e.target.value }) });\n return h(Field, { key: field.field, label: \"Secret \u00B7 \" + (field.label || field.field) + (field.required === false ? \" (optional)\" : \"\") },\n h(\"div\", { className: \"flex gap-2\" },\n h(\"select\", { className: \"glass rounded-lg px-2 py-2 text-sm\", style: { width: \"5.5rem\" }, value: src, onChange: (e) => set({ source: e.target.value }) },\n h(\"option\", { value: \"vault\" }, \"vault\"),\n h(\"option\", { value: \"env\" }, \"env\")),\n h(\"div\", { className: \"flex-1\" }, src === \"env\" ? envInput : vaultPicker)));\n}\n\n// The Test flow: sends a REAL email to an operator-supplied address.\nfunction TestModal({ state, setState }) {\n const [to, setTo] = React.useState(\"\");\n if (!state) return null;\n const result = state.result;\n function run() {\n setState({ payload: state.payload, busy: true });\n api.call(\"POST\", \"/email/test\", { ...state.payload, to })\n .then((r) => setState({ payload: state.payload, result: r }))\n .catch((e) => setState({ payload: state.payload, result: { ok: false, detail: String(e) } }));\n }\n return h(Modal, { open: true, onClose: () => setState(null), title: \"Send a test email\" },\n h(\"div\", { className: \"space-y-3\" },\n h(\"p\", { className: \"text-xs text-muted\" },\n \"Sends a REAL email through this account so you can verify end-to-end delivery \u2014 driver, credentials, From address and inbox placement in one go.\"),\n h(Field, { label: \"Send to\" },\n h(\"input\", { className: inputCls, type: \"email\", value: to, placeholder: \"you@example.com\", onChange: (e) => setTo(e.target.value) })),\n h(\"div\", { className: \"flex items-center gap-3\" },\n h(NeonButton, { onClick: run, disabled: state.busy || !to }, state.busy ? \"Sending\u2026\" : \"Send test email\")),\n result && h(\"div\", { className: \"space-y-2\" },\n h(\"p\", { className: \"text-sm font-medium\", style: { color: result.ok ? \"var(--color-neon-lime)\" : \"var(--color-neon-pink)\" } },\n result.ok ? \"\u2713 Sent \u2014 check the inbox.\" + (result.messageId ? \" (id \" + result.messageId + \")\" : \"\") : \"\u2717 Failed\"),\n !result.ok && h(\"pre\", { className: \"glass rounded-lg p-3 text-xs whitespace-pre-wrap\", style: { maxHeight: \"16rem\", overflow: \"auto\" } }, result.detail || \"unknown error\"))));\n}\n\n// The form that creates/edits one account.\nfunction AccountForm({ providers, secrets, form, setForm, reload, onTest }) {\n const [busy, setBusy] = React.useState(false);\n const blank = { name: \"\", provider: (providers[0] && providers[0].id) || \"\", from: \"\", secrets: {}, options: {} };\n const spec = providers.find((p) => p.id === form.provider) || providers[0] || { secrets: [], options: [] };\n\n function pickProvider(id) { setForm({ ...form, provider: id, secrets: {}, options: {} }); }\n function setSecret(field, refValue) { setForm({ ...form, secrets: { ...form.secrets, [field]: refValue } }); }\n function setOption(field, val) { setForm({ ...form, options: { ...form.options, [field]: val } }); }\n\n // Drop empty secret rows so we never persist a sourceless reference.\n function payload() {\n const s = {};\n for (const k of Object.keys(form.secrets)) { const v = form.secrets[k]; if (v && v.key) s[k] = { source: v.source || \"vault\", key: v.key }; }\n const provider = form.provider || (spec && spec.id) || \"\";\n return { name: form.name, provider, from: form.from, secrets: s, options: form.options };\n }\n function save() {\n setBusy(true);\n api.call(\"POST\", \"/email/accounts\", payload()).then(() => { setForm(blank); reload(); }).finally(() => setBusy(false));\n }\n\n return h(GlassPanel, { className: \"p-6 space-y-4\" },\n h(\"div\", null,\n h(\"h3\", { className: \"font-semibold\" }, form.name ? \"Edit account\" : \"New account\"),\n h(\"p\", { className: \"text-xs text-muted mt-1\" },\n \"An account is a named sender \u2014 a driver, a From address and the credentials it needs. Wire one with an \",\n h(\"span\", { className: \"font-mono\" }, \"email.account\"),\n \" node, or leave a node's account unset and the \", h(\"span\", { className: \"font-mono\" }, \"default\"),\n \" account is used. The packaged sign-in delivery workflow emails magic links the moment a default account exists.\")),\n h(\"div\", { className: \"space-y-3\" },\n h(Field, { label: \"Account name\", hint: 'the \"default\" account powers sign-in link delivery.' },\n h(\"input\", { className: inputCls, value: form.name, onChange: (e) => setForm({ ...form, name: e.target.value }), placeholder: \"default\" })),\n h(Field, { label: \"Driver\" },\n h(\"select\", { className: inputCls, value: form.provider || (spec && spec.id) || \"\", onChange: (e) => pickProvider(e.target.value) },\n providers.map((p) => h(\"option\", { key: p.id, value: p.id }, p.label)))),\n h(Field, { label: \"From\", hint: \"the default sender; email.send's from input overrides per message.\" },\n h(\"input\", { className: inputCls, value: form.from, onChange: (e) => setForm({ ...form, from: e.target.value }), placeholder: \"App <hello@example.com>\" })),\n ((spec && spec.secrets) || []).map((f) => h(SecretRow, { key: f.field, field: f, refValue: form.secrets[f.field], secrets: secrets, onChange: (refValue) => setSecret(f.field, refValue) })),\n ((spec && spec.options) || []).map((f) => h(Field, { key: f.field, label: (f.label || f.field) + (f.required ? \"\" : \" (optional)\") },\n h(\"input\", { className: inputCls, value: form.options[f.field] || \"\", placeholder: f.placeholder || \"\", onChange: (e) => setOption(f.field, e.target.value) }))),\n h(\"div\", { className: \"flex items-center gap-3 pt-1\" },\n h(NeonButton, { onClick: save, disabled: busy || !form.name || !form.from }, \"Save account\"),\n h(NeonButton, { onClick: () => onTest(payload()), disabled: busy || !form.from }, \"Test\"),\n form.name && h(NeonButton, { onClick: () => setForm(blank) }, \"New\")),\n h(\"p\", { className: \"text-xs text-muted\" }, \"Secrets come from the vault (System \u2192 Secrets) or an env var \u2014 pick the source per field.\")));\n}\n\n// The list of saved accounts (click to edit, delete).\nfunction AccountList({ accounts, onEdit, reload }) {\n function del(name) { api.call(\"DELETE\", \"/email/accounts/\" + encodeURIComponent(name)).then(reload); }\n return h(GlassPanel, { className: \"p-6 space-y-3\" },\n h(\"div\", { className: \"flex items-center justify-between\" },\n h(\"h3\", { className: \"font-semibold\" }, \"Accounts\"),\n h(\"span\", { className: \"text-xs text-muted\" }, accounts.length + \" configured\")),\n accounts.length === 0\n ? h(\"p\", { className: \"text-sm text-muted\" }, 'No accounts yet. Create one named \"default\" and sign-in links start emailing automatically.')\n : h(\"div\", { className: \"space-y-1\" }, accounts.map((a) =>\n h(\"div\", { key: a.name, className: \"flex items-center justify-between rounded-lg px-2 py-1.5 hover:bg-white/5\" },\n h(\"button\", { className: \"text-left text-sm flex-1 min-w-0\", onClick: () => onEdit(a) },\n h(Badge, { hue: a.name === \"default\" ? 140 : 200 }, a.name), \" \",\n h(\"span\", { className: \"font-mono text-xs text-muted\" }, a.provider + \" \u00B7 \" + a.from)),\n h(\"button\", { className: \"text-xs text-muted hover:text-[var(--color-neon-pink)] shrink-0\", onClick: () => del(a.name) }, \"Delete\")))));\n}\n\nexport default function EmailPage() {\n const blank = { name: \"\", provider: \"\", from: \"\", secrets: {}, options: {} };\n const [providers, setProviders] = React.useState([]);\n const [secrets, setSecrets] = React.useState([]);\n const [accounts, setAccounts] = React.useState([]);\n const [form, setForm] = React.useState(blank);\n const [test, setTest] = React.useState(null);\n\n const reloadAccounts = () => api.call(\"GET\", \"/email/accounts\").then((r) => setAccounts(arr(r, \"accounts\")));\n\n React.useEffect(() => {\n api.call(\"GET\", \"/email/providers\").then((r) => setProviders(arr(r, \"providers\"))).catch(() => {});\n api.call(\"GET\", \"/vault/secrets\").then((r) => setSecrets(arr(r, \"secrets\").map((s) => (typeof s === \"string\" ? s : s.name)))).catch(() => {});\n reloadAccounts().catch(() => {});\n }, []);\n\n function edit(a) { setForm({ name: a.name, provider: a.provider, from: a.from, secrets: { ...(a.secrets || {}) }, options: { ...(a.options || {}) } }); }\n function reloadAndReset() { reloadAccounts(); setForm(blank); }\n\n if (providers.length === 0) {\n return h(GlassPanel, { className: \"p-6 space-y-2\" },\n h(\"h3\", { className: \"font-semibold\" }, \"No email drivers installed\"),\n h(\"p\", { className: \"text-sm text-muted\" },\n \"mod-email is the contract; a driver mod does the sending. Install \",\n h(\"span\", { className: \"font-mono\" }, \"@pattern-js/mod-email-resend\"),\n \" or \", h(\"span\", { className: \"font-mono\" }, \"@pattern-js/mod-email-smtp\"),\n \" and list it in pattern.config.json, then reload this page.\"));\n }\n\n return h(\"div\", null,\n h(\"div\", { className: \"grid gap-6 lg:grid-cols-2\" },\n h(AccountForm, { providers, secrets, form, setForm, reload: reloadAndReset, onTest: (payload) => setTest({ payload }) }),\n h(AccountList, { accounts, onEdit: edit, reload: reloadAccounts })),\n h(TestModal, { state: test, setState: setTest }));\n}\n";
|
|
12
|
+
export declare const REMOTE = "\nconst { React, api, ui } = globalThis.__PATTERN_ADMIN__;\nconst { GlassPanel, NeonButton, Badge, Modal } = ui;\nconst h = React.createElement;\nconst inputCls = \"glass w-full rounded-lg px-3 py-2 text-sm\";\n\nfunction Field({ label, hint, children }) {\n return h(\"label\", { className: \"block text-sm space-y-1\" },\n h(\"span\", { className: \"text-muted\" }, label),\n children,\n hint && h(\"span\", { className: \"block text-xs text-muted/70\" }, hint));\n}\n\n// Normalize an api.call result that may be the array directly or { key: [...] }.\nfunction arr(r, key) { return Array.isArray(r) ? r : ((r && r[key]) || []); }\n\n// One secret field: a vault|env source toggle + the matching key picker/input.\nfunction SecretRow({ field, refValue, secrets, onChange }) {\n const src = (refValue && refValue.source) || \"vault\";\n const key = (refValue && refValue.key) || \"\";\n const set = (patch) => onChange({ source: src, key, ...patch });\n const vaultPicker = h(\"select\", { className: inputCls, value: key, onChange: (e) => set({ key: e.target.value }) },\n h(\"option\", { value: \"\" }, \"\u2014 pick a vault secret \u2014\"),\n secrets.map((s) => h(\"option\", { key: s, value: s }, s)),\n key && secrets.indexOf(key) < 0 ? h(\"option\", { key: key, value: key }, key + \" (missing)\") : null);\n const envInput = h(\"input\", { className: inputCls, value: key, placeholder: \"ENV_VAR_NAME\", onChange: (e) => set({ key: e.target.value }) });\n return h(Field, { key: field.field, label: \"Secret \u00B7 \" + (field.label || field.field) + (field.required === false ? \" (optional)\" : \"\") },\n h(\"div\", { className: \"flex gap-2\" },\n h(\"select\", { className: \"glass rounded-lg px-2 py-2 text-sm\", style: { width: \"5.5rem\" }, value: src, onChange: (e) => set({ source: e.target.value }) },\n h(\"option\", { value: \"vault\" }, \"vault\"),\n h(\"option\", { value: \"env\" }, \"env\")),\n h(\"div\", { className: \"flex-1\" }, src === \"env\" ? envInput : vaultPicker)));\n}\n\n// The Test flow: sends a REAL email to an operator-supplied address.\nfunction TestModal({ state, setState }) {\n const [to, setTo] = React.useState(\"\");\n if (!state) return null;\n const result = state.result;\n function run() {\n setState({ payload: state.payload, busy: true });\n api.call(\"POST\", \"/email/test\", { ...state.payload, to })\n .then((r) => setState({ payload: state.payload, result: r }))\n .catch((e) => setState({ payload: state.payload, result: { ok: false, detail: String(e) } }));\n }\n return h(Modal, { open: true, onClose: () => setState(null), title: \"Send a test email\" },\n h(\"div\", { className: \"space-y-3\" },\n h(\"p\", { className: \"text-xs text-muted\" },\n \"Sends a REAL email through this account so you can verify end-to-end delivery \u2014 driver, credentials, From address and inbox placement in one go.\"),\n h(Field, { label: \"Send to\" },\n h(\"input\", { className: inputCls, type: \"email\", value: to, placeholder: \"you@example.com\", onChange: (e) => setTo(e.target.value) })),\n h(\"div\", { className: \"flex items-center gap-3\" },\n h(NeonButton, { onClick: run, disabled: state.busy || !to }, state.busy ? \"Sending\u2026\" : \"Send test email\")),\n result && h(\"div\", { className: \"space-y-2\" },\n h(\"p\", { className: \"text-sm font-medium\", style: { color: result.ok ? \"var(--color-neon-lime)\" : \"var(--color-neon-pink)\" } },\n result.ok ? \"\u2713 Sent \u2014 check the inbox.\" + (result.messageId ? \" (id \" + result.messageId + \")\" : \"\") : \"\u2717 Failed\"),\n !result.ok && h(\"pre\", { className: \"glass rounded-lg p-3 text-xs whitespace-pre-wrap\", style: { maxHeight: \"16rem\", overflow: \"auto\" } }, result.detail || \"unknown error\"))));\n}\n\n// The form that creates/edits one account.\nfunction AccountForm({ providers, secrets, form, setForm, reload, onTest }) {\n const [busy, setBusy] = React.useState(false);\n const blank = { name: \"\", provider: (providers[0] && providers[0].id) || \"\", from: \"\", secrets: {}, options: {} };\n const spec = providers.find((p) => p.id === form.provider) || providers[0] || { secrets: [], options: [] };\n\n function pickProvider(id) { setForm({ ...form, provider: id, secrets: {}, options: {} }); }\n function setSecret(field, refValue) { setForm({ ...form, secrets: { ...form.secrets, [field]: refValue } }); }\n function setOption(field, val) { setForm({ ...form, options: { ...form.options, [field]: val } }); }\n\n // Drop empty secret rows so we never persist a sourceless reference.\n function payload() {\n const s = {};\n for (const k of Object.keys(form.secrets)) { const v = form.secrets[k]; if (v && v.key) s[k] = { source: v.source || \"vault\", key: v.key }; }\n const provider = form.provider || (spec && spec.id) || \"\";\n return { name: form.name, provider, from: form.from, secrets: s, options: form.options };\n }\n function save() {\n setBusy(true);\n api.call(\"POST\", \"/email/accounts\", payload()).then(() => { setForm(blank); reload(); }).finally(() => setBusy(false));\n }\n\n return h(GlassPanel, { className: \"p-6 space-y-4\" },\n h(\"div\", null,\n h(\"h3\", { className: \"font-semibold\" }, form.name ? \"Edit account\" : \"New account\"),\n h(\"p\", { className: \"text-xs text-muted mt-1\" },\n \"An account is a named sender \u2014 a driver, a From address and the credentials it needs. Wire one with an \",\n h(\"span\", { className: \"font-mono\" }, \"email.account\"),\n \" node, or leave a node's account unset and the \", h(\"span\", { className: \"font-mono\" }, \"default\"),\n \" account is used. The packaged sign-in delivery workflow emails magic links the moment a default account exists.\")),\n h(\"div\", { className: \"space-y-3\" },\n h(Field, { label: \"Account name\", hint: 'the \"default\" account powers sign-in link delivery.' },\n h(\"input\", { className: inputCls, value: form.name, onChange: (e) => setForm({ ...form, name: e.target.value }), placeholder: \"default\" })),\n h(Field, { label: \"Driver\" },\n h(\"select\", { className: inputCls, value: form.provider || (spec && spec.id) || \"\", onChange: (e) => pickProvider(e.target.value) },\n providers.map((p) => h(\"option\", { key: p.id, value: p.id }, p.label)))),\n h(Field, { label: \"From\", hint: \"the default sender; email.send's from input overrides per message.\" },\n h(\"input\", { className: inputCls, value: form.from, onChange: (e) => setForm({ ...form, from: e.target.value }), placeholder: \"App <hello@example.com>\" })),\n ((spec && spec.secrets) || []).map((f) => h(SecretRow, { key: f.field, field: f, refValue: form.secrets[f.field], secrets: secrets, onChange: (refValue) => setSecret(f.field, refValue) })),\n ((spec && spec.options) || []).map((f) => h(Field, { key: f.field, label: (f.label || f.field) + (f.required ? \"\" : \" (optional)\") },\n h(\"input\", { className: inputCls, value: form.options[f.field] || \"\", placeholder: f.placeholder || \"\", onChange: (e) => setOption(f.field, e.target.value) }))),\n h(\"div\", { className: \"flex items-center gap-3 pt-1\" },\n h(NeonButton, { onClick: save, disabled: busy || !form.name || !form.from }, \"Save account\"),\n h(NeonButton, { onClick: () => onTest(payload()), disabled: busy || !form.from }, \"Test\"),\n form.name && h(NeonButton, { onClick: () => setForm(blank) }, \"New\")),\n h(\"p\", { className: \"text-xs text-muted\" }, \"Secrets come from the vault (Resources \u2192 Secrets) or an env var \u2014 pick the source per field.\")));\n}\n\n// The list of saved accounts (click to edit, delete).\nfunction AccountList({ accounts, onEdit, reload }) {\n function del(name) { api.call(\"DELETE\", \"/email/accounts/\" + encodeURIComponent(name)).then(reload); }\n return h(GlassPanel, { className: \"p-6 space-y-3\" },\n h(\"div\", { className: \"flex items-center justify-between\" },\n h(\"h3\", { className: \"font-semibold\" }, \"Accounts\"),\n h(\"span\", { className: \"text-xs text-muted\" }, accounts.length + \" configured\")),\n accounts.length === 0\n ? h(\"p\", { className: \"text-sm text-muted\" }, 'No accounts yet. Create one named \"default\" and sign-in links start emailing automatically.')\n : h(\"div\", { className: \"space-y-1\" }, accounts.map((a) =>\n h(\"div\", { key: a.name, className: \"flex items-center justify-between rounded-lg px-2 py-1.5 hover:bg-white/5\" },\n h(\"button\", { className: \"text-left text-sm flex-1 min-w-0\", onClick: () => onEdit(a) },\n h(Badge, { hue: a.name === \"default\" ? 140 : 200 }, a.name), \" \",\n h(\"span\", { className: \"font-mono text-xs text-muted\" }, a.provider + \" \u00B7 \" + a.from)),\n h(\"button\", { className: \"text-xs text-muted hover:text-[var(--color-neon-pink)] shrink-0\", onClick: () => del(a.name) }, \"Delete\")))));\n}\n\nexport default function EmailPage() {\n const blank = { name: \"\", provider: \"\", from: \"\", secrets: {}, options: {} };\n const [providers, setProviders] = React.useState([]);\n const [secrets, setSecrets] = React.useState([]);\n const [accounts, setAccounts] = React.useState([]);\n const [form, setForm] = React.useState(blank);\n const [test, setTest] = React.useState(null);\n\n const reloadAccounts = () => api.call(\"GET\", \"/email/accounts\").then((r) => setAccounts(arr(r, \"accounts\")));\n\n React.useEffect(() => {\n api.call(\"GET\", \"/email/providers\").then((r) => setProviders(arr(r, \"providers\"))).catch(() => {});\n api.call(\"GET\", \"/vault/secrets\").then((r) => setSecrets(arr(r, \"secrets\").map((s) => (typeof s === \"string\" ? s : s.name)))).catch(() => {});\n reloadAccounts().catch(() => {});\n }, []);\n\n function edit(a) { setForm({ name: a.name, provider: a.provider, from: a.from, secrets: { ...(a.secrets || {}) }, options: { ...(a.options || {}) } }); }\n function reloadAndReset() { reloadAccounts(); setForm(blank); }\n\n if (providers.length === 0) {\n return h(GlassPanel, { className: \"p-6 space-y-2\" },\n h(\"h3\", { className: \"font-semibold\" }, \"No email drivers installed\"),\n h(\"p\", { className: \"text-sm text-muted\" },\n \"mod-email is the contract; a driver mod does the sending. Install \",\n h(\"span\", { className: \"font-mono\" }, \"@pattern-js/mod-email-resend\"),\n \" or \", h(\"span\", { className: \"font-mono\" }, \"@pattern-js/mod-email-smtp\"),\n \" and list it in pattern.config.json, then reload this page.\"));\n }\n\n return h(\"div\", null,\n h(\"div\", { className: \"grid gap-6 lg:grid-cols-2\" },\n h(AccountForm, { providers, secrets, form, setForm, reload: reloadAndReset, onTest: (payload) => setTest({ payload }) }),\n h(AccountList, { accounts, onEdit: edit, reload: reloadAccounts })),\n h(TestModal, { state: test, setState: setTest }));\n}\n";
|
|
13
13
|
//# sourceMappingURL=app.d.ts.map
|
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,MAAM,47VA+JlB,CAAC"}
|
package/dist/app.js
CHANGED
|
@@ -113,7 +113,7 @@ function AccountForm({ providers, secrets, form, setForm, reload, onTest }) {
|
|
|
113
113
|
h(NeonButton, { onClick: save, disabled: busy || !form.name || !form.from }, "Save account"),
|
|
114
114
|
h(NeonButton, { onClick: () => onTest(payload()), disabled: busy || !form.from }, "Test"),
|
|
115
115
|
form.name && h(NeonButton, { onClick: () => setForm(blank) }, "New")),
|
|
116
|
-
h("p", { className: "text-xs text-muted" }, "Secrets come from the vault (
|
|
116
|
+
h("p", { className: "text-xs text-muted" }, "Secrets come from the vault (Resources → Secrets) or an env var — pick the source per field.")));
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// The list of saved accounts (click to edit, delete).
|
package/dist/mod.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAW3E,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAYD,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,UAAU,CAsBlE;AAED,gEAAgE;;AAChE,wBAA0B"}
|
package/dist/mod.js
CHANGED
|
@@ -19,6 +19,7 @@ import { accountOp } from "./ops/account.js";
|
|
|
19
19
|
import { inboundOps } from "./ops/inbound.js";
|
|
20
20
|
import { sendOp } from "./ops/send.js";
|
|
21
21
|
import { deliverTokenWorkflow } from "./delivery.js";
|
|
22
|
+
import { alertFailedRunWorkflow } from "./alerts.js";
|
|
22
23
|
import { emailAdminRoutes, emailFrontend, settingsOps } from "./settings.js";
|
|
23
24
|
/** The packaged docs/ chapter (the `docs` contribution points at "email-docs"). */
|
|
24
25
|
function packagedDocs(engine) {
|
|
@@ -40,8 +41,9 @@ export function emailMod(options = {}) {
|
|
|
40
41
|
ops: [sendOp, accountOp, ...inboundOps, ...settingsOps],
|
|
41
42
|
// The delivery workflow registers unconditionally: without mod-identity its
|
|
42
43
|
// hook is auto-declared and never invoked (inert); with it, delivery starts
|
|
43
|
-
// the moment a "default" account exists.
|
|
44
|
-
|
|
44
|
+
// the moment a "default" account exists. Same posture for failure alerts:
|
|
45
|
+
// inert until PATTERN_ALERTS_TO is set.
|
|
46
|
+
workflows: [deliverTokenWorkflow(), alertFailedRunWorkflow(), ...emailAdminRoutes()],
|
|
45
47
|
frontend: emailFrontend(),
|
|
46
48
|
setup: (engine) => {
|
|
47
49
|
packagedDocs(engine);
|
package/dist/mod.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mod.js","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,SAAS,EAAgC,MAAM,kBAAkB,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAO7E,mFAAmF;AACnF,SAAS,YAAY,CAAC,MAAc;IAClC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/D,IAAI,UAAU,CAAC,GAAG,CAAC;YAAE,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;IAClE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,UAA2B,EAAE;IACpD,MAAM,MAAM,GAAG,IAAI,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,IAAI,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAChD,OAAO,SAAS,CAAC;QACf,IAAI,EAAE,uBAAuB;QAC7B,IAAI,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE;QAC7D,GAAG,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,UAAU,EAAE,GAAG,WAAW,CAAC;QACvD,4EAA4E;QAC5E,4EAA4E;QAC5E,
|
|
1
|
+
{"version":3,"file":"mod.js","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,SAAS,EAAgC,MAAM,kBAAkB,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAO7E,mFAAmF;AACnF,SAAS,YAAY,CAAC,MAAc;IAClC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/D,IAAI,UAAU,CAAC,GAAG,CAAC;YAAE,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;IAClE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,UAA2B,EAAE;IACpD,MAAM,MAAM,GAAG,IAAI,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,IAAI,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAChD,OAAO,SAAS,CAAC;QACf,IAAI,EAAE,uBAAuB;QAC7B,IAAI,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE;QAC7D,GAAG,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,UAAU,EAAE,GAAG,WAAW,CAAC;QACvD,4EAA4E;QAC5E,4EAA4E;QAC5E,0EAA0E;QAC1E,wCAAwC;QACxC,SAAS,EAAE,CAAC,oBAAoB,EAAE,EAAE,sBAAsB,EAAE,EAAE,GAAG,gBAAgB,EAAE,CAAC;QACpF,QAAQ,EAAE,aAAa,EAAE;QACzB,KAAK,EAAE,CAAC,MAAc,EAAE,EAAE;YACxB,YAAY,CAAC,MAAM,CAAC,CAAC;YACrB,MAAM,CAAC,cAAc,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC;YACpD,MAAM,CAAC,cAAc,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;QAChD,CAAC;QACD,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACtB,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,gEAAgE;AAChE,eAAe,QAAQ,EAAE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../src/ops/account.ts"],"names":[],"mappings":"AAAA,qGAAqG;AAErG,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAK/D,eAAO,MAAM,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../src/ops/account.ts"],"names":[],"mappings":"AAAA,qGAAqG;AAErG,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAK/D,eAAO,MAAM,SAAS,EAAE,YA4BvB,CAAC"}
|
package/dist/ops/account.js
CHANGED
|
@@ -5,8 +5,9 @@ import { DEFAULT_ACCOUNT } from "../config.js";
|
|
|
5
5
|
import { emailConfig, maybe } from "./shared.js";
|
|
6
6
|
export const accountOp = {
|
|
7
7
|
type: "email.account",
|
|
8
|
+
effects: "pure",
|
|
8
9
|
title: "email.account",
|
|
9
|
-
description: "Resolve a named email account (configured in admin →
|
|
10
|
+
description: "Resolve a named email account (configured in admin → Resources → Email) to an account reference. " +
|
|
10
11
|
"Re-pointing the account in admin instantly re-targets every workflow using it. Defaults to " +
|
|
11
12
|
'"default". With required=false it probes instead of throwing: `configured` reports whether the ' +
|
|
12
13
|
"account exists — the packaged delivery workflow branches on it.",
|
|
@@ -25,7 +26,7 @@ export const accountOp = {
|
|
|
25
26
|
const name = (await maybe(ctx, "account")) ?? cfg.account;
|
|
26
27
|
const ref = emailConfig(ctx).resolveAccount(name);
|
|
27
28
|
if (!ref && cfg.required) {
|
|
28
|
-
throw new Error(`email.account: no account "${name}" is configured — add it in admin →
|
|
29
|
+
throw new Error(`email.account: no account "${name}" is configured — add it in admin → Resources → Email.`);
|
|
29
30
|
}
|
|
30
31
|
return { account: ref ?? null, configured: Boolean(ref) };
|
|
31
32
|
},
|
package/dist/ops/account.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.js","sourceRoot":"","sources":["../../src/ops/account.ts"],"names":[],"mappings":"AAAA,qGAAqG;AAErG,OAAO,EAAE,KAAK,EAAE,CAAC,EAAqB,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,CAAC,MAAM,SAAS,GAAiB;IACrC,IAAI,EAAE,eAAe;IACrB,KAAK,EAAE,eAAe;IACtB,WAAW,EACT,
|
|
1
|
+
{"version":3,"file":"account.js","sourceRoot":"","sources":["../../src/ops/account.ts"],"names":[],"mappings":"AAAA,qGAAqG;AAErG,OAAO,EAAE,KAAK,EAAE,CAAC,EAAqB,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,CAAC,MAAM,SAAS,GAAiB;IACrC,IAAI,EAAE,eAAe;IACrB,OAAO,EAAE,MAAM;IACf,KAAK,EAAE,eAAe;IACtB,WAAW,EACT,mGAAmG;QACnG,6FAA6F;QAC7F,iGAAiG;QACjG,iEAAiE;IACnE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC;QACnD,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;KACpC,CAAC;IACF,YAAY,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;IAC5C,MAAM,EAAE,EAAE;IACV,OAAO,EAAE;QACP,OAAO,EAAE,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC;QAC3C,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;KAC/B;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QACrB,MAAM,GAAG,GAAG,GAAG,CAAC,MAAgD,CAAC;QACjE,MAAM,IAAI,GAAG,CAAC,MAAM,KAAK,CAAS,GAAG,EAAE,SAAS,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC;QAClE,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QAClD,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,8BAA8B,IAAI,wDAAwD,CAAC,CAAC;QAC9G,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;IAC5D,CAAC;CACF,CAAC"}
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAA4B,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE5E,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEvE,OAAO,KAAK,EAEV,YAAY,EAEZ,eAAe,EACf,eAAe,EAGf,mBAAmB,EACnB,YAAY,EACZ,SAAS,EACV,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,kFAAkF;IAClF,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAAC;IAC5C,OAAO,IAAI,eAAe,EAAE,CAAC;IAC7B,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IAChD,qFAAqF;IACrF,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5D,mFAAmF;IACnF,WAAW,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAClF;;;;;OAKG;IACH,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACjF,iGAAiG;IACjG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAChG;AAED,qBAAa,mBAAoB,YAAW,YAAY;IAG1C,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;gBAElC,MAAM,EAAE,kBAAkB;IAEvD,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI;IAI3C,OAAO,IAAI,eAAe,EAAE;IAI5B,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIzC,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAW3D,WAAW,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAqBjF,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA8ChF,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;YAStF,OAAO;IA0BrB,8FAA8F;YAChF,cAAc;IAmB5B,4GAA4G;YAC9F,SAAS;YAoCT,iBAAiB;CAqBhC"}
|
package/dist/service.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* secrets (vault/env), markdown → html+text, attachments → bytes, then the
|
|
9
9
|
* driver. Secret VALUES never sit in workflow values or persisted config.
|
|
10
10
|
*/
|
|
11
|
-
import { resolveSourced } from "@pattern-js/core";
|
|
11
|
+
import { noEffect, resolveSourced } from "@pattern-js/core";
|
|
12
12
|
import { renderEmailMarkdown } from "./markdown.js";
|
|
13
13
|
import { DEFAULT_ACCOUNT } from "./config.js";
|
|
14
14
|
import { blobStore, STORE_SERVICE_KEY } from "./well-known.js";
|
|
@@ -31,7 +31,9 @@ export class DefaultEmailService {
|
|
|
31
31
|
const name = typeof input.account === "string" ? input.account : (input.account?.account ?? DEFAULT_ACCOUNT);
|
|
32
32
|
const account = this.config.account(name);
|
|
33
33
|
if (!account) {
|
|
34
|
-
|
|
34
|
+
// Preflight failures never reach a provider — stamp them so durable
|
|
35
|
+
// resume re-runs the node without asking (see core's `noEffect`).
|
|
36
|
+
throw noEffect(new Error(`mod-email: no account "${name}" is configured — add it in admin → Resources → Email.`));
|
|
35
37
|
}
|
|
36
38
|
return this.sendVia(account, input, ctx);
|
|
37
39
|
}
|
|
@@ -104,11 +106,22 @@ export class DefaultEmailService {
|
|
|
104
106
|
async sendVia(account, input, ctx) {
|
|
105
107
|
const driver = this.registry.get(account.provider);
|
|
106
108
|
if (!driver) {
|
|
107
|
-
throw new Error(`mod-email: account "${account.name}" uses provider "${account.provider}" but no such driver is registered — ` +
|
|
108
|
-
`install its mod (e.g. @pattern-js/mod-email-${account.provider}) and list it in pattern.config.json.`);
|
|
109
|
+
throw noEffect(new Error(`mod-email: account "${account.name}" uses provider "${account.provider}" but no such driver is registered — ` +
|
|
110
|
+
`install its mod (e.g. @pattern-js/mod-email-${account.provider}) and list it in pattern.config.json.`));
|
|
111
|
+
}
|
|
112
|
+
// Everything up to `driver.send` is preflight: a missing secret, an empty
|
|
113
|
+
// recipient list, a missing body, an unknown blob — nothing left the
|
|
114
|
+
// process, so those errors carry the no-effect verdict. The driver call
|
|
115
|
+
// itself is the ambiguous zone (drivers classify what they can).
|
|
116
|
+
let message;
|
|
117
|
+
let creds;
|
|
118
|
+
try {
|
|
119
|
+
creds = await this.resolveSecrets(driver, account, ctx);
|
|
120
|
+
message = await this.normalize(input, account, ctx);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
throw err instanceof Error ? noEffect(err) : err;
|
|
109
124
|
}
|
|
110
|
-
const creds = await this.resolveSecrets(driver, account, ctx);
|
|
111
|
-
const message = await this.normalize(input, account, ctx);
|
|
112
125
|
const { messageId } = await driver.send(message, creds, account.options, ctx);
|
|
113
126
|
return { messageId, provider: account.provider, account: account.name };
|
|
114
127
|
}
|
package/dist/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,cAAc,EAAkB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/dist/settings.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAOL,KAAK,oBAAoB,EAEzB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAOL,KAAK,oBAAoB,EAEzB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAC;AAiH1B,eAAO,MAAM,WAAW,EAAE,YAAY,EAA8E,CAAC;AAIrH,wBAAgB,gBAAgB,IAAI,QAAQ,EAAE,CAU7C;AAED,wBAAgB,aAAa,IAAI,oBAAoB,CAMpD"}
|
package/dist/settings.js
CHANGED
|
@@ -18,6 +18,7 @@ import { REMOTE } from "./app.js";
|
|
|
18
18
|
// ───────────────────────────── Providers ──────────────────────────────
|
|
19
19
|
const providersList = {
|
|
20
20
|
type: "email.providers.list",
|
|
21
|
+
effects: "pure",
|
|
21
22
|
title: "email.providers.list",
|
|
22
23
|
description: "List the registered email drivers + their secret/option field specs (drives the account form).",
|
|
23
24
|
reusable: false,
|
|
@@ -42,10 +43,21 @@ async function readAccountFields(ctx) {
|
|
|
42
43
|
maybe(ctx, "secrets"),
|
|
43
44
|
maybe(ctx, "options"),
|
|
44
45
|
]);
|
|
45
|
-
|
|
46
|
+
// A partial write — `secrets` or `options` not wired / not in the body —
|
|
47
|
+
// keeps what the account already has. An omitted field must never wipe a
|
|
48
|
+
// secret ref; sending an explicit `{}` is how you clear one.
|
|
49
|
+
const existing = emailConfig(ctx).account(name);
|
|
50
|
+
return accountSchema.parse({
|
|
51
|
+
name,
|
|
52
|
+
provider,
|
|
53
|
+
from,
|
|
54
|
+
secrets: secrets ?? existing?.secrets ?? {},
|
|
55
|
+
options: options ?? existing?.options ?? {},
|
|
56
|
+
});
|
|
46
57
|
}
|
|
47
58
|
const accountsRead = {
|
|
48
59
|
type: "email.accounts.read",
|
|
60
|
+
effects: "pure",
|
|
49
61
|
title: "email.accounts.read",
|
|
50
62
|
description: "List the configured email accounts (secret NAMES/sources only, never values).",
|
|
51
63
|
reusable: false,
|
|
@@ -57,6 +69,7 @@ const accountsRead = {
|
|
|
57
69
|
};
|
|
58
70
|
const accountsWrite = {
|
|
59
71
|
type: "email.accounts.write",
|
|
72
|
+
effects: "idempotent",
|
|
60
73
|
title: "email.accounts.write",
|
|
61
74
|
description: "Create or update an email account (upsert by name).",
|
|
62
75
|
reusable: false,
|
|
@@ -72,6 +85,7 @@ const accountsWrite = {
|
|
|
72
85
|
};
|
|
73
86
|
const accountsDelete = {
|
|
74
87
|
type: "email.accounts.delete",
|
|
88
|
+
effects: "idempotent",
|
|
75
89
|
title: "email.accounts.delete",
|
|
76
90
|
description: "Delete an email account by name.",
|
|
77
91
|
reusable: false,
|
|
@@ -114,7 +128,7 @@ export function emailAdminRoutes() {
|
|
|
114
128
|
}
|
|
115
129
|
export function emailFrontend() {
|
|
116
130
|
return {
|
|
117
|
-
menu: [{ category: "
|
|
131
|
+
menu: [{ category: "Resources", label: "Email", icon: "mail", path: "/x/email/accounts", order: 50 }],
|
|
118
132
|
// The Tier-2 page is just its source; the admin serves + imports it (no workflow).
|
|
119
133
|
pages: [{ path: "/x/email/accounts", title: "Email", module: REMOTE }],
|
|
120
134
|
};
|
package/dist/settings.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,KAAK,EACL,CAAC,GAKF,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,yEAAyE;AAEzE,MAAM,aAAa,GAAiB;IAClC,IAAI,EAAE,sBAAsB;IAC5B,KAAK,EAAE,sBAAsB;IAC7B,WAAW,EAAE,gGAAgG;IAC7G,QAAQ,EAAE,KAAK;IACf,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;IACpB,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE;IAC/B,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;CAC/D,CAAC;AAEF,yEAAyE;AAEzE,MAAM,aAAa,GAAG;IACpB,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC1B,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC9B,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC1B,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACtE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;CACjD,CAAC;AAEF,KAAK,UAAU,iBAAiB,CAAC,GAAc;IAC7C,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACjE,GAAG,CAAC,KAAK,CAAC,KAAK,CAAS,MAAM,CAAC;QAC/B,GAAG,CAAC,KAAK,CAAC,KAAK,CAAS,UAAU,CAAC;QACnC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAS,MAAM,CAAC;QAC/B,KAAK,CAAoD,GAAG,EAAE,SAAS,CAAC;QACxE,KAAK,CAAyB,GAAG,EAAE,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH,OAAO,aAAa,CAAC,KAAK,CAAC,
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -88,7 +88,7 @@ export interface EmailMessage {
|
|
|
88
88
|
/**
|
|
89
89
|
* A driver, registered by a provider mod (`mod-email-resend`, `mod-email-smtp`,
|
|
90
90
|
* …) via `EmailService.registerDriver` in its `ready()`. The `secrets`/`options`
|
|
91
|
-
* field lists drive the auto-generated account form in admin →
|
|
91
|
+
* field lists drive the auto-generated account form in admin → Resources → Email.
|
|
92
92
|
* (Field descriptors use `field` for the key — this mod's admin page is new
|
|
93
93
|
* code, so it doesn't inherit mod-ai's `name` naming for the same idea.)
|
|
94
94
|
*/
|
package/docs/guides/delivery.md
CHANGED
|
@@ -21,9 +21,9 @@ SMTP at the "Sign-in link delivery?" question.
|
|
|
21
21
|
|
|
22
22
|
## 2. Create the `default` account
|
|
23
23
|
|
|
24
|
-
Admin → **
|
|
24
|
+
Admin → **Resources → Email** → new account named `default`:
|
|
25
25
|
|
|
26
|
-
- **Resend**: the API key, from the vault (
|
|
26
|
+
- **Resend**: the API key, from the vault (Resources → Secrets) or an env var
|
|
27
27
|
such as `RESEND_API_KEY`. The From address must be on a domain you verified
|
|
28
28
|
with Resend.
|
|
29
29
|
- **SMTP**: host (+ port/TLS/username as options) and the password as the one
|
package/docs/index.md
CHANGED
|
@@ -17,7 +17,7 @@ The one persisted concept is the **account** — a memorable name bound to a
|
|
|
17
17
|
driver, a From address, sourced secrets and driver options. Two accounts of
|
|
18
18
|
the same driver with different credentials are just two records (prod + a
|
|
19
19
|
staging relay, a marketing sender + an alerts sender). Manage them in admin →
|
|
20
|
-
**
|
|
20
|
+
**Resources → Email**; the form generates itself from whatever fields the
|
|
21
21
|
registered drivers declare, and the **Test** button sends a real email so you
|
|
22
22
|
verify end-to-end delivery, not a wiring check.
|
|
23
23
|
|
|
@@ -55,6 +55,25 @@ anyway. Open the workflow in the admin editor to reword the email (two
|
|
|
55
55
|
`core.string.template` nodes) or fork it entirely — see the
|
|
56
56
|
[delivery guide](guides/delivery.md).
|
|
57
57
|
|
|
58
|
+
## Failure alerts (0.5)
|
|
59
|
+
|
|
60
|
+
A run fails, you get an email. The engine emits one `run.failed` event per
|
|
61
|
+
failed top-level run (sub-run failures fold into their parent; cancels don't
|
|
62
|
+
count), and this mod ships **`email.alert-failed-run`** subscribed to it: set
|
|
63
|
+
`PATTERN_ALERTS_TO=ops@example.com` and every failure lands in that inbox
|
|
64
|
+
with the workflow name, the error, and a deep link to the run's trace
|
|
65
|
+
(`PATTERN_PUBLIC_URL` shapes the link; durable runs can resume right from
|
|
66
|
+
that page). Unset, the workflow gates out silently — installing mod-email
|
|
67
|
+
changes nothing until you opt in. It's an ordinary editable workflow: open it
|
|
68
|
+
to reword the copy, alert only on certain workflows, or reroute to another
|
|
69
|
+
channel. A failing alert can't alert about itself — the engine's recursion
|
|
70
|
+
guard skips runs that `run.failed` itself triggered.
|
|
71
|
+
|
|
72
|
+
A `core.flow.throttle` node keyed on the workflow id holds the mail to one
|
|
73
|
+
email per failing workflow every 10 minutes, so a route that fails on every
|
|
74
|
+
request (or someone hammering one) costs one email. Change `max` and `window`
|
|
75
|
+
on that node to taste.
|
|
76
|
+
|
|
58
77
|
## Writing a driver
|
|
59
78
|
|
|
60
79
|
A driver mod registers an `EmailDriverSpec` on the email service in `ready()`:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Resolve an account name (configured in admin →
|
|
1
|
+
Resolve an account name (configured in admin → Resources → Email) to an account reference for `email.send` — the ref carries the NAME, never the credentials, so re-pointing the account in admin instantly re-targets every workflow using it. Defaults to `"default"`. Two modes: `required: true` (the default) throws with an admin hint when the account is missing — the right failure for a workflow that must send; `required: false` probes instead, outputting `account: null` + `configured: false` so a `core.flow.branch` can take a fallback path — exactly how the packaged `email.deliver-token` workflow decides between emailing a sign-in link and leaving the console fallback alone.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Proves an account works by sending a real email: a short "It works" message
|
|
2
|
+
with subject "Pattern test email" goes to the `to` address through the
|
|
3
|
+
account draft you pass (same fields as `email.accounts.write`). The draft is
|
|
4
|
+
not saved, so you can test before committing; omitted `secrets` or `options`
|
|
5
|
+
fall back to the saved account of that name. The result is `{ ok: true,
|
|
6
|
+
messageId? }` or `{ ok: false, detail }` with the driver's error message,
|
|
7
|
+
and the op itself does not throw.
|
|
8
|
+
|
|
9
|
+
It sends for real because for email only end-to-end delivery means
|
|
10
|
+
anything; use an address you control, and expect it to count against the
|
|
11
|
+
provider's quota. Privileged: served at `POST /admin/api/email/test` behind
|
|
12
|
+
the `admin` scope.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Remove an email account by `name` (the admin route takes it from the path:
|
|
2
|
+
`DELETE /admin/api/email/accounts/:name`). Deleting a name that does not
|
|
3
|
+
exist still answers `{ ok: true }`. The vault secrets the account pointed at
|
|
4
|
+
stay where they are. Workflows that name the account fail at their next
|
|
5
|
+
`email.send` with a "no account configured" error, and `email.account` with
|
|
6
|
+
`required: false` starts reporting `configured: false`. Privileged: gate the
|
|
7
|
+
trigger with the `admin` scope.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Every configured email account as stored: `{ name, provider, from, secrets,
|
|
2
|
+
options }`. `secrets` maps each field to its source (`{ source: "vault" |
|
|
3
|
+
"env", key }`), so the output names where a credential lives and never holds
|
|
4
|
+
its value. Backs the admin Email page at `GET /admin/api/email/accounts`.
|
|
5
|
+
Privileged: gate the trigger with the `admin` scope. To turn one name into a
|
|
6
|
+
ref a workflow can send through, use `email.account`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Create or update an email account, upserted by `name`. Requires `name`,
|
|
2
|
+
`provider` (a driver id from `email.providers.list`) and `from` (for example
|
|
3
|
+
`App <hello@example.com>`); `secrets` is an object of `{ source, key }` refs
|
|
4
|
+
and `options` an object of strings. A field you leave out keeps the saved
|
|
5
|
+
value, so re-saving `from` never wipes a secret ref; send `{}` to clear one.
|
|
6
|
+
Idempotent.
|
|
7
|
+
|
|
8
|
+
Name the main account `default`: the packaged delivery workflow and
|
|
9
|
+
`email.account` fall back to it. Workflows hold the account name, so editing
|
|
10
|
+
it here re-targets every sender at the next send. Accounts persist in
|
|
11
|
+
`.pattern-data/email-config.json` (in memory only on a read-only filesystem).
|
|
12
|
+
Privileged: the admin route carries the `admin` scope. Prove a draft with
|
|
13
|
+
`email.account.test` before saving it.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The email drivers registered in this installation, each as `{ id, label,
|
|
2
|
+
secrets, options }` where `secrets` and `options` list field descriptors
|
|
3
|
+
(`{ field, label, required? }`; option fields may add a `placeholder`). The admin Email page builds its
|
|
4
|
+
account form from this, at `GET /admin/api/email/providers` behind the `admin`
|
|
5
|
+
scope. A driver appears once its mod (`@pattern-js/mod-email-resend`,
|
|
6
|
+
`@pattern-js/mod-email-smtp`, or your own `registerDriver` call) is
|
|
7
|
+
installed; an empty list means nothing can send yet. Use the field names here
|
|
8
|
+
as the keys of an account's `secrets` and `options` when you call
|
|
9
|
+
`email.accounts.write`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pattern-js/mod-email",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-main.79cf721",
|
|
4
4
|
"description": "Transactional email for Pattern — the contract mod. Admin-configured accounts (provider + sourced secrets + from), driver mods plug in (Resend, SMTP, …), one email.send op, and a packaged workflow that delivers identity sign-in links the moment a default account exists.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pattern",
|
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"zod": "^4.
|
|
30
|
-
"@pattern-js/core": "0.
|
|
31
|
-
"@pattern-js/runtime-node": "0.
|
|
29
|
+
"zod": "^4.6.7",
|
|
30
|
+
"@pattern-js/core": "0.5.0-main.79cf721",
|
|
31
|
+
"@pattern-js/runtime-node": "0.5.0-main.79cf721"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^22.0.0",
|
|
35
35
|
"typescript": "^5.6.0",
|
|
36
|
-
"@pattern-js/mod-auth-magic-link": "0.
|
|
37
|
-
"@pattern-js/mod-identity": "0.
|
|
36
|
+
"@pattern-js/mod-auth-magic-link": "0.5.0-main.79cf721",
|
|
37
|
+
"@pattern-js/mod-identity": "0.5.0-main.79cf721"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=22.5"
|