@camstack/server 1.2.368 → 1.2.370

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.
@@ -22,6 +22,8 @@
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.COLLECTION_ARRAY_METHODS = void 0;
24
24
  exports.aggregateCollectionCall = aggregateCollectionCall;
25
+ exports.devicePinOf = devicePinOf;
26
+ exports.pinnedDeviceCollectionCall = pinnedDeviceCollectionCall;
25
27
  exports.addonPinOf = addonPinOf;
26
28
  exports.pinnedCollectionCall = pinnedCollectionCall;
27
29
  exports.createCapRouterPrimitives = createCapRouterPrimitives;
@@ -142,6 +144,84 @@ async function aggregateCollectionCall(reg, capName, method, args) {
142
144
  const out = await fn(args);
143
145
  return Array.isArray(out) ? out : null;
144
146
  }
147
+ /**
148
+ * A DEVICE collection cap — several providers PER DEVICE, mounted per device.
149
+ *
150
+ * `videoclips` is the one: `resolveCapMount` checks `scope: 'device' + kind:
151
+ * 'wrapper'` BEFORE `mode: 'collection'` and answers `device-scoped`, so
152
+ * {@link isGlobalCollection} is false for it and the `{ addonId }` pin above
153
+ * does not apply. Its pin is `provider`, read by
154
+ * `device-collection-dispatch.ts` on the tRPC path.
155
+ */
156
+ function isDeviceCollection(reg, capName) {
157
+ const def = reg.getDefinition(capName);
158
+ if (def === undefined)
159
+ return false;
160
+ return def.mode === 'collection' && (0, types_1.resolveCapMount)(def).kind === 'device-scoped';
161
+ }
162
+ /**
163
+ * The `{ provider }` a DEVICE-collection call pins to, or `null`.
164
+ *
165
+ * The device twin of {@link addonPinOf}, and it exists because the same defect
166
+ * was found a THIRD time. `videoclips` has three providers on the operator's
167
+ * hub; the clip byte read named `provider-reolink` every time and was answered
168
+ * by the recorder, then — after a redeploy in a different order — by
169
+ * export-hap ("was not minted by the HomeKit source"), which is what a
170
+ * first-wins resolution looks like from outside.
171
+ *
172
+ * Two earlier fixes were each correct and each missed this path:
173
+ * - D592 fixed the tRPC MOUNT, which a forked addon never reaches.
174
+ * - D593 fixed the Moleculer transport link — but a hub-local runner is a
175
+ * broker-less UDS child whose link chain is `[localProviderLink,
176
+ * ipcParentLink]` with no `brokerTransportLink` at all
177
+ * (`addon-context-factory.ts`), so it never ran there either.
178
+ *
179
+ * The call lands on the parent's `onUnownedCall`, where routing discards the
180
+ * selector by construction — `resolveCapRoute` takes only `nodeId`/`deviceId`
181
+ * — and `cap-child-index.ts` answers "first child wins".
182
+ *
183
+ * Unlike the global pin this DOES apply to array methods: naming the provider
184
+ * is required on `listClips` too (D554), and the narrowing happens inside
185
+ * `requireDeviceCollection`, which then decides fan-out vs scalar itself.
186
+ */
187
+ function devicePinOf(reg, capName, args) {
188
+ if (!reg)
189
+ return null;
190
+ if (!isDeviceCollection(reg, capName))
191
+ return null;
192
+ if (args === null || typeof args !== 'object' || Array.isArray(args))
193
+ return null;
194
+ const rec = args;
195
+ if (typeof rec['deviceId'] !== 'number')
196
+ return null;
197
+ const pin = rec[types_1.PROVIDER_PIN_FIELD];
198
+ return typeof pin === 'string' && pin.length > 0 ? pin : null;
199
+ }
200
+ /**
201
+ * Run a pinned DEVICE-collection call through the SAME dispatcher the tRPC
202
+ * mount uses, so the two paths cannot answer differently.
203
+ *
204
+ * Nothing is re-implemented here on purpose: `requireDeviceCollection` already
205
+ * narrows to the pinned provider, refuses a pin the device is not bound to BY
206
+ * NAME (D552's rule), and picks fan-out or scalar afterwards. Re-deriving any
207
+ * of that here would be the fourth authority on one question.
208
+ *
209
+ * Boxed like {@link pinnedCollectionCall}: a device-collection method may
210
+ * legitimately answer `null`, which a bare return could not tell from "not
211
+ * handled".
212
+ */
213
+ async function pinnedDeviceCollectionCall(reg, capName, method, args) {
214
+ if (devicePinOf(reg, capName, args) === null)
215
+ return null;
216
+ const dispatcher = (0, device_collection_dispatch_js_1.requireDeviceCollection)(reg, capName);
217
+ if (dispatcher === null)
218
+ return null;
219
+ const fn = dispatcher[method];
220
+ if (typeof fn !== 'function')
221
+ return null;
222
+ const call = fn;
223
+ return { value: await call(args) };
224
+ }
145
225
  /**
146
226
  * The `{ addonId }` a call PINS to, or `null` when the call is not an
147
227
  * addon-pinned collection call and must route normally.
@@ -354,7 +354,14 @@ class MoleculerService {
354
354
  // registered provider answers for a resource it does not own: HA's
355
355
  // export read Homematic's `null` for `ha_001` and never opened a link
356
356
  // (2026-08-09).
357
- callPinnedCollection: (capName, method, args) => (0, cap_router_runtime_js_1.pinnedCollectionCall)(this.capabilityService.getRegistry(), capName, method, args),
357
+ //
358
+ // The DEVICE twin runs first: `videoclips` mounts `device-scoped`
359
+ // (scope+wrapper is checked before `mode`), so the global pin above
360
+ // does not see it, and its pin is `provider`. Without this a forked
361
+ // addon's clip byte read named `provider-reolink` and was answered by
362
+ // whichever child registered first — the recorder, then export-hap
363
+ // after a redeploy in a different order (2026-09-22, D594).
364
+ callPinnedCollection: async (capName, method, args) => (await (0, cap_router_runtime_js_1.pinnedDeviceCollectionCall)(this.capabilityService.getRegistry(), capName, method, args)) ?? (0, cap_router_runtime_js_1.pinnedCollectionCall)(this.capabilityService.getRegistry(), capName, method, args),
358
365
  // Hub-core `$`-service discriminant for the general
359
366
  // provider-not-yet-registered recovery. A resolver miss on a NON-core
360
367
  // cap (any addon-provided system/singleton cap, e.g.
@@ -409,7 +416,15 @@ class MoleculerService {
409
416
  // with it — the bypass and the routing must never disagree about what
410
417
  // counts as a pin, or the call is bounced out of the sibling path and
411
418
  // then routed by first-provider anyway.
412
- isAddonPinnedCall: (capName, method, args) => (0, cap_router_runtime_js_1.addonPinOf)(this.capabilityService.getRegistry(), capName, method, args) !== null,
419
+ //
420
+ // BOTH pins, because this predicate and `callPinnedCollection` must
421
+ // agree about what counts as one. A pin the bypass does not recognise
422
+ // is short-circuited to the first sibling and never reaches the
423
+ // routing that would honour it — "honoured only there is honoured
424
+ // never", which is how the first attempt at the `addonId` fix failed
425
+ // live, and how the `provider` pin failed twice more (D592, D593).
426
+ isAddonPinnedCall: (capName, method, args) => (0, cap_router_runtime_js_1.addonPinOf)(this.capabilityService.getRegistry(), capName, method, args) !== null ||
427
+ (0, cap_router_runtime_js_1.devicePinOf)(this.capabilityService.getRegistry(), capName, args) !== null,
413
428
  // Bound every UDS cap call by the cap's own declared timeout, falling
414
429
  // back to the channel default. Same resolver the REMOTE plane uses
415
430
  // below, so a method that survives 16 minutes across nodes survives it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.368",
3
+ "version": "1.2.370",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -35,7 +35,7 @@
35
35
  ]
36
36
  },
37
37
  "dependencies": {
38
- "@camstack/addon-admin-ui": "1.2.251",
38
+ "@camstack/addon-admin-ui": "1.2.252",
39
39
  "@camstack/addon-agent-ui": "1.2.137",
40
40
  "@camstack/addon-auth": "1.2.123",
41
41
  "@camstack/addon-decoder-nodeav": "1.2.115",
@@ -44,8 +44,8 @@
44
44
  "@camstack/addon-pipeline-orchestrator": "1.2.240",
45
45
  "@camstack/addon-post-analysis": "1.2.273",
46
46
  "@camstack/sdk": "1.2.118",
47
- "@camstack/system": "1.2.283",
48
- "@camstack/types": "1.2.240",
47
+ "@camstack/system": "1.2.285",
48
+ "@camstack/types": "1.2.241",
49
49
  "@camstack/ui-library": "1.2.195",
50
50
  "@fastify/compress": "^9.0.0",
51
51
  "@fastify/cookie": "^11.0.2",