@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.
- package/LICENSE.md +18 -0
- package/README.md +98 -0
- package/client/asset_bundle/bundle.ts +95 -0
- package/client/data/datastore.ts +85 -0
- package/client/data/kv_primitives.ts +25 -0
- package/client/markdown_parser/constants.ts +13 -0
- package/client/plugos/event.ts +36 -0
- package/client/plugos/eventhook.ts +8 -0
- package/client/plugos/hooks/code_widget.ts +59 -0
- package/client/plugos/hooks/command.ts +104 -0
- package/client/plugos/hooks/document_editor.ts +77 -0
- package/client/plugos/hooks/event.ts +187 -0
- package/client/plugos/hooks/mq.ts +154 -0
- package/client/plugos/hooks/plug_namespace.ts +85 -0
- package/client/plugos/hooks/slash_command.ts +192 -0
- package/client/plugos/hooks/syscall.ts +66 -0
- package/client/plugos/manifest_cache.ts +67 -0
- package/client/plugos/plug.ts +99 -0
- package/client/plugos/plug_compile.ts +202 -0
- package/client/plugos/protocol.ts +40 -0
- package/client/plugos/proxy_fetch.ts +53 -0
- package/client/plugos/sandboxes/deno_worker_sandbox.ts +6 -0
- package/client/plugos/sandboxes/sandbox.ts +14 -0
- package/client/plugos/sandboxes/web_worker_sandbox.ts +17 -0
- package/client/plugos/sandboxes/worker_sandbox.ts +132 -0
- package/client/plugos/syscalls/asset.ts +35 -0
- package/client/plugos/syscalls/clientStore.ts +21 -0
- package/client/plugos/syscalls/client_code_widget.ts +12 -0
- package/client/plugos/syscalls/code_widget.ts +24 -0
- package/client/plugos/syscalls/config.ts +46 -0
- package/client/plugos/syscalls/datastore.ts +89 -0
- package/client/plugos/syscalls/editor.ts +673 -0
- package/client/plugos/syscalls/event.ts +36 -0
- package/client/plugos/syscalls/fetch.ts +128 -0
- package/client/plugos/syscalls/index.ts +102 -0
- package/client/plugos/syscalls/jsonschema.ts +69 -0
- package/client/plugos/syscalls/language.ts +23 -0
- package/client/plugos/syscalls/lua.ts +58 -0
- package/client/plugos/syscalls/markdown.ts +84 -0
- package/client/plugos/syscalls/mq.ts +52 -0
- package/client/plugos/syscalls/service_registry.ts +43 -0
- package/client/plugos/syscalls/shell.ts +39 -0
- package/client/plugos/syscalls/space.ts +139 -0
- package/client/plugos/syscalls/sync.ts +77 -0
- package/client/plugos/syscalls/system.ts +150 -0
- package/client/plugos/system.ts +201 -0
- package/client/plugos/types.ts +60 -0
- package/client/plugos/util.ts +14 -0
- package/client/plugos/worker_runtime.ts +195 -0
- package/client/space_lua/ast.ts +328 -0
- package/client/space_lua/ast_narrow.ts +81 -0
- package/client/space_lua/eval.ts +2478 -0
- package/client/space_lua/labels.ts +416 -0
- package/client/space_lua/numeric.ts +240 -0
- package/client/space_lua/parse.ts +1522 -0
- package/client/space_lua/query_collection.ts +232 -0
- package/client/space_lua/rp.ts +27 -0
- package/client/space_lua/runtime.ts +1702 -0
- package/client/space_lua/stdlib/crypto.ts +10 -0
- package/client/space_lua/stdlib/encoding.ts +19 -0
- package/client/space_lua/stdlib/format.ts +770 -0
- package/client/space_lua/stdlib/js.ts +73 -0
- package/client/space_lua/stdlib/load.ts +52 -0
- package/client/space_lua/stdlib/math.ts +193 -0
- package/client/space_lua/stdlib/net.ts +113 -0
- package/client/space_lua/stdlib/os.ts +368 -0
- package/client/space_lua/stdlib/space_lua.ts +153 -0
- package/client/space_lua/stdlib/string.ts +286 -0
- package/client/space_lua/stdlib/table.ts +401 -0
- package/client/space_lua/stdlib.ts +489 -0
- package/client/space_lua/tonumber.ts +501 -0
- package/client/space_lua/util.ts +96 -0
- package/dist/plug-compile.js +1513 -0
- package/package.json +120 -0
- package/plug-api/constants.ts +42 -0
- package/plug-api/lib/async.ts +162 -0
- package/plug-api/lib/crypto.ts +202 -0
- package/plug-api/lib/dates.ts +13 -0
- package/plug-api/lib/json.ts +136 -0
- package/plug-api/lib/limited_map.ts +72 -0
- package/plug-api/lib/memory_cache.ts +21 -0
- package/plug-api/lib/native_fetch.ts +6 -0
- package/plug-api/lib/ref.ts +275 -0
- package/plug-api/lib/resolve.ts +90 -0
- package/plug-api/lib/tags.ts +15 -0
- package/plug-api/lib/transclusion.ts +122 -0
- package/plug-api/lib/tree.ts +232 -0
- package/plug-api/lib/yaml.ts +284 -0
- package/plug-api/syscall.ts +15 -0
- package/plug-api/syscalls/asset.ts +36 -0
- package/plug-api/syscalls/client_store.ts +33 -0
- package/plug-api/syscalls/code_widget.ts +8 -0
- package/plug-api/syscalls/config.ts +58 -0
- package/plug-api/syscalls/datastore.ts +96 -0
- package/plug-api/syscalls/editor.ts +517 -0
- package/plug-api/syscalls/event.ts +47 -0
- package/plug-api/syscalls/index.ts +77 -0
- package/plug-api/syscalls/jsonschema.ts +25 -0
- package/plug-api/syscalls/language.ts +23 -0
- package/plug-api/syscalls/lua.ts +20 -0
- package/plug-api/syscalls/markdown.ts +38 -0
- package/plug-api/syscalls/mq.ts +79 -0
- package/plug-api/syscalls/shell.ts +14 -0
- package/plug-api/syscalls/space.ts +212 -0
- package/plug-api/syscalls/sync.ts +28 -0
- package/plug-api/syscalls/system.ts +102 -0
- package/plug-api/syscalls/yaml.ts +28 -0
- package/plug-api/syscalls.ts +21 -0
- package/plug-api/system_mock.ts +89 -0
- package/plug-api/types/client.ts +116 -0
- package/plug-api/types/config.ts +22 -0
- package/plug-api/types/datastore.ts +28 -0
- package/plug-api/types/event.ts +27 -0
- package/plug-api/types/index.ts +56 -0
- package/plug-api/types/manifest.ts +98 -0
- package/plug-api/types/namespace.ts +6 -0
- 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
|
+
}
|