dkimkit 0.0.0-stage → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +153 -2
  2. package/dist/src/cli.d.ts +2 -0
  3. package/dist/src/cli.js +398 -0
  4. package/dist/src/cli.js.map +1 -0
  5. package/dist/src/dns-host.d.ts +10 -0
  6. package/dist/src/dns-host.js +31 -0
  7. package/dist/src/dns-host.js.map +1 -0
  8. package/dist/src/errors.d.ts +17 -0
  9. package/dist/src/errors.js +51 -0
  10. package/dist/src/errors.js.map +1 -0
  11. package/dist/src/hosted/api.d.ts +95 -0
  12. package/dist/src/hosted/api.js +781 -0
  13. package/dist/src/hosted/api.js.map +1 -0
  14. package/dist/src/hosted/auth.d.ts +70 -0
  15. package/dist/src/hosted/auth.js +121 -0
  16. package/dist/src/hosted/auth.js.map +1 -0
  17. package/dist/src/hosted/billing-dodo.d.ts +58 -0
  18. package/dist/src/hosted/billing-dodo.js +139 -0
  19. package/dist/src/hosted/billing-dodo.js.map +1 -0
  20. package/dist/src/hosted/billing-stripe.d.ts +82 -0
  21. package/dist/src/hosted/billing-stripe.js +205 -0
  22. package/dist/src/hosted/billing-stripe.js.map +1 -0
  23. package/dist/src/hosted/billing.d.ts +114 -0
  24. package/dist/src/hosted/billing.js +173 -0
  25. package/dist/src/hosted/billing.js.map +1 -0
  26. package/dist/src/hosted/cli.d.ts +2 -0
  27. package/dist/src/hosted/cli.js +305 -0
  28. package/dist/src/hosted/cli.js.map +1 -0
  29. package/dist/src/hosted/crypto.d.ts +22 -0
  30. package/dist/src/hosted/crypto.js +55 -0
  31. package/dist/src/hosted/crypto.js.map +1 -0
  32. package/dist/src/hosted/index.d.ts +7 -0
  33. package/dist/src/hosted/index.js +11 -0
  34. package/dist/src/hosted/index.js.map +1 -0
  35. package/dist/src/hosted/markdown.d.ts +16 -0
  36. package/dist/src/hosted/markdown.js +168 -0
  37. package/dist/src/hosted/markdown.js.map +1 -0
  38. package/dist/src/hosted/net.d.ts +9 -0
  39. package/dist/src/hosted/net.js +60 -0
  40. package/dist/src/hosted/net.js.map +1 -0
  41. package/dist/src/hosted/postgres-store.d.ts +62 -0
  42. package/dist/src/hosted/postgres-store.js +387 -0
  43. package/dist/src/hosted/postgres-store.js.map +1 -0
  44. package/dist/src/hosted/serialize.d.ts +144 -0
  45. package/dist/src/hosted/serialize.js +126 -0
  46. package/dist/src/hosted/serialize.js.map +1 -0
  47. package/dist/src/hosted/service.d.ts +166 -0
  48. package/dist/src/hosted/service.js +582 -0
  49. package/dist/src/hosted/service.js.map +1 -0
  50. package/dist/src/hosted/site.d.ts +54 -0
  51. package/dist/src/hosted/site.js +472 -0
  52. package/dist/src/hosted/site.js.map +1 -0
  53. package/dist/src/hosted/sqlite-store.d.ts +45 -0
  54. package/dist/src/hosted/sqlite-store.js +351 -0
  55. package/dist/src/hosted/sqlite-store.js.map +1 -0
  56. package/dist/src/hosted/store.d.ts +257 -0
  57. package/dist/src/hosted/store.js +181 -0
  58. package/dist/src/hosted/store.js.map +1 -0
  59. package/dist/src/hosted/widget.d.ts +46 -0
  60. package/dist/src/hosted/widget.js +130 -0
  61. package/dist/src/hosted/widget.js.map +1 -0
  62. package/dist/src/hosted/worker.d.ts +45 -0
  63. package/dist/src/hosted/worker.js +142 -0
  64. package/dist/src/hosted/worker.js.map +1 -0
  65. package/dist/src/http.d.ts +14 -0
  66. package/dist/src/http.js +54 -0
  67. package/dist/src/http.js.map +1 -0
  68. package/dist/src/index.d.ts +19 -0
  69. package/dist/src/index.js +19 -0
  70. package/dist/src/index.js.map +1 -0
  71. package/dist/src/orchestrator.d.ts +70 -0
  72. package/dist/src/orchestrator.js +69 -0
  73. package/dist/src/orchestrator.js.map +1 -0
  74. package/dist/src/propagation.d.ts +30 -0
  75. package/dist/src/propagation.js +76 -0
  76. package/dist/src/propagation.js.map +1 -0
  77. package/dist/src/providers/dns/aws.d.ts +28 -0
  78. package/dist/src/providers/dns/aws.js +103 -0
  79. package/dist/src/providers/dns/aws.js.map +1 -0
  80. package/dist/src/providers/dns/cloudflare.d.ts +26 -0
  81. package/dist/src/providers/dns/cloudflare.js +97 -0
  82. package/dist/src/providers/dns/cloudflare.js.map +1 -0
  83. package/dist/src/providers/dns/manual.d.ts +12 -0
  84. package/dist/src/providers/dns/manual.js +18 -0
  85. package/dist/src/providers/dns/manual.js.map +1 -0
  86. package/dist/src/providers/dns/route53.d.ts +34 -0
  87. package/dist/src/providers/dns/route53.js +176 -0
  88. package/dist/src/providers/dns/route53.js.map +1 -0
  89. package/dist/src/providers/email/factory.d.ts +29 -0
  90. package/dist/src/providers/email/factory.js +67 -0
  91. package/dist/src/providers/email/factory.js.map +1 -0
  92. package/dist/src/providers/email/mailgun.d.ts +36 -0
  93. package/dist/src/providers/email/mailgun.js +141 -0
  94. package/dist/src/providers/email/mailgun.js.map +1 -0
  95. package/dist/src/providers/email/postmark.d.ts +37 -0
  96. package/dist/src/providers/email/postmark.js +121 -0
  97. package/dist/src/providers/email/postmark.js.map +1 -0
  98. package/dist/src/providers/email/sendgrid.d.ts +31 -0
  99. package/dist/src/providers/email/sendgrid.js +109 -0
  100. package/dist/src/providers/email/sendgrid.js.map +1 -0
  101. package/dist/src/providers/email/ses.d.ts +39 -0
  102. package/dist/src/providers/email/ses.js +123 -0
  103. package/dist/src/providers/email/ses.js.map +1 -0
  104. package/dist/src/providers/factory.d.ts +14 -0
  105. package/dist/src/providers/factory.js +32 -0
  106. package/dist/src/providers/factory.js.map +1 -0
  107. package/dist/src/records.d.ts +35 -0
  108. package/dist/src/records.js +109 -0
  109. package/dist/src/records.js.map +1 -0
  110. package/dist/src/server.d.ts +67 -0
  111. package/dist/src/server.js +301 -0
  112. package/dist/src/server.js.map +1 -0
  113. package/dist/src/types.d.ts +75 -0
  114. package/dist/src/types.js +2 -0
  115. package/dist/src/types.js.map +1 -0
  116. package/dist/src/xml.d.ts +6 -0
  117. package/dist/src/xml.js +23 -0
  118. package/dist/src/xml.js.map +1 -0
  119. package/docs/billing.md +43 -0
  120. package/docs/getting-started.md +57 -0
  121. package/docs/hosted-api.md +192 -0
  122. package/docs/self-hosting.md +148 -0
  123. package/docs/widget.md +121 -0
  124. package/package.json +63 -4
  125. package/web/dashboard.html +390 -0
  126. package/web/index.html +697 -0
  127. package/web/widget-frame.html +480 -0
  128. package/web/widget.js +91 -0
@@ -0,0 +1,390 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Dashboard · __APP_NAME__</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f6f7f9; --surface: #ffffff; --surface-2: #f1f3f6; --border: #e2e5ea; --text: #15181d; --muted: #5d6573;
10
+ --accent: #3b5bdb; --accent-hover: #3249b8; --accent-soft: #e8edff; --on-accent: #ffffff;
11
+ --ok: #1f8a4c; --ok-soft: #e4f5ea; --warn: #9a5800; --warn-soft: #fcf0dc; --bad: #c4302b; --bad-soft: #fdeaea;
12
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --radius: 10px;
13
+ color-scheme: light;
14
+ }
15
+ @media (prefers-color-scheme: dark) {
16
+ :root:not([data-theme="light"]) {
17
+ --bg: #0f1115; --surface: #171a20; --surface-2: #1e222a; --border: #2a2f39; --text: #e8eaee; --muted: #9aa3b2;
18
+ --accent: #7b93ff; --accent-hover: #93a7ff; --accent-soft: #1f2645; --on-accent: #0b0f1f;
19
+ --ok: #4cc27e; --ok-soft: #14301f; --warn: #f0a640; --warn-soft: #33260f; --bad: #ff6b65; --bad-soft: #3a1717;
20
+ color-scheme: dark;
21
+ }
22
+ }
23
+ :root[data-theme="dark"] {
24
+ --bg: #0f1115; --surface: #171a20; --surface-2: #1e222a; --border: #2a2f39; --text: #e8eaee; --muted: #9aa3b2;
25
+ --accent: #7b93ff; --accent-hover: #93a7ff; --accent-soft: #1f2645; --on-accent: #0b0f1f;
26
+ --ok: #4cc27e; --ok-soft: #14301f; --warn: #f0a640; --warn-soft: #33260f; --bad: #ff6b65; --bad-soft: #3a1717;
27
+ color-scheme: dark;
28
+ }
29
+ * { box-sizing: border-box; }
30
+ body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
31
+ header { background: var(--surface); border-bottom: 1px solid var(--border); }
32
+ .bar { max-width: 960px; margin: 0 auto; padding: 12px 16px; display: flex; align-items: center; gap: 10px; }
33
+ .mark { width: 26px; height: 26px; border-radius: 7px; background: var(--accent); color: var(--on-accent); display: grid; place-items: center; flex: none; }
34
+ .bar h1 { font-size: 15px; margin: 0; font-weight: 600; }
35
+ .bar .who { margin-left: auto; display: flex; gap: 12px; align-items: center; font-size: 13px; color: var(--muted); min-width: 0; }
36
+ .bar .who span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
37
+ main { max-width: 960px; margin: 0 auto; padding: 24px 16px 64px; display: grid; gap: 16px; }
38
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; }
39
+ .card > h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin: 0 0 14px; font-weight: 600; }
40
+ .grid { display: grid; gap: 14px; }
41
+ .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
42
+ @media (max-width: 720px) { .cols { grid-template-columns: 1fr; } }
43
+ .row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
44
+ .muted { color: var(--muted); }
45
+ .small { font-size: 13px; }
46
+ label.field { display: grid; gap: 6px; font-size: 13px; font-weight: 500; }
47
+ label.field .hint { font-weight: 400; color: var(--muted); }
48
+ input, textarea { width: 100%; font: inherit; color: var(--text); background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 9px 11px; outline: none; }
49
+ textarea { font-family: var(--mono); font-size: 13px; min-height: 70px; resize: vertical; }
50
+ input:focus-visible, textarea:focus-visible { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
51
+ .btn { font: inherit; font-size: 14px; font-weight: 500; border-radius: 8px; padding: 9px 16px; cursor: pointer; border: 1px solid var(--border); background: var(--surface); color: var(--text); }
52
+ .btn:hover { background: var(--surface-2); }
53
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
54
+ .btn.primary:hover { background: var(--accent-hover); }
55
+ .btn:disabled { opacity: .55; cursor: default; }
56
+ .link { background: none; border: 0; padding: 0; color: var(--accent); font: inherit; font-size: 13px; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
57
+ .banner { border-radius: 8px; padding: 12px 14px; font-size: 14px; }
58
+ .banner.ok { background: var(--ok-soft); color: var(--ok); }
59
+ .banner.warn { background: var(--warn-soft); color: var(--warn); }
60
+ .banner.bad { background: var(--bad-soft); color: var(--bad); }
61
+ .badge { font-size: 12px; font-weight: 600; padding: 2px 10px; border-radius: 99px; background: var(--surface-2); color: var(--muted); }
62
+ .badge.paid { background: var(--accent-soft); color: var(--accent); }
63
+ .badge.bad { background: var(--bad-soft); color: var(--bad); }
64
+ .meter { height: 8px; border-radius: 99px; background: var(--surface-2); overflow: hidden; }
65
+ .meter > div { height: 100%; background: var(--accent); }
66
+ .meter.full > div { background: var(--warn); }
67
+ .big { font-size: 34px; font-weight: 650; line-height: 1.1; letter-spacing: -.01em; }
68
+ .stat { display: grid; gap: 2px; }
69
+ .stat b { font-size: 20px; font-weight: 600; }
70
+ .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
71
+ .plan { border: 1px solid var(--border); border-radius: var(--radius); padding: 18px; display: grid; gap: 10px; align-content: start; }
72
+ .plan.current { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
73
+ .plan h3 { margin: 0; font-size: 16px; display: flex; gap: 8px; align-items: center; }
74
+ .plan .price { font-size: 26px; font-weight: 650; }
75
+ .plan .price small { font-size: 13px; font-weight: 400; color: var(--muted); }
76
+ .plan ul { margin: 0; padding-left: 18px; display: grid; gap: 4px; font-size: 14px; }
77
+ code, .code { font-family: var(--mono); font-size: 13px; }
78
+ pre { margin: 0; font-family: var(--mono); font-size: 12.5px; background: var(--surface-2); border-radius: 8px; padding: 12px; overflow-x: auto; }
79
+ .secret { display: flex; gap: 8px; align-items: center; background: var(--surface-2); border-radius: 8px; padding: 8px 10px; }
80
+ .secret code { flex: 1; overflow-wrap: anywhere; }
81
+ .signin { max-width: 420px; margin: 8vh auto 0; }
82
+ .signin h2 { font-size: 24px; margin: 0 0 4px; text-transform: none; letter-spacing: 0; color: var(--text); }
83
+ [hidden] { display: none !important; }
84
+ </style>
85
+ </head>
86
+ <body>
87
+ <header>
88
+ <div class="bar">
89
+ <div class="mark" aria-hidden="true"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg></div>
90
+ <a href="/" style="display:flex;align-items:center;gap:10px;color:inherit;text-decoration:none"><h1>__APP_NAME__</h1></a>
91
+ <div class="who" id="who" hidden><span id="whoEmail"></span><button class="link" id="signOut" type="button">Sign out</button></div>
92
+ </div>
93
+ </header>
94
+ <main id="app" aria-live="polite"></main>
95
+
96
+ <script nonce="__NONCE__">
97
+ (() => {
98
+ const app = document.getElementById('app');
99
+ const params = new URLSearchParams(location.search);
100
+ let me = null;
101
+
102
+ function h(tag, attrs, ...children) {
103
+ const el = document.createElement(tag);
104
+ for (const [k, v] of Object.entries(attrs || {})) {
105
+ if (k.startsWith('on')) el.addEventListener(k.slice(2), v);
106
+ else if (k === 'class') el.className = v;
107
+ else if (v === true) el.setAttribute(k, '');
108
+ else if (v !== false && v != null) el.setAttribute(k, v);
109
+ }
110
+ for (const c of children.flat()) if (c != null && c !== false) el.append(c instanceof Node ? c : String(c));
111
+ return el;
112
+ }
113
+
114
+ async function api(method, path, body) {
115
+ const res = await fetch(path, {
116
+ method,
117
+ headers: body ? { 'content-type': 'application/json' } : {},
118
+ body: body ? JSON.stringify(body) : undefined,
119
+ });
120
+ const data = await res.json().catch(() => ({}));
121
+ if (!res.ok) {
122
+ const err = new Error(data.error?.message || 'Something went wrong. Please try again.');
123
+ err.status = res.status;
124
+ throw err;
125
+ }
126
+ return data;
127
+ }
128
+
129
+ const money = (cents, currency) =>
130
+ new Intl.NumberFormat(undefined, { style: 'currency', currency: (currency || 'usd').toUpperCase() }).format(cents / 100);
131
+ const dayLabel = (d) => d ? new Date(d + 'T00:00:00Z').toLocaleDateString(undefined, { month: 'short', day: 'numeric', timeZone: 'UTC' }) : '';
132
+
133
+ async function busy(button, label, fn) {
134
+ const original = button.textContent;
135
+ button.disabled = true;
136
+ button.textContent = label;
137
+ try { return await fn(); } finally { button.disabled = false; button.textContent = original; }
138
+ }
139
+
140
+ function copyButton(text) {
141
+ return h('button', { class: 'btn', type: 'button', onclick: async (e) => {
142
+ try { await navigator.clipboard.writeText(text); e.target.textContent = 'Copied'; } catch { e.target.textContent = 'Select & copy'; }
143
+ setTimeout(() => (e.target.textContent = 'Copy'), 1400);
144
+ } }, 'Copy');
145
+ }
146
+
147
+ // ---- signed out ----
148
+ function renderSignIn(sent) {
149
+ document.getElementById('who').hidden = true;
150
+ const email = h('input', { type: 'email', id: 'email', autocomplete: 'email', required: true, placeholder: 'you@company.com' });
151
+ const name = h('input', { type: 'text', id: 'name', autocomplete: 'organization', placeholder: 'Acme Inc. (optional)' });
152
+ const error = h('div', { class: 'banner bad', role: 'alert', hidden: true });
153
+ const submit = h('button', { class: 'btn primary', type: 'submit' }, 'Continue with email');
154
+ const form = h('form', { class: 'grid', onsubmit: async (e) => {
155
+ e.preventDefault();
156
+ error.hidden = true;
157
+ await busy(submit, 'Sending…', async () => {
158
+ try {
159
+ await api('POST', '/v1/auth/email-link', { email: email.value.trim(), name: name.value.trim() || undefined });
160
+ renderSignIn(email.value.trim());
161
+ } catch (err) {
162
+ error.textContent = err.message;
163
+ error.hidden = false;
164
+ }
165
+ });
166
+ } },
167
+ h('label', { class: 'field' }, 'Work email', email),
168
+ h('label', { class: 'field' }, h('span', {}, 'Company or project name ', h('span', { class: 'hint' }, '(for new accounts)')), name),
169
+ error, submit,
170
+ h('p', { class: 'small muted' }, 'No password needed. We email you a sign-in link. New here? The same link creates your account.'));
171
+
172
+ app.replaceChildren(h('div', { class: 'signin card grid' },
173
+ params.get('link') === 'expired' ? h('div', { class: 'banner warn' }, 'That sign-in link expired or was already used. Request a new one below.') : null,
174
+ sent
175
+ ? h('div', { class: 'grid' },
176
+ h('h2', {}, 'Check your inbox'),
177
+ h('p', { class: 'muted' }, `We sent a sign-in link to ${sent}. It works once and expires in 15 minutes.`),
178
+ h('button', { class: 'link', type: 'button', onclick: () => renderSignIn() }, 'Use a different email'))
179
+ : h('div', { class: 'grid' },
180
+ h('div', {}, h('h2', {}, 'Start free'), h('p', { class: 'muted', style: 'margin:0' }, 'Authenticate up to 3 sending domains for free. No credit card required.')),
181
+ form)));
182
+ if (!sent) email.focus();
183
+ }
184
+
185
+ // ---- signed in ----
186
+ function planCard() {
187
+ const b = me.billing;
188
+ if (!me.billing_enabled) {
189
+ return h('section', { class: 'card grid' }, h('h2', {}, 'Plan'), h('p', { class: 'muted' }, 'Billing is not enabled on this server.'));
190
+ }
191
+ const paid = b.plan === 'pay_as_you_go';
192
+ const badge = b.status === 'past_due' ? h('span', { class: 'badge bad' }, 'Payment issue') : h('span', { class: paid ? 'badge paid' : 'badge' }, paid ? 'Pay as you go' : 'Free');
193
+
194
+ const checkout = h('button', { class: 'btn primary', type: 'button', onclick: (e) => go(e.target, '/dashboard/api/checkout') }, 'Add payment method');
195
+ const portal = h('button', { class: paid && b.status !== 'past_due' ? 'btn' : 'btn primary', type: 'button', onclick: (e) => go(e.target, '/dashboard/api/portal') }, b.status === 'past_due' ? 'Update payment method' : 'Manage billing');
196
+
197
+ let body;
198
+ if (paid) {
199
+ body = h('div', { class: 'grid' },
200
+ h('div', { class: 'small muted' }, `This billing period · ${dayLabel(b.period_start)}${b.period_end ? ' – ' + dayLabel(b.period_end) : ''}`),
201
+ h('div', { class: 'row', style: 'align-items:baseline;gap:16px' },
202
+ h('div', {}, h('div', { class: 'big' }, money(b.amount_due, b.currency)), h('div', { class: 'small muted' }, 'estimated so far')),
203
+ h('div', { class: 'small muted' },
204
+ `${b.active_domains} active domain${b.active_domains === 1 ? '' : 's'}: first ${b.free_domains} free`,
205
+ b.billable_domains ? `, then ${b.billable_domains} × ${money(b.unit_amount, b.currency)}` : '')),
206
+ b.status === 'past_due' ? h('div', { class: 'banner bad' }, 'Your last payment failed. Update your payment method to keep your domains running smoothly.') : null,
207
+ h('div', { class: 'row' }, portal));
208
+ } else {
209
+ const used = me.usage.current_domains;
210
+ const pct = Math.min(100, Math.round((used / b.free_domains) * 100));
211
+ body = h('div', { class: 'grid' },
212
+ h('div', { class: 'row', style: 'justify-content:space-between' },
213
+ h('b', {}, `${used} of ${b.free_domains} free domains used`),
214
+ used >= b.free_domains ? h('span', { class: 'small', style: 'color:var(--warn)' }, 'Limit reached') : null),
215
+ h('div', { class: used >= b.free_domains ? 'meter full' : 'meter', role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': b.free_domains, 'aria-valuenow': used }, h('div', { style: `width:${pct}%` })),
216
+ h('p', { class: 'small muted', style: 'margin:0' },
217
+ `Need more? Add a payment method: your first ${b.free_domains} active domains stay free, then ${money(b.unit_amount, b.currency)} per active domain per month. Cancel anytime.`),
218
+ h('div', { class: 'row' }, checkout));
219
+ }
220
+ return h('section', { class: 'card grid' }, h('div', { class: 'row', style: 'justify-content:space-between' }, h('h2', { style: 'margin:0' }, 'Plan'), badge), body);
221
+ }
222
+
223
+ function pricingCard() {
224
+ if (!me.billing_enabled) return null;
225
+ const b = me.billing;
226
+ const paid = b.plan === 'pay_as_you_go';
227
+ const plan = (title, price, unit, items, current) => h('div', { class: current ? 'plan current' : 'plan' },
228
+ h('h3', {}, title, current ? h('span', { class: 'badge paid' }, 'Current') : null),
229
+ h('div', { class: 'price' }, price, h('small', {}, unit)),
230
+ h('ul', {}, items.map((i) => h('li', {}, i))));
231
+ return h('section', { class: 'card grid' }, h('h2', {}, 'Pricing'),
232
+ h('div', { class: 'cols' },
233
+ plan('Free', money(0, b.currency), ' forever', [`Up to ${b.free_domains} domains`, 'CLI, API and embeddable widget', 'Cloudflare, Route 53 and manual DNS', 'Webhooks and usage reports'], !paid),
234
+ plan('Pay as you go', money(b.unit_amount, b.currency), ' / active domain / month', [`First ${b.free_domains} active domains free`, 'Unlimited domains', 'Billed monthly for what you use', 'Cancel anytime from the billing portal'], paid)));
235
+ }
236
+
237
+ function apiKeyCard() {
238
+ const out = h('div', { class: 'grid' });
239
+ const create = h('button', { class: 'btn', type: 'button', onclick: async () => {
240
+ if (!confirm('Create a new API key? Your current key stops working immediately.')) return;
241
+ await busy(create, 'Creating…', async () => {
242
+ try {
243
+ const { api_key } = await api('POST', '/dashboard/api/api-key');
244
+ out.replaceChildren(
245
+ h('div', { class: 'banner warn' }, 'Copy this key now. It won\'t be shown again.'),
246
+ h('div', { class: 'secret' }, h('code', {}, api_key), copyButton(api_key)));
247
+ } catch (err) { out.replaceChildren(h('div', { class: 'banner bad' }, err.message)); }
248
+ });
249
+ } }, 'Create new API key');
250
+ return h('section', { class: 'card grid' }, h('h2', {}, 'API key'),
251
+ h('p', { class: 'small muted', style: 'margin:0' }, 'Use it as Authorization: Bearer <key> with the API. For security we only store a hash, so a key is shown once when created.'),
252
+ h('div', { class: 'row' }, create), out);
253
+ }
254
+
255
+ // Credential fields per email provider: [API field, label, hint, secret?]
256
+ const EMAIL_FIELDS = {
257
+ sendgrid: [['api_key', 'SendGrid API key', 'Sender Authentication and Inbound Parse access', true]],
258
+ postmark: [['account_token', 'Account API token', 'Account → API Tokens (not a server token)', true], ['inbound_server_id', 'Inbound server ID', 'only needed to receive email', false]],
259
+ mailgun: [['api_key', 'Mailgun API key', 'a private API key', true], ['region', 'Region', 'us or eu', false]],
260
+ ses: [['access_key_id', 'AWS access key ID', 'IAM user with SES access', false], ['secret_access_key', 'AWS secret access key', '', true], ['region', 'AWS region', 'e.g. us-east-1', false]],
261
+ };
262
+ const EMAIL_LABELS = { sendgrid: 'SendGrid', postmark: 'Postmark', mailgun: 'Mailgun', ses: 'Amazon SES' };
263
+
264
+ function settingsCard() {
265
+ const a = me.account;
266
+ const provider = h('select', { 'aria-label': 'Email provider', style: 'font:inherit;padding:9px 11px;border-radius:8px;border:1px solid var(--border);background:var(--surface);color:var(--text)' },
267
+ Object.entries(EMAIL_LABELS).map(([value, label]) => h('option', { value, selected: value === a.email_provider }, label)));
268
+ const fieldsBox = h('div', { class: 'grid' });
269
+ let inputs = {};
270
+ const renderFields = () => {
271
+ const p = provider.value;
272
+ const saved = p === a.email_provider && a.email_credentials_set;
273
+ inputs = {};
274
+ fieldsBox.replaceChildren(...EMAIL_FIELDS[p].map(([key, label, hint, secret]) => {
275
+ const input = h('input', { type: secret ? 'password' : 'text', autocomplete: 'off', spellcheck: 'false', placeholder: saved ? 'Saved. Leave blank to keep it' : '' });
276
+ inputs[key] = input;
277
+ return h('label', { class: 'field' }, h('span', {}, `${label} `, hint ? h('span', { class: 'hint' }, `(${hint})`) : null), input);
278
+ }), p === 'ses' ? h('p', { class: 'small muted', style: 'margin:0' }, 'Receiving email isn’t supported with Amazon SES yet.') : null,
279
+ p === 'mailgun' ? h('p', { class: 'small muted', style: 'margin:0' }, 'Mailgun recommends sending from a subdomain, like mg.yourcustomer.com.') : null);
280
+ };
281
+ provider.addEventListener('change', renderFields);
282
+ renderFields();
283
+
284
+ const hook = h('input', { type: 'url', value: a.webhook_url || '', placeholder: 'https://api.yourapp.com/webhooks/email-domains' });
285
+ const origins = h('textarea', { placeholder: 'https://app.yourapp.com' }, (a.widget_origins || []).join('\n'));
286
+ const status = h('div', { hidden: true });
287
+ const save = h('button', { class: 'btn primary', type: 'submit' }, 'Save settings');
288
+ const secret = h('code', {}, '•'.repeat(24));
289
+ return h('section', { class: 'card' }, h('h2', {}, 'Settings'),
290
+ h('form', { class: 'grid', onsubmit: async (e) => {
291
+ e.preventDefault();
292
+ const credentials = Object.fromEntries(Object.entries(inputs).map(([k, el]) => [k, el.value.trim()]).filter(([, v]) => v));
293
+ // Send provider settings only when something changed; blank fields keep saved values.
294
+ const providerChanged = provider.value !== a.email_provider;
295
+ const patch = {
296
+ webhook_url: hook.value.trim() || null,
297
+ widget_origins: origins.value.split(/\s+/).map((o) => o.trim()).filter(Boolean),
298
+ ...(providerChanged || Object.keys(credentials).length ? { email_provider: { provider: provider.value, ...credentials } } : {}),
299
+ };
300
+ await busy(save, 'Saving…', async () => {
301
+ try {
302
+ me = await api('PATCH', '/dashboard/api/account', patch);
303
+ render({ saved: true });
304
+ } catch (err) { status.className = 'banner bad'; status.textContent = err.message; status.hidden = false; }
305
+ });
306
+ } },
307
+ h('label', { class: 'field' }, h('span', {}, 'Email provider ', h('span', { class: 'hint' }, '(where your customers’ domains are authenticated)')), provider),
308
+ fieldsBox,
309
+ h('label', { class: 'field' }, h('span', {}, 'Webhook URL ', h('span', { class: 'hint' }, '(domain status events)')), hook),
310
+ h('div', { class: 'grid', style: 'gap:6px' }, h('span', { class: 'small', style: 'font-weight:500' }, 'Webhook signing secret'),
311
+ h('div', { class: 'secret' }, secret,
312
+ h('button', { class: 'btn', type: 'button', onclick: (e) => { secret.textContent = a.webhook_secret; e.target.remove(); } }, 'Reveal'),
313
+ copyButton(a.webhook_secret))),
314
+ h('label', { class: 'field' }, h('span', {}, 'Widget origins ', h('span', { class: 'hint' }, '(one per line; sites allowed to embed the setup widget)')), origins),
315
+ status, h('div', { class: 'row' }, save)));
316
+ }
317
+
318
+
319
+ function usageCard() {
320
+ const u = me.usage;
321
+ const stat = (n, label) => h('div', { class: 'stat' }, h('b', {}, String(n)), h('span', { class: 'small muted' }, label));
322
+ return h('section', { class: 'card grid' }, h('h2', {}, `Usage this month · ${dayLabel(u.from)} – ${dayLabel(u.to)}`),
323
+ h('div', { class: 'stats' }, stat(u.active_domains, 'active domains'), stat(u.domains_created, 'domains created'), stat(u.verifications, 'verifications')));
324
+ }
325
+
326
+ function quickstartCard() {
327
+ const base = location.origin;
328
+ const curl = `curl -X POST ${base}/v1/domains \\\n -H "Authorization: Bearer $API_KEY" \\\n -H "Content-Type: application/json" \\\n -d '{"domain": "yourcustomer.com", "dns": {"provider": "manual"}}'`;
329
+ return h('section', { class: 'card grid' }, h('h2', {}, 'Quickstart'),
330
+ h('p', { class: 'small muted', style: 'margin:0' }, 'Connect your email provider in Settings, create an API key, then register a domain:'),
331
+ h('pre', {}, curl), h('div', {}, copyButton(curl)));
332
+ }
333
+
334
+ function go(button, path) {
335
+ return busy(button, 'Opening…', async () => {
336
+ try {
337
+ const { url } = await api('POST', path);
338
+ location.assign(url);
339
+ } catch (err) { alert(err.message); }
340
+ });
341
+ }
342
+
343
+ function render(flags = {}) {
344
+ document.getElementById('who').hidden = false;
345
+ document.getElementById('whoEmail').textContent = me.email || me.account.name;
346
+ const banners = [];
347
+ if (params.get('welcome')) banners.push(h('div', { class: 'banner ok' }, 'Your account is ready. Connect your email provider in Settings and create an API key to get started.'));
348
+ if (params.get('billing') === 'return') {
349
+ // The provider returns here after success or failure; the webhook decides.
350
+ banners.push(me.billing?.plan === 'pay_as_you_go'
351
+ ? h('div', { class: 'banner ok' }, 'Payment method added. You\'re on pay as you go.')
352
+ : flags.checkoutPending
353
+ ? h('div', { class: 'banner ok' }, 'Checking your payment…')
354
+ : h('div', { class: 'banner warn' }, 'We haven\'t received a payment confirmation yet. If you completed checkout, refresh this page in a minute.'));
355
+ }
356
+ if (params.get('billing') === 'canceled') banners.push(h('div', { class: 'banner warn' }, 'Checkout canceled. Nothing was charged.'));
357
+ if (flags.saved) banners.push(h('div', { class: 'banner ok' }, 'Settings saved.'));
358
+ app.replaceChildren(...banners, planCard(), ...[pricingCard(), usageCard(), apiKeyCard(), settingsCard(), quickstartCard()].filter(Boolean));
359
+ }
360
+
361
+ async function load() {
362
+ try {
363
+ me = await api('GET', '/dashboard/api/me');
364
+ // After checkout, the provider's webhook may land a moment after the redirect.
365
+ const awaitingPayment = params.get('billing') === 'return' && me.billing?.plan !== 'pay_as_you_go';
366
+ render({ checkoutPending: awaitingPayment });
367
+ if (awaitingPayment) {
368
+ for (let i = 0; i < 10 && me.billing?.plan !== 'pay_as_you_go'; i++) {
369
+ await new Promise((r) => setTimeout(r, 1500));
370
+ me = await api('GET', '/dashboard/api/me');
371
+ }
372
+ render();
373
+ }
374
+ } catch (err) {
375
+ if (err.status === 401) renderSignIn();
376
+ else app.replaceChildren(h('div', { class: 'banner bad' }, err.message));
377
+ }
378
+ }
379
+
380
+ document.getElementById('signOut').addEventListener('click', async () => {
381
+ await api('POST', '/dashboard/api/logout').catch(() => {});
382
+ me = null;
383
+ history.replaceState(null, '', '/dashboard');
384
+ renderSignIn();
385
+ });
386
+ load();
387
+ })();
388
+ </script>
389
+ </body>
390
+ </html>