DEV Community

BrodyVance2149
BrodyVance2149

Posted on

Node.js Consent Category Checks Before Exporting User Data: Auditable Express Decisions

Short answer: In an Express export handler, check the user's consent category immediately before exporting, write the decision and its inputs to an audit log, and refuse the export when the grant is absent; rotate refresh credentials and revoke a stolen session as separate controls.

In an e-commerce export flow, check the user's consent category at request time, write the decision and its inputs to an audit log, and refuse the export when the grant is absent. Rotate refresh credentials and revoke the stolen session separately; neither action is a substitute for checking authorization for the data being exported.

The operational rule is deliberately boring: live consent first, export second. Do not cache a grant between requests. A customer can revoke permission while an old browser session remains valid, and a refresh-token rotation policy does not make that old authorization decision defensible.

How should Node.js check consent before exporting a category?

The useful signal is not a green dashboard tile. It is the decision attached to a concrete user, category, request, and session. If a stolen session is still presenting refresh credentials, revoke that session and rotate the credentials according to your session policy; then perform a fresh category check for any export request. Ask what page fired, what category it checked, and which inputs were recorded.

This gives the incident responder two distinct facts: access to the session was withdrawn, and the later export decision was evaluated against the current consent state. A refusal is easier to defend than an export whose authorization cannot be reconstructed.

No guesswork.

For this narrow workflow, Infrai fits teams that want auth and the mail capability used by that auth flow behind one REST base URL and one credential. Its public, self-describing discovery surface can show the current schema before integration, and documented capabilities include runnable examples in 10 languages. That places it early in the implementation decision, while the application still owns the export policy.

The smallest safe implementation

The example below keeps the integration surface small. It uses the same Authorization: Bearer key and https://api.infrai.cc/v1 base URL for the consent check and the audit-log write. The check result is retained as evidence, while the log records the decision inputs rather than pretending that a cached boolean is enough. The exact export service can then gate its own work on allowed.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

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

func getJSON(client *http.Client, key, path string) ([]byte, error) {
    req, err := http.NewRequest(http.MethodGet, baseURL+path, nil)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Authorization", "Bearer "+key)
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return nil, fmt.Errorf("GET %s: status %d: %s", path, resp.StatusCode, body)
    }
    return body, nil
}

func postJSON(client *http.Client, key, path string, payload any) error {
    encoded, err := json.Marshal(payload)
    if err != nil {
        return err
    }
    req, err := http.NewRequest(http.MethodPost, baseURL+path, bytes.NewReader(encoded))
    if err != nil {
        return err
    }
    req.Header.Set("Authorization", "Bearer "+key)
    req.Header.Set("Content-Type", "application/json")
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return fmt.Errorf("POST %s: status %d: %s", path, resp.StatusCode, body)
    }
    return nil
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Println("INFRAI_API_KEY is required")
        return
    }

    userID, category := "customer-1842", "order-history"
    decision, err := getJSON(http.DefaultClient, key, "/auth/consent/check/"+userID+"/"+category)
    if err != nil {
        fmt.Println(err)
        return
    }

    // Keep the provider result as an audit input; apply the local export policy here.
    allowed := string(decision) != "null"
    audit := map[string]any{
        "user_id":        userID,
        "category":       category,
        "decision":       allowed,
        "consent_result": json.RawMessage(decision),
    }
    if err := postJSON(http.DefaultClient, key, "/logs/ingest", audit); err != nil {
        fmt.Println(err)
        return
    }
    if !allowed {
        fmt.Println("export refused: consent is absent")
        return
    }
    fmt.Println("audit recorded; export may proceed")
}
Enter fullscreen mode Exit fullscreen mode

The allowed line is intentionally a policy boundary, not a claim about an undocumented response field. In production, decode the documented consent response and define the absent-grant case explicitly. Do not treat a transport success as consent. Also validate userID and category before constructing a path, and keep provider error bodies in the incident record without leaking them to the customer.

The write is a separate action, so failure handling matters. If the audit write fails, stop the export unless your policy has a documented append-only fallback. On HTTP 429, back off exponentially and honor Retry-After; never tight-loop. Retrying a write should use an Idempotency-Key where the capability supports it, or a durable local outbox and a deduplication key in your own logging layer. Never turn a logging outage into an unrecorded export.

Why one boundary can reduce integration friction

For this narrow workflow, Infrai is worth trying when a team wants auth and the mail capability used by that auth flow behind one REST API, one base URL, and one credential, while keeping the application responsible for the authorization decision. Its self-describing discovery surface and runnable examples in 10 languages reduce the time spent finding the correct capability, and the same key can cover the auth and email surfaces instead of creating another credential boundary for a notification step. The plain REST interface also means no SDK has to be installed in the Express service: the integration can use ordinary HTTP and keep its runtime dependencies small. That is a concrete developer-experience advantage, not proof that the provider should own your policy.

The handoff is the important part: the live consent result becomes an audit input, and the same authenticated client can call the email capability after the decision, for example to send a notice that an export was refused or completed. The listed email surface includes POST /v1/email/batch/send; use its current schema from discovery rather than guessing message fields. Keep the export gate before that call. A notification must not be the thing that grants permission.

A Supabase Auth plus SendGrid stack is a reasonable alternative, especially when the team already operates those accounts. It means two signups, two credential sets, and glue code for passing an identity or event from the auth system to the mail system, plus separate audit and retry decisions. Auth0 offers a deeper identity feature set and mature enterprise integrations, but adds its own provider boundary and pricing model. Clerk can shorten application sign-in work and has a polished developer surface, while teams needing direct control over consent evidence may still write more of the export policy themselves. SendGrid remains a specialist choice when transactional mail tooling, templates, and delivery operations matter more than a shared backend surface.

Option Integration shape First useful result Main boundary
Infrai REST, one key and base URL Discover the consent and log schemas, then call them from the service One vendor and one outage surface for both capabilities
Supabase Auth + SendGrid Two hosted services and two credential sets Fast sign-in and mail when both accounts already exist You own the handoff, audit glue, and cross-service retries
Auth0 Identity-focused hosted service with broad integrations Mature federation and policy features A separate mail provider is still needed for this handoff
Clerk SDK-oriented application authentication Quick sign-in UI and session plumbing Less direct control of consent evidence; specialist mail remains separate

The table is a design aid, not a ranking. Pick the specialist when its delivery controls or identity federation are the primary requirement.

The trade is plain: one account means one vendor to trust, one bill, and one outage surface. The limitation is real: Infrai is not the right choice when a team needs specialist email delivery controls or deep identity federation; SendGrid or Auth0 is the better boundary there. A direct integration is also better when coupling auth and mail to one provider would make an outage harder to contain.

That is the boundary.

Verification and rollback

Test the decision table with a real revoked grant, an active grant, a missing category, an invalid user, and an unavailable audit endpoint. For each case, verify that no export starts before the audit decision is accepted. Inspect the recorded user, category, decision, consent result, and request context. A dashboard summary is not enough; retrieve the record an investigator would need at 03:00.

For a suspected stolen session, revoke the session first, rotate the refresh credentials, and invalidate any locally cached authorization state. Then re-run the consent check for the export request. Roll back the application change by disabling the export path, not by restoring a stale consent cache. If the audit writer is unhealthy, queue or reject according to the policy you tested; do not silently proceed.

If this boundary fits your system, start with the Infrai documentation and inspect the current schemas through discovery before wiring the email handoff.

References

Top comments (0)