@camstack/server 1.2.381 → 1.2.382

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.
@@ -218,6 +218,8 @@ function buildNetworkQualityProvider(nq) {
218
218
  jitterMs: input.jitterMs,
219
219
  estimatedBandwidthKbps: input.estimatedBandwidthKbps,
220
220
  packetLossPercent: input.packetLossPercent,
221
+ ...(input.downlinkMbps !== undefined ? { downlinkMbps: input.downlinkMbps } : {}),
222
+ ...(input.downlinkAgeMs !== undefined ? { downlinkAgeMs: input.downlinkAgeMs } : {}),
221
223
  });
222
224
  },
223
225
  };
@@ -1,12 +1,66 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ADAPTIVE_PROBE_BYTES = void 0;
3
+ exports.ADAPTIVE_PROBE_PER_IP_PER_MINUTE = exports.ADAPTIVE_PROBE_BYTES = void 0;
4
4
  exports.registerAdaptiveProbeRoute = registerAdaptiveProbeRoute;
5
- exports.ADAPTIVE_PROBE_BYTES = 128 * 1024;
6
- const BODY = Buffer.alloc(exports.ADAPTIVE_PROBE_BYTES, 0x61);
7
- function registerAdaptiveProbeRoute(fastify) {
8
- fastify.get('/adaptive-probe', async (_req, reply) => {
5
+ /**
6
+ * Burst blob for adaptive-tier downlink measurement (D635).
7
+ *
8
+ * Size: 1 MiB. The client times the body from its FIRST byte and decides
9
+ * against 2.5 Mbps (`mid`) and 8 Mbps (`high`). At 8 Mbps, 1 MiB is ~1.05 s of
10
+ * transfer, so a 16 ms WebView timer tick is ~1.5 % of the reading and a WAN
11
+ * slow start (~3 RTTs at 50 ms) is ~15 %; the previous 128 KiB was 131 ms at
12
+ * the same rate — a single timer tick was 12 % of it, and timed from the
13
+ * request on a busy hub it measured latency (1.2–1.46 Mbps on a LAN).
14
+ *
15
+ * Cost, including cellular: one probe per camera app-wide per 30 s cache
16
+ * window (the client single-flights it), cut by the client's 2.5 s deadline —
17
+ * at most ~0.8 MB at 2.5 Mbps, 1 MiB on anything faster. That is about three
18
+ * minutes of a 41 kbps sub stream per camera OPEN, not per minute of viewing;
19
+ * renegotiations reuse the cached value and never probe.
20
+ *
21
+ * Access (round 3, review I6): a 100-byte request that returns 1 MiB is an
22
+ * amplifier, and the hub is reachable through relays. The route therefore
23
+ * requires a SESSION — Bearer JWT or the session cookie, the same session-grade
24
+ * check the stream data plane applies — and is rate-limited per client IP.
25
+ * Everything a refusal returns is a few bytes.
26
+ */
27
+ const node_crypto_1 = require("node:crypto");
28
+ const health_routes_js_1 = require("./health.routes.js");
29
+ const auth_rate_limit_js_1 = require("../../auth/auth-rate-limit.js");
30
+ exports.ADAPTIVE_PROBE_BYTES = 1024 * 1024;
31
+ /**
32
+ * Probes a client IP may draw per minute: 12. The client probes at most once
33
+ * per 30 s per app (single-flight + cache), so 12 leaves room for several
34
+ * apps/devices behind one NAT and caps an abuser at 12 MiB/min.
35
+ */
36
+ exports.ADAPTIVE_PROBE_PER_IP_PER_MINUTE = 12;
37
+ /**
38
+ * Random, so it is INCOMPRESSIBLE. The previous body was one repeated byte and
39
+ * `@fastify/compress` (global in `main.ts`; its default types include
40
+ * `octet-stream`) gzipped it to a few hundred bytes — the client timed a round
41
+ * trip and called it bandwidth. `compress: false` below stops our own
42
+ * compressor; randomness also defeats one in a relay on the way.
43
+ */
44
+ const BODY = (0, node_crypto_1.randomBytes)(exports.ADAPTIVE_PROBE_BYTES);
45
+ function registerAdaptiveProbeRoute(fastify, deps) {
46
+ fastify.get('/adaptive-probe', { compress: false }, async (req, reply) => {
9
47
  reply.header('cache-control', 'no-store');
48
+ const authorized = (0, health_routes_js_1.isAuthorizedHealthRequest)({
49
+ ...(typeof req.headers.authorization === 'string'
50
+ ? { authorization: req.headers.authorization }
51
+ : {}),
52
+ ...(typeof req.headers.cookie === 'string' ? { cookie: req.headers.cookie } : {}),
53
+ }, deps.verifyToken);
54
+ if (!authorized)
55
+ return reply.status(401).send({ ok: false, error: 'unauthorized' });
56
+ const key = (0, auth_rate_limit_js_1.clientKeyFromRequest)(req);
57
+ if (key !== null) {
58
+ const verdict = deps.limiter.check(`adaptive-probe:${key}`);
59
+ if (!verdict.allowed) {
60
+ reply.header('retry-after', String(verdict.retryAfterSeconds));
61
+ return reply.status(429).send({ ok: false, error: 'rate limited' });
62
+ }
63
+ }
10
64
  reply.header('content-type', 'application/octet-stream');
11
65
  return reply.send(BODY);
12
66
  });
@@ -3136,6 +3136,15 @@ function createCapRouter_deviceManager(getProvider, createRemoteProxy) {
3136
3136
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
3137
3137
  return p.getAllBindings(methodInput);
3138
3138
  }),
3139
+ listDeviceIdsByCaps: trpc_middleware_js_1.protectedProcedure
3140
+ .input(types_43.deviceManagerCapability.methods.listDeviceIdsByCaps.input.loose())
3141
+ .output(types_43.deviceManagerCapability.methods.listDeviceIdsByCaps.output)
3142
+ .query(async ({ input, ctx }) => {
3143
+ const { nodeId, ...methodInput } = input;
3144
+ const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
3145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
3146
+ return p.listDeviceIdsByCaps(methodInput);
3147
+ }),
3139
3148
  setWrapperActive: trpc_middleware_js_1.adminProcedure
3140
3149
  .input(types_43.deviceManagerCapability.methods.setWrapperActive.input.loose())
3141
3150
  .output(types_43.deviceManagerCapability.methods.setWrapperActive.output)
package/dist/main.js CHANGED
@@ -60,6 +60,7 @@ const addon_upload_1 = require("./api/addon-upload");
60
60
  const auth_whoami_1 = require("./api/auth-whoami");
61
61
  const backup_download_js_1 = require("./api/backup-download.js");
62
62
  const health_routes_1 = require("./api/health/health.routes");
63
+ const auth_rate_limit_1 = require("./auth/auth-rate-limit");
63
64
  const adaptive_probe_routes_1 = require("./api/health/adaptive-probe.routes");
64
65
  const model_distributor_js_1 = require("./api/model-distributor.js");
65
66
  const oauth2_routes_js_1 = require("./api/oauth2/oauth2-routes.js");
@@ -583,7 +584,10 @@ async function bootstrap() {
583
584
  verifyToken: (token) => app.get(auth_service_1.AuthService).verifyToken(token),
584
585
  });
585
586
  console.log(`[bootstrap] Health routes registered (hub v${hubVersion})`);
586
- (0, adaptive_probe_routes_1.registerAdaptiveProbeRoute)(fastify);
587
+ (0, adaptive_probe_routes_1.registerAdaptiveProbeRoute)(fastify, {
588
+ verifyToken: (token) => app.get(auth_service_1.AuthService).verifyToken(token),
589
+ limiter: (0, auth_rate_limit_1.createRateLimiter)({ windowMs: 60_000, max: adaptive_probe_routes_1.ADAPTIVE_PROBE_PER_IP_PER_MINUTE }),
590
+ });
587
591
  }
588
592
  catch (err) {
589
593
  console.warn('[bootstrap] Failed to register health routes:', err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.381",
3
+ "version": "1.2.382",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -35,18 +35,18 @@
35
35
  ]
36
36
  },
37
37
  "dependencies": {
38
- "@camstack/addon-admin-ui": "1.2.262",
39
- "@camstack/addon-agent-ui": "1.2.143",
38
+ "@camstack/addon-admin-ui": "1.2.264",
39
+ "@camstack/addon-agent-ui": "1.2.145",
40
40
  "@camstack/addon-auth": "1.2.123",
41
41
  "@camstack/addon-decoder-nodeav": "1.2.115",
42
42
  "@camstack/addon-notifiers": "1.2.120",
43
- "@camstack/addon-pipeline": "1.2.290",
44
- "@camstack/addon-pipeline-orchestrator": "1.2.247",
45
- "@camstack/addon-post-analysis": "1.2.277",
43
+ "@camstack/addon-pipeline": "1.2.292",
44
+ "@camstack/addon-pipeline-orchestrator": "1.2.249",
45
+ "@camstack/addon-post-analysis": "1.2.279",
46
46
  "@camstack/sdk": "1.2.118",
47
- "@camstack/system": "1.2.295",
48
- "@camstack/types": "1.2.251",
49
- "@camstack/ui-library": "1.2.201",
47
+ "@camstack/system": "1.2.297",
48
+ "@camstack/types": "1.2.253",
49
+ "@camstack/ui-library": "1.2.203",
50
50
  "@fastify/compress": "^9.0.0",
51
51
  "@fastify/cookie": "^11.0.2",
52
52
  "@fastify/cors": "^11.2.0",