KDPika Licensing Sign inStart free
API v1
Show all languages
Browse documentation

Choose a page in the navigation drawer.

KDPika Licensing · API v1 Advanced

Store service: Node.js native ingress

Store service: Node.js native ingress for the complete private reference application.

On this page
Updated 2026-10-18 UTCDownload Markdown

This complete Node.js 18+ server relays native provider notifications to the shared PHP store service. The PHP service remains required for provider verification, persistence, worker and mail. It is not a standalone Node payment worker or a Node requirement for PHP shared hosting.

What you will learn

Read and run the exact Store service: Node.js native ingress with the complete shared application, without exposing private credentials or claiming an unverified payment.

Configure and run

Set private KDP_STORE_SERVICE_URL to the dedicated PHP service's HTTPS origin followed by /provider-event. Set optional KDP_STORE_RELAY_PORT; default 8118. Put your own HTTPS reverse proxy in front of this loopback listener and preserve raw bodies/headers. Register that public proxy URL with the provider. Do not use a redirect or expose any provider/API credential in JavaScript sent to a browser.

bashbash
node --check stores/relay.mjs
node stores/relay.mjs

Expected: listening:true and loopback_only:true. Forward only POST /provider-event; a complete, verified upstream durable receipt returns 202 with accepted:true. A failed/oversized/invalid/uncertain upstream receipt returns a refusal, never false success. The worker cron runs independently on PHP hosting.

Complete source

relay.mjsjavascript
#!/usr/bin/env node
// Optional Node18+ native HTTP ingress for the complete PHP store service.
// This is NOT a pure Node payment worker: service.php authenticates and commits.
// Set KDP_STORE_SERVICE_URL (HTTPS /provider-event), KDP_STORE_RELAY_PORT (8118).
// Run: node relay.mjs. Expected: only verified durable upstream receipts return202.
import http from 'node:http';

const target = new URL(process.env.KDP_STORE_SERVICE_URL ?? '');
const loopback = process.env.KDP_STORE_ENVIRONMENT === 'sandbox' && process.env.KDP_STORE_ALLOW_LOOPBACK === '1' && target.hostname === '127.0.0.1' && target.protocol === 'http:';
if ((target.protocol !== 'https:' && !loopback) || target.username || target.password || target.search || target.hash || target.pathname !== '/provider-event') throw new Error('Private service URL required');
const allowed = new Set(['content-type', 'stripe-signature', 'paddle-signature', 'x-signature', 'x-wc-webhook-signature', 'x-wc-webhook-topic', 'x-shopify-hmac-sha256', 'x-shopify-topic', 'x-shopify-shop-domain', 'x-shopify-webhook-id', 'x-fs-signature', 'paypal-auth-algo', 'paypal-cert-url', 'paypal-transmission-id', 'paypal-transmission-sig', 'paypal-transmission-time']);
const reply = (response, status, body) => { response.writeHead(status, { 'Content-Type': 'application/json', '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 !== '/provider-event') return reply(response, 404, '{"accepted":false}');
  if (!/^\d+$/.test(request.headers['content-length'] ?? '') || request.headers['transfer-encoding']) return reply(response, 400, '{"accepted":false}');
  if (Number(request.headers['content-length']) > 262144) return reply(response, 413, '{"accepted":false}');
  const headers = {}, seen = new Set();
  for (let i = 0; i < request.rawHeaders.length; i += 2) {
    const name = request.rawHeaders[i].toLowerCase();
    if (!allowed.has(name)) continue;
    if (seen.has(name)) return reply(response, 401, '{"accepted":false}');
    seen.add(name); headers[name] = request.rawHeaders[i + 1];
  }
  const chunks = []; let length = 0;
  request.on('data', chunk => { length += chunk.length; if (length <= 262144) chunks.push(chunk); });
  request.on('error', () => response.destroy());
  request.on('end', async () => {
    if (length !== Number(request.headers['content-length']) || length > 262144) return reply(response, 400, '{"accepted":false}');
    try {
      const upstream = await fetch(target, { method: 'POST', headers, body: Buffer.concat(chunks), redirect: 'manual', signal: AbortSignal.timeout(12000) });
      // Bound allocation while reading, not only after an arbitrarily large body.
      const reader = upstream.body?.getReader();
      if (!reader) throw new Error('Missing receipt');
      const parts = []; let received = 0;
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          received += value.byteLength;
          if (received > 4096) { await reader.cancel(); throw new Error('Invalid receipt'); }
          parts.push(Buffer.from(value));
        }
      } finally { reader.releaseLock(); }
      const bytes = Buffer.concat(parts, received);
      const receipt = JSON.parse(bytes.toString('utf8'));
      if (upstream.status === 202 && receipt.accepted === true && typeof receipt.duplicate === 'boolean') return reply(response, 202, JSON.stringify({ accepted: true, duplicate: receipt.duplicate }));
      return reply(response, [400, 401, 409, 413, 415, 429].includes(upstream.status) ? upstream.status : 503, '{"accepted":false}');
    } catch { return reply(response, 503, '{"accepted":false}'); }
  });
});
server.requestTimeout = 5000; server.headersTimeout = 5000; server.keepAliveTimeout = 1000;
server.listen(Number(process.env.KDP_STORE_RELAY_PORT ?? 8118), '127.0.0.1', () => console.log('{"listening":true,"loopback_only":true}'));

Test and operating boundary

This exact ingress was executed for all eight provider contracts and both one-time/recurring profiles where supplied. It preserves the original bytes and allowlisted authentication headers and refuses duplicate relevant headers. The response stream is bounded during reading. Local test overrides require Sandbox plus literal HTTP 127.0.0.1 and explicit consent flags; remove them in deployment. A public HTTPS proxy/merchant account was not used in these local contract tests.

Prerequisites

Complete the private service installation. Keep all files from the exact source archive together, use PHP 8.2+ with its documented extensions and start with an isolated Sandbox product/key. A source fragment alone is not the application.

Expected result

Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting license with the same buyer email; no full key appears in public replies.

Common mistakes

Do not deploy one module without its archive dependencies, place configuration under the web root, skip provider API verification or clear a durable ledger to bypass review. Optional Node/Python ingress still requires the PHP worker.

Security notes

Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed.

Troubleshooting

For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider delivery evidence. Do not post raw events, keys or SQLite files publicly.

Next steps

Run the selected provider Sandbox procedure, then test private key email delivery. Review supported recipe limitations before enabling Live.

Was this page helpful?

Choose an answer

Do not include personal data, license keys or credentials. Retained for up to 30 days. This is not a support inbox.

Search documentation

Arrow keys to move, Enter to open, Escape to close. Search stays in your browser.

Cookie settings