@camstack/system 1.0.2
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/dist/addon/addon-api-factory.d.ts +35 -0
- package/dist/addon-routes/addon-route-registry.d.ts +37 -0
- package/dist/addon-runner.js +599 -0
- package/dist/addon-runner.mjs +597 -0
- package/dist/auth/api-key-manager.d.ts +26 -0
- package/dist/auth/auth-manager.d.ts +109 -0
- package/dist/auth/parse-record.d.ts +18 -0
- package/dist/auth/scope-matcher.d.ts +7 -0
- package/dist/auth/scoped-token-manager.d.ts +40 -0
- package/dist/auth/totp-manager.d.ts +51 -0
- package/dist/auth/user-manager.d.ts +34 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.d.ts +53 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +259 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +251 -0
- package/dist/builtins/addon-pages-aggregator/dedupe-pages.d.ts +6 -0
- package/dist/builtins/addon-pages-aggregator/index.d.ts +1 -0
- package/dist/builtins/addon-pages-aggregator/index.js +8 -0
- package/dist/builtins/addon-pages-aggregator/index.mjs +2 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts +47 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +228 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +220 -0
- package/dist/builtins/addon-widgets-aggregator/index.d.ts +1 -0
- package/dist/builtins/addon-widgets-aggregator/index.js +8 -0
- package/dist/builtins/addon-widgets-aggregator/index.mjs +2 -0
- package/dist/builtins/alerts/alerts.addon.d.ts +81 -0
- package/dist/builtins/alerts/alerts.addon.js +601 -0
- package/dist/builtins/alerts/alerts.addon.mjs +595 -0
- package/dist/builtins/alerts/index.d.ts +1 -0
- package/dist/builtins/alerts/index.js +4 -0
- package/dist/builtins/alerts/index.mjs +2 -0
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +147 -0
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2229 -0
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2220 -0
- package/dist/builtins/backup-orchestrator/cron-helpers.d.ts +23 -0
- package/dist/builtins/backup-orchestrator/destination-policy.d.ts +72 -0
- package/dist/builtins/backup-orchestrator/download-helpers.d.ts +12 -0
- package/dist/builtins/backup-orchestrator/index.d.ts +2 -0
- package/dist/builtins/backup-orchestrator/index.js +8 -0
- package/dist/builtins/backup-orchestrator/index.mjs +2 -0
- package/dist/builtins/backup-orchestrator/manifest-store.d.ts +77 -0
- package/dist/builtins/console-logging/console-destination.d.ts +13 -0
- package/dist/builtins/console-logging/console-logging.addon.d.ts +25 -0
- package/dist/builtins/console-logging/index.d.ts +3 -0
- package/dist/builtins/console-logging/index.js +104 -0
- package/dist/builtins/console-logging/index.mjs +95 -0
- package/dist/builtins/device-manager/device-config-contribution.d.ts +32 -0
- package/dist/builtins/device-manager/device-event-propagator.d.ts +26 -0
- package/dist/builtins/device-manager/device-link-overlay.d.ts +23 -0
- package/dist/builtins/device-manager/device-link-resolver.d.ts +15 -0
- package/dist/builtins/device-manager/device-manager.addon.d.ts +452 -0
- package/dist/builtins/device-manager/device-manager.addon.js +3299 -0
- package/dist/builtins/device-manager/device-manager.addon.mjs +3292 -0
- package/dist/builtins/device-manager/index.d.ts +2 -0
- package/dist/builtins/device-manager/index.js +8 -0
- package/dist/builtins/device-manager/index.mjs +2 -0
- package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts +44 -0
- package/dist/builtins/hub-forwarder/hub-forwarder.addon.d.ts +15 -0
- package/dist/builtins/hub-forwarder/index.d.ts +3 -0
- package/dist/builtins/hub-forwarder/index.js +154 -0
- package/dist/builtins/hub-forwarder/index.mjs +145 -0
- package/dist/builtins/local-auth/auth-schema.d.ts +26 -0
- package/dist/builtins/local-auth/index.d.ts +1 -0
- package/dist/builtins/local-auth/index.js +4 -0
- package/dist/builtins/local-auth/index.mjs +2 -0
- package/dist/builtins/local-auth/local-auth.addon.d.ts +18 -0
- package/dist/builtins/local-auth/local-auth.addon.js +8094 -0
- package/dist/builtins/local-auth/local-auth.addon.mjs +8063 -0
- package/dist/builtins/local-auth/oauth-grants.d.ts +45 -0
- package/dist/builtins/local-auth/oauth-session-manager.d.ts +50 -0
- package/dist/builtins/local-network/index.d.ts +2 -0
- package/dist/builtins/local-network/index.js +10 -0
- package/dist/builtins/local-network/index.mjs +2 -0
- package/dist/builtins/local-network/local-network.addon.d.ts +150 -0
- package/dist/builtins/local-network/local-network.addon.js +489 -0
- package/dist/builtins/local-network/local-network.addon.mjs +477 -0
- package/dist/builtins/native-metrics/index.d.ts +2 -0
- package/dist/builtins/native-metrics/native-metrics-provider.d.ts +48 -0
- package/dist/builtins/native-metrics/native-metrics.addon.d.ts +73 -0
- package/dist/builtins/native-metrics/native-metrics.addon.js +922 -0
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +914 -0
- package/dist/builtins/platform-probe/hardware-decode-accel-probe.d.ts +37 -0
- package/dist/builtins/platform-probe/hardware-encoder-probe.d.ts +13 -0
- package/dist/builtins/platform-probe/index.d.ts +22 -0
- package/dist/builtins/platform-probe/index.js +834 -0
- package/dist/builtins/platform-probe/index.mjs +822 -0
- package/dist/builtins/platform-probe/inference-config-resolver.d.ts +29 -0
- package/dist/builtins/platform-probe/intel-accelerators.d.ts +11 -0
- package/dist/builtins/platform-probe/platform-scorer.d.ts +30 -0
- package/dist/builtins/platform-probe/runtime-packages.d.ts +6 -0
- package/dist/builtins/remote-access-orchestrator/enabled-providers-reconcile.d.ts +96 -0
- package/dist/builtins/remote-access-orchestrator/index.d.ts +1 -0
- package/dist/builtins/remote-access-orchestrator/index.js +8 -0
- package/dist/builtins/remote-access-orchestrator/index.mjs +2 -0
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts +40 -0
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +214 -0
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +208 -0
- package/dist/builtins/shared/settle-sources.d.ts +22 -0
- package/dist/builtins/snapshot/index.d.ts +2 -0
- package/dist/builtins/snapshot/index.js +494 -0
- package/dist/builtins/snapshot/index.mjs +488 -0
- package/dist/builtins/snapshot/snapshot.addon.d.ts +120 -0
- package/dist/builtins/sqlite-storage/config-store.d.ts +8 -0
- package/dist/builtins/sqlite-storage/device-store.d.ts +23 -0
- package/dist/builtins/sqlite-storage/filesystem-browse-provider.d.ts +25 -0
- package/dist/builtins/sqlite-storage/filesystem-storage-provider.d.ts +83 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts +32 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +396 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +388 -0
- package/dist/builtins/sqlite-storage/index.d.ts +8 -0
- package/dist/builtins/sqlite-storage/index.js +62 -0
- package/dist/builtins/sqlite-storage/index.mjs +49 -0
- package/dist/builtins/sqlite-storage/integration-registry.d.ts +27 -0
- package/dist/builtins/sqlite-storage/path-guard.d.ts +4 -0
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +102 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +14 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +644 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +636 -0
- package/dist/builtins/storage-orchestrator/index.d.ts +6 -0
- package/dist/builtins/storage-orchestrator/index.js +10 -0
- package/dist/builtins/storage-orchestrator/index.mjs +2 -0
- package/dist/builtins/storage-orchestrator/location-store.d.ts +49 -0
- package/dist/builtins/storage-orchestrator/provider-discovery.d.ts +10 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +103 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1138 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1128 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +236 -0
- package/dist/builtins/storage-orchestrator/storage-pressure-manager.d.ts +38 -0
- package/dist/builtins/system-backup/system-backup.service.d.ts +137 -0
- package/dist/builtins/system-config/index.d.ts +1 -0
- package/dist/builtins/system-config/index.js +8 -0
- package/dist/builtins/system-config/index.mjs +2 -0
- package/dist/builtins/system-config/system-config.addon.d.ts +10 -0
- package/dist/builtins/system-config/system-config.addon.js +232 -0
- package/dist/builtins/system-config/system-config.addon.mjs +226 -0
- package/dist/builtins/winston-logging/index.d.ts +3 -0
- package/dist/builtins/winston-logging/index.js +156 -0
- package/dist/builtins/winston-logging/index.mjs +144 -0
- package/dist/builtins/winston-logging/winston-destination.d.ts +21 -0
- package/dist/builtins/winston-logging/winston-logging.addon.d.ts +19 -0
- package/dist/chunk-CNf5ZN-e.mjs +37 -0
- package/dist/chunk-Cek0wNdY.js +64 -0
- package/dist/download/model-download-service.d.ts +41 -0
- package/dist/download/model-downloader.d.ts +31 -0
- package/dist/events/event-bus.d.ts +10 -0
- package/dist/events/system-event-bus.d.ts +14 -0
- package/dist/feature/feature-manager.d.ts +11 -0
- package/dist/formatter-B7qW8bPJ.mjs +162 -0
- package/dist/formatter-DqAKDlvN.js +167 -0
- package/dist/http/authenticated-file-server.d.ts +53 -0
- package/dist/http/data-plane-registry.d.ts +23 -0
- package/dist/http/file-data-plane.d.ts +10 -0
- package/dist/http/reverse-proxy.d.ts +15 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +93485 -0
- package/dist/index.mjs +93179 -0
- package/dist/intel-accelerators-Gg0P5mnl.js +20 -0
- package/dist/intel-accelerators-hGgpZ0pX.mjs +19 -0
- package/dist/kernel/addon-class-resolver.d.ts +4 -0
- package/dist/kernel/addon-engine-manager.d.ts +22 -0
- package/dist/kernel/addon-health-monitor.d.ts +154 -0
- package/dist/kernel/addon-installer.d.ts +208 -0
- package/dist/kernel/addon-loader.d.ts +106 -0
- package/dist/kernel/addon-manifest.d.ts +77 -0
- package/dist/kernel/capability-handle.d.ts +46 -0
- package/dist/kernel/capability-registry.d.ts +412 -0
- package/dist/kernel/config-manager.d.ts +212 -0
- package/dist/kernel/config-schema.d.ts +93 -0
- package/dist/kernel/custom-action-registry.d.ts +23 -0
- package/dist/kernel/deps/addon-deps-manager.d.ts +19 -0
- package/dist/kernel/deps/manifest-native-deps.d.ts +25 -0
- package/dist/kernel/deps/manifest-python-deps.d.ts +20 -0
- package/dist/kernel/device-registry.d.ts +29 -0
- package/dist/kernel/fs-utils.d.ts +41 -0
- package/dist/kernel/hwaccel/hwaccel-resolver.d.ts +19 -0
- package/dist/kernel/hwaccel/hwaccel-service.d.ts +4 -0
- package/dist/kernel/index.d.ts +74 -0
- package/dist/kernel/infra-capabilities.d.ts +13 -0
- package/dist/kernel/moleculer/addon-context-factory.d.ts +91 -0
- package/dist/kernel/moleculer/addon-data-plane-facility.d.ts +19 -0
- package/dist/kernel/moleculer/addon-runner.d.ts +1 -0
- package/dist/kernel/moleculer/addon-service-factory.d.ts +50 -0
- package/dist/kernel/moleculer/broker-factory.d.ts +50 -0
- package/dist/kernel/moleculer/cap-usage-registry.d.ts +46 -0
- package/dist/kernel/moleculer/capabilities-access.d.ts +21 -0
- package/dist/kernel/moleculer/child-addon-call-dispatch.d.ts +46 -0
- package/dist/kernel/moleculer/child-cap-dispatch.d.ts +20 -0
- package/dist/kernel/moleculer/cluster-secret.d.ts +15 -0
- package/dist/kernel/moleculer/core-cap-service.d.ts +50 -0
- package/dist/kernel/moleculer/crash-supervisor.d.ts +50 -0
- package/dist/kernel/moleculer/device-cap-proxy.d.ts +79 -0
- package/dist/kernel/moleculer/event-bus-core.d.ts +53 -0
- package/dist/kernel/moleculer/event-bus.d.ts +53 -0
- package/dist/kernel/moleculer/hub-log-forwarder.d.ts +36 -0
- package/dist/kernel/moleculer/hub-service.d.ts +35 -0
- package/dist/kernel/moleculer/node-registry.d.ts +126 -0
- package/dist/kernel/moleculer/process-context.d.ts +4 -0
- package/dist/kernel/moleculer/process-service.d.ts +72 -0
- package/dist/kernel/moleculer/provider-registry.d.ts +28 -0
- package/dist/kernel/moleculer/readiness-context.d.ts +62 -0
- package/dist/kernel/moleculer/readiness-service.d.ts +7 -0
- package/dist/kernel/moleculer/register-node-client.d.ts +35 -0
- package/dist/kernel/moleculer/remote-logger.d.ts +43 -0
- package/dist/kernel/moleculer/resilient-cap-call.d.ts +28 -0
- package/dist/kernel/moleculer/stream-probe-service.d.ts +9 -0
- package/dist/kernel/moleculer/trpc-links.d.ts +189 -0
- package/dist/kernel/moleculer/typed-array-serde.d.ts +25 -0
- package/dist/kernel/moleculer/worker-device-restore.d.ts +10 -0
- package/dist/kernel/provider-kind-drift.d.ts +12 -0
- package/dist/kernel/restart-coordinator.d.ts +90 -0
- package/dist/kernel/storage-location-registry.d.ts +40 -0
- package/dist/kernel/transport/cap-action-name.d.ts +100 -0
- package/dist/kernel/transport/cap-route-resolver.d.ts +148 -0
- package/dist/kernel/transport/cap-route.d.ts +148 -0
- package/dist/kernel/transport/child-cap-protocol.d.ts +136 -0
- package/dist/kernel/transport/create-local-transport.d.ts +7 -0
- package/dist/kernel/transport/frame-codec.d.ts +7 -0
- package/dist/kernel/transport/index.d.ts +27 -0
- package/dist/kernel/transport/local-child-client.d.ts +136 -0
- package/dist/kernel/transport/local-child-registry.d.ts +179 -0
- package/dist/kernel/transport/local-endpoint-path.d.ts +6 -0
- package/dist/kernel/transport/local-transport.d.ts +46 -0
- package/dist/kernel/transport/parent-unowned-call.d.ts +75 -0
- package/dist/kernel/transport/socket-channel.d.ts +27 -0
- package/dist/kernel/transport/uds-event-bridge.d.ts +36 -0
- package/dist/kernel/transport/uds-event-bus.d.ts +22 -0
- package/dist/kernel/transport/uds-local-transport.d.ts +18 -0
- package/dist/kernel/transport/uds-log-ingest.d.ts +28 -0
- package/dist/kernel/transport/uds-logger.d.ts +44 -0
- package/dist/kernel/utils/ring-buffer.d.ts +15 -0
- package/dist/kernel/workspace-detect.d.ts +9 -0
- package/dist/lifecycle/lifecycle-state-machine.d.ts +28 -0
- package/dist/logging/formatter.d.ts +30 -0
- package/dist/logging/log-manager.d.ts +54 -0
- package/dist/logging/log-ring-buffer.d.ts +47 -0
- package/dist/logging/partitioned-log-buffer.d.ts +35 -0
- package/dist/logging/scoped-logger.d.ts +17 -0
- package/dist/main-DNnMW7Z2.js +9983 -0
- package/dist/main-rtjOwPBR.mjs +9976 -0
- package/dist/manifest-python-deps-D1DbAQEv.js +6724 -0
- package/dist/manifest-python-deps-DZsKTbs1.mjs +6315 -0
- package/dist/network/network-quality.d.ts +11 -0
- package/dist/notification/notification-service.d.ts +37 -0
- package/dist/notification/toast-service.d.ts +22 -0
- package/dist/pipeline/engine-manager-resolver.d.ts +15 -0
- package/dist/pipeline/pipeline-runner.d.ts +8 -0
- package/dist/pipeline/pipeline-validator.d.ts +13 -0
- package/dist/process/resource-monitor.d.ts +11 -0
- package/dist/python/python-env-manager.d.ts +12 -0
- package/dist/repl/interfaces.d.ts +31 -0
- package/dist/repl/repl-engine.d.ts +8 -0
- package/dist/resource-monitor-ClDGFyf6.mjs +57 -0
- package/dist/resource-monitor-IIEanuJt.js +74 -0
- package/dist/settle-sources-Bhsy57y-.js +38 -0
- package/dist/settle-sources-CDtNC8ub.mjs +33 -0
- package/dist/storage/fs-storage-backend.d.ts +40 -0
- package/dist/storage/storage-location-manager.d.ts +23 -0
- package/dist/storage/storage-manager.d.ts +83 -0
- package/dist/tar-BgAEMRBR.js +5434 -0
- package/dist/tar-ByMOPNM0.mjs +5429 -0
- package/dist/tls/cert-manager.d.ts +26 -0
- package/dist/tls/index.d.ts +1 -0
- package/package.json +343 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path$1 from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
import { BaseAddon, filesystemBrowseCapability, storageProviderCapability } from "@camstack/types";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { mkdir, readdir, realpath, statfs } from "node:fs/promises";
|
|
8
|
+
//#region src/builtins/sqlite-storage/filesystem-storage-provider.ts
|
|
9
|
+
/**
|
|
10
|
+
* Filesystem `storage-provider` (Task 7).
|
|
11
|
+
*
|
|
12
|
+
* Implements the `storage-provider` collection cap. Replaces the legacy
|
|
13
|
+
* sync `IStorageProvider` shape — every method now takes the live
|
|
14
|
+
* `StorageLocation` inline (no per-provider location state) and works
|
|
15
|
+
* off `location.config.basePath`.
|
|
16
|
+
*
|
|
17
|
+
* Adds chunked upload (`.partial` rename trick) + chunked download
|
|
18
|
+
* sessions for moving multi-GB archives without buffering the whole
|
|
19
|
+
* archive in memory. Sessions auto-expire after 5 minutes idle.
|
|
20
|
+
*
|
|
21
|
+
* The legacy `FilesystemStorageProvider` in `@camstack/types/node` is
|
|
22
|
+
* deliberately untouched — it still backs the sync `ctx.kernel.storage`
|
|
23
|
+
* helper that addons like `pipeline-analytics` use to derive paths
|
|
24
|
+
* synchronously. That sync API will be retired in a later refactor;
|
|
25
|
+
* Task 7 is scoped to the cap surface only.
|
|
26
|
+
*/
|
|
27
|
+
/** Idle TTL for upload + download sessions. */
|
|
28
|
+
var SESSION_IDLE_TTL_MS = 300 * 1e3;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve `location.config.basePath` into a string. Throws an actionable
|
|
31
|
+
* error when the field is missing or wrong-typed — every primitive
|
|
32
|
+
* funnels through here, so a missing field surfaces as a single clear
|
|
33
|
+
* message instead of a downstream `path.join` TypeError.
|
|
34
|
+
*/
|
|
35
|
+
function getBasePath(location) {
|
|
36
|
+
const basePath = location.config["basePath"];
|
|
37
|
+
if (typeof basePath !== "string" || basePath.length === 0) throw new Error(`filesystem-storage: location "${location.id}" missing string config.basePath`);
|
|
38
|
+
return basePath;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a relative path against the location's base, rejecting any
|
|
42
|
+
* traversal that escapes the base directory. Mirrors the
|
|
43
|
+
* `addon-asset` endpoint security check in `main.ts`: normalize both
|
|
44
|
+
* the base + the joined target, then enforce `target.startsWith(base + sep)`
|
|
45
|
+
* (with the `+ sep` guard so `/data` doesn't accidentally allow
|
|
46
|
+
* `/datasecret`).
|
|
47
|
+
*/
|
|
48
|
+
function safeResolve(base, relativePath) {
|
|
49
|
+
const normalizedBase = path$1.resolve(base);
|
|
50
|
+
const target = path$1.resolve(normalizedBase, relativePath);
|
|
51
|
+
if (target !== normalizedBase && !target.startsWith(normalizedBase + path$1.sep)) throw new Error(`filesystem-storage: relativePath "${relativePath}" escapes basePath "${normalizedBase}"`);
|
|
52
|
+
return target;
|
|
53
|
+
}
|
|
54
|
+
var FilesystemStorageProvider = class FilesystemStorageProvider {
|
|
55
|
+
static providerId = "filesystem-storage";
|
|
56
|
+
static displayName = "Filesystem";
|
|
57
|
+
uploads = /* @__PURE__ */ new Map();
|
|
58
|
+
downloads = /* @__PURE__ */ new Map();
|
|
59
|
+
async getProviderInfo() {
|
|
60
|
+
return {
|
|
61
|
+
providerId: FilesystemStorageProvider.providerId,
|
|
62
|
+
displayName: FilesystemStorageProvider.displayName,
|
|
63
|
+
shouldSaveDiskSpace: true,
|
|
64
|
+
minFreePercent: 10,
|
|
65
|
+
nodeLocal: true,
|
|
66
|
+
configSchema: { sections: [{
|
|
67
|
+
id: "filesystem-storage",
|
|
68
|
+
title: "Filesystem",
|
|
69
|
+
fields: [{
|
|
70
|
+
type: "text",
|
|
71
|
+
key: "basePath",
|
|
72
|
+
label: "Base path",
|
|
73
|
+
description: "Absolute directory path where files are stored.",
|
|
74
|
+
required: true
|
|
75
|
+
}]
|
|
76
|
+
}] }
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async testLocation({ config }) {
|
|
80
|
+
const basePath = config["basePath"];
|
|
81
|
+
if (typeof basePath !== "string" || basePath.length === 0) return {
|
|
82
|
+
ok: false,
|
|
83
|
+
error: "config.basePath must be a non-empty string"
|
|
84
|
+
};
|
|
85
|
+
try {
|
|
86
|
+
const stat = await fs.promises.stat(basePath).catch(() => null);
|
|
87
|
+
if (stat) {
|
|
88
|
+
if (!stat.isDirectory()) return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: `Path "${basePath}" exists but is not a directory`
|
|
91
|
+
};
|
|
92
|
+
await fs.promises.access(basePath, fs.constants.W_OK);
|
|
93
|
+
return { ok: true };
|
|
94
|
+
}
|
|
95
|
+
await fs.promises.mkdir(basePath, { recursive: true });
|
|
96
|
+
return { ok: true };
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
error: err instanceof Error ? err.message : String(err)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async resolve({ location, relativePath }) {
|
|
105
|
+
return safeResolve(getBasePath(location), relativePath);
|
|
106
|
+
}
|
|
107
|
+
async write({ location, relativePath, data }) {
|
|
108
|
+
const filePath = safeResolve(getBasePath(location), relativePath);
|
|
109
|
+
await fs.promises.mkdir(path$1.dirname(filePath), { recursive: true });
|
|
110
|
+
await fs.promises.writeFile(filePath, data);
|
|
111
|
+
}
|
|
112
|
+
async read({ location, relativePath }) {
|
|
113
|
+
const filePath = safeResolve(getBasePath(location), relativePath);
|
|
114
|
+
const buf = await fs.promises.readFile(filePath);
|
|
115
|
+
const out = new Uint8Array(new ArrayBuffer(buf.byteLength));
|
|
116
|
+
out.set(buf);
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
async exists({ location, relativePath }) {
|
|
120
|
+
const filePath = safeResolve(getBasePath(location), relativePath);
|
|
121
|
+
try {
|
|
122
|
+
await fs.promises.access(filePath);
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async list({ location, prefix }) {
|
|
129
|
+
const base = getBasePath(location);
|
|
130
|
+
const dir = prefix ? safeResolve(base, prefix) : path$1.resolve(base);
|
|
131
|
+
try {
|
|
132
|
+
return (await fs.promises.readdir(dir, { withFileTypes: true })).map((e) => prefix ? `${prefix}/${e.name}` : e.name);
|
|
133
|
+
} catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async delete({ location, relativePath }) {
|
|
138
|
+
const filePath = safeResolve(getBasePath(location), relativePath);
|
|
139
|
+
await fs.promises.rm(filePath, { force: true });
|
|
140
|
+
}
|
|
141
|
+
async getAvailableSpace({ location }) {
|
|
142
|
+
const base = path$1.resolve(getBasePath(location));
|
|
143
|
+
try {
|
|
144
|
+
let target = base;
|
|
145
|
+
while (!fs.existsSync(target)) {
|
|
146
|
+
const parent = path$1.dirname(target);
|
|
147
|
+
if (!parent || parent === target) return null;
|
|
148
|
+
target = parent;
|
|
149
|
+
}
|
|
150
|
+
const stats = await fs.promises.statfs(target);
|
|
151
|
+
return stats.bavail * stats.bsize;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async beginUpload({ location, relativePath }) {
|
|
157
|
+
const target = safeResolve(getBasePath(location), relativePath);
|
|
158
|
+
const partial = `${target}.partial`;
|
|
159
|
+
await fs.promises.mkdir(path$1.dirname(target), { recursive: true });
|
|
160
|
+
await fs.promises.unlink(partial).catch(() => {});
|
|
161
|
+
const stream = fs.createWriteStream(partial);
|
|
162
|
+
const uploadId = randomUUID();
|
|
163
|
+
const session = {
|
|
164
|
+
stream,
|
|
165
|
+
target,
|
|
166
|
+
partial,
|
|
167
|
+
bytesWritten: 0,
|
|
168
|
+
timer: setTimeout(() => {
|
|
169
|
+
this.abortUpload({ uploadId }).catch(() => {});
|
|
170
|
+
}, SESSION_IDLE_TTL_MS)
|
|
171
|
+
};
|
|
172
|
+
this.uploads.set(uploadId, session);
|
|
173
|
+
return { uploadId };
|
|
174
|
+
}
|
|
175
|
+
async writeChunk({ uploadId, offset, data }) {
|
|
176
|
+
const session = this.uploads.get(uploadId);
|
|
177
|
+
if (!session) throw new Error(`filesystem-storage: upload session "${uploadId}" not found`);
|
|
178
|
+
if (offset !== session.bytesWritten) throw new Error(`filesystem-storage: chunk offset mismatch (expected ${session.bytesWritten}, got ${offset})`);
|
|
179
|
+
await new Promise((resolve, reject) => {
|
|
180
|
+
session.stream.write(data, (err) => {
|
|
181
|
+
if (err) reject(err);
|
|
182
|
+
else resolve();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
clearTimeout(session.timer);
|
|
186
|
+
const nextBytesWritten = session.bytesWritten + data.length;
|
|
187
|
+
this.uploads.set(uploadId, {
|
|
188
|
+
...session,
|
|
189
|
+
bytesWritten: nextBytesWritten,
|
|
190
|
+
timer: setTimeout(() => {
|
|
191
|
+
this.abortUpload({ uploadId }).catch(() => {});
|
|
192
|
+
}, SESSION_IDLE_TTL_MS)
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async finalizeUpload({ uploadId }) {
|
|
196
|
+
const session = this.uploads.get(uploadId);
|
|
197
|
+
if (!session) throw new Error(`filesystem-storage: upload session "${uploadId}" not found`);
|
|
198
|
+
await new Promise((resolve, reject) => {
|
|
199
|
+
session.stream.end(() => resolve());
|
|
200
|
+
session.stream.on("error", reject);
|
|
201
|
+
});
|
|
202
|
+
await fs.promises.rename(session.partial, session.target);
|
|
203
|
+
clearTimeout(session.timer);
|
|
204
|
+
this.uploads.delete(uploadId);
|
|
205
|
+
}
|
|
206
|
+
async abortUpload({ uploadId }) {
|
|
207
|
+
const session = this.uploads.get(uploadId);
|
|
208
|
+
if (!session) return;
|
|
209
|
+
try {
|
|
210
|
+
session.stream.destroy();
|
|
211
|
+
} catch {}
|
|
212
|
+
await fs.promises.unlink(session.partial).catch(() => {});
|
|
213
|
+
clearTimeout(session.timer);
|
|
214
|
+
this.uploads.delete(uploadId);
|
|
215
|
+
}
|
|
216
|
+
async beginDownload({ location, relativePath }) {
|
|
217
|
+
const target = safeResolve(getBasePath(location), relativePath);
|
|
218
|
+
const stat = await fs.promises.stat(target);
|
|
219
|
+
const fd = await fs.promises.open(target, "r");
|
|
220
|
+
const downloadId = randomUUID();
|
|
221
|
+
const session = {
|
|
222
|
+
fd,
|
|
223
|
+
sizeBytes: stat.size,
|
|
224
|
+
timer: setTimeout(() => {
|
|
225
|
+
this.endDownload({ downloadId }).catch(() => {});
|
|
226
|
+
}, SESSION_IDLE_TTL_MS)
|
|
227
|
+
};
|
|
228
|
+
this.downloads.set(downloadId, session);
|
|
229
|
+
return {
|
|
230
|
+
downloadId,
|
|
231
|
+
sizeBytes: stat.size
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async readChunk({ downloadId, offset, length }) {
|
|
235
|
+
const session = this.downloads.get(downloadId);
|
|
236
|
+
if (!session) throw new Error(`filesystem-storage: download session "${downloadId}" not found`);
|
|
237
|
+
const out = new Uint8Array(new ArrayBuffer(length));
|
|
238
|
+
const { bytesRead } = await session.fd.read(out, 0, length, offset);
|
|
239
|
+
clearTimeout(session.timer);
|
|
240
|
+
this.downloads.set(downloadId, {
|
|
241
|
+
...session,
|
|
242
|
+
timer: setTimeout(() => {
|
|
243
|
+
this.endDownload({ downloadId }).catch(() => {});
|
|
244
|
+
}, SESSION_IDLE_TTL_MS)
|
|
245
|
+
});
|
|
246
|
+
return out.subarray(0, bytesRead);
|
|
247
|
+
}
|
|
248
|
+
async endDownload({ downloadId }) {
|
|
249
|
+
const session = this.downloads.get(downloadId);
|
|
250
|
+
if (!session) return;
|
|
251
|
+
try {
|
|
252
|
+
await session.fd.close();
|
|
253
|
+
} catch {}
|
|
254
|
+
clearTimeout(session.timer);
|
|
255
|
+
this.downloads.delete(downloadId);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Abort all outstanding sessions. Called on addon shutdown so timers
|
|
259
|
+
* don't leak across hot-reload cycles.
|
|
260
|
+
*/
|
|
261
|
+
async dispose() {
|
|
262
|
+
const uploadIds = [...this.uploads.keys()];
|
|
263
|
+
const downloadIds = [...this.downloads.keys()];
|
|
264
|
+
await Promise.all(uploadIds.map((uploadId) => this.abortUpload({ uploadId })));
|
|
265
|
+
await Promise.all(downloadIds.map((downloadId) => this.endDownload({ downloadId })));
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/builtins/sqlite-storage/path-guard.ts
|
|
270
|
+
/**
|
|
271
|
+
* Allowed-root containment guard for the filesystem-browse cap. Pure +
|
|
272
|
+
* synchronous so it is trivially unit-testable; the provider applies it AFTER
|
|
273
|
+
* resolving the real path (so symlink escapes are also caught upstream).
|
|
274
|
+
*/
|
|
275
|
+
/** True iff `candidate` is one of, or nested under, an allowed root. */
|
|
276
|
+
function isWithinAllowedRoots(candidate, allowedRoots) {
|
|
277
|
+
const norm = resolve(candidate);
|
|
278
|
+
return allowedRoots.some((root) => {
|
|
279
|
+
const r = resolve(root);
|
|
280
|
+
return norm === r || norm.startsWith(r.endsWith(sep) ? r : r + sep);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/** Throws unless `candidate` is within an allowed root. */
|
|
284
|
+
function assertWithinAllowedRoots(candidate, allowedRoots) {
|
|
285
|
+
if (!isWithinAllowedRoots(candidate, allowedRoots)) throw new Error(`path "${candidate}" is not within an allowed root`);
|
|
286
|
+
}
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/builtins/sqlite-storage/filesystem-browse-provider.ts
|
|
289
|
+
/**
|
|
290
|
+
* Per-node filesystem-browse provider. Lists/creates directories under
|
|
291
|
+
* operator-configured allowed roots only (sandboxed by `path-guard`). Backs the
|
|
292
|
+
* `filesystem-browse` cap so the admin UI can pick a node + path for a
|
|
293
|
+
* node-local storage location.
|
|
294
|
+
*/
|
|
295
|
+
var FilesystemBrowseProvider = class {
|
|
296
|
+
allowedRoots;
|
|
297
|
+
/** allowedRoots is injected (read from addon config) so it stays per-node + testable. */
|
|
298
|
+
constructor(allowedRoots) {
|
|
299
|
+
this.allowedRoots = allowedRoots;
|
|
300
|
+
}
|
|
301
|
+
async listAllowedRoots() {
|
|
302
|
+
return [...this.allowedRoots()];
|
|
303
|
+
}
|
|
304
|
+
async browse({ path }) {
|
|
305
|
+
const real = await realpath(path).catch(() => path);
|
|
306
|
+
assertWithinAllowedRoots(real, await this.resolvedRoots());
|
|
307
|
+
const entries = (await readdir(real, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => ({
|
|
308
|
+
name: d.name,
|
|
309
|
+
path: join(real, d.name)
|
|
310
|
+
}));
|
|
311
|
+
const fs = await statfs(real);
|
|
312
|
+
const totalBytes = fs.blocks * fs.bsize;
|
|
313
|
+
return {
|
|
314
|
+
path: real,
|
|
315
|
+
entries,
|
|
316
|
+
freeBytes: fs.bavail * fs.bsize,
|
|
317
|
+
totalBytes
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async createDir({ path }) {
|
|
321
|
+
const parent = dirname(path);
|
|
322
|
+
assertWithinAllowedRoots(join(await realpath(parent).catch(() => parent), basename(path)), await this.resolvedRoots());
|
|
323
|
+
await mkdir(path, { recursive: true });
|
|
324
|
+
return { path };
|
|
325
|
+
}
|
|
326
|
+
/** Allowed roots with symlinks resolved (falls back to the raw root if it does not exist). */
|
|
327
|
+
async resolvedRoots() {
|
|
328
|
+
return Promise.all(this.allowedRoots().map((r) => realpath(r).catch(() => r)));
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/builtins/sqlite-storage/filesystem-storage.addon.ts
|
|
333
|
+
function defaultAllowedRoots() {
|
|
334
|
+
return [
|
|
335
|
+
"/mnt",
|
|
336
|
+
"/Volumes",
|
|
337
|
+
"/media",
|
|
338
|
+
homedir()
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Filesystem Storage addon — registers a filesystem-backed
|
|
343
|
+
* `storage-provider` (Task 7).
|
|
344
|
+
*
|
|
345
|
+
* Pre-Task 7 this addon registered against the (then-collection)
|
|
346
|
+
* `storage` cap. After Tasks 2/3/4 the consumer-facing `storage` cap is
|
|
347
|
+
* a singleton owned by `storage-orchestrator`; the orchestrator
|
|
348
|
+
* dispatches each call to whichever `storage-provider` matches
|
|
349
|
+
* `location.providerId`.
|
|
350
|
+
*
|
|
351
|
+
* The OLD `rootPath` config field is preserved for back-compat: the
|
|
352
|
+
* orchestrator's first-boot seeding step still uses the same env-var
|
|
353
|
+
* resolution chain (operator override → `CAMSTACK_DATA` → fallback) to
|
|
354
|
+
* derive the base path of every default location, then writes
|
|
355
|
+
* `config.basePath` into each persisted `StorageLocation` row. The
|
|
356
|
+
* provider itself is now stateless — every call carries the live
|
|
357
|
+
* `StorageLocation` inline.
|
|
358
|
+
*/
|
|
359
|
+
var FilesystemStorageAddon = class extends BaseAddon {
|
|
360
|
+
provider = null;
|
|
361
|
+
constructor() {
|
|
362
|
+
super({
|
|
363
|
+
rootPath: "camstack-data",
|
|
364
|
+
storageAllowedRoots: defaultAllowedRoots()
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
async onInitialize() {
|
|
368
|
+
this.provider = new FilesystemStorageProvider();
|
|
369
|
+
const browse = new FilesystemBrowseProvider(() => this.config.storageAllowedRoots);
|
|
370
|
+
this.ctx.logger.info("Filesystem storage-provider initialized");
|
|
371
|
+
return [{
|
|
372
|
+
capability: storageProviderCapability,
|
|
373
|
+
provider: this.provider
|
|
374
|
+
}, {
|
|
375
|
+
capability: filesystemBrowseCapability,
|
|
376
|
+
provider: browse
|
|
377
|
+
}];
|
|
378
|
+
}
|
|
379
|
+
async onShutdown() {
|
|
380
|
+
await this.provider?.dispose();
|
|
381
|
+
this.provider = null;
|
|
382
|
+
}
|
|
383
|
+
getProvider() {
|
|
384
|
+
return this.provider;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
//#endregion
|
|
388
|
+
export { FilesystemStorageAddon, FilesystemStorageAddon as default, FilesystemStorageProvider as t };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { FilesystemStorageProvider } from './filesystem-storage-provider.js';
|
|
2
|
+
export { FilesystemStorageAddon } from './filesystem-storage.addon.js';
|
|
3
|
+
export { SqliteSettingsBackend } from './sqlite-settings-backend.js';
|
|
4
|
+
export { SqliteSettingsAddon } from './sqlite-settings.addon.js';
|
|
5
|
+
export { DeviceStore } from './device-store.js';
|
|
6
|
+
export type { DeviceRow } from './device-store.js';
|
|
7
|
+
export { ConfigStore } from './config-store.js';
|
|
8
|
+
export { FilesystemStorageAddon as default } from './filesystem-storage.addon.js';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
require("../../chunk-Cek0wNdY.js");
|
|
6
|
+
const require_builtins_sqlite_storage_filesystem_storage_addon = require("./filesystem-storage.addon.js");
|
|
7
|
+
const require_builtins_sqlite_storage_sqlite_settings_addon = require("./sqlite-settings.addon.js");
|
|
8
|
+
//#region src/builtins/sqlite-storage/device-store.ts
|
|
9
|
+
var DeviceStore = class {
|
|
10
|
+
db;
|
|
11
|
+
constructor(db) {
|
|
12
|
+
this.db = db;
|
|
13
|
+
}
|
|
14
|
+
insert(addonId, device) {
|
|
15
|
+
this.db.prepare(`INSERT INTO devices (addon_id, stable_id, type, name, parent_stable_id) VALUES (?, ?, ?, ?, ?)`).run(addonId, device.stableId, device.type, device.name, device.parentStableId);
|
|
16
|
+
}
|
|
17
|
+
listByAddon(addonId) {
|
|
18
|
+
return this.db.prepare(`SELECT stable_id as stableId, type, name, parent_stable_id as parentStableId, enabled FROM devices WHERE addon_id = ?`).all(addonId);
|
|
19
|
+
}
|
|
20
|
+
listChildren(addonId, parentStableId) {
|
|
21
|
+
return this.db.prepare(`SELECT stable_id as stableId, type, name, parent_stable_id as parentStableId, enabled FROM devices WHERE addon_id = ? AND parent_stable_id = ?`).all(addonId, parentStableId);
|
|
22
|
+
}
|
|
23
|
+
remove(addonId, stableId) {
|
|
24
|
+
this.db.prepare(`DELETE FROM devices WHERE addon_id = ? AND stable_id = ?`).run(addonId, stableId);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/builtins/sqlite-storage/config-store.ts
|
|
29
|
+
var ConfigStore = class {
|
|
30
|
+
db;
|
|
31
|
+
constructor(db) {
|
|
32
|
+
this.db = db;
|
|
33
|
+
}
|
|
34
|
+
save(addonId, stableId, data) {
|
|
35
|
+
if (stableId === null) this.db.prepare(`INSERT INTO addon_config (addon_id, stable_id, data, updated_at)
|
|
36
|
+
VALUES (?, NULL, ?, datetime('now'))
|
|
37
|
+
ON CONFLICT(addon_id) WHERE stable_id IS NULL
|
|
38
|
+
DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`).run(addonId, JSON.stringify(data));
|
|
39
|
+
else this.db.prepare(`INSERT INTO addon_config (addon_id, stable_id, data, updated_at)
|
|
40
|
+
VALUES (?, ?, ?, datetime('now'))
|
|
41
|
+
ON CONFLICT(addon_id, stable_id) WHERE stable_id IS NOT NULL
|
|
42
|
+
DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`).run(addonId, stableId, JSON.stringify(data));
|
|
43
|
+
}
|
|
44
|
+
load(addonId, stableId) {
|
|
45
|
+
const row = stableId === null ? this.db.prepare(`SELECT data FROM addon_config WHERE addon_id = ? AND stable_id IS NULL`).get(addonId) : this.db.prepare(`SELECT data FROM addon_config WHERE addon_id = ? AND stable_id = ?`).get(addonId, stableId);
|
|
46
|
+
return row ? JSON.parse(row.data) : {};
|
|
47
|
+
}
|
|
48
|
+
remove(addonId, stableId) {
|
|
49
|
+
if (stableId === null) this.db.prepare(`DELETE FROM addon_config WHERE addon_id = ? AND stable_id IS NULL`).run(addonId);
|
|
50
|
+
else this.db.prepare(`DELETE FROM addon_config WHERE addon_id = ? AND stable_id = ?`).run(addonId, stableId);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
exports.ConfigStore = ConfigStore;
|
|
55
|
+
exports.ConfigStore$1 = ConfigStore;
|
|
56
|
+
exports.DeviceStore = DeviceStore;
|
|
57
|
+
exports.DeviceStore$1 = DeviceStore;
|
|
58
|
+
exports.FilesystemStorageAddon = require_builtins_sqlite_storage_filesystem_storage_addon.FilesystemStorageAddon;
|
|
59
|
+
exports.FilesystemStorageProvider = require_builtins_sqlite_storage_filesystem_storage_addon.FilesystemStorageProvider;
|
|
60
|
+
exports.SqliteSettingsAddon = require_builtins_sqlite_storage_sqlite_settings_addon.SqliteSettingsAddon;
|
|
61
|
+
exports.SqliteSettingsBackend = require_builtins_sqlite_storage_sqlite_settings_addon.SqliteSettingsBackend;
|
|
62
|
+
exports.default = require_builtins_sqlite_storage_filesystem_storage_addon.FilesystemStorageAddon;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { FilesystemStorageAddon, t as FilesystemStorageProvider } from "./filesystem-storage.addon.mjs";
|
|
2
|
+
import { SqliteSettingsAddon, t as SqliteSettingsBackend } from "./sqlite-settings.addon.mjs";
|
|
3
|
+
//#region src/builtins/sqlite-storage/device-store.ts
|
|
4
|
+
var DeviceStore = class {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
insert(addonId, device) {
|
|
10
|
+
this.db.prepare(`INSERT INTO devices (addon_id, stable_id, type, name, parent_stable_id) VALUES (?, ?, ?, ?, ?)`).run(addonId, device.stableId, device.type, device.name, device.parentStableId);
|
|
11
|
+
}
|
|
12
|
+
listByAddon(addonId) {
|
|
13
|
+
return this.db.prepare(`SELECT stable_id as stableId, type, name, parent_stable_id as parentStableId, enabled FROM devices WHERE addon_id = ?`).all(addonId);
|
|
14
|
+
}
|
|
15
|
+
listChildren(addonId, parentStableId) {
|
|
16
|
+
return this.db.prepare(`SELECT stable_id as stableId, type, name, parent_stable_id as parentStableId, enabled FROM devices WHERE addon_id = ? AND parent_stable_id = ?`).all(addonId, parentStableId);
|
|
17
|
+
}
|
|
18
|
+
remove(addonId, stableId) {
|
|
19
|
+
this.db.prepare(`DELETE FROM devices WHERE addon_id = ? AND stable_id = ?`).run(addonId, stableId);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/builtins/sqlite-storage/config-store.ts
|
|
24
|
+
var ConfigStore = class {
|
|
25
|
+
db;
|
|
26
|
+
constructor(db) {
|
|
27
|
+
this.db = db;
|
|
28
|
+
}
|
|
29
|
+
save(addonId, stableId, data) {
|
|
30
|
+
if (stableId === null) this.db.prepare(`INSERT INTO addon_config (addon_id, stable_id, data, updated_at)
|
|
31
|
+
VALUES (?, NULL, ?, datetime('now'))
|
|
32
|
+
ON CONFLICT(addon_id) WHERE stable_id IS NULL
|
|
33
|
+
DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`).run(addonId, JSON.stringify(data));
|
|
34
|
+
else this.db.prepare(`INSERT INTO addon_config (addon_id, stable_id, data, updated_at)
|
|
35
|
+
VALUES (?, ?, ?, datetime('now'))
|
|
36
|
+
ON CONFLICT(addon_id, stable_id) WHERE stable_id IS NOT NULL
|
|
37
|
+
DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`).run(addonId, stableId, JSON.stringify(data));
|
|
38
|
+
}
|
|
39
|
+
load(addonId, stableId) {
|
|
40
|
+
const row = stableId === null ? this.db.prepare(`SELECT data FROM addon_config WHERE addon_id = ? AND stable_id IS NULL`).get(addonId) : this.db.prepare(`SELECT data FROM addon_config WHERE addon_id = ? AND stable_id = ?`).get(addonId, stableId);
|
|
41
|
+
return row ? JSON.parse(row.data) : {};
|
|
42
|
+
}
|
|
43
|
+
remove(addonId, stableId) {
|
|
44
|
+
if (stableId === null) this.db.prepare(`DELETE FROM addon_config WHERE addon_id = ? AND stable_id IS NULL`).run(addonId);
|
|
45
|
+
else this.db.prepare(`DELETE FROM addon_config WHERE addon_id = ? AND stable_id = ?`).run(addonId, stableId);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
export { ConfigStore, DeviceStore, FilesystemStorageAddon, FilesystemStorageAddon as default, FilesystemStorageProvider, SqliteSettingsAddon, SqliteSettingsBackend };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Integration, PersistedDevice, CreateIntegrationInput, CreateDeviceInput, IIntegrationRegistry, ISettingsBackend } from '@camstack/types';
|
|
2
|
+
export declare class IntegrationRegistry implements IIntegrationRegistry {
|
|
3
|
+
private readonly backend;
|
|
4
|
+
constructor(backend: ISettingsBackend);
|
|
5
|
+
initialize(): Promise<void>;
|
|
6
|
+
createIntegration(input: CreateIntegrationInput): Promise<Integration>;
|
|
7
|
+
getIntegration(id: string): Promise<Integration | null>;
|
|
8
|
+
getIntegrationByAddonId(addonId: string): Promise<Integration | null>;
|
|
9
|
+
listIntegrations(): Promise<readonly Integration[]>;
|
|
10
|
+
updateIntegration(id: string, updates: Partial<Pick<Integration, 'name' | 'enabled' | 'info'>>): Promise<Integration | null>;
|
|
11
|
+
deleteIntegration(id: string): Promise<boolean>;
|
|
12
|
+
getIntegrationSettings(integrationId: string): Promise<Record<string, unknown>>;
|
|
13
|
+
setIntegrationSetting(integrationId: string, key: string, value: unknown): Promise<void>;
|
|
14
|
+
setIntegrationSettings(integrationId: string, settings: Record<string, unknown>): Promise<void>;
|
|
15
|
+
createDevice(input: CreateDeviceInput): Promise<PersistedDevice>;
|
|
16
|
+
getDevice(id: string): Promise<PersistedDevice | null>;
|
|
17
|
+
getDeviceByStableId(stableId: string): Promise<PersistedDevice | null>;
|
|
18
|
+
listDevices(integrationId?: string): Promise<readonly PersistedDevice[]>;
|
|
19
|
+
listCameras(): Promise<readonly PersistedDevice[]>;
|
|
20
|
+
updateDevice(id: string, updates: Partial<Pick<PersistedDevice, 'name' | 'enabled' | 'info'>>): Promise<PersistedDevice | null>;
|
|
21
|
+
deleteDevice(id: string): Promise<boolean>;
|
|
22
|
+
getDeviceSettings(deviceId: string): Promise<Record<string, unknown>>;
|
|
23
|
+
setDeviceSetting(deviceId: string, key: string, value: unknown): Promise<void>;
|
|
24
|
+
setDeviceSettings(deviceId: string, settings: Record<string, unknown>): Promise<void>;
|
|
25
|
+
private mapIntegration;
|
|
26
|
+
private mapDevice;
|
|
27
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** True iff `candidate` is one of, or nested under, an allowed root. */
|
|
2
|
+
export declare function isWithinAllowedRoots(candidate: string, allowedRoots: readonly string[]): boolean;
|
|
3
|
+
/** Throws unless `candidate` is within an allowed root. */
|
|
4
|
+
export declare function assertWithinAllowedRoots(candidate: string, allowedRoots: readonly string[]): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { default as Database } from 'better-sqlite3';
|
|
2
|
+
import { ISettingsBackend, SettingsRecord, TableSchema, TableQueryOptions, SettingsGetInput, SettingsSetInput, SettingsQueryInput, SettingsInsertInput, SettingsUpdateInput, SettingsDeleteInput, SettingsCountInput, SettingsIsEmptyInput, SettingsHistogramInput, HistogramBucket, CollectionColumn, CollectionIndex } from '@camstack/types';
|
|
3
|
+
/**
|
|
4
|
+
* SQLite implementation of ISettingsBackend.
|
|
5
|
+
*
|
|
6
|
+
* Every collection is structured: declared at boot (canonical KV
|
|
7
|
+
* collections) or via `declareCollection` (typed schemas like
|
|
8
|
+
* `pipeline-analytics:tracks`). The legacy auto-create-on-first-access
|
|
9
|
+
* path was removed — see commit history for the migration. Operations
|
|
10
|
+
* on undeclared collections throw at runtime so callers fail fast.
|
|
11
|
+
*
|
|
12
|
+
* WAL mode for concurrent reads.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SqliteSettingsBackend implements ISettingsBackend {
|
|
15
|
+
private readonly dbPath;
|
|
16
|
+
private db;
|
|
17
|
+
private readonly structuredTables;
|
|
18
|
+
/** Map from scoped collection name → set of column names (non-id) that
|
|
19
|
+
* the structured schema owns. Routes set/get/insert/update/query to
|
|
20
|
+
* typed columns. Every collection MUST be declared here before use. */
|
|
21
|
+
private readonly declaredCollections;
|
|
22
|
+
private readonly runtimeDefaults;
|
|
23
|
+
/**
|
|
24
|
+
* Canonical key/value collections — declared with a `(id TEXT PK,
|
|
25
|
+
* data TEXT NOT NULL)` schema at boot so existing JSON-blob rows
|
|
26
|
+
* keep working through the structured path. Generates SQL identical
|
|
27
|
+
* to the previous legacy path; only the routing is unified.
|
|
28
|
+
*/
|
|
29
|
+
private static readonly CANONICAL_KV_COLLECTIONS;
|
|
30
|
+
constructor(dbPath: string, runtimeDefaults?: Record<string, unknown>);
|
|
31
|
+
initialize(): Promise<void>;
|
|
32
|
+
private requireDeclared;
|
|
33
|
+
/**
|
|
34
|
+
* Decode a single column value off a SQLite row into the JS shape the
|
|
35
|
+
* caller's schema expects. Handles:
|
|
36
|
+
* - BOOLEAN columns: `0|1` → `false|true`
|
|
37
|
+
* - JSON columns (TEXT starting with `{` / `[`): parse
|
|
38
|
+
* - everything else: pass-through with null coalescing
|
|
39
|
+
*/
|
|
40
|
+
private decodeColumnValue;
|
|
41
|
+
shutdown(): Promise<void>;
|
|
42
|
+
get({ namespace, collection, key }: SettingsGetInput): Promise<unknown>;
|
|
43
|
+
set({ namespace, collection, key, value }: SettingsSetInput): Promise<void>;
|
|
44
|
+
query<T extends object = Record<string, unknown>>({ namespace, collection, filter, }: SettingsQueryInput): Promise<readonly SettingsRecord<T>[]>;
|
|
45
|
+
insert<T extends object = Record<string, unknown>>({ namespace, collection, record, }: SettingsInsertInput<T>): Promise<void>;
|
|
46
|
+
update({ namespace, collection, id, data }: SettingsUpdateInput): Promise<void>;
|
|
47
|
+
delete({ namespace, collection, key }: SettingsDeleteInput): Promise<void>;
|
|
48
|
+
count({ namespace, collection, filter }: SettingsCountInput): Promise<number>;
|
|
49
|
+
histogram({ namespace, collection, field, bucketSize, origin, filter, }: SettingsHistogramInput): Promise<readonly HistogramBucket[]>;
|
|
50
|
+
isEmpty({ namespace, collection }: SettingsIsEmptyInput): Promise<boolean>;
|
|
51
|
+
private queryDeclared;
|
|
52
|
+
/** Get a system setting by dot-notation key */
|
|
53
|
+
getSystem(key: string): unknown;
|
|
54
|
+
/** Set a system setting */
|
|
55
|
+
setSystem(key: string, value: unknown): void;
|
|
56
|
+
/** Get all system settings as flat key-value */
|
|
57
|
+
getAllSystem(): Record<string, unknown>;
|
|
58
|
+
/** Get all settings for an addon */
|
|
59
|
+
getAllAddon(addonId: string): Record<string, unknown>;
|
|
60
|
+
/** Bulk-set all settings for an addon */
|
|
61
|
+
setAllAddon(addonId: string, config: Record<string, unknown>): void;
|
|
62
|
+
/** Get all settings for a provider */
|
|
63
|
+
getAllProvider(providerId: string): Record<string, unknown>;
|
|
64
|
+
/** Set a provider setting */
|
|
65
|
+
setProvider(providerId: string, key: string, value: unknown): void;
|
|
66
|
+
/** Get all settings for a device */
|
|
67
|
+
getAllDevice(deviceId: string): Record<string, unknown>;
|
|
68
|
+
/** Set a device setting */
|
|
69
|
+
setDevice(deviceId: string, key: string, value: unknown): void;
|
|
70
|
+
getAddonDevice(addonId: string, deviceId: string): Record<string, unknown>;
|
|
71
|
+
setAddonDevice(addonId: string, deviceId: string, values: Record<string, unknown>): void;
|
|
72
|
+
clearAddonDevice(addonId: string, deviceId: string): void;
|
|
73
|
+
/** Seed system-settings with runtime defaults (first boot) */
|
|
74
|
+
private seedDefaults;
|
|
75
|
+
/**
|
|
76
|
+
* Expose the raw better-sqlite3 Database instance for components that
|
|
77
|
+
* need direct SQL access (e.g. DeviceStore, ConfigStore).
|
|
78
|
+
* Returns null if the backend has not been initialized yet.
|
|
79
|
+
*/
|
|
80
|
+
getDatabase(): Database.Database | null;
|
|
81
|
+
private getDb;
|
|
82
|
+
private getAllScoped;
|
|
83
|
+
private setScopedKey;
|
|
84
|
+
private scopedName;
|
|
85
|
+
declareCollection(input: {
|
|
86
|
+
namespace?: string;
|
|
87
|
+
collection: string;
|
|
88
|
+
columns: readonly CollectionColumn[];
|
|
89
|
+
indexes?: readonly CollectionIndex[];
|
|
90
|
+
}): Promise<void>;
|
|
91
|
+
/** Serialise per-column values for SQL binding: objects → JSON, booleans → 0/1. */
|
|
92
|
+
private serializeColumnValue;
|
|
93
|
+
ensureTable(table: string, schema: TableSchema): Promise<void>;
|
|
94
|
+
tableInsert(table: string, row: Record<string, unknown>): Promise<void>;
|
|
95
|
+
tableUpdate(table: string, filter: Record<string, unknown>, updates: Record<string, unknown>): Promise<number>;
|
|
96
|
+
tableDelete(table: string, filter: Record<string, unknown>): Promise<number>;
|
|
97
|
+
tableQuery(table: string, options?: TableQueryOptions): Promise<readonly Record<string, unknown>[]>;
|
|
98
|
+
tableGet(table: string, filter: Record<string, unknown>): Promise<Record<string, unknown> | null>;
|
|
99
|
+
tableCount(table: string, filter?: Record<string, unknown>): Promise<number>;
|
|
100
|
+
private buildWhere;
|
|
101
|
+
}
|
|
102
|
+
export default SqliteSettingsBackend;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ProviderRegistration, BaseAddon } from '@camstack/types';
|
|
2
|
+
import { SqliteSettingsBackend } from './sqlite-settings-backend.js';
|
|
3
|
+
/**
|
|
4
|
+
* SQLite Settings addon — provides persistent settings storage.
|
|
5
|
+
* Capability: 'settings-store' (singleton)
|
|
6
|
+
*/
|
|
7
|
+
export declare class SqliteSettingsAddon extends BaseAddon {
|
|
8
|
+
private backend;
|
|
9
|
+
constructor();
|
|
10
|
+
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
11
|
+
protected onShutdown(): Promise<void>;
|
|
12
|
+
getBackend(): SqliteSettingsBackend | null;
|
|
13
|
+
}
|
|
14
|
+
export default SqliteSettingsAddon;
|