@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.
- package/LICENSE +202 -0
- package/README.md +144 -0
- package/client/postmark-mirror.css +79 -0
- package/client/postmark-mirror.tsx +221 -0
- package/dist/client/postmark-mirror.bundle.js +321 -0
- package/dist/client/postmark-mirror.css +79 -0
- package/dist/client/postmark-mirror.d.ts +18 -0
- package/dist/client/postmark-mirror.js +153 -0
- package/dist/client/postmark-mirror.tsx +221 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +31 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +54 -0
- package/dist/src/postmark-capabilities.d.ts +12 -0
- package/dist/src/postmark-capabilities.js +1502 -0
- package/dist/src/postmark-conformance.d.ts +33 -0
- package/dist/src/postmark-conformance.js +265 -0
- package/dist/src/postmark-connector.d.ts +167 -0
- package/dist/src/postmark-connector.js +251 -0
- package/dist/src/postmark-events.d.ts +85 -0
- package/dist/src/postmark-events.js +169 -0
- package/dist/src/postmark-mirror-ui.d.ts +58 -0
- package/dist/src/postmark-mirror-ui.js +207 -0
- package/dist/src/postmark-perform-harness.d.ts +9 -0
- package/dist/src/postmark-perform-harness.js +24 -0
- package/dist/src/postmark-server.d.ts +14 -0
- package/dist/src/postmark-server.js +29 -0
- package/dist/src/postmark-twin.d.ts +82 -0
- package/dist/src/postmark-twin.js +1575 -0
- package/dist/test-fixtures/postmark-swagger-operations.json +846 -0
- package/package.json +76 -0
- package/src/cli.ts +29 -0
- package/src/index.ts +89 -0
- package/src/postmark-capabilities.ts +1737 -0
- package/src/postmark-conformance.ts +282 -0
- package/src/postmark-connector.ts +312 -0
- package/src/postmark-events.ts +189 -0
- package/src/postmark-mirror-ui.ts +213 -0
- package/src/postmark-perform-harness.ts +21 -0
- package/src/postmark-server.ts +37 -0
- package/src/postmark-twin.ts +1520 -0
- package/test-fixtures/postmark-swagger-operations.json +846 -0
|
@@ -0,0 +1,207 @@
|
|
|
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.js";
|
|
22
|
+
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)
|
|
23
|
+
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)
|
|
24
|
+
/** The recipients of an outbound message, as a readable list. */
|
|
25
|
+
export function recipientsOf(m) {
|
|
26
|
+
if (Array.isArray(m?.Recipients))
|
|
27
|
+
return m.Recipients.map(String).join(', ');
|
|
28
|
+
if (Array.isArray(m?.To))
|
|
29
|
+
return m.To.map((r) => String(r?.Email ?? '')).filter(Boolean).join(', ');
|
|
30
|
+
if (typeof m?.To === 'string')
|
|
31
|
+
return m.To;
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
/** A subject line for a message row, or a placeholder. */
|
|
35
|
+
export function messageSubject(m) {
|
|
36
|
+
return m?.Subject ? String(m.Subject) : '(no subject)';
|
|
37
|
+
}
|
|
38
|
+
/** The ordered `MessageEvents` types the twin recorded for a message. */
|
|
39
|
+
export function deliveryTimeline(m) {
|
|
40
|
+
const events = Array.isArray(m?.MessageEvents) ? m.MessageEvents : [];
|
|
41
|
+
return ['Sent', ...events.map((e) => String(e?.Type ?? ''))].filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The headline delivery status of a message: its LAST recorded MessageEvent, falling back to
|
|
45
|
+
* the vendor's `Status` field. This is what the Activity list and the status pill show.
|
|
46
|
+
*/
|
|
47
|
+
export function messageStatus(m) {
|
|
48
|
+
const timeline = deliveryTimeline(m);
|
|
49
|
+
const last = timeline[timeline.length - 1];
|
|
50
|
+
return last && last !== 'Sent' ? last : String(m?.Status ?? 'Sent');
|
|
51
|
+
}
|
|
52
|
+
/** The rendered preview of a message body — prefers HtmlBody, falls back to TextBody. */
|
|
53
|
+
export function messagePreview(m) {
|
|
54
|
+
if (m?.HtmlBody)
|
|
55
|
+
return { kind: 'html', value: String(m.HtmlBody) };
|
|
56
|
+
if (m?.TextBody)
|
|
57
|
+
return { kind: 'text', value: String(m.TextBody) };
|
|
58
|
+
return { kind: 'none', value: '' };
|
|
59
|
+
}
|
|
60
|
+
/** Format an ISO timestamp as a short date, or an em dash. */
|
|
61
|
+
export function formatPostmarkDate(iso) {
|
|
62
|
+
if (typeof iso !== 'string' || !iso)
|
|
63
|
+
return '—';
|
|
64
|
+
const t = Date.parse(iso);
|
|
65
|
+
if (!Number.isFinite(t))
|
|
66
|
+
return '—';
|
|
67
|
+
try {
|
|
68
|
+
return new Date(t).toISOString().slice(0, 16).replace('T', ' ');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return '—';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A status → CSS tone class for the status pill. The returned values are STATIC STRING
|
|
76
|
+
* LITERALS (never template-built) so they survive the bundler's minification and a UI
|
|
77
|
+
* verify can assert on the class the component actually emits.
|
|
78
|
+
*/
|
|
79
|
+
export function statusTone(value) {
|
|
80
|
+
const v = String(value ?? '').toLowerCase();
|
|
81
|
+
if (['delivered', 'opened', 'linkclicked', 'sent', 'processed', 'verified', 'active', 'true'].includes(v))
|
|
82
|
+
return 'ok';
|
|
83
|
+
if (['queued', 'pending', 'scheduled', 'unconfirmed', 'false'].includes(v))
|
|
84
|
+
return 'warn';
|
|
85
|
+
if (['bounced', 'hardbounce', 'spamcomplaint', 'blocked', 'failed', 'suppressed', 'transient'].includes(v))
|
|
86
|
+
return 'bad';
|
|
87
|
+
return 'neutral';
|
|
88
|
+
}
|
|
89
|
+
export function flattenPostmarkValue(value, prefix = '', depth = 0, out = []) {
|
|
90
|
+
if (value === null || value === undefined) {
|
|
91
|
+
out.push({ key: prefix || '(value)', value: '—', depth });
|
|
92
|
+
}
|
|
93
|
+
else if (Array.isArray(value)) {
|
|
94
|
+
if (value.length === 0)
|
|
95
|
+
out.push({ key: prefix, value: '[]', depth });
|
|
96
|
+
else
|
|
97
|
+
value.forEach((v, i) => flattenPostmarkValue(v, `${prefix}[${i}]`, depth, out));
|
|
98
|
+
}
|
|
99
|
+
else if (typeof value === 'object') {
|
|
100
|
+
for (const [k, v] of Object.entries(value)) {
|
|
101
|
+
flattenPostmarkValue(v, prefix ? `${prefix}.${k}` : k, depth, out);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
out.push({ key: prefix || '(value)', value: String(value), depth });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
export const POSTMARK_MIRROR_SECTIONS = [
|
|
110
|
+
{ key: 'messages', label: 'Activity', path: '/messages/outbound', collection: 'Messages', idKey: 'MessageID' },
|
|
111
|
+
{ key: 'bounces', label: 'Bounces', path: '/bounces', collection: 'Bounces', idKey: 'ID' },
|
|
112
|
+
{ key: 'templates', label: 'Templates', path: '/templates', collection: 'Templates', idKey: 'TemplateId' },
|
|
113
|
+
{ key: 'streams', label: 'Streams', path: '/message-streams', collection: 'MessageStreams', idKey: 'ID' },
|
|
114
|
+
{ key: 'webhooks', label: 'Webhooks', path: '/webhooks', collection: 'Webhooks', idKey: 'ID' },
|
|
115
|
+
{ key: 'suppressions', label: 'Suppressions', path: '/message-streams/outbound/suppressions/dump', collection: 'Suppressions', idKey: 'EmailAddress' },
|
|
116
|
+
];
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Server-only (Bun) below — NOT imported by the browser client.
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
const APP_SHELL = `<!doctype html><html lang="en"><head><meta charset="utf-8" />
|
|
121
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
122
|
+
<base href="/"><title>Postmark twin — activity mirror</title>
|
|
123
|
+
<link rel="stylesheet" href="assets/styles.css" /></head>
|
|
124
|
+
<body><div id="root"></div><script type="module" src="assets/app.js"></script></body></html>`;
|
|
125
|
+
let clientBundle = null;
|
|
126
|
+
/** Build the React/TSX mirror client to browser JS (Bun bundles TSX); memoized at module
|
|
127
|
+
* scope so a pack on its own does exactly ONE `Bun.build`. */
|
|
128
|
+
export function buildPostmarkMirrorClient() {
|
|
129
|
+
if (!clientBundle) {
|
|
130
|
+
clientBundle = bundleClient(CLIENT_ENTRY())
|
|
131
|
+
.catch((error) => { clientBundle = null; throw error; });
|
|
132
|
+
}
|
|
133
|
+
return clientBundle;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The mirror's first-party credential, added to a request the adapter is about to serve.
|
|
137
|
+
*
|
|
138
|
+
* A pure REQUEST REWRITE — the mirror never calls the handler and never reaches around the wire;
|
|
139
|
+
* it only fills in the token header a browser cannot carry (Postmark authenticates every route
|
|
140
|
+
* with `X-Postmark-Server-Token`, or `X-Postmark-Account-Token` on its account-scoped families).
|
|
141
|
+
* The console is signed in to ITS root, so any non-empty token does — deliberately NOT the
|
|
142
|
+
* vendor's public test token, which is /email-only and changes the send reply text. Headers
|
|
143
|
+
* already present are left alone, so a caller that brings its own token keeps it.
|
|
144
|
+
*/
|
|
145
|
+
const MIRROR_TOKEN = 'postmark-twin-mirror-token';
|
|
146
|
+
async function withMirrorCredentials(request) {
|
|
147
|
+
const headers = new Headers(request.headers);
|
|
148
|
+
if (!headers.has('x-postmark-server-token'))
|
|
149
|
+
headers.set('x-postmark-server-token', MIRROR_TOKEN);
|
|
150
|
+
if (!headers.has('x-postmark-account-token'))
|
|
151
|
+
headers.set('x-postmark-account-token', MIRROR_TOKEN);
|
|
152
|
+
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
153
|
+
return new Request(request.url, {
|
|
154
|
+
method: request.method,
|
|
155
|
+
headers,
|
|
156
|
+
...(hasBody ? { body: await request.text() } : {}),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/** Serve the Postmark activity mirror (React app) over the twin's OWN REST API. */
|
|
160
|
+
export async function createPostmarkMirrorServer(options) {
|
|
161
|
+
const twin = createPostmarkTwinFetch(options);
|
|
162
|
+
const server = await serveHttp({
|
|
163
|
+
// LOOPBACK-SPECIFIC bind (2026-08-20, the roving ui-verify flake): with the default
|
|
164
|
+
// wildcard hostname, `port: 0` can be handed a port some long-running app already LISTENS
|
|
165
|
+
// on at 127.0.0.1 (SO_REUSEADDR allows the overlapping non-identical bind), and the more
|
|
166
|
+
// specific loopback listener then shadows this server for every 127.0.0.1 fetch — the
|
|
167
|
+
// verify talks to a STRANGER (captured: a desktop app's asset server answering 404s on the
|
|
168
|
+
// mirror's port). Binding 127.0.0.1 makes the kernel allocate a port that is actually free
|
|
169
|
+
// on loopback, so the verify's fetches deterministically reach THIS server.
|
|
170
|
+
hostname: '127.0.0.1',
|
|
171
|
+
port: options.port ?? 0,
|
|
172
|
+
idleTimeout: 60,
|
|
173
|
+
async fetch(request) {
|
|
174
|
+
const url = new URL(request.url);
|
|
175
|
+
if (request.method === 'GET' && url.pathname === '/assets/app.js') {
|
|
176
|
+
try {
|
|
177
|
+
return new Response(await buildPostmarkMirrorClient(), { headers: { 'content-type': 'text/javascript; charset=utf-8' } });
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
return new Response(String(error), { status: 500 });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (request.method === 'GET' && url.pathname === '/assets/styles.css') {
|
|
184
|
+
return fileResponse(CLIENT_CSS(), { headers: { 'content-type': 'text/css; charset=utf-8' } });
|
|
185
|
+
}
|
|
186
|
+
if (request.method === 'GET' && (url.pathname === '/' || url.pathname === '')) {
|
|
187
|
+
return new Response(APP_SHELL, { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
188
|
+
}
|
|
189
|
+
// Everything else → the twin's OWN FETCH ADAPTER (composition, R2): the same closure
|
|
190
|
+
// `createPostmarkTwinServer` serves, so the mirror port and the API port cannot drift — the
|
|
191
|
+
// uniform `GET /twin` door, the world clock, `readOnly`, Postmark's token auth and the
|
|
192
|
+
// `connection: close` Bun-socket workaround all come from it rather than from a hand-rolled
|
|
193
|
+
// copy that has to be kept in step. The client fetches the vendor's real paths, so the
|
|
194
|
+
// mirror and an SDK consumer are served by literally the same code (archetype A).
|
|
195
|
+
return twin(await withMirrorCredentials(request));
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
199
|
+
}
|
|
200
|
+
/** The app-shell HTML (pure, for tests). The mirror itself is the React client. */
|
|
201
|
+
export function postmarkMirrorHtml() {
|
|
202
|
+
return APP_SHELL;
|
|
203
|
+
}
|
|
204
|
+
/** The mirror's stylesheet, for a host serving the shell's `assets/styles.css` itself (the hosted mirror mount). */
|
|
205
|
+
export function postmarkMirrorStyles() {
|
|
206
|
+
return readFile(CLIENT_CSS(), 'utf8');
|
|
207
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type PostmarkClient } from './postmark-connector.js';
|
|
2
|
+
export declare function performPending(client: PostmarkClient, opts?: {
|
|
3
|
+
root?: string;
|
|
4
|
+
occurredAt?: string;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
pushed: number;
|
|
7
|
+
confirmed: string[];
|
|
8
|
+
externalIds: Record<string, string>;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,24 @@
|
|
|
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 } from "./postmark-connector.js";
|
|
4
|
+
export async function performPending(client, opts = {}) {
|
|
5
|
+
const confirmed = [];
|
|
6
|
+
const externalIds = {};
|
|
7
|
+
for (const entry of deployableEntries('postmark', opts.root)) {
|
|
8
|
+
let externalId;
|
|
9
|
+
try {
|
|
10
|
+
({ externalId } = await pushPostmarkAction(client, { operation: entry.operation ?? `${entry.subject.type}.update`, subject: entry.subject, fields: entry.fields ?? {} }));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
continue;
|
|
14
|
+
} // the twin's own record: nothing to write
|
|
15
|
+
confirmAction({
|
|
16
|
+
service: 'postmark', actionId: entry.id, subject: entry.subject, fields: entry.fields ?? {},
|
|
17
|
+
occurredAt: opts.occurredAt ?? worldNow(), vendorSubjectId: externalId, receipt: { status: 'deployed' },
|
|
18
|
+
...(opts.root !== undefined ? { root: opts.root } : {}),
|
|
19
|
+
});
|
|
20
|
+
confirmed.push(entry.id);
|
|
21
|
+
externalIds[entry.id] = externalId;
|
|
22
|
+
}
|
|
23
|
+
return { pushed: confirmed.length, confirmed, externalIds };
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Options every Postmark-twin HTTP surface needs, independent of who owns the socket. */
|
|
2
|
+
export interface PostmarkTwinFetchOptions {
|
|
3
|
+
root?: string;
|
|
4
|
+
readOnly?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function createPostmarkTwinFetch(options: PostmarkTwinFetchOptions): (request: Request) => Promise<Response>;
|
|
7
|
+
export declare function createPostmarkTwinServer(options: {
|
|
8
|
+
root?: string;
|
|
9
|
+
port?: number;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
port: number;
|
|
13
|
+
stop: () => void;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,29 @@
|
|
|
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.js";
|
|
11
|
+
import { createTwinFetchFromHandler, statefulTwinManifest } from '@volter/world-core';
|
|
12
|
+
export function createPostmarkTwinFetch(options) {
|
|
13
|
+
return createTwinFetchFromHandler(handlePostmarkTwinRequest, {
|
|
14
|
+
...options,
|
|
15
|
+
manifest: statefulTwinManifest({ vendor: 'postmark', twinOf: 'the Postmark transactional-email API', stores: 'servers, sent messages and their opens/deliveries (the mirror shows the inbox)' }),
|
|
16
|
+
// The official SDK's Node transport reuses sockets. Bun 1.2 can reject a later request on
|
|
17
|
+
// that socket with its own HTTP 400 before fetch() runs; closing makes each modeled request
|
|
18
|
+
// reach the handler. (Inert under workerd, which owns its own connection handling.)
|
|
19
|
+
responseHeaders: { connection: 'close' },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function createPostmarkTwinServer(options) {
|
|
23
|
+
const server = await serveHttp({
|
|
24
|
+
port: options.port ?? 0,
|
|
25
|
+
idleTimeout: 60,
|
|
26
|
+
fetch: createPostmarkTwinFetch(options),
|
|
27
|
+
});
|
|
28
|
+
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
29
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type PostmarkWebhookDelivery } from './postmark-events.js';
|
|
2
|
+
export type PostmarkRequest = {
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
body?: string;
|
|
6
|
+
occurredAt?: string;
|
|
7
|
+
root?: string;
|
|
8
|
+
readOnly?: boolean;
|
|
9
|
+
/** Injected webhook deliverer (fake in tests, HTTP live). Keeps verify() offline. */
|
|
10
|
+
deliver?: PostmarkWebhookDelivery;
|
|
11
|
+
/**
|
|
12
|
+
* Request headers (case-insensitive). When PRESENT (the HTTP server always supplies them),
|
|
13
|
+
* the twin enforces Postmark's token auth: a missing/empty `X-Postmark-Server-Token` (or
|
|
14
|
+
* `X-Postmark-Account-Token` on account routes) → 401 `{ErrorCode:10}`. When the whole
|
|
15
|
+
* object is ABSENT, auth is not exercised — the in-process verify()/mirror path is a
|
|
16
|
+
* trusted local call, mirroring the resend pack's convention.
|
|
17
|
+
*/
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
export type PostmarkResponse = {
|
|
21
|
+
status: number;
|
|
22
|
+
body: unknown;
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The Postmark `ErrorCode` values this twin emits, each with the HTTP status the vendor
|
|
27
|
+
* pairs it with. Every number + status here is transcribed from developer.postmarkapp.com's
|
|
28
|
+
* published API-error-codes table (see the GROUNDING note above). Note how few of these are
|
|
29
|
+
* 404: Postmark answers "not found" for a KNOWN route with 422 + a per-domain code, and
|
|
30
|
+
* reserves 404 for a route that does not exist at all.
|
|
31
|
+
*/
|
|
32
|
+
export declare const POSTMARK_ERRORS: {
|
|
33
|
+
readonly ok: 0;
|
|
34
|
+
readonly badToken: 10;
|
|
35
|
+
readonly bulkNotApproved: 14;
|
|
36
|
+
readonly invalidEmailRequest: 300;
|
|
37
|
+
readonly invalidJson: 402;
|
|
38
|
+
readonly invalidRequestField: 403;
|
|
39
|
+
readonly inactiveRecipient: 406;
|
|
40
|
+
readonly tooManyBatchMessages: 410;
|
|
41
|
+
readonly signatureNotFound: 501;
|
|
42
|
+
readonly signatureNoData: 502;
|
|
43
|
+
readonly publicDomainSignature: 503;
|
|
44
|
+
readonly signatureExists: 504;
|
|
45
|
+
readonly domainNotFound: 510;
|
|
46
|
+
readonly domainExists: 512;
|
|
47
|
+
readonly domainNameRequired: 514;
|
|
48
|
+
readonly fromEmailRequired: 520;
|
|
49
|
+
readonly invalidEmailValue: 522;
|
|
50
|
+
readonly serverInboundDomainInUse: 602;
|
|
51
|
+
readonly serverNameExists: 603;
|
|
52
|
+
readonly serverNameInvalid: 608;
|
|
53
|
+
readonly serverNoData: 609;
|
|
54
|
+
readonly serverNotFound: 1453;
|
|
55
|
+
readonly messageNotFound: 701;
|
|
56
|
+
readonly inboundRuleNoData: 809;
|
|
57
|
+
readonly inboundRuleExists: 810;
|
|
58
|
+
readonly inboundRuleNotFound: 812;
|
|
59
|
+
readonly bounceNotFound: 1001;
|
|
60
|
+
readonly bounceCannotActivate: 1003;
|
|
61
|
+
readonly templateNotFound: 1101;
|
|
62
|
+
readonly templateNoData: 1109;
|
|
63
|
+
readonly streamTypeInvalid: 1221;
|
|
64
|
+
readonly streamNameRequired: 1223;
|
|
65
|
+
readonly streamNotFound: 1226;
|
|
66
|
+
readonly streamIdInvalid: 1227;
|
|
67
|
+
readonly streamCannotArchiveDefault: 1229;
|
|
68
|
+
readonly streamIdExists: 1230;
|
|
69
|
+
readonly streamCannotArchive: 1241;
|
|
70
|
+
readonly streamCannotUnarchive: 1232;
|
|
71
|
+
readonly streamIdReservedPrefix: 1233;
|
|
72
|
+
readonly sendStreamNotFound: 1235;
|
|
73
|
+
readonly dataRemovalIdInvalid: 1301;
|
|
74
|
+
readonly webhookArchivedStream: 1350;
|
|
75
|
+
readonly webhookNotFound: 1352;
|
|
76
|
+
readonly webhookUrlRequired: 1354;
|
|
77
|
+
readonly suppressionAuthority: 1406;
|
|
78
|
+
readonly suppressionInvalidEmail: 1408;
|
|
79
|
+
readonly suppressionNoBody: 1409;
|
|
80
|
+
};
|
|
81
|
+
export declare function handlePostmarkTwinRequest(req: PostmarkRequest): Promise<PostmarkResponse>;
|
|
82
|
+
export declare const POSTMARK_RESOURCE_TYPES: readonly ["message", "bounce", "template", "message_stream", "webhook", "suppression", "server", "domain", "sender_signature", "inbound_rule", "inbound_message", "data_removal"];
|