@wenathlan/saddle 2.0.150 → 2.0.152
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/Dockerfile +43 -23
- package/README.md +4 -4
- package/biome.jsonc +134 -0
- package/dist/App.d.ts +3 -0
- package/dist/App.js +50 -0
- package/dist/Sol/Sol.d.ts +3 -0
- package/dist/Sol/Sol.js +46 -0
- package/dist/Sol/agentbrowser/agentbrowser.d.ts +9 -0
- package/dist/Sol/agentbrowser/agentbrowser.js +121 -0
- package/dist/Sol/architecture/architecture.d.ts +9 -0
- package/dist/Sol/architecture/architecture.js +93 -0
- package/dist/Sol/compute/compute.d.ts +9 -0
- package/dist/Sol/compute/compute.js +22 -0
- package/dist/Sol/console/console.d.ts +15 -0
- package/dist/Sol/console/console.js +292 -0
- package/dist/Sol/dashboard/dashboard.d.ts +10 -0
- package/dist/Sol/dashboard/dashboard.js +181 -0
- package/dist/Sol/docs/docs.d.ts +9 -0
- package/dist/Sol/docs/docs.js +28 -0
- package/dist/Sol/home/home.d.ts +35 -0
- package/dist/Sol/home/home.js +239 -0
- package/dist/Sol/integrations/integrations.d.ts +9 -0
- package/dist/Sol/integrations/integrations.js +83 -0
- package/dist/Sol/intro/intro.d.ts +7 -0
- package/dist/Sol/intro/intro.js +102 -0
- package/dist/Sol/login/login.d.ts +16 -0
- package/dist/Sol/login/login.js +49 -0
- package/dist/Sol/notfound/notfound.d.ts +9 -0
- package/dist/Sol/notfound/notfound.js +15 -0
- package/dist/Sol/onboarding/onboarding.d.ts +25 -0
- package/dist/Sol/onboarding/onboarding.js +60 -0
- package/dist/Sol/playground/playground.d.ts +9 -0
- package/dist/Sol/playground/playground.js +127 -0
- package/dist/Sol/register/register.d.ts +16 -0
- package/dist/Sol/register/register.js +193 -0
- package/dist/Sol/shell/Shell.d.ts +124 -0
- package/dist/Sol/shell/Shell.js +335 -0
- package/dist/Sol/wingets/ComputeBootladder.d.ts +6 -0
- package/dist/Sol/wingets/ComputeBootladder.js +71 -0
- package/dist/Sol/wingets/ComputeModalitycatalog.d.ts +7 -0
- package/dist/Sol/wingets/ComputeModalitycatalog.js +71 -0
- package/dist/Sol/wingets/ConsoleApibadge.d.ts +7 -0
- package/dist/Sol/wingets/ConsoleApibadge.js +5 -0
- package/dist/Sol/wingets/ConsoleEdgelaw.d.ts +6 -0
- package/dist/Sol/wingets/ConsoleEdgelaw.js +36 -0
- package/dist/Sol/wingets/ConsoleEventstimeline.d.ts +19 -0
- package/dist/Sol/wingets/ConsoleEventstimeline.js +12 -0
- package/dist/Sol/wingets/ConsoleSnapshotcontrols.d.ts +8 -0
- package/dist/Sol/wingets/ConsoleSnapshotcontrols.js +8 -0
- package/dist/Sol/wingets/ConsoleSpecpanel.d.ts +21 -0
- package/dist/Sol/wingets/ConsoleSpecpanel.js +75 -0
- package/dist/Sol/wingets/ConsoleTerminal.d.ts +22 -0
- package/dist/Sol/wingets/ConsoleTerminal.js +77 -0
- package/dist/Sol/wingets/DashboardAccountpanel.d.ts +18 -0
- package/dist/Sol/wingets/DashboardAccountpanel.js +65 -0
- package/dist/Sol/wingets/DashboardAdminpanel.d.ts +6 -0
- package/dist/Sol/wingets/DashboardAdminpanel.js +90 -0
- package/dist/Sol/wingets/DashboardAuditlog.d.ts +8 -0
- package/dist/Sol/wingets/DashboardAuditlog.js +50 -0
- package/dist/Sol/wingets/DashboardCreatesandboxform.d.ts +10 -0
- package/dist/Sol/wingets/DashboardCreatesandboxform.js +71 -0
- package/dist/Sol/wingets/DashboardEventstimeline.d.ts +8 -0
- package/dist/Sol/wingets/DashboardEventstimeline.js +83 -0
- package/dist/Sol/wingets/DashboardGlobalsandboxestable.d.ts +8 -0
- package/dist/Sol/wingets/DashboardGlobalsandboxestable.js +57 -0
- package/dist/Sol/wingets/DashboardMeshnodestable.d.ts +8 -0
- package/dist/Sol/wingets/DashboardMeshnodestable.js +103 -0
- package/dist/Sol/wingets/DashboardSandboxlist.d.ts +10 -0
- package/dist/Sol/wingets/DashboardSandboxlist.js +163 -0
- package/dist/Sol/wingets/DashboardUserstable.d.ts +8 -0
- package/dist/Sol/wingets/DashboardUserstable.js +78 -0
- package/dist/Sol/wingets/HomeEntrygate.d.ts +23 -0
- package/dist/Sol/wingets/HomeEntrygate.js +73 -0
- package/dist/Sol/wingets/HomeMetricstrip.d.ts +9 -0
- package/dist/Sol/wingets/HomeMetricstrip.js +4 -0
- package/dist/Sol/wingets/HomeRuntimediagram.d.ts +1 -0
- package/dist/Sol/wingets/HomeRuntimediagram.js +22 -0
- package/dist/Sol/wingets/LoginForm.d.ts +19 -0
- package/dist/Sol/wingets/LoginForm.js +141 -0
- package/dist/Sol/wingets/RegisterPasswordconfirm.d.ts +13 -0
- package/dist/Sol/wingets/RegisterPasswordconfirm.js +16 -0
- package/dist/Sol/wingets/RegisterPasswordmeter.d.ts +19 -0
- package/dist/Sol/wingets/RegisterPasswordmeter.js +65 -0
- package/dist/Sol/wingets/RegisterUsernamerules.d.ts +11 -0
- package/dist/Sol/wingets/RegisterUsernamerules.js +16 -0
- package/dist/Sol/wingets/index.d.ts +39 -0
- package/dist/Sol/wingets/index.js +39 -0
- package/dist/alternatives.d.ts +508 -0
- package/dist/alternatives.js +950 -0
- package/dist/api.d.ts +154 -0
- package/dist/api.js +174 -0
- package/dist/auth.d.ts +268 -0
- package/dist/auth.js +476 -0
- package/dist/automation.d.ts +425 -0
- package/dist/automation.js +566 -0
- package/dist/browser.d.ts +561 -0
- package/dist/browser.js +1058 -0
- package/dist/capacitor.config.d.ts +45 -0
- package/dist/capacitor.config.js +48 -0
- package/dist/catalog.d.ts +43 -0
- package/dist/catalog.js +44 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +168 -0
- package/dist/communication.d.ts +181 -0
- package/dist/communication.js +376 -0
- package/dist/compute.d.ts +1160 -0
- package/dist/compute.js +2272 -0
- package/dist/dashboardlib.d.ts +58 -0
- package/dist/dashboardlib.js +161 -0
- package/dist/db.d.ts +569 -0
- package/dist/db.js +1080 -0
- package/dist/distribution.d.ts +528 -0
- package/dist/distribution.js +810 -0
- package/dist/drizzle.config.d.ts +20 -0
- package/dist/drizzle.config.js +18 -0
- package/dist/edgelandes.d.ts +70 -0
- package/dist/edgelandes.host.d.ts +26 -0
- package/dist/edgelandes.host.js +1089 -0
- package/dist/edgelandes.inproc.d.ts +27 -0
- package/dist/edgelandes.inproc.js +952 -0
- package/dist/edgelandes.js +1133 -0
- package/dist/edgelandes.ninety.d.ts +31 -0
- package/dist/edgelandes.ninety.js +174 -0
- package/dist/edgelandes.virtual.d.ts +25 -0
- package/dist/edgelandes.virtual.js +629 -0
- package/dist/edgeplane.d.ts +114 -0
- package/dist/edgeplane.js +305 -0
- package/dist/examplelocaljob.d.ts +1 -0
- package/{docs/example-localjob.ts → dist/examplelocaljob.js} +0 -1
- package/dist/execution.d.ts +529 -0
- package/dist/execution.js +760 -0
- package/dist/family.d.ts +16 -0
- package/dist/family.js +42 -0
- package/dist/familyurl.d.ts +21 -0
- package/dist/familyurl.js +24 -0
- package/dist/format.d.ts +18 -0
- package/dist/format.js +59 -0
- package/dist/foundation.d.ts +172 -0
- package/dist/foundation.js +312 -0
- package/dist/index.d.ts +735 -0
- package/dist/index.js +1020 -0
- package/dist/integration.d.ts +235 -0
- package/dist/integration.js +228 -0
- package/dist/intelligence.d.ts +93 -0
- package/dist/intelligence.js +144 -0
- package/dist/isolation.d.ts +128 -0
- package/dist/isolation.js +95 -0
- package/dist/labels.d.ts +665 -0
- package/dist/labels.js +751 -0
- package/dist/localauth.d.ts +127 -0
- package/dist/localauth.js +413 -0
- package/dist/media.d.ts +343 -0
- package/dist/media.js +985 -0
- package/dist/mesh.d.ts +177 -0
- package/dist/mesh.js +468 -0
- package/dist/modalityregistry.d.ts +109 -0
- package/dist/modalityregistry.js +1386 -0
- package/dist/modes.d.ts +358 -0
- package/dist/modes.js +133 -0
- package/dist/onboardingstate.d.ts +79 -0
- package/dist/onboardingstate.js +282 -0
- package/dist/orchestrator.d.ts +1338 -0
- package/dist/orchestrator.js +4310 -0
- package/dist/packager/targetcli.js +4 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +14 -0
- package/dist/performance.d.ts +415 -0
- package/dist/performance.js +826 -0
- package/dist/quantum.d.ts +809 -0
- package/dist/quantum.js +2072 -0
- package/dist/render.d.ts +380 -0
- package/dist/render.js +1072 -0
- package/dist/runall.d.ts +2 -0
- package/dist/runall.js +39 -0
- package/dist/runtimeprobe.d.ts +4 -0
- package/dist/runtimeprobe.js +8 -0
- package/dist/sandbox.d.ts +355 -0
- package/dist/sandbox.js +1721 -0
- package/dist/sandboxenvprofile.d.ts +29 -0
- package/dist/sandboxenvprofile.js +89 -0
- package/dist/sandboxprofile.d.ts +137 -0
- package/dist/sandboxprofile.js +241 -0
- package/dist/sandboxqueue.d.ts +108 -0
- package/dist/sandboxqueue.js +201 -0
- package/dist/sandboxquota.d.ts +121 -0
- package/dist/sandboxquota.js +236 -0
- package/dist/scheduler.d.ts +595 -0
- package/dist/scheduler.js +1258 -0
- package/dist/scriptlanes.d.ts +182 -0
- package/dist/scriptlanes.js +660 -0
- package/dist/scripts.buildengine.d.ts +1 -0
- package/dist/scripts.buildengine.js +39 -0
- package/dist/scripts.securitygate.d.ts +1 -0
- package/dist/scripts.securitygate.js +68 -0
- package/dist/scripts.validateflatnative.d.ts +1 -0
- package/dist/scripts.validateflatnative.js +72 -0
- package/dist/security.d.ts +500 -0
- package/dist/security.js +1169 -0
- package/dist/seedcatalog.d.ts +13 -0
- package/dist/seedcatalog.js +46 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +53 -0
- package/dist/speccatalog.d.ts +31 -0
- package/dist/speccatalog.js +163 -0
- package/dist/tiers.d.ts +1001 -0
- package/dist/tiers.js +2169 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +5 -0
- package/dist/vaulthandoff.d.ts +141 -0
- package/dist/vaulthandoff.js +444 -0
- package/dist/virtual.d.ts +640 -0
- package/dist/virtual.js +1284 -0
- package/dist/virtualcpu.d.ts +315 -0
- package/dist/virtualcpu.js +1180 -0
- package/dist/virtualgpu.d.ts +344 -0
- package/dist/virtualgpu.js +1000 -0
- package/dist/virtualization.d.ts +606 -0
- package/dist/virtualization.js +1186 -0
- package/dist/virtualmemory.d.ts +817 -0
- package/dist/virtualmemory.js +1795 -0
- package/dist/vite.config.d.ts +3 -0
- package/dist/vite.config.js +43 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +25 -0
- package/dist/webserver.d.ts +166 -0
- package/dist/webserver.js +2226 -0
- package/dist/workloadlaw.d.ts +133 -0
- package/dist/workloadlaw.js +193 -0
- package/docker.config +13 -6
- package/docs/artifactavailability.md +2 -2
- package/docs/{containerplatforms-2.1.0.md → containerplatforms.2.1.0.md} +1 -1
- package/docs/{documentmap-1.8.12.md → documentmap.1.8.12.md} +1 -1
- package/docs/ninety.modalities.md +108 -0
- package/docs/planning.ninety.md +391 -0
- package/docs/{releasenotes-1.8.13.md → releasenotes.1.8.13.md} +2 -2
- package/docs/{releasenotes-1.8.14.md → releasenotes.1.8.14.md} +1 -1
- package/docs/{repo-synthesis-1.8.16.md → repo.synthesis.1.8.16.md} +1 -1
- package/docs/{todo-1.8.15.md → todo.1.8.15.md} +1 -1
- package/docs/{todo-1.8.16.md → todo.1.8.16.md} +5 -5
- package/docs/todo.md +2 -2
- package/docs/workloadlaw.md +104 -0
- package/index.ts +4 -3
- package/manifest.json +1 -1
- package/modalityregistry.ts +1493 -0
- package/orchestrator.ts +1 -1
- package/package.json +76 -37
- package/docs/CONVERSA.txt +0 -1523
- package/docs/architecture-1.8.19.png +0 -0
- package/docs/assets/architecture.svg +0 -45
- package/docs/assets/saddle-browser-trace.webp +0 -0
- package/docs/assets/saddle-hero-bridge.webp +0 -0
- package/docs/assets/saddle-mark.webp +0 -0
- package/docs/assets/saddle-runtime-map.webp +0 -0
- package/docs/assets/saddlemark.svg +0 -13
- package/docs/example-publicapi.ts +0 -7
- package/docs/saddle.archive.1.8.17.tar.gz.gpg +0 -0
- /package/docs/{architecture-1.8.19.md → architecture.1.8.19.md} +0 -0
- /package/docs/{architecture-1.8.19.mmd → architecture.1.8.19.mmd} +0 -0
- /package/docs/{artifactresearch-1.8.9.md → artifactresearch.1.8.9.md} +0 -0
- /package/docs/{batches-1816-1820.md → batches.1816.to.1820.md} +0 -0
- /package/docs/{brancharchive-2026-08-18.md → brancharchive.2026.08.18.md} +0 -0
- /package/docs/{containerplatforms-1.8.17.md → containerplatforms.1.8.17.md} +0 -0
- /package/docs/{e2ugh-engine.md → e2ugh.engine.md} +0 -0
- /package/docs/{flatbuildresearch-1.8.11.md → flatbuildresearch.1.8.11.md} +0 -0
- /package/docs/{licenseaudit-1.8.12.md → licenseaudit.1.8.12.md} +0 -0
- /package/docs/{mobileconversionresearch-1.8.10.md → mobileconversionresearch.1.8.10.md} +0 -0
- /package/docs/{native-wrappers.md → native.wrappers.md} +0 -0
- /package/docs/{nativeidentityresearch-1.8.11.md → nativeidentityresearch.1.8.11.md} +0 -0
- /package/docs/{releasenotes-1.8.12.md → releasenotes.1.8.12.md} +0 -0
- /package/docs/{releasenotes-1.8.15.md → releasenotes.1.8.15.md} +0 -0
- /package/docs/{releasenotes-1.8.16.md → releasenotes.1.8.16.md} +0 -0
- /package/docs/{releasenotes-1.8.17.md → releasenotes.1.8.17.md} +0 -0
- /package/docs/{releasenotes-1.8.18.md → releasenotes.1.8.18.md} +0 -0
- /package/docs/{releasenotes-1.8.19.md → releasenotes.1.8.19.md} +0 -0
- /package/docs/{reorganization-1.8.8.md → reorganization.1.8.8.md} +0 -0
- /package/docs/{reorganization-1.8.9.md → reorganization.1.8.9.md} +0 -0
- /package/docs/{repo-analysis-1.8.21.json → repo.analysis.1.8.21.json} +0 -0
- /package/docs/{repo-candidates-1.8.21.jsonl → repo.candidates.1.8.21.jsonl} +0 -0
- /package/docs/{repo-research-1.8.16.md → repo.research.1.8.16.md} +0 -0
- /package/docs/{repo-research-1.8.21.md → repo.research.1.8.21.md} +0 -0
- /package/docs/{repo-selected-1.8.21.json → repo.selected.1.8.21.json} +0 -0
- /package/docs/{repo-synthesis-1.8.21.md → repo.synthesis.1.8.21.md} +0 -0
- /package/docs/{research-1.8.18-isolation.md → research.1.8.18.isolation.md} +0 -0
- /package/docs/{research-1.8.19-virtual-browser.md → research.1.8.19.virtual.browser.md} +0 -0
- /package/docs/{scanningresearch-1.8.11.md → scanningresearch.1.8.11.md} +0 -0
- /package/docs/{securityaudit-1.8.7.md → securityaudit.1.8.7.md} +0 -0
- /package/docs/{securityreferences-1.8.11.md → securityreferences.1.8.11.md} +0 -0
- /package/docs/{securityresearch-1.8.11.md → securityresearch.1.8.11.md} +0 -0
- /package/docs/{signingresearch-1.8.12.md → signingresearch.1.8.12.md} +0 -0
- /package/docs/{tauri-glib-risk-1.8.11.md → tauri.glib.risk.1.8.11.md} +0 -0
- /package/docs/{todo-1.8.18.md → todo.1.8.18.md} +0 -0
- /package/docs/{todo-1.8.19.md → todo.1.8.19.md} +0 -0
- /package/docs/{toolchainresearch-1.8.9.md → toolchainresearch.1.8.9.md} +0 -0
- /package/docs/{web-duplicatecleanup.md → web.duplicatecleanup.md} +0 -0
- /package/docs/{web-ideas.md → web.ideas.md} +0 -0
- /package/docs/{workflowimprovements-2026-08-19.md → workflowimprovements.2026.08.19.md} +0 -0
package/dist/db.js
ADDED
|
@@ -0,0 +1,1080 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db.ts — node:sqlite data layer for the saddle web node (v7-BACK).
|
|
3
|
+
*
|
|
4
|
+
* the whole persistence surface of a node (main or clone) lives in this
|
|
5
|
+
* module: one sqlite database opened through the native node:sqlite
|
|
6
|
+
* DatabaseSync driver, the embedded migrations (executeschema), prepared
|
|
7
|
+
* statements memoized once per process and one small function api per
|
|
8
|
+
* table. the file is created with mode 0o600 right after open because it
|
|
9
|
+
* carries password hashes and session token hashes.
|
|
10
|
+
*
|
|
11
|
+
* contexts (13): dbopen, filemode, rowtypes, executeschema, users,
|
|
12
|
+
* sessions, nodes, sandboxes, sandboxfiles, events, audit, labels, counts.
|
|
13
|
+
*
|
|
14
|
+
* rules: lowercase identifiers, english jsdoc in third person, no emoji,
|
|
15
|
+
* try/catch on every fallible path with the standardized {code,message}
|
|
16
|
+
* error shape, node:* modules only, zero dependencies.
|
|
17
|
+
*/
|
|
18
|
+
import { chmodSync } from 'node:fs';
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
21
|
+
import { resolve } from 'node:path';
|
|
22
|
+
import process from 'node:process';
|
|
23
|
+
import { LABEL_REVISION, labelmanifest } from './labels.ts';
|
|
24
|
+
/* ------------------------------------------------------------------ */
|
|
25
|
+
/* context: dbopen and filemode */
|
|
26
|
+
/* ------------------------------------------------------------------ */
|
|
27
|
+
/**
|
|
28
|
+
* resolves the database file location once at import time: the saddle_db
|
|
29
|
+
* environment variable wins, then ./saddle.db relative to the current
|
|
30
|
+
* working directory. the special value ":memory:" keeps the database in
|
|
31
|
+
* ram (used by tests and throwaway boots).
|
|
32
|
+
*
|
|
33
|
+
* @returns the database path handed to databasesync.
|
|
34
|
+
*/
|
|
35
|
+
function resolvedbpath() {
|
|
36
|
+
try {
|
|
37
|
+
const fromenv = process.env.SADDLE_DB ?? '';
|
|
38
|
+
if (fromenv.length > 0) {
|
|
39
|
+
return fromenv;
|
|
40
|
+
}
|
|
41
|
+
return resolve('./saddle.db');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return resolve('./saddle.db');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** the database file path resolved once at module load. */
|
|
48
|
+
export const dbpath = resolvedbpath();
|
|
49
|
+
/**
|
|
50
|
+
* builds the standardized database error thrown by every wrapper below.
|
|
51
|
+
*
|
|
52
|
+
* @param code machine readable code prefixed with db-.
|
|
53
|
+
* @param message human readable explanation.
|
|
54
|
+
* @returns the decorated error.
|
|
55
|
+
*/
|
|
56
|
+
function dberror(code, message) {
|
|
57
|
+
// log internal technical details server-side only; the thrown error only
|
|
58
|
+
// exposes the machine-readable code so no underlying driver stack trace or
|
|
59
|
+
// sqlite error string can propagate to client responses (CodeQL
|
|
60
|
+
// js/stack-trace-exposure). the `message` parameter is preserved on a
|
|
61
|
+
// non-enumerable `internal` property for callers that need server-side
|
|
62
|
+
// diagnostics without exposing anything to the network.
|
|
63
|
+
if (typeof message === 'string' && message.length > 0) {
|
|
64
|
+
console.error(`[db] ${code}: ${message}`);
|
|
65
|
+
}
|
|
66
|
+
// public messages are static, hardcoded strings keyed by code; they are
|
|
67
|
+
// safe to surface in user-facing sandbox command output because they do
|
|
68
|
+
// not depend on the underlying driver or stack trace.
|
|
69
|
+
const publicmessages = {
|
|
70
|
+
'quota-exceeded': 'quota exceeded',
|
|
71
|
+
'invalid-quota': 'invalid quota',
|
|
72
|
+
'db-createuser-failed': 'database error',
|
|
73
|
+
'db-finduser-failed': 'database error',
|
|
74
|
+
'db-writefile-failed': 'database error',
|
|
75
|
+
'db-readfile-failed': 'database error',
|
|
76
|
+
'db-sandbox-failed': 'database error',
|
|
77
|
+
'db-session-failed': 'database error',
|
|
78
|
+
'db-event-failed': 'database error',
|
|
79
|
+
'db-audit-failed': 'database error',
|
|
80
|
+
'db-onboarding-failed': 'database error',
|
|
81
|
+
'db-label-failed': 'database error',
|
|
82
|
+
};
|
|
83
|
+
const publicmessage = publicmessages[code] ?? code;
|
|
84
|
+
const err = Object.assign(new Error(publicmessage), { code });
|
|
85
|
+
Object.defineProperty(err, 'internal', {
|
|
86
|
+
value: typeof message === 'string' ? message : '',
|
|
87
|
+
enumerable: false,
|
|
88
|
+
writable: false,
|
|
89
|
+
configurable: false,
|
|
90
|
+
});
|
|
91
|
+
Object.defineProperty(err, 'publicMessage', {
|
|
92
|
+
value: publicmessage,
|
|
93
|
+
enumerable: false,
|
|
94
|
+
writable: false,
|
|
95
|
+
configurable: false,
|
|
96
|
+
});
|
|
97
|
+
return err;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* opens the database, applies the pragma profile and tightens the file
|
|
101
|
+
* mode to 0o600 (skipped for the in-memory database).
|
|
102
|
+
*
|
|
103
|
+
* @param path the resolved database path.
|
|
104
|
+
* @returns the opened database.
|
|
105
|
+
*/
|
|
106
|
+
function opendatabase(path) {
|
|
107
|
+
const database = new DatabaseSync(path);
|
|
108
|
+
database.exec('pragma busy_timeout = 2000;');
|
|
109
|
+
if (path !== ':memory:') {
|
|
110
|
+
try {
|
|
111
|
+
chmodSync(path, 0o600);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* the mode is best effort on exotic filesystems */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return database;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* resolves the per-sandbox workspace quota in bytes once per call: the
|
|
121
|
+
* saddle_sandbox_quota_bytes environment variable wins (any positive
|
|
122
|
+
* integer), otherwise the 16 mib default documented in the readme.
|
|
123
|
+
*
|
|
124
|
+
* @returns the quota in bytes.
|
|
125
|
+
*/
|
|
126
|
+
export function sandboxquota() {
|
|
127
|
+
try {
|
|
128
|
+
const parsed = Number.parseInt(String(process.env.SADDLE_SANDBOX_QUOTA_BYTES ?? ''), 10);
|
|
129
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
130
|
+
return parsed;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* a malformed value falls back to the default */
|
|
135
|
+
}
|
|
136
|
+
return 16 * 1024 * 1024;
|
|
137
|
+
}
|
|
138
|
+
/* ------------------------------------------------------------------ */
|
|
139
|
+
/* context: executeschema — embedded migrations */
|
|
140
|
+
/* ------------------------------------------------------------------ */
|
|
141
|
+
/**
|
|
142
|
+
* the v7 schema plus the v10 sandboxfiles workspace table, the
|
|
143
|
+
* onboarding_states entry-flow table and the labels copy store: users,
|
|
144
|
+
* sessions, nodes, sandboxes (with the v10 files/bytes usage columns),
|
|
145
|
+
* sandboxfiles, events, audit, onboarding_states and labels. every table
|
|
146
|
+
* is created with "if not exists" so boots are idempotent and the same
|
|
147
|
+
* statements run against any node role.
|
|
148
|
+
*/
|
|
149
|
+
const schema = `
|
|
150
|
+
create table if not exists users (
|
|
151
|
+
id text primary key,
|
|
152
|
+
username text not null unique,
|
|
153
|
+
passwordhash text not null,
|
|
154
|
+
salt text not null,
|
|
155
|
+
role text not null default 'user',
|
|
156
|
+
createdat text not null,
|
|
157
|
+
lastlogin text
|
|
158
|
+
);
|
|
159
|
+
create table if not exists sessions (
|
|
160
|
+
tokenhash text primary key,
|
|
161
|
+
userid text not null,
|
|
162
|
+
createdat text not null,
|
|
163
|
+
expiresat text not null,
|
|
164
|
+
ip text,
|
|
165
|
+
useragent text
|
|
166
|
+
);
|
|
167
|
+
create index if not exists sessions_expiresat on sessions(expiresat);
|
|
168
|
+
create table if not exists nodes (
|
|
169
|
+
id text primary key,
|
|
170
|
+
url text not null,
|
|
171
|
+
rolename text,
|
|
172
|
+
region text,
|
|
173
|
+
lastheartbeat text,
|
|
174
|
+
status text,
|
|
175
|
+
registeredat text not null,
|
|
176
|
+
meta text
|
|
177
|
+
);
|
|
178
|
+
create table if not exists sandboxes (
|
|
179
|
+
id text primary key,
|
|
180
|
+
userid text,
|
|
181
|
+
model text,
|
|
182
|
+
vcpus integer,
|
|
183
|
+
ramgb integer,
|
|
184
|
+
gpu text,
|
|
185
|
+
state text,
|
|
186
|
+
createdat text,
|
|
187
|
+
expiresat text,
|
|
188
|
+
files integer,
|
|
189
|
+
bytes integer
|
|
190
|
+
);
|
|
191
|
+
create index if not exists sandboxes_userid on sandboxes(userid);
|
|
192
|
+
create table if not exists sandboxfiles (
|
|
193
|
+
id integer primary key autoincrement,
|
|
194
|
+
sandboxid text not null,
|
|
195
|
+
path text not null,
|
|
196
|
+
content blob,
|
|
197
|
+
size integer not null default 0,
|
|
198
|
+
updatedat text not null,
|
|
199
|
+
unique(sandboxid, path)
|
|
200
|
+
);
|
|
201
|
+
create index if not exists sandboxfiles_sandboxid on sandboxfiles(sandboxid);
|
|
202
|
+
create table if not exists events (
|
|
203
|
+
id integer primary key autoincrement,
|
|
204
|
+
topic text,
|
|
205
|
+
payload text,
|
|
206
|
+
nodeid text,
|
|
207
|
+
createdat text not null
|
|
208
|
+
);
|
|
209
|
+
create index if not exists events_createdat on events(createdat);
|
|
210
|
+
create table if not exists audit (
|
|
211
|
+
id integer primary key autoincrement,
|
|
212
|
+
userid text,
|
|
213
|
+
action text,
|
|
214
|
+
detail text,
|
|
215
|
+
ip text,
|
|
216
|
+
createdat text not null
|
|
217
|
+
);
|
|
218
|
+
create table if not exists onboarding_states (
|
|
219
|
+
id integer primary key autoincrement,
|
|
220
|
+
visitor text not null unique,
|
|
221
|
+
introseen integer not null default 0,
|
|
222
|
+
onboardingseen integer not null default 0,
|
|
223
|
+
updatedAt text not null default (datetime('now'))
|
|
224
|
+
);
|
|
225
|
+
create table if not exists labels (
|
|
226
|
+
id text primary key,
|
|
227
|
+
text text not null,
|
|
228
|
+
revision integer not null default 0,
|
|
229
|
+
updatedAt text not null default (datetime('now')),
|
|
230
|
+
translated integer not null default 0
|
|
231
|
+
);
|
|
232
|
+
`;
|
|
233
|
+
/**
|
|
234
|
+
* adds the v10 usage columns to pre-existing sandboxes tables; the
|
|
235
|
+
* "duplicate column name" failure of a second boot is swallowed so the
|
|
236
|
+
* migration is idempotent on any node age.
|
|
237
|
+
*
|
|
238
|
+
* @param target the database handle.
|
|
239
|
+
* @returns void.
|
|
240
|
+
*/
|
|
241
|
+
function migrateusagecolumns(target) {
|
|
242
|
+
for (const column of ['files', 'bytes', 'quotabytes']) {
|
|
243
|
+
try {
|
|
244
|
+
target.exec(`alter table sandboxes add column ${column} integer;`);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
/* the column already exists; the alter is best effort */
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/* ------------------------------------------------------------------ */
|
|
252
|
+
/* context: the connection and the prepared statement cache */
|
|
253
|
+
/* ------------------------------------------------------------------ */
|
|
254
|
+
/** the single database connection owned by this module. */
|
|
255
|
+
let database = opendatabase(dbpath);
|
|
256
|
+
/** memoized prepared statements keyed by their sql text. */
|
|
257
|
+
const statements = new Map();
|
|
258
|
+
/**
|
|
259
|
+
* returns the prepared statement for one sql text, preparing it once and
|
|
260
|
+
* reusing it for the whole process lifetime.
|
|
261
|
+
*
|
|
262
|
+
* @param sql the sql text.
|
|
263
|
+
* @returns the prepared statement.
|
|
264
|
+
*/
|
|
265
|
+
function stmt(sql) {
|
|
266
|
+
try {
|
|
267
|
+
let prepared = statements.get(sql);
|
|
268
|
+
if (prepared === undefined) {
|
|
269
|
+
prepared = database.prepare(sql);
|
|
270
|
+
statements.set(sql, prepared);
|
|
271
|
+
}
|
|
272
|
+
return prepared;
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
throw dberror('db-prepare-failed', `preparing "${sql.slice(0, 48)}..." failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/* ------------------------------------------------------------------ */
|
|
279
|
+
/* context: users */
|
|
280
|
+
/* ------------------------------------------------------------------ */
|
|
281
|
+
/**
|
|
282
|
+
* creates one user row; the id is a fresh uuid unless the caller pins
|
|
283
|
+
* one (the mesh forward path caches the main node's user rows with
|
|
284
|
+
* their original ids so clone sessions resolve).
|
|
285
|
+
*
|
|
286
|
+
* @param fields the user attributes.
|
|
287
|
+
* @returns the stored user projection.
|
|
288
|
+
*/
|
|
289
|
+
export function createuser(fields) {
|
|
290
|
+
try {
|
|
291
|
+
const id = fields.id ?? randomUUID();
|
|
292
|
+
const createdat = fields.createdat ?? new Date().toISOString();
|
|
293
|
+
stmt('insert into users (id, username, passwordhash, salt, role, createdat, lastlogin) values (?, ?, ?, ?, ?, ?, null)').run(id, fields.username, fields.passwordhash, fields.salt, fields.role ?? 'user', createdat);
|
|
294
|
+
return { id, username: fields.username, role: fields.role ?? 'user', createdat };
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
throw dberror('db-createuser-failed', error instanceof Error ? error.message : String(error));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* finds one user by username.
|
|
302
|
+
*
|
|
303
|
+
* @param username the exact username.
|
|
304
|
+
* @returns the full row or null when absent.
|
|
305
|
+
*/
|
|
306
|
+
export function finduserbyname(username) {
|
|
307
|
+
try {
|
|
308
|
+
return (stmt('select * from users where username = ?').get(username) ?? null);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
throw dberror('db-finduser-failed', error instanceof Error ? error.message : String(error));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* finds one user by id.
|
|
316
|
+
*
|
|
317
|
+
* @param id the user id.
|
|
318
|
+
* @returns the full row or null when absent.
|
|
319
|
+
*/
|
|
320
|
+
export function finduserbyid(id) {
|
|
321
|
+
try {
|
|
322
|
+
return stmt('select * from users where id = ?').get(id) ?? null;
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
throw dberror('db-finduser-failed', error instanceof Error ? error.message : String(error));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* lists every user ordered by creation time; the route layer projects
|
|
330
|
+
* out the password hash and salt columns.
|
|
331
|
+
*
|
|
332
|
+
* @returns the full user rows.
|
|
333
|
+
*/
|
|
334
|
+
export function listusers() {
|
|
335
|
+
try {
|
|
336
|
+
return stmt('select * from users order by createdat asc').all();
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
throw dberror('db-listusers-failed', error instanceof Error ? error.message : String(error));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* stamps the last login column with the given timestamp.
|
|
344
|
+
*
|
|
345
|
+
* @param id the user id.
|
|
346
|
+
* @param isotime the iso timestamp.
|
|
347
|
+
* @returns void.
|
|
348
|
+
*/
|
|
349
|
+
export function updatelastlogin(id, isotime) {
|
|
350
|
+
try {
|
|
351
|
+
stmt('update users set lastlogin = ? where id = ?').run(isotime, id);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
throw dberror('db-lastlogin-failed', error instanceof Error ? error.message : String(error));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/* ------------------------------------------------------------------ */
|
|
358
|
+
/* context: sessions */
|
|
359
|
+
/* ------------------------------------------------------------------ */
|
|
360
|
+
/**
|
|
361
|
+
* persists one session; only the sha256 token hash is stored, never the
|
|
362
|
+
* bearer token itself.
|
|
363
|
+
*
|
|
364
|
+
* @param fields the session attributes.
|
|
365
|
+
* @returns void.
|
|
366
|
+
*/
|
|
367
|
+
export function createsession(fields) {
|
|
368
|
+
try {
|
|
369
|
+
stmt('insert into sessions (tokenhash, userid, createdat, expiresat, ip, useragent) values (?, ?, ?, ?, ?, ?)').run(fields.tokenhash, fields.userid, fields.createdat ?? new Date().toISOString(), fields.expiresat, fields.ip ?? null, fields.useragent ?? null);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
throw dberror('db-createsession-failed', error instanceof Error ? error.message : String(error));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* finds one session row by token hash; expiry is enforced by the caller
|
|
377
|
+
* (auth.ts) so clones can forward remote sessions verbatim.
|
|
378
|
+
*
|
|
379
|
+
* @param tokenhash the sha256 hex of the bearer token.
|
|
380
|
+
* @returns the session row or null.
|
|
381
|
+
*/
|
|
382
|
+
export function findsession(tokenhash) {
|
|
383
|
+
try {
|
|
384
|
+
return (stmt('select * from sessions where tokenhash = ?').get(tokenhash) ??
|
|
385
|
+
null);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
throw dberror('db-findsession-failed', error instanceof Error ? error.message : String(error));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* deletes one session by token hash.
|
|
393
|
+
*
|
|
394
|
+
* @param tokenhash the sha256 hex of the bearer token.
|
|
395
|
+
* @returns true when a row was removed.
|
|
396
|
+
*/
|
|
397
|
+
export function deletesession(tokenhash) {
|
|
398
|
+
try {
|
|
399
|
+
const result = stmt('delete from sessions where tokenhash = ?').run(tokenhash);
|
|
400
|
+
return Number(result.changes) > 0;
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
throw dberror('db-deletesession-failed', error instanceof Error ? error.message : String(error));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* removes every expired session (iso strings compare lexicographically).
|
|
408
|
+
* the main node schedules this sweep at boot.
|
|
409
|
+
*
|
|
410
|
+
* @returns the count of removed rows.
|
|
411
|
+
*/
|
|
412
|
+
export function cleansessions() {
|
|
413
|
+
try {
|
|
414
|
+
const now = new Date().toISOString();
|
|
415
|
+
const result = stmt('delete from sessions where expiresat < ?').run(now);
|
|
416
|
+
return Number(result.changes);
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
throw dberror('db-cleansessions-failed', error instanceof Error ? error.message : String(error));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/* ------------------------------------------------------------------ */
|
|
423
|
+
/* context: nodes (the mesh registry) */
|
|
424
|
+
/* ------------------------------------------------------------------ */
|
|
425
|
+
/**
|
|
426
|
+
* registers or re-registers one node keyed by its url: an existing row
|
|
427
|
+
* for the same url is refreshed (new heartbeat, online status) instead
|
|
428
|
+
* of duplicated, so clones can re-register after a restart.
|
|
429
|
+
*
|
|
430
|
+
* @param fields the node announcement.
|
|
431
|
+
* @returns the registry row.
|
|
432
|
+
*/
|
|
433
|
+
export function registernode(fields) {
|
|
434
|
+
try {
|
|
435
|
+
const now = new Date().toISOString();
|
|
436
|
+
const existing = stmt('select id from nodes where url = ?').get(fields.url) ??
|
|
437
|
+
null;
|
|
438
|
+
if (existing !== null) {
|
|
439
|
+
stmt('update nodes set rolename = ?, region = ?, lastheartbeat = ?, status = ?, meta = ? where id = ?').run(fields.rolename ?? 'clone', fields.region ?? null, now, 'online', fields.meta ?? null, existing.id);
|
|
440
|
+
return { id: existing.id, url: fields.url, status: 'online' };
|
|
441
|
+
}
|
|
442
|
+
const id = randomUUID();
|
|
443
|
+
stmt('insert into nodes (id, url, rolename, region, lastheartbeat, status, registeredat, meta) values (?, ?, ?, ?, ?, ?, ?, ?)').run(id, fields.url, fields.rolename ?? 'clone', fields.region ?? null, now, 'online', now, fields.meta ?? null);
|
|
444
|
+
return { id, url: fields.url, status: 'online' };
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
throw dberror('db-registernode-failed', error instanceof Error ? error.message : String(error));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* stamps one node heartbeat.
|
|
452
|
+
*
|
|
453
|
+
* @param id the node id.
|
|
454
|
+
* @param isotime the heartbeat timestamp.
|
|
455
|
+
* @returns true when the node id exists.
|
|
456
|
+
*/
|
|
457
|
+
export function heartbeatnode(id, isotime) {
|
|
458
|
+
try {
|
|
459
|
+
const result = stmt('update nodes set lastheartbeat = ?, status = ? where id = ?').run(isotime, 'online', id);
|
|
460
|
+
return Number(result.changes) > 0;
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
throw dberror('db-heartbeat-failed', error instanceof Error ? error.message : String(error));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* lists every registered node ordered by registration time.
|
|
468
|
+
*
|
|
469
|
+
* @returns the node rows.
|
|
470
|
+
*/
|
|
471
|
+
export function listnodes() {
|
|
472
|
+
try {
|
|
473
|
+
return stmt('select * from nodes order by registeredat asc').all();
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
throw dberror('db-listnodes-failed', error instanceof Error ? error.message : String(error));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/* ------------------------------------------------------------------ */
|
|
480
|
+
/* context: sandboxes */
|
|
481
|
+
/* ------------------------------------------------------------------ */
|
|
482
|
+
/**
|
|
483
|
+
* persists one sandbox lifecycle row mirroring the in-memory record.
|
|
484
|
+
*
|
|
485
|
+
* @param fields the sandbox projection.
|
|
486
|
+
* @returns void.
|
|
487
|
+
*/
|
|
488
|
+
export function createsandboxrecord(fields) {
|
|
489
|
+
try {
|
|
490
|
+
stmt('insert into sandboxes (id, userid, model, vcpus, ramgb, gpu, state, createdat, expiresat, quotabytes) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').run(fields.id, fields.userid ?? null, fields.model ?? null, fields.vcpus ?? null, fields.ramgb ?? null, fields.gpu ?? null, fields.state, fields.createdat ?? new Date().toISOString(), fields.expiresat ?? null, fields.quotabytes ?? null);
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
throw dberror('db-createsandbox-failed', error instanceof Error ? error.message : String(error));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* updates the state column of one sandbox row (running, destroyed, ...).
|
|
498
|
+
*
|
|
499
|
+
* @param id the sandbox id.
|
|
500
|
+
* @param state the new state.
|
|
501
|
+
* @returns void.
|
|
502
|
+
*/
|
|
503
|
+
export function updatesandboxstate(id, state) {
|
|
504
|
+
try {
|
|
505
|
+
stmt('update sandboxes set state = ? where id = ?').run(state, id);
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
throw dberror('db-updatesandbox-failed', error instanceof Error ? error.message : String(error));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* lists every sandbox row, newest first.
|
|
513
|
+
*
|
|
514
|
+
* @param limit the row cap.
|
|
515
|
+
* @returns the sandbox rows.
|
|
516
|
+
*/
|
|
517
|
+
export function listsandboxes(limit = 200) {
|
|
518
|
+
try {
|
|
519
|
+
return stmt('select * from sandboxes order by createdat desc limit ?').all(limit);
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
throw dberror('db-listsandboxes-failed', error instanceof Error ? error.message : String(error));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* lists the sandbox rows owned by one user, newest first.
|
|
527
|
+
*
|
|
528
|
+
* @param userid the owner id.
|
|
529
|
+
* @param limit the row cap.
|
|
530
|
+
* @returns the sandbox rows.
|
|
531
|
+
*/
|
|
532
|
+
export function listsandboxesbyuser(userid, limit = 200) {
|
|
533
|
+
try {
|
|
534
|
+
return stmt('select * from sandboxes where userid = ? order by createdat desc limit ?').all(userid, limit);
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
throw dberror('db-listsandboxes-failed', error instanceof Error ? error.message : String(error));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* finds one sandbox row by id; the files surface resolves ownership
|
|
542
|
+
* through this lookup even when the in-memory record is gone (expired
|
|
543
|
+
* sandbox or restarted process).
|
|
544
|
+
*
|
|
545
|
+
* @param id the sandbox id.
|
|
546
|
+
* @returns the sandbox row or null.
|
|
547
|
+
*/
|
|
548
|
+
export function findsandboxbyid(id) {
|
|
549
|
+
try {
|
|
550
|
+
return stmt('select * from sandboxes where id = ?').get(id) ?? null;
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
throw dberror('db-findsandbox-failed', error instanceof Error ? error.message : String(error));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* stamps the workspace usage counters of one sandbox row.
|
|
558
|
+
*
|
|
559
|
+
* @param id the sandbox id.
|
|
560
|
+
* @param files the file count.
|
|
561
|
+
* @param bytes the total content bytes.
|
|
562
|
+
* @returns void.
|
|
563
|
+
*/
|
|
564
|
+
export function updatesandboxusage(id, files, bytes) {
|
|
565
|
+
try {
|
|
566
|
+
stmt('update sandboxes set files = ?, bytes = ? where id = ?').run(Number(files) || 0, Number(bytes) || 0, id);
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
throw dberror('db-updatesandboxusage-failed', error instanceof Error ? error.message : String(error));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/* ------------------------------------------------------------------ */
|
|
573
|
+
/* context: sandboxfiles — the persistent workspace */
|
|
574
|
+
/* ------------------------------------------------------------------ */
|
|
575
|
+
/**
|
|
576
|
+
* decodes one stored content cell (node:sqlite may hand back a
|
|
577
|
+
* uint8array for blob columns) into a utf8 string.
|
|
578
|
+
*
|
|
579
|
+
* @param value the stored cell.
|
|
580
|
+
* @returns the decoded text.
|
|
581
|
+
*/
|
|
582
|
+
function decodecontent(value) {
|
|
583
|
+
if (value instanceof Uint8Array) {
|
|
584
|
+
return Buffer.from(value).toString('utf8');
|
|
585
|
+
}
|
|
586
|
+
if (value === null || value === undefined) {
|
|
587
|
+
return '';
|
|
588
|
+
}
|
|
589
|
+
return String(value);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* writes (or overwrites) one workspace file; the sandbox quota caps the
|
|
593
|
+
* summed size of every file of the sandbox and an over-quota write
|
|
594
|
+
* throws the standardized "quota exceeded" error without touching the
|
|
595
|
+
* stored rows.
|
|
596
|
+
*
|
|
597
|
+
* @param sandboxid the sandbox id.
|
|
598
|
+
* @param path the normalized absolute path.
|
|
599
|
+
* @param content the file text (null and undefined write as empty).
|
|
600
|
+
* @param customquota the optional per-sandbox quota in bytes, always
|
|
601
|
+
* capped by the node maximum.
|
|
602
|
+
* @returns the write receipt.
|
|
603
|
+
*/
|
|
604
|
+
export function writefile(sandboxid, path, content, customquota) {
|
|
605
|
+
try {
|
|
606
|
+
const text = content === null || content === undefined ? '' : String(content);
|
|
607
|
+
const size = Buffer.byteLength(text, 'utf8');
|
|
608
|
+
const nodemax = sandboxquota();
|
|
609
|
+
// per sandbox quota chosen at creation (4-256 MiB), always capped by
|
|
610
|
+
// the node level maximum so a single id can never exceed the operator
|
|
611
|
+
// budget; omitting it keeps the node default.
|
|
612
|
+
const quota = customquota === undefined ? nodemax : Math.min(nodemax, Math.max(4 * 1024 * 1024, Number(customquota)));
|
|
613
|
+
const existingrow = stmt('select size from sandboxfiles where sandboxid = ? and path = ?').get(sandboxid, path);
|
|
614
|
+
const previous = existingrow === undefined ? 0 : Number(existingrow.size) || 0;
|
|
615
|
+
const usage = sandboxusage(sandboxid);
|
|
616
|
+
if (usage.bytes - previous + size > quota) {
|
|
617
|
+
throw dberror('quota-exceeded', `quota exceeded: sandbox ${sandboxid} is capped at ${quota} bytes (usage ${usage.bytes - previous + size} bytes)`);
|
|
618
|
+
}
|
|
619
|
+
const updatedat = new Date().toISOString();
|
|
620
|
+
stmt('insert into sandboxfiles (sandboxid, path, content, size, updatedat) values (?, ?, ?, ?, ?) ' +
|
|
621
|
+
'on conflict(sandboxid, path) do update set content = excluded.content, size = excluded.size, updatedat = excluded.updatedat').run(sandboxid, path, text, size, updatedat);
|
|
622
|
+
return { path, size, updatedat };
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
if (error instanceof Error && error.code === 'quota-exceeded') {
|
|
626
|
+
throw error;
|
|
627
|
+
}
|
|
628
|
+
throw dberror('db-writefile-failed', error instanceof Error ? error.message : String(error));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* reads one workspace file.
|
|
633
|
+
*
|
|
634
|
+
* @param sandboxid the sandbox id.
|
|
635
|
+
* @param path the normalized absolute path.
|
|
636
|
+
* @returns the file document or null when absent.
|
|
637
|
+
*/
|
|
638
|
+
export function readfile(sandboxid, path) {
|
|
639
|
+
try {
|
|
640
|
+
const row = stmt('select path, content, size, updatedat from sandboxfiles where sandboxid = ? and path = ?').get(sandboxid, path);
|
|
641
|
+
if (row === undefined) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
const content = decodecontent(row.content);
|
|
645
|
+
return {
|
|
646
|
+
path: String(row.path),
|
|
647
|
+
content,
|
|
648
|
+
size: Number(row.size) || Buffer.byteLength(content, 'utf8'),
|
|
649
|
+
updatedat: String(row.updatedat),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
throw dberror('db-readfile-failed', error instanceof Error ? error.message : String(error));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* lists every workspace file of one sandbox ordered by path; contents
|
|
658
|
+
* stay out of the projection.
|
|
659
|
+
*
|
|
660
|
+
* @param sandboxid the sandbox id.
|
|
661
|
+
* @returns the file rows.
|
|
662
|
+
*/
|
|
663
|
+
export function listfiles(sandboxid) {
|
|
664
|
+
try {
|
|
665
|
+
return stmt('select path, size, updatedat from sandboxfiles where sandboxid = ? order by path asc')
|
|
666
|
+
.all(sandboxid)
|
|
667
|
+
.map((row) => ({
|
|
668
|
+
path: String(row.path),
|
|
669
|
+
size: Number(row.size) || 0,
|
|
670
|
+
updatedat: String(row.updatedat),
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
throw dberror('db-listfiles-failed', error instanceof Error ? error.message : String(error));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* deletes one workspace file.
|
|
679
|
+
*
|
|
680
|
+
* @param sandboxid the sandbox id.
|
|
681
|
+
* @param path the normalized absolute path.
|
|
682
|
+
* @returns true when a row was removed.
|
|
683
|
+
*/
|
|
684
|
+
export function deletefile(sandboxid, path) {
|
|
685
|
+
try {
|
|
686
|
+
const result = stmt('delete from sandboxfiles where sandboxid = ? and path = ?').run(sandboxid, path);
|
|
687
|
+
return Number(result.changes) > 0;
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
throw dberror('db-deletefile-failed', error instanceof Error ? error.message : String(error));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* removes every workspace file of one sandbox; the purge path of the
|
|
695
|
+
* delete endpoint calls this so a destroyed sandbox leaves nothing
|
|
696
|
+
* behind.
|
|
697
|
+
*
|
|
698
|
+
* @param sandboxid the sandbox id.
|
|
699
|
+
* @returns the count of removed rows.
|
|
700
|
+
*/
|
|
701
|
+
export function deletesandboxfiles(sandboxid) {
|
|
702
|
+
try {
|
|
703
|
+
const result = stmt('delete from sandboxfiles where sandboxid = ?').run(sandboxid);
|
|
704
|
+
return Number(result.changes);
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
throw dberror('db-deletesandboxfiles-failed', error instanceof Error ? error.message : String(error));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* sums the workspace usage of one sandbox.
|
|
712
|
+
*
|
|
713
|
+
* @param sandboxid the sandbox id.
|
|
714
|
+
* @returns the usage counters.
|
|
715
|
+
*/
|
|
716
|
+
export function sandboxusage(sandboxid) {
|
|
717
|
+
try {
|
|
718
|
+
const row = stmt('select count(*) as files, coalesce(sum(size), 0) as bytes from sandboxfiles where sandboxid = ?').get(sandboxid);
|
|
719
|
+
return { files: Number(row?.files ?? 0), bytes: Number(row?.bytes ?? 0) };
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
throw dberror('db-sandboxusage-failed', error instanceof Error ? error.message : String(error));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/* ------------------------------------------------------------------ */
|
|
726
|
+
/* context: events (the durable local bus) */
|
|
727
|
+
/* ------------------------------------------------------------------ */
|
|
728
|
+
/**
|
|
729
|
+
* appends one event row; the payload is stored as a json string.
|
|
730
|
+
*
|
|
731
|
+
* @param fields the event envelope.
|
|
732
|
+
* @returns the inserted event id.
|
|
733
|
+
*/
|
|
734
|
+
export function addevent(fields) {
|
|
735
|
+
try {
|
|
736
|
+
let payload = null;
|
|
737
|
+
if (fields.payload !== undefined) {
|
|
738
|
+
payload = typeof fields.payload === 'string' ? fields.payload : JSON.stringify(fields.payload);
|
|
739
|
+
}
|
|
740
|
+
const result = stmt('insert into events (topic, payload, nodeid, createdat) values (?, ?, ?, ?)').run(fields.topic, payload, fields.nodeid ?? null, fields.createdat ?? new Date().toISOString());
|
|
741
|
+
return Number(result.lastInsertRowid);
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
throw dberror('db-addevent-failed', error instanceof Error ? error.message : String(error));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* lists the events with id greater than the cursor, oldest first, so
|
|
749
|
+
* the dashboard can poll /api/v1/events?since=<lastid>.
|
|
750
|
+
*
|
|
751
|
+
* @param since the exclusive id cursor.
|
|
752
|
+
* @param limit the row cap.
|
|
753
|
+
* @returns the event rows.
|
|
754
|
+
*/
|
|
755
|
+
export function listevents(since = 0, limit = 100) {
|
|
756
|
+
try {
|
|
757
|
+
return stmt('select * from events where id > ? order by id asc limit ?').all(since, limit);
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
throw dberror('db-listevents-failed', error instanceof Error ? error.message : String(error));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
/* ------------------------------------------------------------------ */
|
|
764
|
+
/* context: audit */
|
|
765
|
+
/* ------------------------------------------------------------------ */
|
|
766
|
+
/**
|
|
767
|
+
* appends one audit row (register, login, logout, sandbox lifecycle,
|
|
768
|
+
* mesh registration).
|
|
769
|
+
*
|
|
770
|
+
* @param fields the audit envelope.
|
|
771
|
+
* @returns the inserted audit id.
|
|
772
|
+
*/
|
|
773
|
+
export function addaudit(fields) {
|
|
774
|
+
try {
|
|
775
|
+
const result = stmt('insert into audit (userid, action, detail, ip, createdat) values (?, ?, ?, ?, ?)').run(fields.userid ?? null, fields.action, fields.detail ?? null, fields.ip ?? null, fields.createdat ?? new Date().toISOString());
|
|
776
|
+
return Number(result.lastInsertRowid);
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
throw dberror('db-addaudit-failed', error instanceof Error ? error.message : String(error));
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* lists the audit trail, newest first.
|
|
784
|
+
*
|
|
785
|
+
* @param limit the row cap.
|
|
786
|
+
* @returns the audit rows.
|
|
787
|
+
*/
|
|
788
|
+
export function listaudit(limit = 200) {
|
|
789
|
+
try {
|
|
790
|
+
return stmt('select * from audit order by id desc limit ?').all(limit);
|
|
791
|
+
}
|
|
792
|
+
catch (error) {
|
|
793
|
+
throw dberror('db-listaudit-failed', error instanceof Error ? error.message : String(error));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/* ------------------------------------------------------------------ */
|
|
797
|
+
/* context: onboarding — the entry-flow state */
|
|
798
|
+
/* ------------------------------------------------------------------ */
|
|
799
|
+
/**
|
|
800
|
+
* reads the onboarding stage flags of one visitor (parameterized; null
|
|
801
|
+
* when the visitor never touched the flow). the platform gateway serves
|
|
802
|
+
* these over GET /api/v1/onboarding for the browser accessor
|
|
803
|
+
* onboardingstate.ts.
|
|
804
|
+
*
|
|
805
|
+
* @param visitor the stable visitor id.
|
|
806
|
+
* @returns the flags, or null when no row exists.
|
|
807
|
+
*/
|
|
808
|
+
export function getonboardingstate(visitor) {
|
|
809
|
+
try {
|
|
810
|
+
const row = stmt('select introseen, onboardingseen from onboarding_states where visitor = ?').get(visitor);
|
|
811
|
+
if (row === undefined)
|
|
812
|
+
return null;
|
|
813
|
+
return {
|
|
814
|
+
introseen: Number(row.introseen) === 1,
|
|
815
|
+
onboardingseen: Number(row.onboardingseen) === 1,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
catch (error) {
|
|
819
|
+
throw dberror('db-onboarding-failed', error instanceof Error ? error.message : String(error));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* marks one onboarding stage seen for a visitor — idempotent seen-wins
|
|
824
|
+
* upsert: the flags only ever move forward, so a replayed stage (or an
|
|
825
|
+
* out-of-order gateway write) never unsees an earlier one.
|
|
826
|
+
*
|
|
827
|
+
* @param visitor the stable visitor id.
|
|
828
|
+
* @param stage the stage being marked seen ("intro" or "onboarding"; the
|
|
829
|
+
* onboarding stage implies the intro — the flow order).
|
|
830
|
+
* @returns the flags as they stand after the write.
|
|
831
|
+
*/
|
|
832
|
+
export function setonboardingstage(visitor, stage) {
|
|
833
|
+
try {
|
|
834
|
+
stmt(`insert into onboarding_states (visitor, introseen, onboardingseen, updatedAt)
|
|
835
|
+
values (?, ?, ?, ?)
|
|
836
|
+
on conflict(visitor) do update set
|
|
837
|
+
introseen = max(introseen, excluded.introseen),
|
|
838
|
+
onboardingseen = max(onboardingseen, excluded.onboardingseen),
|
|
839
|
+
updatedAt = excluded.updatedAt`).run(visitor, stage === 'intro' ? 1 : 0, stage === 'onboarding' ? 1 : 0, new Date().toISOString());
|
|
840
|
+
return getonboardingstate(visitor) ?? { introseen: false, onboardingseen: false };
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
throw dberror('db-onboarding-failed', error instanceof Error ? error.message : String(error));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/* ------------------------------------------------------------------ */
|
|
847
|
+
/* context: labels — the interface copy store */
|
|
848
|
+
/* ------------------------------------------------------------------ */
|
|
849
|
+
/** projects one raw labels row into the stored label projection. */
|
|
850
|
+
function labelrow(row) {
|
|
851
|
+
return {
|
|
852
|
+
id: row.id,
|
|
853
|
+
text: row.text,
|
|
854
|
+
revision: row.revision,
|
|
855
|
+
updatedAt: row.updatedAt,
|
|
856
|
+
...(row.translated === 1 ? { translated: true } : {}),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* writes one label row (idempotent upsert; the caller owns the revision).
|
|
861
|
+
*
|
|
862
|
+
* @param id the page.widget.key path of the label.
|
|
863
|
+
* @param text the surface text.
|
|
864
|
+
* @param revision the manifest revision the text carries.
|
|
865
|
+
* @param translated true when the row carries a runtime translation.
|
|
866
|
+
* @returns the stored label projection.
|
|
867
|
+
*/
|
|
868
|
+
export function savelabel(id, text, revision, translated = false) {
|
|
869
|
+
try {
|
|
870
|
+
stmt(`insert into labels (id, text, revision, updatedAt, translated) values (?, ?, ?, datetime('now'), ?)
|
|
871
|
+
on conflict(id) do update set
|
|
872
|
+
text = excluded.text,
|
|
873
|
+
revision = excluded.revision,
|
|
874
|
+
updatedAt = excluded.updatedAt,
|
|
875
|
+
translated = excluded.translated`).run(id, text, revision, translated ? 1 : 0);
|
|
876
|
+
const row = stmt('select id, text, revision, updatedAt, translated from labels where id = ?').get(id);
|
|
877
|
+
if (row === undefined) {
|
|
878
|
+
throw dberror('db-label-failed', `label "${id}" write did not land`);
|
|
879
|
+
}
|
|
880
|
+
return labelrow(row);
|
|
881
|
+
}
|
|
882
|
+
catch (error) {
|
|
883
|
+
if (error instanceof Error && error.code === 'db-label-failed') {
|
|
884
|
+
throw error;
|
|
885
|
+
}
|
|
886
|
+
throw dberror('db-label-failed', error instanceof Error ? error.message : String(error));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* reads one label row by its page.widget.key path.
|
|
891
|
+
*
|
|
892
|
+
* @param id the label id.
|
|
893
|
+
* @returns the row or null when absent.
|
|
894
|
+
*/
|
|
895
|
+
export function readlabel(id) {
|
|
896
|
+
try {
|
|
897
|
+
const row = stmt('select id, text, revision, updatedAt, translated from labels where id = ?').get(id);
|
|
898
|
+
return row === undefined ? null : labelrow(row);
|
|
899
|
+
}
|
|
900
|
+
catch (error) {
|
|
901
|
+
throw dberror('db-label-failed', error instanceof Error ? error.message : String(error));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* reads every label row of the store ordered by id.
|
|
906
|
+
*
|
|
907
|
+
* @returns the label rows.
|
|
908
|
+
*/
|
|
909
|
+
export function readalllabels() {
|
|
910
|
+
try {
|
|
911
|
+
return stmt('select id, text, revision, updatedAt, translated from labels order by id asc')
|
|
912
|
+
.all().map(labelrow);
|
|
913
|
+
}
|
|
914
|
+
catch (error) {
|
|
915
|
+
throw dberror('db-label-failed', error instanceof Error ? error.message : String(error));
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* ingests the english manifest of labels.ts into the labels table — the
|
|
920
|
+
* db-side twin of the browser seedlabels: idempotent, the changed rows
|
|
921
|
+
* refresh, and the translated rows stay protected from the overwrite.
|
|
922
|
+
*
|
|
923
|
+
* @returns the seeded and protected row counts.
|
|
924
|
+
*/
|
|
925
|
+
export async function seedlabelrows() {
|
|
926
|
+
try {
|
|
927
|
+
executeschema();
|
|
928
|
+
const rows = new Map(readalllabels().map((row) => [row.id, row]));
|
|
929
|
+
let seeded = 0;
|
|
930
|
+
let saved = 0;
|
|
931
|
+
for (const [id, text] of Object.entries(labelmanifest)) {
|
|
932
|
+
const row = rows.get(id);
|
|
933
|
+
if (row === undefined || (row.translated !== true && row.text !== text)) {
|
|
934
|
+
savelabel(id, text, LABEL_REVISION, row?.translated ?? false);
|
|
935
|
+
seeded += 1;
|
|
936
|
+
}
|
|
937
|
+
saved += 1;
|
|
938
|
+
}
|
|
939
|
+
return { seeded, protected: saved - seeded };
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
throw dberror('db-label-failed', error instanceof Error ? error.message : String(error));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/* ------------------------------------------------------------------ */
|
|
946
|
+
/* context: counts and health */
|
|
947
|
+
/* ------------------------------------------------------------------ */
|
|
948
|
+
/**
|
|
949
|
+
* counts one table with a bound fallback of zero on any failure.
|
|
950
|
+
*
|
|
951
|
+
* @param table one of users, sessions, nodes, sandboxes, events.
|
|
952
|
+
* @returns the row count.
|
|
953
|
+
*/
|
|
954
|
+
function count(table) {
|
|
955
|
+
const allowed = new Set(['users', 'sessions', 'nodes', 'sandboxes', 'events', 'audit']);
|
|
956
|
+
if (!allowed.has(table)) {
|
|
957
|
+
throw dberror('db-count-failed', `refusing to count unknown table "${table}"`);
|
|
958
|
+
}
|
|
959
|
+
const row = stmt(`select count(*) as total from ${table}`).get();
|
|
960
|
+
return Number(row?.total ?? 0);
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* the overview counts consumed by get /api/v1/admin/overview.
|
|
964
|
+
*
|
|
965
|
+
* @returns the counts.
|
|
966
|
+
*/
|
|
967
|
+
export function counts() {
|
|
968
|
+
try {
|
|
969
|
+
return {
|
|
970
|
+
users: count('users'),
|
|
971
|
+
sessions: count('sessions'),
|
|
972
|
+
nodes: count('nodes'),
|
|
973
|
+
sandboxes: count('sandboxes'),
|
|
974
|
+
events: count('events'),
|
|
975
|
+
audit: count('audit'),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
catch (error) {
|
|
979
|
+
throw dberror('db-counts-failed', error instanceof Error ? error.message : String(error));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* runs the cheapest possible query so /api/v1/health can report whether
|
|
984
|
+
* the database connection answers.
|
|
985
|
+
*
|
|
986
|
+
* @returns true when the round trip succeeds.
|
|
987
|
+
*/
|
|
988
|
+
export function healthcheck() {
|
|
989
|
+
try {
|
|
990
|
+
return stmt('select 1 as ok').get()?.ok === 1;
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
/* ------------------------------------------------------------------ */
|
|
997
|
+
/* context: the singleton */
|
|
998
|
+
/* ------------------------------------------------------------------ */
|
|
999
|
+
/**
|
|
1000
|
+
* applies the embedded schema migrations; exported for tools and tests
|
|
1001
|
+
* that want to reset or verify the schema explicitly.
|
|
1002
|
+
*
|
|
1003
|
+
* @param target an optional database handle (defaults to the module
|
|
1004
|
+
* connection).
|
|
1005
|
+
* @returns void.
|
|
1006
|
+
*/
|
|
1007
|
+
export function executeschema(target = database) {
|
|
1008
|
+
try {
|
|
1009
|
+
target.exec(schema);
|
|
1010
|
+
migrateusagecolumns(target);
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
throw dberror('db-schema-failed', error instanceof Error ? error.message : String(error));
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
/** the store singleton: every api function plus close and dispose. */
|
|
1017
|
+
const store = {
|
|
1018
|
+
dbpath,
|
|
1019
|
+
createuser,
|
|
1020
|
+
finduserbyname,
|
|
1021
|
+
finduserbyid,
|
|
1022
|
+
listusers,
|
|
1023
|
+
updatelastlogin,
|
|
1024
|
+
createsession,
|
|
1025
|
+
findsession,
|
|
1026
|
+
deletesession,
|
|
1027
|
+
cleansessions,
|
|
1028
|
+
registernode,
|
|
1029
|
+
heartbeatnode,
|
|
1030
|
+
listnodes,
|
|
1031
|
+
createsandboxrecord,
|
|
1032
|
+
updatesandboxstate,
|
|
1033
|
+
updatesandboxusage,
|
|
1034
|
+
findsandboxbyid,
|
|
1035
|
+
listsandboxes,
|
|
1036
|
+
listsandboxesbyuser,
|
|
1037
|
+
writefile,
|
|
1038
|
+
readfile,
|
|
1039
|
+
listfiles,
|
|
1040
|
+
deletefile,
|
|
1041
|
+
deletesandboxfiles,
|
|
1042
|
+
sandboxusage,
|
|
1043
|
+
sandboxquota,
|
|
1044
|
+
addevent,
|
|
1045
|
+
listevents,
|
|
1046
|
+
addaudit,
|
|
1047
|
+
listaudit,
|
|
1048
|
+
getonboardingstate,
|
|
1049
|
+
setonboardingstage,
|
|
1050
|
+
savelabel,
|
|
1051
|
+
readlabel,
|
|
1052
|
+
readalllabels,
|
|
1053
|
+
seedlabelrows,
|
|
1054
|
+
counts,
|
|
1055
|
+
healthcheck,
|
|
1056
|
+
executeschema,
|
|
1057
|
+
/** closes the connection (idempotent). */
|
|
1058
|
+
close() {
|
|
1059
|
+
try {
|
|
1060
|
+
database.close();
|
|
1061
|
+
}
|
|
1062
|
+
catch {
|
|
1063
|
+
/* closing twice or after dispose is a no-op */
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
/** explicit resource management: `using` support for node >= 24. */
|
|
1067
|
+
[Symbol.dispose]() {
|
|
1068
|
+
this.close();
|
|
1069
|
+
},
|
|
1070
|
+
};
|
|
1071
|
+
/* the schema runs at import time so the very first request finds the
|
|
1072
|
+
* tables in place; a failure here is fatal by design (bad path, locked
|
|
1073
|
+
* file) and surfaces as a boot error instead of a runtime 500. */
|
|
1074
|
+
executeschema();
|
|
1075
|
+
/* the copy law rides the db boot: the label seed ingests the english
|
|
1076
|
+
* manifest of labels.ts beside the schema (fire and forget with the
|
|
1077
|
+
* catch — the interface strings land in the store, the translated rows
|
|
1078
|
+
* protected from the overwrite). */
|
|
1079
|
+
void seedlabelrows().catch(() => { });
|
|
1080
|
+
export default store;
|