Spaces:
Running
Running
Dev5 ecosystem foundation (direct HF push): byte-identical shared modules + ecosystem router + serve.py registration + Dockerfile COPY. Fixes partial GitHub auto-sync; unblocks COPY.
Browse files
static/shared/szl_codename_sanitizer.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
* (c) 2026 Lutar, Stephen P. - SZL Holdings - ORCID 0009-0001-0110-4173
|
| 3 |
+
* ============================================================================
|
| 4 |
+
* szl_codename_sanitizer.js - SHARED MODULE (byte-identical across both apps)
|
| 5 |
+
* ----------------------------------------------------------------------------
|
| 6 |
+
* Doctrine gate G5: 0 user-visible codenames. Internal route keys
|
| 7 |
+
* (amaru_* / rosie_* / sentra / jarvis) are fine as JS keys, but EVERY
|
| 8 |
+
* user-visible string must read its honest public role:
|
| 9 |
+
* amaru -> YACHAY (cortex / brain / OSINT ingest)
|
| 10 |
+
* rosie -> Operator (orchestrator)
|
| 11 |
+
* sentra -> CHAPAQ (verdict / immune)
|
| 12 |
+
* jarvis -> Operator (assistant / orchestrator)
|
| 13 |
+
*
|
| 14 |
+
* NO CDN. Vanilla JS. Browser + Node. This is the single mapping table so the
|
| 15 |
+
* two apps never drift, plus a live rendered-DOM scanner used by the CI gate.
|
| 16 |
+
*
|
| 17 |
+
* Public API (window.SZLCodenames / module.exports):
|
| 18 |
+
* MAP - frozen {codename: publicRole}
|
| 19 |
+
* BANNED_RE - the canonical detector regex (global, i)
|
| 20 |
+
* sanitize(str) - replace banned tokens with public roles
|
| 21 |
+
* scan(str) - return [{token, index}] of banned hits
|
| 22 |
+
* scanDOM(rootNode?) - scan rendered text + key attrs; returns hits[]
|
| 23 |
+
* guardedRender(el) - sanitize textContent of a node in-place
|
| 24 |
+
* isClean(str) - boolean
|
| 25 |
+
* ============================================================================ */
|
| 26 |
+
(function (root, factory) {
|
| 27 |
+
var mod = factory();
|
| 28 |
+
if (typeof module === "object" && module.exports) { module.exports = mod; }
|
| 29 |
+
if (root) { root.SZLCodenames = mod; }
|
| 30 |
+
})(typeof self !== "undefined" ? self : (typeof window !== "undefined" ? window : null), function () {
|
| 31 |
+
"use strict";
|
| 32 |
+
|
| 33 |
+
// Case-preserving public-role mapping. Order: most-specific first.
|
| 34 |
+
var MAP = {
|
| 35 |
+
"amaru": "YACHAY",
|
| 36 |
+
"rosie": "Operator",
|
| 37 |
+
"sentra": "CHAPAQ",
|
| 38 |
+
"jarvis": "Operator"
|
| 39 |
+
};
|
| 40 |
+
try { Object.freeze(MAP); } catch (e) {}
|
| 41 |
+
|
| 42 |
+
// Word-ish boundary: catch amaru, rosie_digest, Sentra, JARVIS, amaru-feed.
|
| 43 |
+
// We match the bare codename token wherever it appears as a visible word part.
|
| 44 |
+
var TOKENS = ["amaru", "rosie", "sentra", "jarvis"];
|
| 45 |
+
var BANNED_RE = new RegExp("(" + TOKENS.join("|") + ")", "ig");
|
| 46 |
+
|
| 47 |
+
function _roleFor(tok) {
|
| 48 |
+
var low = String(tok).toLowerCase();
|
| 49 |
+
return MAP[low] || low;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Replace a matched codename, preserving a leading capital if the source was
|
| 53 |
+
// capitalized (so "Rosie" -> "Operator", "rosie" -> "Operator" both read well).
|
| 54 |
+
function sanitize(str) {
|
| 55 |
+
if (str == null) { return str; }
|
| 56 |
+
return String(str).replace(BANNED_RE, function (m) { return _roleFor(m); });
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
function scan(str) {
|
| 60 |
+
var out = [], s = String(str == null ? "" : str), m;
|
| 61 |
+
BANNED_RE.lastIndex = 0;
|
| 62 |
+
while ((m = BANNED_RE.exec(s)) !== null) {
|
| 63 |
+
out.push({ token: m[0], index: m.index });
|
| 64 |
+
if (m.index === BANNED_RE.lastIndex) { BANNED_RE.lastIndex++; }
|
| 65 |
+
}
|
| 66 |
+
return out;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
function isClean(str) { return scan(str).length === 0; }
|
| 70 |
+
|
| 71 |
+
/* Scan a rendered DOM subtree for visible banned tokens. Checks visible text
|
| 72 |
+
* nodes + the human-visible attributes (title, aria-label, alt, placeholder,
|
| 73 |
+
* value). Ignores id/class/data-* route keys (those are allowed internal keys).
|
| 74 |
+
* Returns [{token, where, sample}]. */
|
| 75 |
+
function scanDOM(rootNode) {
|
| 76 |
+
var doc = (typeof document !== "undefined") ? document : null;
|
| 77 |
+
var root = rootNode || (doc ? doc.body : null);
|
| 78 |
+
if (!root) { return []; }
|
| 79 |
+
var hits = [];
|
| 80 |
+
var VISIBLE_ATTRS = ["title", "aria-label", "alt", "placeholder", "value"];
|
| 81 |
+
|
| 82 |
+
// 1) text nodes
|
| 83 |
+
if (doc && doc.createTreeWalker) {
|
| 84 |
+
var walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
|
| 85 |
+
var n;
|
| 86 |
+
while ((n = walker.nextNode())) {
|
| 87 |
+
var t = n.nodeValue || "";
|
| 88 |
+
var sc = scan(t);
|
| 89 |
+
for (var i = 0; i < sc.length; i++) {
|
| 90 |
+
hits.push({ token: sc[i].token, where: "text", sample: t.slice(Math.max(0, sc[i].index - 12), sc[i].index + 16) });
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
// 2) visible attributes
|
| 95 |
+
var all = root.querySelectorAll ? root.querySelectorAll("*") : [];
|
| 96 |
+
for (var e = 0; e < all.length; e++) {
|
| 97 |
+
for (var a = 0; a < VISIBLE_ATTRS.length; a++) {
|
| 98 |
+
var av = all[e].getAttribute ? all[e].getAttribute(VISIBLE_ATTRS[a]) : null;
|
| 99 |
+
if (av) {
|
| 100 |
+
var sca = scan(av);
|
| 101 |
+
for (var j = 0; j < sca.length; j++) {
|
| 102 |
+
hits.push({ token: sca[j].token, where: "@" + VISIBLE_ATTRS[a], sample: av.slice(0, 40) });
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
return hits;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
function guardedRender(el) {
|
| 111 |
+
if (!el) { return el; }
|
| 112 |
+
if (typeof el.textContent === "string") { el.textContent = sanitize(el.textContent); }
|
| 113 |
+
return el;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
return {
|
| 117 |
+
VERSION: "1.0.0",
|
| 118 |
+
MAP: MAP,
|
| 119 |
+
TOKENS: TOKENS.slice(),
|
| 120 |
+
BANNED_RE: BANNED_RE,
|
| 121 |
+
sanitize: sanitize,
|
| 122 |
+
scan: scan,
|
| 123 |
+
isClean: isClean,
|
| 124 |
+
scanDOM: scanDOM,
|
| 125 |
+
guardedRender: guardedRender
|
| 126 |
+
};
|
| 127 |
+
});
|
static/shared/szl_label_engine.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
* (c) 2026 Lutar, Stephen P. - SZL Holdings - ORCID 0009-0001-0110-4173
|
| 3 |
+
* ============================================================================
|
| 4 |
+
* szl_label_engine.js - SHARED MODULE (byte-identical across a11oy + killinchu)
|
| 5 |
+
* ----------------------------------------------------------------------------
|
| 6 |
+
* The single honest-label primitive for the SZL estate. Renders the canonical
|
| 7 |
+
* doctrine badges. NO build step, NO CDN, vanilla ES5-safe JS, browser + Node.
|
| 8 |
+
*
|
| 9 |
+
* Doctrine gate G7 (honest labels) + G8 (the half-state is the only
|
| 10 |
+
* unacceptable outcome). Every demo surface labels what is real; this module is
|
| 11 |
+
* the one place the badge vocabulary, colors and semantics live so the two apps
|
| 12 |
+
* never drift. Importing tabs MUST NOT invent their own pills.
|
| 13 |
+
*
|
| 14 |
+
* Canonical badge vocabulary (the ONLY allowed honest labels):
|
| 15 |
+
* LIVE - real backend wired, data is genuinely live, honest
|
| 16 |
+
* SAMPLE - honest sample / illustrative fixture (not live)
|
| 17 |
+
* FORECAST - model-produced forward estimate (e.g. seismic pulse)
|
| 18 |
+
* OSINT - open-source intelligence (sensed, unauthenticated claims)
|
| 19 |
+
* MODELED - deterministic model output, labeled (e.g. RUL, ent. forecast)
|
| 20 |
+
* SIMULATED - effector / decision simulation, human-on-the-loop (G6)
|
| 21 |
+
* CONNECT-READY - real schema, awaits OAuth; sample until connected
|
| 22 |
+
* EXPERIMENTAL - CI-green Lean wave, not locked-8
|
| 23 |
+
* HEURISTIC - rule/heuristic advisory, not proven
|
| 24 |
+
* PQC-ROADMAP - post-quantum signing is roadmap, never shown deployed (G4)
|
| 25 |
+
* ILLUSTRATIVE - explicitly illustrative (e.g. wow ROI)
|
| 26 |
+
*
|
| 27 |
+
* Public API (window.SZLLabels / module.exports):
|
| 28 |
+
* SCHEMES - frozen registry {KEY:{label,cls,title,...}}
|
| 29 |
+
* badge(key, opts?) - returns an HTMLElement <span class="szl-pill ...">
|
| 30 |
+
* badgeHTML(key, opts?) - returns a sanitized HTML string
|
| 31 |
+
* isHonestLabel(key) - boolean: is this a canonical honest label?
|
| 32 |
+
* normalize(key) - canonicalizes aliases ("connect_ready" -> "CONNECT-READY")
|
| 33 |
+
* auditText(str) - returns [] of forbidden raw claims (e.g. "100%", "tamper-proof")
|
| 34 |
+
* ensureStyle(doc?) - injects the <style> once (idempotent)
|
| 35 |
+
* ============================================================================ */
|
| 36 |
+
(function (root, factory) {
|
| 37 |
+
var mod = factory();
|
| 38 |
+
if (typeof module === "object" && module.exports) { module.exports = mod; }
|
| 39 |
+
if (root) { root.SZLLabels = mod; }
|
| 40 |
+
})(typeof self !== "undefined" ? self : (typeof window !== "undefined" ? window : null), function () {
|
| 41 |
+
"use strict";
|
| 42 |
+
|
| 43 |
+
/* Canonical registry. cls is a CSS modifier; colors are doctrine-fixed so the
|
| 44 |
+
* two apps render identical pills. tone: ok|warn|info|sim|bad — drives color. */
|
| 45 |
+
var SCHEMES = {
|
| 46 |
+
"LIVE": { label: "LIVE", tone: "ok", title: "Real backend wired - data is genuinely live and honest." },
|
| 47 |
+
"SAMPLE": { label: "SAMPLE", tone: "info", title: "Honest sample / illustrative fixture - not a live feed." },
|
| 48 |
+
"FORECAST": { label: "FORECAST", tone: "warn", title: "Model-produced forward estimate. Not an observation." },
|
| 49 |
+
"OSINT": { label: "OSINT", tone: "info", title: "Open-source intelligence. Sensed, unauthenticated claims." },
|
| 50 |
+
"MODELED": { label: "MODELED", tone: "warn", title: "Deterministic model output, labeled. Not measured." },
|
| 51 |
+
"SIMULATED": { label: "SIMULATED", tone: "sim", title: "Effector / decision simulation, human-on-the-loop. No live control." },
|
| 52 |
+
"CONNECT-READY": { label: "CONNECT-READY", tone: "info", title: "Real schema, awaits OAuth. Sample data until connected." },
|
| 53 |
+
"EXPERIMENTAL": { label: "EXPERIMENTAL", tone: "warn", title: "CI-green Lean wave - NOT part of the locked-8." },
|
| 54 |
+
"HEURISTIC": { label: "HEURISTIC", tone: "warn", title: "Rule / heuristic advisory. Not a proven result." },
|
| 55 |
+
"PQC-ROADMAP": { label: "PQC-ROADMAP", tone: "warn", title: "Post-quantum signing is roadmap. Never shown as deployed (G4)." },
|
| 56 |
+
"ILLUSTRATIVE": { label: "ILLUSTRATIVE", tone: "info", title: "Explicitly illustrative figure (e.g. ROI)." }
|
| 57 |
+
};
|
| 58 |
+
// freeze so importing tabs cannot mutate the doctrine vocabulary
|
| 59 |
+
try { Object.freeze(SCHEMES); for (var k in SCHEMES) { Object.freeze(SCHEMES[k]); } } catch (e) {}
|
| 60 |
+
|
| 61 |
+
var ALIASES = {
|
| 62 |
+
"CONNECT_READY": "CONNECT-READY", "CONNECTREADY": "CONNECT-READY",
|
| 63 |
+
"PQC_ROADMAP": "PQC-ROADMAP", "PQCROADMAP": "PQC-ROADMAP", "PQC": "PQC-ROADMAP",
|
| 64 |
+
"SIM": "SIMULATED", "EXP": "EXPERIMENTAL", "MODEL": "MODELED"
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
function normalize(key) {
|
| 68 |
+
if (key == null) { return ""; }
|
| 69 |
+
var u = String(key).trim().toUpperCase().replace(/\s+/g, "-");
|
| 70 |
+
if (SCHEMES[u]) { return u; }
|
| 71 |
+
var nodash = u.replace(/[-_]/g, "");
|
| 72 |
+
if (ALIASES[u]) { return ALIASES[u]; }
|
| 73 |
+
if (ALIASES[nodash]) { return ALIASES[nodash]; }
|
| 74 |
+
return u; // unknown - caller decides
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
function isHonestLabel(key) { return Object.prototype.hasOwnProperty.call(SCHEMES, normalize(key)); }
|
| 78 |
+
|
| 79 |
+
function esc(s) {
|
| 80 |
+
return String(s == null ? "" : s)
|
| 81 |
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
| 82 |
+
.replace(/"/g, """).replace(/'/g, "'");
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/* Forbidden raw claims a tab must never render (doctrine G2/G3/G4/G7).
|
| 86 |
+
* auditText() returns the list of violations found in a rendered string. */
|
| 87 |
+
var FORBIDDEN = [
|
| 88 |
+
{ re: /\b100\s*%/, why: "trust is never 100% (Lambda capped < 1.0)" },
|
| 89 |
+
{ re: /\btamper-?proof\b/i, why: "use 'tamper-evident', not 'tamper-proof' (G3)" },
|
| 90 |
+
{ re: /\bunique\s+theorem\b/i, why: "Lambda uniqueness is Conjecture 1, not a theorem (G2)" },
|
| 91 |
+
{ re: /\bunconditional(ly)?\s+(unique|proven)\b/i, why: "Lambda uniqueness is conditional (Conjecture 1) (G2)" },
|
| 92 |
+
{ re: /\bSLSA\s*L3\b(?!\s*roadmap)/i, why: "SLSA must read 'L1 honest / L2 attested / L3 roadmap' (G4)" },
|
| 93 |
+
{ re: /\b(FedRAMP|IronBank|CMMC|ATO)\b/i, why: "never claim FedRAMP/IronBank/CMMC/ATO as achieved (G4)" },
|
| 94 |
+
{ re: /\blocked\s*=?\s*5\b/i, why: "locked-proven is EXACTLY 8, never 5 (G1)" }
|
| 95 |
+
];
|
| 96 |
+
function auditText(str) {
|
| 97 |
+
var out = [], s = String(str == null ? "" : str), i;
|
| 98 |
+
for (i = 0; i < FORBIDDEN.length; i++) { if (FORBIDDEN[i].re.test(s)) { out.push(FORBIDDEN[i].why); } }
|
| 99 |
+
return out;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
var STYLE_ID = "szl-label-engine-style";
|
| 103 |
+
var CSS =
|
| 104 |
+
"." + "szl-pill{display:inline-flex;align-items:center;gap:6px;font:700 11px/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;" +
|
| 105 |
+
"letter-spacing:.04em;padding:4px 9px;border-radius:7px;border:1px solid;text-transform:uppercase;white-space:nowrap;vertical-align:middle;}" +
|
| 106 |
+
".szl-pill .szl-dot{width:7px;height:7px;border-radius:50%;background:currentColor;flex:0 0 auto;}" +
|
| 107 |
+
".szl-pill.ok{color:#16c784;border-color:#16c78455;background:#16c7841a;}" +
|
| 108 |
+
".szl-pill.warn{color:#e0a106;border-color:#e0a10655;background:#e0a1061a;}" +
|
| 109 |
+
".szl-pill.info{color:#3aa0ff;border-color:#3aa0ff55;background:#3aa0ff1a;}" +
|
| 110 |
+
".szl-pill.sim{color:#b07cff;border-color:#b07cff55;background:#b07cff1a;}" +
|
| 111 |
+
".szl-pill.bad{color:#ff5c5c;border-color:#ff5c5c55;background:#ff5c5c1a;}" +
|
| 112 |
+
".szl-pill.unknown{color:#888;border-color:#88888855;background:#8888881a;}";
|
| 113 |
+
|
| 114 |
+
function ensureStyle(doc) {
|
| 115 |
+
var d = doc || (typeof document !== "undefined" ? document : null);
|
| 116 |
+
if (!d || d.getElementById(STYLE_ID)) { return; }
|
| 117 |
+
var st = d.createElement("style"); st.id = STYLE_ID; st.textContent = CSS;
|
| 118 |
+
(d.head || d.documentElement).appendChild(st);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
function resolve(key) {
|
| 122 |
+
var nk = normalize(key);
|
| 123 |
+
var s = SCHEMES[nk];
|
| 124 |
+
if (s) { return { key: nk, label: s.label, tone: s.tone, title: s.title, known: true }; }
|
| 125 |
+
return { key: nk, label: nk || "?", tone: "unknown", title: "Unknown / non-canonical label - not part of the honest vocabulary.", known: false };
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
function badgeHTML(key, opts) {
|
| 129 |
+
opts = opts || {};
|
| 130 |
+
var r = resolve(key);
|
| 131 |
+
var label = opts.label != null ? String(opts.label) : r.label;
|
| 132 |
+
var title = opts.title != null ? String(opts.title) : r.title;
|
| 133 |
+
var extra = opts.className ? (" " + String(opts.className)) : "";
|
| 134 |
+
var dot = opts.dot === false ? "" : '<span class="szl-dot" aria-hidden="true"></span>';
|
| 135 |
+
return '<span class="szl-pill ' + esc(r.tone) + extra + '" data-szl-label="' + esc(r.key) +
|
| 136 |
+
'" title="' + esc(title) + '" role="img" aria-label="data label: ' + esc(label) + '">' +
|
| 137 |
+
dot + esc(label) + "</span>";
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
function badge(key, opts) {
|
| 141 |
+
opts = opts || {};
|
| 142 |
+
var doc = opts.document || (typeof document !== "undefined" ? document : null);
|
| 143 |
+
if (!doc) { throw new Error("SZLLabels.badge requires a DOM document; use badgeHTML in Node."); }
|
| 144 |
+
ensureStyle(doc);
|
| 145 |
+
var tmp = doc.createElement("div");
|
| 146 |
+
tmp.innerHTML = badgeHTML(key, opts);
|
| 147 |
+
return tmp.firstChild;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
return {
|
| 151 |
+
VERSION: "1.0.0",
|
| 152 |
+
SCHEMES: SCHEMES,
|
| 153 |
+
badge: badge,
|
| 154 |
+
badgeHTML: badgeHTML,
|
| 155 |
+
isHonestLabel: isHonestLabel,
|
| 156 |
+
normalize: normalize,
|
| 157 |
+
auditText: auditText,
|
| 158 |
+
ensureStyle: ensureStyle,
|
| 159 |
+
keys: function () { var a = [], k; for (k in SCHEMES) { a.push(k); } return a; }
|
| 160 |
+
};
|
| 161 |
+
});
|
static/shared/szl_receipt_cosign.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
* (c) 2026 Lutar, Stephen P. - SZL Holdings - ORCID 0009-0001-0110-4173
|
| 3 |
+
* ============================================================================
|
| 4 |
+
* szl_receipt_cosign.js - SHARED MODULE (byte-identical across a11oy + killinchu)
|
| 5 |
+
* ----------------------------------------------------------------------------
|
| 6 |
+
* The single DSSE receipt + cosign-verify primitive for the SZL estate.
|
| 7 |
+
* NO CDN, no build step. Browser-native SubtleCrypto only. Node-safe import.
|
| 8 |
+
*
|
| 9 |
+
* *** ONE documented signing scheme for the whole estate: ***
|
| 10 |
+
* ECDSA P-256 (SECP256R1) over SHA-256 == Sigstore cosign default.
|
| 11 |
+
*
|
| 12 |
+
* WHY P-256 and NOT Ed25519 (resolving the signing-scheme drift, gate G4):
|
| 13 |
+
* - The deployed estate, szl_dsse.py (byte-identical in both apps), every
|
| 14 |
+
* server verify path, and the PUBLISHED key szl-holdings/.github/cosign.pub
|
| 15 |
+
* are ALL ECDSA P-256. cosign sign-blob / verify-blob round-trips against it.
|
| 16 |
+
* - a11oy_signing_key.py states the curve is "deliberately NOT Ed25519" to
|
| 17 |
+
* match cosign.pub and every existing verify path.
|
| 18 |
+
* - Switching to Ed25519 would orphan cosign.pub and break cosign-CLI
|
| 19 |
+
* interop. Doctrine G8 ("don't claim/ship more than is real") => keep the
|
| 20 |
+
* scheme that actually verifies. The Ed25519 suggestion is therefore
|
| 21 |
+
* RESOLVED in favor of ECDSA-P256. HMAC is forbidden (symmetric, not
|
| 22 |
+
* publicly verifiable). This is the single source of truth for clients.
|
| 23 |
+
*
|
| 24 |
+
* DSSE (secure-systems-lab/dsse) PAE pre-authentication encoding (must match
|
| 25 |
+
* szl_dsse.py byte-for-byte):
|
| 26 |
+
* PAE(type, body) = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body
|
| 27 |
+
* SIGNATURE = ECDSA-P256-SHA256( PAE(payloadType, canonical_body) )
|
| 28 |
+
*
|
| 29 |
+
* HONESTY (gate G7/G8): this module NEVER fabricates a signature. Signing is
|
| 30 |
+
* done by the SAME-ORIGIN server endpoint POST /khipu/sign (which holds the
|
| 31 |
+
* szlholdings-cosign private key). The client:
|
| 32 |
+
* - builds the canonical receipt + chain hash (SHA-256, re-checkable),
|
| 33 |
+
* - asks the server to sign,
|
| 34 |
+
* - records keyid + sig_type returned (truthfully),
|
| 35 |
+
* - if the signer is unreachable OR returns an ephemeral/non-canonical key,
|
| 36 |
+
* marks the receipt UNSIGNED / CROSS-APP-PENDING and shows it honestly.
|
| 37 |
+
*
|
| 38 |
+
* Public API (window.SZLReceipts / module.exports):
|
| 39 |
+
* SCHEME - "ECDSA-P256-SHA256" (frozen)
|
| 40 |
+
* COSIGN_KEYID - "szlholdings-cosign" (frozen)
|
| 41 |
+
* COSIGN_PUB_URL - canonical published key URL
|
| 42 |
+
* canonicalJSON(obj) - deterministic JSON (sorted keys)
|
| 43 |
+
* sha256Hex(strOrBytes) -> Promise - SubtleCrypto SHA-256 hex
|
| 44 |
+
* pae(type, bodyBytes) -> Uint8Array - DSSE PAE bytes
|
| 45 |
+
* chainHash(prevHash, receipt) -> P - SHA-256 of (prev_hash || canonical)
|
| 46 |
+
* signReceipt(receipt, opts) -> P - POST /khipu/sign; honest never-fake
|
| 47 |
+
* verifyEnvelope(env, opts) -> P - POST /khipu/verify; cross-app verdict
|
| 48 |
+
* isCanonicalKey(keyid) - keyid === szlholdings-cosign ?
|
| 49 |
+
* newChain(opts) - a small append-only signed chain helper
|
| 50 |
+
* ============================================================================ */
|
| 51 |
+
(function (root, factory) {
|
| 52 |
+
var mod = factory();
|
| 53 |
+
if (typeof module === "object" && module.exports) { module.exports = mod; }
|
| 54 |
+
if (root) { root.SZLReceipts = mod; }
|
| 55 |
+
})(typeof self !== "undefined" ? self : (typeof window !== "undefined" ? window : null), function () {
|
| 56 |
+
"use strict";
|
| 57 |
+
|
| 58 |
+
var SCHEME = "ECDSA-P256-SHA256";
|
| 59 |
+
var COSIGN_KEYID = "szlholdings-cosign";
|
| 60 |
+
var COSIGN_PUB_URL = "https://github.com/szl-holdings/.github/blob/main/cosign.pub";
|
| 61 |
+
var KHIPU_PAYLOAD_TYPE = "application/vnd.szl.khipu+json";
|
| 62 |
+
|
| 63 |
+
function _subtle() {
|
| 64 |
+
var c = (typeof self !== "undefined" && self.crypto) ? self.crypto
|
| 65 |
+
: (typeof window !== "undefined" && window.crypto) ? window.crypto
|
| 66 |
+
: (typeof globalThis !== "undefined" && globalThis.crypto) ? globalThis.crypto : null;
|
| 67 |
+
return c && c.subtle ? c.subtle : null;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/* Deterministic canonical JSON: object keys sorted recursively, no spaces.
|
| 71 |
+
* MUST match szl_dsse.canonical_json (json.dumps(sort_keys, separators=(',',':'))). */
|
| 72 |
+
function canonicalJSON(obj) {
|
| 73 |
+
if (obj === null || typeof obj !== "object") { return JSON.stringify(obj); }
|
| 74 |
+
if (Object.prototype.toString.call(obj) === "[object Array]") {
|
| 75 |
+
var items = []; for (var i = 0; i < obj.length; i++) { items.push(canonicalJSON(obj[i])); }
|
| 76 |
+
return "[" + items.join(",") + "]";
|
| 77 |
+
}
|
| 78 |
+
var keys = []; for (var k in obj) { if (Object.prototype.hasOwnProperty.call(obj, k)) { keys.push(k); } }
|
| 79 |
+
keys.sort();
|
| 80 |
+
var parts = []; for (var j = 0; j < keys.length; j++) { parts.push(JSON.stringify(keys[j]) + ":" + canonicalJSON(obj[keys[j]])); }
|
| 81 |
+
return "{" + parts.join(",") + "}";
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
function _toBytes(strOrBytes) {
|
| 85 |
+
if (strOrBytes instanceof Uint8Array) { return strOrBytes; }
|
| 86 |
+
var s = String(strOrBytes);
|
| 87 |
+
if (typeof TextEncoder !== "undefined") { return new TextEncoder().encode(s); }
|
| 88 |
+
var arr = []; for (var i = 0; i < s.length; i++) { arr.push(s.charCodeAt(i) & 0xff); }
|
| 89 |
+
return new Uint8Array(arr);
|
| 90 |
+
}
|
| 91 |
+
function _hex(buf) {
|
| 92 |
+
var b = new Uint8Array(buf), h = ""; for (var i = 0; i < b.length; i++) { h += (b[i] < 16 ? "0" : "") + b[i].toString(16); }
|
| 93 |
+
return h;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
function sha256Hex(strOrBytes) {
|
| 97 |
+
var sub = _subtle();
|
| 98 |
+
if (!sub) { return Promise.reject(new Error("SubtleCrypto unavailable (needs HTTPS/secure context)")); }
|
| 99 |
+
return sub.digest("SHA-256", _toBytes(strOrBytes)).then(_hex);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/* DSSE PAE bytes. Lengths are decimal ASCII of the UTF-8 byte length. */
|
| 103 |
+
function pae(payloadType, bodyBytes) {
|
| 104 |
+
var t = _toBytes(payloadType), b = (bodyBytes instanceof Uint8Array) ? bodyBytes : _toBytes(bodyBytes);
|
| 105 |
+
var head = "DSSEv1 " + t.length + " ";
|
| 106 |
+
var mid = " " + b.length + " ";
|
| 107 |
+
var pre = _toBytes(head), tb = t, midb = _toBytes(mid);
|
| 108 |
+
var out = new Uint8Array(pre.length + tb.length + midb.length + b.length);
|
| 109 |
+
var o = 0;
|
| 110 |
+
out.set(pre, o); o += pre.length;
|
| 111 |
+
out.set(tb, o); o += tb.length;
|
| 112 |
+
out.set(midb, o); o += midb.length;
|
| 113 |
+
out.set(b, o);
|
| 114 |
+
return out;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
function chainHash(prevHash, receipt) {
|
| 118 |
+
var canonical = canonicalJSON(receipt);
|
| 119 |
+
return sha256Hex(String(prevHash || "0") + "|" + canonical).then(function (h) {
|
| 120 |
+
return { hash: h, canonical: canonical };
|
| 121 |
+
});
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
function isCanonicalKey(keyid) { return keyid === COSIGN_KEYID; }
|
| 125 |
+
|
| 126 |
+
/* Ask the SAME-ORIGIN server to sign. NEVER fabricates a signature.
|
| 127 |
+
* opts: { base:"", payloadType, fetchImpl } */
|
| 128 |
+
function signReceipt(receipt, opts) {
|
| 129 |
+
opts = opts || {};
|
| 130 |
+
var base = opts.base || "";
|
| 131 |
+
var f = opts.fetchImpl || (typeof fetch !== "undefined" ? fetch : null);
|
| 132 |
+
if (!f) { return Promise.reject(new Error("fetch unavailable")); }
|
| 133 |
+
var body = JSON.stringify(receipt);
|
| 134 |
+
return f(base + "/khipu/sign", {
|
| 135 |
+
method: "POST", headers: { "Content-Type": "application/json" }, body: body
|
| 136 |
+
}).then(function (resp) {
|
| 137 |
+
var ct = resp.headers && resp.headers.get ? (resp.headers.get("content-type") || "") : "";
|
| 138 |
+
if (!resp.ok) { return resp.text().then(function (t) { throw new Error("signer HTTP " + resp.status + ": " + t.slice(0, 160)); }); }
|
| 139 |
+
if (ct.indexOf("application/json") === -1) { throw new Error("signer returned non-JSON (route not mounted)"); }
|
| 140 |
+
return resp.json();
|
| 141 |
+
}).then(function (j) {
|
| 142 |
+
var env = j.envelope || j;
|
| 143 |
+
var sig0 = (env.signatures && env.signatures[0]) || null;
|
| 144 |
+
var keyid = sig0 ? sig0.keyid : null;
|
| 145 |
+
var sigType = (sig0 && sig0.sig_type) || (j.sig_types && j.sig_types[0]) || SCHEME;
|
| 146 |
+
var hasSig = !!(sig0 && sig0.sig);
|
| 147 |
+
var canonical = isCanonicalKey(keyid);
|
| 148 |
+
return {
|
| 149 |
+
signed: hasSig,
|
| 150 |
+
canonicalKey: canonical,
|
| 151 |
+
crossAppVerifiable: hasSig && canonical,
|
| 152 |
+
keyid: keyid,
|
| 153 |
+
sigType: sigType,
|
| 154 |
+
sig: sig0 ? sig0.sig : null,
|
| 155 |
+
envelope: env,
|
| 156 |
+
signerNote: hasSig
|
| 157 |
+
? (canonical ? "" : "signer used a NON-CANONICAL key (" + keyid + "); cross-app verify PENDING operator cosign secret")
|
| 158 |
+
: "signer returned no signature - receipt is UNSIGNED (honest)"
|
| 159 |
+
};
|
| 160 |
+
})["catch"](function (err) {
|
| 161 |
+
// HONEST failure - never fake a signature
|
| 162 |
+
return {
|
| 163 |
+
signed: false, canonicalKey: false, crossAppVerifiable: false,
|
| 164 |
+
keyid: null, sigType: null, sig: null, envelope: null,
|
| 165 |
+
signerNote: "signer unreachable - receipt is UNSIGNED (honest): " + err.message
|
| 166 |
+
};
|
| 167 |
+
});
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/* Verify a DSSE envelope via the server /khipu/verify (verifies against the
|
| 171 |
+
* published cosign.pub). Returns a truthful verdict. */
|
| 172 |
+
function verifyEnvelope(envelope, opts) {
|
| 173 |
+
opts = opts || {};
|
| 174 |
+
var base = opts.base || "";
|
| 175 |
+
var f = opts.fetchImpl || (typeof fetch !== "undefined" ? fetch : null);
|
| 176 |
+
if (!f) { return Promise.reject(new Error("fetch unavailable")); }
|
| 177 |
+
return f(base + "/khipu/verify", {
|
| 178 |
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(envelope)
|
| 179 |
+
}).then(function (resp) {
|
| 180 |
+
if (!resp.ok) { throw new Error("verify HTTP " + resp.status); }
|
| 181 |
+
return resp.json();
|
| 182 |
+
}).then(function (j) {
|
| 183 |
+
return {
|
| 184 |
+
verified: j.verified === true,
|
| 185 |
+
keyidMatch: j.keyid_match === true,
|
| 186 |
+
keyidExpected: (j.detail && j.detail.keyid_expected) || COSIGN_KEYID,
|
| 187 |
+
pubFingerprint: (j.detail && j.detail.pub_fingerprint_sha256) || null,
|
| 188 |
+
verifyKeyUrl: (j.detail && j.detail.verify_key_url) || COSIGN_PUB_URL,
|
| 189 |
+
raw: j
|
| 190 |
+
};
|
| 191 |
+
});
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
/* Small append-only signed chain helper. Each push() hashes (prev||receipt),
|
| 195 |
+
* signs via the server, and links. Honest about UNSIGNED / cross-app state. */
|
| 196 |
+
function newChain(opts) {
|
| 197 |
+
opts = opts || {};
|
| 198 |
+
var base = opts.base || "";
|
| 199 |
+
var chain = [];
|
| 200 |
+
var tip = "0";
|
| 201 |
+
return {
|
| 202 |
+
receipts: chain,
|
| 203 |
+
tip: function () { return tip; },
|
| 204 |
+
push: function (action, data) {
|
| 205 |
+
var seq = chain.length;
|
| 206 |
+
var receipt = { action: String(action), seq: seq, prev_hash: tip, data: data || {}, ts: new Date().toISOString() };
|
| 207 |
+
return chainHash(tip, receipt).then(function (ch) {
|
| 208 |
+
receipt.hash = ch.hash; receipt.canonical = ch.canonical;
|
| 209 |
+
return signReceipt({ action: receipt.action, seq: seq, prev_hash: tip, data: receipt.data }, { base: base });
|
| 210 |
+
}).then(function (sg) {
|
| 211 |
+
receipt.signed = sg.signed; receipt.keyid = sg.keyid; receipt.sigType = sg.sigType;
|
| 212 |
+
receipt.sig = sg.sig; receipt.canonicalKey = sg.canonicalKey;
|
| 213 |
+
receipt.crossAppVerifiable = sg.crossAppVerifiable; receipt.signerNote = sg.signerNote;
|
| 214 |
+
chain.push(receipt); tip = receipt.hash;
|
| 215 |
+
return receipt;
|
| 216 |
+
});
|
| 217 |
+
}
|
| 218 |
+
};
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
try { /* freeze constants */ } catch (e) {}
|
| 222 |
+
return {
|
| 223 |
+
VERSION: "1.0.0",
|
| 224 |
+
SCHEME: SCHEME,
|
| 225 |
+
COSIGN_KEYID: COSIGN_KEYID,
|
| 226 |
+
COSIGN_PUB_URL: COSIGN_PUB_URL,
|
| 227 |
+
PAYLOAD_TYPE: KHIPU_PAYLOAD_TYPE,
|
| 228 |
+
canonicalJSON: canonicalJSON,
|
| 229 |
+
sha256Hex: sha256Hex,
|
| 230 |
+
pae: pae,
|
| 231 |
+
chainHash: chainHash,
|
| 232 |
+
isCanonicalKey: isCanonicalKey,
|
| 233 |
+
signReceipt: signReceipt,
|
| 234 |
+
verifyEnvelope: verifyEnvelope,
|
| 235 |
+
newChain: newChain
|
| 236 |
+
};
|
| 237 |
+
});
|