@pattern-js/mod-email 0.3.0-main.4a77893
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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/app.d.ts +13 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +172 -0
- package/dist/app.js.map +1 -0
- package/dist/config.d.ts +52 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +85 -0
- package/dist/config.js.map +1 -0
- package/dist/delivery.d.ts +26 -0
- package/dist/delivery.d.ts.map +1 -0
- package/dist/delivery.js +83 -0
- package/dist/delivery.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +17 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +173 -0
- package/dist/markdown.js.map +1 -0
- package/dist/mod.d.ts +20 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +57 -0
- package/dist/mod.js.map +1 -0
- package/dist/ops/account.d.ts +4 -0
- package/dist/ops/account.d.ts.map +1 -0
- package/dist/ops/account.js +33 -0
- package/dist/ops/account.js.map +1 -0
- package/dist/ops/send.d.ts +4 -0
- package/dist/ops/send.d.ts.map +1 -0
- package/dist/ops/send.js +57 -0
- package/dist/ops/send.js.map +1 -0
- package/dist/ops/shared.d.ts +8 -0
- package/dist/ops/shared.d.ts.map +1 -0
- package/dist/ops/shared.js +18 -0
- package/dist/ops/shared.js.map +1 -0
- package/dist/service.d.ts +51 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +156 -0
- package/dist/service.js.map +1 -0
- package/dist/settings.d.ts +18 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +122 -0
- package/dist/settings.js.map +1 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +63 -0
- package/dist/types.js.map +1 -0
- package/dist/well-known.d.ts +34 -0
- package/dist/well-known.d.ts.map +1 -0
- package/dist/well-known.js +28 -0
- package/dist/well-known.js.map +1 -0
- package/docs/guides/delivery.md +57 -0
- package/docs/index.md +95 -0
- package/docs/ops/email.account.md +1 -0
- package/docs/ops/email.send.md +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Makkr Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# @pattern-js/mod-email
|
|
2
|
+
|
|
3
|
+
Transactional email for [Pattern](../../README.md) — the **contract mod**.
|
|
4
|
+
Accounts (a named sender: driver + From + sourced credentials) are configured
|
|
5
|
+
in the admin; driver mods do the wire work
|
|
6
|
+
([`mod-email-resend`](https://www.npmjs.com/package/@pattern-js/mod-email-resend),
|
|
7
|
+
[`mod-email-smtp`](https://www.npmjs.com/package/@pattern-js/mod-email-smtp));
|
|
8
|
+
workflows send with one `email.send` node. Install it next to
|
|
9
|
+
`mod-auth-magic-link` and sign-in links start emailing the moment a `default`
|
|
10
|
+
account exists — console fallback until then, so nothing breaks on day one.
|
|
11
|
+
|
|
12
|
+
**Links:** [pattern-js.dev](https://pattern-js.dev) · [npm](https://www.npmjs.com/package/@pattern-js/mod-email)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @pattern-js/mod-email @pattern-js/mod-email-resend
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## When to use / when not
|
|
19
|
+
|
|
20
|
+
Reach for it when your app sends email — sign-in links, notifications, agent
|
|
21
|
+
tools — and you want the sender to be swappable config, not code: accounts
|
|
22
|
+
live in admin → **System → Email**, credentials in the vault or env vars, and
|
|
23
|
+
re-pointing an account re-targets every workflow using it. Skip it if your app
|
|
24
|
+
never sends email; if you only need dev sign-in, the magic-link console
|
|
25
|
+
fallback already covers you with zero mods.
|
|
26
|
+
|
|
27
|
+
## What you get
|
|
28
|
+
|
|
29
|
+
- **`email.send`** — to/subject + a `markdown` body (rendered to inline-styled
|
|
30
|
+
HTML + a text alternative; a lone link becomes a button), or explicit
|
|
31
|
+
`html`/`text`. Attachments from media bytes, blob refs, or literal files.
|
|
32
|
+
- **`email.account`** — resolve a named account to a ref (like `ai.alias` for
|
|
33
|
+
models); `required: false` turns it into a probe to branch on.
|
|
34
|
+
- **Admin → System → Email** — accounts CRUD with per-driver fields, secrets
|
|
35
|
+
from the vault or env, and a Test button that sends a REAL email.
|
|
36
|
+
- **`email.deliver-token`** — a visible, forkable workflow subscribed to
|
|
37
|
+
identity's `identity.deliverToken` hook: sign-in links email themselves once
|
|
38
|
+
a `default` account exists.
|
|
39
|
+
- A tiny **driver SPI** — `registerDriver({ id, label, secrets, options, send })`
|
|
40
|
+
in your mod's `ready()`; the admin form generates itself from the field specs.
|
|
41
|
+
|
|
42
|
+
## Config
|
|
43
|
+
|
|
44
|
+
Defaults work from the bare config entry:
|
|
45
|
+
|
|
46
|
+
```jsonc
|
|
47
|
+
{ "mods": ["@pattern-js/mod-email", "@pattern-js/mod-email-resend"] }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
To move the accounts file, export a local wrapper mod:
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
// mods/email.mjs
|
|
54
|
+
import { emailMod } from "@pattern-js/mod-email";
|
|
55
|
+
export default emailMod({ configPath: "./.pattern-data/email-config.json" });
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Full documentation: the **Email** chapter at `/docs` (served by
|
|
59
|
+
`@pattern-js/mod-docs`), or [the source](docs/index.md).
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-email — the Tier-2 "Email" admin page.
|
|
3
|
+
*
|
|
4
|
+
* Just the ESM SOURCE of the page (default export = the component), written
|
|
5
|
+
* against the shared `__PATTERN_ADMIN__` global (React, the API client, the
|
|
6
|
+
* glass UI kit) — the admin's exact stack, no bundler. It manages ACCOUNTS:
|
|
7
|
+
* a name + a driver (which drives the secret + option fields), a From address,
|
|
8
|
+
* each secret from the vault or an env var — plus a Test that sends a REAL
|
|
9
|
+
* email so the operator sees end-to-end delivery, not a wiring check. The
|
|
10
|
+
* "default" account is what the packaged sign-in delivery workflow uses.
|
|
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";
|
|
13
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,MAAM,y7VA+JlB,CAAC"}
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-email — the Tier-2 "Email" admin page.
|
|
3
|
+
*
|
|
4
|
+
* Just the ESM SOURCE of the page (default export = the component), written
|
|
5
|
+
* against the shared `__PATTERN_ADMIN__` global (React, the API client, the
|
|
6
|
+
* glass UI kit) — the admin's exact stack, no bundler. It manages ACCOUNTS:
|
|
7
|
+
* a name + a driver (which drives the secret + option fields), a From address,
|
|
8
|
+
* each secret from the vault or an env var — plus a Test that sends a REAL
|
|
9
|
+
* email so the operator sees end-to-end delivery, not a wiring check. The
|
|
10
|
+
* "default" account is what the packaged sign-in delivery workflow uses.
|
|
11
|
+
*/
|
|
12
|
+
export const REMOTE = `
|
|
13
|
+
const { React, api, ui } = globalThis.__PATTERN_ADMIN__;
|
|
14
|
+
const { GlassPanel, NeonButton, Badge, Modal } = ui;
|
|
15
|
+
const h = React.createElement;
|
|
16
|
+
const inputCls = "glass w-full rounded-lg px-3 py-2 text-sm";
|
|
17
|
+
|
|
18
|
+
function Field({ label, hint, children }) {
|
|
19
|
+
return h("label", { className: "block text-sm space-y-1" },
|
|
20
|
+
h("span", { className: "text-muted" }, label),
|
|
21
|
+
children,
|
|
22
|
+
hint && h("span", { className: "block text-xs text-muted/70" }, hint));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Normalize an api.call result that may be the array directly or { key: [...] }.
|
|
26
|
+
function arr(r, key) { return Array.isArray(r) ? r : ((r && r[key]) || []); }
|
|
27
|
+
|
|
28
|
+
// One secret field: a vault|env source toggle + the matching key picker/input.
|
|
29
|
+
function SecretRow({ field, refValue, secrets, onChange }) {
|
|
30
|
+
const src = (refValue && refValue.source) || "vault";
|
|
31
|
+
const key = (refValue && refValue.key) || "";
|
|
32
|
+
const set = (patch) => onChange({ source: src, key, ...patch });
|
|
33
|
+
const vaultPicker = h("select", { className: inputCls, value: key, onChange: (e) => set({ key: e.target.value }) },
|
|
34
|
+
h("option", { value: "" }, "— pick a vault secret —"),
|
|
35
|
+
secrets.map((s) => h("option", { key: s, value: s }, s)),
|
|
36
|
+
key && secrets.indexOf(key) < 0 ? h("option", { key: key, value: key }, key + " (missing)") : null);
|
|
37
|
+
const envInput = h("input", { className: inputCls, value: key, placeholder: "ENV_VAR_NAME", onChange: (e) => set({ key: e.target.value }) });
|
|
38
|
+
return h(Field, { key: field.field, label: "Secret · " + (field.label || field.field) + (field.required === false ? " (optional)" : "") },
|
|
39
|
+
h("div", { className: "flex gap-2" },
|
|
40
|
+
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 }) },
|
|
41
|
+
h("option", { value: "vault" }, "vault"),
|
|
42
|
+
h("option", { value: "env" }, "env")),
|
|
43
|
+
h("div", { className: "flex-1" }, src === "env" ? envInput : vaultPicker)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The Test flow: sends a REAL email to an operator-supplied address.
|
|
47
|
+
function TestModal({ state, setState }) {
|
|
48
|
+
const [to, setTo] = React.useState("");
|
|
49
|
+
if (!state) return null;
|
|
50
|
+
const result = state.result;
|
|
51
|
+
function run() {
|
|
52
|
+
setState({ payload: state.payload, busy: true });
|
|
53
|
+
api.call("POST", "/email/test", { ...state.payload, to })
|
|
54
|
+
.then((r) => setState({ payload: state.payload, result: r }))
|
|
55
|
+
.catch((e) => setState({ payload: state.payload, result: { ok: false, detail: String(e) } }));
|
|
56
|
+
}
|
|
57
|
+
return h(Modal, { open: true, onClose: () => setState(null), title: "Send a test email" },
|
|
58
|
+
h("div", { className: "space-y-3" },
|
|
59
|
+
h("p", { className: "text-xs text-muted" },
|
|
60
|
+
"Sends a REAL email through this account so you can verify end-to-end delivery — driver, credentials, From address and inbox placement in one go."),
|
|
61
|
+
h(Field, { label: "Send to" },
|
|
62
|
+
h("input", { className: inputCls, type: "email", value: to, placeholder: "you@example.com", onChange: (e) => setTo(e.target.value) })),
|
|
63
|
+
h("div", { className: "flex items-center gap-3" },
|
|
64
|
+
h(NeonButton, { onClick: run, disabled: state.busy || !to }, state.busy ? "Sending…" : "Send test email")),
|
|
65
|
+
result && h("div", { className: "space-y-2" },
|
|
66
|
+
h("p", { className: "text-sm font-medium", style: { color: result.ok ? "var(--color-neon-lime)" : "var(--color-neon-pink)" } },
|
|
67
|
+
result.ok ? "✓ Sent — check the inbox." + (result.messageId ? " (id " + result.messageId + ")" : "") : "✗ Failed"),
|
|
68
|
+
!result.ok && h("pre", { className: "glass rounded-lg p-3 text-xs whitespace-pre-wrap", style: { maxHeight: "16rem", overflow: "auto" } }, result.detail || "unknown error"))));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The form that creates/edits one account.
|
|
72
|
+
function AccountForm({ providers, secrets, form, setForm, reload, onTest }) {
|
|
73
|
+
const [busy, setBusy] = React.useState(false);
|
|
74
|
+
const blank = { name: "", provider: (providers[0] && providers[0].id) || "", from: "", secrets: {}, options: {} };
|
|
75
|
+
const spec = providers.find((p) => p.id === form.provider) || providers[0] || { secrets: [], options: [] };
|
|
76
|
+
|
|
77
|
+
function pickProvider(id) { setForm({ ...form, provider: id, secrets: {}, options: {} }); }
|
|
78
|
+
function setSecret(field, refValue) { setForm({ ...form, secrets: { ...form.secrets, [field]: refValue } }); }
|
|
79
|
+
function setOption(field, val) { setForm({ ...form, options: { ...form.options, [field]: val } }); }
|
|
80
|
+
|
|
81
|
+
// Drop empty secret rows so we never persist a sourceless reference.
|
|
82
|
+
function payload() {
|
|
83
|
+
const s = {};
|
|
84
|
+
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 }; }
|
|
85
|
+
const provider = form.provider || (spec && spec.id) || "";
|
|
86
|
+
return { name: form.name, provider, from: form.from, secrets: s, options: form.options };
|
|
87
|
+
}
|
|
88
|
+
function save() {
|
|
89
|
+
setBusy(true);
|
|
90
|
+
api.call("POST", "/email/accounts", payload()).then(() => { setForm(blank); reload(); }).finally(() => setBusy(false));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return h(GlassPanel, { className: "p-6 space-y-4" },
|
|
94
|
+
h("div", null,
|
|
95
|
+
h("h3", { className: "font-semibold" }, form.name ? "Edit account" : "New account"),
|
|
96
|
+
h("p", { className: "text-xs text-muted mt-1" },
|
|
97
|
+
"An account is a named sender — a driver, a From address and the credentials it needs. Wire one with an ",
|
|
98
|
+
h("span", { className: "font-mono" }, "email.account"),
|
|
99
|
+
" node, or leave a node's account unset and the ", h("span", { className: "font-mono" }, "default"),
|
|
100
|
+
" account is used. The packaged sign-in delivery workflow emails magic links the moment a default account exists.")),
|
|
101
|
+
h("div", { className: "space-y-3" },
|
|
102
|
+
h(Field, { label: "Account name", hint: 'the "default" account powers sign-in link delivery.' },
|
|
103
|
+
h("input", { className: inputCls, value: form.name, onChange: (e) => setForm({ ...form, name: e.target.value }), placeholder: "default" })),
|
|
104
|
+
h(Field, { label: "Driver" },
|
|
105
|
+
h("select", { className: inputCls, value: form.provider || (spec && spec.id) || "", onChange: (e) => pickProvider(e.target.value) },
|
|
106
|
+
providers.map((p) => h("option", { key: p.id, value: p.id }, p.label)))),
|
|
107
|
+
h(Field, { label: "From", hint: "the default sender; email.send's from input overrides per message." },
|
|
108
|
+
h("input", { className: inputCls, value: form.from, onChange: (e) => setForm({ ...form, from: e.target.value }), placeholder: "App <hello@example.com>" })),
|
|
109
|
+
((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) })),
|
|
110
|
+
((spec && spec.options) || []).map((f) => h(Field, { key: f.field, label: (f.label || f.field) + (f.required ? "" : " (optional)") },
|
|
111
|
+
h("input", { className: inputCls, value: form.options[f.field] || "", placeholder: f.placeholder || "", onChange: (e) => setOption(f.field, e.target.value) }))),
|
|
112
|
+
h("div", { className: "flex items-center gap-3 pt-1" },
|
|
113
|
+
h(NeonButton, { onClick: save, disabled: busy || !form.name || !form.from }, "Save account"),
|
|
114
|
+
h(NeonButton, { onClick: () => onTest(payload()), disabled: busy || !form.from }, "Test"),
|
|
115
|
+
form.name && h(NeonButton, { onClick: () => setForm(blank) }, "New")),
|
|
116
|
+
h("p", { className: "text-xs text-muted" }, "Secrets come from the vault (System → Secrets) or an env var — pick the source per field.")));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The list of saved accounts (click to edit, delete).
|
|
120
|
+
function AccountList({ accounts, onEdit, reload }) {
|
|
121
|
+
function del(name) { api.call("DELETE", "/email/accounts/" + encodeURIComponent(name)).then(reload); }
|
|
122
|
+
return h(GlassPanel, { className: "p-6 space-y-3" },
|
|
123
|
+
h("div", { className: "flex items-center justify-between" },
|
|
124
|
+
h("h3", { className: "font-semibold" }, "Accounts"),
|
|
125
|
+
h("span", { className: "text-xs text-muted" }, accounts.length + " configured")),
|
|
126
|
+
accounts.length === 0
|
|
127
|
+
? h("p", { className: "text-sm text-muted" }, 'No accounts yet. Create one named "default" and sign-in links start emailing automatically.')
|
|
128
|
+
: h("div", { className: "space-y-1" }, accounts.map((a) =>
|
|
129
|
+
h("div", { key: a.name, className: "flex items-center justify-between rounded-lg px-2 py-1.5 hover:bg-white/5" },
|
|
130
|
+
h("button", { className: "text-left text-sm flex-1 min-w-0", onClick: () => onEdit(a) },
|
|
131
|
+
h(Badge, { hue: a.name === "default" ? 140 : 200 }, a.name), " ",
|
|
132
|
+
h("span", { className: "font-mono text-xs text-muted" }, a.provider + " · " + a.from)),
|
|
133
|
+
h("button", { className: "text-xs text-muted hover:text-[var(--color-neon-pink)] shrink-0", onClick: () => del(a.name) }, "Delete")))));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default function EmailPage() {
|
|
137
|
+
const blank = { name: "", provider: "", from: "", secrets: {}, options: {} };
|
|
138
|
+
const [providers, setProviders] = React.useState([]);
|
|
139
|
+
const [secrets, setSecrets] = React.useState([]);
|
|
140
|
+
const [accounts, setAccounts] = React.useState([]);
|
|
141
|
+
const [form, setForm] = React.useState(blank);
|
|
142
|
+
const [test, setTest] = React.useState(null);
|
|
143
|
+
|
|
144
|
+
const reloadAccounts = () => api.call("GET", "/email/accounts").then((r) => setAccounts(arr(r, "accounts")));
|
|
145
|
+
|
|
146
|
+
React.useEffect(() => {
|
|
147
|
+
api.call("GET", "/email/providers").then((r) => setProviders(arr(r, "providers"))).catch(() => {});
|
|
148
|
+
api.call("GET", "/vault/secrets").then((r) => setSecrets(arr(r, "secrets").map((s) => (typeof s === "string" ? s : s.name)))).catch(() => {});
|
|
149
|
+
reloadAccounts().catch(() => {});
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
function edit(a) { setForm({ name: a.name, provider: a.provider, from: a.from, secrets: { ...(a.secrets || {}) }, options: { ...(a.options || {}) } }); }
|
|
153
|
+
function reloadAndReset() { reloadAccounts(); setForm(blank); }
|
|
154
|
+
|
|
155
|
+
if (providers.length === 0) {
|
|
156
|
+
return h(GlassPanel, { className: "p-6 space-y-2" },
|
|
157
|
+
h("h3", { className: "font-semibold" }, "No email drivers installed"),
|
|
158
|
+
h("p", { className: "text-sm text-muted" },
|
|
159
|
+
"mod-email is the contract; a driver mod does the sending. Install ",
|
|
160
|
+
h("span", { className: "font-mono" }, "@pattern-js/mod-email-resend"),
|
|
161
|
+
" or ", h("span", { className: "font-mono" }, "@pattern-js/mod-email-smtp"),
|
|
162
|
+
" and list it in pattern.config.json, then reload this page."));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return h("div", null,
|
|
166
|
+
h("div", { className: "grid gap-6 lg:grid-cols-2" },
|
|
167
|
+
h(AccountForm, { providers, secrets, form, setForm, reload: reloadAndReset, onTest: (payload) => setTest({ payload }) }),
|
|
168
|
+
h(AccountList, { accounts, onEdit: edit, reload: reloadAccounts })),
|
|
169
|
+
h(TestModal, { state: test, setState: setTest }));
|
|
170
|
+
}
|
|
171
|
+
`;
|
|
172
|
+
//# sourceMappingURL=app.js.map
|
package/dist/app.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+JrB,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-email — persisted settings: email accounts.
|
|
3
|
+
*
|
|
4
|
+
* Provider CREDENTIALS live in the vault (mod-vault's Secrets screen) or in
|
|
5
|
+
* env vars. What mod-email persists is a flat list of **accounts** — memorable
|
|
6
|
+
* names ("default", "alerts", …), each a fully self-contained sending handle
|
|
7
|
+
* (driver + from + sourced secrets + options), resolved by `email.account`.
|
|
8
|
+
* The packaged delivery workflow falls back to the "default" account. Stored
|
|
9
|
+
* as a small JSON file under the runtime data dir; in-memory on a read-only fs.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "@pattern-js/core";
|
|
12
|
+
import { type EmailAccount, type EmailAccountRef } from "./types.js";
|
|
13
|
+
export declare const DEFAULT_ACCOUNT = "default";
|
|
14
|
+
export declare const emailSettingsSchema: z.ZodObject<{
|
|
15
|
+
accounts: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
16
|
+
name: z.ZodString;
|
|
17
|
+
provider: z.ZodString;
|
|
18
|
+
from: z.ZodString;
|
|
19
|
+
secrets: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
20
|
+
source: z.ZodDefault<z.ZodEnum<{
|
|
21
|
+
vault: "vault";
|
|
22
|
+
env: "env";
|
|
23
|
+
}>>;
|
|
24
|
+
key: z.ZodString;
|
|
25
|
+
}, z.core.$strip>>>;
|
|
26
|
+
options: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
27
|
+
}, z.core.$strip>>>;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
export type EmailSettings = z.infer<typeof emailSettingsSchema>;
|
|
30
|
+
export declare class EmailConfigService {
|
|
31
|
+
private readonly path;
|
|
32
|
+
private settings;
|
|
33
|
+
private loaded;
|
|
34
|
+
constructor(path?: string);
|
|
35
|
+
load(): Promise<void>;
|
|
36
|
+
get(): EmailSettings;
|
|
37
|
+
accounts(): EmailAccount[];
|
|
38
|
+
account(name: string): EmailAccount | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve an account to an EmailAccountRef. The ref carries the account NAME
|
|
41
|
+
* so the send path re-resolves the (sourced) secrets + options at run time —
|
|
42
|
+
* keeping re-pointing dynamic and secret VALUES out of the value that flows
|
|
43
|
+
* on edges. `provider` is filled for display + validation.
|
|
44
|
+
*/
|
|
45
|
+
resolveAccount(name: string): EmailAccountRef | undefined;
|
|
46
|
+
/** The account the delivery workflow (and tools) fall back to: "default". */
|
|
47
|
+
defaultAccount(): EmailAccountRef | undefined;
|
|
48
|
+
upsertAccount(a: EmailAccount): Promise<void>;
|
|
49
|
+
deleteAccount(name: string): Promise<void>;
|
|
50
|
+
private write;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,CAAC,EAAE,MAAM,kBAAkB,CAAC;AACrC,OAAO,EAAiB,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAEpF,eAAO,MAAM,eAAe,YAAY,CAAC;AAEzC,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;iBAE9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,qBAAa,kBAAkB;IAIjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,MAAM,CAAS;gBAEM,IAAI,SAAoC;IAE/D,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAW3B,GAAG,IAAI,aAAa;IAIpB,QAAQ,IAAI,YAAY,EAAE;IAI1B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAI/C;;;;;OAKG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAMzD,6EAA6E;IAC7E,cAAc,IAAI,eAAe,GAAG,SAAS;IAMvC,aAAa,CAAC,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAO7C,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAIlC,KAAK;CASpB"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-email — persisted settings: email accounts.
|
|
3
|
+
*
|
|
4
|
+
* Provider CREDENTIALS live in the vault (mod-vault's Secrets screen) or in
|
|
5
|
+
* env vars. What mod-email persists is a flat list of **accounts** — memorable
|
|
6
|
+
* names ("default", "alerts", …), each a fully self-contained sending handle
|
|
7
|
+
* (driver + from + sourced secrets + options), resolved by `email.account`.
|
|
8
|
+
* The packaged delivery workflow falls back to the "default" account. Stored
|
|
9
|
+
* as a small JSON file under the runtime data dir; in-memory on a read-only fs.
|
|
10
|
+
*/
|
|
11
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
import { z } from "@pattern-js/core";
|
|
14
|
+
import { accountSchema } from "./types.js";
|
|
15
|
+
export const DEFAULT_ACCOUNT = "default";
|
|
16
|
+
export const emailSettingsSchema = z.object({
|
|
17
|
+
accounts: z.array(accountSchema).default([]),
|
|
18
|
+
});
|
|
19
|
+
export class EmailConfigService {
|
|
20
|
+
path;
|
|
21
|
+
settings = { accounts: [] };
|
|
22
|
+
loaded = false;
|
|
23
|
+
constructor(path = ".pattern-data/email-config.json") {
|
|
24
|
+
this.path = path;
|
|
25
|
+
}
|
|
26
|
+
async load() {
|
|
27
|
+
if (this.loaded)
|
|
28
|
+
return;
|
|
29
|
+
this.loaded = true;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = emailSettingsSchema.safeParse(JSON.parse(await readFile(this.path, "utf8")));
|
|
32
|
+
if (parsed.success)
|
|
33
|
+
this.settings = parsed.data;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* no file yet — defaults */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
get() {
|
|
40
|
+
return this.settings;
|
|
41
|
+
}
|
|
42
|
+
accounts() {
|
|
43
|
+
return this.settings.accounts;
|
|
44
|
+
}
|
|
45
|
+
account(name) {
|
|
46
|
+
return this.settings.accounts.find((a) => a.name === name);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve an account to an EmailAccountRef. The ref carries the account NAME
|
|
50
|
+
* so the send path re-resolves the (sourced) secrets + options at run time —
|
|
51
|
+
* keeping re-pointing dynamic and secret VALUES out of the value that flows
|
|
52
|
+
* on edges. `provider` is filled for display + validation.
|
|
53
|
+
*/
|
|
54
|
+
resolveAccount(name) {
|
|
55
|
+
const a = this.account(name);
|
|
56
|
+
if (!a)
|
|
57
|
+
return undefined;
|
|
58
|
+
return { kind: "emailAccount", account: a.name, provider: a.provider };
|
|
59
|
+
}
|
|
60
|
+
/** The account the delivery workflow (and tools) fall back to: "default". */
|
|
61
|
+
defaultAccount() {
|
|
62
|
+
return this.resolveAccount(DEFAULT_ACCOUNT);
|
|
63
|
+
}
|
|
64
|
+
// ── Mutations (upsert by name; each persists) ──
|
|
65
|
+
async upsertAccount(a) {
|
|
66
|
+
const parsed = accountSchema.parse(a);
|
|
67
|
+
const next = this.settings.accounts.filter((x) => x.name !== parsed.name);
|
|
68
|
+
next.push(parsed);
|
|
69
|
+
await this.write({ ...this.settings, accounts: next });
|
|
70
|
+
}
|
|
71
|
+
async deleteAccount(name) {
|
|
72
|
+
await this.write({ ...this.settings, accounts: this.settings.accounts.filter((a) => a.name !== name) });
|
|
73
|
+
}
|
|
74
|
+
async write(next) {
|
|
75
|
+
this.settings = emailSettingsSchema.parse(next);
|
|
76
|
+
try {
|
|
77
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
78
|
+
await writeFile(this.path, JSON.stringify(this.settings, null, 2));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* read-only fs — keep the value in memory for this process */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-email — the packaged sign-in delivery workflow.
|
|
3
|
+
*
|
|
4
|
+
* Subscribed to mod-identity's `identity.deliverToken` hook. The graph probes
|
|
5
|
+
* for a "default" account (`email.account` with required:false) and branches:
|
|
6
|
+
*
|
|
7
|
+
* - configured (and nobody upstream already delivered — a custom subscriber
|
|
8
|
+
* at a lower priority may have; double-sending would be rude) → compose
|
|
9
|
+
* subject/body from the payload, `email.send`, then flip `delivered:true`
|
|
10
|
+
* — strictly AFTER the send succeeded (the flag node is control-gated on
|
|
11
|
+
* the send's completion pulse).
|
|
12
|
+
* - otherwise → return the payload untouched, so identity's console
|
|
13
|
+
* fallback prints the link with no warning. Installing mod-email changes
|
|
14
|
+
* nothing until an operator creates the account; then links start sending.
|
|
15
|
+
*
|
|
16
|
+
* A broken account (bad key, driver down) makes `email.send` throw → the hook
|
|
17
|
+
* chain fails fast → identity logs a warning and still prints the console
|
|
18
|
+
* link, so the operator is never locked out.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately `internal: false`: this is a user-meaningful workflow. Open it
|
|
21
|
+
* in the admin editor to reword the email (the two `core.string.template`
|
|
22
|
+
* nodes), or fork it and subscribe your own.
|
|
23
|
+
*/
|
|
24
|
+
import type { Workflow } from "@pattern-js/core";
|
|
25
|
+
export declare function deliverTokenWorkflow(): Workflow;
|
|
26
|
+
//# sourceMappingURL=delivery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery.d.ts","sourceRoot":"","sources":["../src/delivery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,wBAAgB,oBAAoB,IAAI,QAAQ,CA4D/C"}
|
package/dist/delivery.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-email — the packaged sign-in delivery workflow.
|
|
3
|
+
*
|
|
4
|
+
* Subscribed to mod-identity's `identity.deliverToken` hook. The graph probes
|
|
5
|
+
* for a "default" account (`email.account` with required:false) and branches:
|
|
6
|
+
*
|
|
7
|
+
* - configured (and nobody upstream already delivered — a custom subscriber
|
|
8
|
+
* at a lower priority may have; double-sending would be rude) → compose
|
|
9
|
+
* subject/body from the payload, `email.send`, then flip `delivered:true`
|
|
10
|
+
* — strictly AFTER the send succeeded (the flag node is control-gated on
|
|
11
|
+
* the send's completion pulse).
|
|
12
|
+
* - otherwise → return the payload untouched, so identity's console
|
|
13
|
+
* fallback prints the link with no warning. Installing mod-email changes
|
|
14
|
+
* nothing until an operator creates the account; then links start sending.
|
|
15
|
+
*
|
|
16
|
+
* A broken account (bad key, driver down) makes `email.send` throw → the hook
|
|
17
|
+
* chain fails fast → identity logs a warning and still prints the console
|
|
18
|
+
* link, so the operator is never locked out.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately `internal: false`: this is a user-meaningful workflow. Open it
|
|
21
|
+
* in the admin editor to reword the email (the two `core.string.template`
|
|
22
|
+
* nodes), or fork it and subscribe your own.
|
|
23
|
+
*/
|
|
24
|
+
export function deliverTokenWorkflow() {
|
|
25
|
+
return {
|
|
26
|
+
id: "email.deliver-token",
|
|
27
|
+
name: "Email · deliver sign-in links (identity.deliverToken)",
|
|
28
|
+
description: 'Emails identity sign-in links through the "default" email account; leaves the payload ' +
|
|
29
|
+
"untouched (console fallback) while no account is configured.",
|
|
30
|
+
source: "code",
|
|
31
|
+
internal: false,
|
|
32
|
+
nodes: [
|
|
33
|
+
{ id: "in", op: "boundary.hook", config: { hook: "identity.deliverToken" } },
|
|
34
|
+
{ id: "acct", op: "email.account", config: { account: "default", required: false } },
|
|
35
|
+
{ id: "del", op: "core.object.get", config: { path: "delivered" } },
|
|
36
|
+
{ id: "fresh", op: "core.bool.not" },
|
|
37
|
+
{ id: "should", op: "core.bool.and" },
|
|
38
|
+
{ id: "gate", op: "core.flow.branch" },
|
|
39
|
+
{ id: "to", op: "core.object.get", config: { path: "email" } },
|
|
40
|
+
{ id: "subj", op: "core.string.template", config: { template: "Your {{purpose}} link" } },
|
|
41
|
+
{
|
|
42
|
+
id: "body",
|
|
43
|
+
op: "core.string.template",
|
|
44
|
+
config: {
|
|
45
|
+
template: "# Your {{purpose}} link\n\nClick the button to continue:\n\n{{url}}\n\n" +
|
|
46
|
+
"This link is single-use and expires soon. If you didn't request it, you can safely ignore this email.",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{ id: "send", op: "email.send" },
|
|
50
|
+
{ id: "flag", op: "core.const.boolean", config: { value: true } },
|
|
51
|
+
{ id: "set", op: "core.object.set", config: { path: "delivered" } },
|
|
52
|
+
{ id: "ret1", op: "boundary.hook.return" },
|
|
53
|
+
{ id: "ret2", op: "boundary.hook.return" },
|
|
54
|
+
],
|
|
55
|
+
edges: [
|
|
56
|
+
// The probe + "nobody already delivered" drive the branch.
|
|
57
|
+
{ from: { node: "in", port: "payload" }, to: { node: "del", port: "object" } },
|
|
58
|
+
{ from: { node: "del", port: "out" }, to: { node: "fresh", port: "a" } },
|
|
59
|
+
{ from: { node: "acct", port: "configured" }, to: { node: "should", port: "a" } },
|
|
60
|
+
{ from: { node: "fresh", port: "out" }, to: { node: "should", port: "b" } },
|
|
61
|
+
{ from: { node: "should", port: "out" }, to: { node: "gate", port: "condition" } },
|
|
62
|
+
// Compose recipient / subject / body from the hook payload.
|
|
63
|
+
{ from: { node: "in", port: "payload" }, to: { node: "to", port: "object" } },
|
|
64
|
+
{ from: { node: "in", port: "payload" }, to: { node: "subj", port: "data" } },
|
|
65
|
+
{ from: { node: "in", port: "payload" }, to: { node: "body", port: "data" } },
|
|
66
|
+
// The send — only when the branch pulses `then`.
|
|
67
|
+
{ from: { node: "acct", port: "account" }, to: { node: "send", port: "account" } },
|
|
68
|
+
{ from: { node: "to", port: "out" }, to: { node: "send", port: "to" } },
|
|
69
|
+
{ from: { node: "subj", port: "out" }, to: { node: "send", port: "subject" } },
|
|
70
|
+
{ from: { node: "body", port: "out" }, to: { node: "send", port: "markdown" } },
|
|
71
|
+
{ from: { node: "gate", port: "then" }, to: { node: "send", port: "in" } },
|
|
72
|
+
// delivered=true, control-gated on the send's completion pulse.
|
|
73
|
+
{ from: { node: "in", port: "payload" }, to: { node: "set", port: "object" } },
|
|
74
|
+
{ from: { node: "flag", port: "out" }, to: { node: "set", port: "value" } },
|
|
75
|
+
{ from: { node: "send", port: "out" }, to: { node: "set", port: "in" } },
|
|
76
|
+
{ from: { node: "set", port: "out" }, to: { node: "ret1", port: "payload" } },
|
|
77
|
+
// Not configured → the payload passes through unchanged.
|
|
78
|
+
{ from: { node: "in", port: "payload" }, to: { node: "ret2", port: "payload" } },
|
|
79
|
+
{ from: { node: "gate", port: "else" }, to: { node: "ret2", port: "in" } },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=delivery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery.js","sourceRoot":"","sources":["../src/delivery.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-email — public surface.
|
|
3
|
+
*
|
|
4
|
+
* Driver mods import the service key + spec types; apps import `emailMod` (or
|
|
5
|
+
* just list the package in pattern.config.json for the defaults).
|
|
6
|
+
*/
|
|
7
|
+
export { emailMod, type EmailModOptions } from "./mod.js";
|
|
8
|
+
export { default } from "./mod.js";
|
|
9
|
+
export { EMAIL_SERVICE, EMAIL_CONFIG_SERVICE, VAULT_SERVICE_KEY, STORE_SERVICE_KEY } from "./well-known.js";
|
|
10
|
+
export type { VaultLike, BlobStoreLike } from "./well-known.js";
|
|
11
|
+
export { EmailConfigService, DEFAULT_ACCOUNT, emailSettingsSchema, type EmailSettings } from "./config.js";
|
|
12
|
+
export { DefaultEmailService, type EmailService, type SendResult, type TestResult } from "./service.js";
|
|
13
|
+
export { renderEmailMarkdown } from "./markdown.js";
|
|
14
|
+
export { deliverTokenWorkflow } from "./delivery.js";
|
|
15
|
+
export { secretRefSchema, accountSchema, accountRefSchema, attachmentInputSchema, type SecretRef, type EmailAccount, type EmailAccountRef, type AttachmentInput, type EmailAttachment, type EmailMessage, type EmailDriverSpec, type EmailDriverInfo, type SendInput, } from "./types.js";
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC5G,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3G,OAAO,EAAE,mBAAmB,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AACxG,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD,OAAO,EACL,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,SAAS,GACf,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-email — public surface.
|
|
3
|
+
*
|
|
4
|
+
* Driver mods import the service key + spec types; apps import `emailMod` (or
|
|
5
|
+
* just list the package in pattern.config.json for the defaults).
|
|
6
|
+
*/
|
|
7
|
+
export { emailMod } from "./mod.js";
|
|
8
|
+
export { default } from "./mod.js";
|
|
9
|
+
export { EMAIL_SERVICE, EMAIL_CONFIG_SERVICE, VAULT_SERVICE_KEY, STORE_SERVICE_KEY } from "./well-known.js";
|
|
10
|
+
export { EmailConfigService, DEFAULT_ACCOUNT, emailSettingsSchema } from "./config.js";
|
|
11
|
+
export { DefaultEmailService } from "./service.js";
|
|
12
|
+
export { renderEmailMarkdown } from "./markdown.js";
|
|
13
|
+
export { deliverTokenWorkflow } from "./delivery.js";
|
|
14
|
+
export { secretRefSchema, accountSchema, accountRefSchema, attachmentInputSchema, } from "./types.js";
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAwB,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAG5G,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,mBAAmB,EAAsB,MAAM,aAAa,CAAC;AAC3G,OAAO,EAAE,mBAAmB,EAAuD,MAAM,cAAc,CAAC;AACxG,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD,OAAO,EACL,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,qBAAqB,GAUtB,MAAM,YAAY,CAAC"}
|