@volter/twin-postmark 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +144 -0
  3. package/client/postmark-mirror.css +79 -0
  4. package/client/postmark-mirror.tsx +221 -0
  5. package/dist/client/postmark-mirror.bundle.js +321 -0
  6. package/dist/client/postmark-mirror.css +79 -0
  7. package/dist/client/postmark-mirror.d.ts +18 -0
  8. package/dist/client/postmark-mirror.js +153 -0
  9. package/dist/client/postmark-mirror.tsx +221 -0
  10. package/dist/src/cli.d.ts +2 -0
  11. package/dist/src/cli.js +31 -0
  12. package/dist/src/index.d.ts +10 -0
  13. package/dist/src/index.js +54 -0
  14. package/dist/src/postmark-capabilities.d.ts +12 -0
  15. package/dist/src/postmark-capabilities.js +1502 -0
  16. package/dist/src/postmark-conformance.d.ts +33 -0
  17. package/dist/src/postmark-conformance.js +265 -0
  18. package/dist/src/postmark-connector.d.ts +167 -0
  19. package/dist/src/postmark-connector.js +251 -0
  20. package/dist/src/postmark-events.d.ts +85 -0
  21. package/dist/src/postmark-events.js +169 -0
  22. package/dist/src/postmark-mirror-ui.d.ts +58 -0
  23. package/dist/src/postmark-mirror-ui.js +207 -0
  24. package/dist/src/postmark-perform-harness.d.ts +9 -0
  25. package/dist/src/postmark-perform-harness.js +24 -0
  26. package/dist/src/postmark-server.d.ts +14 -0
  27. package/dist/src/postmark-server.js +29 -0
  28. package/dist/src/postmark-twin.d.ts +82 -0
  29. package/dist/src/postmark-twin.js +1575 -0
  30. package/dist/test-fixtures/postmark-swagger-operations.json +846 -0
  31. package/package.json +76 -0
  32. package/src/cli.ts +29 -0
  33. package/src/index.ts +89 -0
  34. package/src/postmark-capabilities.ts +1737 -0
  35. package/src/postmark-conformance.ts +282 -0
  36. package/src/postmark-connector.ts +312 -0
  37. package/src/postmark-events.ts +189 -0
  38. package/src/postmark-mirror-ui.ts +213 -0
  39. package/src/postmark-perform-harness.ts +21 -0
  40. package/src/postmark-server.ts +37 -0
  41. package/src/postmark-twin.ts +1520 -0
  42. package/test-fixtures/postmark-swagger-operations.json +846 -0
@@ -0,0 +1,189 @@
1
+ // Postmark WEBHOOK DELIVERY + the deterministic offline delivery plan.
2
+ //
3
+ // ⚠ FIDELITY NOTE — READ THIS BEFORE "ADDING SIGNATURES". Postmark webhooks are NOT signed.
4
+ // There is no `svix-signature`, no HMAC, no shared secret anywhere in Postmark's webhook
5
+ // design (this is a real difference from Resend/Clerk/Svix-backed vendors, and a twin that
6
+ // invented a signature scheme would be teaching consumers to verify something the vendor
7
+ // never sends). Postmark authenticates a webhook delivery the two ways its Webhooks API
8
+ // actually models — both of which this module reproduces:
9
+ // · `HttpAuth: { Username, Password }` → an HTTP Basic `Authorization` header, and
10
+ // · `HttpHeaders: [{ Name, Value }]` → arbitrary caller-chosen headers (the usual place
11
+ // a shared secret goes).
12
+ // Both are stored on the webhook resource by the Webhooks API (POST /webhooks), so the
13
+ // registry here is the twin's OWN projected state — not a parallel in-memory store.
14
+ //
15
+ // PAYLOAD. Each delivery is a plain JSON POST whose body carries a `RecordType` discriminator
16
+ // — `Delivery` | `Open` | `Click` | `Bounce` | `SpamComplaint` | `SubscriptionChange` |
17
+ // `Inbound` — matching the vendor's documented webhook payloads. A webhook only receives a
18
+ // record type whose `Triggers.<Type>.Enabled` is true, and only for its own MessageStream.
19
+ //
20
+ // DELIVERY PLAN. A sent email progresses through a DETERMINISTIC, OFFLINE outcome the twin
21
+ // drives — no SMTP, no network. `deliveryPlan()` is pure so the twin + its tests are
22
+ // repeatable.
23
+ import { projectResources } from '@volter/world-core';
24
+ import { worldEgressRefusal } from '@volter/world-core/network-policy';
25
+
26
+ const SERVICE = 'postmark';
27
+
28
+ /** Postmark's webhook `RecordType` discriminators (the outbound delivery-lifecycle family). */
29
+ export const POSTMARK_RECORD_TYPES = ['Delivery', 'Open', 'Click', 'Bounce', 'SpamComplaint', 'SubscriptionChange'] as const;
30
+ export type PostmarkRecordType = (typeof POSTMARK_RECORD_TYPES)[number];
31
+
32
+ /** RecordType → the `Triggers` key on the webhook resource that gates it. */
33
+ const TRIGGER_FOR: Record<PostmarkRecordType, string> = {
34
+ Delivery: 'Delivery', Open: 'Open', Click: 'Click',
35
+ Bounce: 'Bounce', SpamComplaint: 'SpamComplaint', SubscriptionChange: 'SubscriptionChange',
36
+ };
37
+
38
+ /** An injected webhook deliverer — a fake in tests, a real HTTP POST live. */
39
+ export type PostmarkWebhookDelivery = (url: string, body: string, headers: Record<string, string>) => Promise<void> | void;
40
+
41
+ /** What the twin needs to route a webhook: the root to read from + the injected deliverer. */
42
+ export type WebhookContext = { root?: string; deliver?: PostmarkWebhookDelivery };
43
+
44
+ function asObject(v: unknown): Record<string, unknown> {
45
+ return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {};
46
+ }
47
+
48
+ /**
49
+ * The webhooks registered on a stream whose trigger for `recordType` is enabled — read from
50
+ * the SAME projection the Webhooks API serves, so a webhook created through `POST /webhooks`
51
+ * is the one that fires (no parallel registry to drift).
52
+ */
53
+ export function webhooksFor(stream: string, recordType: PostmarkRecordType, root?: string): Array<Record<string, unknown>> {
54
+ return projectResources(SERVICE, root)
55
+ .filter((r) => r.type === 'webhook' && (r as Record<string, unknown>).deleted !== true)
56
+ .filter((w) => w.MessageStream === stream)
57
+ .filter((w) => asObject(asObject(w.Triggers)[TRIGGER_FOR[recordType]]).Enabled === true);
58
+ }
59
+
60
+ /**
61
+ * The headers Postmark sends with a webhook delivery: JSON content type, the webhook's own
62
+ * custom `HttpHeaders`, and an HTTP Basic `Authorization` header when `HttpAuth` is set.
63
+ * Pure — exported so a verify() can assert the auth/header wiring without any I/O.
64
+ */
65
+ export function webhookHeaders(webhook: Record<string, unknown>): Record<string, string> {
66
+ const headers: Record<string, string> = { 'content-type': 'application/json' };
67
+ for (const raw of Array.isArray(webhook.HttpHeaders) ? webhook.HttpHeaders : []) {
68
+ const h = asObject(raw);
69
+ if (typeof h.Name === 'string' && h.Name !== '') headers[h.Name] = String(h.Value ?? '');
70
+ }
71
+ const auth = asObject(webhook.HttpAuth);
72
+ if (typeof auth.Username === 'string' && auth.Username !== '') {
73
+ headers.Authorization = `Basic ${Buffer.from(`${auth.Username}:${String(auth.Password ?? '')}`).toString('base64')}`;
74
+ }
75
+ return headers;
76
+ }
77
+
78
+ /** Fire-and-forget HTTP delivery — the live path, like the real vendor's. */
79
+ const httpDelivery: PostmarkWebhookDelivery = async (url, body, headers) => {
80
+ if (worldEgressRefusal(url) !== null) return; // the World's egress rule: refused like an unreachable endpoint
81
+ try {
82
+ await fetch(url, { method: 'POST', headers, body });
83
+ } catch {
84
+ /* fire-and-forget, like the real vendor */
85
+ }
86
+ };
87
+
88
+ /**
89
+ * POST one Postmark webhook payload to every webhook on `stream` whose trigger for this
90
+ * `RecordType` is enabled. Returns what was delivered (for assertions); a no-op when no
91
+ * webhook subscribes. Offline whenever `ctx.deliver` is injected.
92
+ */
93
+ export async function emitPostmarkWebhook(
94
+ stream: string,
95
+ recordType: PostmarkRecordType,
96
+ payload: Record<string, unknown>,
97
+ ctx: WebhookContext = {},
98
+ ): Promise<Array<{ url: string; body: string; headers: Record<string, string> }>> {
99
+ const targets = webhooksFor(stream, recordType, ctx.root);
100
+ if (targets.length === 0) return [];
101
+ const deliver = ctx.deliver ?? httpDelivery;
102
+ const body = JSON.stringify({ RecordType: recordType, ...payload });
103
+ const out: Array<{ url: string; body: string; headers: Record<string, string> }> = [];
104
+ for (const w of targets) {
105
+ const headers = webhookHeaders(w);
106
+ await deliver(String(w.Url), body, headers);
107
+ out.push({ url: String(w.Url), body, headers });
108
+ }
109
+ return out;
110
+ }
111
+
112
+ /**
113
+ * Deliver ONE arriving inbound message to the server's `InboundHookUrl`.
114
+ *
115
+ * This is a different door from `emitPostmarkWebhook` on purpose, because the vendor's is:
116
+ * Postmark's Webhooks API (`POST /webhooks`, `Triggers`) covers the OUTBOUND delivery
117
+ * lifecycle, while an inbound message is posted to the URL configured on the SERVER
118
+ * (`InboundHookUrl`, settable via `PUT /server`). The payload is the inbound message
119
+ * document itself — the same object `GET /messages/inbound/:id/details` serves, with NO
120
+ * `RecordType` discriminator, which is exactly what the vendor sends.
121
+ *
122
+ * AUTH. Postmark's inbound hook has no header configuration at all: the only credential it
123
+ * can carry is HTTP Basic userinfo embedded in the configured URL. That userinfo is split
124
+ * out into an `Authorization: Basic` header here rather than left on the URL, because that
125
+ * is both what the vendor puts on the wire and what `fetch` refuses to send otherwise.
126
+ */
127
+ export async function emitPostmarkInboundHook(
128
+ message: Record<string, unknown>,
129
+ ctx: WebhookContext = {},
130
+ ): Promise<Array<{ url: string; body: string; headers: Record<string, string> }>> {
131
+ const server = projectResources(SERVICE, ctx.root).find((r) => r.type === 'server' && (r as Record<string, unknown>).deleted !== true);
132
+ const configured = typeof server?.InboundHookUrl === 'string' ? server.InboundHookUrl : '';
133
+ if (configured === '') return [];
134
+ let parsed: URL;
135
+ try {
136
+ parsed = new URL(configured);
137
+ } catch {
138
+ return [];
139
+ }
140
+ const headers: Record<string, string> = { 'content-type': 'application/json' };
141
+ if (parsed.username !== '') {
142
+ headers.Authorization = `Basic ${Buffer.from(`${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`).toString('base64')}`;
143
+ parsed.username = '';
144
+ parsed.password = '';
145
+ }
146
+ const url = parsed.toString();
147
+ const body = JSON.stringify(message);
148
+ await (ctx.deliver ?? httpDelivery)(url, body, headers);
149
+ return [{ url, body, headers }];
150
+ }
151
+
152
+ // ── deterministic offline delivery plan ─────────────────────────────────────────────
153
+ export type PostmarkEventType = 'Delivered' | 'Opened' | 'LinkClicked' | 'Bounced' | 'SpamComplaint';
154
+
155
+ /**
156
+ * The ordered `MessageEvents` the twin drives a sent message through, OFFLINE.
157
+ *
158
+ * The recipient prefixes below are a TWIN-LOCAL convention (Postmark's own bounce testing
159
+ * needs real mail flow, which a local twin has none of), deliberately documented rather than
160
+ * dressed up as vendor behavior — they are the seam that lets a test drive the bounce and
161
+ * spam-complaint paths deterministically:
162
+ * · a recipient whose local part starts with `bounce` → a HARD BOUNCE (no delivery);
163
+ * · one starting with `spam` or `complaint` → delivered, then a SPAM COMPLAINT;
164
+ * · anything else → delivered.
165
+ * Opens and clicks are emitted ONLY when the message asked for that tracking — `TrackOpens`
166
+ * and `TrackLinks` respectively — exactly as the vendor does, so a message sent with tracking
167
+ * off records neither.
168
+ *
169
+ * `status` is the outbound message's `Status`. It stays `Sent` for every submitted message:
170
+ * on real Postmark a bounce does NOT rewrite the message's Status, it surfaces through
171
+ * `MessageEvents` and the Bounce API. Modeled that way here on purpose.
172
+ */
173
+ export function deliveryPlan(input: { recipient: string; trackOpens: boolean; trackLinks: string }): { events: PostmarkEventType[]; status: string } {
174
+ const local = String(input.recipient ?? '').split('@')[0]?.toLowerCase() ?? '';
175
+ if (local.startsWith('bounce')) return { events: ['Bounced'], status: 'Sent' };
176
+ if (local.startsWith('spam') || local.startsWith('complaint')) {
177
+ return { events: ['Delivered', 'SpamComplaint'], status: 'Sent' };
178
+ }
179
+ const events: PostmarkEventType[] = ['Delivered'];
180
+ if (input.trackOpens) events.push('Opened');
181
+ if (input.trackLinks && input.trackLinks !== 'None') events.push('LinkClicked');
182
+ return { events, status: 'Sent' };
183
+ }
184
+
185
+ /** The terminal event a message lands on, given its plan (the outbound activity headline). */
186
+ export function terminalEvent(input: { recipient: string; trackOpens: boolean; trackLinks: string }): PostmarkEventType {
187
+ const { events } = deliveryPlan(input);
188
+ return events[events.length - 1]!;
189
+ }
@@ -0,0 +1,213 @@
1
+ // Postmark MIRROR UI — a Postmark-dashboard-like Activity view served as a React/TSX app
2
+ // (Bun-bundled, the repo convention).
3
+ //
4
+ // TRANSPORT: archetype A, API PASSTHROUGH (ADDING_A_TWIN "Does this vendor get a mirror?").
5
+ // Every non-asset request falls straight through to the pack's OWN FETCH ADAPTER, and the
6
+ // browser client fetches POSTMARK'S OWN REAL API PATHS — `/messages/outbound`,
7
+ // `/messages/outbound/:id/details`, `/bounces`, `/templates`, `/message-streams`,
8
+ // `/webhooks`, `/message-streams/outbound/suppressions/dump`. There is exactly ONE code path
9
+ // serving the screen and the API, so API↔UI parity cannot drift: it is not "kept in sync",
10
+ // it is the same handler. (Resend needed a `/_twin/emails` side-read only because Resend has
11
+ // no list-emails endpoint; Postmark's Messages API does, so no side projection exists here.)
12
+ //
13
+ // The pure render/format helpers below are framework-agnostic so they can be unit-tested AND
14
+ // imported by the browser client — Bun tree-shakes the server-only exports out of the bundle.
15
+ //
16
+ // PURE FRONTEND (R3): the mirror imports no handler and no twin internals — it MOUNTS the pack's
17
+ // own fetch adapter as its API backend and reads every byte of state back over the wire.
18
+ import { readFile } from 'node:fs/promises';
19
+ import { bundleClient, fileResponse } from '@volter/world-core';
20
+ import { serveHttp } from '@volter/world-core';
21
+ import { createPostmarkTwinFetch } from './postmark-server.ts';
22
+
23
+ const CLIENT_ENTRY = () => new URL('../client/postmark-mirror.tsx', import.meta.url).pathname; // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
24
+ const CLIENT_CSS = () => new URL('../client/postmark-mirror.css', import.meta.url).pathname; // lazy: workerd rejects top-level relative URL from import.meta.url (bundled via pack index)
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Pure, dependency-free render/format helpers (no `@volter/world-core`, no `Bun`, no handler).
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export type PostmarkRow = Record<string, any>;
31
+
32
+ /** The recipients of an outbound message, as a readable list. */
33
+ export function recipientsOf(m: PostmarkRow): string {
34
+ if (Array.isArray(m?.Recipients)) return m.Recipients.map(String).join(', ');
35
+ if (Array.isArray(m?.To)) return m.To.map((r: PostmarkRow) => String(r?.Email ?? '')).filter(Boolean).join(', ');
36
+ if (typeof m?.To === 'string') return m.To;
37
+ return '';
38
+ }
39
+
40
+ /** A subject line for a message row, or a placeholder. */
41
+ export function messageSubject(m: PostmarkRow): string {
42
+ return m?.Subject ? String(m.Subject) : '(no subject)';
43
+ }
44
+
45
+ /** The ordered `MessageEvents` types the twin recorded for a message. */
46
+ export function deliveryTimeline(m: PostmarkRow): string[] {
47
+ const events = Array.isArray(m?.MessageEvents) ? m.MessageEvents : [];
48
+ return ['Sent', ...events.map((e: PostmarkRow) => String(e?.Type ?? ''))].filter(Boolean);
49
+ }
50
+
51
+ /**
52
+ * The headline delivery status of a message: its LAST recorded MessageEvent, falling back to
53
+ * the vendor's `Status` field. This is what the Activity list and the status pill show.
54
+ */
55
+ export function messageStatus(m: PostmarkRow): string {
56
+ const timeline = deliveryTimeline(m);
57
+ const last = timeline[timeline.length - 1];
58
+ return last && last !== 'Sent' ? last : String(m?.Status ?? 'Sent');
59
+ }
60
+
61
+ /** The rendered preview of a message body — prefers HtmlBody, falls back to TextBody. */
62
+ export function messagePreview(m: PostmarkRow): { kind: 'html' | 'text' | 'none'; value: string } {
63
+ if (m?.HtmlBody) return { kind: 'html', value: String(m.HtmlBody) };
64
+ if (m?.TextBody) return { kind: 'text', value: String(m.TextBody) };
65
+ return { kind: 'none', value: '' };
66
+ }
67
+
68
+ /** Format an ISO timestamp as a short date, or an em dash. */
69
+ export function formatPostmarkDate(iso: unknown): string {
70
+ if (typeof iso !== 'string' || !iso) return '—';
71
+ const t = Date.parse(iso);
72
+ if (!Number.isFinite(t)) return '—';
73
+ try {
74
+ return new Date(t).toISOString().slice(0, 16).replace('T', ' ');
75
+ } catch {
76
+ return '—';
77
+ }
78
+ }
79
+
80
+ /**
81
+ * A status → CSS tone class for the status pill. The returned values are STATIC STRING
82
+ * LITERALS (never template-built) so they survive the bundler's minification and a UI
83
+ * verify can assert on the class the component actually emits.
84
+ */
85
+ export function statusTone(value: string): string {
86
+ const v = String(value ?? '').toLowerCase();
87
+ if (['delivered', 'opened', 'linkclicked', 'sent', 'processed', 'verified', 'active', 'true'].includes(v)) return 'ok';
88
+ if (['queued', 'pending', 'scheduled', 'unconfirmed', 'false'].includes(v)) return 'warn';
89
+ if (['bounced', 'hardbounce', 'spamcomplaint', 'blocked', 'failed', 'suppressed', 'transient'].includes(v)) return 'bad';
90
+ return 'neutral';
91
+ }
92
+
93
+ /** Flatten a (possibly nested) value into label/value lines for the detail panel. */
94
+ export type FlatLine = { key: string; value: string; depth: number };
95
+ export function flattenPostmarkValue(value: unknown, prefix = '', depth = 0, out: FlatLine[] = []): FlatLine[] {
96
+ if (value === null || value === undefined) {
97
+ out.push({ key: prefix || '(value)', value: '—', depth });
98
+ } else if (Array.isArray(value)) {
99
+ if (value.length === 0) out.push({ key: prefix, value: '[]', depth });
100
+ else value.forEach((v, i) => flattenPostmarkValue(v, `${prefix}[${i}]`, depth, out));
101
+ } else if (typeof value === 'object') {
102
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
103
+ flattenPostmarkValue(v, prefix ? `${prefix}.${k}` : k, depth, out);
104
+ }
105
+ } else {
106
+ out.push({ key: prefix || '(value)', value: String(value), depth });
107
+ }
108
+ return out;
109
+ }
110
+
111
+ /** The nav sections the mirror renders, each bound to one REAL Postmark API path + its
112
+ * collection key. Shared by the client (to fetch) and by tests (to assert the binding). */
113
+ export type MirrorSection = { key: string; label: string; path: string; collection: string; idKey: string };
114
+ export const POSTMARK_MIRROR_SECTIONS: MirrorSection[] = [
115
+ { key: 'messages', label: 'Activity', path: '/messages/outbound', collection: 'Messages', idKey: 'MessageID' },
116
+ { key: 'bounces', label: 'Bounces', path: '/bounces', collection: 'Bounces', idKey: 'ID' },
117
+ { key: 'templates', label: 'Templates', path: '/templates', collection: 'Templates', idKey: 'TemplateId' },
118
+ { key: 'streams', label: 'Streams', path: '/message-streams', collection: 'MessageStreams', idKey: 'ID' },
119
+ { key: 'webhooks', label: 'Webhooks', path: '/webhooks', collection: 'Webhooks', idKey: 'ID' },
120
+ { key: 'suppressions', label: 'Suppressions', path: '/message-streams/outbound/suppressions/dump', collection: 'Suppressions', idKey: 'EmailAddress' },
121
+ ];
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Server-only (Bun) below — NOT imported by the browser client.
125
+ // ---------------------------------------------------------------------------
126
+
127
+ const APP_SHELL = `<!doctype html><html lang="en"><head><meta charset="utf-8" />
128
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
129
+ <base href="/"><title>Postmark twin — activity mirror</title>
130
+ <link rel="stylesheet" href="assets/styles.css" /></head>
131
+ <body><div id="root"></div><script type="module" src="assets/app.js"></script></body></html>`;
132
+
133
+ let clientBundle: Promise<string> | null = null;
134
+ /** Build the React/TSX mirror client to browser JS (Bun bundles TSX); memoized at module
135
+ * scope so a pack on its own does exactly ONE `Bun.build`. */
136
+ export function buildPostmarkMirrorClient(): Promise<string> {
137
+ if (!clientBundle) {
138
+ clientBundle = bundleClient(CLIENT_ENTRY())
139
+ .catch((error) => { clientBundle = null; throw error; });
140
+ }
141
+ return clientBundle;
142
+ }
143
+
144
+ /**
145
+ * The mirror's first-party credential, added to a request the adapter is about to serve.
146
+ *
147
+ * A pure REQUEST REWRITE — the mirror never calls the handler and never reaches around the wire;
148
+ * it only fills in the token header a browser cannot carry (Postmark authenticates every route
149
+ * with `X-Postmark-Server-Token`, or `X-Postmark-Account-Token` on its account-scoped families).
150
+ * The console is signed in to ITS root, so any non-empty token does — deliberately NOT the
151
+ * vendor's public test token, which is /email-only and changes the send reply text. Headers
152
+ * already present are left alone, so a caller that brings its own token keeps it.
153
+ */
154
+ const MIRROR_TOKEN = 'postmark-twin-mirror-token';
155
+ async function withMirrorCredentials(request: Request): Promise<Request> {
156
+ const headers = new Headers(request.headers);
157
+ if (!headers.has('x-postmark-server-token')) headers.set('x-postmark-server-token', MIRROR_TOKEN);
158
+ if (!headers.has('x-postmark-account-token')) headers.set('x-postmark-account-token', MIRROR_TOKEN);
159
+ const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
160
+ return new Request(request.url, {
161
+ method: request.method,
162
+ headers,
163
+ ...(hasBody ? { body: await request.text() } : {}),
164
+ });
165
+ }
166
+
167
+ /** Serve the Postmark activity mirror (React app) over the twin's OWN REST API. */
168
+ export async function createPostmarkMirrorServer(options: { root?: string; port?: number; readOnly?: boolean }): Promise<{ port: number; stop: () => void }> {
169
+ const twin = createPostmarkTwinFetch(options);
170
+ const server = await serveHttp({
171
+ // LOOPBACK-SPECIFIC bind (2026-08-20, the roving ui-verify flake): with the default
172
+ // wildcard hostname, `port: 0` can be handed a port some long-running app already LISTENS
173
+ // on at 127.0.0.1 (SO_REUSEADDR allows the overlapping non-identical bind), and the more
174
+ // specific loopback listener then shadows this server for every 127.0.0.1 fetch — the
175
+ // verify talks to a STRANGER (captured: a desktop app's asset server answering 404s on the
176
+ // mirror's port). Binding 127.0.0.1 makes the kernel allocate a port that is actually free
177
+ // on loopback, so the verify's fetches deterministically reach THIS server.
178
+ hostname: '127.0.0.1',
179
+ port: options.port ?? 0,
180
+ idleTimeout: 60,
181
+ async fetch(request) {
182
+ const url = new URL(request.url);
183
+ if (request.method === 'GET' && url.pathname === '/assets/app.js') {
184
+ try { return new Response(await buildPostmarkMirrorClient(), { headers: { 'content-type': 'text/javascript; charset=utf-8' } }); }
185
+ catch (error) { return new Response(String(error), { status: 500 }); }
186
+ }
187
+ if (request.method === 'GET' && url.pathname === '/assets/styles.css') {
188
+ return fileResponse(CLIENT_CSS(), { headers: { 'content-type': 'text/css; charset=utf-8' } });
189
+ }
190
+ if (request.method === 'GET' && (url.pathname === '/' || url.pathname === '')) {
191
+ return new Response(APP_SHELL, { headers: { 'content-type': 'text/html; charset=utf-8' } });
192
+ }
193
+ // Everything else → the twin's OWN FETCH ADAPTER (composition, R2): the same closure
194
+ // `createPostmarkTwinServer` serves, so the mirror port and the API port cannot drift — the
195
+ // uniform `GET /twin` door, the world clock, `readOnly`, Postmark's token auth and the
196
+ // `connection: close` Bun-socket workaround all come from it rather than from a hand-rolled
197
+ // copy that has to be kept in step. The client fetches the vendor's real paths, so the
198
+ // mirror and an SDK consumer are served by literally the same code (archetype A).
199
+ return twin(await withMirrorCredentials(request));
200
+ },
201
+ });
202
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
203
+ }
204
+
205
+ /** The app-shell HTML (pure, for tests). The mirror itself is the React client. */
206
+ export function postmarkMirrorHtml(): string {
207
+ return APP_SHELL;
208
+ }
209
+
210
+ /** The mirror's stylesheet, for a host serving the shell's `assets/styles.css` itself (the hosted mirror mount). */
211
+ export function postmarkMirrorStyles(): Promise<string> {
212
+ return readFile(CLIENT_CSS(), 'utf8');
213
+ }
@@ -0,0 +1,21 @@
1
+ // A MINIATURE OF THE HEAD, for this pack's own claims and suites (protocol 2). Not on the serve path.
2
+ import { confirmAction, deployableEntries, worldNow } from '@volter/world-core';
3
+ import { pushPostmarkAction, type PostmarkClient } from './postmark-connector.ts';
4
+
5
+ export async function performPending(client: PostmarkClient, opts: { root?: string; occurredAt?: string } = {}): Promise<{ pushed: number; confirmed: string[]; externalIds: Record<string, string> }> {
6
+ const confirmed: string[] = [];
7
+ const externalIds: Record<string, string> = {};
8
+ for (const entry of deployableEntries('postmark', opts.root)) {
9
+ let externalId: string;
10
+ try {
11
+ ({ externalId } = await pushPostmarkAction(client, { operation: entry.operation ?? `${entry.subject.type}.update`, subject: entry.subject, fields: entry.fields ?? {} }));
12
+ } catch { continue; } // the twin's own record: nothing to write
13
+ confirmAction({
14
+ service: 'postmark', actionId: entry.id, subject: entry.subject, fields: entry.fields ?? {},
15
+ occurredAt: opts.occurredAt ?? worldNow(), vendorSubjectId: externalId, receipt: { status: 'deployed' },
16
+ ...(opts.root !== undefined ? { root: opts.root } : {}),
17
+ });
18
+ confirmed.push(entry.id); externalIds[entry.id] = externalId;
19
+ }
20
+ return { pushed: confirmed.length, confirmed, externalIds };
21
+ }
@@ -0,0 +1,37 @@
1
+ // Postmark twin HTTP server — serve the full Postmark API twin handler over HTTP so the real
2
+ // `postmark` SDK (pointed at this base URL) works unmodified. JSON bodies pass through to the
3
+ // handler. Writable by default; pass readOnly to reject writes (D3).
4
+ //
5
+ // FETCH-FIRST (runtime contract R12b): the serve path is the plain fetch below, built from the
6
+ // kernel's ONE adaptation (`createTwinFetchFromHandler`); this file contributes only VALUES —
7
+ // the manifest and the `connection: close` response header. The server is one line of Bun.serve
8
+ // around that same closure.
9
+ import { serveHttp } from '@volter/world-core';
10
+ import { handlePostmarkTwinRequest } from './postmark-twin.ts';
11
+ import { createTwinFetchFromHandler, statefulTwinManifest } from '@volter/world-core';
12
+
13
+ /** Options every Postmark-twin HTTP surface needs, independent of who owns the socket. */
14
+ export interface PostmarkTwinFetchOptions {
15
+ root?: string;
16
+ readOnly?: boolean;
17
+ }
18
+
19
+ export function createPostmarkTwinFetch(options: PostmarkTwinFetchOptions): (request: Request) => Promise<Response> {
20
+ return createTwinFetchFromHandler(handlePostmarkTwinRequest, {
21
+ ...options,
22
+ manifest: statefulTwinManifest({ vendor: 'postmark', twinOf: 'the Postmark transactional-email API', stores: 'servers, sent messages and their opens/deliveries (the mirror shows the inbox)' }),
23
+ // The official SDK's Node transport reuses sockets. Bun 1.2 can reject a later request on
24
+ // that socket with its own HTTP 400 before fetch() runs; closing makes each modeled request
25
+ // reach the handler. (Inert under workerd, which owns its own connection handling.)
26
+ responseHeaders: { connection: 'close' },
27
+ });
28
+ }
29
+
30
+ export async function createPostmarkTwinServer(options: { root?: string; port?: number; readOnly?: boolean }): Promise<{ port: number; stop: () => void }> {
31
+ const server = await serveHttp({
32
+ port: options.port ?? 0,
33
+ idleTimeout: 60,
34
+ fetch: createPostmarkTwinFetch(options),
35
+ });
36
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
37
+ }