zilla-util 1.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE.txt +202 -0
  2. package/lib/esm/array.d.ts +30 -0
  3. package/lib/esm/array.js +111 -0
  4. package/lib/esm/array.js.map +1 -0
  5. package/lib/esm/base64.d.ts +2 -0
  6. package/lib/esm/base64.js +51 -0
  7. package/lib/esm/base64.js.map +1 -0
  8. package/lib/esm/crypto.d.ts +16 -0
  9. package/lib/esm/crypto.js +182 -0
  10. package/lib/esm/crypto.js.map +1 -0
  11. package/lib/esm/date.d.ts +2 -0
  12. package/lib/esm/date.js +47 -0
  13. package/lib/esm/date.js.map +1 -0
  14. package/lib/esm/deep.d.ts +24 -0
  15. package/lib/esm/deep.js +318 -0
  16. package/lib/esm/deep.js.map +1 -0
  17. package/lib/esm/error.d.ts +1 -0
  18. package/lib/esm/error.js +9 -0
  19. package/lib/esm/error.js.map +1 -0
  20. package/lib/esm/hash.d.ts +4 -0
  21. package/lib/esm/hash.js +18 -0
  22. package/lib/esm/hash.js.map +1 -0
  23. package/lib/esm/index.d.ts +17 -0
  24. package/lib/esm/index.js +18 -0
  25. package/lib/esm/index.js.map +1 -0
  26. package/lib/esm/logger.d.ts +18 -0
  27. package/lib/esm/logger.js +18 -0
  28. package/lib/esm/logger.js.map +1 -0
  29. package/lib/esm/lru.d.ts +25 -0
  30. package/lib/esm/lru.js +137 -0
  31. package/lib/esm/lru.js.map +1 -0
  32. package/lib/esm/msg.d.ts +29 -0
  33. package/lib/esm/msg.js +7 -0
  34. package/lib/esm/msg.js.map +1 -0
  35. package/lib/esm/object.d.ts +8 -0
  36. package/lib/esm/object.js +31 -0
  37. package/lib/esm/object.js.map +1 -0
  38. package/lib/esm/package.d.ts +2 -0
  39. package/lib/esm/package.js +38 -0
  40. package/lib/esm/package.js.map +1 -0
  41. package/lib/esm/regex.d.ts +1 -0
  42. package/lib/esm/regex.js +22 -0
  43. package/lib/esm/regex.js.map +1 -0
  44. package/lib/esm/retry.d.ts +13 -0
  45. package/lib/esm/retry.js +57 -0
  46. package/lib/esm/retry.js.map +1 -0
  47. package/lib/esm/sha256.d.ts +50 -0
  48. package/lib/esm/sha256.js +181 -0
  49. package/lib/esm/sha256.js.map +1 -0
  50. package/lib/esm/string.d.ts +106 -0
  51. package/lib/esm/string.js +257 -0
  52. package/lib/esm/string.js.map +1 -0
  53. package/lib/esm/subst.d.ts +6 -0
  54. package/lib/esm/subst.js +37 -0
  55. package/lib/esm/subst.js.map +1 -0
  56. package/lib/esm/time.d.ts +27 -0
  57. package/lib/esm/time.js +84 -0
  58. package/lib/esm/time.js.map +1 -0
  59. package/lib/esm/url.d.ts +9 -0
  60. package/lib/esm/url.js +80 -0
  61. package/lib/esm/url.js.map +1 -0
  62. package/package.json +72 -0
  63. package/src/array.ts +134 -0
  64. package/src/base64.ts +53 -0
  65. package/src/crypto.ts +281 -0
  66. package/src/date.ts +48 -0
  67. package/src/deep.ts +348 -0
  68. package/src/error.ts +7 -0
  69. package/src/hash.ts +20 -0
  70. package/src/index.ts +17 -0
  71. package/src/logger.ts +35 -0
  72. package/src/lru.ts +187 -0
  73. package/src/msg.ts +33 -0
  74. package/src/object.ts +43 -0
  75. package/src/package.ts +39 -0
  76. package/src/retry.ts +71 -0
  77. package/src/string.ts +338 -0
  78. package/src/subst.ts +41 -0
  79. package/src/time.ts +105 -0
  80. package/src/url.ts +92 -0
package/lib/esm/url.js ADDED
@@ -0,0 +1,80 @@
1
+ const tracking = new Set([
2
+ "ref",
3
+ "ref_src",
4
+ "gclsrc",
5
+ "fbp",
6
+ "fbc",
7
+ "irclickid",
8
+ "hsCtaTracking",
9
+ "pi_opt_in",
10
+ "visitor_id",
11
+ "mc_cid",
12
+ "mc_eid",
13
+ ]);
14
+ const trackingRegexes = [
15
+ /^[a-z_]+clid$/,
16
+ /^[a-z_]+clkid$/,
17
+ /^[a-z_]+click_?id$/,
18
+ /^utm_[a-z_]+$/,
19
+ /^__utm(?:a|b|c|z|v)$/,
20
+ /^_hs(?:enc|mi)$/,
21
+ /^mkt_[a-z_]+$/,
22
+ /^pk_[a-z]+$/,
23
+ ];
24
+ export const normalizeUrl = (input) => {
25
+ try {
26
+ return new URL(input).toString();
27
+ }
28
+ catch {
29
+ return input;
30
+ }
31
+ };
32
+ export const canonicalizeUrl = async (url) => {
33
+ const original = url.trim();
34
+ const u = new URL(original);
35
+ u.protocol = u.protocol.toLowerCase();
36
+ u.hostname = u.hostname.toLowerCase();
37
+ if ((u.protocol === "http:" && u.port === "80") || (u.protocol === "https:" && u.port === "443"))
38
+ u.port = "";
39
+ u.pathname = u.pathname.replace(/\/{2,}/g, "/").replace(/%7E/gi, "~");
40
+ const kept = Array.from(u.searchParams.entries()).filter(([k]) => !tracking.has(k.toLowerCase()) && !trackingRegexes.find((r) => r.test(k.toLowerCase())));
41
+ kept.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
42
+ u.search = kept.length ? `?${new URLSearchParams(kept).toString()}` : "";
43
+ const normalized = u.toString();
44
+ const redirectChain = [];
45
+ const maxRedirects = 5;
46
+ let landing = normalized;
47
+ try {
48
+ let cur = normalized;
49
+ for (let n = 0; n < maxRedirects; n += 1) {
50
+ const r = await fetch(cur, { method: "HEAD", redirect: "manual" });
51
+ const loc = r.status >= 300 && r.status < 400 ? r.headers.get("location") : null;
52
+ if (!loc)
53
+ break;
54
+ cur = new URL(loc, cur).toString();
55
+ redirectChain.push(cur);
56
+ }
57
+ if (redirectChain.length)
58
+ landing = redirectChain[redirectChain.length - 1];
59
+ }
60
+ catch {
61
+ landing = normalized;
62
+ }
63
+ let canonical = landing;
64
+ try {
65
+ const res = await fetch(landing, { method: "GET", redirect: "follow" });
66
+ if (res.headers.get("content-type")?.includes("text/html")) {
67
+ const html = await res.text();
68
+ const found = html.match(/<link[^>]*rel=["']canonical["'][^>]*href=["']([^"']+)["']/i)?.[1] ??
69
+ html.match(/<meta[^>]*property=["']og:url["'][^>]*content=["']([^"']+)["']/i)?.[1] ??
70
+ html.match(/<meta[^>]*property=["']twitter:url["'][^>]*content=["']([^"']+)["']/i)?.[1];
71
+ if (found)
72
+ canonical = new URL(found, landing).toString();
73
+ }
74
+ }
75
+ catch {
76
+ /* ignore */
77
+ }
78
+ return { original, normalized, redirectChain, landing, canonical };
79
+ };
80
+ //# sourceMappingURL=url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url.js","sourceRoot":"","sources":["../../src/url.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "zilla-util",
3
+ "version": "1.2.27",
4
+ "type": "module",
5
+ "description": "Simple zero-dependency utility library",
6
+ "keywords": [
7
+ "cobbzilla",
8
+ "util",
9
+ "utils",
10
+ "utility",
11
+ "typescript"
12
+ ],
13
+ "homepage": "https://github.com/cobbzilla/zilla-util",
14
+ "author": "Jonathan Cobb <bqppl0m2@duck.com> (https://github.com/cobbzilla)",
15
+ "funding": {
16
+ "type": "patreon",
17
+ "url": "https://www.patreon.com/cobbzilla"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/cobbzilla/zilla-util.git"
22
+ },
23
+ "license": "Apache-2.0",
24
+ "scripts": {
25
+ "tsc": "tsc",
26
+ "build": "tsc",
27
+ "lint": "npx eslint src",
28
+ "lint:fix": "npm run lint -- --fix",
29
+ "prettier": "npx prettier src --check",
30
+ "prettier:fix": "npm run prettier -- --write",
31
+ "test": "if [[ -z \"$SKIP_TESTS\" ]] ; then mocha --timeout 5000 ./test/*.spec.ts; else echo 'Skipping tests due to SKIP_TESTS env variable'; fi",
32
+ "release": "pnpm tsc && pnpm lint && pnpm test"
33
+ },
34
+ "main": "./lib/esm/index.js",
35
+ "module": "./lib/esm/index.js",
36
+ "exports": {
37
+ ".": "./lib/esm/index.js"
38
+ },
39
+ "files": [
40
+ "lib/",
41
+ "src/**/*.ts"
42
+ ],
43
+ "devDependencies": {
44
+ "@eslint/eslintrc": "^3.2.0",
45
+ "@eslint/js": "^9.20.0",
46
+ "@types/chai": "^4.3.5",
47
+ "@types/mocha": "^10.0.1",
48
+ "@types/node": "^22.15.29",
49
+ "@typescript-eslint/eslint-plugin": "^8.24.0",
50
+ "@typescript-eslint/parser": "^8.24.0",
51
+ "chai": "^4.3.6",
52
+ "eslint": "^9.20.0",
53
+ "eslint-config-prettier": "^10.0.1",
54
+ "eslint-plugin-import": "^2.31.0",
55
+ "globals": "^15.14.0",
56
+ "h3": "^1.15.3",
57
+ "mocha": "^10.2.0",
58
+ "prettier": "^2.8.8",
59
+ "shasum": "^1.0.2",
60
+ "tslint-config-prettier": "^1.18.0",
61
+ "tsx": "^4.15.4",
62
+ "typescript": "^5.7.3"
63
+ },
64
+ "pnpm": {
65
+ "onlyBuiltDependencies": [
66
+ "esbuild"
67
+ ]
68
+ },
69
+ "dependencies": {
70
+ "grapheme-splitter": "^1.0.4"
71
+ }
72
+ }
package/src/array.ts ADDED
@@ -0,0 +1,134 @@
1
+ export const setsEqual = <T>(a: T[], b: T[]): boolean => {
2
+ if (a.length !== b.length) return false;
3
+ return a.every((val) => b.includes(val)) && b.every((val) => a.includes(val));
4
+ };
5
+
6
+ export const containsAll = <T>(source: T[], required: T[]): boolean =>
7
+ required.every((item: T): boolean => source.includes(item));
8
+
9
+ const _cartesianProduct = <T>(arr: T[][], index: number, sentence: T[], result: T[][]) => {
10
+ for (let i = 0; i < arr[index].length; i++) {
11
+ const newSentence = sentence.slice(); // To not mutate the original array
12
+ newSentence.push(arr[index][i]);
13
+ if (index < arr.length - 1) {
14
+ _cartesianProduct(arr, index + 1, newSentence, result);
15
+ } else {
16
+ result.push(newSentence);
17
+ }
18
+ }
19
+ };
20
+
21
+ export const cartesianProduct = <T>(arr: T[][]): T[][] => {
22
+ const result: T[][] = [];
23
+ _cartesianProduct(arr, 0, [], result);
24
+ return result;
25
+ };
26
+
27
+ export const isAnyTrue = <T extends boolean[]>(arr: T): boolean => arr.some(Boolean);
28
+
29
+ export const insertAfterElement = <T>(sourceArray: T[], targetArray: T[], element: T): T[] => {
30
+ const index: number = targetArray.findIndex((item) => item === element);
31
+
32
+ if (index !== -1) {
33
+ targetArray.splice(index + 1, 0, ...sourceArray);
34
+ } else {
35
+ targetArray.push(...sourceArray);
36
+ }
37
+
38
+ return targetArray;
39
+ };
40
+
41
+ export const asyncFilter = async <T>(arr: T[], predicate: (item: T) => Promise<boolean>): Promise<T[]> => {
42
+ const results = await Promise.all(arr.map(predicate));
43
+ return arr.filter((_v, index) => results[index]);
44
+ };
45
+
46
+ export const shuffleArray = <T>(array: T[]): T[] => {
47
+ const result = [...array];
48
+ for (let i = result.length - 1, j: number; i > 0; i--) {
49
+ j = Math.floor(Math.random() * (i + 1));
50
+ [result[i], result[j]] = [result[j], result[i]];
51
+ }
52
+ return result;
53
+ };
54
+
55
+ export const shuffleNumbers = (n: number): number[] => shuffleArray([...Array(n).keys()]);
56
+
57
+ export const deduplicateArrayByName = (entries: Record<string, unknown>[]): Record<string, unknown>[] =>
58
+ deduplicateArray(entries, "name");
59
+
60
+ export const deduplicateArray = (entries: Record<string, unknown>[], field: string): Record<string, unknown>[] => [
61
+ ...new Map(entries.map((entry) => [entry[field], entry])).values(),
62
+ ];
63
+
64
+ export class SortedSet<T> {
65
+ private items = new Set<T>();
66
+ private comparator: (a: T, b: T) => number;
67
+
68
+ constructor(comparator: (a: T, b: T) => number) {
69
+ this.comparator = comparator;
70
+ }
71
+
72
+ add(value: T): void {
73
+ this.items.add(value);
74
+ }
75
+
76
+ delete(value: T): boolean {
77
+ return this.items.delete(value);
78
+ }
79
+
80
+ has(value: T): boolean {
81
+ return this.items.has(value);
82
+ }
83
+
84
+ size(): number {
85
+ return this.items.size;
86
+ }
87
+
88
+ toArray(): T[] {
89
+ return Array.from(this.items).sort(this.comparator);
90
+ }
91
+ }
92
+
93
+ export class SortedIdSet<T, ID> extends SortedSet<T> {
94
+ private ids = new Set<ID>();
95
+ private id: (a: T) => ID;
96
+
97
+ constructor(comparator: (a: T, b: T) => number, id: (a: T) => ID) {
98
+ super(comparator);
99
+ this.id = id;
100
+ }
101
+
102
+ add(value: T): void {
103
+ if (!this.ids.has(this.id(value))) {
104
+ super.add(value);
105
+ this.ids.add(this.id(value));
106
+ }
107
+ }
108
+
109
+ delete(value: T): boolean {
110
+ const deleted = super.delete(value);
111
+ if (deleted) {
112
+ this.ids.delete(this.id(value));
113
+ }
114
+ return deleted;
115
+ }
116
+
117
+ has(value: T): boolean {
118
+ return this.ids.has(this.id(value));
119
+ }
120
+
121
+ size(): number {
122
+ return this.ids.size;
123
+ }
124
+ }
125
+
126
+ export const firstByProperty = <T extends Record<string, unknown>>(items: T[], prop: keyof T): T[] => {
127
+ const seen: Set<unknown> = new Set();
128
+ return items.filter((item) => {
129
+ const val = item[prop];
130
+ if (seen.has(val)) return false;
131
+ seen.add(val);
132
+ return true;
133
+ });
134
+ };
package/src/base64.ts ADDED
@@ -0,0 +1,53 @@
1
+ export const base64Encode = (str: string): string => {
2
+ let utf8str = "";
3
+ for (let i = 0; i < str.length; i++) {
4
+ const code = str.charCodeAt(i);
5
+ if (code < 0x80) {
6
+ utf8str += String.fromCharCode(code);
7
+ } else if (code < 0x800) {
8
+ utf8str += String.fromCharCode(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
9
+ } else if (code < 0xd800 || code >= 0xe000) {
10
+ utf8str += String.fromCharCode(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
11
+ } else {
12
+ i++;
13
+ const surrogatePair = 0x10000 + ((code & 0x3ff) << 10) + (str.charCodeAt(i) & 0x3ff);
14
+ utf8str += String.fromCharCode(
15
+ 0xf0 | (surrogatePair >> 18),
16
+ 0x80 | ((surrogatePair >> 12) & 0x3f),
17
+ 0x80 | ((surrogatePair >> 6) & 0x3f),
18
+ 0x80 | (surrogatePair & 0x3f)
19
+ );
20
+ }
21
+ }
22
+ const base64 = btoa(utf8str);
23
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
24
+ };
25
+
26
+ export const base64Decode = (base64: string): string => {
27
+ const base64Str = base64.replace(/-/g, "+").replace(/_/g, "/");
28
+ const paddedBase64Str = base64Str.padEnd(base64Str.length + ((4 - (base64Str.length % 4)) % 4), "=");
29
+ const utf8Str = atob(paddedBase64Str);
30
+ let result = "";
31
+ let i = 0;
32
+ while (i < utf8Str.length) {
33
+ const byte1 = utf8Str.charCodeAt(i++);
34
+ if (byte1 < 0x80) {
35
+ result += String.fromCharCode(byte1);
36
+ continue;
37
+ }
38
+ const byte2 = utf8Str.charCodeAt(i++) & 0x3f;
39
+ if (byte1 < 0xe0) {
40
+ result += String.fromCharCode(((byte1 & 0x1f) << 6) | byte2);
41
+ continue;
42
+ }
43
+ const byte3 = utf8Str.charCodeAt(i++) & 0x3f;
44
+ if (byte1 < 0xf0) {
45
+ result += String.fromCharCode(((byte1 & 0x0f) << 12) | (byte2 << 6) | byte3);
46
+ continue;
47
+ }
48
+ const byte4 = utf8Str.charCodeAt(i++) & 0x3f;
49
+ const codePoint = (((byte1 & 0x07) << 18) | (byte2 << 12) | (byte3 << 6) | byte4) - 0x10000;
50
+ result += String.fromCharCode(0xd800 + (codePoint >> 10), 0xdc00 + (codePoint & 0x3ff));
51
+ }
52
+ return result;
53
+ };
package/src/crypto.ts ADDED
@@ -0,0 +1,281 @@
1
+ // crypto-util.ts (browser-only, no Node APIs)
2
+ import { LRUCache } from "zilla-util";
3
+
4
+ export enum CryptoParam {
5
+ RSA_MODULUS_LENGTH = "RSA_MODULUS_LENGTH",
6
+ RSA_PUBLIC_EXPONENT = "RSA_PUBLIC_EXPONENT",
7
+ OAEP_HASH = "OAEP_HASH",
8
+ }
9
+
10
+ export type CryptoKeyPair = {
11
+ publicKey: string; // base64-encoded SPKI
12
+ privateKey: string; // base64-encoded PKCS#8
13
+ };
14
+
15
+ type SafeHash = "sha256" | "sha384" | "sha512";
16
+ type WebHash = "SHA-256" | "SHA-384" | "SHA-512";
17
+ type WebCryptoKeyPair = globalThis.CryptoKeyPair;
18
+
19
+ const DEFAULTS: Readonly<{
20
+ modulusLength: number;
21
+ publicExponent: number;
22
+ oaepHash: SafeHash;
23
+ }> = {
24
+ modulusLength: 3072,
25
+ publicExponent: 65537,
26
+ oaepHash: "sha256",
27
+ };
28
+
29
+ const ALLOWED_MODULUS: ReadonlySet<number> = new Set([2048, 3072, 4096]);
30
+ const ALLOWED_EXPONENTS: ReadonlySet<number> = new Set([65537]);
31
+ const ALLOWED_HASHES: ReadonlySet<SafeHash> = new Set(["sha256", "sha384", "sha512"]);
32
+
33
+ const SIX_HOURS_MS: number = 6 * 60 * 60 * 1000;
34
+
35
+ const subtle: SubtleCrypto = (() => {
36
+ const c: Crypto | undefined = globalThis.crypto as Crypto | undefined;
37
+ if (!c?.subtle) {
38
+ throw new Error("WebCrypto SubtleCrypto is not available in this environment");
39
+ }
40
+ return c.subtle;
41
+ })();
42
+
43
+ const toB64 = (input: ArrayBuffer | ArrayBufferView): string => {
44
+ const bytes: Uint8Array =
45
+ input instanceof ArrayBuffer
46
+ ? new Uint8Array(input)
47
+ : new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
48
+ let binary: string = "";
49
+ for (let i = 0; i < bytes.length; i++) {
50
+ binary += String.fromCharCode(bytes[i]);
51
+ }
52
+ return btoa(binary);
53
+ };
54
+
55
+ const fromB64 = (b64: string): Uint8Array => {
56
+ const binary: string = atob(b64);
57
+ const len: number = binary.length;
58
+ const bytes: Uint8Array = new Uint8Array(len);
59
+ for (let i = 0; i < len; i++) {
60
+ bytes[i] = binary.charCodeAt(i);
61
+ }
62
+ return bytes;
63
+ };
64
+
65
+ const toWebHash = (h: SafeHash): WebHash => (h === "sha256" ? "SHA-256" : h === "sha384" ? "SHA-384" : "SHA-512");
66
+
67
+ // Add these utilities (kept local to signature ops)
68
+ const cacheKeySig = (hash: WebHash, keyB64: string): string => `sig|${hash}|${keyB64}`;
69
+
70
+ const importPublicKeyForVerify = async (publicKeyB64: string, webHash: WebHash): Promise<CryptoKey> => {
71
+ const k: string = cacheKeySig(webHash, publicKeyB64);
72
+ const cached: CryptoKey | undefined = pkCache().get(k);
73
+ if (cached !== undefined) return cached;
74
+
75
+ const key: CryptoKey = await subtle.importKey(
76
+ "spki",
77
+ fromB64(publicKeyB64),
78
+ { name: "RSASSA-PKCS1-v1_5", hash: webHash },
79
+ false,
80
+ ["verify"]
81
+ );
82
+ pkCache().set(k, key);
83
+ return key;
84
+ };
85
+
86
+ const importPrivateKeyForSign = async (privateKeyB64: string, webHash: WebHash): Promise<CryptoKey> => {
87
+ const k: string = cacheKeySig(webHash, privateKeyB64);
88
+ const cached: CryptoKey | undefined = pvCache().get(k);
89
+ if (cached !== undefined) return cached;
90
+
91
+ const key: CryptoKey = await subtle.importKey(
92
+ "pkcs8",
93
+ fromB64(privateKeyB64),
94
+ { name: "RSASSA-PKCS1-v1_5", hash: webHash },
95
+ false,
96
+ ["sign"]
97
+ );
98
+ pvCache().set(k, key);
99
+ return key;
100
+ };
101
+
102
+ const resolveParams = (
103
+ parameters?: Partial<Record<CryptoParam, string>>
104
+ ): {
105
+ modulusLength: number;
106
+ publicExponent: number;
107
+ oaepHash: SafeHash;
108
+ webHash: WebHash;
109
+ } => {
110
+ const modulusLength: number =
111
+ parameters?.[CryptoParam.RSA_MODULUS_LENGTH] !== undefined
112
+ ? Number.parseInt(parameters[CryptoParam.RSA_MODULUS_LENGTH], 10)
113
+ : DEFAULTS.modulusLength;
114
+
115
+ const publicExponent: number =
116
+ parameters?.[CryptoParam.RSA_PUBLIC_EXPONENT] !== undefined
117
+ ? Number.parseInt(parameters[CryptoParam.RSA_PUBLIC_EXPONENT], 10)
118
+ : DEFAULTS.publicExponent;
119
+
120
+ const oaepHashStr: string | undefined = parameters?.[CryptoParam.OAEP_HASH];
121
+ const oaepHash: SafeHash = (oaepHashStr?.toLowerCase() as SafeHash | undefined) ?? DEFAULTS.oaepHash;
122
+
123
+ if (!ALLOWED_MODULUS.has(modulusLength)) {
124
+ throw new Error(`Invalid RSA modulus length. Allowed: ${Array.from(ALLOWED_MODULUS).join(", ")}`);
125
+ }
126
+ if (!ALLOWED_EXPONENTS.has(publicExponent)) {
127
+ throw new Error(`Invalid RSA public exponent. Allowed: ${Array.from(ALLOWED_EXPONENTS).join(", ")}`);
128
+ }
129
+ if (!ALLOWED_HASHES.has(oaepHash)) {
130
+ throw new Error(`Invalid OAEP hash. Allowed: ${Array.from(ALLOWED_HASHES).join(", ")}`);
131
+ }
132
+
133
+ return { modulusLength, publicExponent, oaepHash, webHash: toWebHash(oaepHash) };
134
+ };
135
+
136
+ // LRU caches for imported CryptoKey objects (avoid re-importing keys)
137
+ let publicKeyCache: LRUCache<string, CryptoKey>;
138
+ const pkCache = () => {
139
+ if (!publicKeyCache) {
140
+ publicKeyCache = new LRUCache<string, CryptoKey>({
141
+ maxSize: 2048,
142
+ maxAge: SIX_HOURS_MS,
143
+ touchOnGet: true,
144
+ });
145
+ }
146
+ return publicKeyCache;
147
+ };
148
+
149
+ let privateKeyCache: LRUCache<string, CryptoKey>;
150
+ const pvCache = () => {
151
+ if (!privateKeyCache) {
152
+ privateKeyCache = new LRUCache<string, CryptoKey>({
153
+ maxSize: 2048,
154
+ maxAge: SIX_HOURS_MS,
155
+ touchOnGet: true,
156
+ });
157
+ }
158
+ return privateKeyCache;
159
+ };
160
+
161
+ const cacheKey = (hash: WebHash, keyB64: string): string => `${hash}|${keyB64}`;
162
+
163
+ const importPublicKey = async (publicKeyB64: string, webHash: WebHash): Promise<CryptoKey> => {
164
+ const k: string = cacheKey(webHash, publicKeyB64);
165
+ const cached: CryptoKey | undefined = pkCache().get(k);
166
+ if (cached !== undefined) return cached;
167
+
168
+ const key: CryptoKey = await subtle.importKey(
169
+ "spki",
170
+ fromB64(publicKeyB64), // BufferSource
171
+ { name: "RSA-OAEP", hash: webHash },
172
+ false,
173
+ ["encrypt"]
174
+ );
175
+ pkCache().set(k, key);
176
+ return key;
177
+ };
178
+
179
+ const importPrivateKey = async (privateKeyB64: string, webHash: WebHash): Promise<CryptoKey> => {
180
+ const k: string = cacheKey(webHash, privateKeyB64);
181
+ const cached: CryptoKey | undefined = pvCache().get(k);
182
+ if (cached !== undefined) return cached;
183
+
184
+ const key: CryptoKey = await subtle.importKey(
185
+ "pkcs8",
186
+ fromB64(privateKeyB64), // BufferSource
187
+ { name: "RSA-OAEP", hash: webHash },
188
+ false,
189
+ ["decrypt"]
190
+ );
191
+ pvCache().set(k, key);
192
+ return key;
193
+ };
194
+
195
+ const resolvePublicKeyB64 = (key: string | CryptoKeyPair): string => (typeof key === "string" ? key : key.publicKey);
196
+ const resolvePrivateKeyB64 = (key: string | CryptoKeyPair): string => (typeof key === "string" ? key : key.privateKey);
197
+
198
+ export const CryptoUtil = {
199
+ generateKeyPair: async (parameters?: Partial<Record<CryptoParam, string>>): Promise<CryptoKeyPair> => {
200
+ const { modulusLength, publicExponent, webHash } = resolveParams(parameters);
201
+
202
+ if (publicExponent !== 65537) {
203
+ throw new Error("Only RSA public exponent 65537 is supported");
204
+ }
205
+
206
+ const expBytes: Uint8Array = new Uint8Array([0x01, 0x00, 0x01]); // 65537
207
+
208
+ const webPair: WebCryptoKeyPair = await subtle.generateKey(
209
+ { name: "RSA-OAEP", modulusLength, publicExponent: expBytes, hash: webHash },
210
+ true, // extractable (so we can export to SPKI/PKCS8)
211
+ ["encrypt", "decrypt"]
212
+ );
213
+
214
+ const spki: ArrayBuffer = await subtle.exportKey("spki", webPair.publicKey);
215
+ const pkcs8: ArrayBuffer = await subtle.exportKey("pkcs8", webPair.privateKey);
216
+
217
+ const result: CryptoKeyPair = {
218
+ publicKey: toB64(spki),
219
+ privateKey: toB64(pkcs8),
220
+ };
221
+ return result;
222
+ },
223
+
224
+ encrypt: async (
225
+ publicKey: string | CryptoKeyPair,
226
+ plaintext: string,
227
+ parameters?: Partial<Record<CryptoParam, string>>
228
+ ): Promise<string> => {
229
+ const { webHash } = resolveParams(parameters);
230
+ const pubB64: string = resolvePublicKeyB64(publicKey);
231
+ const keyObj: CryptoKey = await importPublicKey(pubB64, webHash);
232
+
233
+ const data: Uint8Array = new TextEncoder().encode(plaintext);
234
+ const ciphertext: ArrayBuffer = await subtle.encrypt({ name: "RSA-OAEP" }, keyObj, data);
235
+ return toB64(ciphertext);
236
+ },
237
+
238
+ decrypt: async (
239
+ privateKey: string | CryptoKeyPair,
240
+ ciphertext: string,
241
+ parameters?: Partial<Record<CryptoParam, string>>
242
+ ): Promise<string> => {
243
+ const { webHash } = resolveParams(parameters);
244
+ const privB64: string = resolvePrivateKeyB64(privateKey);
245
+ const keyObj: CryptoKey = await importPrivateKey(privB64, webHash);
246
+
247
+ const ptBuf: ArrayBuffer = await subtle.decrypt({ name: "RSA-OAEP" }, keyObj, fromB64(ciphertext));
248
+ const plaintext: string = new TextDecoder().decode(new Uint8Array(ptBuf));
249
+ return plaintext;
250
+ },
251
+
252
+ sign: async (
253
+ privateKey: string | CryptoKeyPair,
254
+ data: string,
255
+ parameters?: Partial<Record<CryptoParam, string>>
256
+ ): Promise<string> => {
257
+ const { webHash } = resolveParams(parameters);
258
+ const privB64: string = resolvePrivateKeyB64(privateKey);
259
+ const keyObj: CryptoKey = await importPrivateKeyForSign(privB64, webHash);
260
+
261
+ const bytes: Uint8Array = new TextEncoder().encode(data);
262
+ const sigBuf: ArrayBuffer = await subtle.sign({ name: "RSASSA-PKCS1-v1_5" }, keyObj, bytes);
263
+ return toB64(sigBuf);
264
+ },
265
+
266
+ verifySignature: async (
267
+ publicKey: string | CryptoKeyPair,
268
+ data: string,
269
+ signatureB64: string,
270
+ parameters?: Partial<Record<CryptoParam, string>>
271
+ ): Promise<boolean> => {
272
+ const { webHash } = resolveParams(parameters);
273
+ const pubB64: string = resolvePublicKeyB64(publicKey);
274
+ const keyObj: CryptoKey = await importPublicKeyForVerify(pubB64, webHash);
275
+
276
+ const bytes: Uint8Array = new TextEncoder().encode(data);
277
+ const sigBytes: Uint8Array = fromB64(signatureB64);
278
+ const ok: boolean = await subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, keyObj, sigBytes, bytes);
279
+ return ok;
280
+ },
281
+ };
package/src/date.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { DEFAULT_CLOCK, ZillaClock } from "./time.js";
2
+
3
+ export const formatDate = (template: string, clockOrMoment?: ZillaClock | number): string => {
4
+ const d =
5
+ typeof clockOrMoment === "number"
6
+ ? new Date(clockOrMoment)
7
+ : new Date(clockOrMoment ? clockOrMoment.now() : DEFAULT_CLOCK.now());
8
+ const year = d.getUTCFullYear();
9
+ const month = d.getUTCMonth() + 1;
10
+ const day = d.getUTCDate();
11
+ const hour = d.getUTCHours();
12
+ const minute = d.getUTCMinutes();
13
+ const second = d.getUTCSeconds();
14
+
15
+ return (template.match(/(YYYY|YY|MM|DD|HH|mm|ss|M|D|H|m|s|'[^']*'|[^YMDHms']+)/g) || [])
16
+ .map((token) => {
17
+ if (token.startsWith("'")) return token.slice(1, -1);
18
+ switch (token) {
19
+ case "YYYY":
20
+ return String(year);
21
+ case "YY":
22
+ return String(year).slice(-2);
23
+ case "MM":
24
+ return String(month).padStart(2, "0");
25
+ case "M":
26
+ return String(month);
27
+ case "DD":
28
+ return String(day).padStart(2, "0");
29
+ case "D":
30
+ return String(day);
31
+ case "HH":
32
+ return String(hour).padStart(2, "0");
33
+ case "H":
34
+ return String(hour);
35
+ case "mm":
36
+ return String(minute).padStart(2, "0");
37
+ case "m":
38
+ return String(minute);
39
+ case "ss":
40
+ return String(second).padStart(2, "0");
41
+ case "s":
42
+ return String(second);
43
+ default:
44
+ return token;
45
+ }
46
+ })
47
+ .join("");
48
+ };