@silverbulletmd/silverbullet 2.4.1

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 (117) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +98 -0
  3. package/client/asset_bundle/bundle.ts +95 -0
  4. package/client/data/datastore.ts +85 -0
  5. package/client/data/kv_primitives.ts +25 -0
  6. package/client/markdown_parser/constants.ts +13 -0
  7. package/client/plugos/event.ts +36 -0
  8. package/client/plugos/eventhook.ts +8 -0
  9. package/client/plugos/hooks/code_widget.ts +59 -0
  10. package/client/plugos/hooks/command.ts +104 -0
  11. package/client/plugos/hooks/document_editor.ts +77 -0
  12. package/client/plugos/hooks/event.ts +187 -0
  13. package/client/plugos/hooks/mq.ts +154 -0
  14. package/client/plugos/hooks/plug_namespace.ts +85 -0
  15. package/client/plugos/hooks/slash_command.ts +192 -0
  16. package/client/plugos/hooks/syscall.ts +66 -0
  17. package/client/plugos/manifest_cache.ts +67 -0
  18. package/client/plugos/plug.ts +99 -0
  19. package/client/plugos/plug_compile.ts +202 -0
  20. package/client/plugos/protocol.ts +40 -0
  21. package/client/plugos/proxy_fetch.ts +53 -0
  22. package/client/plugos/sandboxes/deno_worker_sandbox.ts +6 -0
  23. package/client/plugos/sandboxes/sandbox.ts +14 -0
  24. package/client/plugos/sandboxes/web_worker_sandbox.ts +17 -0
  25. package/client/plugos/sandboxes/worker_sandbox.ts +132 -0
  26. package/client/plugos/syscalls/asset.ts +35 -0
  27. package/client/plugos/syscalls/clientStore.ts +21 -0
  28. package/client/plugos/syscalls/client_code_widget.ts +12 -0
  29. package/client/plugos/syscalls/code_widget.ts +24 -0
  30. package/client/plugos/syscalls/config.ts +46 -0
  31. package/client/plugos/syscalls/datastore.ts +89 -0
  32. package/client/plugos/syscalls/editor.ts +673 -0
  33. package/client/plugos/syscalls/event.ts +36 -0
  34. package/client/plugos/syscalls/fetch.ts +128 -0
  35. package/client/plugos/syscalls/index.ts +102 -0
  36. package/client/plugos/syscalls/jsonschema.ts +69 -0
  37. package/client/plugos/syscalls/language.ts +23 -0
  38. package/client/plugos/syscalls/lua.ts +58 -0
  39. package/client/plugos/syscalls/markdown.ts +84 -0
  40. package/client/plugos/syscalls/mq.ts +52 -0
  41. package/client/plugos/syscalls/service_registry.ts +43 -0
  42. package/client/plugos/syscalls/shell.ts +39 -0
  43. package/client/plugos/syscalls/space.ts +139 -0
  44. package/client/plugos/syscalls/sync.ts +77 -0
  45. package/client/plugos/syscalls/system.ts +150 -0
  46. package/client/plugos/system.ts +201 -0
  47. package/client/plugos/types.ts +60 -0
  48. package/client/plugos/util.ts +14 -0
  49. package/client/plugos/worker_runtime.ts +195 -0
  50. package/client/space_lua/ast.ts +328 -0
  51. package/client/space_lua/ast_narrow.ts +81 -0
  52. package/client/space_lua/eval.ts +2478 -0
  53. package/client/space_lua/labels.ts +416 -0
  54. package/client/space_lua/numeric.ts +240 -0
  55. package/client/space_lua/parse.ts +1522 -0
  56. package/client/space_lua/query_collection.ts +232 -0
  57. package/client/space_lua/rp.ts +27 -0
  58. package/client/space_lua/runtime.ts +1702 -0
  59. package/client/space_lua/stdlib/crypto.ts +10 -0
  60. package/client/space_lua/stdlib/encoding.ts +19 -0
  61. package/client/space_lua/stdlib/format.ts +770 -0
  62. package/client/space_lua/stdlib/js.ts +73 -0
  63. package/client/space_lua/stdlib/load.ts +52 -0
  64. package/client/space_lua/stdlib/math.ts +193 -0
  65. package/client/space_lua/stdlib/net.ts +113 -0
  66. package/client/space_lua/stdlib/os.ts +368 -0
  67. package/client/space_lua/stdlib/space_lua.ts +153 -0
  68. package/client/space_lua/stdlib/string.ts +286 -0
  69. package/client/space_lua/stdlib/table.ts +401 -0
  70. package/client/space_lua/stdlib.ts +489 -0
  71. package/client/space_lua/tonumber.ts +501 -0
  72. package/client/space_lua/util.ts +96 -0
  73. package/dist/plug-compile.js +1513 -0
  74. package/package.json +120 -0
  75. package/plug-api/constants.ts +42 -0
  76. package/plug-api/lib/async.ts +162 -0
  77. package/plug-api/lib/crypto.ts +202 -0
  78. package/plug-api/lib/dates.ts +13 -0
  79. package/plug-api/lib/json.ts +136 -0
  80. package/plug-api/lib/limited_map.ts +72 -0
  81. package/plug-api/lib/memory_cache.ts +21 -0
  82. package/plug-api/lib/native_fetch.ts +6 -0
  83. package/plug-api/lib/ref.ts +275 -0
  84. package/plug-api/lib/resolve.ts +90 -0
  85. package/plug-api/lib/tags.ts +15 -0
  86. package/plug-api/lib/transclusion.ts +122 -0
  87. package/plug-api/lib/tree.ts +232 -0
  88. package/plug-api/lib/yaml.ts +284 -0
  89. package/plug-api/syscall.ts +15 -0
  90. package/plug-api/syscalls/asset.ts +36 -0
  91. package/plug-api/syscalls/client_store.ts +33 -0
  92. package/plug-api/syscalls/code_widget.ts +8 -0
  93. package/plug-api/syscalls/config.ts +58 -0
  94. package/plug-api/syscalls/datastore.ts +96 -0
  95. package/plug-api/syscalls/editor.ts +517 -0
  96. package/plug-api/syscalls/event.ts +47 -0
  97. package/plug-api/syscalls/index.ts +77 -0
  98. package/plug-api/syscalls/jsonschema.ts +25 -0
  99. package/plug-api/syscalls/language.ts +23 -0
  100. package/plug-api/syscalls/lua.ts +20 -0
  101. package/plug-api/syscalls/markdown.ts +38 -0
  102. package/plug-api/syscalls/mq.ts +79 -0
  103. package/plug-api/syscalls/shell.ts +14 -0
  104. package/plug-api/syscalls/space.ts +212 -0
  105. package/plug-api/syscalls/sync.ts +28 -0
  106. package/plug-api/syscalls/system.ts +102 -0
  107. package/plug-api/syscalls/yaml.ts +28 -0
  108. package/plug-api/syscalls.ts +21 -0
  109. package/plug-api/system_mock.ts +89 -0
  110. package/plug-api/types/client.ts +116 -0
  111. package/plug-api/types/config.ts +22 -0
  112. package/plug-api/types/datastore.ts +28 -0
  113. package/plug-api/types/event.ts +27 -0
  114. package/plug-api/types/index.ts +56 -0
  115. package/plug-api/types/manifest.ts +98 -0
  116. package/plug-api/types/namespace.ts +6 -0
  117. package/plugs/builtin_plugs.ts +14 -0
package/package.json ADDED
@@ -0,0 +1,120 @@
1
+ {
2
+ "name": "@silverbulletmd/silverbullet",
3
+ "version": "2.4.1",
4
+ "type": "module",
5
+ "description": "A self-hosted, web-based note taking app",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "exports": {
10
+ "./syscall": "./plug-api/syscall.ts",
11
+ "./syscalls": "./plug-api/syscalls.ts",
12
+ "./constants": "./plug-api/constants.ts",
13
+ "./lib/json": "./plug-api/lib/json.ts",
14
+ "./lib/tree": "./plug-api/lib/tree.ts",
15
+ "./lib/ref": "./plug-api/lib/ref.ts",
16
+ "./lib/resolve": "./plug-api/lib/resolve.ts",
17
+ "./lib/dates": "./plug-api/lib/dates.ts",
18
+ "./lib/async": "./plug-api/lib/async.ts",
19
+ "./lib/crypto": "./plug-api/lib/crypto.ts",
20
+ "./lib/limited_map": "./plug-api/lib/limited_map.ts",
21
+ "./lib/tags": "./plug-api/lib/tags.ts",
22
+ "./lib/transclusion": "./plug-api/lib/transclusion.ts",
23
+ "./lib/native_fetch": "./plug-api/lib/native_fetch.ts",
24
+ "./type/client": "./plug-api/types/client.ts",
25
+ "./type/config": "./plug-api/types/config.ts",
26
+ "./type/manifest": "./plug-api/types/manifest.ts",
27
+ "./type/namespace": "./plug-api/types/namespace.ts",
28
+ "./type/datastore": "./plug-api/types/datastore.ts",
29
+ "./type/event": "./plug-api/types/event.ts",
30
+ "./type/index": "./plug-api/types/index.ts"
31
+ },
32
+ "bin": {
33
+ "plug-compile": "dist/plug-compile.js"
34
+ },
35
+ "files": [
36
+ "plug-api/**/*.ts",
37
+ "!plug-api/**/*.test.ts",
38
+ "!plug-api/**/*.bench.ts",
39
+ "client/plugos/**/*.ts",
40
+ "!client/plugos/**/*.test.ts",
41
+ "client/asset_bundle/bundle.ts",
42
+ "client/data/kv_primitives.ts",
43
+ "client/data/datastore.ts",
44
+ "client/space_lua/**/*.ts",
45
+ "!client/space_lua/**/*.test.ts",
46
+ "!client/space_lua/**/*.bench.ts",
47
+ "client/markdown_parser/constants.ts",
48
+ "plugs/builtin_plugs.ts",
49
+ "dist/plug-compile.js"
50
+ ],
51
+ "scripts": {
52
+ "build": "npm run build:plugs && npm run build:client",
53
+ "build:plugs": "tsx build_plugs_libraries.ts",
54
+ "build:client": "tsx build_client.ts",
55
+ "build:plug-compile": "tsx build_plug_compile.ts",
56
+ "prepublishOnly": "npm run build:plug-compile",
57
+ "test": "vitest run",
58
+ "check": "tsc --noEmit",
59
+ "bench": "vitest bench"
60
+ },
61
+ "dependencies": {
62
+ "@codemirror/autocomplete": "6.20.0",
63
+ "@codemirror/commands": "6.10.1",
64
+ "@codemirror/lang-css": "6.3.1",
65
+ "@codemirror/lang-html": "6.4.11",
66
+ "@codemirror/lang-javascript": "6.2.4",
67
+ "@codemirror/lang-markdown": "6.5.0",
68
+ "@codemirror/language": "6.12.1",
69
+ "@codemirror/legacy-modes": "6.5.2",
70
+ "@codemirror/lint": "6.9.2",
71
+ "@codemirror/search": "6.6.0",
72
+ "@codemirror/state": "6.5.4",
73
+ "@codemirror/view": "6.39.11",
74
+ "@joplin/turndown-plugin-gfm": "1.0.64",
75
+ "@lezer/common": "1.5.0",
76
+ "@lezer/css": "1.3.0",
77
+ "@lezer/highlight": "1.2.3",
78
+ "@lezer/html": "1.3.13",
79
+ "@lezer/javascript": "1.5.4",
80
+ "@lezer/lr": "1.4.7",
81
+ "@lezer/markdown": "1.6.3",
82
+ "@msgpack/msgpack": "3.1.3",
83
+ "@replit/codemirror-lang-nix": "6.0.1",
84
+ "@replit/codemirror-vim": "6.3.0",
85
+ "ajv": "8.17.1",
86
+ "crelt": "1.0.6",
87
+ "fast-diff": "1.3.0",
88
+ "fuse.js": "7.1.0",
89
+ "gitignore-parser": "0.0.2",
90
+ "idb": "8.0.3",
91
+ "js-yaml": "4.1.0",
92
+ "mime": "4.1.0",
93
+ "preact": "10.28.2",
94
+ "preact-feather": "4.2.1",
95
+ "react-icons": "5.5.0",
96
+ "style-mod": "4.1.2",
97
+ "turndown": "7.2.2"
98
+ },
99
+ "devDependencies": {
100
+ "@preact/preset-vite": "^2.10.3",
101
+ "@types/gitignore-parser": "^0.0.3",
102
+ "@types/js-yaml": "^4.0.9",
103
+ "@types/node": "^22.0.0",
104
+ "@types/picomatch": "^4.0.2",
105
+ "@types/turndown": "^5.0.6",
106
+ "commander": "^14.0.3",
107
+ "esbuild": "^0.27.3",
108
+ "fake-indexeddb": "6.0.1",
109
+ "fast-glob": "^3.3.0",
110
+ "picomatch": "^4.0.0",
111
+ "sass": "^1.97.3",
112
+ "tsx": "^4.19.0",
113
+ "typescript": "^5.9.3",
114
+ "vite": "^7.3.1",
115
+ "vitest": "^4.0.18"
116
+ },
117
+ "engines": {
118
+ "node": ">=20.0.0"
119
+ }
120
+ }
@@ -0,0 +1,42 @@
1
+ export const maximumDocumentSize: number = 10; // MiB
2
+ export const defaultLinkStyle: string = "wikilink";
3
+ export const offlineError: Error = new Error("Offline");
4
+ export const notFoundError: Error = new Error("Not found");
5
+ export const notAuthenticatedError: Error = new Error("Unauthenticated");
6
+ export const wrongSpacePathError: Error = new Error(
7
+ "Space folder path different on server, reloading the page",
8
+ );
9
+ export const pingTimeout: number = 2000;
10
+ export const pingInterval: number = 5000;
11
+
12
+ /**
13
+ * HTTP status codes that should be treated as "offline" conditions.
14
+ *
15
+ * This is particularly useful for cases where a proxy (such as Cloudflare or other reverse proxies)
16
+ * indicates that the backend server is down, but there is still network connectivity between
17
+ * the user and the proxy. In these scenarios, we want to allow the user to continue working
18
+ * with their cached data rather than showing an error, even though technically there is network
19
+ * connectivity to the proxy.
20
+ *
21
+ * This enables SilverBullet to work in a true "offline-first" manner, falling back to cached
22
+ * content when the backend is unavailable through no fault of the user's network connection.
23
+ *
24
+ * All 5xx server errors are included to prevent the client from caching error HTML pages
25
+ * (e.g., Nginx 500 error pages) which would prevent the client from booting in offline mode.
26
+ */
27
+ export const offlineStatusCodes = {
28
+ 500: "Internal Server Error", // Server encountered an unexpected condition
29
+ 501: "Not Implemented", // Server does not support the functionality required
30
+ 502: "Bad Gateway", // Proxy server received invalid response from upstream server
31
+ 503: "Service Unavailable", // Server is temporarily unable to handle the request
32
+ 504: "Gateway Timeout", // Proxy server did not receive a timely response from upstream server
33
+ 505: "HTTP Version Not Supported", // Server does not support the HTTP version
34
+ 506: "Variant Also Negotiates", // Server has an internal configuration error
35
+ 507: "Insufficient Storage", // Server is unable to store the representation
36
+ 508: "Loop Detected", // Server detected an infinite loop while processing
37
+ 509: "Bandwidth Limit Exceeded", // Server bandwidth limit has been exceeded
38
+ 510: "Not Extended", // Further extensions to the request are required
39
+ 511: "Network Authentication Required", // Client needs to authenticate to gain network access
40
+
41
+ 530: "Unable to resolve origin hostname", // Served when cloudflared is down on the host
42
+ } as const;
@@ -0,0 +1,162 @@
1
+ export function throttle(func: () => void, limit: number): () => void {
2
+ let timer: any = null;
3
+ return function () {
4
+ if (!timer) {
5
+ timer = setTimeout(() => {
6
+ func();
7
+ timer = null;
8
+ }, limit);
9
+ }
10
+ };
11
+ }
12
+
13
+ export function throttleImmediately(
14
+ func: () => void,
15
+ limit: number,
16
+ ): () => void {
17
+ let timer: any = null;
18
+ return function () {
19
+ if (!timer) {
20
+ func();
21
+ timer = setTimeout(() => {
22
+ timer = null;
23
+ }, limit);
24
+ }
25
+ };
26
+ }
27
+
28
+ // race for promises returns first promise that resolves
29
+ export function race<T>(promises: Promise<T>[]): Promise<T> {
30
+ return new Promise((resolve, reject) => {
31
+ for (const p of promises) {
32
+ p.then(resolve, reject);
33
+ }
34
+ });
35
+ }
36
+
37
+ export function timeout(ms: number): Promise<never> {
38
+ return new Promise((_resolve, reject) =>
39
+ setTimeout(() => {
40
+ reject(new Error("timeout"));
41
+ }, ms)
42
+ );
43
+ }
44
+
45
+ export function sleep(ms: number): Promise<void> {
46
+ return new Promise((resolve) => setTimeout(resolve, ms));
47
+ }
48
+
49
+ export class PromiseQueue {
50
+ private queue: {
51
+ fn: () => Promise<any>;
52
+ resolve: (value: any) => void;
53
+ reject: (error: any) => void;
54
+ }[] = [];
55
+ private processing = false;
56
+
57
+ runInQueue(fn: () => Promise<any>): Promise<any> {
58
+ return new Promise((resolve, reject) => {
59
+ this.queue.push({ fn, resolve, reject });
60
+ if (!this.processing) {
61
+ this.process();
62
+ }
63
+ });
64
+ }
65
+
66
+ private async process(): Promise<void> {
67
+ if (this.queue.length === 0) {
68
+ this.processing = false;
69
+ return;
70
+ }
71
+
72
+ this.processing = true;
73
+ const { fn, resolve, reject } = this.queue.shift()!;
74
+
75
+ try {
76
+ const result = await fn();
77
+ resolve(result);
78
+ } catch (error) {
79
+ reject(error);
80
+ }
81
+
82
+ this.process(); // Continue processing the next promise in the queue
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Batches up values, and processes in batches of batchSize in parallel
88
+ * then merges the results in the appropriate order.
89
+ * @param values - The values to batch.
90
+ * @param fn - The function to run on each batch.
91
+ * @param batchSize - The size of each batch.
92
+ */
93
+ export async function batchRequests<I, O>(
94
+ values: I[],
95
+ fn: (batch: I[]) => Promise<O[]>,
96
+ batchSize: number,
97
+ ): Promise<O[]> {
98
+ const results: O[] = [];
99
+ // Split values into batches of batchSize
100
+ const batches: I[][] = [];
101
+ for (let i = 0; i < values.length; i += batchSize) {
102
+ batches.push(values.slice(i, i + batchSize));
103
+ }
104
+ // Run fn on them in parallel
105
+ const batchResults = await Promise.all(batches.map(fn));
106
+ // Flatten the results
107
+ for (const batchResult of batchResults) {
108
+ if (Array.isArray(batchResult)) { // If fn returns an array, collect them
109
+ results.push(...batchResult);
110
+ }
111
+ }
112
+ return results;
113
+ }
114
+
115
+ /**
116
+ * Processes items in parallel with a specified concurrency limit.
117
+ * @param items - The items to process.
118
+ * @param handler - The function to run on each item.
119
+ * @param concurrency - The maximum number of concurrent operations.
120
+ */
121
+ export async function processWithConcurrency<I, O>(
122
+ items: I[],
123
+ handler: (item: I) => Promise<O>,
124
+ concurrency: number,
125
+ ): Promise<O[]> {
126
+ const results: O[] = [];
127
+ let idx = 0;
128
+
129
+ async function worker() {
130
+ while (idx < items.length) {
131
+ const currentIdx = idx++;
132
+ const item = items[currentIdx];
133
+ const result = await handler(item);
134
+ results[currentIdx] = result;
135
+ }
136
+ }
137
+
138
+ const workers = [];
139
+ for (let i = 0; i < Math.min(concurrency, items.length); i++) {
140
+ workers.push(worker());
141
+ }
142
+
143
+ await Promise.all(workers);
144
+ return results;
145
+ }
146
+
147
+ /**
148
+ * Runs a function safely by catching any errors and logging them to the console.
149
+ * @param fn - The function to run.
150
+ */
151
+ export function safeRun(fn: () => Promise<void>): void {
152
+ fn().catch((e) => {
153
+ console.error(e);
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Generates a random delay between 0 and 1000 milliseconds.
159
+ */
160
+ export function jitter(maxLength = 1000): number {
161
+ return Math.floor(Math.random() * maxLength);
162
+ }
@@ -0,0 +1,202 @@
1
+ export function base64Decode(s: string): Uint8Array {
2
+ const binString = atob(s);
3
+ const len = binString.length;
4
+ const bytes = new Uint8Array(len);
5
+ for (let i = 0; i < len; i++) {
6
+ bytes[i] = binString.charCodeAt(i);
7
+ }
8
+ return bytes;
9
+ }
10
+
11
+ export function base64Encode(buffer: Uint8Array | string): string {
12
+ if (typeof buffer === "string") {
13
+ buffer = new TextEncoder().encode(buffer);
14
+ }
15
+ let binary = "";
16
+ const len = buffer.byteLength;
17
+ for (let i = 0; i < len; i++) {
18
+ binary += String.fromCharCode(buffer[i]);
19
+ }
20
+ return btoa(binary);
21
+ }
22
+
23
+ export function base64EncodedDataUrl(
24
+ mimeType: string,
25
+ buffer: Uint8Array,
26
+ ): string {
27
+ return `data:${mimeType};base64,${base64Encode(buffer)}`;
28
+ }
29
+
30
+ export function base64DecodeDataUrl(dataUrl: string): Uint8Array {
31
+ const b64Encoded = dataUrl.split(",", 2)[1];
32
+ return base64Decode(b64Encoded);
33
+ }
34
+
35
+ /**
36
+ * Perform sha256 hash using the browser's crypto APIs
37
+ * Note: this will only work over HTTPS
38
+ * @param message
39
+ */
40
+ export async function hashSHA256(
41
+ message: string | Uint8Array,
42
+ ): Promise<string> {
43
+ // Transform the string into an ArrayBuffer
44
+ const encoder = new TextEncoder();
45
+ const data: Uint8Array = typeof message === "string"
46
+ ? encoder.encode(message)
47
+ : message;
48
+
49
+ // Generate the hash
50
+ const hashBuffer = await globalThis.crypto.subtle.digest(
51
+ "SHA-256",
52
+ data as BufferSource,
53
+ );
54
+
55
+ // Transform the hash into a hex string
56
+ return Array.from(new Uint8Array(hashBuffer)).map((b) =>
57
+ b.toString(16).padStart(2, "0")
58
+ ).join("");
59
+ }
60
+
61
+ /**
62
+ * To avoid database clashes based on space folder path name, base URLs and encryption keys we derive
63
+ * a database name from a hash of all these combined together
64
+ */
65
+ export async function deriveDbName(
66
+ type: "data" | "files",
67
+ spaceFolderPath: string,
68
+ baseURI: string,
69
+ encryptionKey?: CryptoKey,
70
+ ): Promise<string> {
71
+ let keyPart = "";
72
+ if (encryptionKey) {
73
+ keyPart = await exportKey(encryptionKey);
74
+ }
75
+ const spaceHash = await hashSHA256(
76
+ `${spaceFolderPath}:${baseURI}:${keyPart}`,
77
+ );
78
+ return `sb_${type}_${spaceHash}`;
79
+ }
80
+
81
+ // Fixed counter for AES-CTR all zeroes, for determinism
82
+ const fixedCounter = new Uint8Array(16);
83
+
84
+ export async function encryptStringDeterministic(
85
+ key: CryptoKey,
86
+ clearText: string,
87
+ ): Promise<string> {
88
+ const encrypted = await crypto.subtle.encrypt(
89
+ { name: "AES-CTR", counter: fixedCounter, length: fixedCounter.length * 8 },
90
+ key,
91
+ new TextEncoder().encode(clearText),
92
+ );
93
+ return base64Encode(new Uint8Array(encrypted));
94
+ }
95
+
96
+ export async function decryptStringDeterministic(
97
+ key: CryptoKey,
98
+ cipherText: string,
99
+ ): Promise<string> {
100
+ const decrypted = await crypto.subtle.decrypt(
101
+ { name: "AES-CTR", counter: fixedCounter, length: fixedCounter.length * 8 },
102
+ key,
103
+ base64Decode(cipherText) as BufferSource,
104
+ );
105
+ return new TextDecoder().decode(decrypted);
106
+ }
107
+
108
+ // Encrypt using AES-GCM with random IV; output = IV + ciphertext
109
+ export async function encryptAesGcm(
110
+ key: CryptoKey,
111
+ data: Uint8Array,
112
+ ): Promise<Uint8Array> {
113
+ const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit IV recommended for GCM
114
+ const encryptedBuffer = await crypto.subtle.encrypt(
115
+ { name: "AES-GCM", iv },
116
+ key,
117
+ data as BufferSource,
118
+ );
119
+ const encrypted = new Uint8Array(encryptedBuffer);
120
+
121
+ // Prepend IV to ciphertext
122
+ const result = new Uint8Array(iv.length + encrypted.length);
123
+ result.set(iv, 0);
124
+ result.set(encrypted, iv.length);
125
+ return result;
126
+ }
127
+
128
+ // Decrypt using AES-GCM assuming input format IV + ciphertext
129
+ export async function decryptAesGcm(
130
+ key: CryptoKey,
131
+ encryptedData: Uint8Array,
132
+ ): Promise<Uint8Array> {
133
+ const iv = encryptedData.slice(0, 12); // extract IV (first 12 bytes)
134
+ const ciphertext = encryptedData.slice(12);
135
+ const decryptedBuffer = await crypto.subtle.decrypt(
136
+ { name: "AES-GCM", iv },
137
+ key,
138
+ ciphertext,
139
+ );
140
+ return new Uint8Array(decryptedBuffer);
141
+ }
142
+
143
+ export async function deriveCTRKeyFromPassword(
144
+ password: string,
145
+ salt: Uint8Array,
146
+ ): Promise<CryptoKey> {
147
+ // Encode password to ArrayBuffer
148
+ const passwordBytes = new TextEncoder().encode(password);
149
+
150
+ // Import password as a CryptoKey
151
+ const baseKey = await crypto.subtle.importKey(
152
+ "raw",
153
+ passwordBytes,
154
+ { name: "PBKDF2" },
155
+ false,
156
+ ["deriveBits", "deriveKey"],
157
+ );
158
+
159
+ return crypto.subtle.deriveKey(
160
+ {
161
+ name: "PBKDF2",
162
+ salt: salt as BufferSource,
163
+ iterations: 100000,
164
+ hash: "SHA-256",
165
+ },
166
+ baseKey,
167
+ {
168
+ name: "AES-CTR",
169
+ length: 256,
170
+ },
171
+ true, // extractable
172
+ ["encrypt", "decrypt"],
173
+ );
174
+ }
175
+
176
+ export function importKey(b64EncodedKey: string): Promise<CryptoKey> {
177
+ return crypto.subtle.importKey(
178
+ "raw",
179
+ base64Decode(b64EncodedKey) as BufferSource,
180
+ { name: "AES-CTR" },
181
+ true,
182
+ ["encrypt", "decrypt"],
183
+ );
184
+ }
185
+
186
+ export async function exportKey(ctrKey: CryptoKey): Promise<string> {
187
+ const key = await crypto.subtle.exportKey("raw", ctrKey);
188
+ return base64Encode(new Uint8Array(key));
189
+ }
190
+
191
+ export async function deriveGCMKeyFromCTR(
192
+ ctrKey: CryptoKey,
193
+ ): Promise<CryptoKey> {
194
+ const rawKey = await crypto.subtle.exportKey("raw", ctrKey);
195
+ return crypto.subtle.importKey(
196
+ "raw",
197
+ rawKey,
198
+ { name: "AES-GCM" },
199
+ true,
200
+ ["encrypt", "decrypt"],
201
+ );
202
+ }
@@ -0,0 +1,13 @@
1
+ export function niceDate(d: Date): string {
2
+ return localDateString(d).split("T")[0];
3
+ }
4
+
5
+ export function localDateString(d: Date): string {
6
+ return d.getFullYear() +
7
+ "-" + String(d.getMonth() + 1).padStart(2, "0") +
8
+ "-" + String(d.getDate()).padStart(2, "0") +
9
+ "T" + String(d.getHours()).padStart(2, "0") +
10
+ ":" + String(d.getMinutes()).padStart(2, "0") +
11
+ ":" + String(d.getSeconds()).padStart(2, "0") +
12
+ "." + String(d.getMilliseconds()).padStart(3, "0");
13
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Performs a deep comparison of two objects, returning true if they are equal
3
+ * @param a first object
4
+ * @param b second object
5
+ * @returns
6
+ */
7
+ export function deepEqual(a: any, b: any): boolean {
8
+ if (a === b) {
9
+ return true;
10
+ }
11
+ if (typeof a !== typeof b) {
12
+ return false;
13
+ }
14
+ if (a === null || b === null) {
15
+ return false;
16
+ }
17
+ if (a === undefined || b === undefined) {
18
+ return false;
19
+ }
20
+ if (typeof a === "object") {
21
+ if (Array.isArray(a) && Array.isArray(b)) {
22
+ if (a.length !== b.length) {
23
+ return false;
24
+ }
25
+ for (let i = 0; i < a.length; i++) {
26
+ if (!deepEqual(a[i], b[i])) {
27
+ return false;
28
+ }
29
+ }
30
+ return true;
31
+ } else {
32
+ const aKeys = Object.keys(a);
33
+ const bKeys = Object.keys(b);
34
+ if (aKeys.length !== bKeys.length) {
35
+ return false;
36
+ }
37
+ for (const key of aKeys) {
38
+ if (!deepEqual(a[key], b[key])) {
39
+ return false;
40
+ }
41
+ }
42
+ return true;
43
+ }
44
+ }
45
+ return false;
46
+ }
47
+
48
+ /**
49
+ * Converts a Date object to a date string in the format YYYY-MM-DD if it just contains a date (and no significant time), or a full ISO string otherwise
50
+ * @param d the date to convert
51
+ */
52
+ export function cleanStringDate(d: Date): string {
53
+ // If no significant time, return a date string only
54
+ if (
55
+ d.getUTCHours() === 0 && d.getUTCMinutes() === 0 && d.getUTCSeconds() === 0
56
+ ) {
57
+ return d.getUTCFullYear() + "-" +
58
+ String(d.getUTCMonth() + 1).padStart(2, "0") + "-" +
59
+ String(d.getUTCDate()).padStart(2, "0");
60
+ } else {
61
+ return d.toISOString();
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Processes a JSON (typically coming from parse YAML frontmatter) in two ways:
67
+ * 1. Expands property names in an object containing a .-separated path
68
+ * 2. Converts dates to strings in sensible ways
69
+ * @param a
70
+ * @returns
71
+ */
72
+ export function cleanupJSON(a: any): any {
73
+ if (!a) {
74
+ return a;
75
+ }
76
+ if (typeof a !== "object") {
77
+ return a;
78
+ }
79
+ if (Array.isArray(a)) {
80
+ return a.map(cleanupJSON);
81
+ }
82
+ // If a is a date, convert to a string
83
+ if (a instanceof Date) {
84
+ return cleanStringDate(a);
85
+ }
86
+ const expanded: any = {};
87
+ for (const key of Object.keys(a)) {
88
+ const parts = key.split(".");
89
+ let target = expanded;
90
+ for (let i = 0; i < parts.length - 1; i++) {
91
+ const part = parts[i];
92
+ if (!target[part]) {
93
+ target[part] = {};
94
+ }
95
+ target = target[part];
96
+ }
97
+ target[parts[parts.length - 1]] = cleanupJSON(a[key]);
98
+ }
99
+ return expanded;
100
+ }
101
+
102
+ export function deepClone<T>(obj: T, ignoreKeys: string[] = []): T {
103
+ // Handle null, undefined, or primitive types (string, number, boolean, symbol, bigint)
104
+ if (obj === null || typeof obj !== "object") {
105
+ return obj;
106
+ }
107
+
108
+ // Handle Date
109
+ if (obj instanceof Date) {
110
+ return new Date(obj.getTime()) as any;
111
+ }
112
+
113
+ // Handle Array
114
+ if (Array.isArray(obj)) {
115
+ const arrClone: any[] = [];
116
+ for (let i = 0; i < obj.length; i++) {
117
+ arrClone[i] = deepClone(obj[i], ignoreKeys);
118
+ }
119
+ return arrClone as any;
120
+ }
121
+
122
+ // Handle Object
123
+ if (obj instanceof Object) {
124
+ const objClone: { [key: string]: any } = {};
125
+ for (const key in obj) {
126
+ if (ignoreKeys.includes(key)) {
127
+ objClone[key] = obj[key];
128
+ } else if (Object.prototype.hasOwnProperty.call(obj, key)) {
129
+ objClone[key] = deepClone(obj[key], ignoreKeys);
130
+ }
131
+ }
132
+ return objClone as T;
133
+ }
134
+
135
+ throw new Error("Unsupported data type.");
136
+ }