UNPKG

zeus-time

Version:

Deterministic, cryptographically verifiable time hashing for Node, browser, and Expo/React Native.

368 lines (358 loc) 12.9 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { executeAtZeusEpoch: () => executeAtZeusEpoch, generateZeusHash: () => generateZeusHash, isValidUnixTimestampSeconds: () => isValidUnixTimestampSeconds, isValidZPK1: () => isValidZPK1, isValidZeusBase64Url: () => isValidZeusBase64Url, isValidZeusHex: () => isValidZeusHex, legacyUnixToZeus: () => legacyUnixToZeus, legacyZeusHash: () => legacyZeusHash, legacyZeusToUnix: () => legacyZeusToUnix, normalizeTime: () => normalizeTime, packZPK1: () => packZPK1, unixToZeus: () => unixToZeus, unixToZeusSync: () => unixToZeusSync, unpackZPK1: () => unpackZPK1, validateZeusTimestamp: () => validateZeusTimestamp, verifyZeusHash: () => verifyZeusHash, zeusHash: () => zeusHash, zeusToUnix: () => zeusToUnix }); module.exports = __toCommonJS(index_exports); // src/normalize.ts function normalizeTime(input) { if (input instanceof Date) return input.toISOString(); if (typeof input === "number") { const ms = input < 1e12 ? input * 1e3 : input; const d = new Date(ms); if (isNaN(d.getTime())) throw new Error("Invalid UNIX timestamp number."); return d.toISOString(); } if (typeof input === "string") { const ISO_WITH_TZ = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/; if (!ISO_WITH_TZ.test(input)) { throw new Error( "Invalid timestamp string. Expected ISO 8601 with timezone, for example 2025-01-01T00:00:00Z." ); } const d = new Date(input); if (isNaN(d.getTime())) { throw new Error("Invalid timestamp string. Could not parse ISO 8601 value."); } return d.toISOString(); } throw new Error("Invalid timestamp input."); } // src/hash.ts var import_blake3 = require("@noble/hashes/blake3"); var import_sha256 = require("@noble/hashes/sha256"); var import_utils = require("@noble/hashes/utils"); // src/encode.ts function bytesToHex(bytes) { let out = ""; for (let i = 0; i < bytes.length; i++) { out += bytes[i].toString(16).padStart(2, "0"); } return out; } function bytesToBase64(bytes) { const g = globalThis; if (typeof g.Buffer !== "undefined") { return g.Buffer.from(bytes).toString("base64"); } if (typeof g.btoa === "function") { let bin = ""; for (let i2 = 0; i2 < bytes.length; i2++) bin += String.fromCharCode(bytes[i2]); return g.btoa(bin); } const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; let result = ""; let i = 0; for (; i + 2 < bytes.length; i += 3) { const n = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2]; result += chars[n >> 18 & 63] + chars[n >> 12 & 63] + chars[n >> 6 & 63] + chars[n & 63]; } if (i < bytes.length) { const a = bytes[i]; const b = i + 1 < bytes.length ? bytes[i + 1] : 0; const n = a << 16 | b << 8; result += chars[n >> 18 & 63] + chars[n >> 12 & 63]; result += i + 1 < bytes.length ? chars[n >> 6 & 63] : "="; result += "="; } return result; } function bytesToBase64Url(bytes) { return bytesToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } function constantTimeEqual(a, b) { if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i); return diff === 0; } // src/hash.ts function hashTimeNormalized(normalizedIsoUtc, algorithm, format) { const msg = (0, import_utils.utf8ToBytes)(normalizedIsoUtc); const digest = algorithm === "sha256" ? (0, import_sha256.sha256)(msg) : (0, import_blake3.blake3)(msg); if (format === "base64url") return bytesToBase64Url(digest); return bytesToHex(digest); } // src/api.ts function zeusHash(input, options = {}) { var _a, _b; const algorithm = (_a = options.algorithm) != null ? _a : "blake3"; const format = (_b = options.format) != null ? _b : "hex"; const iso = normalizeTime(input); return hashTimeNormalized(iso, algorithm, format); } async function generateZeusHash(input, options = {}) { return zeusHash(input, options); } function verifyZeusHash(input, expectedHash, options = {}) { const actual = zeusHash(input, options); return constantTimeEqual(actual, expectedHash); } function unixToZeusSync(unix, options = {}) { return zeusHash(unix, options); } async function unixToZeus(unix, options = {}) { return unixToZeusSync(unix, options); } function zeusToUnix(_zeusHash) { throw new Error("ZEUS hashes are one-way. Use the original timestamp or implement a lookup store if you need reverse mapping."); } // src/legacy.ts function legacyUnixToZeus(unix, format = "hex") { return zeusHash(unix, { algorithm: "sha256", format }); } function legacyZeusHash(input, format = "hex") { return zeusHash(input, { algorithm: "sha256", format }); } // src/validation.ts function isValidUnixTimestampSeconds(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0 && value < 1e11; } function isValidZeusHex(hash) { return typeof hash === "string" && /^[a-f0-9]{64}$/.test(hash); } function isValidZeusBase64Url(hash) { return typeof hash === "string" && /^[A-Za-z0-9_-]{43}$/.test(hash); } // src/compat.ts async function validateZeusTimestamp(timestamp, expectedHash) { const okFormat = isValidZeusHex(expectedHash) || isValidZeusBase64Url(expectedHash); if (!okFormat) return false; try { return verifyZeusHash(timestamp, expectedHash); } catch { return false; } } async function executeAtZeusEpoch(epochTime, callback) { const targetHash = unixToZeusSync(epochTime); const interval = setInterval(() => { const now = Math.floor(Date.now() / 1e3); const currentHash = unixToZeusSync(now); if (currentHash === targetHash) { clearInterval(interval); callback(); } }, 1e3); } function looksLikeIso(input) { return /^\d{4}-\d{2}-\d{2}T/.test(input); } function legacyZeusToUnix(zeusTime) { if (looksLikeIso(zeusTime)) { const ms = Date.parse(zeusTime); if (!Number.isFinite(ms)) { throw new Error("Invalid ISO timestamp. Cannot convert to unix seconds."); } return Math.floor(ms / 1e3); } throw new Error( "Input appears to be a ZEUS hash. ZEUS hashes are one-way. Use the original timestamp or a lookup store for reverse mapping." ); } // src/zpk.ts var import_blake32 = require("@noble/hashes/blake3"); var import_sha2562 = require("@noble/hashes/sha256"); var import_utils2 = require("@noble/hashes/utils"); function assertNoWhitespace(s) { if (/\s/.test(s)) throw new Error("ZPK1 contains whitespace, which is not permitted."); } function assertTag(tag) { if (tag.length === 0) throw new Error("ZPK1 tag must not be empty."); if (tag.includes("|") || tag.includes("=")) throw new Error("ZPK1 tag must not include '|' or '=' characters."); } function isLowerHex64(s) { return /^[0-9a-f]{64}$/.test(s); } function sortJson(value) { if (value === null) return null; if (typeof value !== "object") return value; if (Array.isArray(value)) return value.map(sortJson); const obj = value; const keys = Object.keys(obj).sort(); const out = {}; for (const k of keys) out[k] = sortJson(obj[k]); return out; } function base64ToBytes(input) { assertNoWhitespace(input); let s = input.replace(/-/g, "+").replace(/_/g, "/"); while (s.length % 4 !== 0) s += "="; const g = globalThis; if (typeof g.Buffer !== "undefined") { return new Uint8Array(g.Buffer.from(s, "base64")); } if (typeof g.atob === "function") { const bin = g.atob(s); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; } const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; const rev = {}; for (let i = 0; i < chars.length; i++) rev[chars[i]] = i; const cleaned = s.replace(/=+$/g, ""); const bytes = []; let buffer = 0; let bits = 0; for (let i = 0; i < cleaned.length; i++) { const c = cleaned[i]; const v = rev[c]; if (v === void 0) throw new Error("Invalid base64 character in bytes_b64 payload."); buffer = buffer << 6 | v; bits += 6; if (bits >= 8) { bits -= 8; bytes.push(buffer >> bits & 255); } } return new Uint8Array(bytes); } function canonicalizeToBytes(payload, canon) { if (canon === "utf8_exact") { if (typeof payload !== "string") throw new Error("utf8_exact canon requires a string payload."); return (0, import_utils2.utf8ToBytes)(payload); } if (canon === "json_sorted_compact") { const sorted = sortJson(payload); const compact = JSON.stringify(sorted); if (typeof compact !== "string") throw new Error("Failed to stringify JSON payload."); return (0, import_utils2.utf8ToBytes)(compact); } if (payload instanceof Uint8Array) return payload; if (typeof payload !== "string") throw new Error("bytes_b64 canon requires a base64/base64url string or Uint8Array payload."); return base64ToBytes(payload); } function hashBytes(bytes, algo) { return algo === "sha256" ? (0, import_sha2562.sha256)(bytes) : (0, import_blake32.blake3)(bytes); } function packZPK1(payload, opts) { var _a; const canon = opts.canon; const algo = (_a = opts.algo) != null ? _a : "blake3"; if (algo !== "blake3" && algo !== "sha256") { throw new Error("Invalid algo. Expected 'blake3' or 'sha256'."); } const bytes = canonicalizeToBytes(payload, canon); const digestHex = bytesToHex(hashBytes(bytes, algo)); if (!isLowerHex64(digestHex)) { throw new Error("Invalid digest produced. Expected 64 lowercase hex characters."); } if (opts.tag !== void 0) assertTag(opts.tag); const base = `ZPK1|canon=${canon}|algo=${algo}`; if (opts.tag) return `${base}|tag=${opts.tag}|digest=${digestHex}`; return `${base}|digest=${digestHex}`; } function unpackZPK1(packed) { if (typeof packed !== "string") throw new Error("ZPK1 must be a string."); assertNoWhitespace(packed); const parts = packed.split("|"); if (parts[0] !== "ZPK1") throw new Error("Invalid packed payload: expected prefix ZPK1."); if (parts.length !== 4 && parts.length !== 5) { throw new Error("Invalid packed payload: expected 4 or 5 pipe-delimited parts."); } const canonPart = parts[1]; const algoPart = parts[2]; const canon = canonPart.startsWith("canon=") ? canonPart.slice(6) : null; if (!canon) throw new Error("Invalid packed payload: missing canon field."); if (canon !== "utf8_exact" && canon !== "json_sorted_compact" && canon !== "bytes_b64") { throw new Error("Invalid packed payload: unsupported canon value."); } const algo = algoPart.startsWith("algo=") ? algoPart.slice(5) : null; if (!algo) throw new Error("Invalid packed payload: missing algo field."); if (algo !== "blake3" && algo !== "sha256") { throw new Error("Invalid packed payload: unsupported algo value."); } let tag; let digestPart; if (parts.length === 5) { const tagPart = parts[3]; if (!tagPart.startsWith("tag=")) throw new Error("Invalid packed payload: expected tag field."); tag = tagPart.slice(4); assertTag(tag); digestPart = parts[4]; } else { digestPart = parts[3]; } if (!digestPart.startsWith("digest=")) throw new Error("Invalid packed payload: missing digest field."); const digest = digestPart.slice(7); if (!isLowerHex64(digest)) { throw new Error("Invalid packed payload: digest must be 64 lowercase hex characters."); } return { canon, algo, tag, digest }; } function isValidZPK1(packed) { try { unpackZPK1(packed); return true; } catch { return false; } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { executeAtZeusEpoch, generateZeusHash, isValidUnixTimestampSeconds, isValidZPK1, isValidZeusBase64Url, isValidZeusHex, legacyUnixToZeus, legacyZeusHash, legacyZeusToUnix, normalizeTime, packZPK1, unixToZeus, unixToZeusSync, unpackZPK1, validateZeusTimestamp, verifyZeusHash, zeusHash, zeusToUnix }); //# sourceMappingURL=index.cjs.map