@camstack/server 1.1.0 → 1.1.1

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.
@@ -54,7 +54,13 @@ function getDeployStageRegistry() {
54
54
  * the agent in the `$agent.deploy` `hub-http` source descriptor.
55
55
  */
56
56
  function registerDeployBundleRoute(fastify) {
57
- fastify.get('/api/addons/deploy-bundle/:id', (request, reply) => {
57
+ // `compress: false` — never gzip/brotli this route. The body is a large
58
+ // binary tarball (addon bundle / model artifact) the agent pulls with undici
59
+ // + sha256 verification. With global @fastify/compress on, the 1–2MB response
60
+ // was Brotli-encoded and undici's BrotliDecompress aborted the stream
61
+ // ("TypeError: terminated"), silently breaking EVERY agent-pull (addon-deploy
62
+ // AND model distribution). Identity bytes also keep the byte-length check valid.
63
+ fastify.get('/api/addons/deploy-bundle/:id', { compress: false }, (request, reply) => {
58
64
  const auth = request.headers.authorization ?? '';
59
65
  const token = auth.startsWith('Bearer ') ? auth.slice('Bearer '.length) : '';
60
66
  if (!token) {
@@ -0,0 +1,128 @@
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.tarModelFiles = tarModelFiles;
37
+ exports.distributeModelWith = distributeModelWith;
38
+ exports.buildModelDistributorProvider = buildModelDistributorProvider;
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const node_child_process_1 = require("node:child_process");
42
+ const system_1 = require("@camstack/system");
43
+ const addon_upload_js_1 = require("./addon-upload.js");
44
+ const AGENT_DISTRIBUTE_TIMEOUT_MS = 60_000;
45
+ function hubModelsDir() {
46
+ return path.join(process.env['CAMSTACK_DATA'] ?? 'camstack-data', 'models');
47
+ }
48
+ function hubBundleBaseUrl() {
49
+ return process.env['CAMSTACK_HUB_PUBLIC_URL'] ?? 'https://127.0.0.1:4443';
50
+ }
51
+ /**
52
+ * `tar -czf -` the given files relative to `cwd`, buffering the gzip stream.
53
+ * Async (spawn, not execFileSync) so a large model doesn't block the event
54
+ * loop. The agent untars this exact buffer into its own modelsDir.
55
+ */
56
+ function tarModelFiles(cwd, files) {
57
+ return new Promise((resolve, reject) => {
58
+ const child = (0, node_child_process_1.spawn)('tar', ['-czf', '-', '-C', cwd, ...files]);
59
+ const chunks = [];
60
+ const errChunks = [];
61
+ child.stdout.on('data', (c) => chunks.push(c));
62
+ child.stderr.on('data', (c) => errChunks.push(c));
63
+ child.on('error', reject);
64
+ child.on('close', (code) => {
65
+ if (code === 0)
66
+ resolve(Buffer.concat(chunks));
67
+ else
68
+ reject(new Error(`tar exited ${code}: ${Buffer.concat(errChunks).toString().slice(0, 300)}`));
69
+ });
70
+ });
71
+ }
72
+ /**
73
+ * Core distribution logic (pure orchestration over the seam):
74
+ * precondition-check → collect → tar → stage → trigger the agent pull.
75
+ * `nodeId === 'hub'` is a no-op (the artifact is already resident).
76
+ */
77
+ async function distributeModelWith(seam, input) {
78
+ const { nodeId, modelId, format, entry } = input;
79
+ if (nodeId === 'hub') {
80
+ return { ok: true, sha256: '', bytes: 0, path: seam.modelsDir };
81
+ }
82
+ if (!seam.isPresent(entry, format)) {
83
+ throw new Error(`model ${modelId}/${format} not present on hub — download or convert it first`);
84
+ }
85
+ const files = (0, system_1.collectModelFiles)(entry, format).filter((f) => seam.fileExists(f));
86
+ if (files.length === 0) {
87
+ throw new Error(`model ${modelId}/${format} resolved no files to distribute`);
88
+ }
89
+ const tgz = await seam.tar(files);
90
+ const source = seam.stage(tgz);
91
+ const res = await seam.callAgent(nodeId, { modelId, format, source });
92
+ return { ok: true, sha256: source.sha256, bytes: source.bytes, path: res.path ?? '' };
93
+ }
94
+ /**
95
+ * Build the hub-resident `model-distributor` singleton provider. Mounted as a
96
+ * service-backed cap override in `trpc.router.ts` because it needs the
97
+ * Moleculer broker + the deploy-stage registry, which a forked addon can't
98
+ * reach.
99
+ */
100
+ function buildModelDistributorProvider(moleculer, loggingService) {
101
+ const logger = loggingService.createLogger('model-distributor');
102
+ const modelsDir = hubModelsDir();
103
+ const seam = {
104
+ modelsDir,
105
+ isPresent: (entry, format) => (0, system_1.isModelDownloaded)(modelsDir, entry, format),
106
+ fileExists: (rel) => fs.existsSync(path.join(modelsDir, rel)),
107
+ tar: (files) => tarModelFiles(modelsDir, files),
108
+ stage: (buffer) => (0, addon_upload_js_1.buildHubHttpSource)(buffer, hubBundleBaseUrl()),
109
+ callAgent: async (nodeId, payload) => {
110
+ const broker = moleculer?.broker;
111
+ if (!broker)
112
+ throw new Error('model-distributor: Moleculer broker unavailable');
113
+ const raw = await broker.call('$agent.distributeModel', payload, {
114
+ nodeID: nodeId,
115
+ timeout: AGENT_DISTRIBUTE_TIMEOUT_MS,
116
+ });
117
+ return (raw ?? {});
118
+ },
119
+ };
120
+ return {
121
+ distributeModel: async (input) => {
122
+ logger.info('Distributing model to node', {
123
+ meta: { modelId: input.modelId, format: input.format, nodeId: input.nodeId },
124
+ });
125
+ return distributeModelWith(seam, input);
126
+ },
127
+ };
128
+ }
@@ -2,7 +2,7 @@
2
2
  // AUTO-GENERATED by scripts/generate-cap-mounts.ts — DO NOT EDIT
3
3
  // Re-run: npx tsx scripts/generate-cap-mounts.ts
4
4
  //
5
- // Mounted: 127 Skipped (legacy): 6
5
+ // Mounted: 130 Skipped (legacy): 6
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.LEGACY_SHAPE_SKIP = void 0;
8
8
  exports.mountAllCaps = mountAllCaps;
@@ -121,6 +121,22 @@ function mountAllCaps(services) {
121
121
  contact: (0, generated_cap_routers_js_1.createCapRouter_contact)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'contact'), remoteCapProxy),
122
122
  control: (0, generated_cap_routers_js_1.createCapRouter_control)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'control'), remoteCapProxy),
123
123
  cover: (0, generated_cap_routers_js_1.createCapRouter_cover)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'cover'), remoteCapProxy),
124
+ customModelRegistry: (0, generated_cap_routers_js_1.createCapRouter_customModelRegistry)((_ctx, addonId) => {
125
+ if (!reg)
126
+ return null;
127
+ if (addonId !== undefined) {
128
+ return reg.getProviderByAddonId('custom-model-registry', addonId);
129
+ }
130
+ const entries = reg.getCollectionEntries('custom-model-registry');
131
+ if (entries.length === 0)
132
+ return null;
133
+ const providers = entries.map(([, p]) => p);
134
+ const first = providers[0];
135
+ return {
136
+ ...first,
137
+ listModels: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listModels'),
138
+ };
139
+ }, remoteCapProxy),
124
140
  decoder: (0, generated_cap_routers_js_1.createCapRouter_decoder)((_ctx) => reg?.getSingleton('decoder') ?? null, remoteCapProxy),
125
141
  detectionPipeline: (0, generated_cap_routers_js_1.createCapRouter_detectionPipeline)((_ctx) => reg?.getSingleton('detection-pipeline') ?? null, remoteCapProxy),
126
142
  deviceAdoption: (0, generated_cap_routers_js_1.createCapRouter_deviceAdoption)((_ctx) => reg?.getSingleton('device-adoption') ??
@@ -204,6 +220,9 @@ function mountAllCaps(services) {
204
220
  }, remoteCapProxy),
205
221
  metricsProvider: (0, generated_cap_routers_js_1.createCapRouter_metricsProvider)((_ctx) => reg?.getSingleton('metrics-provider') ??
206
222
  null, remoteCapProxy),
223
+ modelConvert: (0, generated_cap_routers_js_1.createCapRouter_modelConvert)((_ctx) => reg?.getSingleton('model-convert') ?? null, remoteCapProxy),
224
+ modelDistributor: (0, generated_cap_routers_js_1.createCapRouter_modelDistributor)((_ctx) => reg?.getSingleton('model-distributor') ??
225
+ null, remoteCapProxy),
207
226
  motion: (0, generated_cap_routers_js_1.createCapRouter_motion)((_ctx) => reg?.getSingleton('motion') ?? null, remoteCapProxy),
208
227
  motionDetection: (0, generated_cap_routers_js_1.createCapRouter_motionDetection)((_ctx) => reg?.getSingleton('motion-detection') ??
209
228
  null, remoteCapProxy),