@camstack/server 1.2.683 → 1.2.685

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.685] - 2026-10-29
11
+
12
+ ### Added
13
+
14
+ - Generated routers for the new ir-transmitter and remote capabilities
15
+ - The remote and template capabilities are routed, and a bound remote command is honoured on cross-node calls
16
+
17
+ ## [1.2.684] - 2026-10-29
18
+
19
+ ### Changed
20
+
21
+ - A form posted through an addon's data plane (a vendor sign-in page) reaches the addon unchanged instead of re-encoded as JSON
22
+
10
23
  ## [1.2.683] - 2026-10-28
11
24
 
12
25
  ### Changed
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseFormBody = parseFormBody;
4
+ exports.registerFormBodyParser = registerFormBodyParser;
5
+ exports.dataPlaneReplayBody = dataPlaneReplayBody;
6
+ const FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded';
7
+ const rawFormBodies = new WeakMap();
8
+ /** Parse a urlencoded body string into a plain object (duplicate keys collapse). */
9
+ function parseFormBody(raw) {
10
+ const result = {};
11
+ for (const [key, value] of new URLSearchParams(raw).entries()) {
12
+ result[key] = value;
13
+ }
14
+ return result;
15
+ }
16
+ /** Register the urlencoded parser: the parsed object for hub routes, the raw bytes on the side. */
17
+ function registerFormBodyParser(fastify) {
18
+ fastify.addContentTypeParser(FORM_CONTENT_TYPE, { parseAs: 'buffer' }, (req, body, done) => {
19
+ try {
20
+ const bytes = Buffer.isBuffer(body) ? body : Buffer.from(body);
21
+ rawFormBodies.set(req.raw, bytes);
22
+ done(null, parseFormBody(bytes.toString('utf8')));
23
+ }
24
+ catch (err) {
25
+ done(err instanceof Error ? err : new Error(String(err)), undefined);
26
+ }
27
+ });
28
+ }
29
+ /**
30
+ * The bytes a data-plane upstream receives. Never a re-serialised object: a
31
+ * parsed body with no recorded raw bytes is a bug path, logged (no values) and
32
+ * refused with `null`.
33
+ */
34
+ function dataPlaneReplayBody(raw, body, logger) {
35
+ if (body === undefined)
36
+ return undefined;
37
+ if (Buffer.isBuffer(body))
38
+ return body;
39
+ if (typeof body === 'string')
40
+ return Buffer.from(body);
41
+ const recorded = rawFormBodies.get(raw);
42
+ if (recorded !== undefined)
43
+ return recorded;
44
+ // A JSON body is re-serialised from its parsed object (Fastify's JSON parser
45
+ // keeps no bytes); every other parsed object has lost its original encoding.
46
+ const contentType = String(raw.headers['content-type'] ?? '');
47
+ if (/^application\/(?:[\w.+-]+\+)?json\b/i.test(contentType)) {
48
+ return Buffer.from(JSON.stringify(body));
49
+ }
50
+ logger.error('data-plane request body was parsed but its raw bytes were not recorded', {
51
+ tags: { contentType: String(raw.headers['content-type'] ?? ''), url: raw.url ?? '' },
52
+ });
53
+ return null;
54
+ }
@@ -9,6 +9,7 @@ exports.registerOauth2Routes = registerOauth2Routes;
9
9
  const types_1 = require("@camstack/types");
10
10
  const session_cookie_js_1 = require("../../auth/session-cookie.js");
11
11
  const app_page_strings_js_1 = require("../../auth/app-page-strings.js");
12
+ const form_body_js_1 = require("../data-plane/form-body.js");
12
13
  const consent_page_js_1 = require("./consent-page.js");
13
14
  const private_host_js_1 = require("./private-host.js");
14
15
  /**
@@ -328,27 +329,10 @@ async function readIntegrationRegistry(registry) {
328
329
  .filter((addonId) => !registry.hasProvider('oauth-integration', addonId));
329
330
  return { descriptors, pendingAddons };
330
331
  }
331
- /** Parse an application/x-www-form-urlencoded body string into a plain object. */
332
- function parseFormBody(raw) {
333
- const params = new URLSearchParams(raw);
334
- const result = {};
335
- for (const [key, value] of params.entries()) {
336
- result[key] = value;
337
- }
338
- return result;
339
- }
340
332
  function registerOauth2Routes(fastify, deps) {
341
- // Register a content-type parser for application/x-www-form-urlencoded so that
342
- // consent POST and token POST can read form bodies. @fastify/formbody is not in
343
- // the project's dependencies; we use URLSearchParams (Node built-in) instead.
344
- fastify.addContentTypeParser('application/x-www-form-urlencoded', { parseAs: 'string' }, (_req, body, done) => {
345
- try {
346
- done(null, parseFormBody(body));
347
- }
348
- catch (err) {
349
- done(err, undefined);
350
- }
351
- });
333
+ // Form bodies (consent POST, token POST): parsed object for these routes, raw
334
+ // bytes recorded for the data plane (api/data-plane/form-body.ts).
335
+ (0, form_body_js_1.registerFormBodyParser)(fastify);
352
336
  // ─── GET /api/oauth2/authorize ────────────────────────────────────────────
353
337
  // Mounted under /api/* so it is naturally excluded from the SPA catch-all,
354
338
  // the PWA service-worker navigate fallback, and the Vite dev proxy — no
@@ -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: 185 Skipped (legacy): 3
5
+ // Mounted: 187 Skipped (legacy): 3
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.LEGACY_SHAPE_SKIP = void 0;
8
8
  exports.mountAllCaps = mountAllCaps;
@@ -516,6 +516,7 @@ function mountAllCaps(services) {
516
516
  recordingOnboard: (0, generated_cap_routers_js_1.createCapRouter_recordingOnboard)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'recording-onboard'), remoteCapProxy),
517
517
  recordingSignal: (0, generated_cap_routers_js_1.createCapRouter_recordingSignal)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'recording-signal'), remoteCapProxy),
518
518
  records: (0, generated_cap_routers_js_1.createCapRouter_records)((_ctx) => reg?.getSingleton('records') ?? null, remoteCapProxy),
519
+ remote: (0, generated_cap_routers_js_1.createCapRouter_remote)((_ctx) => reg?.getSingleton('remote') ?? null, remoteCapProxy),
519
520
  runningState: (0, generated_cap_routers_js_1.createCapRouter_runningState)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'running-state'), remoteCapProxy),
520
521
  sceneMonitor: (0, generated_cap_routers_js_1.createCapRouter_sceneMonitor)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'scene-monitor'), remoteCapProxy),
521
522
  scriptRunner: (0, generated_cap_routers_js_1.createCapRouter_scriptRunner)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'script-runner'), remoteCapProxy),
@@ -587,6 +588,22 @@ function mountAllCaps(services) {
587
588
  system: (0, generated_cap_routers_js_1.createCapRouter_system)((_ctx) => reg?.getSingleton('system') ?? null, remoteCapProxy),
588
589
  tamper: (0, generated_cap_routers_js_1.createCapRouter_tamper)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'tamper'), remoteCapProxy),
589
590
  temperatureSensor: (0, generated_cap_routers_js_1.createCapRouter_temperatureSensor)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'temperature-sensor'), remoteCapProxy),
591
+ template: (0, generated_cap_routers_js_1.createCapRouter_template)((_ctx, addonId) => {
592
+ if (!reg)
593
+ return null;
594
+ if (addonId !== undefined) {
595
+ return reg.getProviderByAddonId('template', addonId);
596
+ }
597
+ const entries = reg.getCollectionEntries('template');
598
+ if (entries.length === 0)
599
+ return null;
600
+ const providers = entries.map(([, p]) => p);
601
+ const first = providers[0];
602
+ return {
603
+ ...first,
604
+ list: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'list'),
605
+ };
606
+ }, remoteCapProxy),
590
607
  terminalSession: (0, generated_cap_routers_js_1.createCapRouter_terminalSession)((_ctx) => reg?.getSingleton('terminal-session') ??
591
608
  null, remoteCapProxy),
592
609
  toast: (0, generated_cap_routers_js_1.createCapRouter_toast)((_ctx) => reg?.getSingleton('toast') ?? null, remoteCapProxy),