---
title: "Node.js store fulfillment reference"
description: "Run the complete Node.js trusted-command receiver and durable licensing worker with private state, encrypted key export and Sandbox evidence."
slug: "/docs/v1/guides/any-store/node"
section: "Webhooks and integrations"
order: 781
audience: "all"
level: "advanced"
last_updated: "2026-10-18"
version: "v1"
related: ["/docs/v1/guides/any-store","/docs/v1/api/idempotency"]
draft: false
---
Run the complete Node.js reference after your trusted store adapter has independently verified the purchase and authorized one durable licensing effect.

## What you will learn

- Install the exact reference dependencies without changing KDPika's application.
- Start a loopback receiver and run its bounded worker separately.
- Inspect durable outcomes and export a saved encrypted key for private [delivery](/docs/v1/start/glossary#delivery).

## Prerequisites

- Read the complete [store backend protocol and mapping guide](/docs/v1/guides/any-store).
- Node.js 22.13+ with built-in node:sqlite; no npm packages. SQLite is experimental in this tested Node 22 runtime.
- Use a POSIX macOS/Linux-oriented private filesystem, not a public download or temporary state directory.
- Configure the private mapping and secret environment variables from the parent guide.
- Use a [Sandbox](/docs/v1/start/glossary#sandbox) [product](/docs/v1/start/glossary#product) and backend API key; do not set [Live](/docs/v1/start/glossary#live) confirmation for this test.

## Install and check

1. Download the [complete reference archive](/docs/downloads/store-fulfillment.zip).
2. Check its [SHA-256 source manifest](/docs/fulfillment-examples.json).
3. Extract the archive into a private backend project and enter its fulfillment directory.
4. Replace /private example paths with existing owner-only absolute paths for your backend.
5. Run the following installation/check commands.

```bash
node --version
node --check node/server.mjs
```

Expected result: The version is at least 22.13 and the syntax check exits 0. An experimental SQLite warning is not a successful fulfillment result.

Keep credentials in private environment configuration, not in these commands, source files, terminal screenshots or a browser. This source package has no hidden service installation step and does not alter platform billing.

## Start the receiver

```bash
node node/server.mjs serve
```

Expected result: the development receiver listens at HTTP 127.0.0.1:8112. It does not expose a public HTTPS service. For Node/Python/C#, KDP_FULFILLMENT_PORT selects another loopback port; PHP takes the port in its -S command. Keep this process running while sending a command from a separate terminal.

Follow [send to the private receiver](/docs/v1/guides/any-store#send-to-the-private-receiver) with your verified private command file. The supplied sender requires PHP/cURL; it is not provider payment verification. A first valid receipt is accepted:true and duplicate:false. No [license](/docs/v1/start/glossary#license) is created until work runs.

## Run the worker

```bash
node node/server.mjs work
```

Expected result for one new authorized issue: processed is 1 and states contains DONE:1. A completed unchanged replay processes 0 new effects. On uncertain/failed API outcomes, inspect REVIEW or IN_FLIGHT instead of assuming the operation did nothing. No full [license key](/docs/v1/start/glossary#license-key) is printed.

The worker uses atomic worker.lockdir creation because Node core has no flock API. A killed worker leaves the directory, refusing the next run. Confirm the process is stopped and reconcile held effects before removing only the reviewed empty lock directory. Do not automate stale-lock deletion.

Each run handles at most 25 ready effects and pauses 1.1 seconds after an attempt. The durable state must persist across cron invocations and deployments. Do not mix language implementations on the same state directory or clear it when rotating the scoped API key.

## Inspect and export

```bash
node node/server.mjs inspect
```

Expected result: states maps each existing effect state to its count. Inspection does not retry an API operation or print a complete key.

```bash
node node/server.mjs export YOUR_ORDER_ID /private/key.json
```

Expected result after verified DONE issuance and with a new private target path: exported:true and contains_secret:true. The file contains license_id, license_key and email. Export refuses overwrite. This confirms a file was written, not that an email was sent. Authenticate the intended recipient and use your private delivery workflow; never place the export in public_html.

## Complete source

The code below is included from the exact files executed by the Sandbox test. Common config.example.json, command.example.json, send-command.php and CONTRACT.md are in the same archive. The parent guide defines their required fields and security boundary in full.

```javascript
#!/usr/bin/env node
// Trusted fulfillment receiver/worker, not a payment gateway or a browser client.
// Node.js 22.13+; built-in SQLite, crypto and HTTP. No npm dependencies.
// Private KDP_FULFILLMENT_CONFIG/STATE/SECRET/OUTBOX_KEY and KDP_API_KEY required.
// Run: node server.mjs serve | work | inspect | export YOUR_ORDER_ID /private/key.json
// Expected: safe receipt/work JSON; full keys only in explicit private exports.
import * as fs from 'node:fs';
import path from 'node:path';
import http from 'node:http';
import { createHash, createHmac, timingSafeEqual, randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';
import { DatabaseSync } from 'node:sqlite';

const LIMIT = 262144;
const id = value => typeof value === 'string' && /^[A-Za-z0-9_.:-]{1,190}$/.test(value);
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
const sha = value => createHash('sha256').update(value).digest('hex');
const identity = (...values) => sha(values.join('\0'));
const fail = code => { throw new Refusal(code); };
const canonical = value => JSON.stringify(sort(value));
const now = () => Math.floor(Date.now() / 1000);
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
class Refusal extends Error {}

function sort(value) {
  if (Array.isArray(value)) return value.map(sort);
  if (!object(value)) return value;
  return Object.fromEntries(Object.keys(value).sort().map(key => [key, sort(value[key])]));
}

class Fulfillment {
  constructor() {
    process.umask(0o077);
    const file = process.env.KDP_FULFILLMENT_CONFIG ?? '';
    if (!path.isAbsolute(file)) fail('PRIVATE_ABSOLUTE_CONFIG_REQUIRED');
    this.config = JSON.parse(fs.readFileSync(file, 'utf8'));
    this.directory = process.env.KDP_FULFILLMENT_STATE ?? '';
    if (!path.isAbsolute(this.directory)) fail('PRIVATE_ABSOLUTE_STATE_REQUIRED');
    if (fs.existsSync(this.directory) && fs.lstatSync(this.directory).isSymbolicLink()) fail('INVALID_STATE_DIRECTORY');
    fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
    if (fs.statSync(this.directory).mode & 0o077) fail('PRIVATE_STATE_PERMISSIONS_REQUIRED');
    this.secret = process.env.KDP_FULFILLMENT_SECRET ?? '';
    const key = process.env.KDP_FULFILLMENT_OUTBOX_KEY ?? '';
    this.outboxKey = Buffer.from(key, 'base64');
    if (this.secret.length < 32 || this.outboxKey.length !== 32 || this.outboxKey.toString('base64') !== key) fail('PRIVATE_SECRETS_REQUIRED');
    this.environment = this.config.environment;
    if (!['sandbox', 'live'].includes(this.environment)) fail('INVALID_ENVIRONMENT');
    if (this.environment === 'live' && process.env.KDP_CONFIRM_LIVE !== '1') fail('LIVE_CONFIRMATION_REQUIRED');
    for (const field of ['source', 'seller_id', 'product_id']) if (!id(this.config[field])) fail('INVALID_CONFIGURATION');
    if (!object(this.config.offers) || !Object.keys(this.config.offers).length) fail('OFFERS_REQUIRED');
    for (const [offerId, offer] of Object.entries(this.config.offers)) {
      if (!id(offerId) || !id(offer?.plan_id) || !Number.isInteger(offer.renewal_days) || offer.renewal_days < 1 || offer.renewal_days > 3650) fail('INVALID_OFFER_MAPPING');
    }
    this.base = this.config.base_url.replace(/\/+$/, '');
    const url = new URL(this.base);
    const local = this.environment === 'sandbox' && process.env.KDP_ALLOW_LOOPBACK === '1' && url.protocol === 'http:' && url.hostname === '127.0.0.1';
    if (!url.hostname || url.username || url.password || url.search || url.hash || url.pathname !== '/' || (url.protocol !== 'https:' && !local)) fail('INVALID_BASE_URL');
    this.apiKey = process.env.KDP_API_KEY ?? '';
    if (!this.apiKey.startsWith('lmp_' + this.environment + '_')) fail('API_KEY_ENVIRONMENT_MISMATCH');
    const database = path.join(this.directory, 'fulfillment.sqlite');
    if (fs.existsSync(database) && fs.lstatSync(database).isSymbolicLink()) fail('INVALID_STATE_FILE');
    this.db = new DatabaseSync(database);
    fs.chmodSync(database, 0o600);
    this.db.exec(`PRAGMA busy_timeout=3000;
      CREATE TABLE IF NOT EXISTS inbox(event_id TEXT PRIMARY KEY,body_hash TEXT NOT NULL,effect_key TEXT NOT NULL,received_at INTEGER NOT NULL);
      CREATE TABLE IF NOT EXISTS orders(order_key TEXT PRIMARY KEY,offer_id TEXT NOT NULL,email TEXT NOT NULL,license_id TEXT,last_sequence INTEGER NOT NULL DEFAULT 0,held INTEGER NOT NULL DEFAULT 0);
      CREATE TABLE IF NOT EXISTS effects(effect_key TEXT PRIMARY KEY,order_key TEXT NOT NULL,sequence INTEGER NOT NULL,command_hash TEXT NOT NULL,command_json TEXT NOT NULL,state TEXT NOT NULL,request_id TEXT,error_code TEXT,created_at INTEGER NOT NULL,UNIQUE(order_key,sequence));
      CREATE TABLE IF NOT EXISTS outbox(order_key TEXT PRIMARY KEY,license_id TEXT NOT NULL,cipher TEXT NOT NULL,created_at INTEGER NOT NULL);
      CREATE TABLE IF NOT EXISTS binding(id INTEGER PRIMARY KEY CHECK(id=1),config_hash TEXT NOT NULL,outbox_key_hash TEXT NOT NULL);`);
    const configHash = sha(canonical(this.config)), keyHash = sha(this.outboxKey);
    this.run('INSERT OR IGNORE INTO binding(id,config_hash,outbox_key_hash) VALUES(1,?,?)', configHash, keyHash);
    const bound = this.one('SELECT * FROM binding WHERE id=1');
    if (bound.config_hash !== configHash || bound.outbox_key_hash !== keyHash) fail('CONFIGURATION_CHANGED_RECONCILE_FIRST');
  }

  run(sql, ...values) { return this.db.prepare(sql).run(...values); }
  one(sql, ...values) { return this.db.prepare(sql).get(...values); }
  transaction(operation) {
    this.db.exec('BEGIN IMMEDIATE');
    try { const result = operation(); this.db.exec('COMMIT'); return result; }
    catch (error) { this.db.exec('ROLLBACK'); throw error; }
  }

  validate(event) {
    const fields = ['version', 'source', 'seller_id', 'environment', 'event_id', 'effect_id', 'order_id', 'sequence', 'action', 'offer_id', 'customer', 'expected_status', 'expected_expires_at'];
    if (!object(event) || canonical(Object.keys(event).sort()) !== canonical(fields.sort()) || event.version !== 1) fail('INVALID_COMMAND');
    for (const field of ['source', 'seller_id', 'environment']) if (event[field] !== this.config[field]) fail('WRONG_SELLER_OR_ENVIRONMENT');
    for (const field of ['event_id', 'effect_id', 'order_id', 'offer_id']) if (!id(event[field])) fail('INVALID_IDENTIFIER');
    if (!Object.hasOwn(this.config.offers, event.offer_id) || !['issue', 'renew', 'suspend', 'restore', 'revoke', 'record_cancellation'].includes(event.action) || !Number.isInteger(event.sequence) || event.sequence < 1 || event.sequence > 2147483647) fail('UNSUPPORTED_COMMAND');
    const customer = event.customer;
    if (!object(customer) || canonical(Object.keys(customer).sort()) !== '["email","name"]' || typeof customer.email !== 'string' || !/^[^\s@]{1,120}@[^\s@]{1,120}$/.test(customer.email) || customer.email.length > 190 || typeof customer.name !== 'string' || [...customer.name].length > 160) fail('INVALID_CUSTOMER');
    if (event.expected_expires_at !== null && (typeof event.expected_expires_at !== 'string' || event.expected_expires_at.length > 40)) fail('INVALID_EXPECTED_EXPIRY');
    if (event.action === 'issue') {
      if (event.sequence !== 1 || event.expected_status !== null || event.expected_expires_at !== null) fail('INVALID_ISSUANCE_COMMAND');
    } else if (!['active', 'suspended', 'revoked', 'expired', 'pending', 'cancelled'].includes(event.expected_status)) fail('CURRENT_STATE_REQUIRED');
    return event;
  }

  accept(body, signature) {
    const match = /^t=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})$/.exec(signature);
    if (body.length > LIMIT || !match || Math.abs(now() - Number(match[1])) > 300) fail('INVALID_SIGNATURE');
    const actual = createHmac('sha256', this.secret).update(match[1] + '.').update(body).digest();
    if (!timingSafeEqual(actual, Buffer.from(match[2], 'hex'))) fail('INVALID_SIGNATURE');
    const event = this.validate(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(body)));
    const orderKey = identity(event.source, event.seller_id, event.environment, event.order_id);
    const effectKey = identity(event.source, event.seller_id, event.environment, event.effect_id);
    const semantic = { ...event }; delete semantic.event_id;
    const json = canonical(semantic), commandHash = sha(json), rawHash = sha(body);
    return this.transaction(() => {
      const previous = this.one('SELECT body_hash FROM inbox WHERE event_id=?', event.event_id);
      if (previous) {
        if (previous.body_hash !== rawHash) fail('EVENT_ID_CONFLICT');
        return { accepted: true, duplicate: true };
      }
      const effect = this.one('SELECT command_hash FROM effects WHERE effect_key=?', effectKey);
      if (effect && effect.command_hash !== commandHash) fail('EFFECT_CONFLICT');
      const order = this.one('SELECT * FROM orders WHERE order_key=?', orderKey);
      if (order && (order.offer_id !== event.offer_id || order.email !== event.customer.email)) fail('ORDER_MAPPING_CONFLICT');
      this.run('INSERT OR IGNORE INTO orders(order_key,offer_id,email) VALUES(?,?,?)', orderKey, event.offer_id, event.customer.email);
      if (!effect) {
        if (this.one('SELECT effect_key FROM effects WHERE order_key=? AND sequence=?', orderKey, event.sequence)) fail('SEQUENCE_CONFLICT');
        this.run("INSERT INTO effects(effect_key,order_key,sequence,command_hash,command_json,state,created_at) VALUES(?,?,?,?,?,'WAITING',?)", effectKey, orderKey, event.sequence, commandHash, json, now());
      }
      this.run('INSERT INTO inbox(event_id,body_hash,effect_key,received_at) VALUES(?,?,?,?)', event.event_id, rawHash, effectKey, now());
      return { accepted: true, duplicate: Boolean(effect) };
    });
  }

  async api(method, route, body = null, idempotency = null) {
    const headers = { Authorization: 'Bearer ' + this.apiKey, Accept: 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'KDPika-Reference-Fulfillment/1' };
    if (idempotency) headers['Idempotency-Key'] = idempotency;
    const response = await fetch(this.base + '/api/v1' + route, {
      method, headers, body: body === null ? undefined : canonical(body), redirect: 'manual', signal: AbortSignal.timeout(15000)
    });
    const reader = response.body.getReader();
    const chunks = []; let size = 0;
    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        size += value.length;
        if (size > 1048576) { await reader.cancel(); fail('UNEXPECTED_API_BODY'); }
        chunks.push(value);
      }
    } finally { reader.releaseLock(); }
    const data = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
    if (!object(data)) fail('UNEXPECTED_API_BODY');
    return [response.status, data];
  }

  async perform(command, order, effectKey) {
    if (command.action === 'issue') {
      if (order.license_id) fail('ORDER_ALREADY_HAS_LICENSE');
      const [status, result] = await this.api('POST', '/licenses', {
        product_id: this.config.product_id, plan_id: this.config.offers[command.offer_id].plan_id, customer: command.customer
      }, 'store_' + effectKey);
      const item = result.license;
      if (status !== 201 || result.success !== true || !object(item) || typeof item.id !== 'string' || !/^lic_[A-Za-z0-9_-]{1,100}$/.test(item.id) || typeof item.license_key !== 'string' || item.license_key.length < 12 || item.license_key.length > 100 || item.environment !== this.environment) fail('ISSUANCE_RESULT_REQUIRES_REVIEW');
      return [item.id, item.license_key, result.request_id ?? null];
    }
    if (!order.license_id) fail('ISSUE_FIRST');
    const route = '/licenses/' + encodeURIComponent(order.license_id);
    const [status, current] = await this.api('GET', route), item = current.license;
    if (status !== 200 || current.success !== true || !object(item) || item.id !== order.license_id || item.environment !== this.environment || item.status !== command.expected_status || item.expires_at !== command.expected_expires_at) fail('LICENSE_CHANGED_RECONCILE_FIRST');
    if (command.action === 'record_cancellation') return [item.id, null, current.request_id ?? null];
    const body = command.action === 'renew' ? { days: this.config.offers[command.offer_id].renewal_days } : { reason: 'Verified store fulfillment decision' };
    const [changedStatus, changed] = await this.api('POST', route + '/' + command.action, body);
    const expected = { suspend: 'suspended', restore: 'active', revoke: 'revoked', renew: item.status === 'expired' ? 'active' : item.status }[command.action];
    if (changedStatus !== 200 || changed.success !== true || changed.license?.id !== item.id || changed.license.status !== expected || (command.action === 'renew' && typeof changed.license.expires_at !== 'string')) fail('MUTATION_RESULT_REQUIRES_REVIEW');
    return [item.id, null, changed.request_id ?? null];
  }

  async work() {
    // Node core has no flock. An atomic directory lock fails closed after a crash.
    // Never remove a stale lock before confirming its worker is no longer running.
    const lock = path.join(this.directory, 'worker.lockdir');
    try { fs.mkdirSync(lock, { mode: 0o700 }); } catch { fail('WORKER_BUSY_OR_STALE_LOCK'); }
    let processed = 0;
    try {
      while (processed < 25) {
        const claimed = this.transaction(() => {
          const effect = this.one("SELECT e.* FROM effects e JOIN orders o ON o.order_key=e.order_key WHERE e.state='WAITING' AND o.held=0 AND e.sequence=o.last_sequence+1 ORDER BY e.created_at,e.effect_key LIMIT 1");
          if (!effect) return null;
          const order = this.one('SELECT * FROM orders WHERE order_key=?', effect.order_key);
          this.run("UPDATE effects SET state='IN_FLIGHT' WHERE effect_key=?", effect.effect_key);
          this.run('UPDATE orders SET held=1 WHERE order_key=?', effect.order_key);
          return [effect, order];
        });
        if (!claimed) break;
        const [effect, order] = claimed;
        try {
          const [licenseId, key, requestId] = await this.perform(JSON.parse(effect.command_json), order, effect.effect_key);
          if (requestId !== null && (typeof requestId !== 'string' || requestId.length > 190)) fail('UNEXPECTED_REQUEST_ID');
          this.transaction(() => {
            if (key !== null) {
              const nonce = randomBytes(12), cipher = createCipheriv('aes-256-gcm', this.outboxKey, nonce);
              cipher.setAAD(Buffer.from(effect.order_key));
              const bytes = Buffer.concat([nonce, cipher.update(key, 'utf8'), cipher.final(), cipher.getAuthTag()]);
              this.run('INSERT INTO outbox(order_key,license_id,cipher,created_at) VALUES(?,?,?,?)', effect.order_key, licenseId, bytes.toString('base64'), now());
            }
            this.run("UPDATE effects SET state='DONE',request_id=? WHERE effect_key=?", requestId, effect.effect_key);
            this.run('UPDATE orders SET license_id=?,last_sequence=?,held=0 WHERE order_key=?', licenseId, effect.sequence, effect.order_key);
          });
        } catch {
          this.run("UPDATE effects SET state='REVIEW',error_code='RECONCILE_BEFORE_RETRY' WHERE effect_key=?", effect.effect_key);
        }
        processed++; await delay(1100);
      }
    } finally { fs.rmdirSync(lock); }
    return { processed, states: this.inspect() };
  }

  inspect() {
    return Object.fromEntries(this.db.prepare('SELECT state,COUNT(*) AS count FROM effects GROUP BY state').all().map(row => [row.state, row.count]));
  }

  export(orderId, target) {
    if (!id(orderId)) fail('INVALID_ORDER_ID');
    const orderKey = identity(this.config.source, this.config.seller_id, this.environment, orderId);
    const item = this.one('SELECT b.*,o.email FROM outbox b JOIN orders o ON o.order_key=b.order_key WHERE b.order_key=?', orderKey);
    if (!item) fail('NO_SAVED_KEY');
    const parent = path.dirname(target);
    if (!path.isAbsolute(target) || !fs.statSync(parent).isDirectory() || (fs.statSync(parent).mode & 0o077)) fail('PRIVATE_EXPORT_DIRECTORY_REQUIRED');
    const raw = Buffer.from(item.cipher, 'base64');
    if (raw.length < 29) fail('INVALID_OUTBOX');
    const cipher = createDecipheriv('aes-256-gcm', this.outboxKey, raw.subarray(0, 12));
    cipher.setAAD(Buffer.from(orderKey)); cipher.setAuthTag(raw.subarray(-16));
    const key = Buffer.concat([cipher.update(raw.subarray(12, -16)), cipher.final()]).toString('utf8');
    fs.writeFileSync(target, canonical({ license_id: item.license_id, license_key: key, email: item.email }), { flag: 'wx', mode: 0o600 });
    return { exported: true, contains_secret: true };
  }

  serve() {
    const reply = (response, status, value) => {
      const body = canonical(value);
      response.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', Connection: 'close' });
      response.end(body);
    };
    const server = http.createServer((request, response) => {
      if (request.method !== 'POST' || request.url !== '/store-event') return reply(response, 404, { accepted: false });
      const counts = name => request.rawHeaders.filter((_, index, values) => index % 2 === 0 && values[index].toLowerCase() === name).length;
      const length = request.headers['content-length'];
      if (request.headers['transfer-encoding'] || counts('content-length') !== 1 || !/^\d+$/.test(length ?? '')) return reply(response, 400, { accepted: false });
      if (Number(length) > LIMIT) return reply(response, 413, { accepted: false });
      if (counts('x-fulfillment-signature') !== 1) return reply(response, 401, { accepted: false });
      if ((request.headers['content-type'] ?? '').split(';')[0].trim().toLowerCase() !== 'application/json') return reply(response, 415, { accepted: false });
      const chunks = []; let size = 0;
      request.on('data', chunk => { size += chunk.length; if (size <= LIMIT) chunks.push(chunk); });
      request.on('end', () => {
        if (size > LIMIT) return reply(response, 413, { accepted: false });
        if (size !== Number(length)) return reply(response, 400, { accepted: false });
        try { reply(response, 202, this.accept(Buffer.concat(chunks), request.headers['x-fulfillment-signature'] ?? '')); }
        catch (error) { reply(response, error instanceof Refusal ? (error.message === 'INVALID_SIGNATURE' ? 401 : 409) : (error instanceof SyntaxError || error instanceof TypeError ? 400 : 503), { accepted: false }); }
      });
      request.on('error', () => response.destroy());
    });
    server.requestTimeout = 5000; server.headersTimeout = 5000; server.keepAliveTimeout = 1000;
    server.listen(Number(process.env.KDP_FULFILLMENT_PORT ?? 8112), '127.0.0.1', () => console.log('{"listening":true,"loopback_only":true}'));
  }
}

try {
  const app = new Fulfillment(), args = process.argv.slice(2);
  if (args.length === 1 && args[0] === 'serve') app.serve();
  else if (args.length === 1 && args[0] === 'work') console.log(canonical(await app.work()));
  else if (args.length === 1 && args[0] === 'inspect') console.log(canonical({ states: app.inspect() }));
  else if (args.length === 3 && args[0] === 'export') console.log(canonical(app.export(args[1], args[2])));
  else fail('USE_SERVE_WORK_INSPECT_OR_EXPORT');
} catch {
  console.log('{"completed":false,"error":"REVIEW_PRIVATE_CONFIGURATION_OR_STATE"}');
  process.exitCode = 1;
}
```

## Expected result

The actual local receiver and worker pass the shared lifecycle suite: durable receipt before work, authentic command acceptance, tamper/time/mode/mapping refusal, duplicate/conflicting effects, ordered gap handling, real creation and recipient [activation](/docs/v1/start/glossary#activation), renewal, suspension, restoration and terminal revocation. The suite also exports an encrypted saved key, refuses unsafe config/key drift, restarts the receiver, holds a simulated interrupted intent, and hides a genuinely successful remote create response behind a failing gateway. No second create is sent after that unknown result.

These are exact-source local Sandbox tests on macOS. They are not an external store purchase, production deployment, native Windows test, email delivery test or promise of cross-system exactly-once behavior. The known API replay limitations remain unchanged. The read-before-write expected-state check is not an atomic remote compare-and-swap.

## Common mistakes

- Sending a provider's native event directly here: this receiver accepts your verified adapter's private command format, not a Stripe/Paddle/PayPal callback.
- Exposing the development server publicly without TLS, request bounds and rate protection.
- Treating a 202 receipt or exported key file as proof of successful buyer email delivery.
- Reusing a fresh state directory after an unknown create instead of reconciling the original order.

## Security notes

- Keep config, state, encrypted-ledger backups and plaintext exports outside all web roots.
- A backend that holds both the encryption key and database can decrypt its outbox; filesystem and process isolation still matter.
- Keep one reviewed writer per order and preserve stable business-effect identity across delivery retries.
- PHP SAPI can buffer bodies before script execution. Configure the ingress 256 KiB body limit and timeouts, not only application validation.
- Recheck runtime support and dependency advisories before production deployment. The recorded advisory scan is not certification.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| Safe configuration error | Missing/mismatched private mapping, key, permissions or runtime dependency | Verify private environment and required modules; never publish raw secrets to debug |
| HTTP 401 | Wrong signature, timestamp, changed bytes or duplicate signature header | Sign exact bytes with the separate command secret and verify the clock |
| HTTP 409 | Mapping/merchant/effect/sequence conflict | Reconcile trusted adapter state; do not invent a different effect ID |
| No work processed | Already complete, sequence gap, held order or busy worker | Inspect saved state and authorized history before any write |
| Export refused | Wrong order/key, unsafe target directory or file already exists | Verify recipient/order and use a new private owner-only target file |

## Next steps

Review [the parent security checklist](/docs/v1/guides/any-store#security-notes), then test your real provider-specific adapter separately in its test environment.

## Related

- [Generic store mapping and lifecycle](/docs/v1/guides/any-store)
- [API idempotency](/docs/v1/api/idempotency)
- [Manual delivery and customer lookup](/docs/v1/guides/manual-issuance)
