// Node.js 22+. No npm dependencies. All activity requires a verified Test key. // node --env-file=.env.stampwing stampwing-test-send.mjs [--check | --status MESSAGE_UUID] const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const statuses = new Set(['queued', 'submitted', 'accepted', 'bounced', 'complained', 'failed', 'expired', 'uncertain']); const object = value => value !== null && typeof value === 'object' && !Array.isArray(value); const metadata = value => typeof value === 'string' && /^[A-Za-z0-9_.:-]{1,128}$/.test(value) ? value : undefined; function retrySeconds(value) { if (!value) return undefined; if (/^\d{1,9}$/.test(value)) return Number(value); if (!/^[A-Za-z]{3}, \d{2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(value)) return undefined; const date = Date.parse(value); return Number.isFinite(date) ? Math.max(0, Math.ceil((date - Date.now()) / 1000)) : undefined; } let apiKey = '', phase = 'configuration', acceptedId; async function main() { const args = process.argv.slice(2); if (args.length === 1 && args[0] === '--help') { console.log('Usage: node --env-file=.env.stampwing stampwing-test-send.mjs [--check | --status MESSAGE_UUID]\nDefault: verify Test and POSTRUNE_EXPECTED_PROJECT_ID, submit one simulated message, then read its status.\n--check: verify the saved operation key and Test send/read permissions; print the project UUID without sending.\n--status: read an existing Test message without submitting or needing an operation key.\nPOSTRUNE_EXPECTED_PROJECT_ID: required for sending; when supplied, all modes verify it. Copy it independently from the intended project Settings.'); return; } const action = args.length === 0 ? 'send' : args.length === 1 && args[0] === '--check' ? 'check' : args.length === 2 && args[0] === '--status' && uuid.test(args[1]) ? 'status' : undefined; if (!action) throw new Error('Use --help, --check, or --status followed by a message UUID. No request was made.'); const required = name => { const value = process.env[name]?.trim(); if (!value || value.startsWith('REPLACE_')) throw new Error(`Set ${name} in your private environment file.`); return value; }; let origin; try { origin = new URL(required('POSTRUNE_BASE_URL')); } catch { throw new Error('Set POSTRUNE_BASE_URL to the installation origin, for example http://127.0.0.1:3017.'); } if (origin.pathname !== '/' || origin.search || origin.hash || origin.username || origin.password) { throw new Error('Use only the deployment origin, without a path, query, fragment, or credentials.'); } if (origin.protocol !== 'https:' && !(origin.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(origin.hostname))) { throw new Error('Use HTTPS, or loopback HTTP for local development.'); } apiKey = required('POSTRUNE_API_KEY'); if (/[\u0000-\u0020\u007f]/.test(apiKey)) throw new Error('The API key must not contain whitespace or control characters.'); const expectedProjectId = action === 'send' ? required('POSTRUNE_EXPECTED_PROJECT_ID') : process.env.POSTRUNE_EXPECTED_PROJECT_ID?.trim(); if (expectedProjectId && !uuid.test(expectedProjectId)) throw new Error('Set POSTRUNE_EXPECTED_PROJECT_ID to the UUID from the intended project Settings.'); let operationKey; if (action !== 'status') { operationKey = required('POSTRUNE_OPERATION_KEY'); if (!/^[A-Za-z0-9_.:-]{8,128}$/.test(operationKey)) throw new Error('Use a saved operation key of 8–128 letters, numbers, dots, underscores, colons, or hyphens.'); if (/^(wf\.|smtp:|template-test:|out\.|signup\.|marketing:)/.test(operationKey)) throw new Error('Choose your own operation-key prefix; this prefix is reserved by Stampwing.'); } async function request(path, options = {}, expectedStatus = 200) { let response; try { response = await fetch(new URL(path, origin), { ...options, headers: { Authorization: `Bearer ${apiKey}`, ...options.headers }, redirect: 'error', signal: AbortSignal.timeout(15_000), }); } catch { throw new Error('No HTTP response was confirmed within the request deadline. Check the origin, network, and server.'); } let data, parseFailure = false; try { // Bound even non-JSON proxy errors. The deadline also covers response-body reads. const reader = response.body?.getReader(); if (!reader) throw new Error('Empty response'); const chunks = []; let size = 0; try { for (;;) { const { done, value } = await reader.read(); if (done) break; size += value.byteLength; if (size > 256 * 1024) throw new Error('Response too large'); chunks.push(value); } } finally { await reader.cancel().catch(() => {}); reader.releaseLock(); } data = JSON.parse(Buffer.concat(chunks).toString('utf8')); if (!object(data)) throw new Error('Expected an object'); } catch { parseFailure = true; } const requestId = metadata(object(data) ? data.requestId : undefined) || metadata(response.headers.get('x-request-id')); const retryAfter = retrySeconds(response.headers.get('retry-after')); const context = `${requestId ? ` Request ID: ${requestId}.` : ''}${retryAfter !== undefined ? ` Retry-After: ${retryAfter} seconds.` : ''}`; if (!response.ok) { const code = object(data) && typeof data.code === 'string' && /^[A-Z_]{1,80}$/.test(data.code) ? data.code : 'API_ERROR'; throw new Error(`HTTP ${response.status} ${code}.${context}${parseFailure ? ' The response was not a valid bounded JSON object.' : ''} See the troubleshooting guide. Nothing is automatically retried.`); } if (response.status !== expectedStatus) throw new Error(`Unexpected HTTP ${response.status}; expected HTTP ${expectedStatus}.${context} Check the installation and existing operation before trying again.`); if (parseFailure) throw new Error(`HTTP ${response.status} returned an unreadable or oversized JSON response.${context} Check the existing operation before trying again.`); return data; } phase = 'verification'; const check = await request('/api/v1/doctor'); if (!object(check.credential) || check.credential.valid !== true || check.credential.environment !== 'test' || check.sent !== false) { throw new Error('Stopped before sending: this example requires a verified Test credential.'); } if (typeof check.credential.projectId !== 'string' || !uuid.test(check.credential.projectId)) throw new Error('The credential response does not identify a valid project UUID. No send was attempted.'); if (expectedProjectId && check.credential.projectId.toLowerCase() !== expectedProjectId.toLowerCase()) throw new Error('The Test credential belongs to a different project than POSTRUNE_EXPECTED_PROJECT_ID. Check the private configuration and shell overrides. No send was attempted.'); const permissions = check.credential.permissions; if (!Array.isArray(permissions) || !permissions.every(value => typeof value === 'string')) throw new Error('The credential response contains an invalid permission list.'); for (const permission of action === 'status' ? ['email:read'] : ['email:send', 'email:read']) { if (!permissions.includes(permission)) throw new Error(`The Test key needs ${permission}.`); } if (action === 'check') { console.log(`Test credential verified for project ${check.credential.projectId} with email:send and email:read. Saved operation-key format checked. No message was submitted.`); console.log(expectedProjectId ? 'Expected project matched. This check does not reserve an operation or guarantee that a later send passes project limits.' : 'Project identity has not been compared. Copy POSTRUNE_EXPECTED_PROJECT_ID independently from the intended project Settings before sending; do not accept this response as proof of your intended project.'); return; } const validMessage = value => object(value) && typeof value.id === 'string' && uuid.test(value.id) && value.mode === 'demo' && statuses.has(value.status); if (action === 'send') { phase = 'submission'; const result = await request('/api/v1/emails', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': operationKey }, body: JSON.stringify({ from: 'hello@postrune.test', to: 'reader@example.com', subject: 'Your Stampwing integration is ready', text: 'This is a simulated Test message. No email is delivered.', }), }, 202); if (!validMessage(result)) throw new Error('Unexpected acceptance response. Inspect Messages before trying again.'); acceptedId = result.id; console.log(JSON.stringify({ id: result.id, status: result.status, mode: result.mode })); } phase = 'status'; const id = acceptedId || args[1]; const progress = await request(`/api/v1/emails/${id}`); if (!validMessage(progress) || progress.id.toLowerCase() !== id.toLowerCase()) throw new Error('The status response does not match the requested Test message.'); console.log(JSON.stringify({ id: progress.id, status: progress.status, mode: progress.mode })); console.log(action === 'send' ? 'Test request accepted. Delivery is simulated; status may already have advanced.' : 'Test status retrieved. No message was submitted.'); } main().catch(error => { let message = error instanceof Error ? error.message : 'The request could not be confirmed.'; if (apiKey) message = message.split(apiKey).join('[redacted]'); console.error(message); if (acceptedId) { console.error(`The send was accepted as ${acceptedId}; only the status lookup failed. Read it again with --status ${acceptedId}. Do not create a new send.`); process.exitCode = 2; } else { console.error(phase === 'submission' ? 'Submission is unconfirmed. Keep the same saved operation key and frozen payload; inspect Messages before retrying.' : 'No message was submitted by this run.'); process.exitCode = 1; } });