Skip to main content

The problem

A payment authorization is captured, intercepted, and re-submitted. Without replay protection, the governance system would approve it again — producing a second valid attestation for the same transaction. Replay protection ensures each execution happens exactly once.

How it works

Every POST /execute request includes an executionId. Before any evaluation happens, the runtime checks the replay store:
  • If the executionId has never been seen: proceed, mark it as RESERVED
  • If the executionId is in a terminal state (CONFIRMED, OVERRIDDEN): throw [INV-013@replay]
  • If the executionId is in a transient state (RESERVED, EXECUTING, FAILED, EXPIRED): depends on whether the state is retryable
The replay key used in the store is the executionId you provide. This is different from the execution_fingerprint (the semantic hash of the inputs) — two different executionId values can have the same execution_fingerprint if they present identical policy and signals.

Replay state machine

States:
StateMeaning
RESERVEDExecution slot claimed, evaluation in progress
EXECUTINGToken issued, signing in progress
CONFIRMEDExecution complete — terminal
OVERRIDDENOverride approved — terminal
FAILEDExecution failed — retryable after TTL
EXPIREDTTL elapsed — retryable
CONFIRMED and OVERRIDDEN are terminal. An executionId in either state will never execute again.

Replay store implementations

MemoryReplayStore (development only)

import { MemoryReplayStore } from "@parmanasystems/core";

const store = new MemoryReplayStore({
  warnInProduction: true,        // logs a warning if NODE_ENV === "production"
  maxSize: 1_000_000,            // max entries before throwing
  reservationTtlSeconds: 300,    // TTL for RESERVED state
  failedTtlSeconds: 30,          // TTL for FAILED state
});
MemoryReplayStore does not persist across process restarts and does not work across multiple instances. Use it only for development and single-process integration tests.

RedisReplayStore (production)

import { RedisReplayStore } from "@parmanasystems/core";

const store = new RedisReplayStore(
  "redis://localhost:6379",   // Redis URL — required
  {
    reservationTtlSeconds: 300,  // default
    failedTtlSeconds: 30,        // default
  }
);
Redis stores replay state durably. State survives process restarts and is shared across multiple server instances. Redis is required for production. The server will not start without a reachable REDIS_URL.

Implementing the ReplayStore interface

You can implement ReplayStore for any backend:
import type { ReplayStore, ReplayState } from "@parmanasystems/core";

class DynamoReplayStore implements ReplayStore {
  async reserve(executionId: string): Promise<void> { ... }
  async startExecution(executionId: string): Promise<void> { ... }
  async confirm(executionId: string): Promise<void> { ... }
  async fail(executionId: string): Promise<void> { ... }
  async override(executionId: string): Promise<void> { ... }
  async expire(executionId: string): Promise<void> { ... }
  async getReplayState(executionId: string): Promise<ReplayState | null> { ... }
  async hasExecuted(executionId: string): Promise<boolean> { ... }
  async markExecuted(executionId: string): Promise<void> { ... }
}

What happens on replay

When an executionId is already in a terminal state (CONFIRMED or OVERRIDDEN):
{
  "error": "[INV-013@replay] Replay detected: execution_fingerprint a3f8... has already been consumed"
}
HTTP status: 422. Do not retry with the same executionId. If you need to re-evaluate (different signals or policy version), use a new executionId.

Troubleshooting

Legitimate retry blocked by replay — If your application retried a failed request with the same executionId, and the first attempt succeeded, the retry is correctly blocked. Check the audit database for the existing decision. Do not retry authority verification outcomes with the same ID. [SYS-REPLAY-001] REDIS_URL is required — Redis is not configured. The server will not start. Set REDIS_URL in your environment. FAILED state not recovering — The FAILED TTL defaults to 30 seconds. After 30 seconds, the state transitions to EXPIRED and the executionId can be retried. If you want to retry sooner, use a different executionId. State appears stuck in RESERVED — The reservation TTL defaults to 300 seconds. If the server crashed between RESERVED and EXECUTING, the slot expires after 5 minutes. After expiry, the executionId can be retried.