@camstack/server 1.2.601 → 1.2.603

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.
@@ -6,6 +6,7 @@ exports.validateAuthorizeQuery = validateAuthorizeQuery;
6
6
  exports.isRedirectUriAllowed = isRedirectUriAllowed;
7
7
  exports.summariseScopes = summariseScopes;
8
8
  exports.registerOauth2Routes = registerOauth2Routes;
9
+ const types_1 = require("@camstack/types");
9
10
  const session_cookie_js_1 = require("../../auth/session-cookie.js");
10
11
  const app_page_strings_js_1 = require("../../auth/app-page-strings.js");
11
12
  const consent_page_js_1 = require("./consent-page.js");
@@ -232,7 +233,7 @@ function summariseScopes(scopes) {
232
233
  * weakened. The RESPONSE carries only the caller's own echoed input; it tells
233
234
  * a client nothing about what else this hub has installed.
234
235
  */
235
- function refuseAuthorize(reply, logger, method, call, refusal) {
236
+ function refuseAuthorize(request, reply, logger, method, call, refusal) {
236
237
  logger.warn(`oauth2 authorize refused: ${refusal.error}`, {
237
238
  meta: {
238
239
  method,
@@ -245,7 +246,28 @@ function refuseAuthorize(reply, logger, method, call, refusal) {
245
246
  if (refusal.retryAfterSec !== undefined) {
246
247
  void reply.header('Retry-After', String(refusal.retryAfterSec));
247
248
  }
248
- return reply.status(refusal.status).send({ error: refusal.error, ...refusal.detail });
249
+ return answerRefusal(request, reply, refusal.status, { error: refusal.error, ...refusal.detail }, refusal.status === 503 ? 'unavailable' : 'invalid_request', typeof refusal.detail?.received === 'string' ? refusal.detail.received : undefined);
250
+ }
251
+ /**
252
+ * Answer a refused authorize. A BROWSER (it asked for HTML: a person who
253
+ * followed a link) is sent to the CamStack completion page with a reason CODE
254
+ * — a bare JSON body is a dead end for them. A machine caller keeps the status
255
+ * and JSON it always got. Relative: the browser is already on the hub's origin,
256
+ * and nothing the request carried (the redirect target, the state, any code)
257
+ * goes into the URL.
258
+ */
259
+ function answerRefusal(request, reply, status, body, reason, integration) {
260
+ const accept = request.headers.accept;
261
+ if (typeof accept === 'string' && accept.includes('text/html')) {
262
+ return reply.redirect((0, types_1.oauthLandingUrl)(null, {
263
+ status: 'error',
264
+ reason,
265
+ ...(integration !== undefined && /^[A-Za-z0-9._-]{1,64}$/.test(integration)
266
+ ? { integration }
267
+ : {}),
268
+ }));
269
+ }
270
+ return reply.status(status).send(body);
249
271
  }
250
272
  /**
251
273
  * An authorize call turned away for want of a usable session.
@@ -317,13 +339,13 @@ function registerOauth2Routes(fastify, deps) {
317
339
  }
318
340
  const registry = deps.getRegistry();
319
341
  if (!registry) {
320
- return reply.status(503).send({ error: 'service_unavailable' });
342
+ return answerRefusal(request, reply, 503, { error: 'service_unavailable' }, 'unavailable');
321
343
  }
322
344
  const { descriptors, pendingAddons } = await readIntegrationRegistry(registry);
323
345
  const query = request.query;
324
346
  const v = validateAuthorizeQuery(query, descriptors, pendingAddons);
325
347
  if (!v.ok) {
326
- return refuseAuthorize(reply, deps.logger, 'GET', call, v);
348
+ return refuseAuthorize(request, reply, deps.logger, 'GET', call, v);
327
349
  }
328
350
  const descriptor = descriptors.get(v.integration);
329
351
  if (!isRedirectUriAllowed(v.redirectUri, descriptor.allowedRedirectPrefixes, descriptor.allowedPrivateHostPaths ?? [])) {
@@ -337,9 +359,7 @@ function registerOauth2Routes(fastify, deps) {
337
359
  allowedPrivateHostPaths: descriptor.allowedPrivateHostPaths ?? [],
338
360
  },
339
361
  });
340
- return reply
341
- .status(400)
342
- .send({ error: 'invalid_request — redirect_uri not allowed for this integration' });
362
+ return answerRefusal(request, reply, 400, { error: 'invalid_request — redirect_uri not allowed for this integration' }, 'invalid_request', v.integration);
343
363
  }
344
364
  const html = (0, consent_page_js_1.renderConsentPage)({
345
365
  lang: (0, app_page_strings_js_1.resolveAppPageLang)({
@@ -417,7 +437,7 @@ function registerOauth2Routes(fastify, deps) {
417
437
  }
418
438
  const registry = deps.getRegistry();
419
439
  if (!registry) {
420
- return reply.status(503).send({ error: 'service_unavailable' });
440
+ return answerRefusal(request, reply, 503, { error: 'service_unavailable' }, 'unavailable');
421
441
  }
422
442
  const { descriptors, pendingAddons } = await readIntegrationRegistry(registry);
423
443
  const body = request.body;
@@ -433,7 +453,7 @@ function registerOauth2Routes(fastify, deps) {
433
453
  // and every one of them is attacker-editable.
434
454
  const v = validateAuthorizeQuery(formQuery, descriptors, pendingAddons);
435
455
  if (!v.ok) {
436
- return refuseAuthorize(reply, deps.logger, 'POST', call, v);
456
+ return refuseAuthorize(request, reply, deps.logger, 'POST', call, v);
437
457
  }
438
458
  const descriptor = descriptors.get(v.integration);
439
459
  if (!isRedirectUriAllowed(v.redirectUri, descriptor.allowedRedirectPrefixes, descriptor.allowedPrivateHostPaths ?? [])) {
@@ -447,9 +467,7 @@ function registerOauth2Routes(fastify, deps) {
447
467
  allowedPrivateHostPaths: descriptor.allowedPrivateHostPaths ?? [],
448
468
  },
449
469
  });
450
- return reply
451
- .status(400)
452
- .send({ error: 'invalid_request — redirect_uri not allowed for this integration' });
470
+ return answerRefusal(request, reply, 400, { error: 'invalid_request — redirect_uri not allowed for this integration' }, 'invalid_request', v.integration);
453
471
  }
454
472
  if (body.consent !== 'allow') {
455
473
  return reply.redirect(`${v.redirectUri}?error=access_denied&state=${encodeURIComponent(v.state)}`);
@@ -345,6 +345,54 @@ function createCapRouterPrimitives() {
345
345
  iterableSubscription: trpc_middleware_js_1.iterableSubscription,
346
346
  };
347
347
  }
348
+ /**
349
+ * `device-adoption` is collection-scoped: without an explicit `{ addonId }`
350
+ * pin the router must NOT fall through to the first registered provider.
351
+ * Resolve the owning addon from `integrationId` (most methods) or
352
+ * `camDeviceId` (`resync` only).
353
+ */
354
+ async function inferDeviceAdoptionCollectionAddonPin(methodName, input, deps) {
355
+ const integrationId = typeof input.integrationId === 'string' && input.integrationId.length > 0
356
+ ? input.integrationId
357
+ : undefined;
358
+ const camDeviceId = typeof input.camDeviceId === 'number' && Number.isInteger(input.camDeviceId)
359
+ ? input.camDeviceId
360
+ : undefined;
361
+ if (methodName === 'resync' || (camDeviceId !== undefined && integrationId === undefined)) {
362
+ if (camDeviceId === undefined) {
363
+ throw new server_1.TRPCError({
364
+ code: 'BAD_REQUEST',
365
+ message: 'deviceAdoption.resync requires camDeviceId (owning addon is resolved from it) or an explicit addonId pin.',
366
+ });
367
+ }
368
+ const resolved = deps.resolveDeviceAddonId
369
+ ? await Promise.resolve(deps.resolveDeviceAddonId(camDeviceId))
370
+ : null;
371
+ if (resolved === null || resolved === '') {
372
+ throw new server_1.TRPCError({
373
+ code: 'BAD_REQUEST',
374
+ message: `deviceAdoption.${methodName}: device ${camDeviceId} not found — pass addonId explicitly.`,
375
+ });
376
+ }
377
+ return resolved;
378
+ }
379
+ if (integrationId !== undefined) {
380
+ const resolved = deps.resolveIntegrationAddonId
381
+ ? await Promise.resolve(deps.resolveIntegrationAddonId(integrationId))
382
+ : null;
383
+ if (resolved === null || resolved === '') {
384
+ throw new server_1.TRPCError({
385
+ code: 'BAD_REQUEST',
386
+ message: `deviceAdoption.${methodName}: unknown integrationId "${integrationId}" — pass addonId explicitly.`,
387
+ });
388
+ }
389
+ return resolved;
390
+ }
391
+ throw new server_1.TRPCError({
392
+ code: 'BAD_REQUEST',
393
+ message: `deviceAdoption.${methodName} requires addonId (top-level pin) or integrationId to resolve the owning provider.`,
394
+ });
395
+ }
348
396
  /** Build the `services` half of `buildCapRouters(primitives, services)`. */
349
397
  function createCapRouterServices(deps) {
350
398
  const reg = deps.capabilityRegistry;
@@ -419,10 +467,16 @@ function createCapRouterServices(deps) {
419
467
  `An unresolvable addonId is refused rather than answered by another provider.`,
420
468
  });
421
469
  };
470
+ const inferCollectionAddonPin = (capName, methodName, methodInput) => {
471
+ if (capName !== 'device-adoption')
472
+ return undefined;
473
+ return inferDeviceAdoptionCollectionAddonPin(methodName, methodInput, deps);
474
+ };
422
475
  return {
423
476
  getLocalProvider,
424
477
  remoteProxy,
425
478
  rejectUnresolvedAddonPin,
479
+ inferCollectionAddonPin,
426
480
  noProvider: (capName, nodeId) => {
427
481
  if (nodeId !== undefined && nodeId !== 'hub') {
428
482
  throw new server_1.TRPCError({
@@ -186,8 +186,15 @@ function mountAllCaps(services) {
186
186
  decoder: (0, generated_cap_routers_js_1.createCapRouter_decoder)((_ctx) => reg?.getSingleton('decoder') ?? null, remoteCapProxy),
187
187
  detectionPipeline: (0, generated_cap_routers_js_1.createCapRouter_detectionPipeline)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'detection-pipeline'), remoteCapProxy),
188
188
  deviceAdminLink: (0, generated_cap_routers_js_1.createCapRouter_deviceAdminLink)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'device-admin-link'), remoteCapProxy),
189
- deviceAdoption: (0, generated_cap_routers_js_1.createCapRouter_deviceAdoption)((_ctx) => reg?.getSingleton('device-adoption') ??
190
- null, remoteCapProxy),
189
+ deviceAdoption: (0, generated_cap_routers_js_1.createCapRouter_deviceAdoption)((_ctx, addonId) => {
190
+ if (!reg)
191
+ return null;
192
+ if (addonId !== undefined) {
193
+ return reg.getProviderByAddonId('device-adoption', addonId);
194
+ }
195
+ const entries = reg.getCollectionEntries('device-adoption');
196
+ return entries[0]?.[1] ?? null;
197
+ }, remoteCapProxy),
191
198
  deviceCredentials: (0, generated_cap_routers_js_1.createCapRouter_deviceCredentials)((_ctx) => reg?.getSingleton('device-credentials') ?? null, remoteCapProxy),
192
199
  deviceDiscovery: (0, generated_cap_routers_js_1.createCapRouter_deviceDiscovery)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'device-discovery'), remoteCapProxy),
193
200
  deviceExport: (0, generated_cap_routers_js_1.createCapRouter_deviceExport)((_ctx, addonId) => {
@@ -2612,8 +2612,8 @@ function createCapRouter_deviceAdoption(getProvider, createRemoteProxy) {
2612
2612
  .input(types_2.DEVICE_STATUS_METHOD.getStatus.input.loose())
2613
2613
  .output(types_2.DEVICE_STATUS_METHOD.getStatus.output)
2614
2614
  .query(async ({ input, ctx }) => {
2615
- const { nodeId, ...methodInput } = input;
2616
- const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx), createRemoteProxy);
2615
+ const { nodeId, addonId, ...methodInput } = input;
2616
+ const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
2617
2617
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2618
2618
  return p.getStatus(methodInput);
2619
2619
  }),
@@ -2621,8 +2621,8 @@ function createCapRouter_deviceAdoption(getProvider, createRemoteProxy) {
2621
2621
  .input(types_45.deviceAdoptionCapability.methods.listCandidateFilters.input.loose())
2622
2622
  .output(types_45.deviceAdoptionCapability.methods.listCandidateFilters.output)
2623
2623
  .query(async ({ input, ctx }) => {
2624
- const { nodeId, ...methodInput } = input;
2625
- const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx), createRemoteProxy);
2624
+ const { nodeId, addonId, ...methodInput } = input;
2625
+ const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
2626
2626
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2627
2627
  return p.listCandidateFilters(methodInput);
2628
2628
  }),
@@ -2630,8 +2630,8 @@ function createCapRouter_deviceAdoption(getProvider, createRemoteProxy) {
2630
2630
  .input(types_45.deviceAdoptionCapability.methods.listCandidates.input.loose())
2631
2631
  .output(types_45.deviceAdoptionCapability.methods.listCandidates.output)
2632
2632
  .query(async ({ input, ctx }) => {
2633
- const { nodeId, ...methodInput } = input;
2634
- const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx), createRemoteProxy);
2633
+ const { nodeId, addonId, ...methodInput } = input;
2634
+ const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
2635
2635
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2636
2636
  return p.listCandidates(methodInput);
2637
2637
  }),
@@ -2639,8 +2639,8 @@ function createCapRouter_deviceAdoption(getProvider, createRemoteProxy) {
2639
2639
  .input(types_45.deviceAdoptionCapability.methods.getCandidate.input.loose())
2640
2640
  .output(types_45.deviceAdoptionCapability.methods.getCandidate.output)
2641
2641
  .query(async ({ input, ctx }) => {
2642
- const { nodeId, ...methodInput } = input;
2643
- const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx), createRemoteProxy);
2642
+ const { nodeId, addonId, ...methodInput } = input;
2643
+ const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
2644
2644
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2645
2645
  return p.getCandidate(methodInput);
2646
2646
  }),
@@ -2648,8 +2648,8 @@ function createCapRouter_deviceAdoption(getProvider, createRemoteProxy) {
2648
2648
  .input(types_45.deviceAdoptionCapability.methods.refresh.input.loose())
2649
2649
  .output(types_45.deviceAdoptionCapability.methods.refresh.output)
2650
2650
  .mutation(async ({ input, ctx }) => {
2651
- const { nodeId, ...methodInput } = input;
2652
- const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx), createRemoteProxy);
2651
+ const { nodeId, addonId, ...methodInput } = input;
2652
+ const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
2653
2653
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2654
2654
  return p.refresh(methodInput);
2655
2655
  }),
@@ -2657,8 +2657,8 @@ function createCapRouter_deviceAdoption(getProvider, createRemoteProxy) {
2657
2657
  .input(types_45.deviceAdoptionCapability.methods.adopt.input.loose())
2658
2658
  .output(types_45.deviceAdoptionCapability.methods.adopt.output)
2659
2659
  .mutation(async ({ input, ctx }) => {
2660
- const { nodeId, ...methodInput } = input;
2661
- const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx), createRemoteProxy);
2660
+ const { nodeId, addonId, ...methodInput } = input;
2661
+ const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
2662
2662
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2663
2663
  return p.adopt(methodInput);
2664
2664
  }),
@@ -2666,8 +2666,8 @@ function createCapRouter_deviceAdoption(getProvider, createRemoteProxy) {
2666
2666
  .input(types_45.deviceAdoptionCapability.methods.release.input.loose())
2667
2667
  .output(types_45.deviceAdoptionCapability.methods.release.output)
2668
2668
  .mutation(async ({ input, ctx }) => {
2669
- const { nodeId, ...methodInput } = input;
2670
- const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx), createRemoteProxy);
2669
+ const { nodeId, addonId, ...methodInput } = input;
2670
+ const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
2671
2671
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2672
2672
  return p.release(methodInput);
2673
2673
  }),
@@ -2675,8 +2675,8 @@ function createCapRouter_deviceAdoption(getProvider, createRemoteProxy) {
2675
2675
  .input(types_45.deviceAdoptionCapability.methods.resync.input.loose())
2676
2676
  .output(types_45.deviceAdoptionCapability.methods.resync.output)
2677
2677
  .mutation(async ({ input, ctx }) => {
2678
- const { nodeId, ...methodInput } = input;
2679
- const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx), createRemoteProxy);
2678
+ const { nodeId, addonId, ...methodInput } = input;
2679
+ const p = resolveProvider('device-adoption', nodeId, () => getProvider(ctx, addonId), createRemoteProxy);
2680
2680
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2681
2681
  return p.resync(methodInput);
2682
2682
  }),
@@ -4269,6 +4269,15 @@ function createCapRouter_dishwasher(getProvider, createRemoteProxy) {
4269
4269
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4270
4270
  return p.pause(methodInput);
4271
4271
  }),
4272
+ resume: trpc_middleware_js_1.protectedProcedure
4273
+ .input(types_56.dishwasherCapability.methods.resume.input.loose())
4274
+ .output(types_56.dishwasherCapability.methods.resume.output)
4275
+ .mutation(async ({ input, ctx }) => {
4276
+ const { nodeId, ...methodInput } = input;
4277
+ const p = resolveProvider('dishwasher', nodeId, () => getProvider(ctx), createRemoteProxy);
4278
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4279
+ return p.resume(methodInput);
4280
+ }),
4272
4281
  stop: trpc_middleware_js_1.protectedProcedure
4273
4282
  .input(types_56.dishwasherCapability.methods.stop.input.loose())
4274
4283
  .output(types_56.dishwasherCapability.methods.stop.output)
@@ -4314,6 +4323,42 @@ function createCapRouter_dishwasher(getProvider, createRemoteProxy) {
4314
4323
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4315
4324
  return p.setHygienePlus(methodInput);
4316
4325
  }),
4326
+ setExtraDry: trpc_middleware_js_1.protectedProcedure
4327
+ .input(types_56.dishwasherCapability.methods.setExtraDry.input.loose())
4328
+ .output(types_56.dishwasherCapability.methods.setExtraDry.output)
4329
+ .mutation(async ({ input, ctx }) => {
4330
+ const { nodeId, ...methodInput } = input;
4331
+ const p = resolveProvider('dishwasher', nodeId, () => getProvider(ctx), createRemoteProxy);
4332
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4333
+ return p.setExtraDry(methodInput);
4334
+ }),
4335
+ setIntensiveZone: trpc_middleware_js_1.protectedProcedure
4336
+ .input(types_56.dishwasherCapability.methods.setIntensiveZone.input.loose())
4337
+ .output(types_56.dishwasherCapability.methods.setIntensiveZone.output)
4338
+ .mutation(async ({ input, ctx }) => {
4339
+ const { nodeId, ...methodInput } = input;
4340
+ const p = resolveProvider('dishwasher', nodeId, () => getProvider(ctx), createRemoteProxy);
4341
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4342
+ return p.setIntensiveZone(methodInput);
4343
+ }),
4344
+ setVarioSpeedPlus: trpc_middleware_js_1.protectedProcedure
4345
+ .input(types_56.dishwasherCapability.methods.setVarioSpeedPlus.input.loose())
4346
+ .output(types_56.dishwasherCapability.methods.setVarioSpeedPlus.output)
4347
+ .mutation(async ({ input, ctx }) => {
4348
+ const { nodeId, ...methodInput } = input;
4349
+ const p = resolveProvider('dishwasher', nodeId, () => getProvider(ctx), createRemoteProxy);
4350
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4351
+ return p.setVarioSpeedPlus(methodInput);
4352
+ }),
4353
+ setChildLock: trpc_middleware_js_1.protectedProcedure
4354
+ .input(types_56.dishwasherCapability.methods.setChildLock.input.loose())
4355
+ .output(types_56.dishwasherCapability.methods.setChildLock.output)
4356
+ .mutation(async ({ input, ctx }) => {
4357
+ const { nodeId, ...methodInput } = input;
4358
+ const p = resolveProvider('dishwasher', nodeId, () => getProvider(ctx), createRemoteProxy);
4359
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4360
+ return p.setChildLock(methodInput);
4361
+ }),
4317
4362
  setOption: trpc_middleware_js_1.protectedProcedure
4318
4363
  .input(types_56.dishwasherCapability.methods.setOption.input.loose())
4319
4364
  .output(types_56.dishwasherCapability.methods.setOption.output)
@@ -4323,6 +4368,15 @@ function createCapRouter_dishwasher(getProvider, createRemoteProxy) {
4323
4368
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4324
4369
  return p.setOption(methodInput);
4325
4370
  }),
4371
+ getOptions: trpc_middleware_js_1.protectedProcedure
4372
+ .input(types_56.dishwasherCapability.methods.getOptions.input.loose())
4373
+ .output(types_56.dishwasherCapability.methods.getOptions.output)
4374
+ .query(async ({ input, ctx }) => {
4375
+ const { nodeId, ...methodInput } = input;
4376
+ const p = resolveProvider('dishwasher', nodeId, () => getProvider(ctx), createRemoteProxy);
4377
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4378
+ return p.getOptions(methodInput);
4379
+ }),
4326
4380
  });
4327
4381
  }
4328
4382
  function createCapRouter_doorbell(getProvider, createRemoteProxy) {
@@ -11092,6 +11146,15 @@ function createCapRouter_storageMigration(getProvider, createRemoteProxy) {
11092
11146
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
11093
11147
  return p.movers(methodInput);
11094
11148
  }),
11149
+ moverClearance: trpc_middleware_js_1.adminProcedure
11150
+ .input(types_127.storageMigrationCapability.methods.moverClearance.input.loose())
11151
+ .output(types_127.storageMigrationCapability.methods.moverClearance.output)
11152
+ .query(async ({ input, ctx }) => {
11153
+ const { nodeId, ...methodInput } = input;
11154
+ const p = resolveProvider('storage-migration', nodeId, () => getProvider(ctx), createRemoteProxy);
11155
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
11156
+ return p.moverClearance(methodInput);
11157
+ }),
11095
11158
  residue: trpc_middleware_js_1.adminProcedure
11096
11159
  .input(types_127.storageMigrationCapability.methods.residue.input.loose())
11097
11160
  .output(types_127.storageMigrationCapability.methods.residue.output)
@@ -296,11 +296,20 @@ function buildCapabilityRouters(services) {
296
296
  // (`import type`) — its value is never called; the codegen
297
297
  // `generated-cap-mounts.ts` / `generated-cap-routers.ts` remain the TYPE
298
298
  // source of record.
299
+ const integrationRegistry = services.addonRegistry.getIntegrationRegistry();
300
+ const deviceRegistry = services.addonRegistry.getDeviceRegistry();
299
301
  const dynamicCaps = (0, cap_router_runtime_js_1.buildRuntimeCapRouters)({
300
302
  capabilityRegistry: services.capabilityRegistry,
301
303
  moleculer: services.moleculer,
302
304
  serverProviders: buildServerProviders(services),
303
305
  logger: services.loggingService.createLogger('cap-router'),
306
+ resolveIntegrationAddonId: integrationRegistry
307
+ ? async (integrationId) => {
308
+ const row = await integrationRegistry.getIntegration(integrationId);
309
+ return row?.addonId ?? null;
310
+ }
311
+ : undefined,
312
+ resolveDeviceAddonId: (camDeviceId) => deviceRegistry.getAddonId(camDeviceId),
304
313
  });
305
314
  return {
306
315
  // ── Runtime-built cap routers: EVERY cap whose mount is
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.alexaLambdaPageStyleSource = alexaLambdaPageStyleSource;
4
+ /**
5
+ * The source text of the skill Lambda's `page-style.js`: the hub's standalone
6
+ * page stylesheet plus the brand mark, as constants. Kept here, beside
7
+ * `app-page-shell.ts`, so there is ONE place the look lives and a spec holds the
8
+ * Lambda's copy to it (`scripts/gen-alexa-lambda-page-style.ts` writes it).
9
+ */
10
+ const app_page_shell_js_1 = require("./app-page-shell.js");
11
+ function alexaLambdaPageStyleSource() {
12
+ return `// biome-ignore-all format: generated file
13
+ // biome-ignore-all lint: generated file
14
+ /**
15
+ * GENERATED by scripts/gen-alexa-lambda-page-style.ts from the hub's page shell
16
+ * (server/backend/src/auth/app-page-shell.ts). Do not edit: change the shell and
17
+ * regenerate. The Lambda ships plain files, so the stylesheet travels as a constant.
18
+ */
19
+ 'use strict'
20
+
21
+ const STYLE = ${JSON.stringify(app_page_shell_js_1.APP_PAGE_STYLE)}
22
+ const LOGO_DARK = ${JSON.stringify(app_page_shell_js_1.APP_PAGE_LOGOS.dark)}
23
+ const LOGO_LIGHT = ${JSON.stringify(app_page_shell_js_1.APP_PAGE_LOGOS.light)}
24
+
25
+ module.exports = { STYLE, LOGO_DARK, LOGO_LIGHT }
26
+ `;
27
+ }
@@ -64,6 +64,10 @@ const COMPONENT_CSS = [
64
64
  `.error:empty{display:none}`,
65
65
  `.btn:focus-visible,.link:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}`,
66
66
  `code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;overflow-wrap:anywhere}`,
67
+ `.field{display:flex;flex-direction:column;gap:var(--spacing-md)}`,
68
+ `.field label{font-size:12px;line-height:16px;font-weight:500;color:var(--color-foreground-muted)}`,
69
+ `input[type=url],input[type=text]{width:100%;min-height:44px;padding:10px 12px;border:1px solid var(--color-border);border-radius:var(--radius-lg);background:var(--color-background);color:var(--color-foreground);font:inherit}`,
70
+ `input[type=url]:focus-visible,input[type=text]:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}`,
67
71
  `form{display:contents}`,
68
72
  `.actions{display:flex;flex-direction:column;gap:var(--spacing-md)}`,
69
73
  ].join('');
@@ -47,6 +47,7 @@ const path = __importStar(require("node:path"));
47
47
  const system_1 = require("@camstack/system");
48
48
  const types_1 = require("@camstack/types");
49
49
  const client_1 = require("@trpc/client");
50
+ const hub_addon_api_js_1 = require("./hub-addon-api.js");
50
51
  const addon_call_gateway_js_1 = require("./addon-call-gateway.js");
51
52
  const addon_row_manifest_1 = require("./addon-row-manifest");
52
53
  const addon_settings_provider_js_1 = require("./addon-settings-provider.js");
@@ -336,29 +337,49 @@ class AddonRegistryService {
336
337
  */
337
338
  getBrokerApi() {
338
339
  if (!this.brokerApi) {
339
- const reg = this.capabilityRegistry;
340
- const resolver = {
341
- getByName: (capName) => reg.getSingleton(capName) ?? reg.getAllProviders(capName)[0] ?? null,
342
- };
343
- const client = (0, client_1.createTRPCClient)({
344
- links: [
345
- (0, system_1.localProviderLink)(resolver),
346
- // Device-scoped caps go through the hub's CapRouteResolver, which is
347
- // the only authority that can route `(cap, deviceId)` into a FORKED
348
- // child — and forked children are broker-less, so the Moleculer
349
- // lookup below cannot see them at all. Without this an in-process
350
- // addon asking `ptz.getOptions({deviceId})` got
351
- // `Service 'ptz.ptz.getOptions' is not found`, which is how the
352
- // autotrack builtin came up switched on and unable to move a camera
353
- // (2026-09-18).
354
- (0, system_1.deviceCapResolverLink)((capName, method, input) => this.moleculer.callCapabilityOnNode(this.moleculer.broker.nodeID, capName, method, input), isDeviceScopedCapName),
355
- (0, system_1.brokerTransportLink)(this.moleculer.broker),
356
- ],
357
- });
358
- this.brokerApi = client;
340
+ this.brokerApi = this.buildBrokerApi(undefined);
359
341
  }
360
342
  return this.brokerApi;
361
343
  }
344
+ /**
345
+ * The `ctx.api` of one in-process addon. Same chain as {@link getBrokerApi},
346
+ * but its `localProviderLink` knows WHO is calling, so a caller-stamped cap
347
+ * (`device-history`, keychain, records) can identify the addon. A shared
348
+ * client cannot: `metrics-native` was refused `declareSeries` ("the caller
349
+ * addon could not be identified") on every sample.
350
+ */
351
+ getAddonApi(addonId) {
352
+ const cached = this.addonApis.get(addonId);
353
+ if (cached)
354
+ return cached;
355
+ const api = this.buildBrokerApi(addonId);
356
+ this.addonApis.set(addonId, api);
357
+ return api;
358
+ }
359
+ addonApis = new Map();
360
+ buildBrokerApi(callerAddonId) {
361
+ const reg = this.capabilityRegistry;
362
+ const resolver = {
363
+ getByName: (capName) => reg.getSingleton(capName) ?? reg.getAllProviders(capName)[0] ?? null,
364
+ };
365
+ // Device-scoped caps go through the hub's CapRouteResolver, which is
366
+ // the only authority that can route `(cap, deviceId)` into a FORKED
367
+ // child — and forked children are broker-less, so the Moleculer
368
+ // lookup below cannot see them at all. Without this an in-process
369
+ // addon asking `ptz.getOptions({deviceId})` got
370
+ // `Service 'ptz.ptz.getOptions' is not found`, which is how the
371
+ // autotrack builtin came up switched on and unable to move a camera
372
+ // (2026-09-18).
373
+ const links = [
374
+ (0, system_1.deviceCapResolverLink)((capName, method, input) => this.moleculer.callCapabilityOnNode(this.moleculer.broker.nodeID, capName, method, input), isDeviceScopedCapName),
375
+ (0, system_1.brokerTransportLink)(this.moleculer.broker),
376
+ ];
377
+ if (callerAddonId !== undefined) {
378
+ return (0, hub_addon_api_js_1.createHubAddonApi)({ resolver, callerAddonId, links });
379
+ }
380
+ const client = (0, client_1.createTRPCClient)({ links: [(0, system_1.localProviderLink)(resolver), ...links] });
381
+ return client;
382
+ }
362
383
  // Common settings resolver (3-level: defaults → global → per-device).
363
384
  // Lazily constructed on first call to `getSettingsResolver()` so tests
364
385
  // that don't exercise the settings API can instantiate the service
@@ -3013,7 +3034,7 @@ class AddonRegistryService {
3013
3034
  dataPlane: dataPlaneFacility.dataPlane,
3014
3035
  peerBytes: peerBytesFacility.peerBytes,
3015
3036
  get api() {
3016
- return registry.getBrokerApi();
3037
+ return registry.getAddonApi(addonId);
3017
3038
  },
3018
3039
  integrationRegistry: this.integrationRegistry ?? undefined,
3019
3040
  // Live capability-collection accessor used by addons like stream-broker
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * The `ctx.api` of an in-process hub addon.
4
+ *
5
+ * Identity is the whole point: a caller-stamped capability (`device-history`,
6
+ * keychain, records) learns WHO is calling only from the `callerAddonId` its
7
+ * `localProviderLink` stamps, so each addon needs its own client — a shared one
8
+ * cannot say whose call it is.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.createHubAddonApi = createHubAddonApi;
12
+ const client_1 = require("@trpc/client");
13
+ const system_1 = require("@camstack/system");
14
+ function createHubAddonApi(options) {
15
+ // The observer options carry the stamp only. Cap usage stays recorded on the
16
+ // callee side (`LocalChildRegistry`), so in-process calls are not counted
17
+ // here and the hub's usage graph keeps its meaning.
18
+ const observerOpts = {
19
+ callerAddonId: options.callerAddonId,
20
+ providerAddonIdForCap: () => null,
21
+ observer: () => undefined,
22
+ };
23
+ const client = (0, client_1.createTRPCClient)({
24
+ links: [(0, system_1.localProviderLink)(options.resolver, observerOpts), ...options.links],
25
+ });
26
+ return client;
27
+ }
@@ -267,7 +267,7 @@ async function bootManual(opts) {
267
267
  throw new Error('the process service did not answer');
268
268
  return rows;
269
269
  },
270
- // The verdict reaches the alert centre (one alert + the direct critical push).
270
+ // The verdict reaches the alert centre (one critical alert).
271
271
  onResult: (check) => {
272
272
  eventBusService.emit({
273
273
  id: (0, node_crypto_1.randomUUID)(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.601",
3
+ "version": "1.2.603",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -38,19 +38,19 @@
38
38
  ]
39
39
  },
40
40
  "dependencies": {
41
- "@camstack/addon-admin-ui": "1.2.438",
42
- "@camstack/addon-agent-ui": "1.2.244",
43
- "@camstack/addon-auth": "1.2.149",
44
- "@camstack/addon-decoder-nodeav": "1.2.123",
45
- "@camstack/addon-notifiers": "1.2.601",
46
- "@camstack/addon-pipeline": "1.2.601",
47
- "@camstack/addon-pipeline-orchestrator": "1.2.602",
48
- "@camstack/addon-post-analysis": "1.2.603",
49
- "@camstack/develop": "0.1.13",
50
- "@camstack/sdk": "1.2.133",
51
- "@camstack/system": "1.2.452",
52
- "@camstack/types": "1.2.383",
53
- "@camstack/ui-library": "1.2.300",
41
+ "@camstack/addon-admin-ui": "1.2.440",
42
+ "@camstack/addon-agent-ui": "1.2.246",
43
+ "@camstack/addon-auth": "1.2.151",
44
+ "@camstack/addon-decoder-nodeav": "1.2.125",
45
+ "@camstack/addon-notifiers": "1.2.603",
46
+ "@camstack/addon-pipeline": "1.2.603",
47
+ "@camstack/addon-pipeline-orchestrator": "1.2.604",
48
+ "@camstack/addon-post-analysis": "1.2.608",
49
+ "@camstack/develop": "0.1.15",
50
+ "@camstack/sdk": "1.2.135",
51
+ "@camstack/system": "1.2.454",
52
+ "@camstack/types": "1.2.385",
53
+ "@camstack/ui-library": "1.2.302",
54
54
  "@fastify/compress": "^9.0.0",
55
55
  "@fastify/cookie": "^11.0.2",
56
56
  "@fastify/cors": "^11.2.0",