@istvan.xyz/phc-format
Version:
An implementation of the PHC format.
236 lines (231 loc) • 7.38 kB
JavaScript
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, {
deserialize: () => deserialize,
serialize: () => serialize
});
module.exports = __toCommonJS(index_exports);
// src/base64.ts
function bytesToBinaryString(bytes) {
let result = "";
for (const b of bytes) {
result += String.fromCharCode(b);
}
return result;
}
function binaryStringToBytes(binary) {
const out = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
out[i] = binary.charCodeAt(i);
}
return out;
}
function bytesToBase64(bytes) {
let b64;
const g = globalThis;
if (typeof g.btoa === "function") {
b64 = g.btoa(bytesToBinaryString(bytes));
} else if (typeof g.Buffer !== "undefined") {
const BufferLike = g.Buffer;
const nodeBuf = BufferLike.from(bytes);
b64 = nodeBuf.toString("base64");
} else {
throw new Error("No base64 encoder available in this environment");
}
return b64.replace(/=+$/u, "");
}
function base64ToBytes(b64) {
const padLength = (4 - b64.length % 4) % 4;
const padded = b64 + "=".repeat(padLength);
const g = globalThis;
if (typeof g.atob === "function") {
const binary = g.atob(padded);
return binaryStringToBytes(binary);
}
if (typeof g.Buffer !== "undefined") {
const BufferLike = g.Buffer;
const buf = BufferLike.from(padded, "base64");
return new Uint8Array(buf);
}
throw new Error("No base64 decoder available in this environment");
}
function asRuntimeBytes(bytes) {
const g = globalThis;
if (typeof g.Buffer !== "undefined") {
const BufferLike = g.Buffer;
return BufferLike.from(bytes);
}
return bytes;
}
// src/patterns.ts
var idRegex = /^[a-z0-9-]{1,32}$/;
var nameRegex = /^[a-z0-9-]{1,32}$/;
var valueRegex = /^[a-zA-Z0-9/+.-]+$/;
// src/serialize.ts
function objectToKeyValueString(object) {
return Object.entries(object).map(([key, value]) => [key, value].join("=")).join(",");
}
function serialize(opts) {
const fields = [""];
if (!idRegex.test(opts.id)) {
throw new TypeError(`id must satisfy ${idRegex}`);
}
fields.push(opts.id);
if (typeof opts.version !== "undefined") {
if (opts.version < 0 || !Number.isInteger(opts.version)) {
throw new TypeError("version must be a positive integer number");
}
fields.push(`v=${opts.version}`);
}
const { params } = opts;
if (typeof params !== "undefined") {
const safeParams = { ...params };
const pk = Object.keys(safeParams);
if (!pk.every((p) => nameRegex.test(p))) {
throw new TypeError(`params names must satisfy ${nameRegex}`);
}
pk.forEach((k) => {
if (typeof safeParams[k] === "number") {
safeParams[k] = String(safeParams[k]);
} else if (safeParams[k] instanceof Uint8Array) {
safeParams[k] = bytesToBase64(safeParams[k]);
}
});
const pv = Object.values(safeParams);
if (!pv.every((v) => typeof v === "string")) {
throw new TypeError("params values must be strings");
}
if (!pv.every((v) => valueRegex.test(v))) {
throw new TypeError(`params values must satisfy ${valueRegex}`);
}
const strpar = objectToKeyValueString(safeParams);
fields.push(strpar);
}
if (typeof opts.salt !== "undefined") {
fields.push(bytesToBase64(opts.salt));
if (typeof opts.hash !== "undefined") {
if (!(opts.hash instanceof Uint8Array)) {
throw new TypeError("hash must be a Uint8Array");
}
fields.push(bytesToBase64(opts.hash));
}
}
const phcString = fields.join("$");
return phcString;
}
// src/deserialize.ts
var b64Regex = /^([a-zA-Z0-9/+.-]+|)$/;
var decimalRegex = /^((-)?[1-9]\d*|0)$/;
var versionRegex = /^v=(\d+)$/;
var keyValueStringToObject = (string) => {
const object = {};
string.split(",").forEach((ps) => {
const tokens = ps.split("=");
if (tokens.length < 2) {
throw new TypeError("params must be in the format name=value");
}
object[tokens.shift()] = tokens.join("=");
});
return object;
};
function deserialize(phcString) {
if (phcString === "") {
throw new TypeError("phcString must be a non-empty string");
}
if (phcString[0] !== "$") {
throw new TypeError("phcString must contain a $ as first char");
}
const fields = phcString.split("$");
fields.shift();
let maxFields = 5;
if (!versionRegex.test(fields[1])) maxFields--;
if (fields.length > maxFields) {
throw new TypeError(`phcString contains too many fields: ${fields.length}/${maxFields}`);
}
const id = fields.shift();
if (!id) {
throw new Error("id cannot be undefined at this point.");
}
if (!idRegex.test(id)) {
throw new TypeError(`id must satisfy ${idRegex}`);
}
let version;
if (versionRegex.test(fields[0])) {
const versionString = fields.shift();
if (!versionString) {
throw new Error("paramString cannot be undefined at this point.");
}
version = parseInt(versionString.match(versionRegex)[1], 10);
}
let hash;
let salt;
if (b64Regex.test(fields[fields.length - 1])) {
if (fields.length > 1 && b64Regex.test(fields[fields.length - 2])) {
hash = asRuntimeBytes(base64ToBytes(fields.pop()));
salt = asRuntimeBytes(base64ToBytes(fields.pop()));
} else {
salt = asRuntimeBytes(base64ToBytes(fields.pop()));
}
}
let params;
if (fields.length > 0) {
const paramString = fields.pop();
if (!paramString) {
throw new Error("paramString cannot be undefined at this point.");
}
const currentParams = keyValueStringToObject(paramString);
if (!Object.keys(currentParams).every((p) => nameRegex.test(p))) {
throw new TypeError(`params names must satisfy ${nameRegex}`);
}
const pv = Object.values(currentParams);
if (!pv.every((v) => valueRegex.test(v))) {
throw new TypeError(`params values must satisfy ${valueRegex}`);
}
const pk = Object.keys(currentParams);
pk.forEach((k) => {
currentParams[k] = decimalRegex.test(currentParams[k]) ? parseInt(currentParams[k], 10) : currentParams[k];
});
params = currentParams;
}
if (fields.length > 0) {
throw new TypeError(`phcString contains unrecognized fields: ${fields}`);
}
const result = { id };
if (version) {
result.version = version;
}
if (params) {
result.params = params;
}
if (salt) {
result.salt = salt;
}
if (hash) {
result.hash = hash;
}
return result;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
deserialize,
serialize
});
//# sourceMappingURL=index.js.map
;