@camstack/server 1.2.459 → 1.2.461

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.
@@ -134,7 +134,7 @@ entry kept resolvable for a saved selection but no longer offered.
134
134
 
135
135
  | Catalog ids | Upstream | Code / weights | Training data | CamStack changes | Downloaded from |
136
136
  | --- | --- | --- | --- | --- | --- |
137
- | `yunet-2023mar` (default) | [OpenCV Zoo YuNet](https://github.com/opencv/opencv_zoo/tree/main/models/face_detection_yunet) | BSD-3-Clause (`BSD-3-Clause.txt`) / MIT (`MIT-yunet-reference.txt`) | WIDER FACE: research-oriented terms; images CC BY-NC-ND | input contract baked into the graph (the pool RGB /255 becomes the upstream BGR 0..255: Mul(255) plus a channel Gather); internal tensor names cleared in the OpenVINO IR; ONNX fp32, OpenVINO IR fp32, CoreML fp16 (scripts/build-yunet-model.py) | mirror `faceDetection/yunet` |
137
+ | `yunet-2023mar` (default) | [OpenCV Zoo YuNet](https://github.com/opencv/opencv_zoo/tree/main/models/face_detection_yunet) | BSD-3-Clause (`BSD-3-Clause.txt`) / MIT (`MIT-yunet-reference.txt`) | WIDER FACE: research-oriented terms; images CC BY-NC-ND | input contract baked into the graph (the pool RGB /255 becomes the upstream BGR 0..255: Mul(255) plus a channel Gather); internal tensor names cleared in the OpenVINO IR; ONNX fp32, OpenVINO IR fp32, CoreML fp16 (scripts/build-yunet-model.py); Edge TPU build: the bake folded into the first Conv, full-integer int8 post-training quantisation calibrated on CamStack person crops, compiled with edgetpu_compiler 14 (scripts/build-yunet-edgetpu.py) | mirror `faceDetection/yunet` |
138
138
 
139
139
  ### Face recognition
140
140
 
@@ -146,7 +146,7 @@ entry kept resolvable for a saved selection but no longer offered.
146
146
 
147
147
  | Catalog ids | Upstream | Code / weights | Training data | CamStack changes | Downloaded from |
148
148
  | --- | --- | --- | --- | --- | --- |
149
- | `yolox-tiny-plate` (default) | [open-lpr models (faisalthaheem/open-lpr-models), YOLOX-tiny](https://huggingface.co/faisalthaheem/open-lpr-models) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | Simanno annotated plate corpus plus community-contributed photographs, pseudo-labelled by a teacher model (not published; the upstream NOTICE says the photographs were uploaded without consent obtained): UNKNOWN | input contract baked into the graph (static 1x3x640x640 RGB /255 becomes the upstream BGR 0..255: Mul(255) plus a channel Gather) and the output reshaped to the YOLOv8 layout (cx, cy, w, h, obj*cls); ONNX fp32, OpenVINO IR fp16, CoreML fp16 (scripts/build-yolox-plate-model.py) | mirror `plateDetection/yolox-tiny-plate` |
149
+ | `yolox-tiny-plate` (default) | [open-lpr models (faisalthaheem/open-lpr-models), YOLOX-tiny](https://huggingface.co/faisalthaheem/open-lpr-models) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | Simanno annotated plate corpus plus community-contributed photographs, pseudo-labelled by a teacher model (not published; the upstream NOTICE says the photographs were uploaded without consent obtained): UNKNOWN | input contract baked into the graph (static 1x3x640x640 RGB /255 becomes the upstream BGR 0..255: Mul(255) plus a channel Gather) and the output reshaped to the YOLOv8 layout (cx, cy, w, h, obj*cls); ONNX fp32, OpenVINO IR fp16, CoreML fp16 (scripts/build-yolox-plate-model.py); Edge TPU build: the Focus stem rewritten as the equivalent 6x6 stride-2 Conv with the input bake folded into it, the graph cut before the grid decode (raw per-stride head), full-integer int8 post-training quantisation calibrated on CamStack vehicle crops, compiled with edgetpu_compiler 14 (scripts/build-yolox-plate-edgetpu.py) | mirror `plateDetection/yolox-tiny-plate` |
150
150
 
151
151
  ### Licence plate reading
152
152
 
@@ -35,7 +35,6 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getDeployStageRegistry = getDeployStageRegistry;
37
37
  exports.registerDeployBundleRoute = registerDeployBundleRoute;
38
- exports.hubBundleBaseUrl = hubBundleBaseUrl;
39
38
  exports.buildHubHttpSource = buildHubHttpSource;
40
39
  exports.registerAddonUploadRoute = registerAddonUploadRoute;
41
40
  const fs = __importStar(require("node:fs"));
@@ -94,9 +93,14 @@ function readMultipartTextField(fields, name) {
94
93
  const value = field.value;
95
94
  return typeof value === 'string' ? value : null;
96
95
  }
97
- /** Base URL agents pull staged deploy bundles from. */
98
- function hubBundleBaseUrl() {
99
- return process.env['CAMSTACK_HUB_PUBLIC_URL'] ?? 'https://127.0.0.1:4443';
96
+ /** Resolve the agent-facing base URL, turning a refusal into its reason. */
97
+ async function resolveBaseUrlOrReason(resolve) {
98
+ try {
99
+ return { ok: true, baseUrl: await resolve() };
100
+ }
101
+ catch (err) {
102
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
103
+ }
100
104
  }
101
105
  function buildHubHttpSource(buffer, hubBaseUrl) {
102
106
  const staged = getDeployStageRegistry().stage(buffer);
@@ -109,6 +113,11 @@ function buildHubHttpSource(buffer, hubBaseUrl) {
109
113
  };
110
114
  }
111
115
  async function registerAddonUploadRoute(fastify, addonBridge, authService, moleculer, addonRegistry, addonPackageService, logger,
116
+ /**
117
+ * The base URL agents pull the staged bundle from — the `local-network`
118
+ * cap's cluster address (`hub-cluster-url.ts`), resolved per delivery.
119
+ */
120
+ resolveClusterBaseUrl,
112
121
  /**
113
122
  * Marks the "hub is writing to this node" window around every agent
114
123
  * delivery so the addon back-fill cannot mistake the roster gap a deploy
@@ -171,12 +180,11 @@ deliveryLedger) {
171
180
  // the hub AND broadcasts to every connected agent that can run any of
172
181
  // the package's addons (i.e. anything not marked hub-only). Any other
173
182
  // explicit `nodeId` value routes only to that agent via `$agent.deploy`.
174
- const baseUrl = hubBundleBaseUrl();
175
183
  if (!nodeId || nodeId === 'hub') {
176
- return installToHub(reply, addonBridge, addonRegistry, addonPackageService, moleculer, logger, data.filename, buffer, baseUrl, deliveryLedger);
184
+ return installToHub(reply, addonBridge, addonRegistry, addonPackageService, moleculer, logger, data.filename, buffer, resolveClusterBaseUrl, deliveryLedger);
177
185
  }
178
186
  const agentAddonId = addonIdHint ?? manifest.name;
179
- return deployToAgent(reply, moleculer, logger, nodeId, agentAddonId, buffer, baseUrl, deliveryLedger);
187
+ return deployToAgent(reply, moleculer, logger, nodeId, agentAddonId, buffer, resolveClusterBaseUrl, deliveryLedger);
180
188
  });
181
189
  }
182
190
  /**
@@ -214,7 +222,7 @@ function packageHasAgentDeployable(addonsDir, packageName) {
214
222
  * without an agent restart. Agents that fail are reported per-node — one
215
223
  * unreachable agent must not block the others or the hub install.
216
224
  */
217
- async function propagateToAgents(moleculer, logger, packageName, buffer, hubBaseUrl, deliveryLedger) {
225
+ async function propagateToAgents(moleculer, logger, packageName, buffer, resolveClusterBaseUrl, deliveryLedger) {
218
226
  const broker = moleculer.broker;
219
227
  const nodes = broker.registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
220
228
  // Moleculer reports both top-level nodes (`hub`, `dev-agent-0`, …) AND
@@ -233,6 +241,13 @@ async function propagateToAgents(moleculer, logger, packageName, buffer, hubBase
233
241
  });
234
242
  if (agentNodeIds.length === 0)
235
243
  return [];
244
+ const hubBaseUrl = await resolveBaseUrlOrReason(resolveClusterBaseUrl);
245
+ if (!hubBaseUrl.ok) {
246
+ logger.error('propagate: no hub address an agent can reach — nothing delivered', {
247
+ meta: { packageName, targetAgents: agentNodeIds, error: hubBaseUrl.error },
248
+ });
249
+ return agentNodeIds.map((nodeId) => ({ nodeId, success: false, error: hubBaseUrl.error }));
250
+ }
236
251
  const results = [];
237
252
  for (const nodeId of agentNodeIds) {
238
253
  // The hub currently only ever sends the `hub-http` source (agent streams
@@ -246,7 +261,7 @@ async function propagateToAgents(moleculer, logger, packageName, buffer, hubBase
246
261
  logger,
247
262
  nodeId,
248
263
  packageName,
249
- source: buildHubHttpSource(buffer, hubBaseUrl),
264
+ source: buildHubHttpSource(buffer, hubBaseUrl.baseUrl),
250
265
  ...(deliveryLedger === undefined ? {} : { deliveryLedger }),
251
266
  }));
252
267
  }
@@ -261,7 +276,7 @@ async function propagateToAgents(moleculer, logger, packageName, buffer, hubBase
261
276
  * Without this the CLI push was write-to-disk-only and required a server
262
277
  * restart to actually run the new code.
263
278
  */
264
- async function installToHub(reply, addonBridge, addonRegistry, addonPackageService, moleculer, logger, filename, buffer, hubBaseUrl, deliveryLedger) {
279
+ async function installToHub(reply, addonBridge, addonRegistry, addonPackageService, moleculer, logger, filename, buffer, resolveClusterBaseUrl, deliveryLedger) {
265
280
  const tmpDir = path.join(os.tmpdir(), `camstack-addon-upload-${Date.now()}`);
266
281
  fs.mkdirSync(tmpDir, { recursive: true });
267
282
  const tgzPath = path.join(tmpDir, filename);
@@ -386,7 +401,7 @@ async function installToHub(reply, addonBridge, addonRegistry, addonPackageServi
386
401
  });
387
402
  }
388
403
  if (propagatable) {
389
- void propagateToAgents(moleculer, logger, result.name, buffer, hubBaseUrl, deliveryLedger).then((agentResults) => {
404
+ void propagateToAgents(moleculer, logger, result.name, buffer, resolveClusterBaseUrl, deliveryLedger).then((agentResults) => {
390
405
  logger.info('propagation done', {
391
406
  meta: { packageName: result.name, agents: agentResults },
392
407
  });
@@ -417,7 +432,14 @@ async function installToHub(reply, addonBridge, addonRegistry, addonPackageServi
417
432
  * evidence. The response now carries what was reloaded and what was verified,
418
433
  * so the CLI can print facts instead of a bare tick.
419
434
  */
420
- async function deployToAgent(reply, moleculer, logger, nodeId, addonId, buffer, hubBaseUrl, deliveryLedger) {
435
+ async function deployToAgent(reply, moleculer, logger, nodeId, addonId, buffer, resolveClusterBaseUrl, deliveryLedger) {
436
+ const hubBaseUrl = await resolveBaseUrlOrReason(resolveClusterBaseUrl);
437
+ if (!hubBaseUrl.ok) {
438
+ logger.error('agent deploy: no hub address the agent can reach — nothing delivered', {
439
+ meta: { nodeId, addonId, error: hubBaseUrl.error },
440
+ });
441
+ return reply.status(502).send({ error: `Agent deploy failed: ${hubBaseUrl.error}` });
442
+ }
421
443
  // The hub currently only ever sends the `hub-http` source (agent streams the
422
444
  // tgz directly from this hub). The `{kind:'npm'}` source is a reserved
423
445
  // follow-up (published-addon optimization) and is intentionally not produced
@@ -429,7 +451,7 @@ async function deployToAgent(reply, moleculer, logger, nodeId, addonId, buffer,
429
451
  logger,
430
452
  nodeId,
431
453
  packageName: addonId,
432
- source: buildHubHttpSource(buffer, hubBaseUrl),
454
+ source: buildHubHttpSource(buffer, hubBaseUrl.baseUrl),
433
455
  ...(deliveryLedger === undefined ? {} : { deliveryLedger }),
434
456
  });
435
457
  if (!result.success) {
@@ -18,8 +18,9 @@
18
18
  * - {@link OPAQUE_ORIGIN} (`null`): the native app's OTA-bundled embed is a
19
19
  * `file://` page inside a WKWebView, whose origin is opaque. Removing it
20
20
  * breaks live view on iOS;
21
- * - `CAMSTACK_HUB_PUBLIC_URL`, when set, and the operator's extras
22
- * (`server.corsOrigins` in config.yaml / `CAMSTACK_CORS_ORIGINS`).
21
+ * - the operator's extras (`server.corsOrigins` in config.yaml /
22
+ * `CAMSTACK_CORS_ORIGINS`). The hub's own public address needs no entry:
23
+ * a browser on it is the `own-host` case above.
23
24
  *
24
25
  * The answer to an allowed origin REFLECTS it exactly — never `*` — and never
25
26
  * carries `Access-Control-Allow-Credentials`. The cross-origin viewer
@@ -111,13 +112,9 @@ function parseConfiguredOrigins(value) {
111
112
  }
112
113
  return { origins, rejected };
113
114
  }
114
- /** The built-in entries plus the optional public origin. */
115
- function builtInOrigins(publicOrigin) {
116
- const entries = [exports.VIEWER_PWA_ORIGIN, exports.OPAQUE_ORIGIN];
117
- const pub = publicOrigin !== undefined ? normalizeOrigin(publicOrigin) : null;
118
- if (pub !== null && pub !== exports.OPAQUE_ORIGIN)
119
- entries.push(pub);
120
- return entries;
115
+ /** The built-in entries. */
116
+ function builtInOrigins() {
117
+ return [exports.VIEWER_PWA_ORIGIN, exports.OPAQUE_ORIGIN];
121
118
  }
122
119
  /** The first value of a possibly comma-joined forwarding header, lower case. */
123
120
  function firstHost(value) {
@@ -19,7 +19,7 @@ function varyText(value) {
19
19
  return undefined;
20
20
  }
21
21
  function createCorsPolicy(deps) {
22
- const builtIns = (0, cors_policy_js_1.builtInOrigins)(deps.publicOrigin);
22
+ const builtIns = (0, cors_policy_js_1.builtInOrigins)();
23
23
  let lastConfigured = Symbol('unset');
24
24
  let listed = new Set(builtIns);
25
25
  const loggedDenials = new Set();
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HubAddressUnavailableError = void 0;
4
+ exports.resolveHubBaseUrl = resolveHubBaseUrl;
5
+ exports.clusterBaseUrlResolver = clusterBaseUrlResolver;
6
+ /** Thrown when the hub cannot name an address `audience` can reach. */
7
+ class HubAddressUnavailableError extends Error {
8
+ audience;
9
+ reason;
10
+ constructor(audience, reason) {
11
+ super(reason === 'local-network-unavailable'
12
+ ? `cannot name this hub's ${audience} address: the local-network capability is not available yet`
13
+ : `cannot name this hub's ${audience} address: Network lists no reachable LAN address ` +
14
+ '(set the hub address in Settings → Network, or check the allowed addresses)');
15
+ this.audience = audience;
16
+ this.reason = reason;
17
+ this.name = 'HubAddressUnavailableError';
18
+ }
19
+ }
20
+ exports.HubAddressUnavailableError = HubAddressUnavailableError;
21
+ /**
22
+ * Resolve the hub's base URL for `audience` from the live provider. Called per
23
+ * delivery — interfaces and the allowlist move, and a deploy is rare enough
24
+ * that a cache would only add a way to be stale.
25
+ */
26
+ async function resolveHubBaseUrl(getSource, audience) {
27
+ const source = getSource();
28
+ if (source === null)
29
+ throw new HubAddressUnavailableError(audience, 'local-network-unavailable');
30
+ const baseUrl = (await source.getHubAddresses())[audience].resolved;
31
+ if (baseUrl === null)
32
+ throw new HubAddressUnavailableError(audience, 'no-reachable-endpoint');
33
+ return baseUrl;
34
+ }
35
+ function clusterBaseUrlResolver(getSource) {
36
+ return () => resolveHubBaseUrl(getSource, 'cluster');
37
+ }
@@ -36,18 +36,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.tarModelFiles = tarModelFiles;
37
37
  exports.distributeModelWith = distributeModelWith;
38
38
  exports.buildModelDistributorProvider = buildModelDistributorProvider;
39
+ const node_child_process_1 = require("node:child_process");
39
40
  const fs = __importStar(require("node:fs"));
40
41
  const path = __importStar(require("node:path"));
41
- const node_child_process_1 = require("node:child_process");
42
42
  const system_1 = require("@camstack/system");
43
43
  const addon_upload_js_1 = require("./addon-upload.js");
44
44
  const AGENT_DISTRIBUTE_TIMEOUT_MS = 60_000;
45
45
  function hubModelsDir() {
46
46
  return path.join(process.env['CAMSTACK_DATA'] ?? 'camstack-data', 'models');
47
47
  }
48
- function hubBundleBaseUrl() {
49
- return process.env['CAMSTACK_HUB_PUBLIC_URL'] ?? 'https://127.0.0.1:4443';
50
- }
51
48
  /**
52
49
  * `tar -czf -` the given files relative to `cwd`, buffering the gzip stream.
53
50
  * Async (spawn, not execFileSync) so a large model doesn't block the event
@@ -87,7 +84,7 @@ async function distributeModelWith(seam, input) {
87
84
  throw new Error(`model ${modelId}/${format} resolved no files to distribute`);
88
85
  }
89
86
  const tgz = await seam.tar(files);
90
- const source = seam.stage(tgz);
87
+ const source = await seam.stage(tgz);
91
88
  const res = await seam.callAgent(targetNodeId, { modelId, format, source });
92
89
  return { ok: true, sha256: source.sha256, bytes: source.bytes, path: res.path ?? '' };
93
90
  }
@@ -97,7 +94,9 @@ async function distributeModelWith(seam, input) {
97
94
  * Moleculer broker + the deploy-stage registry, which a forked addon can't
98
95
  * reach.
99
96
  */
100
- function buildModelDistributorProvider(moleculer, loggingService) {
97
+ function buildModelDistributorProvider(moleculer, loggingService,
98
+ /** The `local-network` cap's cluster address (`hub-cluster-url.ts`). */
99
+ resolveClusterBaseUrl) {
101
100
  const logger = loggingService.createLogger('model-distributor');
102
101
  const modelsDir = hubModelsDir();
103
102
  const seam = {
@@ -105,7 +104,7 @@ function buildModelDistributorProvider(moleculer, loggingService) {
105
104
  isPresent: (entry, format) => (0, system_1.isModelDownloaded)(modelsDir, entry, format),
106
105
  fileExists: (rel) => fs.existsSync(path.join(modelsDir, rel)),
107
106
  tar: (files) => tarModelFiles(modelsDir, files),
108
- stage: (buffer) => (0, addon_upload_js_1.buildHubHttpSource)(buffer, hubBundleBaseUrl()),
107
+ stage: async (buffer) => (0, addon_upload_js_1.buildHubHttpSource)(buffer, await resolveClusterBaseUrl()),
109
108
  callAgent: async (nodeId, payload) => {
110
109
  const broker = moleculer?.broker;
111
110
  if (!broker)
@@ -463,7 +463,7 @@ function registerOauth2Routes(fastify, deps) {
463
463
  // external-access endpoint surfaced by a forked exporter addon) so the
464
464
  // claim the cloud Lambda routes back on is the reachable public URL, not
465
465
  // the hub-global fallback (which defaults to localhost in dev).
466
- hubUrl: descriptor.hubUrl ?? deps.publicHubUrl(),
466
+ hubUrl: descriptor.hubUrl ?? (await deps.publicHubUrl()),
467
467
  ...(v.codeChallenge !== '' ? { codeChallenge: v.codeChallenge } : {}),
468
468
  // The integration declares its own refresh lifetime; omitting the field
469
469
  // keeps the 30-day default. Baked in HERE, at consent, so the link the
@@ -4912,6 +4912,22 @@ function createCapRouter_localNetwork(getProvider, createRemoteProxy) {
4912
4912
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4913
4913
  return p.setViewerEndpoints(methodInput);
4914
4914
  }),
4915
+ getHubAddresses: trpc_middleware_js_1.protectedProcedure
4916
+ .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
4917
+ .output(types_65.localNetworkCapability.methods.getHubAddresses.output)
4918
+ .query(async ({ input, ctx }) => {
4919
+ const p = resolveProvider('local-network', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
4920
+ return p.getHubAddresses();
4921
+ }),
4922
+ setHubAddress: trpc_middleware_js_1.adminProcedure
4923
+ .input(types_65.localNetworkCapability.methods.setHubAddress.input.loose())
4924
+ .output(types_65.localNetworkCapability.methods.setHubAddress.output)
4925
+ .mutation(async ({ input, ctx }) => {
4926
+ const { nodeId, ...methodInput } = input;
4927
+ const p = resolveProvider('local-network', nodeId, () => getProvider(ctx), createRemoteProxy);
4928
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4929
+ return p.setHubAddress(methodInput);
4930
+ }),
4915
4931
  getAllowedAddresses: trpc_middleware_js_1.protectedProcedure
4916
4932
  .input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
4917
4933
  .output(types_65.localNetworkCapability.methods.getAllowedAddresses.output)
@@ -38,6 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  };
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.ignoredTlsCertSources = ignoredTlsCertSources;
41
42
  exports.loadBootstrapConfig = loadBootstrapConfig;
42
43
  exports.autoGenerateJwtSecret = autoGenerateJwtSecret;
43
44
  exports.setupInfra = setupInfra;
@@ -67,9 +68,15 @@ const ENV_VAR_MAP = {
67
68
  CAMSTACK_ADMIN_USER: 'auth.adminUsername',
68
69
  CAMSTACK_ADMIN_PASS: 'auth.adminPassword',
69
70
  CAMSTACK_TLS_ENABLED: 'tls.enabled',
70
- CAMSTACK_TLS_CERT: 'tls.certPath',
71
- CAMSTACK_TLS_KEY: 'tls.keyPath',
72
71
  };
72
+ /**
73
+ * Env variables that once set the node's certificate. Ignored since D726:
74
+ * the admin UI (Network → Local access) is the one way to set it. Still
75
+ * named here so a node that has them set is told, not silently re-keyed.
76
+ */
77
+ const IGNORED_TLS_CERT_ENV = ['CAMSTACK_TLS_CERT', 'CAMSTACK_TLS_KEY'];
78
+ /** The `config.yaml` keys those variables were mapped onto — ignored too. */
79
+ const IGNORED_TLS_CERT_KEYS = ['certPath', 'keyPath'];
73
80
  // ---------------------------------------------------------------------------
74
81
  // Helpers
75
82
  // ---------------------------------------------------------------------------
@@ -82,6 +89,20 @@ function setNested(obj, p, value) {
82
89
  const child = (0, types_1.asJsonObject)(obj[head]) ?? {};
83
90
  return { ...obj, [head]: setNested(child, rest.join('.'), value) };
84
91
  }
92
+ /**
93
+ * Every env variable and `config.yaml` key that would once have set the
94
+ * certificate and is now ignored (D726). Blank env values count as unset,
95
+ * exactly as the override loop treats them.
96
+ */
97
+ function ignoredTlsCertSources(env, raw) {
98
+ const fromEnv = IGNORED_TLS_CERT_ENV.filter((name) => {
99
+ const value = env[name];
100
+ return value !== undefined && value !== '';
101
+ });
102
+ const tls = (0, types_1.asJsonObject)(raw['tls']) ?? {};
103
+ const fromYaml = IGNORED_TLS_CERT_KEYS.filter((key) => tls[key] !== undefined).map((key) => `tls.${key}`);
104
+ return [...fromEnv, ...fromYaml];
105
+ }
85
106
  // ---------------------------------------------------------------------------
86
107
  // loadBootstrapConfig
87
108
  // ---------------------------------------------------------------------------
@@ -145,6 +166,11 @@ function loadBootstrapConfig(configPath) {
145
166
  raw = setNested(raw, configPath_, coerced);
146
167
  console.log(`[Phase1] Env override: ${envKey} → ${configPath_}`);
147
168
  }
169
+ const ignored = ignoredTlsCertSources(process.env, raw);
170
+ if (ignored.length > 0) {
171
+ console.warn(`[Phase1] WARN ${ignored.join(', ')} ignored — the certificate is set only in the admin UI. ` +
172
+ 'Upload the certificate in Network → Local access (D726).');
173
+ }
148
174
  return config_schema_1.bootstrapSchema.parse(raw);
149
175
  }
150
176
  // ---------------------------------------------------------------------------
@@ -210,22 +236,21 @@ async function setupInfra(configPath, bootstrapConfig) {
210
236
  // broken chunks with require("fs") when leaked .js files exist in core/src/.
211
237
  // CJS build works correctly and tsx supports require().
212
238
  const core = require('@camstack/system');
213
- const { ensureTlsCert, loadTlsCert } = core;
214
- if (config.tls.certPath && config.tls.keyPath) {
215
- // User-provided cert
216
- console.log(`[Phase2c] Loading custom TLS cert from ${config.tls.certPath}`);
217
- const pair = loadTlsCert(config.tls.certPath, config.tls.keyPath);
218
- tlsOptions = { key: pair.key, cert: pair.cert };
239
+ // The certificate comes only from the admin UI (D726): the generated
240
+ // local CA → leaf (D227), or an operator upload served verbatim.
241
+ const extraSans = [...core.readExtraSans(dataPath)];
242
+ const boot = await core.prepareBootTls(dataPath, extraSans.length > 0 ? { extraSans } : undefined);
243
+ if (boot.uploadedMissing) {
244
+ console.warn(`[Phase2c] WARN the uploaded TLS certificate is missing from ${path.join(dataPath, 'tls')} — serving the generated one. Upload it again in Network → Local access.`);
245
+ }
246
+ if (boot.ensured !== undefined) {
247
+ tlsCert = boot.ensured;
248
+ logTlsCert(boot.ensured);
219
249
  }
220
250
  else {
221
- // Hub-generated local CA → leaf (D227)
222
- const extraSans = [...(core.readExtraSans?.(dataPath) ?? [])];
223
- const tlsResult = await ensureTlsCert(dataPath, extraSans.length > 0 ? { extraSans } : undefined);
224
- tlsCert = tlsResult;
225
- logTlsCert(tlsResult);
226
- const pair = loadTlsCert(tlsResult.certPath, tlsResult.keyPath);
227
- tlsOptions = { key: pair.key, cert: pair.cert };
251
+ console.log(`[Phase2c] Serving the uploaded TLS certificate from ${path.join(dataPath, 'tls')}`);
228
252
  }
253
+ tlsOptions = { key: boot.pair.key, cert: boot.pair.cert };
229
254
  }
230
255
  return {
231
256
  bootstrapConfig: config,
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * The hub's address is not an environment variable.
4
+ *
5
+ * `CAMSTACK_HUB_PUBLIC_URL` (and its older alias `CAMSTACK_PUBLIC_ORIGIN`)
6
+ * restated something the hub already knows: its interfaces, its listen port,
7
+ * the operator's allowlist, its tunnel and mesh names — the `local-network`
8
+ * capability, shown and edited on the admin Network page. A hub without the
9
+ * variable (every Mac hub: the desktop app has no field for it) handed agents
10
+ * `https://127.0.0.1:4443`, which names the agent, and every addon and model
11
+ * pull to an agent failed. Every reader now asks `local-network`
12
+ * (`pickHubBaseUrl`); nothing reads the variables.
13
+ *
14
+ * A container that still sets one gets a WARN on every boot naming it as
15
+ * ignored, and the variable is removed from this process's environment before
16
+ * any addon runner is forked — so no child can grow a new reader of it.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.RETIRED_HUB_ADDRESS_ENVS = void 0;
20
+ exports.dropRetiredHubAddressEnv = dropRetiredHubAddressEnv;
21
+ /** Variables that used to name the hub's address; all ignored now. */
22
+ exports.RETIRED_HUB_ADDRESS_ENVS = [
23
+ 'CAMSTACK_HUB_PUBLIC_URL',
24
+ 'CAMSTACK_PUBLIC_ORIGIN',
25
+ ];
26
+ /**
27
+ * Remove every retired address variable from `env` and return the warning to
28
+ * log once a logger exists, or `undefined` when none was set.
29
+ */
30
+ function dropRetiredHubAddressEnv(env) {
31
+ const present = exports.RETIRED_HUB_ADDRESS_ENVS.filter((name) => {
32
+ const value = env[name];
33
+ return value !== undefined && value.trim() !== '';
34
+ });
35
+ for (const name of exports.RETIRED_HUB_ADDRESS_ENVS)
36
+ delete env[name];
37
+ if (present.length === 0)
38
+ return undefined;
39
+ return (`${present.join(' and ')} ${present.length === 1 ? 'is' : 'are'} ignored — the hub address ` +
40
+ 'comes from Network (Settings → Network). Remove it from this container.');
41
+ }
@@ -55,7 +55,7 @@ function decide(pkg, installed, closureVersions, closureProvided) {
55
55
  installedVersion: version,
56
56
  closureVersion,
57
57
  twoVersions: false,
58
- reason: 'provided by the server closure — a copy here would shadow it (D15)',
58
+ reason: 'provided by the server closure — a copy here would shadow it',
59
59
  };
60
60
  }
61
61
  if (version === null) {
@@ -78,7 +78,7 @@ function decide(pkg, installed, closureVersions, closureProvided) {
78
78
  closureVersion,
79
79
  twoVersions,
80
80
  reason: twoVersions
81
- ? `installed ${version}; this closure carries ${closureVersion} — the installed copy is what runs (D90)`
81
+ ? `installed ${version}; this closure carries ${closureVersion} — the installed copy is what runs`
82
82
  : `installed ${version} — kept`,
83
83
  };
84
84
  }
package/dist/main.js CHANGED
@@ -40,6 +40,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
40
40
  const node_child_process_1 = require("node:child_process");
41
41
  const fs = __importStar(require("node:fs"));
42
42
  const path = __importStar(require("node:path"));
43
+ const node_tls_1 = require("node:tls");
43
44
  // Boot-time capability declaration runs over the auto-generated
44
45
  // `ALL_CAPABILITY_DEFINITIONS` array — every `*.cap.ts` file that ships
45
46
  // with `@camstack/types` is included automatically. Adding a new cap
@@ -65,6 +66,7 @@ const request_census_settings_1 = require("./api/core/request-census-settings");
65
66
  const register_cors_policy_1 = require("./api/cors/register-cors-policy");
66
67
  const adaptive_probe_routes_1 = require("./api/health/adaptive-probe.routes");
67
68
  const health_routes_1 = require("./api/health/health.routes");
69
+ const hub_cluster_url_1 = require("./api/hub-cluster-url");
68
70
  const model_distributor_js_1 = require("./api/model-distributor.js");
69
71
  const oauth2_routes_js_1 = require("./api/oauth2/oauth2-routes.js");
70
72
  const server_upload_1 = require("./api/server-upload");
@@ -89,9 +91,9 @@ const session_cookie_js_1 = require("./auth/session-cookie.js");
89
91
  const session_mint_js_1 = require("./auth/session-mint.js");
90
92
  const sso_finish_js_1 = require("./auth/sso-finish.js");
91
93
  const boot_config_1 = require("./boot/boot-config");
92
- const public_url_1 = require("./boot/public-url");
93
94
  const integration_id_backfill_1 = require("./boot/integration-id-backfill");
94
95
  const post_boot_service_1 = require("./boot/post-boot.service");
96
+ const retired_public_url_env_1 = require("./boot/retired-public-url-env");
95
97
  const addon_package_service_1 = require("./core/addon/addon-package.service");
96
98
  const addon_registry_service_1 = require("./core/addon/addon-registry.service");
97
99
  const addon_bridge_service_1 = require("./core/addon-bridge/addon-bridge.service");
@@ -241,11 +243,11 @@ async function sendMaybePrecompressed(reply, absolutePath, acceptEncoding) {
241
243
  }
242
244
  // ---- Bootstrap ----
243
245
  async function bootstrap() {
244
- // ONE public-URL variable (CAMSTACK_HUB_PUBLIC_URL). The retired
245
- // CAMSTACK_PUBLIC_ORIGIN is folded into it here — before any addon runner is
246
- // forked, so every child inherits one name — and warned about once a logger
247
- // exists (below). See `boot/public-url.ts`.
248
- const publicUrlWarning = (0, public_url_1.applyHubPublicUrl)(process.env);
246
+ // The hub's address comes from the `local-network` cap, never from env. A
247
+ // retired address variable is dropped here — before any addon runner is
248
+ // forked, so no child can read it — and warned about once a logger exists
249
+ // (below). See `boot/retired-public-url-env.ts`.
250
+ const publicUrlWarning = (0, retired_public_url_env_1.dropRetiredHubAddressEnv)(process.env);
249
251
  // hub-main's own memory, on the record. Deliberately started HERE and not in
250
252
  // the starter: the starter runs from the image's /opt closure, which only
251
253
  // changes with a new image, while this entry is what `applyServerUpdate`
@@ -364,7 +366,6 @@ async function bootstrap() {
364
366
  const corsConfig = app.get(config_service_1.ConfigService);
365
367
  const corsPolicy = (0, register_cors_policy_1.createCorsPolicy)({
366
368
  configuredOrigins: () => corsConfig.get('server.corsOrigins'),
367
- publicOrigin: process.env[public_url_1.HUB_PUBLIC_URL_ENV],
368
369
  logger: app.get(logging_service_1.LoggingService).createLogger('cors'),
369
370
  });
370
371
  (0, register_cors_policy_1.registerCorsPolicy)(fastify, corsPolicy);
@@ -411,7 +412,9 @@ async function bootstrap() {
411
412
  // The delivery ledger marks "the hub is writing addons to this node" for
412
413
  // the whole deploy+reload, so the addon back-fill cannot read the roster
413
414
  // gap a deploy opens and repair the node over the bytes just shipped.
414
- await (0, addon_upload_1.registerAddonUploadRoute)(fastify, uploadAddonBridge, uploadAuthService, uploadMoleculer, uploadAddonRegistry, uploadAddonPackage, uploadLogger, app.get(agent_registry_service_1.AgentRegistryService).deliveryLedger);
415
+ await (0, addon_upload_1.registerAddonUploadRoute)(fastify, uploadAddonBridge, uploadAuthService, uploadMoleculer, uploadAddonRegistry, uploadAddonPackage, uploadLogger, (0, hub_cluster_url_1.clusterBaseUrlResolver)(() => uploadAddonRegistry
416
+ .getCapabilityRegistry()
417
+ .getSingleton('local-network')), app.get(agent_registry_service_1.AgentRegistryService).deliveryLedger);
415
418
  console.log('[bootstrap] Addon upload route registered at POST /api/addons/upload');
416
419
  // Dev server-deploy channel gateway (`camstack deploy-server`). MUST be
417
420
  // registered AFTER registerAddonUploadRoute — it reuses the multipart
@@ -591,7 +594,7 @@ async function bootstrap() {
591
594
  // router) so forked addons (addon-model-studio) can reach it via `ctx.api`,
592
595
  // not only external HTTP clients. Needs the broker + the deploy-stage
593
596
  // registry, which an addon can't access.
594
- capabilityRegistry.registerProvider('model-distributor', '$hub', (0, model_distributor_js_1.buildModelDistributorProvider)(moleculer, loggingService));
597
+ capabilityRegistry.registerProvider('model-distributor', '$hub', (0, model_distributor_js_1.buildModelDistributorProvider)(moleculer, loggingService, (0, hub_cluster_url_1.clusterBaseUrlResolver)(() => capabilityRegistry.getSingleton('local-network'))));
595
598
  // ── Health routes (hub self + agent forwarding) ──────────────────
596
599
  // Registered after app.init() so the AgentRegistryService is wired and
597
600
  // the Moleculer broker is ready to forward `$agent.health` calls.
@@ -1116,7 +1119,10 @@ async function bootstrap() {
1116
1119
  (0, oauth2_routes_js_1.registerOauth2Routes)(fastify, {
1117
1120
  getRegistry: () => capabilityRegistry,
1118
1121
  verifyToken: (t) => authService.verifyToken(t),
1119
- publicHubUrl: () => process.env[public_url_1.HUB_PUBLIC_URL_ENV] ?? `https://localhost:${port}`,
1122
+ // The hub's browser-facing address from the `local-network` cap (tunnel,
1123
+ // then mesh, then LAN). `localhost` only when the palette names nothing —
1124
+ // the dev case, where it is the truth.
1125
+ publicHubUrl: () => (0, hub_cluster_url_1.resolveHubBaseUrl)(() => capabilityRegistry.getSingleton('local-network'), 'browser').catch(() => `https://localhost:${port}`),
1120
1126
  // Every /authorize refusal is a dropped account-linking attempt. Without
1121
1127
  // this scope the drop is invisible in Loki and the only signal left is an
1122
1128
  // operator noticing that linking stopped working.
@@ -1489,6 +1495,17 @@ async function bootstrap() {
1489
1495
  const logger = app.get(logging_service_1.LoggingService).createLogger('System');
1490
1496
  const protocol = tlsOptions ? 'https' : 'http';
1491
1497
  logger.info('CamStack server listening', { meta: { protocol, host, port, trpcRegistered } });
1498
+ // Certificate actions (Network → Local access) swap the served certificate
1499
+ // in place through this registration — without it an upload sat on disk
1500
+ // while the listener kept serving the boot-time leaf (D726).
1501
+ if (tlsOptions) {
1502
+ if (fastify.server instanceof node_tls_1.Server) {
1503
+ (0, system_1.registerHttpsListener)(fastify.server, tlsOptions);
1504
+ }
1505
+ else {
1506
+ logger.warn('HTTPS listener is not a TLS server — certificate changes need a restart');
1507
+ }
1508
+ }
1492
1509
  // LAN HTTP sibling — same Fastify request graph, dedicated port. Handler
1493
1510
  // is registered as soon as we have the instance; bind happens after HTTPS
1494
1511
  // is up so a failed HTTPS listen does not leave HTTP serving alone.
@@ -51,6 +51,7 @@ const types_1 = require("@camstack/types");
51
51
  const fastify_1 = __importDefault(require("fastify"));
52
52
  const addon_upload_1 = require("./api/addon-upload");
53
53
  const cap_providers_1 = require("./api/core/cap-providers");
54
+ const hub_cluster_url_1 = require("./api/hub-cluster-url");
54
55
  const init_sequence_1 = require("./boot/init-sequence");
55
56
  const post_boot_service_1 = require("./boot/post-boot.service");
56
57
  const addon_package_service_1 = require("./core/addon/addon-package.service");
@@ -184,6 +185,11 @@ async function bootManual(opts) {
184
185
  // reverted ~1.5s later and read as a stale dist. See `packInstalledPackage`.
185
186
  // Delivery is an RPC pair, never an event (D8/D11).
186
187
  const backfillLogger = loggingService.createLogger('agent-backfill');
188
+ // Where the agent pulls the bundle from: the hub's own cluster address, from
189
+ // the `local-network` cap — never an env var, never loopback.
190
+ const resolveClusterBaseUrl = (0, hub_cluster_url_1.clusterBaseUrlResolver)(() => addonRegistryService
191
+ .getCapabilityRegistry()
192
+ .getSingleton('local-network'));
187
193
  agentRegistryService.setAddonBackfill({
188
194
  pack: async (name, version) => {
189
195
  const installed = await addonPackageService.packInstalledPackage(name);
@@ -200,7 +206,7 @@ async function bootManual(opts) {
200
206
  return (installed ?? (await addonPackageService.packPackage(name, version))).buffer;
201
207
  },
202
208
  deploy: async (nodeId, packageName, bundle) => {
203
- await moleculerService.broker.call('$agent.deploy', { addonId: packageName, source: (0, addon_upload_1.buildHubHttpSource)(bundle, (0, addon_upload_1.hubBundleBaseUrl)()) }, { nodeID: nodeId, timeout: agent_registry_service_1.AGENT_BACKFILL_RPC_TIMEOUT_MS });
209
+ await moleculerService.broker.call('$agent.deploy', { addonId: packageName, source: (0, addon_upload_1.buildHubHttpSource)(bundle, await resolveClusterBaseUrl()) }, { nodeID: nodeId, timeout: agent_registry_service_1.AGENT_BACKFILL_RPC_TIMEOUT_MS });
204
210
  },
205
211
  reload: async (nodeId) => {
206
212
  await moleculerService.broker.call('$agent.reload', {}, { nodeID: nodeId, timeout: agent_registry_service_1.AGENT_BACKFILL_RPC_TIMEOUT_MS });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.459",
3
+ "version": "1.2.461",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -37,18 +37,18 @@
37
37
  ]
38
38
  },
39
39
  "dependencies": {
40
- "@camstack/addon-admin-ui": "1.2.333",
41
- "@camstack/addon-agent-ui": "1.2.175",
42
- "@camstack/addon-auth": "1.2.127",
40
+ "@camstack/addon-admin-ui": "1.2.335",
41
+ "@camstack/addon-agent-ui": "1.2.177",
42
+ "@camstack/addon-auth": "1.2.128",
43
43
  "@camstack/addon-decoder-nodeav": "1.2.119",
44
44
  "@camstack/addon-notifiers": "1.2.128",
45
- "@camstack/addon-pipeline": "1.2.328",
46
- "@camstack/addon-pipeline-orchestrator": "1.2.283",
47
- "@camstack/addon-post-analysis": "1.2.335",
45
+ "@camstack/addon-pipeline": "1.2.330",
46
+ "@camstack/addon-pipeline-orchestrator": "1.2.285",
47
+ "@camstack/addon-post-analysis": "1.2.337",
48
48
  "@camstack/sdk": "1.2.122",
49
- "@camstack/system": "1.2.356",
50
- "@camstack/types": "1.2.307",
51
- "@camstack/ui-library": "1.2.233",
49
+ "@camstack/system": "1.2.358",
50
+ "@camstack/types": "1.2.309",
51
+ "@camstack/ui-library": "1.2.235",
52
52
  "@fastify/compress": "^9.0.0",
53
53
  "@fastify/cookie": "^11.0.2",
54
54
  "@fastify/cors": "^11.2.0",
@@ -1,67 +0,0 @@
1
- "use strict";
2
- /**
3
- * The hub's public URL — ONE environment variable, resolved once at boot.
4
- *
5
- * Two names used to answer the same question ("at which address do clients
6
- * reach this hub?"): `CAMSTACK_HUB_PUBLIC_URL` (addon bundle and model
7
- * distribution, notification links, the Dreame RFC host) and
8
- * `CAMSTACK_PUBLIC_ORIGIN` (magic-link, OIDC callback, WebAuthn relying party,
9
- * the OAuth2 issuer and the CORS allowlist, D707). An operator who set one got
10
- * half a configured hub and no line saying so.
11
- *
12
- * `CAMSTACK_HUB_PUBLIC_URL` is the survivor. `CAMSTACK_PUBLIC_ORIGIN` is still
13
- * accepted, for hubs that already set it, and costs a WARN on every boot until
14
- * it is renamed. The resolution writes the survivor back into `process.env`
15
- * and deletes the retired name, so every forked addon runner — which inherits
16
- * this process's environment — reads exactly one variable.
17
- */
18
- Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.RETIRED_PUBLIC_ORIGIN_ENV = exports.HUB_PUBLIC_URL_ENV = void 0;
20
- exports.resolveHubPublicUrl = resolveHubPublicUrl;
21
- exports.applyHubPublicUrl = applyHubPublicUrl;
22
- /** The single public-URL variable every reader uses. */
23
- exports.HUB_PUBLIC_URL_ENV = 'CAMSTACK_HUB_PUBLIC_URL';
24
- /** Retired alias, accepted with a deprecation warning. */
25
- exports.RETIRED_PUBLIC_ORIGIN_ENV = 'CAMSTACK_PUBLIC_ORIGIN';
26
- function readTrimmed(env, name) {
27
- const raw = env[name]?.trim();
28
- if (raw === undefined || raw === '')
29
- return undefined;
30
- return raw.replace(/\/+$/, '');
31
- }
32
- /** Pure: which URL wins, and what to warn about. */
33
- function resolveHubPublicUrl(env) {
34
- const current = readTrimmed(env, exports.HUB_PUBLIC_URL_ENV);
35
- const retired = readTrimmed(env, exports.RETIRED_PUBLIC_ORIGIN_ENV);
36
- if (retired === undefined)
37
- return { url: current, warning: undefined };
38
- if (current === undefined) {
39
- return {
40
- url: retired,
41
- warning: `${exports.RETIRED_PUBLIC_ORIGIN_ENV} is deprecated and was read as ${exports.HUB_PUBLIC_URL_ENV}=${retired}. ` +
42
- `Rename it to ${exports.HUB_PUBLIC_URL_ENV}; the old name will stop being read in a future release.`,
43
- };
44
- }
45
- const disagree = current !== retired;
46
- return {
47
- url: current,
48
- warning: `${exports.RETIRED_PUBLIC_ORIGIN_ENV} is deprecated and IGNORED because ${exports.HUB_PUBLIC_URL_ENV} is set` +
49
- (disagree ? ` (${current} wins over ${retired})` : '') +
50
- `. Remove ${exports.RETIRED_PUBLIC_ORIGIN_ENV} from this container.`,
51
- };
52
- }
53
- /**
54
- * Apply the resolution to a process environment: the survivor is set (or left
55
- * unset), the retired name is removed. Returns the warning to log once a
56
- * logger exists. Called once, first thing in the hub's bootstrap, before any
57
- * addon runner is forked.
58
- */
59
- function applyHubPublicUrl(env) {
60
- const { url, warning } = resolveHubPublicUrl(env);
61
- if (url === undefined)
62
- delete env[exports.HUB_PUBLIC_URL_ENV];
63
- else
64
- env[exports.HUB_PUBLIC_URL_ENV] = url;
65
- delete env[exports.RETIRED_PUBLIC_ORIGIN_ENV];
66
- return warning;
67
- }