@camstack/server 1.2.89 → 1.2.91

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.
@@ -0,0 +1,99 @@
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
+ }
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BulkUpdateCoordinator = void 0;
4
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument -- The server installs @camstack/types@0.1.38 (last published) in server/backend/node_modules, while the workspace has 0.1.39 with the new BulkUpdate* types. ESLint's type-checker resolves against 0.1.38 and treats the new imports as `any`. Runtime is correct because Node module resolution walks up to root node_modules → workspace symlink. This disable mirrors the pattern in cap-providers.ts (same root cause). Will resolve when 0.1.39 is published and the local dist is synced. */
5
+ const node_crypto_1 = require("node:crypto");
6
+ const types_1 = require("@camstack/types");
7
+ const DEFAULT_CLEANUP_AFTER_MS = 5 * 60 * 1_000;
8
+ class BulkUpdateCoordinator {
9
+ deps;
10
+ states = new Map();
11
+ cancelFlags = new Map();
12
+ /**
13
+ * Tracks wall-clock time (ms) when each bulk completed. Used for lazy
14
+ * cleanup in `get()` — avoids scheduling a fake-timer `setTimeout` that
15
+ * would be eagerly fired by `vi.runAllTimersAsync()` in tests.
16
+ */
17
+ completedWallMs = new Map();
18
+ /** Tracks which nodeIds currently have an active (non-completed) bulk update. */
19
+ activeNodeIds = new Set();
20
+ now;
21
+ cleanupAfterMs;
22
+ /** Wall-clock source. Fake timers intercept `Date.now`, so tests can advance via `advanceTimersByTimeAsync`. */
23
+ wallNow;
24
+ constructor(deps) {
25
+ this.deps = deps;
26
+ this.now = deps.clock ?? (() => Date.now());
27
+ this.cleanupAfterMs = deps.cleanupAfterMs ?? DEFAULT_CLEANUP_AFTER_MS;
28
+ this.wallNow = () => Date.now();
29
+ }
30
+ // ── Public API ────────────────────────────────────────────────────
31
+ start(input) {
32
+ if (this.activeNodeIds.has(input.nodeId)) {
33
+ throw new Error(`Bulk update already in progress for node ${input.nodeId}`);
34
+ }
35
+ const id = (0, node_crypto_1.randomUUID)();
36
+ const items = input.items.map((i) => ({
37
+ name: i.name,
38
+ isSystem: i.isSystem,
39
+ // fromVersion: the cap interface receives name+version+isSystem only;
40
+ // the caller (cap-providers.ts) may enrich this with the current version
41
+ // if available. Empty string is acceptable per plan spec.
42
+ fromVersion: '',
43
+ toVersion: i.version,
44
+ status: 'queued',
45
+ }));
46
+ const state = {
47
+ id,
48
+ nodeId: input.nodeId,
49
+ startedAtMs: this.now(),
50
+ total: items.length,
51
+ completed: 0,
52
+ failed: 0,
53
+ current: null,
54
+ phase: 'regular',
55
+ cancelled: false,
56
+ items,
57
+ };
58
+ this.states.set(id, state);
59
+ this.activeNodeIds.add(input.nodeId);
60
+ const cancelFlag = { cancelled: false };
61
+ this.cancelFlags.set(id, cancelFlag);
62
+ // Emit initial state so clients see the bulk as started immediately
63
+ this.emit(state);
64
+ void this.runLoop(id, cancelFlag).catch((err) => {
65
+ this.deps.logger.error('BulkUpdateCoordinator: loop crashed unexpectedly', err);
66
+ });
67
+ return { id };
68
+ }
69
+ get(id) {
70
+ const state = this.states.get(id);
71
+ if (state === undefined)
72
+ return null;
73
+ // Lazy cleanup: purge if the wall-clock elapsed since completion exceeds threshold.
74
+ // This avoids scheduling a long-lived setTimeout that would be eagerly fired
75
+ // by vi.runAllTimersAsync() in tests.
76
+ const completedWall = this.completedWallMs.get(id);
77
+ if (completedWall !== undefined && this.wallNow() - completedWall >= this.cleanupAfterMs) {
78
+ this.purge(id);
79
+ return null;
80
+ }
81
+ return state;
82
+ }
83
+ list(nodeId) {
84
+ const all = [...this.states.keys()]
85
+ .map((id) => this.get(id)) // get() applies lazy-cleanup
86
+ .filter((s) => s !== null);
87
+ return nodeId === undefined ? all : all.filter((s) => s.nodeId === nodeId);
88
+ }
89
+ cancel(id) {
90
+ const state = this.states.get(id);
91
+ const flag = this.cancelFlags.get(id);
92
+ if (state === undefined || flag === undefined)
93
+ return { cancelled: false };
94
+ // Once restarting, the hub restart is committed — cancel has no effect.
95
+ if (state.phase === 'restarting')
96
+ return { cancelled: false };
97
+ // Already completed.
98
+ if (state.completedAtMs !== undefined)
99
+ return { cancelled: false };
100
+ flag.cancelled = true;
101
+ this.mutate(id, (s) => ({ ...s, cancelled: true }));
102
+ return { cancelled: true };
103
+ }
104
+ // ── Internal loop ─────────────────────────────────────────────────
105
+ async runLoop(id, cancelFlag) {
106
+ const initial = this.states.get(id);
107
+ // ── Phase 1: regular addons ──────────────────────────────────────
108
+ this.transitionPhase(id, 'regular');
109
+ for (const item of initial.items.filter((i) => !i.isSystem)) {
110
+ if (cancelFlag.cancelled)
111
+ break;
112
+ await this.processItem(id, item, false);
113
+ }
114
+ // ── Phase 2: system packages (deferRestart: true) ────────────────
115
+ if (!cancelFlag.cancelled && initial.items.some((i) => i.isSystem)) {
116
+ this.transitionPhase(id, 'system');
117
+ for (const item of initial.items.filter((i) => i.isSystem)) {
118
+ if (cancelFlag.cancelled)
119
+ break;
120
+ await this.processItem(id, item, true);
121
+ }
122
+ // ── Phase 3: single restart ──────────────────────────────────
123
+ const anySystemPendingRestart = this.states
124
+ .get(id)
125
+ .items.some((i) => i.isSystem && i.status === 'done-pending-restart');
126
+ if (anySystemPendingRestart && !cancelFlag.cancelled) {
127
+ this.transitionPhase(id, 'restarting');
128
+ try {
129
+ await this.deps.restartServer({ confirm: true });
130
+ // NOTE: In production, restartServer kills+respawns the hub process.
131
+ // Code below this point will not execute in that scenario.
132
+ // If the mock/stub returns (e.g. in tests), we fall through to finalizing.
133
+ }
134
+ catch (err) {
135
+ // Restart failed but the npm installs already completed. Promote all
136
+ // done-pending-restart items to done with a caveat error so the UI
137
+ // can inform the user that a manual restart is needed.
138
+ this.deps.logger.error('BulkUpdateCoordinator: restart failed', err);
139
+ const errMsg = err instanceof Error ? err.message : String(err);
140
+ for (const it of this.states.get(id).items) {
141
+ if (it.status === 'done-pending-restart') {
142
+ this.setItemStatus(id, it.name, 'done', {
143
+ error: `Restart failed; manual restart required (${errMsg})`,
144
+ });
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ // ── Phase 4: finalize ────────────────────────────────────────────
151
+ // Reached when:
152
+ // a) no system packages at all, OR
153
+ // b) restart failed (process continued), OR
154
+ // c) cancelled before the restart phase.
155
+ this.transitionPhase(id, 'finalizing');
156
+ this.completeBulk(id);
157
+ }
158
+ async processItem(id, item, isSystem) {
159
+ this.setItemStatus(id, item.name, 'updating', { startedAtMs: this.now() });
160
+ this.mutate(id, (s) => ({ ...s, current: item.name }));
161
+ this.emit(this.states.get(id));
162
+ try {
163
+ if (isSystem) {
164
+ await this.deps.updateFrameworkPackage({
165
+ packageName: item.name,
166
+ version: item.toVersion,
167
+ deferRestart: true,
168
+ });
169
+ this.setItemStatus(id, item.name, 'done-pending-restart', { completedAtMs: this.now() });
170
+ }
171
+ else {
172
+ await this.deps.updateAddon({ name: item.name, version: item.toVersion });
173
+ this.setItemStatus(id, item.name, 'done', { completedAtMs: this.now() });
174
+ }
175
+ }
176
+ catch (err) {
177
+ const msg = err instanceof Error ? err.message : String(err);
178
+ this.setItemStatus(id, item.name, 'failed', { error: msg, completedAtMs: this.now() });
179
+ }
180
+ this.mutate(id, (s) => ({ ...s, current: null }));
181
+ this.emit(this.states.get(id));
182
+ }
183
+ // ── State mutation helpers ────────────────────────────────────────
184
+ setItemStatus(id, name, status, fields = {}) {
185
+ this.mutate(id, (s) => {
186
+ const items = s.items.map((it) => (it.name === name ? { ...it, status, ...fields } : it));
187
+ // completed = all terminal states: done | done-pending-restart | failed
188
+ const completed = items.filter((it) => it.status === 'done' || it.status === 'done-pending-restart' || it.status === 'failed').length;
189
+ const failed = items.filter((it) => it.status === 'failed').length;
190
+ return { ...s, items, completed, failed };
191
+ });
192
+ }
193
+ transitionPhase(id, phase) {
194
+ this.mutate(id, (s) => ({ ...s, phase }));
195
+ this.emit(this.states.get(id));
196
+ }
197
+ completeBulk(id) {
198
+ this.mutate(id, (s) => ({ ...s, completedAtMs: this.now(), current: null }));
199
+ this.emit(this.states.get(id));
200
+ // Free the nodeId slot so a new bulk for the same node can be started
201
+ const nodeId = this.states.get(id).nodeId;
202
+ this.activeNodeIds.delete(nodeId);
203
+ // Record wall-clock completion time for lazy cleanup in `get()`.
204
+ // We intentionally avoid scheduling a setTimeout here: a long-lived
205
+ // setTimeout (5 min) would be eagerly fired by vi.runAllTimersAsync()
206
+ // in tests, causing `get()` to return null immediately after the run.
207
+ // Instead, `get()` lazily checks whether the cleanup threshold has
208
+ // elapsed using Date.now() — which fake timers DO advance via
209
+ // advanceTimersByTimeAsync(), making the cleanup testable without
210
+ // a long-running timer.
211
+ this.completedWallMs.set(id, this.wallNow());
212
+ }
213
+ purge(id) {
214
+ this.states.delete(id);
215
+ this.cancelFlags.delete(id);
216
+ this.completedWallMs.delete(id);
217
+ }
218
+ /** Immutably update the state for the given id. No-op if id is unknown. */
219
+ mutate(id, update) {
220
+ const current = this.states.get(id);
221
+ if (current === undefined)
222
+ return;
223
+ this.states.set(id, update(current));
224
+ }
225
+ emit(state) {
226
+ this.deps.eventBus.emit(types_1.EventCategory.AddonsBulkUpdateProgress, state);
227
+ }
228
+ }
229
+ exports.BulkUpdateCoordinator = BulkUpdateCoordinator;
@@ -1232,7 +1232,7 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
1232
1232
  const nodeId = input.nodeId;
1233
1233
  const updates = nodeId === undefined || isHubNode(nodeId)
1234
1234
  ? await ps.checkUpdates()
1235
- : await ps.checkUpdatesForInstalled(await fetchAgentInstalledPackages(broker, nodeId));
1235
+ : await ps.checkUpdatesForInstalled(await fetchAgentInstalledPackages(broker, nodeId), nodeId);
1236
1236
  return updates.map((u) => ({ ...u, isSystem: frameworkAllowSet.has(u.name) }));
1237
1237
  },
1238
1238
  updatePackage: async (input) => {
@@ -1271,7 +1271,7 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
1271
1271
  return ps.checkUpdates(true);
1272
1272
  // Agent rosters carry no hub-side cache — the diff is always live.
1273
1273
  const installed = await fetchAgentInstalledPackages(broker, nodeId);
1274
- return ps.checkUpdatesForInstalled(installed);
1274
+ return ps.checkUpdatesForInstalled(installed, nodeId);
1275
1275
  },
1276
1276
  restartServer: async () => ps.restartServer(ctx.user?.username ?? ctx.user?.id),
1277
1277
  getLastRestart: async () => {
@@ -0,0 +1,121 @@
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
+ }
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.spaAssetCacheControl = spaAssetCacheControl;
11
11
  exports.addonBundleCacheControl = addonBundleCacheControl;
12
12
  exports.contentTypeForPath = contentTypeForPath;
13
+ exports.isRetiredPublicPath = isRetiredPublicPath;
13
14
  /**
14
15
  * Cache-Control value for a static SPA asset addressed by its dist-relative
15
16
  * path. Policy (shared by admin-ui + viewer-ui):
@@ -24,7 +25,10 @@ function spaAssetCacheControl(rel) {
24
25
  if (/^(sw\.js|registerSW\.js|workbox-.*\.js|manifest\.webmanifest)$/.test(base)) {
25
26
  return 'no-cache, must-revalidate';
26
27
  }
27
- const inAssetDir = rel.startsWith('assets/') || rel.startsWith('_expo/');
28
+ const inAssetDir = rel.startsWith('assets/') ||
29
+ rel.startsWith('_expo/') ||
30
+ rel.includes('/assets/') ||
31
+ rel.includes('/_expo/');
28
32
  if (inAssetDir && /-[A-Za-z0-9_-]{8,}\./.test(base)) {
29
33
  return 'public, max-age=31536000, immutable';
30
34
  }
@@ -90,3 +94,8 @@ function contentTypeForPath(pathname) {
90
94
  const ext = dot >= 0 ? base.slice(dot + 1).toLowerCase() : '';
91
95
  return CONTENT_TYPES[ext] ?? 'application/octet-stream';
92
96
  }
97
+ /** Paths intentionally retired instead of falling through to the admin SPA. */
98
+ function isRetiredPublicPath(url) {
99
+ const pathname = url.split('?')[0] ?? url;
100
+ return (pathname === '/addon/stream-broker/embed' || pathname.startsWith('/addon/stream-broker/embed/'));
101
+ }
@@ -8770,6 +8770,24 @@ function createCapRouter_terminalSession(getProvider, createRemoteProxy) {
8770
8770
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8771
8771
  return p.resize(methodInput);
8772
8772
  }),
8773
+ pullOutput: trpc_middleware_js_1.adminProcedure
8774
+ .input(types_109.terminalSessionCapability.methods.pullOutput.input.loose())
8775
+ .output(types_109.terminalSessionCapability.methods.pullOutput.output)
8776
+ .mutation(async ({ input, ctx }) => {
8777
+ const { nodeId, ...methodInput } = input;
8778
+ const p = resolveProvider('terminal-session', nodeId, () => getProvider(ctx), createRemoteProxy);
8779
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8780
+ return p.pullOutput(methodInput);
8781
+ }),
8782
+ writeInput: trpc_middleware_js_1.adminProcedure
8783
+ .input(types_109.terminalSessionCapability.methods.writeInput.input.loose())
8784
+ .output(types_109.terminalSessionCapability.methods.writeInput.output)
8785
+ .mutation(async ({ input, ctx }) => {
8786
+ const { nodeId, ...methodInput } = input;
8787
+ const p = resolveProvider('terminal-session', nodeId, () => getProvider(ctx), createRemoteProxy);
8788
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8789
+ return p.writeInput(methodInput);
8790
+ }),
8773
8791
  close: trpc_middleware_js_1.adminProcedure
8774
8792
  .input(types_109.terminalSessionCapability.methods.close.input.loose())
8775
8793
  .output(types_109.terminalSessionCapability.methods.close.output)
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * Boot-time framework-swap job resume + health confirm.
4
+ *
5
+ * After a framework swap reboot, `post-boot.service.ts` calls this once the
6
+ * hub is healthy. It:
7
+ * 1. Reads `.framework-swap-confirm.json` (written by the launcher on apply).
8
+ * 2. Marks the journal task `applied` → `done`, then finalises the job →
9
+ * `completed`.
10
+ * 3. Calls `confirmFrameworkSwapHealthy` to delete the confirm marker +
11
+ * backup dirs (disarms the crash-loop rollback).
12
+ *
13
+ * Best-effort: a missing/corrupt journal is tolerated — `confirmFrameworkSwapHealthy`
14
+ * is still called so the rollback is always disarmed when the hub boots healthy.
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.resumeFrameworkSwapJob = resumeFrameworkSwapJob;
51
+ const fs = __importStar(require("node:fs"));
52
+ const path = __importStar(require("node:path"));
53
+ const types_1 = require("@camstack/types");
54
+ const system_1 = require("@camstack/system");
55
+ const launcher_framework_swap_js_1 = require("../launcher-framework-swap.js");
56
+ const lifecycle_journal_path_js_1 = require("../lifecycle-journal-path.js");
57
+ const SWAP_CONFIRM_FILE = '.framework-swap-confirm.json';
58
+ /**
59
+ * Resume a framework-swap journal job to `done`/`completed` and confirm the
60
+ * hub is healthy (deletes the confirm marker + backups).
61
+ *
62
+ * @returns `{ resumed: false }` when no confirm marker exists.
63
+ * `{ resumed: true, jobId }` when the marker was found and processed.
64
+ * Never throws — errors are swallowed to avoid crashing the post-boot path.
65
+ */
66
+ async function resumeFrameworkSwapJob(dataDir) {
67
+ try {
68
+ const confirmMarker = readConfirmMarker(dataDir);
69
+ if (confirmMarker === null) {
70
+ return { resumed: false };
71
+ }
72
+ const { jobId, taskId } = confirmMarker;
73
+ let journalPatched = false;
74
+ try {
75
+ const journal = new system_1.JobJournal((0, lifecycle_journal_path_js_1.lifecycleJobsDir)(dataDir));
76
+ const job = journal.getJob(jobId);
77
+ if (job !== null) {
78
+ const task = job.tasks.find((t) => t.taskId === taskId);
79
+ if (task !== undefined && task.phase === 'applied') {
80
+ journal.patchTask(jobId, taskId, { phase: 'done', finishedAtMs: Date.now() });
81
+ // Single-task framework job: if all tasks are now terminal and none
82
+ // failed, mark the job completed (mirrors the engine's finalize logic).
83
+ const updatedJob = journal.getJob(jobId);
84
+ if (updatedJob !== null) {
85
+ const allTerminal = updatedJob.tasks.every((t) => t.phase === 'done' || t.phase === 'failed' || t.phase === 'skipped');
86
+ const anyFailed = updatedJob.tasks.some((t) => t.phase === 'failed');
87
+ if (allTerminal && !anyFailed) {
88
+ journal.setJobState(jobId, 'completed');
89
+ }
90
+ }
91
+ journalPatched = true;
92
+ }
93
+ }
94
+ }
95
+ catch {
96
+ // Journal is missing or corrupt — still clean up the confirm marker so
97
+ // the rollback is disarmed on a healthy hub boot.
98
+ }
99
+ (0, launcher_framework_swap_js_1.confirmFrameworkSwapHealthy)(dataDir);
100
+ return journalPatched ? { resumed: true, jobId } : { resumed: false };
101
+ }
102
+ catch {
103
+ // Never crash the caller (post-boot service).
104
+ return { resumed: false };
105
+ }
106
+ }
107
+ /** Read and shape-check the confirm marker. Returns null on any error. */
108
+ function readConfirmMarker(dataDir) {
109
+ try {
110
+ const raw = JSON.parse(fs.readFileSync(path.join(dataDir, SWAP_CONFIRM_FILE), 'utf-8'));
111
+ const parsed = types_1.frameworkSwapConfirmSchema.safeParse(raw);
112
+ if (!parsed.success)
113
+ return null;
114
+ return { jobId: parsed.data.jobId, taskId: parsed.data.taskId };
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }