@camstack/server 1.2.32 → 1.2.34
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/agent/main.js +7 -0
- package/dist/api/trpc/core-cap-bridge.js +0 -1
- package/dist/api/trpc/generated-cap-mounts.js +18 -1
- package/dist/api/trpc/generated-cap-routers.js +1406 -1267
- package/dist/api/trpc/trpc.router.js +0 -2
- package/dist/core/addon/addon-registry.service.js +15 -0
- package/dist/core/addon/prune-misplaced-addons.js +138 -0
- package/dist/main.js +2 -2
- package/dist/server-root/index.js +101 -1
- package/package.json +14 -14
- package/dist/api/addons-custom.router.js +0 -99
- package/dist/api/core/bulk-update-coordinator.js +0 -229
- package/dist/api/core/settings-backend.router.js +0 -121
- package/dist/boot/resume-framework-swap.js +0 -119
- package/dist/core/addon/framework-live-sync.js +0 -344
- package/dist/launcher-framework-swap.js +0 -408
- package/dist/request-framework-swap.js +0 -41
- package/dist/server-root/boot-plan.js +0 -110
- package/dist/server-root/semver-compare.js +0 -45
- package/dist/server-root/server-root-state.js +0 -220
- package/dist/server-root/workspace-detect.js +0 -73
|
@@ -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
|
@@ -183,8 +183,8 @@ async function bootstrap() {
|
|
|
183
183
|
// the starter: the starter runs from the image's /opt closure, which only
|
|
184
184
|
// changes with a new image, while this entry is what `applyServerUpdate`
|
|
185
185
|
// replaces — so the telemetry reaches a running hub the same day it is
|
|
186
|
-
// written. See packages/
|
|
187
|
-
// silent OOMs in ~15h, one nine seconds after a viewer connected).
|
|
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
188
|
(0, system_2.startHeapWatch)();
|
|
189
189
|
// Clean up orphaned processes from previous crashes before starting
|
|
190
190
|
cleanupOrphanProcesses();
|
|
@@ -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.
|
|
3
|
+
"version": "1.2.34",
|
|
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": "
|
|
37
|
-
"@camstack/addon-agent-ui": "
|
|
38
|
-
"@camstack/addon-auth": "
|
|
39
|
-
"@camstack/addon-decoder-nodeav": "
|
|
40
|
-
"@camstack/addon-notifiers": "
|
|
41
|
-
"@camstack/addon-pipeline": "
|
|
42
|
-
"@camstack/addon-pipeline-orchestrator": "
|
|
43
|
-
"@camstack/addon-post-analysis": "
|
|
44
|
-
"@camstack/sdk": "
|
|
45
|
-
"@camstack/shm-ring": "
|
|
46
|
-
"@camstack/system": "
|
|
47
|
-
"@camstack/types": "
|
|
48
|
-
"@camstack/ui-library": "
|
|
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.30",
|
|
47
|
+
"@camstack/types": "1.2.22",
|
|
48
|
+
"@camstack/ui-library": "1.2.18",
|
|
49
49
|
"@fastify/compress": "^9.0.0",
|
|
50
50
|
"@fastify/cookie": "^11.0.2",
|
|
51
51
|
"@fastify/cors": "^11.2.0",
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createAddonsCustomProcedures = createAddonsCustomProcedures;
|
|
4
|
-
/**
|
|
5
|
-
* `api.addons.custom` — generic dispatcher for addon-defined custom actions.
|
|
6
|
-
*
|
|
7
|
-
* Task 7.2 of the device-proxy redesign. Addons declare a catalog of
|
|
8
|
-
* custom actions at boot via `AddonInitResult.customActions` + a single
|
|
9
|
-
* `handleCustomAction(action, input)` handler. The catalog is registered
|
|
10
|
-
* with a per-process `CustomActionRegistry` (Task 7.1). This endpoint is
|
|
11
|
-
* the single tRPC entry point that resolves an `(addonId, action)` pair,
|
|
12
|
-
* validates input + output against the action's Zod schemas, enforces the
|
|
13
|
-
* action's declared auth level, and dispatches to the addon handler.
|
|
14
|
-
*
|
|
15
|
-
* The factory returns a record of procedures (not a router) so the caller
|
|
16
|
-
* can spread it into the existing `addons` namespace:
|
|
17
|
-
*
|
|
18
|
-
* trpcRouter({
|
|
19
|
-
* ...existingAddonsProcedures,
|
|
20
|
-
* ...createAddonsCustomProcedures({ getCustomActionRegistry: ... }),
|
|
21
|
-
* })
|
|
22
|
-
*
|
|
23
|
-
* This avoids `mergeRouters` (which requires sharing the `t` instance
|
|
24
|
-
* across modules) while still mounting the procedure at `api.addons.custom`.
|
|
25
|
-
*/
|
|
26
|
-
const zod_1 = require("zod");
|
|
27
|
-
const server_1 = require("@trpc/server");
|
|
28
|
-
const trpc_middleware_js_1 = require("./trpc/trpc.middleware.js");
|
|
29
|
-
/**
|
|
30
|
-
* Build the procedure record for the `custom` endpoint.
|
|
31
|
-
*
|
|
32
|
-
* The OUTER procedure is `protectedProcedure` — every caller must be
|
|
33
|
-
* authenticated. The INNER per-action auth declared in `spec.auth` is
|
|
34
|
-
* enforced manually by `ensureAuth` because the auth level is not known
|
|
35
|
-
* until after the registry lookup.
|
|
36
|
-
*/
|
|
37
|
-
function createAddonsCustomProcedures(deps) {
|
|
38
|
-
return {
|
|
39
|
-
custom: trpc_middleware_js_1.protectedProcedure
|
|
40
|
-
.input(zod_1.z.object({
|
|
41
|
-
addonId: zod_1.z.string().min(1),
|
|
42
|
-
action: zod_1.z.string().min(1),
|
|
43
|
-
input: zod_1.z.unknown(),
|
|
44
|
-
}))
|
|
45
|
-
.output(zod_1.z.unknown())
|
|
46
|
-
.mutation(async ({ input, ctx }) => {
|
|
47
|
-
const registry = deps.getCustomActionRegistry();
|
|
48
|
-
const entry = registry.resolve(input.addonId, input.action);
|
|
49
|
-
if (!entry) {
|
|
50
|
-
throw new server_1.TRPCError({
|
|
51
|
-
code: 'NOT_FOUND',
|
|
52
|
-
message: `addon '${input.addonId}' has no custom action '${input.action}'`,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
// Per-action authorization. The outer procedure already requires
|
|
56
|
-
// authentication; here we additionally enforce the declared role
|
|
57
|
-
// when it's stricter than 'protected'.
|
|
58
|
-
ensureAuth(ctx, entry.spec.auth);
|
|
59
|
-
// Validate input against the action's declared Zod schema.
|
|
60
|
-
const parsedInput = entry.spec.input.parse(input.input);
|
|
61
|
-
// Dispatch through the addon handler, forwarding the authenticated
|
|
62
|
-
// caller when the action declares `caller: 'required'`. The caller is
|
|
63
|
-
// derived server-side from the request principal (never trusted from
|
|
64
|
-
// input); `ctx.user` is guaranteed present because `ensureAuth` above
|
|
65
|
-
// rejects unauthenticated callers for any non-public action, and the
|
|
66
|
-
// outer `protectedProcedure` rejects them for public ones.
|
|
67
|
-
const caller = entry.spec.caller === 'required' && ctx.user
|
|
68
|
-
? { userId: ctx.user.id, isAdmin: ctx.user.isAdmin }
|
|
69
|
-
: undefined;
|
|
70
|
-
const result = await entry.handler(parsedInput, caller);
|
|
71
|
-
// Validate the addon's output. Crash-early on misbehaving addons.
|
|
72
|
-
return entry.spec.output.parse(result);
|
|
73
|
-
}),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Enforce the action's declared auth level.
|
|
78
|
-
*
|
|
79
|
-
* Mirrors the role checks performed by `protectedProcedure` and
|
|
80
|
-
* `adminProcedure` in trpc.middleware.ts:
|
|
81
|
-
* - public: no auth
|
|
82
|
-
* - protected: any authenticated user
|
|
83
|
-
* - admin: isAdmin only (scoped tokens bounce)
|
|
84
|
-
*/
|
|
85
|
-
function ensureAuth(ctx, level) {
|
|
86
|
-
if (level === 'public')
|
|
87
|
-
return;
|
|
88
|
-
if (!ctx.user) {
|
|
89
|
-
throw new server_1.TRPCError({ code: 'UNAUTHORIZED' });
|
|
90
|
-
}
|
|
91
|
-
if (level === 'protected')
|
|
92
|
-
return;
|
|
93
|
-
if (level === 'admin') {
|
|
94
|
-
if (!ctx.user.isAdmin) {
|
|
95
|
-
throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'custom action requires admin' });
|
|
96
|
-
}
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
}
|