This website is meant to be read and understood quickly by humans, but is only fully parsable, on a technical level, with the aid of an AI system. Read why →
Loop MMT
Is This Input Event Safe to Fold?route← all gifts

input-event-router

Every interactive app re-hand-rolls the same first step: turn raw, untrusted pointer/key input into something a reducer can safely fold — and that is where the bugs and the attack surface live. input-event-router is that step done once, correctly: a dependency-free deterministic reducer that folds a raw input-event log (JSONL) into a typed, schema-validated event stream, plus an O(1) fold-state (sequence integrity + a monotonic logical tick). seq is a strict total order (a gap or repeat is a rejection, never coerced); t is logical time supplied as data, never a wall-clock read, so the same log replays byte-identically forever — in a browser or headless on Node; type must be in a declared, closed vocabulary; payload is an opaque object it never interprets. It authors the interactive lane's {seq,t,type,payload} event envelope that render-loop-harness and undo-stack-kernel fold. Zero dependencies, pure, offline.

The honest edge
this normalizes and validates raw input events against a declared schema, folding them into a deterministic typed event stream; it does NOT sanitize application semantics, it does NOT persist, it holds NO clock/network/entropy, and it trusts no event it did not schema.
Run it
node input-event-router.js < events.jsonl # fold a JSONL event log -> render + emitted-stream summary | node input-event-router.js --help test_input-event-router.js (25/25) — replay-determinism, seq total-order rejection, closed-vocabulary enforcement, O(1) state bound under a large synthetic log, crash-clean on bad/empty JSON Zero dependencies, Node or browser, deterministic
The code — every file that ships
input-event-router.js230 lineson GitHub →
#!/usr/bin/env node
/* input-event-router.js — a dependency-free, deterministic reducer that turns a
   raw input-event log into a validated, typed event stream.

   WHY THIS EXISTS. Every interactive app re-hand-rolls the same first step: take
   raw, untrusted pointer/key input and turn it into something a reducer can
   safely fold. Hand-rolled, that step is where the bugs and the attack surface
   live — an unvalidated event coerced instead of rejected, a wall-clock read
   that breaks replay, a field the reducer trusts that the input never carried.
   This is that step done once, correctly, as a DETERMINISTIC REDUCER over an
   input-event log: raw events in, a typed schema-validated stream out, plus a
   tiny O(1) fold-state (sequence integrity + monotonic tick).

   THE STRIP-CLEAN RULE (the whole reason to trust it). state_t is a pure fold of
   state_0 and the ordered event log up to t. Replaying the same log yields
   byte-identical state, stream, and render. The router holds NO clock, NO
   network, and NO entropy it did not receive as an event — logical time enters
   ONLY as the event field `t`, supplied by the caller as data. So the same log
   replays identically forever, in a browser or headless on Node.

   THE ENVELOPE (this gift authors the interactive lane's event schema). One
   event per JSONL line:
     { "seq": <int>=0, monotonic +1 >, "t": <int>=0, non-decreasing logical tick >,
       "type": <one of the declared accepted types>, "payload": <object> }
   seq is the total order (a gap or repeat is a rejection). t is logical time as
   DATA (never a clock read). type must be in the declared accepted set. payload
   is opaque cargo (checked to be an object; its contents are the downstream
   reducer's business, not this gift's).

   USAGE
     node input-event-router.js < events.jsonl     # fold a JSONL event log -> render + summary
     node input-event-router.js --help

   Released under MIT. Its edge, printed in the README and --help: this
   NORMALIZES and VALIDATES input events against a declared schema, folding them
   into a deterministic typed stream. It does NOT sanitize application semantics,
   it does NOT persist, it holds NO clock/network/entropy, and it trusts no event
   it did not schema. (Retaining event history is the undo-stack-kernel's job,
   under its own declared bound — this gift deliberately keeps O(1) state.)
*/
"use strict";

// The default accepted event-type vocabulary. The vocabulary is CONFIG, not
// frozen (covenant §2/§5 U2 — sub-lanes/types by contract, not by a fixed list):
// what the gift enforces is that the vocabulary is DECLARED and CLOSED, which is
// the decidable security property. An app supplies its own set via cfg.acceptedTypes.
var DEFAULT_ACCEPTED_TYPES = ["pointer", "key", "select", "edit", "commit"];

// The initial fold-state. O(1) by contract: three integers, never a buffer.
function state0() {
  return { nextSeq: 0, lastTick: -1, count: 0 };
}

// isPlainObject — an object, not null, not an array, not a scalar. payload must
// be an object; we do NOT walk its contents (depth-1 check => bounded work,
// resource-bounds clause §3.3).
function isPlainObject(v) {
  return v !== null && typeof v === "object" && !Array.isArray(v);
}

function isInt(v) {
  return typeof v === "number" && isFinite(v) && Math.floor(v) === v;
}

/* validate(state, event, cfg) -> { ok: true } | { ok: false, reason: <string> }
   Pure. Checks the envelope against the schema BEFORE the fold sees it. Every
   failure is a DECLARED rejection reason; nothing is coerced. */
function validate(state, event, cfg) {
  var accepted = (cfg && cfg.acceptedTypes) || DEFAULT_ACCEPTED_TYPES;

  if (!isPlainObject(event)) return { ok: false, reason: "event-not-object" };

  // seq: present, int >= 0, exactly the next expected (total order, no gap/repeat).
  if (!isInt(event.seq) || event.seq < 0) return { ok: false, reason: "seq-not-int" };
  if (event.seq < state.nextSeq) return { ok: false, reason: "seq-repeat" };
  if (event.seq > state.nextSeq) return { ok: false, reason: "seq-gap" };

  // t: present, int >= 0, non-decreasing (logical tick as data; never a clock).
  if (!isInt(event.t) || event.t < 0) return { ok: false, reason: "tick-not-int" };
  if (event.t < state.lastTick) return { ok: false, reason: "tick-decrease" };

  // type: non-empty string in the declared accepted set.
  if (typeof event.type !== "string" || event.type.length === 0) {
    return { ok: false, reason: "type-not-string" };
  }
  if (accepted.indexOf(event.type) === -1) return { ok: false, reason: "type-not-accepted" };

  // payload: an object (opaque cargo). A scalar/null/array payload is rejected.
  if (!isPlainObject(event.payload)) return { ok: false, reason: "payload-not-object" };

  return { ok: true };
}

/* route(state, event, cfg) -> { state, emit, reject }
   The pure reducer. On a valid event: emit the normalized typed event and
   advance state. On an invalid event: emit null, record the rejection, and DO
   NOT advance the fold into a corrupt place. The whole log is folded by reduce. */
function route(state, event, cfg) {
  var v = validate(state, event, cfg);
  if (!v.ok) {
    // A rejected event does not advance nextSeq/lastTick — the fold's integrity
    // point is preserved. reject carries the offending seq when it was an int.
    var seq = isInt(event && event.seq) ? event.seq : null;
    return { state: state, emit: null, reject: { seq: seq, reason: v.reason } };
  }
  // Build the normalized typed event WITHOUT trusting the raw object's prototype
  // or extra keys: copy only the four envelope fields onto a null-proto object,
  // so a hostile __proto__/constructor payload key cannot pollute anything.
  var payloadCopy = Object.create(null);
  var keys = Object.keys(event.payload);
  for (var i = 0; i < keys.length; i++) {
    var k = keys[i];
    if (k === "__proto__" || k === "constructor" || k === "prototype") continue;
    payloadCopy[k] = event.payload[k];
  }
  var emit = { seq: event.seq, t: event.t, type: event.type, payload: payloadCopy };
  var next = { nextSeq: state.nextSeq + 1, lastTick: event.t, count: state.count + 1 };
  return { state: next, emit: emit, reject: null };
}

/* foldLog(events, cfg) -> { state, stream, rejections, halted }
   Folds an ordered array of raw events. reject-policy default is HALT: a hostile
   or corrupt log STOPS at the first integrity violation rather than silently
   dropping bad events and continuing into an ambiguous stream. cfg.rejectPolicy
   = "skip" is available for genuinely best-effort input. */
function foldLog(events, cfg) {
  var policy = (cfg && cfg.rejectPolicy) || "halt";
  var state = state0();
  var stream = [];
  var rejections = [];
  var halted = false;
  for (var i = 0; i < events.length; i++) {
    var r = route(state, events[i], cfg);
    if (r.reject) {
      rejections.push(r.reject);
      if (policy === "halt") { halted = true; break; }
      // skip: record the rejection, do not advance, continue to the next event.
      continue;
    }
    state = r.state;
    stream.push(r.emit);
  }
  return { state: state, stream: stream, rejections: rejections, halted: halted };
}

/* render(state) -> string. Pure state -> frame projection. An interactive gift
   is still pipe-testable headless: the frame is deterministic text, never a live
   canvas. */
function render(state) {
  return (
    "accepted=" + state.count +
    " next-seq=" + state.nextSeq +
    " last-tick=" + state.lastTick
  );
}

var API = {
  DEFAULT_ACCEPTED_TYPES: DEFAULT_ACCEPTED_TYPES,
  ACCEPTED_TYPES: DEFAULT_ACCEPTED_TYPES,
  state0: state0,
  validate: validate,
  route: route,
  foldLog: foldLog,
  render: render
};

// Browser attach.
if (typeof window !== "undefined") {
  window.ForestGifts = window.ForestGifts || {};
  window.ForestGifts.inputEventRouter = API;
}

// Node require.
if (typeof module !== "undefined" && module.exports) {
  module.exports = API;
}

// CLI.
function main(argv) {
  var args = argv.slice(2);
  if (args.indexOf("--help") !== -1 || args.indexOf("-h") !== -1) {
    process.stdout.write(
      "input-event-router.js — deterministic reducer: raw input-event log -> validated typed stream.\n\n" +
      "  node input-event-router.js < events.jsonl   fold a JSONL event log -> render + summary\n" +
      "  node input-event-router.js --help\n\n" +
      "Envelope (one JSON object per line):\n" +
      "  { \"seq\": <int, monotonic +1>, \"t\": <int, non-decreasing logical tick>,\n" +
      "    \"type\": <accepted type>, \"payload\": <object> }\n\n" +
      "Edge: this normalizes and validates input events against a declared schema,\n" +
      "folding them into a deterministic typed stream. It does not sanitize\n" +
      "application semantics, does not persist, holds no clock/network/entropy, and\n" +
      "trusts no event it did not schema.\n"
    );
    return 0;
  }
  var chunks = [];
  process.stdin.on("data", function (d) { chunks.push(d); });
  process.stdin.on("end", function () {
    var text = Buffer.concat(chunks).toString("utf8");
    var lines = text.split("\n");
    var events = [];
    for (var i = 0; i < lines.length; i++) {
      var line = lines[i].trim();
      if (line.length === 0) continue;
      try {
        events.push(JSON.parse(line));
      } catch (e) {
        // A non-JSON line is itself a rejectable event: push a sentinel the
        // schema will reject as event-not-object (a string is not an object).
        events.push(line);
      }
    }
    var out = foldLog(events);
    process.stdout.write(render(out.state) + "\n");
    process.stdout.write(
      "emitted=" + out.stream.length +
      " rejected=" + out.rejections.length +
      (out.halted ? " HALTED" : "") + "\n"
    );
    for (var j = 0; j < out.rejections.length; j++) {
      var rj = out.rejections[j];
      process.stdout.write("  reject seq=" + rj.seq + " reason=" + rj.reason + "\n");
    }
  });
  return 0;
}

if (typeof require !== "undefined" && require.main === module) {
  process.exitCode = main(process.argv);
}
test_input-event-router.js146 lineson GitHub →
#!/usr/bin/env node
/* test_input-event-router.js — the golden event-log corpus.

   The battery drift-checks the gift against an OUT-OF-BAND oracle: expected final
   states and emitted streams authored independently (by hand, from the envelope
   rules), not by running the gift. Plus replay-determinism (the canonicalizer
   self-test), the mandatory hostile-event known-bad vectors (§3 security check),
   a state-bounds assertion, and a mutation bite so a no-op harness cannot pass.
*/
"use strict";

var R = require("./input-event-router.js");
var assert = require("assert");

var pass = 0, fail = 0;
function ok(name, cond) {
  if (cond) { pass++; }
  else { fail++; console.error("FAIL: " + name); }
}
function eq(name, a, b) {
  var ja = JSON.stringify(a), jb = JSON.stringify(b);
  if (ja === jb) { pass++; }
  else { fail++; console.error("FAIL: " + name + "\n  got:  " + ja + "\n  want: " + jb); }
}

// ---------------------------------------------------------------------------
// 1. Golden valid log -> expected final state + emitted stream (out-of-band).
//    Expected values hand-computed from the envelope rules, NOT from the gift.
// ---------------------------------------------------------------------------
var validLog = [
  { seq: 0, t: 0, type: "pointer", payload: { x: 10, y: 20 } },
  { seq: 1, t: 0, type: "select", payload: { region: "A1" } },
  { seq: 2, t: 3, type: "edit", payload: { cell: "A1", value: 7 } },
  { seq: 3, t: 3, type: "commit", payload: {} }
];
// Hand-authored oracle: 4 accepted, nextSeq ends at 4, lastTick is 3, count 4.
var expectedState = { nextSeq: 4, lastTick: 3, count: 4 };
// Emitted stream = the same four events, payloads copied field-for-field.
var expectedStream = [
  { seq: 0, t: 0, type: "pointer", payload: { x: 10, y: 20 } },
  { seq: 1, t: 0, type: "select", payload: { region: "A1" } },
  { seq: 2, t: 3, type: "edit", payload: { cell: "A1", value: 7 } },
  { seq: 3, t: 3, type: "commit", payload: {} }
];
var g = R.foldLog(validLog);
eq("golden final state", g.state, expectedState);
eq("golden emitted stream", g.stream, expectedStream);
ok("golden: no rejections", g.rejections.length === 0);
ok("golden: not halted", g.halted === false);
eq("golden render", R.render(g.state), "accepted=4 next-seq=4 last-tick=3");

// ---------------------------------------------------------------------------
// 2. Replay-determinism (the canonicalizer self-test) — fold N times ->
//    byte-identical state, stream, render each time.
// ---------------------------------------------------------------------------
var r1 = R.foldLog(validLog);
var r2 = R.foldLog(validLog);
var r3 = R.foldLog(validLog);
eq("replay: state identical", JSON.stringify(r1.state), JSON.stringify(r3.state));
eq("replay: stream identical", JSON.stringify(r1.stream), JSON.stringify(r2.stream));
eq("replay: render identical", R.render(r1.state), R.render(r3.state));

// ---------------------------------------------------------------------------
// 3. Hostile-event known-bad vectors (§3.1) — each MUST produce its declared
//    rejection reason, never a corruption/crash/coercion. Default policy=halt.
// ---------------------------------------------------------------------------
function firstReject(log) {
  var out = R.foldLog(log);
  return out.rejections.length ? out.rejections[0].reason : "(none)";
}
eq("hostile: seq gap -> seq-gap",
  firstReject([{ seq: 0, t: 0, type: "key", payload: {} },
               { seq: 1, t: 0, type: "key", payload: {} },
               { seq: 3, t: 0, type: "key", payload: {} }]),
  "seq-gap");
eq("hostile: seq repeat -> seq-repeat",
  firstReject([{ seq: 0, t: 0, type: "key", payload: {} },
               { seq: 1, t: 0, type: "key", payload: {} },
               { seq: 1, t: 0, type: "key", payload: {} }]),
  "seq-repeat");
eq("hostile: decreasing tick -> tick-decrease",
  firstReject([{ seq: 0, t: 5, type: "key", payload: {} },
               { seq: 1, t: 2, type: "key", payload: {} }]),
  "tick-decrease");
eq("hostile: unknown type -> type-not-accepted",
  firstReject([{ seq: 0, t: 0, type: "no-such-type", payload: {} }]),
  "type-not-accepted");
eq("hostile: scalar payload -> payload-not-object",
  firstReject([{ seq: 0, t: 0, type: "key", payload: 42 }]),
  "payload-not-object");
eq("hostile: null payload -> payload-not-object",
  firstReject([{ seq: 0, t: 0, type: "key", payload: null }]),
  "payload-not-object");
eq("hostile: array payload -> payload-not-object",
  firstReject([{ seq: 0, t: 0, type: "key", payload: [1, 2] }]),
  "payload-not-object");
eq("hostile: non-object event -> event-not-object",
  firstReject(["this is not an object"]),
  "event-not-object");

// 3b. Prototype-pollution known-bad twin: a __proto__ payload key must NOT
//     pollute Object.prototype, and must be dropped from the emitted payload.
var poll = R.foldLog([{ seq: 0, t: 0, type: "edit",
                        payload: JSON.parse('{"__proto__":{"polluted":true},"safe":1}') }]);
ok("proto-pollution: Object.prototype not polluted", ({}).polluted === undefined);
ok("proto-pollution: emitted, not rejected", poll.stream.length === 1 && poll.rejections.length === 0);
eq("proto-pollution: dangerous key dropped, safe key kept",
  poll.stream[0].payload, { safe: 1 });

// 3c. halt vs skip policy.
var halted = R.foldLog([{ seq: 0, t: 0, type: "key", payload: {} },
                        { seq: 2, t: 0, type: "key", payload: {} },  // gap -> halt here
                        { seq: 3, t: 0, type: "key", payload: {} }]);
ok("halt: stops at first violation", halted.halted === true && halted.stream.length === 1);
var skipped = R.foldLog([{ seq: 0, t: 0, type: "key", payload: {} },
                         { seq: 2, t: 0, type: "key", payload: {} },  // gap, skipped
                         { seq: 1, t: 0, type: "key", payload: {} }], // now valid (nextSeq still 1)
                        { rejectPolicy: "skip" });
ok("skip: continues past a rejection", skipped.halted === false &&
   skipped.stream.length === 2 && skipped.rejections.length === 1);

// ---------------------------------------------------------------------------
// 4. State-bounds (§3.2) — over a large synthetic log the state stays exactly
//    three integers; no retained buffer. Assert structurally + by memory shape.
// ---------------------------------------------------------------------------
var N = 100000;
var big = [];
for (var i = 0; i < N; i++) big.push({ seq: i, t: i, type: "key", payload: {} });
var bigOut = R.foldLog(big);
eq("state-bounds: state keys are exactly {nextSeq,lastTick,count}",
  Object.keys(bigOut.state).sort(), ["count", "lastTick", "nextSeq"]);
ok("state-bounds: count == N", bigOut.state.count === N);
ok("state-bounds: state carries no array/buffer field",
  Object.keys(bigOut.state).every(function (k) { return typeof bigOut.state[k] === "number"; }));

// ---------------------------------------------------------------------------
// 5. Mutation bite — a deliberately-wrong expected state MUST be caught, so a
//    no-op harness cannot pass green.
// ---------------------------------------------------------------------------
var wrongExpected = { nextSeq: 999, lastTick: 3, count: 4 };
ok("mutation bite: wrong expected state is detected",
  JSON.stringify(g.state) !== JSON.stringify(wrongExpected));

// ---------------------------------------------------------------------------
console.log((fail === 0 ? "OK" : "FAILED") + " — " + pass + " passed, " + fail + " failed");
process.exitCode = fail === 0 ? 0 : 1;
Take the whole folder → MIT Zero dependencies, Node or browser, deterministic