Skip to content

Technical design — MOD-024 Device & session intelligence

Module: MOD-024 System: SD04 Payments Processing Repo: bank-payments FR scope: FR-137, FR-138, FR-139, FR-140 NFR scope: NFR-021, NFR-023, NFR-024 Policies satisfied: DT-001 (GATE), PAY-005 (ALERT), AML-005 (LOG) Author: AI coding agent (Claude) Date: 2026-05-07

Objective

MOD-024 is the fraud / AML-grade device & session intelligence store. It exists deliberately separate from MOD-068's auth-grade access.device_registry (which is the trust-for-login record). The two modules answer different questions:

Module DB Table Question answered
MOD-068 bank_app access.device_registry Can this device establish an authenticated session?
MOD-024 bank_payments payments.device_intelligence Should this device be trusted for payment initiation?

Both stores key on the same device_fingerprint_hash (SHA-256), so cross-correlation is possible without a cross-DB join. The two trust signals can diverge: a device can be auth-trusted (MOD-068 happy) while simultaneously raising fraud signals in MOD-024. Downstream payment gating (MOD-020 when built) reads MOD-024's check-device API, not MOD-068's registry.

Architecture

App  ──── /auth/session ───────────────► MOD-068 (issue session)
  │                                       │ writes access.device_registry
  │                                       │ emits bank.app.session_created
  │── (sequential, after session) ──────► MOD-024 /devices/observe
  │                                       │ validates session via MOD-068
  │                                       │ upserts payments.device_intelligence
  │                                       │ inserts payments.session_observations
  │                                       │ runs anomaly detection
  │                                       │ emits bank.payments.device_anomaly_detected

EventBridge bank-app (cross-bus) ─ session_created ─► Mod024SessionConsumerHandler
                                                       (first-touch NEW_DEVICE check)

EventBridge bank-payments         ─ fraud_alert_raised ─► Mod024FraudAlertConsumerHandler
  (MOD-023 future)                                         (sets device.flagged_as_fraudulent)

API Gateway HTTP API
   POST /internal/v1/devices/observe   ─► Mod024ObserveDeviceHandler  (FR-137 sync from app)
   POST /internal/v1/devices/check     ─► Mod024CheckDeviceHandler    (PAY-002 / NFR-021 hot path; MOD-020 caller)
   GET  /internal/v1/sessions/{id}     ─► Mod024SessionsQueryHandler  (FR-140 read API for MOD-022)

EventBridge bank-payments (publish)
   device_anomaly_detected            ◀─ observe-device, check-device, session-consumer

Five Lambdas; one HTTP API; two EventBridge consumer rules (cross-bus on bank-app + same-bus on bank-payments); six CloudWatch alarms; one dashboard. No scheduled jobs.

Tables

Table Migration Mutability Purpose
payments.device_intelligence V001 MUTABLE Per-device fraud/AML profile. Counters, last_seen_at, sticky boolean signals, trust_score, flagged_as_fraudulent are all updated by application code.
payments.session_observations V002 APPEND-ONLY (V004 trigger) Per-session fingerprint snapshot. FR-140 7-year retention.
payments.device_anomalies V003 APPEND-ONLY (V004 trigger) Per-anomaly record. AML-005 LOG surface.
payments.idempotency_keys (MOD-021 V005) n/a Shared SD04 idempotency store. We use module_id='MOD-024'.

The mutable / append-only split is intentional: the audit trail of what changed and when lives in the immutable companion tables; the current aggregate state lives in the mutable one.

payments.device_intelligence — fields of interest

  • device_fingerprint_hash — canonical key, shared with MOD-068.
  • trust_score ∈ [0.00, 1.00] — MOD-024's fraud-trust assessment. Independent of MOD-068's device_status / trust_level. Decreases on each anomaly.
  • flagged_as_fraudulent — sticky flag set by the fraud-alert-consumer when MOD-023 emits a fraud_alert. Once set, every check-device for the device returns action_recommended=BLOCK.
  • is_emulator, is_rooted, is_jailbroken — sticky boolean signals (OR'd on each observation; once true, stay true).

Dev / UAT seed (V900)

Fingerprint hash trust_score flagged_as_fraudulent Purpose
aaaa…aa 1.00 false Known-good device for happy-path tests
ffff…ff 0.10 true Flagged device for FR-138 BLOCK assertion

Lambdas

Function Trigger Purpose
Mod024ObserveDeviceHandler HTTP POST /internal/v1/devices/observe FR-137 — capture rich fingerprint per-session. Sequential after MOD-068 session establishment.
Mod024CheckDeviceHandler HTTP POST /internal/v1/devices/check FR-138/139 — payment hot path; MOD-020 caller.
Mod024SessionsQueryHandler HTTP GET /internal/v1/sessions/{session_id} FR-140 — read API for MOD-022 (when built).
Mod024SessionConsumerHandler EventBridge bank.app.session_created (cross-bus) First-touch new-device check on every authenticated session.
Mod024FraudAlertConsumerHandler EventBridge bank.payments.fraud_alert_raised Sets flagged_as_fraudulent=true on the device when MOD-023 (future) emits a fraud alert.

Memory: 512 MB. Timeouts: 10s hot-path, 30s consumer. Reserved concurrency tiered (prod / uat / dev).

EventBridge

Consumes

Event Source bus Notes
bank.app.session_created bank-app (cross-bus) MOD-068 publisher. Cross-bus rule needs MOD-104 grant.
bank.payments.fraud_alert_raised bank-payments (same-bus) MOD-023 future publisher. Rule binds; no events flow until MOD-023 ships.

Publishes

Event Notes
bank.payments.device_anomaly_detected NEW — pending wiki catalogue add. Includes payment_id: uuid \| null (populated when triggered from check-device, null from observe path).

Sequential observe flow (FR-137)

The observe API requires a valid MOD-068 session token. The flow is sequential, not parallel:

1. App authenticates to MOD-068 (POST /auth/session) → receives session_token
2. App calls MOD-024 (POST /internal/v1/devices/observe) with Bearer session_token + rich fingerprint
3. MOD-024 invokes MOD-068 validate-session via SDK Lambda invoke
4. On 200, MOD-024 stores observation + runs anomaly detection
5. Anomaly event(s) emitted on bank-payments bus

Step 3 is the gate: the observe handler will not persist a row without a valid MOD-068 session. This is the boundary between auth identity and fraud signal.

SSM contract

Reads

Path Owner
/bank/{stage}/neon/pooler-host, /bank/{stage}/neon/direct-host MOD-103
/bank/{stage}/eventbridge/bank-payments/arn MOD-104
/bank/{stage}/eventbridge/bank-app/arn MOD-104
/bank/{stage}/iam/lambda/bank-payments/arn MOD-104
/bank/{stage}/observability/adot-nodejs-layer-arn MOD-076
/bank/{stage}/sns/alerts/arn MOD-104
/bank/{stage}/mod068/validate-session/fn-arn MOD-068

Writes

Path Value
/bank/{stage}/mod-024/api/base-url API Gateway base URL
/bank/{stage}/mod-024/observe-device/url Full /internal/v1/devices/observe URL
/bank/{stage}/mod-024/check-device/url Full /internal/v1/devices/check URL
/bank/{stage}/mod-024/sessions-query/url Full /internal/v1/sessions URL prefix
/bank/{stage}/mod-024/{observe-device,check-device,sessions-query,session-consumer,fraud-alert-consumer}-lambda/arn Lambda ARNs
/bank/{stage}/mod-024/device-intelligence-table payments.device_intelligence
/bank/{stage}/mod-024/session-observations-table payments.session_observations
/bank/{stage}/mod-024/device-anomalies-table payments.device_anomalies

Configuration

Env Default Purpose
IMPOSSIBLE_TRAVEL_KM 500 FR-139 distance threshold
IMPOSSIBLE_TRAVEL_WINDOW_MINUTES 30 FR-139 time window
MOD068_VALIDATE_SESSION_LAMBDA_ARN (from SSM) observe-device session validator
LOG_LEVEL info (prod) / debug (non-prod) Log gate

Policy mapping

Policy Mode How satisfied Test
DT-001 GATE Detection runs unconditionally on every observe / check. Source contains no skip / override / SOC-bypass tokens. KNOWN_FRAUD_DEVICE always emits with action=BLOCK. tests/policy/dt-001-gate.test.ts (unit + token scan) + tests/integration/fr-138-check-device.test.ts (live)
PAY-005 ALERT Every FR-138/139 condition emits a device_anomaly_detected event with severity + action_recommended; downstream consumers (MOD-068 step-up, MOD-018 case mgmt, MOD-022 audit) act on it. tests/policy/pay-005-alert.test.ts (signal coverage + schema)
AML-005 LOG payments.session_observations and payments.device_anomalies are append-only via V004 triggers. INSERT-only role grants. Application code emits no UPDATE/DELETE. tests/policy/aml-005-log.test.ts (structural) + tests/integration/nfr-024-audit-immutability.test.ts (runtime)

Performance approach

  • NFR-021 ≤ 200 ms p99 on the check-device hot path: a single findByFingerprint SELECT + optional customerHasUsedDeviceBefore EXISTS, plus optional INSERT on anomaly. withConnection for the read-mostly path; withTransaction only when an anomaly is being recorded. Alarm trips at p99 ≥ 200 ms.
  • NFR-023 MTTD ≤ 5 min on anomalous access: CloudWatch alarm (bank-{stage}-MOD-024-critical-anomaly) trips on the first CRITICAL anomaly metric in any 1-minute window.

Error handling

  • Sync HTTP paths — standard error envelope (HTTP 422 / 502 / 503 / 500).
  • Observe-device — 401-class errors when session validation fails (mapped from MOD-068 to SESSION_NOT_VALIDATED 422).
  • EventBridge consumer — re-raise on transient failures so EB retries; bank-payments / bank-app DLQ catches after retry exhaustion.

Event types in structured logs

device_observed, device_observe_failed, device_check_passed, device_check_failed, anomaly_detected, anomaly_publish_failed, device_flagged_fraudulent, session_consumed, session_consume_failed, fraud_alert_consumed, sessions_query_served, session_validated, session_validation_failed, idempotency_replay, trace_id_missing_from_upstream, validation_failed, internal_error.

Test approach

Tier Files Cases
Unit (≥80% gate) tests/unit/{errors,trace,logger,emf,fingerprint-hash,impossible-travel,anomaly-detector}.test.ts 35
Contract tests/contract/{device-anomaly-detected-schema,api-contract}.test.ts 17
Policy satisfaction tests/policy/{dt-001-gate,pay-005-alert,aml-005-log}.test.ts 12
Integration tests/integration/{fr-138-check-device,fr-139-anomaly-types,fr-140-sessions-query,nfr-024-audit-immutability,idempotency,observability-fields}.test.ts one per FR + idempotency + observability + NFR-024
Smoke tests/verify-deployment.mjs check-device against V900 flagged device

Security and data handling

  • No customer PII flows through MOD-024 except customer_id (uuid reference). The rich fingerprint (OS / app / screen / network) is device data, not PII.
  • Raw ip_address is not stored — only ip_region (e.g. NZ-AKL). Geolocation is rounded to 1dp (~11 km cell) at ingest.
  • The two audit tables have INSERT-only role grants + trigger-level append-only enforcement.

Open items / handoff follow-ups

  1. bank.payments.device_anomaly_detected — wiki catalogue add. Schema bundled in this module's schemas/. Consumers: MOD-022 (audit trail), MOD-018 (case mgmt — future), MOD-068 (step-up — future). Add to bank-wiki/source/pages/design/system/event-catalogue.md.

  2. SD04 data model — three new tables. Add to bank-wiki/source/pages/design/system/data-models/SD04-payments.md:

  3. payments.device_intelligence (mutable)
  4. payments.session_observations (immutable per ADR-048 Cat 1)
  5. payments.device_anomalies (immutable per ADR-048 Cat 1)

  6. Event-catalogue clarification. The existing bank.app.session_created entry says device_fingerprint_id: uuid ✓ "Reference to MOD-024 device record". As-built, that field references MOD-068's access.device_registry.id, not MOD-024's. Update the catalogue note.

  7. Cross-bus IAM grant. BankPaymentsRole needs events:PutRule + events:PutTargets on the bank-app bus. Filed in docs/handoffs/MOD-104-bank-app-cross-bus-grant.handoff.md. Same pattern as MOD-082's bank-core grant. Per the established contract pattern: SST resource step fails until grant lands; build is not gated on the grant.

  8. bank.payments.fraud_alert_raised consumer — MOD-023 isn't built yet. The rule binds, no events flow. The fraud-alert-consumer Lambda is wired; once MOD-023 ships, devices flagged by fraud alerts automatically propagate to MOD-024.

  9. MOD-068 session-token forwarding from MOD-024 events — when MOD-068 ships an enriched session_created event with the rich fingerprint inline, the session-consumer Lambda can fold the same anomaly logic the observe API runs. v2 — until then the observe path is the canonical fingerprint-capture surface.