cesr
Version:
[](https://www.npmjs.com/package/cesr) [](https://github.com/lenkan/cesr-js/blob/main/LICENSE)
302 lines (301 loc) • 10.8 kB
JavaScript
import { encodeBase64Int } from "./encoding-base64.js";
import { decodeBase64Int, decodeBase64Url } from "./encoding-base64.js";
import { IndexCode, IndexTable } from "./codes.js";
import { MatterCode, MatterTable } from "./codes.js";
import { CountCode_10, CountCode_20 } from "./codes.js";
import { decodeUtf8, encodeUtf8 } from "./encoding-utf8.js";
import { CodeTable } from "./code-table.js";
const encoder = new CodeTable(MatterTable);
const indexer = new CodeTable(IndexTable);
// VERSION = "PPPPVVVKKKKBBBB.";
// LEGACY_VERSION = "PPPPvvKKKKllllll_";
const REGEX_VERSION_STRING_PROTOCOL = /^[A-Z]{4}$/;
const REGEX_VERSION_STRING_KIND = /^[A-Z]{4}$/;
const REGEX_VERSION_JSON = /^\{"v":"(.*?)".*$/;
const REGEX_BASE64_CHARACTER = /^[A-Za-z0-9\-_]+$/;
function padNumber(num, length) {
return num.toString().padStart(length, "0");
}
function encodeHexInt(value, length) {
if (value >= 16 ** length) {
throw new Error(`value ${value} too big for hex length ${length}`);
}
return value.toString(16).padStart(length, "0");
}
export function encodeIndexedSignature(alg, raw, index) {
switch (alg) {
case "ed25519":
return encodeIndexer({ code: IndexCode.Ed25519_Sig, raw, index });
default:
throw new Error(`Unsupported signature algorithm: ${alg}`);
}
}
export function encodeMatter(raw) {
return decodeUtf8(encoder.encode(raw, "text"));
}
export function encodeMap(data) {
const frames = [];
for (const [key, value] of Object.entries(data)) {
frames.push(encodeTag(key));
switch (typeof value) {
case "string":
frames.push(encodeString(value));
break;
case "number":
frames.push(encodeNumber(value));
break;
case "object": {
if (!Array.isArray(value) && value !== null && !(value instanceof Date)) {
frames.push(encodeMap({ ...value }));
}
else {
throw new Error(`Unsupported object type for key ${key}: ${JSON.stringify(value)}`);
}
break;
}
case "boolean":
frames.push(encodeMatter({
code: value ? MatterCode.Yes : MatterCode.No,
raw: new Uint8Array(0),
}));
break;
default:
throw new Error(`Unsupported value type ${typeof value} for key ${key}`);
}
}
const result = frames.join("");
const header = encodeCounter({ code: CountCode_20.GenericMapGroup, count: Math.floor(result.length / 4) });
return `${header}${frames.join("")}`;
}
export function encodeDate(date) {
if (date.toString() === "Invalid Date") {
throw new Error("Invalid date");
}
// TODO: Better design for date encoding
const YYYY = date.getFullYear();
const MM = padNumber(date.getUTCMonth() + 1, 2);
const dd = padNumber(date.getUTCDate(), 2);
const hh = padNumber(date.getUTCHours(), 2);
const mm = padNumber(date.getUTCMinutes(), 2);
const ss = padNumber(date.getUTCSeconds(), 2);
const ms = padNumber(date.getUTCMilliseconds(), 3);
const raw = decodeBase64Url(`${YYYY}-${MM}-${dd}T${hh}c${mm}c${ss}d${ms}000p00c00`);
return encodeMatter({ code: MatterCode.DateTime, raw });
}
export function encodeTag(tag) {
switch (tag.length) {
case 1:
return `${MatterCode.Tag1}${tag.padStart(2, "_")}`;
case 2:
return `${MatterCode.Tag2}${tag}`;
default:
throw new Error(`Could not determine tag size`);
}
}
export function encodeString(txt) {
if (REGEX_BASE64_CHARACTER.test(txt) && !txt.startsWith("A")) {
const textsize = txt.length % 4;
const padsize = (4 - textsize) % 4;
const leadsize = (3 - textsize) % 3;
const raw = decodeBase64Url("A".repeat(padsize) + txt).slice(leadsize);
switch (leadsize) {
case 0:
return encodeMatter({ code: MatterCode.StrB64_L0, raw });
case 1:
return encodeMatter({ code: MatterCode.StrB64_L1, raw });
case 2:
return encodeMatter({ code: MatterCode.StrB64_L2, raw });
default:
throw new Error(`Could not determine lead size`);
}
}
const raw = encodeUtf8(txt);
const length = raw.byteLength;
const leadSize = length % 3;
switch (leadSize) {
case 0:
return encodeMatter({ code: MatterCode.Bytes_L0, raw });
case 1:
return encodeMatter({ code: MatterCode.Bytes_L1, raw });
case 2:
return encodeMatter({ code: MatterCode.Bytes_L2, raw });
default:
throw new Error(`Could not determine lead size`);
}
}
export function encodeInt(value) {
return `${MatterCode.Short}${encodeBase64Int(value, 4)}`;
}
export function encodeNumber(value) {
const txt = value.toString().replace(".", "p");
const textsize = txt.length % 4;
const padsize = (4 - textsize) % 4;
const leadsize = (3 - textsize) % 3;
const raw = decodeBase64Url("A".repeat(padsize) + txt).slice(leadsize);
switch (leadsize) {
case 0:
return encodeMatter({ code: MatterCode.Decimal_L0, raw });
case 1:
return encodeMatter({ code: MatterCode.Decimal_L1, raw });
case 2:
return encodeMatter({ code: MatterCode.Decimal_L2, raw });
default:
throw new Error(`Could not determine lead size for decimal ${value}`);
}
}
export function encodeSignature(alg, raw) {
switch (alg) {
case "ed25519":
return encodeMatter({ code: MatterCode.Ed25519_Sig, raw });
case "secp256k1":
return encodeMatter({ code: MatterCode.ECDSA_256k1_Sig, raw });
default:
throw new Error(`Unsupported signature algorithm: ${alg}`);
}
}
export function encodeDigest(alg, raw) {
switch (alg) {
case "blake3_256":
return encodeMatter({ code: MatterCode.Blake3_256, raw });
case "blake3_512":
return encodeMatter({ code: MatterCode.Blake3_512, raw });
default:
throw new Error(`Unsupported digest algorithm: ${alg}`);
}
}
export function encodeGenus(genus) {
if (typeof genus.major !== "number" || genus.major < 0 || genus.major > 63) {
throw new Error(`Invalid major version: ${genus.major}`);
}
const minor = genus.minor ?? 0;
if (typeof minor !== "number" || minor < 0) {
throw new Error(`Invalid minor version: ${minor}`);
}
const qb64 = `${CountCode_10.KERIACDCGenusVersion}${encodeBase64Int(genus.major, 1)}${encodeBase64Int(minor, 2)}`;
return qb64;
}
export function encodeCounter(raw) {
return decodeUtf8(encoder.encode(raw, "text"));
}
export function encodeAttachmentsV1(count) {
if (count > 64 ** 2) {
return encodeCounter({ code: CountCode_10.BigAttachmentGroup, count });
}
return encodeCounter({ code: CountCode_10.AttachmentGroup, count });
}
export function encodeAttachmentsV2(count) {
if (count > 64 ** 2) {
return encodeCounter({ code: CountCode_20.BigAttachmentGroup, count });
}
return encodeCounter({ code: CountCode_20.AttachmentGroup, count });
}
export function encodeIndexer(frame) {
return decodeUtf8(indexer.encode(frame, "text"));
}
export function encodeVersionString(init) {
const protocol = init.protocol ?? "KERI";
const major = init.major ?? 1;
const minor = init.minor ?? 0;
const format = init.kind ?? "JSON";
if (format !== "JSON") {
throw new Error("Only JSON format is supported for now");
}
if (!REGEX_VERSION_STRING_PROTOCOL.test(protocol)) {
throw new Error("Protocol must be 4 characters");
}
if (!REGEX_VERSION_STRING_KIND.test(format)) {
throw new Error("Format must be 4 characters");
}
if (init.legacy) {
const version = `${encodeHexInt(major, 1)}${encodeHexInt(minor, 1)}`;
const size = encodeHexInt(init.size ?? 0, 6);
return `${protocol}${version}${format}${size}_`;
}
const version = `${encodeBase64Int(major, 1)}${encodeBase64Int(minor, 2)}`;
const size = encodeBase64Int(init.size ?? 0, 4);
return `${protocol}${version}${format}${size}.`;
}
export function encodeMessage(body, init = {}) {
const str = encodeUtf8(JSON.stringify({
v: encodeVersionString(init),
...body,
}));
return JSON.stringify({
v: encodeVersionString({ ...init, size: str.byteLength }),
...body,
});
}
export function decodeGenus(input) {
const genus = input.slice(2, 5);
const major = decodeBase64Int(input.slice(5, 6));
const minor = decodeBase64Int(input.slice(6, 8));
return { genus, major, minor };
}
export function decodeMatter(input) {
return encoder.decode(input);
}
export function decodeCounter(input) {
return encoder.decode(input);
}
export function decodeVersionString(input) {
if (typeof input !== "string") {
input = decodeUtf8(input.slice(0, 24));
}
const match = input.match(REGEX_VERSION_JSON);
if (!match) {
throw new Error(`Unable to extract "v" field from ${input}`);
}
const value = match[1];
if (value.endsWith(".") && value.length === 16) {
const protocol = value.slice(0, 4);
const major = decodeBase64Int(value.slice(4, 5));
const minor = decodeBase64Int(value.slice(5, 7));
const kind = value.slice(7, 11);
const size = decodeBase64Int(value.slice(12, 15));
return {
protocol,
major,
minor,
legacy: false,
kind,
size,
};
}
if (value.endsWith("_") && value.length === 17) {
const protocol = value.slice(0, 4);
const major = parseInt(value.slice(4, 5), 16);
const minor = parseInt(value.slice(5, 6), 16);
const format = value.slice(6, 10);
const size = parseInt(value.slice(10, 16), 16);
return {
protocol,
major,
minor,
kind: format,
size,
legacy: true,
};
}
throw new Error(`Invalid version string ${value}`);
}
export const encoding = {
encodeIndexedSignature,
encodeMap,
encodeMatter,
encodeDate,
encodeString,
encodeNumber,
encodeSignature,
encodeDigest,
encodeGenus,
encodeCounter,
encodeAttachmentsV1,
encodeAttachmentsV2,
encodeIndexer,
encodeVersionString,
encodeMessage,
decodeGenus,
decodeMatter,
decodeCounter,
decodeVersionString,
};