@camstack/server 1.2.30 → 1.2.33

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.
@@ -17,7 +17,6 @@ const logs_router_js_1 = require("../core/logs.router.js");
17
17
  const notifications_router_js_1 = require("../core/notifications.router.js");
18
18
  const repl_router_js_1 = require("../core/repl.router.js");
19
19
  const server_management_provider_js_1 = require("../core/server-management.provider.js");
20
- const settings_backend_router_js_1 = require("../core/settings-backend.router.js");
21
20
  const stream_probe_router_js_1 = require("../core/stream-probe.router.js");
22
21
  const system_events_router_js_1 = require("../core/system-events.router.js");
23
22
  const cap_router_runtime_js_1 = require("./cap-router-runtime.js");
@@ -260,7 +259,6 @@ function buildCapabilityRouters(services) {
260
259
  // NOT the three-level settings gateway — that's the
261
260
  // `addonSettings` cap router (mounted dynamically above).
262
261
  addonSettingsRaw: (0, addon_settings_router_js_1.createAddonSettingsRouter)(services.configService),
263
- settingsBackend: (0, settings_backend_router_js_1.createSettingsBackendRouter)(() => services.addonRegistry.getSettingsBackend()),
264
262
  eventBusProxy: (0, event_bus_proxy_router_js_1.createEventBusProxyRouter)(services.eventBus),
265
263
  repl: (0, repl_router_js_1.createReplRouter)(services.replEngine),
266
264
  systemEvents: (0, system_events_router_js_1.createSystemEventsRouter)(services.eventBus),
@@ -42,6 +42,7 @@ const addon_row_manifest_1 = require("./addon-row-manifest");
42
42
  const runner_convergence_1 = require("./runner-convergence");
43
43
  const package_dir_utils_1 = require("./package-dir-utils");
44
44
  const types_1 = require("@camstack/types");
45
+ const prune_misplaced_addons_js_1 = require("./prune-misplaced-addons.js");
45
46
  const system_1 = require("@camstack/system");
46
47
  const system_2 = require("@camstack/system");
47
48
  const client_1 = require("@trpc/client");
@@ -481,6 +482,20 @@ class AddonRegistryService {
481
482
  this.healthMonitor.recordFailure(fail.packageName, fail.error, fail.addonId);
482
483
  }
483
484
  const loadedAddons = this.addonLoader.listAddons();
485
+ // Self-heal a leftover from before the installer honoured placement:
486
+ // `@camstack/addon-agent-ui` sat in /data/addons on the live hub dated
487
+ // 2026-07-22, skipped by the loop below ever since, and therefore shown as
488
+ // FAILED forever on a node where it cannot run. Fixing the installer was
489
+ // necessary and not sufficient — nothing removed what was already there,
490
+ // and a permanent red entry teaches people to ignore red entries.
491
+ (0, prune_misplaced_addons_js_1.prunePlacementLeftovers)((0, prune_misplaced_addons_js_1.decidePrune)(loadedAddons.map((a) => ({
492
+ packageName: a.packageName,
493
+ dir: path.join(addonsDir, a.packageName),
494
+ placement: (0, types_1.resolveAddonPlacement)(a.declaration),
495
+ })), 'hub'), path.join(addonsDir, '@camstack'), {
496
+ info: (msg, meta) => this.logger.info(msg, { meta }),
497
+ warn: (msg, meta) => this.logger.warn(msg, { meta }),
498
+ });
484
499
  for (const registered of loadedAddons) {
485
500
  // Skip agent-only addons — they never run on the hub, only on remote
486
501
  // agents that opt in via `execution.placement: 'agent-only'`.
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.decidePrune = decidePrune;
37
+ exports.isPrunableDir = isPrunableDir;
38
+ exports.prunePlacementLeftovers = prunePlacementLeftovers;
39
+ /**
40
+ * prune-misplaced-addons — remove an installed addon that can never run here.
41
+ *
42
+ * ## The state this exists to clear
43
+ *
44
+ * `@camstack/addon-agent-ui` declares `execution.placement: 'agent-only'`, and
45
+ * both hub-side load paths honour that: `addon-registry.service.ts` skips it at
46
+ * boot and after an install. `selectHubBootstrapPackages` also subtracts it, so
47
+ * a hub does not install it any more.
48
+ *
49
+ * None of that removes the copy an OLDER build already put on disk. On the live
50
+ * hub `/data/addons/@camstack/addon-agent-ui` is dated 2026-07-22 — eleven days
51
+ * before this was looked at. The loader skips it, so it never initialises, so
52
+ * the fleet UI shows it as **failed**, forever, on a node where it is not
53
+ * supposed to exist. An operator cannot tell that apart from an addon that is
54
+ * genuinely broken, which is the real cost: a permanent red entry teaches
55
+ * people to ignore red entries.
56
+ *
57
+ * Fixing the installer was necessary and not sufficient — the previous fix
58
+ * assumed no leftovers. This closes it.
59
+ *
60
+ * ## Why deleting is the right move
61
+ *
62
+ * An `agent-only` addon on a hub has no dormant use: it cannot be enabled, it
63
+ * cannot be reached, and keeping it costs a false failure. The directory is
64
+ * reinstallable from npm at any time, so removal is not destructive in the way
65
+ * deleting DATA would be. Anything outside `<dataDir>/addons/@camstack/` is
66
+ * refused outright.
67
+ */
68
+ const fs = __importStar(require("node:fs"));
69
+ const path = __importStar(require("node:path"));
70
+ /**
71
+ * Decide what to prune for a node in `role`. Pure — the decision is the part
72
+ * worth testing, and it must never depend on a filesystem to be checked.
73
+ *
74
+ * Only the unambiguous direction is pruned: an `agent-only` addon on a hub. The
75
+ * mirror case (`hub-only` on an agent) is deliberately NOT handled here — an
76
+ * agent seeds its addon set from the hub's closure and a `hub-only` entry there
77
+ * is expected to be inert, so pruning it would fight the seeding rather than
78
+ * fix anything.
79
+ */
80
+ function decidePrune(installed, role) {
81
+ if (role !== 'hub')
82
+ return [];
83
+ return installed
84
+ .filter((a) => a.placement === 'agent-only')
85
+ .map((a) => ({
86
+ packageName: a.packageName,
87
+ dir: a.dir,
88
+ reason: 'agent-only addon installed on the hub — it can never load here',
89
+ }));
90
+ }
91
+ /** Guard: only ever remove something under `<dataDir>/addons/@camstack/`. */
92
+ function isPrunableDir(dir, addonsRoot) {
93
+ const rel = path.relative(addonsRoot, dir);
94
+ if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel))
95
+ return false;
96
+ // Exactly one level below the scope directory — never a nested path.
97
+ return rel.split(path.sep).filter(Boolean).length === 1;
98
+ }
99
+ /**
100
+ * Apply the decisions. Returns what was actually removed.
101
+ *
102
+ * A failure to remove one entry never aborts the rest and never throws into
103
+ * boot: the worst case is the pre-existing state — a stale entry showing as
104
+ * failed — which is exactly what we are trying to improve, not a reason to
105
+ * refuse to start.
106
+ */
107
+ function prunePlacementLeftovers(decisions, addonsRoot, logger, rm = (dir) => fs.rmSync(dir, { recursive: true, force: true })) {
108
+ const removed = [];
109
+ for (const d of decisions) {
110
+ if (!isPrunableDir(d.dir, addonsRoot)) {
111
+ logger.warn('refusing to prune a path outside the addon scope', {
112
+ packageName: d.packageName,
113
+ dir: d.dir,
114
+ addonsRoot,
115
+ });
116
+ continue;
117
+ }
118
+ try {
119
+ rm(d.dir);
120
+ removed.push(d.packageName);
121
+ // Say it out loud: a directory disappearing at boot must never be
122
+ // something the operator discovers by noticing it is gone.
123
+ logger.info('pruned a misplaced addon', {
124
+ packageName: d.packageName,
125
+ dir: d.dir,
126
+ reason: d.reason,
127
+ });
128
+ }
129
+ catch (err) {
130
+ logger.warn('could not prune a misplaced addon', {
131
+ packageName: d.packageName,
132
+ dir: d.dir,
133
+ error: err instanceof Error ? err.message : String(err),
134
+ });
135
+ }
136
+ }
137
+ return removed;
138
+ }
package/dist/main.js CHANGED
@@ -82,11 +82,12 @@ const trpc_context_1 = require("./api/trpc/trpc.context");
82
82
  const addon_upload_1 = require("./api/addon-upload");
83
83
  const server_upload_1 = require("./api/server-upload");
84
84
  const auth_whoami_1 = require("./api/auth-whoami");
85
+ const system_2 = require("@camstack/system");
85
86
  const session_cookie_js_1 = require("./auth/session-cookie.js");
86
87
  const health_routes_1 = require("./api/health/health.routes");
87
88
  const spa_static_1 = require("./api/static/spa-static");
88
89
  const oauth2_routes_js_1 = require("./api/oauth2/oauth2-routes.js");
89
- const system_2 = require("@camstack/system");
90
+ const system_3 = require("@camstack/system");
90
91
  const boot_config_1 = require("./boot/boot-config");
91
92
  const manual_boot_1 = require("./manual-boot");
92
93
  const post_boot_service_1 = require("./boot/post-boot.service");
@@ -178,6 +179,13 @@ function cleanupOrphanProcesses() {
178
179
  }
179
180
  // ---- Bootstrap ----
180
181
  async function bootstrap() {
182
+ // hub-main's own memory, on the record. Deliberately started HERE and not in
183
+ // the starter: the starter runs from the image's /opt closure, which only
184
+ // changes with a new image, while this entry is what `applyServerUpdate`
185
+ // replaces — so the telemetry reaches a running hub the same day it is
186
+ // written. See packages/system/src/kernel/heap-watch.ts for why it exists
187
+ // (four silent OOMs in ~15h, one nine seconds after a viewer connected).
188
+ (0, system_2.startHeapWatch)();
181
189
  // Clean up orphaned processes from previous crashes before starting
182
190
  cleanupOrphanProcesses();
183
191
  // SPA fallback — set later when admin UI is resolved, used by addon route catch-all
@@ -255,8 +263,8 @@ async function bootstrap() {
255
263
  // If registration fails, start in degraded mode: serve a health warning endpoint.
256
264
  // Instantiate new core services
257
265
  const loggingService = app.get(logging_service_1.LoggingService);
258
- const addonRouteRegistry = new system_2.AddonRouteRegistry();
259
- const dataPlaneRegistry = new system_2.DataPlaneRegistry();
266
+ const addonRouteRegistry = new system_3.AddonRouteRegistry();
267
+ const dataPlaneRegistry = new system_3.DataPlaneRegistry();
260
268
  // Use Fastify-managed notification/toast wrappers (globally provided by NotificationModule)
261
269
  const { NotificationServiceWrapper } = await Promise.resolve().then(() => __importStar(require('./core/notification/notification-wrapper.service')));
262
270
  const { ToastServiceWrapper } = await Promise.resolve().then(() => __importStar(require('./core/notification/toast-wrapper.service')));
@@ -785,7 +793,7 @@ async function bootstrap() {
785
793
  const upstreamPath = `/${subPath}${queryString}`;
786
794
  // Take over the socket — `proxyToUpstream` drives the raw response.
787
795
  reply.hijack();
788
- (0, system_2.proxyToUpstream)({
796
+ (0, system_3.proxyToUpstream)({
789
797
  baseUrl: dpMatch.endpoint.baseUrl,
790
798
  secret: dpMatch.endpoint.secret,
791
799
  upstreamPath,
@@ -86,6 +86,7 @@ var require_dist = __commonJS({
86
86
  SERVER_ROOT_DIRNAME: /* @__PURE__ */ __name(() => SERVER_ROOT_DIRNAME, "SERVER_ROOT_DIRNAME"),
87
87
  SERVER_ROOT_STATE_FILE: /* @__PURE__ */ __name(() => SERVER_ROOT_STATE_FILE, "SERVER_ROOT_STATE_FILE"),
88
88
  applyPendingRootSwap: /* @__PURE__ */ __name(() => applyPendingRootSwap, "applyPendingRootSwap"),
89
+ assessImageContract: /* @__PURE__ */ __name(() => assessImageContract, "assessImageContract"),
89
90
  clearPendingRootSwap: /* @__PURE__ */ __name(() => clearPendingRootSwap, "clearPendingRootSwap"),
90
91
  clearRestartIntentMarker: /* @__PURE__ */ __name(() => clearRestartIntentMarker, "clearRestartIntentMarker"),
91
92
  compareSemver: /* @__PURE__ */ __name(() => compareSemver, "compareSemver"),
@@ -712,6 +713,82 @@ var require_dist = __commonJS({
712
713
  };
713
714
  }
714
715
  __name(planBoot, "planBoot");
716
+ function releaseSeries(version) {
717
+ const [major, minor] = version.split(".").map((seg) => {
718
+ const n = Number.parseInt(seg, 10);
719
+ return Number.isNaN(n) ? 0 : n;
720
+ });
721
+ return [
722
+ major ?? 0,
723
+ minor ?? 0
724
+ ];
725
+ }
726
+ __name(releaseSeries, "releaseSeries");
727
+ function sameSeries(a, b) {
728
+ const [aMajor, aMinor] = releaseSeries(a);
729
+ const [bMajor, bMinor] = releaseSeries(b);
730
+ return aMajor === bMajor && aMinor === bMinor;
731
+ }
732
+ __name(sameSeries, "sameSeries");
733
+ function behindMessage(seed, contract, series) {
734
+ const base = `This node is running an image whose baked seed is @camstack/server@${seed}; the deployment contract currently delivers ${contract}. applyServerUpdate keeps the code current but NEVER updates the image \u2014 the starter, the entrypoint and the boot fallback are still the old copies.`;
735
+ if (!series) {
736
+ return `${base} Recreate the container from the contract image to converge.`;
737
+ }
738
+ return `${base} The seed is a full release series behind \u2014 the shape of a pinned tag or an image repository that no longer receives builds, where a docker pull fixes nothing. Verify the container's image reference against the deployment templates.`;
739
+ }
740
+ __name(behindMessage, "behindMessage");
741
+ function assessImageContract(inputs) {
742
+ const { seedVersion, latestVersion, runningVersion } = inputs;
743
+ if (seedVersion === null) {
744
+ return {
745
+ state: "unknown",
746
+ seedVersion: null,
747
+ contractVersion: latestVersion,
748
+ contractSource: latestVersion !== null ? "registry" : null,
749
+ message: "No baked seed closure detected (dev workspace) \u2014 no image contract to compare."
750
+ };
751
+ }
752
+ const contractVersion = latestVersion ?? (runningVersion !== null && compareSemver(seedVersion, runningVersion) < 0 ? runningVersion : null);
753
+ if (contractVersion === null) {
754
+ return {
755
+ state: "unknown",
756
+ seedVersion,
757
+ contractVersion: null,
758
+ contractSource: null,
759
+ message: `This node is running an image whose baked seed is @camstack/server@${seedVersion}; no registry check has run yet, so the release the contract image delivers is unknown. Run a server-update check to verify the image against the contract.`
760
+ };
761
+ }
762
+ const contractSource = latestVersion !== null ? "registry" : "running";
763
+ const cmp = compareSemver(seedVersion, contractVersion);
764
+ if (cmp === 0) {
765
+ return {
766
+ state: "in-sync",
767
+ seedVersion,
768
+ contractVersion,
769
+ contractSource,
770
+ message: `Image seed @camstack/server@${seedVersion} matches the current release \u2014 this node is running the image the deployment contract names.`
771
+ };
772
+ }
773
+ if (cmp > 0) {
774
+ return {
775
+ state: "ahead",
776
+ seedVersion,
777
+ contractVersion,
778
+ contractSource,
779
+ message: `Image seed @camstack/server@${seedVersion} is NEWER than the best-known release ${contractVersion} \u2014 the registry check is stale or the registry was unreachable. Re-run a server-update check.`
780
+ };
781
+ }
782
+ const series = !sameSeries(seedVersion, contractVersion);
783
+ return {
784
+ state: series ? "behind-series" : "behind-patch",
785
+ seedVersion,
786
+ contractVersion,
787
+ contractSource,
788
+ message: behindMessage(seedVersion, contractVersion, series)
789
+ };
790
+ }
791
+ __name(assessImageContract, "assessImageContract");
715
792
  var fs4 = __toESM2(require("fs"));
716
793
  var path4 = __toESM2(require("path"));
717
794
  function detectWorkspaceRoot(fromDir) {
@@ -955,7 +1032,16 @@ var require_dist = __commonJS({
955
1032
  pendingVersion: pending?.version ?? null,
956
1033
  rolledBack: null,
957
1034
  stateFileCorrupt: false,
958
- lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null
1035
+ lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null,
1036
+ // Seed-vs-contract verdict: `runningVersion` can be perfectly current
1037
+ // (applyServerUpdate) while the IMAGE is weeks old from a dead repo —
1038
+ // 2026-08-02, hub on `camstack-server:intel-1.1.74` with every other
1039
+ // field green. See `image-contract.ts` for the full account.
1040
+ imageContract: assessImageContract({
1041
+ seedVersion: this.seedVersion(),
1042
+ latestVersion,
1043
+ runningVersion
1044
+ })
959
1045
  };
960
1046
  }
961
1047
  // ── Check ─────────────────────────────────────────────────────────────
@@ -989,6 +1075,20 @@ var require_dist = __commonJS({
989
1075
  checkedAtMs: this.now(),
990
1076
  error: null
991
1077
  };
1078
+ const imageContract = assessImageContract({
1079
+ seedVersion: this.seedVersion(),
1080
+ latestVersion,
1081
+ runningVersion
1082
+ });
1083
+ if (imageContract.state === "behind-patch" || imageContract.state === "behind-series") {
1084
+ this.logger.warn(`image seed diverges from the release contract: ${imageContract.message}`, {
1085
+ meta: {
1086
+ state: imageContract.state,
1087
+ seedVersion: imageContract.seedVersion,
1088
+ contractVersion: imageContract.contractVersion
1089
+ }
1090
+ });
1091
+ }
992
1092
  const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
993
1093
  return {
994
1094
  packageName: this.spec.packageName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.30",
3
+ "version": "1.2.33",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -33,19 +33,19 @@
33
33
  ]
34
34
  },
35
35
  "dependencies": {
36
- "@camstack/addon-admin-ui": "1.2.20",
37
- "@camstack/addon-agent-ui": "1.2.8",
38
- "@camstack/addon-auth": "1.2.8",
39
- "@camstack/addon-decoder-nodeav": "1.2.8",
40
- "@camstack/addon-notifiers": "1.2.10",
41
- "@camstack/addon-pipeline": "1.2.36",
42
- "@camstack/addon-pipeline-orchestrator": "1.2.17",
43
- "@camstack/addon-post-analysis": "1.2.21",
44
- "@camstack/sdk": "1.2.8",
45
- "@camstack/shm-ring": "1.1.8",
46
- "@camstack/system": "1.2.27",
47
- "@camstack/types": "1.2.19",
48
- "@camstack/ui-library": "1.2.15",
36
+ "@camstack/addon-admin-ui": "1.2.22",
37
+ "@camstack/addon-agent-ui": "1.2.10",
38
+ "@camstack/addon-auth": "1.2.10",
39
+ "@camstack/addon-decoder-nodeav": "1.2.9",
40
+ "@camstack/addon-notifiers": "1.2.11",
41
+ "@camstack/addon-pipeline": "1.2.39",
42
+ "@camstack/addon-pipeline-orchestrator": "1.2.19",
43
+ "@camstack/addon-post-analysis": "1.2.23",
44
+ "@camstack/sdk": "1.2.9",
45
+ "@camstack/shm-ring": "1.1.9",
46
+ "@camstack/system": "1.2.29",
47
+ "@camstack/types": "1.2.21",
48
+ "@camstack/ui-library": "1.2.17",
49
49
  "@fastify/compress": "^9.0.0",
50
50
  "@fastify/cookie": "^11.0.2",
51
51
  "@fastify/cors": "^11.2.0",
@@ -1,121 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createSettingsBackendRouter = createSettingsBackendRouter;
4
- /**
5
- * Settings backend router — tRPC proxy for ISettingsBackend operations.
6
- *
7
- * Exposes the core collection-based operations (get, set, query, insert,
8
- * update, delete, count, isEmpty) so forked worker addons can use
9
- * context.settingsBackend via tRPC instead of requiring in-process access
10
- * to the SQLite database.
11
- *
12
- * Introduced for Task 11 — TrpcSettingsBackend for forked workers.
13
- */
14
- const zod_1 = require("zod");
15
- const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
16
- // ---------------------------------------------------------------------------
17
- // Zod schemas
18
- // ---------------------------------------------------------------------------
19
- const CollectionKeySchema = zod_1.z.object({
20
- collection: zod_1.z.string(),
21
- key: zod_1.z.string(),
22
- });
23
- const SetValueSchema = zod_1.z.object({
24
- collection: zod_1.z.string(),
25
- key: zod_1.z.string(),
26
- value: zod_1.z.unknown(),
27
- });
28
- const QueryFilterSchema = zod_1.z
29
- .object({
30
- where: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
31
- whereIn: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.unknown())).optional(),
32
- whereBetween: zod_1.z.record(zod_1.z.string(), zod_1.z.tuple([zod_1.z.unknown(), zod_1.z.unknown()])).optional(),
33
- orderBy: zod_1.z
34
- .object({
35
- field: zod_1.z.string(),
36
- direction: zod_1.z.enum(['asc', 'desc']),
37
- })
38
- .optional(),
39
- limit: zod_1.z.number().optional(),
40
- offset: zod_1.z.number().optional(),
41
- })
42
- .optional();
43
- const QueryInputSchema = zod_1.z.object({
44
- collection: zod_1.z.string(),
45
- filter: QueryFilterSchema,
46
- });
47
- const InsertInputSchema = zod_1.z.object({
48
- collection: zod_1.z.string(),
49
- record: zod_1.z.object({
50
- id: zod_1.z.string(),
51
- data: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
52
- }),
53
- });
54
- const UpdateInputSchema = zod_1.z.object({
55
- collection: zod_1.z.string(),
56
- id: zod_1.z.string(),
57
- data: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
58
- });
59
- const CountInputSchema = zod_1.z.object({
60
- collection: zod_1.z.string(),
61
- filter: QueryFilterSchema,
62
- });
63
- const IsEmptyInputSchema = zod_1.z.object({
64
- collection: zod_1.z.string(),
65
- });
66
- // ---------------------------------------------------------------------------
67
- // Router factory
68
- // ---------------------------------------------------------------------------
69
- function createSettingsBackendRouter(getBackend) {
70
- const requireBackend = () => {
71
- const backend = getBackend();
72
- if (!backend) {
73
- throw new Error('Settings backend not available — settings-store addon may not be initialized yet');
74
- }
75
- return backend;
76
- };
77
- return (0, trpc_middleware_js_1.trpcRouter)({
78
- get: trpc_middleware_js_1.protectedProcedure.input(CollectionKeySchema).query(async ({ input }) => {
79
- const result = await requireBackend().get(input);
80
- return { value: result };
81
- }),
82
- set: trpc_middleware_js_1.protectedProcedure.input(SetValueSchema).mutation(async ({ input }) => {
83
- await requireBackend().set({
84
- collection: input.collection,
85
- key: input.key,
86
- value: input.value,
87
- });
88
- return { success: true };
89
- }),
90
- query: trpc_middleware_js_1.protectedProcedure.input(QueryInputSchema).query(async ({ input }) => {
91
- const records = await requireBackend().query({
92
- collection: input.collection,
93
- filter: input.filter ?? undefined,
94
- });
95
- return { records: records.map((r) => ({ id: r.id, data: r.data })) };
96
- }),
97
- insert: trpc_middleware_js_1.protectedProcedure.input(InsertInputSchema).mutation(async ({ input }) => {
98
- await requireBackend().insert(input);
99
- return { success: true };
100
- }),
101
- update: trpc_middleware_js_1.protectedProcedure.input(UpdateInputSchema).mutation(async ({ input }) => {
102
- await requireBackend().update(input);
103
- return { success: true };
104
- }),
105
- delete: trpc_middleware_js_1.protectedProcedure.input(CollectionKeySchema).mutation(async ({ input }) => {
106
- await requireBackend().delete(input);
107
- return { success: true };
108
- }),
109
- count: trpc_middleware_js_1.protectedProcedure.input(CountInputSchema).query(async ({ input }) => {
110
- const result = await requireBackend().count({
111
- collection: input.collection,
112
- filter: input.filter ?? undefined,
113
- });
114
- return { count: result };
115
- }),
116
- isEmpty: trpc_middleware_js_1.protectedProcedure.input(IsEmptyInputSchema).query(async ({ input }) => {
117
- const result = await requireBackend().isEmpty(input);
118
- return { empty: result };
119
- }),
120
- });
121
- }