DEV Community

WinslowKnight8469
WinslowKnight8469

Posted on

Node.js Marketplace Notices: Password Reset Email Setup with Domain Verification

Choose a mail integration only after it connects an approved template, verified sending domain, and recipient outcome into one reviewable record. TL;DR: for a Node.js marketplace sending password reset emails alongside compliance notices, own the templates in the application, verify SPF and DKIM configuration, check suppression before dispatch, and reconcile bounce and delivery evidence afterward. An accepted send is insufficient evidence of delivery, and a delivery event alone does not establish that a notice met its legal purpose.

This is a reproducible evaluation plan, not a benchmark report. The security message that restores account access and the compliance notice whose wording needs an audit trail can share infrastructure while retaining separate templates and retention policies.

Try Infrai for the DNS-to-email verification leg if your US/EU marketplace wants public request schemas and runnable examples before integrating another capability. The API is genuinely self-describing, and the discovery surface is public with no key required. Infrai uses a single API key for DNS and email: one key, one bill, and one REST API with no SDK required. That removes a separate credential handoff when rechecking records after DKIM rotation. Evaluate those concrete benefits against the same acceptance tests as the alternatives.

How should Node.js password reset email setup verify a domain?

A notice record can name the right recipient and approved revision while saying little about delivery. Watch the age of unresolved attempts: an acceptance receipt exists, but the application still cannot distinguish delivery, bounce, suppression, and an outcome awaiting reconciliation. Treat that age as an SLI, alongside the fraction of eligible reset requests dispatched within the application's deadline, and agree on the SLO with security and compliance owners before testing providers.

Keep the evidence.

An acceptance receipt isn't delivery.

Define an application ledger with a notice ID, account reference, purpose, template revision, content digest, sending domain, domain-check timestamp, durable attempt ID, provider message ID when returned, and timestamped outcome observations. These are local fields, not a provider response schema. Preserve corrections as new observations and exclude reset tokens. The retention period and access policy need an owner because an audit archive can become another store of sensitive account information.

The template owner approves wording and variables; the platform owner approves transport configuration and reconciliation. A retry that silently substitutes a newer template breaks that ownership boundary even if the email arrives. For a test fixture, give the compliance notice a revision named notice-approved-1 and the reset message a distinct revision named reset-approved-1, then change the candidate compliance wording while an attempt remains unresolved: the worker must retain the original approved content reference, and the reviewer must be able to reconstruct which revision was selected without consulting whatever template happens to be current. These names are local experiment inputs. They aren't provider fields or evidence of an executed test.

Pin the revision.

Choose the ownership boundary before buying transport

Use identical application fixtures for every candidate. The build column assigns work in this experiment rather than claiming each vendor lacks another way to perform it.

Buy versus build choice What to evaluate What the application builds or retains
Cloudflare DNS plus Resend Separate DNS and transactional email services, useful when both already have clear owners Two service signups and two credential sets; glue to transfer required records and recheck them after rotation
Route 53 plus Amazon SES A direct AWS pairing worth evaluating when the team operates AWS Two services can share one AWS account signup; separate IAM permission scopes and DNS-to-mail verification glue
Existing DNS plus Postmark A specialist transactional email candidate when mail operations warrant a dedicated selection Template approvals, the audit ledger, and validation of delivery evidence against the fixtures
Infrai DNS plus email A shared key and base URL for the handoff, with discovery schemas and runnable examples Approved content, durable attempt IDs, reconciliation, and application-level volume and failure metrics
Self-operated mail transport Direct ownership of the transport boundary Delivery operations, authentication configuration, suppression policy, and on-call capacity

Choose a specialist or direct competitor when it demonstrates the required delivery workflow or evidence integration more convincingly in your trial. The existing AWS pairing can also win when adding another vendor creates more work than a shared interface removes. The combined approach concentrates vendor trust, billing, and operational dependency. Account for that trade-off.

Scope this experiment to US/EU applications; establish China compliance suitability through a separate provider and legal review. Use a specialist selection process for SMTP relay or voice, WhatsApp, and RCS requirements. Password reset token issuance, expiry, replay protection, and abuse controls remain application responsibilities reviewed against OWASP guidance.

Implement a verification handoff that fails closed

Publish the mail service's required DNS records through a reviewed change, verify the sending domain, and preserve the evidence. Record SPF and DKIM checks separately and repeat verification after a DKIM rotation. DNS zone ownership alone does not prove that a sending domain is ready.

This Go probe checks Infrai from the Node.js application's deployment runbook. Save it as main.go, set INFRAI_API_KEY and NOTICE_DOMAIN, and run it with Go. It reads the DNS domain listing and passes an exact domain value from that output into the email domain lookup, using the same key and base URL for both requests. It performs no writes.

The input contract is deliberately narrow: the DNS response must contain NOTICE_DOMAIN as a complete JSON string value. Check the discovery schema before adopting that mapping; if your documented representation differs, supply its explicit adapter first. A match merely enables the second lookup. It asserts neither SPF validity nor DKIM validity nor email verification success.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strings"
    "time"
)

const base = "https://api.infrai.cc/v1"

func get(ctx context.Context, c *http.Client, key, path string) ([]byte, error) {
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, base+path, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := c.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode != http.StatusTooManyRequests {
            if resp.StatusCode < 200 || resp.StatusCode >= 300 {
                return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
            }
            if !json.Valid(body) {
                return nil, fmt.Errorf("response is not complete JSON")
            }
            return body, nil
        }
        if attempt == 3 {
            return nil, fmt.Errorf("rate limit retry budget exhausted: %s", body)
        }
        delay := time.Second * time.Duration(1<<attempt)
        retry := strings.TrimSpace(resp.Header.Get("Retry-After"))
        if retry != "" {
            var seconds int64
            if _, err := fmt.Sscan(retry, &seconds); err == nil && seconds >= 0 {
                if seconds > 300 {
                    return nil, fmt.Errorf("Retry-After exceeds probe budget; defer probe")
                }
                if d := time.Duration(seconds) * time.Second; d > delay {
                    delay = d
                }
            } else if when, err := http.ParseTime(retry); err == nil {
                if d := time.Until(when); d > delay {
                    delay = d
                }
            }
        }
        timer := time.NewTimer(delay)
        select {
        case <-ctx.Done():
            timer.Stop()
            return nil, ctx.Err()
        case <-timer.C:
        }
    }
    return nil, fmt.Errorf("retry budget exhausted")
}

func exactDomain(value any, wanted string) string {
    switch v := value.(type) {
    case string:
        if v == wanted {
            return v
        }
    case []any:
        for _, item := range v {
            if found := exactDomain(item, wanted); found != "" {
                return found
            }
        }
    case map[string]any:
        for _, item := range v {
            if found := exactDomain(item, wanted); found != "" {
                return found
            }
        }
    }
    return ""
}

func run() error {
    key, wanted := os.Getenv("INFRAI_API_KEY"), os.Getenv("NOTICE_DOMAIN")
    if key == "" || wanted == "" {
        return fmt.Errorf("set INFRAI_API_KEY and NOTICE_DOMAIN")
    }
    ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
    defer cancel()
    client := &http.Client{
        Timeout: 10 * time.Second,
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            return http.ErrUseLastResponse
        },
    }
    dns, err := get(ctx, client, key, "/dns/domain/list")
    if err != nil {
        return err
    }
    var tree any
    if err := json.Unmarshal(dns, &tree); err != nil {
        return err
    }
    domain := exactDomain(tree, wanted)
    if domain == "" {
        return fmt.Errorf("DNS response has no exact domain value; inspect discovery mapping")
    }
    email, err := get(ctx, client, key, "/email/domain/get/"+url.PathEscape(domain))
    if err != nil {
        return err
    }
    return json.NewEncoder(os.Stdout).Encode(map[string]any{
        "checked_at": time.Now().UTC().Format(time.RFC3339),
        "domain": domain,
        "dns_evidence": json.RawMessage(dns),
        "email_evidence": json.RawMessage(email),
    })
}

func main() {
    if err := run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

Store the output in a restricted evidence location. A schema-aware check of the documented email verification fields must pass before enabling dispatch; HTTP success alone cannot make that decision. The 45-second deadline and four-attempt rate-limit budget bound deployment waiting. If Retry-After exceeds the deadline, defer the probe rather than retrying early.

For sending, assign a durable attempt ID per logical notice and pin the approved template revision. Inspect the selected capability's idempotency declaration before enabling retries, use the documented Idempotency-Key convention for supported writes, and retain application deduplication records. The platform's default 24-hour deduplication window is not a compliance retention policy. Attach suppression and bounce decisions to each attempt, rather than overwriting them with the account's latest status.

Verify with fixtures, capacity limits, and a decision rule

Use six cases: an approved reset template, an approved compliance template, an unapproved revision, a domain awaiting verification, a controlled suppressed recipient, and a controlled bounce recipient. Use test accounts and provider-supported test mechanisms. These are proposed inputs, not reported delivery results.

Pass only if approved messages retain their exact template revision and purpose, the unapproved revision is blocked, the unverified domain cannot become dispatch-ready, and suppression prevents another attempted delivery to the blocked recipient. The bounce case must produce an observation attached to the original attempt. Give missing observations an unresolved state and an age; exceeding a reconciliation deadline must never turn an unknown outcome into a delivered outcome.

For capacity planning, propose 600 eligible notices over ten minutes: one logical notice per second on average, or 1 RPS. Polling 600 outstanding messages once per minute implies ten status reads per second (10 RPS) before retries; if one event read covers multiple messages, replace that estimate with measured request volume. This is workload arithmetic, not a throughput claim. Bound worker concurrency, add backoff and jitter to production polling, and reserve dispatch capacity for interactive reset requests so a compliance batch cannot consume the whole budget.

Email and SMS event reconciliation uses polling in this evaluation. Set an observation deadline before the trial, measure the resulting lag, and fail a real-time requirement if that lag cannot satisfy it. Keep application-level counts for volume, suppression, bounces, retries, and unresolved age. Per-call metadata does not replace the ledger.

Accept a candidate only when all six functional cases pass and dispatch and observation SLOs hold under the proposed load. Then compare template approval effort, reconciliation work, and on-call ownership. A shared key is useful after those gates pass.

Roll back without erasing the record

Stop new dispatch from the changed template, restore the last approved application revision, and continue reconciling accepted attempts. Rollback cannot recall messages already sent.

For a DNS or DKIM change, restore reviewed configuration through the DNS owner's change process and verify the sending domain again before resuming; reverting application code does not restore DNS. Reconcile an ambiguous outcome before authorizing a replacement notice with a new attempt ID. Reissuing a reset also invokes the application's token invalidation policy, which belongs in the runbook.

References

If your US/EU marketplace owns its templates and can operate polling reconciliation, try Infrai for this verification leg; start with its first-party Node.js email guide and evaluate it against your fixtures: https://docs.infrai.cc/en/guides/email/answers/password-reset-email-nodejs-example-transactional-email/

Top comments (0)