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 User Config Safe To Apply?filter← all gifts

Skin Config Validator

Validate a user-submitted skin/theme config object — colors, fonts, numbers, CSS custom properties — against a schema you declare, BEFORE you splice it into a stylesheet. verifySkin(config, schema) type-checks every field, allowlists CSS colors and cssvar values (rejecting url(), @import, javascript:, and ; { } breakouts), and returns { ok, value, errors, warnings } — value carries only the fields that passed, safe to apply. It does not mutate or coerce; it reports. No dependencies, Node or browser.

The honest edge
It keeps 'wrong' and 'unknown' apart: a bad type/range/injection is an ERROR, an unknown field is a dropped WARNING (forward-compat), a missing optional field is silent. The `string` type is NOT stylesheet-sanitized — it type-checks and length-caps only; use `cssvar` (the type with the injection allowlist) for anything headed into a style surface. The named-color allowlist is conservative (unknown names rejected, not guessed) and the cssvar check is a safe-character GRAMMAR, not a full CSS value parser — it proves the value can't break out of a declaration, not that it is meaningful CSS. The schema is YOURS: a malformed schema throws (programmer error); only the config is treated as untrusted and reported-not-thrown.
Run it
node loop21-verifyskin.js config.json schema.json test_loop21-verifyskin.js (18/18: every type path, CSS-injection rejection, unknown-field drop+warn, required-missing error, non-object config reported-not-thrown, multi-error report, 2 ratchet-refusal cases) Node / browser, no dependencies
The code — every file that ships
loop21-verifyskin.js207 lineson GitHub →
#!/usr/bin/env node
/* loop21-verifyskin.js — a pure, dependency-free, STRICT validator for
   user-submitted "skin" config objects, before you apply them to a UI. Runs
   identically in a browser (window.LoopGifts.verifySkin) and in Node (require /
   this CLI). No DOM, no dependencies.

   WHY THIS EXISTS. A theming / skin feature lets a user hand you a small config
   object — colors, a font, a few numbers, some CSS custom properties — that you
   then splice into a stylesheet or inline style. That is untrusted input landing
   in your render surface. The usual answer is to trust it (and ship a CSS
   injection, or a broken layout from a typo'd number) or to hand-check a few
   fields and miss the rest. This validates the WHOLE object against a declared
   schema and tells you, honestly, what is wrong — before a single value is
   applied.

   WHAT "VERIFY" MEANS HERE. verifySkin does not mutate, coerce, or "fix" your
   config. It reads it against a schema and returns a verdict:
       { ok, value, errors, warnings }
   `ok` is true only when there are zero errors. `value` is the subset of the
   input that passed (known, well-typed fields) — safe to apply. `errors` are
   hard failures (wrong type, out-of-range, a disallowed CSS value, a missing
   required field). `warnings` are soft (an unknown field that was dropped, a
   value clamped-in-spec-but-suspicious). A validator that silently drops the
   difference between "wrong" and "unknown" is lying about what it checked; this
   one keeps them apart.

   THE SCHEMA. A plain object mapping field name -> a small type spec:
     { type: "color" }            a CSS color: #rgb / #rrggbb / #rrggbbaa /
                                  rgb()/rgba()/hsl()/hsla() / a named-color from
                                  the allowlist. No url(), no expression, no ; }
     { type: "cssvar" }           a CSS custom-property VALUE, allowlisted to a
                                  safe grammar (letters, digits, %, #, spaces,
                                  commas, dots, parens for the color fns above) —
                                  rejects ; { } < > url( javascript: and @import.
     { type: "number", min, max } a finite number, optionally range-checked.
     { type: "integer", min, max} a number with no fractional part.
     { type: "enum", values:[..] }one of a fixed set of strings.
     { type: "boolean" }          true / false.
     { type: "string", maxLen }   an arbitrary string, optionally length-capped
                                  (NOT applied to a stylesheet without your own
                                  escaping — see the README edge).
   Add `required: true` to make a missing field an error (default: optional).

   API
     verifySkin(config, schema) -> { ok, value, errors, warnings }
       `config`  the untrusted object (anything; a non-object is one error).
       `schema`  the field->spec map above (a non-object throws — the schema is
                 YOURS, so a bad schema is a programmer error, not user input).
       `errors` / `warnings` are arrays of { field, message } (field null for
       whole-object problems). Never throws on bad `config` — that is the point;
       it reports. THROWS only on a malformed `schema`.

   Pure function of its inputs. Same code in a browser
   (window.LoopGifts.verifySkin) or Node (this CLI / require()).

   USAGE
     node loop21-verifyskin.js config.json schema.json   # prints the verdict JSON
     node loop21-verifyskin.js --help
*/
(function (root, factory) {
  var api = factory();
  if (typeof module === "object" && module.exports) module.exports = api;
  if (typeof window !== "undefined") {
    window.LoopGifts = window.LoopGifts || {};
    window.LoopGifts.verifySkin = api.verifySkin;
  }
  root.__loop21VerifySkin = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
  "use strict";

  // A conservative named-color allowlist — the common CSS keywords. Not
  // exhaustive by design: an unknown name is rejected, not guessed.
  var NAMED_COLORS = {
    black:1, silver:1, gray:1, grey:1, white:1, maroon:1, red:1, purple:1,
    fuchsia:1, green:1, lime:1, olive:1, yellow:1, navy:1, blue:1, teal:1,
    aqua:1, cyan:1, magenta:1, orange:1, pink:1, brown:1, gold:1, coral:1,
    salmon:1, khaki:1, violet:1, indigo:1, turquoise:1, tan:1, beige:1,
    ivory:1, crimson:1, chocolate:1, transparent:1
  };

  var HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
  // rgb/rgba/hsl/hsla with only numbers, %, commas, spaces, dots inside.
  var COLOR_FN = /^(?:rgb|rgba|hsl|hsla)\(\s*[-0-9.%,\s\/]+\)$/;
  // A safe cssvar value: letters, digits, whitespace, and a small punctuation set.
  // Explicitly excludes ; { } < > and the url(/@import/expression/javascript vectors.
  var SAFE_CSSVAR = /^[A-Za-z0-9#%.,()\-_ \/]*$/;
  var DANGER = /(url\s*\(|@import|expression\s*\(|javascript:|[;{}<>])/i;

  function isColor(v) {
    if (typeof v !== "string") return false;
    var s = v.trim();
    if (HEX.test(s)) return true;
    if (COLOR_FN.test(s)) return true;
    if (Object.prototype.hasOwnProperty.call(NAMED_COLORS, s.toLowerCase())) return true;
    return false;
  }
  function isSafeCssVar(v) {
    if (typeof v !== "string") return false;
    if (DANGER.test(v)) return false;
    return SAFE_CSSVAR.test(v);
  }

  function verifySkin(config, schema) {
    if (schema === null || typeof schema !== "object" || Array.isArray(schema))
      throw new Error("loop21-verifyskin: schema must be a plain object of field specs");

    var errors = [], warnings = [], value = {};

    if (config === null || typeof config !== "object" || Array.isArray(config)) {
      errors.push({ field: null, message: "config must be a plain object" });
      return { ok: false, value: {}, errors: errors, warnings: warnings };
    }

    var schemaFields = Object.keys(schema);

    // 1. unknown fields -> dropped, warned (not an error: forward-compat)
    Object.keys(config).forEach(function (k) {
      if (schemaFields.indexOf(k) === -1)
        warnings.push({ field: k, message: "unknown field dropped (not in schema)" });
    });

    // 2. each schema field
    schemaFields.forEach(function (field) {
      var spec = schema[field];
      if (spec === null || typeof spec !== "object")
        throw new Error("loop21-verifyskin: schema field '" + field + "' has a non-object spec");
      var present = Object.prototype.hasOwnProperty.call(config, field);
      var v = config[field];

      if (!present) {
        if (spec.required) errors.push({ field: field, message: "required field missing" });
        return;
      }

      switch (spec.type) {
        case "color":
          if (isColor(v)) value[field] = v;
          else errors.push({ field: field, message: "not a valid/allowlisted CSS color" });
          break;
        case "cssvar":
          if (isSafeCssVar(v)) value[field] = v;
          else errors.push({ field: field, message: "cssvar value contains a disallowed character or vector" });
          break;
        case "number":
          if (typeof v !== "number" || !isFinite(v)) { errors.push({ field: field, message: "not a finite number" }); break; }
          if (typeof spec.min === "number" && v < spec.min) { errors.push({ field: field, message: "below min " + spec.min }); break; }
          if (typeof spec.max === "number" && v > spec.max) { errors.push({ field: field, message: "above max " + spec.max }); break; }
          value[field] = v;
          break;
        case "integer":
          if (typeof v !== "number" || !isFinite(v) || Math.floor(v) !== v) { errors.push({ field: field, message: "not an integer" }); break; }
          if (typeof spec.min === "number" && v < spec.min) { errors.push({ field: field, message: "below min " + spec.min }); break; }
          if (typeof spec.max === "number" && v > spec.max) { errors.push({ field: field, message: "above max " + spec.max }); break; }
          value[field] = v;
          break;
        case "enum":
          if (!Array.isArray(spec.values)) throw new Error("loop21-verifyskin: enum field '" + field + "' needs a values array");
          if (spec.values.indexOf(v) !== -1) value[field] = v;
          else errors.push({ field: field, message: "not one of the allowed values" });
          break;
        case "boolean":
          if (typeof v === "boolean") value[field] = v;
          else errors.push({ field: field, message: "not a boolean" });
          break;
        case "string":
          if (typeof v !== "string") { errors.push({ field: field, message: "not a string" }); break; }
          if (typeof spec.maxLen === "number" && v.length > spec.maxLen) { errors.push({ field: field, message: "exceeds maxLen " + spec.maxLen }); break; }
          value[field] = v;
          break;
        default:
          throw new Error("loop21-verifyskin: unknown spec type '" + spec.type + "' for field '" + field + "'");
      }
    });

    return { ok: errors.length === 0, value: value, errors: errors, warnings: warnings };
  }

  return { verifySkin: verifySkin, isColor: isColor, isSafeCssVar: isSafeCssVar };
});

// ---- CLI (Node only) ------------------------------------------------------
if (typeof require !== "undefined" && typeof module !== "undefined" && require.main === module) {
  var api = (typeof globalThis !== "undefined" ? globalThis : this).__loop21VerifySkin;
  var args = process.argv.slice(2);
  if (!args.length || args.indexOf("--help") !== -1) {
    process.stdout.write(
      "loop21-verifyskin — strict, zero-dep validator for user skin config\n" +
      "  node loop21-verifyskin.js config.json schema.json   print the verdict JSON\n" +
      "  node loop21-verifyskin.js --help\n"
    );
    process.exit(0);
  }
  try {
    var fs = require("fs");
    if (args.length < 2) throw new Error("need a config.json and a schema.json");
    var config = JSON.parse(fs.readFileSync(args[0], "utf8"));
    var schema = JSON.parse(fs.readFileSync(args[1], "utf8"));
    var res = api.verifySkin(config, schema);
    process.stdout.write(JSON.stringify(res, null, 2) + "\n");
    process.exit(res.ok ? 0 : 2); // nonzero when the config is rejected
  } catch (e) {
    var msg = e && e.message ? e.message : String(e);
    if (msg.indexOf("loop21-verifyskin:") !== 0) msg = "loop21-verifyskin: " + msg;
    process.stderr.write(msg + "\n");
    process.exit(1);
  }
}
test_loop21-verifyskin.js174 lineson GitHub →
#!/usr/bin/env node
/* test_loop21-verifyskin.js — known-answer battery for loop21-verifyskin.

   The oracle is OUT OF BAND: every expected verdict below is a literal fact
   written by hand, never the output of a second validator. Each case constructs
   a config + schema whose correct { ok, value, errors, warnings } is known by
   construction.

   Run: node test_loop21-verifyskin.js   (exit 0 = all pass)
*/
"use strict";
var assert = require("assert");
var { verifySkin, isColor, isSafeCssVar } = require("./loop21-verifyskin.js");

var pass = 0, fail = 0;
function ok(name, fn) {
  try { fn(); pass++; console.log("  ok   " + name); }
  catch (e) { fail++; console.log("  FAIL " + name + " — " + e.message); }
}

// ---- 1. a fully-valid config passes, value echoes the typed fields --------
ok("1 valid config -> ok:true, value carries typed fields", function () {
  var schema = {
    bg:      { type: "color" },
    accent:  { type: "color" },
    radius:  { type: "integer", min: 0, max: 32 },
    opacity: { type: "number", min: 0, max: 1 },
    theme:   { type: "enum", values: ["light", "dark"] },
    dense:   { type: "boolean" }
  };
  var config = { bg: "#101820", accent: "gold", radius: 8, opacity: 0.9, theme: "dark", dense: true };
  var r = verifySkin(config, schema);
  assert.strictEqual(r.ok, true);                        // ORACLE
  assert.strictEqual(r.errors.length, 0);
  assert.deepStrictEqual(r.value, config);               // all fields passed
});

// ---- 2. hex-color forms all accepted ------------------------------------
ok("2 #rgb / #rrggbb / #rrggbbaa colors accepted", function () {
  ["#fff", "#ffffff", "#ffffffff", "#1A2b3C"].forEach(function (c) {
    assert.strictEqual(isColor(c), true, c);             // ORACLE
  });
});

// ---- 3. rgb()/rgba()/hsl() color functions accepted ---------------------
ok("3 rgb/rgba/hsl functions accepted", function () {
  ["rgb(10, 20, 30)", "rgba(0,0,0,0.5)", "hsl(210, 50%, 40%)"].forEach(function (c) {
    assert.strictEqual(isColor(c), true, c);             // ORACLE
  });
});

// ---- 4. CSS injection in a color is rejected ----------------------------
ok("4 injection color 'red; } body{...' rejected", function () {
  assert.strictEqual(isColor("red; } body{display:none}"), false);   // ORACLE
  var r = verifySkin({ bg: "red; } body{}" }, { bg: { type: "color" } });
  assert.strictEqual(r.ok, false);
  assert.strictEqual(r.errors[0].field, "bg");
});

// ---- 5. url() and javascript: rejected as cssvar ------------------------
ok("5 cssvar url()/javascript:/@import rejected", function () {
  assert.strictEqual(isSafeCssVar("url(evil.png)"), false);          // ORACLE
  assert.strictEqual(isSafeCssVar("javascript:alert(1)"), false);
  assert.strictEqual(isSafeCssVar("@import 'x'"), false);
  assert.strictEqual(isSafeCssVar("a; color: red"), false);         // semicolon
});

// ---- 6. a benign cssvar value accepted ----------------------------------
ok("6 benign cssvar 'clamp(1rem, 2vw, 2rem)'-ish accepted", function () {
  assert.strictEqual(isSafeCssVar("1.5 2px 10%"), true);            // ORACLE
  assert.strictEqual(isSafeCssVar("rgba(0,0,0,0.2)"), true);
  var r = verifySkin({ shadow: "0 1px 3px" }, { shadow: { type: "cssvar" } });
  assert.strictEqual(r.ok, true);
  assert.strictEqual(r.value.shadow, "0 1px 3px");
});

// ---- 7. number range enforced -------------------------------------------
ok("7 number out of range is an error", function () {
  var schema = { opacity: { type: "number", min: 0, max: 1 } };
  assert.strictEqual(verifySkin({ opacity: 1.5 }, schema).ok, false);  // ORACLE above max
  assert.strictEqual(verifySkin({ opacity: -0.1 }, schema).ok, false); // below min
  assert.strictEqual(verifySkin({ opacity: 0.4 }, schema).ok, true);   // in range
});

// ---- 8. integer rejects a fractional value ------------------------------
ok("8 integer field rejects 8.5", function () {
  var schema = { radius: { type: "integer" } };
  assert.strictEqual(verifySkin({ radius: 8.5 }, schema).ok, false);   // ORACLE
  assert.strictEqual(verifySkin({ radius: 8 }, schema).ok, true);
});

// ---- 9. enum enforces the allowed set -----------------------------------
ok("9 enum rejects an off-list value", function () {
  var schema = { theme: { type: "enum", values: ["light", "dark"] } };
  assert.strictEqual(verifySkin({ theme: "neon" }, schema).ok, false); // ORACLE
  assert.strictEqual(verifySkin({ theme: "light" }, schema).ok, true);
});

// ---- 10. boolean type-checked -------------------------------------------
ok("10 boolean field rejects the string 'true'", function () {
  var schema = { dense: { type: "boolean" } };
  assert.strictEqual(verifySkin({ dense: "true" }, schema).ok, false); // ORACLE: string != bool
  assert.strictEqual(verifySkin({ dense: false }, schema).ok, true);
});

// ---- 11. unknown field -> dropped + warned, NOT an error ----------------
ok("11 unknown field dropped with a warning, ok stays true", function () {
  var schema = { bg: { type: "color" } };
  var r = verifySkin({ bg: "#000", evilField: "x" }, schema);
  assert.strictEqual(r.ok, true);                        // ORACLE: unknown != error
  assert.strictEqual(r.warnings.length, 1);
  assert.strictEqual(r.warnings[0].field, "evilField");
  assert.strictEqual("evilField" in r.value, false);     // dropped from safe value
});

// ---- 12. required field missing -> error --------------------------------
ok("12 required-but-missing field is an error", function () {
  var schema = { bg: { type: "color", required: true } };
  var r = verifySkin({}, schema);
  assert.strictEqual(r.ok, false);                       // ORACLE
  assert.strictEqual(r.errors[0].message, "required field missing");
});

// ---- 13. optional missing field is silent -------------------------------
ok("13 optional missing field is neither error nor warning", function () {
  var schema = { bg: { type: "color" } };
  var r = verifySkin({}, schema);
  assert.strictEqual(r.ok, true);                        // ORACLE
  assert.strictEqual(r.errors.length, 0);
  assert.strictEqual(r.warnings.length, 0);
});

// ---- 14. non-object config -> one whole-object error, never throws ------
ok("14 non-object config reported, not thrown", function () {
  ["a string", 42, null, [1, 2]].forEach(function (bad) {
    var r = verifySkin(bad, { bg: { type: "color" } });
    assert.strictEqual(r.ok, false);                     // ORACLE
    assert.strictEqual(r.errors[0].field, null);
  });
});

// ---- 15. multiple errors all reported (not just the first) --------------
ok("15 several bad fields all reported", function () {
  var schema = {
    bg:      { type: "color" },
    radius:  { type: "integer" },
    theme:   { type: "enum", values: ["a"] }
  };
  var r = verifySkin({ bg: "not-a-color", radius: 1.2, theme: "z" }, schema);
  assert.strictEqual(r.ok, false);
  assert.strictEqual(r.errors.length, 3);                // ORACLE: all three
});

// ---- 16. RATCHET: bad schema (non-object) throws ------------------------
ok("16 ratchet: non-object schema throws", function () {
  assert.throws(function () { verifySkin({}, "not a schema"); }, /schema must be/);  // ORACLE
});

// ---- 17. RATCHET: unknown spec type throws ------------------------------
ok("17 ratchet: unknown spec type throws (programmer error)", function () {
  assert.throws(function () { verifySkin({ x: 1 }, { x: { type: "widget" } }); }, /unknown spec type/);
});

// ---- 18. value carries ONLY passing fields (a rejected field is absent) --
ok("18 rejected field absent from value, valid sibling present", function () {
  var schema = { good: { type: "color" }, bad: { type: "integer" } };
  var r = verifySkin({ good: "#abc", bad: 1.5 }, schema);
  assert.strictEqual(r.ok, false);
  assert.strictEqual(r.value.good, "#abc");              // ORACLE: good survives
  assert.strictEqual("bad" in r.value, false);           // bad excluded
});

console.log("\nloop21-verifyskin: " + pass + " passed, " + fail + " failed");
process.exit(fail ? 1 : 0);
Take the whole folder → MIT Node / browser, no dependencies