zeus-time
Version:
Deterministic, cryptographically verifiable time hashing for Node, browser, and Expo/React Native.
324 lines (316 loc) • 11 kB
JavaScript
// 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
import { blake3 } from "@noble/hashes/blake3";
import { sha256 } from "@noble/hashes/sha256";
import { utf8ToBytes } from "@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 = utf8ToBytes(normalizedIsoUtc);
const digest = algorithm === "sha256" ? sha256(msg) : 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
import { blake3 as blake32 } from "@noble/hashes/blake3";
import { sha256 as sha2562 } from "@noble/hashes/sha256";
import { utf8ToBytes as utf8ToBytes2 } from "@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 utf8ToBytes2(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 utf8ToBytes2(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" ? sha2562(bytes) : blake32(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;
}
}
export {
executeAtZeusEpoch,
generateZeusHash,
isValidUnixTimestampSeconds,
isValidZPK1,
isValidZeusBase64Url,
isValidZeusHex,
legacyUnixToZeus,
legacyZeusHash,
legacyZeusToUnix,
normalizeTime,
packZPK1,
unixToZeus,
unixToZeusSync,
unpackZPK1,
validateZeusTimestamp,
verifyZeusHash,
zeusHash,
zeusToUnix
};
//# sourceMappingURL=index.js.map