@camstack/server 1.2.476 → 1.2.478

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.
@@ -569,6 +569,14 @@ function createCapRouter_addons(getProvider, _createRemoteProxy) {
569
569
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
570
570
  return p.uninstallPackage(input);
571
571
  }),
572
+ listInstallations: trpc_middleware_js_1.adminProcedure
573
+ .input(types_10.addonsCapability.methods.listInstallations.input.loose())
574
+ .output(types_10.addonsCapability.methods.listInstallations.output)
575
+ .query(async ({ input, ctx }) => {
576
+ const p = requireCapProvider('addons', () => getProvider(ctx));
577
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
578
+ return p.listInstallations(input);
579
+ }),
572
580
  reloadPackages: trpc_middleware_js_1.adminProcedure
573
581
  .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
574
582
  .output(types_10.addonsCapability.methods.reloadPackages.output)
@@ -2434,15 +2442,6 @@ function createCapRouter_dayNight(getProvider, createRemoteProxy) {
2434
2442
  }
2435
2443
  function createCapRouter_decoder(getProvider, createRemoteProxy) {
2436
2444
  return (0, trpc_middleware_js_1.trpcRouter)({
2437
- supportsCodec: trpc_middleware_js_1.protectedProcedure
2438
- .input(types_42.decoderCapability.methods.supportsCodec.input.loose())
2439
- .output(types_42.decoderCapability.methods.supportsCodec.output)
2440
- .query(async ({ input, ctx }) => {
2441
- const { nodeId, ...methodInput } = input;
2442
- const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
2443
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2444
- return p.supportsCodec(methodInput);
2445
- }),
2446
2445
  getInfo: trpc_middleware_js_1.protectedProcedure
2447
2446
  .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
2448
2447
  .output(types_42.decoderCapability.methods.getInfo.output)
@@ -2450,103 +2449,6 @@ function createCapRouter_decoder(getProvider, createRemoteProxy) {
2450
2449
  const p = resolveProvider('decoder', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
2451
2450
  return p.getInfo();
2452
2451
  }),
2453
- createSession: trpc_middleware_js_1.protectedProcedure
2454
- .input(types_42.decoderCapability.methods.createSession.input.loose())
2455
- .output(types_42.decoderCapability.methods.createSession.output)
2456
- .query(async ({ input, ctx }) => {
2457
- const { nodeId, ...methodInput } = input;
2458
- const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
2459
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2460
- return p.createSession(methodInput);
2461
- }),
2462
- destroySession: trpc_middleware_js_1.protectedProcedure
2463
- .input(types_42.decoderCapability.methods.destroySession.input.loose())
2464
- .output(types_42.decoderCapability.methods.destroySession.output)
2465
- .query(async ({ input, ctx }) => {
2466
- const { nodeId, ...methodInput } = input;
2467
- const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
2468
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2469
- return p.destroySession(methodInput);
2470
- }),
2471
- pushPacket: trpc_middleware_js_1.protectedProcedure
2472
- .input(types_42.decoderCapability.methods.pushPacket.input.loose())
2473
- .output(types_42.decoderCapability.methods.pushPacket.output)
2474
- .query(async ({ input, ctx }) => {
2475
- const { nodeId, ...methodInput } = input;
2476
- const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
2477
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2478
- return p.pushPacket(methodInput);
2479
- }),
2480
- openStream: trpc_middleware_js_1.protectedProcedure
2481
- .input(types_42.decoderCapability.methods.openStream.input.loose())
2482
- .output(types_42.decoderCapability.methods.openStream.output)
2483
- .query(async ({ input, ctx }) => {
2484
- const { nodeId, ...methodInput } = input;
2485
- const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
2486
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2487
- return p.openStream(methodInput);
2488
- }),
2489
- pullFrames: trpc_middleware_js_1.protectedProcedure
2490
- .input(types_42.decoderCapability.methods.pullFrames.input.loose())
2491
- .output(types_42.decoderCapability.methods.pullFrames.output)
2492
- .query(async ({ input, ctx }) => {
2493
- const { nodeId, ...methodInput } = input;
2494
- const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
2495
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2496
- return p.pullFrames(methodInput);
2497
- }),
2498
- pullHandles: trpc_middleware_js_1.protectedProcedure
2499
- .input(types_42.decoderCapability.methods.pullHandles.input.loose())
2500
- .output(types_42.decoderCapability.methods.pullHandles.output)
2501
- .query(async ({ input, ctx }) => {
2502
- const { nodeId, ...methodInput } = input;
2503
- const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
2504
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2505
- return p.pullHandles(methodInput);
2506
- }),
2507
- getFrame: trpc_middleware_js_1.protectedProcedure
2508
- .input(types_42.decoderCapability.methods.getFrame.input.loose())
2509
- .output(types_42.decoderCapability.methods.getFrame.output)
2510
- .query(async ({ input, ctx }) => {
2511
- const { nodeId, ...methodInput } = input;
2512
- const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
2513
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2514
- return p.getFrame(methodInput);
2515
- }),
2516
- getShmStats: trpc_middleware_js_1.protectedProcedure
2517
- .input(types_42.decoderCapability.methods.getShmStats.input.loose())
2518
- .output(types_42.decoderCapability.methods.getShmStats.output)
2519
- .query(async ({ input, ctx }) => {
2520
- const { nodeId, ...methodInput } = input;
2521
- const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
2522
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2523
- return p.getShmStats(methodInput);
2524
- }),
2525
- updateConfig: trpc_middleware_js_1.protectedProcedure
2526
- .input(types_42.decoderCapability.methods.updateConfig.input.loose())
2527
- .output(types_42.decoderCapability.methods.updateConfig.output)
2528
- .query(async ({ input, ctx }) => {
2529
- const { nodeId, ...methodInput } = input;
2530
- const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
2531
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2532
- return p.updateConfig(methodInput);
2533
- }),
2534
- getStats: trpc_middleware_js_1.protectedProcedure
2535
- .input(types_42.decoderCapability.methods.getStats.input.loose())
2536
- .output(types_42.decoderCapability.methods.getStats.output)
2537
- .query(async ({ input, ctx }) => {
2538
- const { nodeId, ...methodInput } = input;
2539
- const p = resolveProvider('decoder', nodeId, () => getProvider(ctx), createRemoteProxy);
2540
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2541
- return p.getStats(methodInput);
2542
- }),
2543
- listActiveSessions: trpc_middleware_js_1.protectedProcedure
2544
- .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
2545
- .output(types_42.decoderCapability.methods.listActiveSessions.output)
2546
- .query(async ({ input, ctx }) => {
2547
- const p = resolveProvider('decoder', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
2548
- return p.listActiveSessions();
2549
- }),
2550
2452
  reprobeHwaccel: trpc_middleware_js_1.adminProcedure
2551
2453
  .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
2552
2454
  .output(types_42.decoderCapability.methods.reprobeHwaccel.output)
@@ -7383,40 +7285,6 @@ function createCapRouter_pipelineExecutor(getProvider, createRemoteProxy) {
7383
7285
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
7384
7286
  return p.validatePipeline(methodInput);
7385
7287
  }),
7386
- listTemplates: trpc_middleware_js_1.protectedProcedure
7387
- .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
7388
- .output(types_93.pipelineExecutorCapability.methods.listTemplates.output)
7389
- .query(async ({ input, ctx }) => {
7390
- const p = resolveProvider('pipeline-executor', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
7391
- return p.listTemplates();
7392
- }),
7393
- saveTemplate: trpc_middleware_js_1.protectedProcedure
7394
- .input(types_93.pipelineExecutorCapability.methods.saveTemplate.input.loose())
7395
- .output(types_93.pipelineExecutorCapability.methods.saveTemplate.output)
7396
- .mutation(async ({ input, ctx }) => {
7397
- const { nodeId, ...methodInput } = input;
7398
- const p = resolveProvider('pipeline-executor', nodeId, () => getProvider(ctx), createRemoteProxy);
7399
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
7400
- return p.saveTemplate(methodInput);
7401
- }),
7402
- updateTemplate: trpc_middleware_js_1.protectedProcedure
7403
- .input(types_93.pipelineExecutorCapability.methods.updateTemplate.input.loose())
7404
- .output(types_93.pipelineExecutorCapability.methods.updateTemplate.output)
7405
- .mutation(async ({ input, ctx }) => {
7406
- const { nodeId, ...methodInput } = input;
7407
- const p = resolveProvider('pipeline-executor', nodeId, () => getProvider(ctx), createRemoteProxy);
7408
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
7409
- return p.updateTemplate(methodInput);
7410
- }),
7411
- deleteTemplate: trpc_middleware_js_1.protectedProcedure
7412
- .input(types_93.pipelineExecutorCapability.methods.deleteTemplate.input.loose())
7413
- .output(types_93.pipelineExecutorCapability.methods.deleteTemplate.output)
7414
- .mutation(async ({ input, ctx }) => {
7415
- const { nodeId, ...methodInput } = input;
7416
- const p = resolveProvider('pipeline-executor', nodeId, () => getProvider(ctx), createRemoteProxy);
7417
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
7418
- return p.deleteTemplate(methodInput);
7419
- }),
7420
7288
  getCapabilities: trpc_middleware_js_1.protectedProcedure
7421
7289
  .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
7422
7290
  .output(types_93.pipelineExecutorCapability.methods.getCapabilities.output)
@@ -8037,38 +7905,6 @@ function createCapRouter_pipelineOrchestrator(getProvider, _createRemoteProxy) {
8037
7905
  const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
8038
7906
  return p.getReconcileFromDiskStatus();
8039
7907
  }),
8040
- listTemplates: trpc_middleware_js_1.protectedProcedure
8041
- .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
8042
- .output(types_94.pipelineOrchestratorCapability.methods.listTemplates.output)
8043
- .query(async ({ input, ctx }) => {
8044
- void input;
8045
- const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
8046
- return p.listTemplates();
8047
- }),
8048
- saveTemplate: trpc_middleware_js_1.adminProcedure
8049
- .input(types_94.pipelineOrchestratorCapability.methods.saveTemplate.input.loose())
8050
- .output(types_94.pipelineOrchestratorCapability.methods.saveTemplate.output)
8051
- .mutation(async ({ input, ctx }) => {
8052
- const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
8053
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8054
- return p.saveTemplate(input);
8055
- }),
8056
- updateTemplate: trpc_middleware_js_1.adminProcedure
8057
- .input(types_94.pipelineOrchestratorCapability.methods.updateTemplate.input.loose())
8058
- .output(types_94.pipelineOrchestratorCapability.methods.updateTemplate.output)
8059
- .mutation(async ({ input, ctx }) => {
8060
- const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
8061
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8062
- return p.updateTemplate(input);
8063
- }),
8064
- deleteTemplate: trpc_middleware_js_1.adminProcedure
8065
- .input(types_94.pipelineOrchestratorCapability.methods.deleteTemplate.input.loose())
8066
- .output(types_94.pipelineOrchestratorCapability.methods.deleteTemplate.output)
8067
- .mutation(async ({ input, ctx }) => {
8068
- const p = requireCapProvider('pipeline-orchestrator', () => getProvider(ctx));
8069
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
8070
- return p.deleteTemplate(input);
8071
- }),
8072
7908
  });
8073
7909
  }
8074
7910
  function createCapRouter_pipelineRunner(getProvider, createRemoteProxy) {
@@ -10321,33 +10157,6 @@ function createCapRouter_streamBroker(getProvider, createRemoteProxy) {
10321
10157
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
10322
10158
  return p.unsubscribeAudioChunks(methodInput);
10323
10159
  }),
10324
- subscribeFrames: trpc_middleware_js_1.protectedProcedure
10325
- .input(types_118.streamBrokerCapability.methods.subscribeFrames.input.loose())
10326
- .output(types_118.streamBrokerCapability.methods.subscribeFrames.output)
10327
- .mutation(async ({ input, ctx }) => {
10328
- const { nodeId, ...methodInput } = input;
10329
- const p = resolveProvider('stream-broker', nodeId, () => getProvider(ctx), createRemoteProxy);
10330
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
10331
- return p.subscribeFrames(methodInput);
10332
- }),
10333
- pullFrameHandles: trpc_middleware_js_1.protectedProcedure
10334
- .input(types_118.streamBrokerCapability.methods.pullFrameHandles.input.loose())
10335
- .output(types_118.streamBrokerCapability.methods.pullFrameHandles.output)
10336
- .query(async ({ input, ctx }) => {
10337
- const { nodeId, ...methodInput } = input;
10338
- const p = resolveProvider('stream-broker', nodeId, () => getProvider(ctx), createRemoteProxy);
10339
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
10340
- return p.pullFrameHandles(methodInput);
10341
- }),
10342
- unsubscribeFrames: trpc_middleware_js_1.protectedProcedure
10343
- .input(types_118.streamBrokerCapability.methods.unsubscribeFrames.input.loose())
10344
- .output(types_118.streamBrokerCapability.methods.unsubscribeFrames.output)
10345
- .mutation(async ({ input, ctx }) => {
10346
- const { nodeId, ...methodInput } = input;
10347
- const p = resolveProvider('stream-broker', nodeId, () => getProvider(ctx), createRemoteProxy);
10348
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
10349
- return p.unsubscribeFrames(methodInput);
10350
- }),
10351
10160
  setPreBufferDuration: trpc_middleware_js_1.adminProcedure
10352
10161
  .input(types_118.streamBrokerCapability.methods.setPreBufferDuration.input.loose())
10353
10162
  .output(types_118.streamBrokerCapability.methods.setPreBufferDuration.output)
@@ -170,7 +170,7 @@ function buildServerProviders(services) {
170
170
  nodes: wrap(() => (0, cap_providers_js_1.buildNodesProvider)(services.agentRegistry, services.moleculer, services.addonRegistry, (0, cap_providers_js_1.createNodeRootPackageLookup)(services.moleculer, services.serverUpdateService), services.loggingService.createLogger('nodes'))),
171
171
  // server-management — runtime-updatable root package (phase 1: hub only).
172
172
  'server-management': wrap(() => (0, server_management_provider_js_1.buildServerManagementProvider)(services.serverUpdateService)),
173
- addons: wrap((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx)),
173
+ addons: wrap((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx, services.agentRegistry)),
174
174
  // webrtc-session — the resolved broker singleton, wrapped per request to
175
175
  // inject the server-read User-Agent into signaling attribution (the broker
176
176
  // is a forked addon that can't see the HTTP request). Cross-node (agent-
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PackageProtectedError = void 0;
4
+ exports.isHubNodeId = isHubNodeId;
5
+ exports.uninstallPackage = uninstallPackage;
6
+ exports.listInstallations = listInstallations;
7
+ /** The hub refuses before touching any node. Mapped to FORBIDDEN by the router. */
8
+ class PackageProtectedError extends Error {
9
+ constructor(packageName) {
10
+ super(`Package ${packageName} is required and cannot be uninstalled`);
11
+ this.name = 'PackageProtectedError';
12
+ }
13
+ }
14
+ exports.PackageProtectedError = PackageProtectedError;
15
+ /** A node id that refers to the hub itself (top-level or a hub group runner). */
16
+ function isHubNodeId(nodeId) {
17
+ return nodeId === 'hub' || nodeId.startsWith('hub/');
18
+ }
19
+ const OK_OUTCOMES = new Set([
20
+ 'uninstalled',
21
+ 'not-installed',
22
+ 'pending-reconnect',
23
+ ]);
24
+ function errMessage(err) {
25
+ return err instanceof Error ? err.message : String(err);
26
+ }
27
+ function nodeResult(nodeId, outcome, extra = {}) {
28
+ return {
29
+ nodeId,
30
+ outcome,
31
+ addonIds: extra.addonIds ?? [],
32
+ runnersStopped: extra.runnersStopped ?? [],
33
+ warnings: extra.warnings ?? [],
34
+ ...(extra.error !== undefined ? { error: extra.error } : {}),
35
+ };
36
+ }
37
+ function stringArray(value) {
38
+ return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
39
+ }
40
+ function isRecord(value) {
41
+ return typeof value === 'object' && value !== null;
42
+ }
43
+ function parseUndeployReply(reply) {
44
+ if (!isRecord(reply)) {
45
+ return {
46
+ packageScoped: false,
47
+ outcome: null,
48
+ reason: undefined,
49
+ addonIds: [],
50
+ runnersStopped: null,
51
+ warnings: [],
52
+ };
53
+ }
54
+ const outcome = reply['outcome'];
55
+ return {
56
+ packageScoped: reply['scope'] === 'package',
57
+ outcome: outcome === 'uninstalled' || outcome === 'not-installed' || outcome === 'refused'
58
+ ? outcome
59
+ : null,
60
+ reason: typeof reply['reason'] === 'string' ? reply['reason'] : undefined,
61
+ addonIds: stringArray(reply['addonIds']),
62
+ runnersStopped: Array.isArray(reply['runnersStopped'])
63
+ ? stringArray(reply['runnersStopped'])
64
+ : null,
65
+ warnings: stringArray(reply['warnings']),
66
+ };
67
+ }
68
+ /**
69
+ * An agent built before package mode answers `{ success, addonId }` and removes
70
+ * ONE addon — its folder, not its runner. Its packages are still removed (one
71
+ * call per addon id), and the result says the runner outlives it.
72
+ */
73
+ const LEGACY_AGENT_WARNING = 'this agent predates runner teardown: the addon is removed from disk but its runner keeps running until the agent restarts — update the agent';
74
+ async function uninstallFromOnlineAgent(seam, nodeId, packageName) {
75
+ let listed;
76
+ try {
77
+ listed = (await seam.agentAddons(nodeId))
78
+ .filter((a) => a.packageName === packageName)
79
+ .map((a) => a.id);
80
+ }
81
+ catch (err) {
82
+ return nodeResult(nodeId, 'failed', { error: `status read failed: ${errMessage(err)}` });
83
+ }
84
+ try {
85
+ // `addonId` is for an agent that predates package mode: it reads only that
86
+ // field. With no listed addon, the package name doubles as the dir to drop.
87
+ const first = parseUndeployReply(await seam.agentUndeploy(nodeId, { packageName, addonId: listed[0] ?? packageName }));
88
+ if (first.packageScoped && first.outcome !== null) {
89
+ if (first.outcome === 'refused') {
90
+ return nodeResult(nodeId, 'refused', { error: first.reason, addonIds: first.addonIds });
91
+ }
92
+ await seam.notePackageUndeployed(nodeId, packageName, first.addonIds);
93
+ return nodeResult(nodeId, first.outcome, {
94
+ addonIds: first.addonIds,
95
+ runnersStopped: first.runnersStopped ?? [],
96
+ warnings: first.warnings,
97
+ });
98
+ }
99
+ for (const addonId of listed.slice(1)) {
100
+ await seam.agentUndeploy(nodeId, { packageName, addonId });
101
+ }
102
+ await seam.notePackageUndeployed(nodeId, packageName, listed);
103
+ return nodeResult(nodeId, listed.length > 0 ? 'uninstalled' : 'not-installed', {
104
+ addonIds: listed,
105
+ warnings: listed.length > 0 ? [LEGACY_AGENT_WARNING] : [],
106
+ });
107
+ }
108
+ catch (err) {
109
+ return nodeResult(nodeId, 'failed', { addonIds: listed, error: errMessage(err) });
110
+ }
111
+ }
112
+ async function uninstallFromAgent(seam, node, packageName) {
113
+ if (node.isOnline)
114
+ return uninstallFromOnlineAgent(seam, node.id, packageName);
115
+ // Offline: nothing can be called. Pruning the record is what keeps the
116
+ // back-fill from returning it; the copy itself is the stale sweep's on rejoin.
117
+ try {
118
+ await seam.notePackageUndeployed(node.id, packageName, []);
119
+ return nodeResult(node.id, 'pending-reconnect', {
120
+ warnings: [
121
+ 'node is offline — its copy is removed when it reconnects, provided the hub no longer installs the package',
122
+ ],
123
+ });
124
+ }
125
+ catch (err) {
126
+ return nodeResult(node.id, 'failed', { error: errMessage(err) });
127
+ }
128
+ }
129
+ async function uninstallFromHub(seam, packageName) {
130
+ const row = seam.listHubPackages().find((p) => p.name === packageName);
131
+ if (!row)
132
+ return nodeResult('hub', 'not-installed');
133
+ try {
134
+ await seam.uninstallOnHub(packageName);
135
+ return nodeResult('hub', 'uninstalled', { addonIds: row.addonIds });
136
+ }
137
+ catch (err) {
138
+ return nodeResult('hub', 'failed', { addonIds: row.addonIds, error: errMessage(err) });
139
+ }
140
+ }
141
+ function summarize(packageName, results) {
142
+ return { success: results.every((r) => OK_OUTCOMES.has(r.outcome)), packageName, results };
143
+ }
144
+ /**
145
+ * Agents that have (or are recorded to have) the package. An online agent
146
+ * whose status read fails is still targeted — the undeploy leg reports the
147
+ * failure for that node rather than silently skipping it.
148
+ */
149
+ async function agentsHolding(seam, packageName) {
150
+ const agents = await seam.listAgents();
151
+ const holding = await Promise.all(agents.map(async (node) => {
152
+ if (node.isOnline) {
153
+ try {
154
+ const addons = await seam.agentAddons(node.id);
155
+ if (addons.some((a) => a.packageName === packageName))
156
+ return node;
157
+ }
158
+ catch {
159
+ return node;
160
+ }
161
+ }
162
+ const recorded = await seam.recordedPackages(node.id).catch(() => []);
163
+ return recorded.some((p) => p.name === packageName) ? node : null;
164
+ }));
165
+ return holding.filter((n) => n !== null);
166
+ }
167
+ async function uninstallPackage(seam, request) {
168
+ const { packageName } = request;
169
+ if (seam.isProtected(packageName))
170
+ throw new PackageProtectedError(packageName);
171
+ if (request.everywhere === true) {
172
+ const agents = await agentsHolding(seam, packageName);
173
+ // Agents first, hub last — the hub routes every call to the agents, and a
174
+ // hub uninstall that fails must not leave the agents' records already
175
+ // pruned for nothing: each leg records only its own success.
176
+ const results = [];
177
+ for (const node of agents)
178
+ results.push(await uninstallFromAgent(seam, node, packageName));
179
+ results.push(await uninstallFromHub(seam, packageName));
180
+ return summarize(packageName, results);
181
+ }
182
+ const nodeId = request.nodeId;
183
+ if (nodeId === undefined || isHubNodeId(nodeId)) {
184
+ // The original contract: hub-only, and a failure THROWS.
185
+ await seam.uninstallOnHub(packageName);
186
+ const row = seam.listHubPackages().find((p) => p.name === packageName);
187
+ return summarize(packageName, [
188
+ nodeResult('hub', 'uninstalled', { addonIds: row?.addonIds ?? [] }),
189
+ ]);
190
+ }
191
+ const node = (await seam.listAgents()).find((n) => n.id === nodeId);
192
+ if (!node) {
193
+ return summarize(packageName, [
194
+ nodeResult(nodeId, 'failed', { error: `unknown node ${nodeId}` }),
195
+ ]);
196
+ }
197
+ return summarize(packageName, [await uninstallFromAgent(seam, node, packageName)]);
198
+ }
199
+ async function listInstallations(seam, packageName) {
200
+ const wanted = (name) => packageName === undefined || name === packageName;
201
+ const hubPackages = seam.listHubPackages();
202
+ const onHub = new Set(hubPackages.map((p) => p.name));
203
+ const installations = hubPackages
204
+ .filter((p) => wanted(p.name))
205
+ .map((p) => ({
206
+ packageName: p.name,
207
+ nodeId: 'hub',
208
+ nodeName: 'Hub',
209
+ isHub: true,
210
+ isOnline: true,
211
+ version: p.version,
212
+ addonIds: p.addonIds,
213
+ source: 'live',
214
+ onHub: true,
215
+ }));
216
+ const unreachable = [];
217
+ const perAgent = await Promise.all((await seam.listAgents()).map(async (node) => {
218
+ const base = { nodeId: node.id, nodeName: node.name, isHub: false, isOnline: node.isOnline };
219
+ if (node.isOnline) {
220
+ try {
221
+ const byPackage = new Map();
222
+ for (const addon of await seam.agentAddons(node.id)) {
223
+ if (addon.packageName === undefined || !wanted(addon.packageName))
224
+ continue;
225
+ const row = byPackage.get(addon.packageName) ?? {
226
+ version: addon.version ?? null,
227
+ addonIds: [],
228
+ };
229
+ row.addonIds.push(addon.id);
230
+ byPackage.set(addon.packageName, row);
231
+ }
232
+ return [...byPackage].map(([name, row]) => ({
233
+ ...base,
234
+ packageName: name,
235
+ version: row.version,
236
+ addonIds: row.addonIds,
237
+ source: 'live',
238
+ onHub: onHub.has(name),
239
+ }));
240
+ }
241
+ catch (err) {
242
+ unreachable.push({ nodeId: node.id, error: errMessage(err) });
243
+ return [];
244
+ }
245
+ }
246
+ const recorded = await seam.recordedPackages(node.id).catch(() => []);
247
+ return recorded
248
+ .filter((p) => wanted(p.name))
249
+ .map((p) => ({
250
+ ...base,
251
+ packageName: p.name,
252
+ version: p.version,
253
+ addonIds: [],
254
+ source: 'recorded',
255
+ onHub: onHub.has(p.name),
256
+ }));
257
+ }));
258
+ return { installations: [...installations, ...perAgent.flat()], unreachable };
259
+ }
@@ -50,11 +50,11 @@ const types_1 = require("@camstack/types");
50
50
  *
51
51
  * What's left is ONLY the addon package management surface: the installer
52
52
  * + the loader + `reloadPackages` + `listAvailableAddons`. These are still
53
- * used by:
54
- * - `server/backend/src/api/addon-upload.ts` (multipart upload → install
55
- * → loader reload)
56
- * - `server/backend/src/api/bridge-addons.router.ts` (install/uninstall
57
- * package lifecycle, addon list via the loader)
53
+ * used by `server/backend/src/api/addon-upload.ts` (multipart upload →
54
+ * install → loader reload). The `bridge-addons.router.ts` that also used it
55
+ * was never mounted and was deleted on 2026-10-08 — its hard-coded REQUIRED
56
+ * set read like the uninstall guard and was not; uninstall is the `addons`
57
+ * cap's (`core/addon/addon-uninstall.ts`).
58
58
  */
59
59
  class AddonBridgeService {
60
60
  loggingService;
@@ -24,13 +24,20 @@
24
24
  * both mutations are attributable actions rather than heuristics.
25
25
  *
26
26
  * THE INTENDED VERSION
27
- * The hub's own installed version wins, falling back to the version the node
28
- * last reported. Never `latest` — resolving `latest` per node is exactly the
29
- * silent per-node drift the bootstrap installer already suffers from
30
- * (`addon-installer.ts` installs unpinned and `ensureRequiredPackages` never
31
- * re-installs, so whatever `latest` meant at a node's first boot is what that
32
- * node runs forever). A package with no resolvable version is reported, not
33
- * guessed at.
27
+ * The hub's own installed version, and only that. Never `latest` — resolving
28
+ * `latest` per node is exactly the silent per-node drift the bootstrap
29
+ * installer already suffers from (`addon-installer.ts` installs unpinned and
30
+ * `ensureRequiredPackages` never re-installs, so whatever `latest` meant at a
31
+ * node's first boot is what that node runs forever).
32
+ *
33
+ * A package the hub does NOT install is never delivered, whatever the node last
34
+ * reported. It used to fall back to the node's last-reported version, which
35
+ * made the back-fill the one path that could put an UNINSTALLED addon back: an
36
+ * operator removes `@camstack/addon-decoder-ffmpeg` from the hub and moves the
37
+ * agent's copy aside, and the next reconnect reinstalled it from npm at the
38
+ * version the node last ran — while the same pass's stale sweep exists to
39
+ * remove exactly such an addon. The two directions disagreed; now the hub's
40
+ * install set bounds both. Such packages are reported as `notOnHub`.
34
41
  *
35
42
  * BOUNDING
36
43
  * Delivery failures are counted per `(node, package, version)` and the pair is
@@ -66,7 +73,7 @@ function computeBackfillPlan(input) {
66
73
  const liveNames = new Set(input.live.map((p) => p.name));
67
74
  const targets = [];
68
75
  const retired = [];
69
- const unresolvable = [];
76
+ const notOnHub = [];
70
77
  for (const entry of input.recorded) {
71
78
  if (liveNames.has(entry.name))
72
79
  continue;
@@ -79,18 +86,13 @@ function computeBackfillPlan(input) {
79
86
  continue;
80
87
  }
81
88
  const hubVersion = input.hubVersions.get(entry.name);
82
- const version = hubVersion ?? entry.version;
83
- if (version === null || version === undefined || version.length === 0) {
84
- unresolvable.push(entry.name);
89
+ if (hubVersion === undefined) {
90
+ notOnHub.push(entry.name);
85
91
  continue;
86
92
  }
87
- targets.push({
88
- name: entry.name,
89
- version,
90
- versionSource: hubVersion === undefined ? 'node-last-reported' : 'hub-installed',
91
- });
93
+ targets.push({ name: entry.name, version: hubVersion, versionSource: 'hub-installed' });
92
94
  }
93
- return { targets, retired, unresolvable, liveCount: liveNames.size };
95
+ return { targets, retired, notOnHub, liveCount: liveNames.size };
94
96
  }
95
97
  function confirmBackfillPlan(plan, secondRead) {
96
98
  const present = new Set(secondRead.map((p) => p.name));
@@ -187,5 +189,5 @@ async function executeBackfill(seam, nodeId, plan, tracker) {
187
189
  if (delivered.length > 0) {
188
190
  await seam.reload(nodeId);
189
191
  }
190
- return { delivered, failed, skipped: plan.retired, unresolvable: plan.unresolvable };
192
+ return { delivered, failed, skipped: plan.retired, notOnHub: plan.notOnHub };
191
193
  }