@matter/node 0.13.0 → 0.13.1-alpha.0-20250501-80c86b03e

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.
Files changed (60) hide show
  1. package/dist/cjs/behavior/context/server/OnlineContext.d.ts.map +1 -1
  2. package/dist/cjs/behavior/context/server/OnlineContext.js +6 -3
  3. package/dist/cjs/behavior/context/server/OnlineContext.js.map +1 -1
  4. package/dist/cjs/behavior/system/controller/ControllerBehavior.js +2 -1
  5. package/dist/cjs/behavior/system/controller/ControllerBehavior.js.map +1 -1
  6. package/dist/cjs/behavior/system/network/ServerNetworkRuntime.d.ts.map +1 -1
  7. package/dist/cjs/behavior/system/network/ServerNetworkRuntime.js +7 -7
  8. package/dist/cjs/behavior/system/network/ServerNetworkRuntime.js.map +1 -1
  9. package/dist/cjs/behavior/system/subscription/SubscriptionBehavior.d.ts +2 -3
  10. package/dist/cjs/behavior/system/subscription/SubscriptionBehavior.d.ts.map +1 -1
  11. package/dist/cjs/behavior/system/subscription/SubscriptionBehavior.js.map +1 -1
  12. package/dist/cjs/node/server/InteractionServer.d.ts +84 -0
  13. package/dist/cjs/node/server/InteractionServer.d.ts.map +1 -0
  14. package/dist/cjs/node/server/InteractionServer.js +1326 -0
  15. package/dist/cjs/node/server/InteractionServer.js.map +6 -0
  16. package/dist/cjs/node/server/ProtocolService.d.ts.map +1 -1
  17. package/dist/cjs/node/server/ProtocolService.js +9 -8
  18. package/dist/cjs/node/server/ProtocolService.js.map +2 -2
  19. package/dist/cjs/node/server/index.d.ts +1 -1
  20. package/dist/cjs/node/server/index.d.ts.map +1 -1
  21. package/dist/cjs/node/server/index.js +1 -1
  22. package/dist/cjs/node/server/index.js.map +1 -1
  23. package/dist/esm/behavior/context/server/OnlineContext.d.ts.map +1 -1
  24. package/dist/esm/behavior/context/server/OnlineContext.js +6 -3
  25. package/dist/esm/behavior/context/server/OnlineContext.js.map +1 -1
  26. package/dist/esm/behavior/system/controller/ControllerBehavior.js +1 -1
  27. package/dist/esm/behavior/system/controller/ControllerBehavior.js.map +1 -1
  28. package/dist/esm/behavior/system/network/ServerNetworkRuntime.d.ts.map +1 -1
  29. package/dist/esm/behavior/system/network/ServerNetworkRuntime.js +2 -3
  30. package/dist/esm/behavior/system/network/ServerNetworkRuntime.js.map +1 -1
  31. package/dist/esm/behavior/system/subscription/SubscriptionBehavior.d.ts +2 -3
  32. package/dist/esm/behavior/system/subscription/SubscriptionBehavior.d.ts.map +1 -1
  33. package/dist/esm/behavior/system/subscription/SubscriptionBehavior.js.map +1 -1
  34. package/dist/esm/node/server/InteractionServer.d.ts +84 -0
  35. package/dist/esm/node/server/InteractionServer.d.ts.map +1 -0
  36. package/dist/esm/node/server/InteractionServer.js +1348 -0
  37. package/dist/esm/node/server/InteractionServer.js.map +6 -0
  38. package/dist/esm/node/server/ProtocolService.d.ts.map +1 -1
  39. package/dist/esm/node/server/ProtocolService.js +9 -8
  40. package/dist/esm/node/server/ProtocolService.js.map +1 -1
  41. package/dist/esm/node/server/index.d.ts +1 -1
  42. package/dist/esm/node/server/index.d.ts.map +1 -1
  43. package/dist/esm/node/server/index.js +1 -1
  44. package/package.json +7 -7
  45. package/src/behavior/context/server/OnlineContext.ts +9 -4
  46. package/src/behavior/system/controller/ControllerBehavior.ts +1 -1
  47. package/src/behavior/system/network/ServerNetworkRuntime.ts +4 -7
  48. package/src/behavior/system/subscription/SubscriptionBehavior.ts +2 -3
  49. package/src/node/server/InteractionServer.ts +1757 -0
  50. package/src/node/server/ProtocolService.ts +10 -8
  51. package/src/node/server/index.ts +1 -1
  52. package/dist/cjs/node/server/TransactionalInteractionServer.d.ts +0 -57
  53. package/dist/cjs/node/server/TransactionalInteractionServer.d.ts.map +0 -1
  54. package/dist/cjs/node/server/TransactionalInteractionServer.js +0 -334
  55. package/dist/cjs/node/server/TransactionalInteractionServer.js.map +0 -6
  56. package/dist/esm/node/server/TransactionalInteractionServer.d.ts +0 -57
  57. package/dist/esm/node/server/TransactionalInteractionServer.d.ts.map +0 -1
  58. package/dist/esm/node/server/TransactionalInteractionServer.js +0 -322
  59. package/dist/esm/node/server/TransactionalInteractionServer.js.map +0 -6
  60. package/src/node/server/TransactionalInteractionServer.ts +0 -413
@@ -0,0 +1,1348 @@
1
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
2
+ var __typeError = (msg) => {
3
+ throw TypeError(msg);
4
+ };
5
+ var __using = (stack, value, async) => {
6
+ if (value != null) {
7
+ if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
8
+ var dispose, inner;
9
+ if (async) dispose = value[__knownSymbol("asyncDispose")];
10
+ if (dispose === void 0) {
11
+ dispose = value[__knownSymbol("dispose")];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") __typeError("Object not disposable");
15
+ if (inner) dispose = function() {
16
+ try {
17
+ inner.call(this);
18
+ } catch (e) {
19
+ return Promise.reject(e);
20
+ }
21
+ };
22
+ stack.push([async, dispose, value]);
23
+ } else if (async) {
24
+ stack.push([async]);
25
+ }
26
+ return value;
27
+ };
28
+ var __callDispose = (stack, error, hasError) => {
29
+ var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
30
+ return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
31
+ };
32
+ var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
33
+ var next = (it) => {
34
+ while (it = stack.pop()) {
35
+ try {
36
+ var result = it[1] && it[1].call(it[2]);
37
+ if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
38
+ } catch (e) {
39
+ fail(e);
40
+ }
41
+ }
42
+ if (hasError) throw error;
43
+ };
44
+ return next();
45
+ };
46
+ /**
47
+ * @license
48
+ * Copyright 2022-2025 Matter.js Authors
49
+ * SPDX-License-Identifier: Apache-2.0
50
+ */
51
+ import { ActionTracer } from "#behavior/context/ActionTracer.js";
52
+ import { NodeActivity } from "#behavior/context/NodeActivity.js";
53
+ import { OfflineContext } from "#behavior/context/server/OfflineContext.js";
54
+ import { OnlineContext } from "#behavior/context/server/OnlineContext.js";
55
+ import { AccessControlServer } from "#behaviors/access-control";
56
+ import { AccessControlCluster } from "#clusters/access-control";
57
+ import { EndpointLifecycle } from "#endpoint/properties/EndpointLifecycle.js";
58
+ import { EndpointServer } from "#endpoint/server/EndpointServer.js";
59
+ import {
60
+ Crypto,
61
+ Diagnostic,
62
+ InternalError,
63
+ Logger,
64
+ MatterError,
65
+ MaybePromise,
66
+ Observable
67
+ } from "#general";
68
+ import { ClusterModel, CommandModel, GLOBAL_IDS, MatterModel, Specification } from "#model";
69
+ import {
70
+ AccessControl,
71
+ AccessDeniedError,
72
+ assertSecureSession,
73
+ attributePathToId,
74
+ AttributeServer,
75
+ clusterPathToId,
76
+ commandPathToId,
77
+ decodeAttributeValueWithSchema,
78
+ decodeListAttributeValueWithSchema,
79
+ ExchangeManager,
80
+ expandPathsInAttributeData,
81
+ FabricScopedAttributeServer,
82
+ InteractionEndpointStructure,
83
+ InteractionServerMessenger,
84
+ MessageType,
85
+ ServerSubscription,
86
+ ServerSubscriptionConfig,
87
+ SessionType
88
+ } from "#protocol";
89
+ import {
90
+ ArraySchema,
91
+ DEFAULT_MAX_PATHS_PER_INVOKE,
92
+ EventNumber,
93
+ INTERACTION_PROTOCOL_ID,
94
+ ReceivedStatusResponseError,
95
+ StatusCode,
96
+ StatusResponseError,
97
+ TlvAny,
98
+ TlvInvokeResponseData,
99
+ TlvInvokeResponseForSend,
100
+ TlvNoArguments,
101
+ TlvNoResponse,
102
+ TlvSubscribeResponse,
103
+ ValidationError
104
+ } from "#types";
105
+ import { ServerInteraction } from "@matter/protocol";
106
+ const logger = Logger.get("InteractionServer");
107
+ const activityKey = Symbol("activity");
108
+ const AclClusterId = AccessControlCluster.id;
109
+ const AclAttributeId = AccessControlCluster.attributes.acl.id;
110
+ function isConcreteAttributePath(path) {
111
+ const { endpointId, clusterId, attributeId } = path;
112
+ return endpointId !== void 0 && clusterId !== void 0 && attributeId !== void 0;
113
+ }
114
+ function validateReadAttributesPath(path, isGroupSession = false) {
115
+ if (isGroupSession) {
116
+ throw new StatusResponseError("Illegal read request with group session", StatusCode.InvalidAction);
117
+ }
118
+ const { clusterId, attributeId } = path;
119
+ if (clusterId === void 0 && attributeId !== void 0) {
120
+ if (!GLOBAL_IDS.has(attributeId)) {
121
+ throw new StatusResponseError(
122
+ `Illegal read request for wildcard cluster and non global attribute ${attributeId}`,
123
+ StatusCode.InvalidAction
124
+ );
125
+ }
126
+ }
127
+ }
128
+ function validateWriteAttributesPath(path, isGroupSession = false) {
129
+ const { endpointId, clusterId, attributeId } = path;
130
+ if (clusterId === void 0 || attributeId === void 0) {
131
+ throw new StatusResponseError(
132
+ "Illegal write request with wildcard cluster or attribute ID",
133
+ StatusCode.InvalidAction
134
+ );
135
+ }
136
+ if (isGroupSession && endpointId !== void 0) {
137
+ throw new StatusResponseError("Illegal write request with group ID and endpoint ID", StatusCode.InvalidAction);
138
+ }
139
+ }
140
+ function isConcreteEventPath(path) {
141
+ const { endpointId, clusterId, eventId } = path;
142
+ return endpointId !== void 0 && clusterId !== void 0 && eventId !== void 0;
143
+ }
144
+ function validateReadEventPath(path, isGroupSession = false) {
145
+ const { clusterId, eventId } = path;
146
+ if (clusterId === void 0 && eventId !== void 0) {
147
+ throw new StatusResponseError("Illegal read request with wildcard cluster ID", StatusCode.InvalidAction);
148
+ }
149
+ if (isGroupSession) {
150
+ throw new StatusResponseError("Illegal read request with group session", StatusCode.InvalidAction);
151
+ }
152
+ }
153
+ function isConcreteCommandPath(path) {
154
+ const { endpointId, clusterId, commandId } = path;
155
+ return endpointId !== void 0 && clusterId !== void 0 && commandId !== void 0;
156
+ }
157
+ function validateCommandPath(path, isGroupSession = false) {
158
+ const { endpointId, clusterId, commandId } = path;
159
+ if (clusterId === void 0 || commandId === void 0) {
160
+ throw new StatusResponseError(
161
+ "Illegal write request with wildcard cluster or attribute ID",
162
+ StatusCode.InvalidAction
163
+ );
164
+ }
165
+ if (isGroupSession && endpointId !== void 0) {
166
+ throw new StatusResponseError("Illegal write request with group ID and endpoint ID", StatusCode.InvalidAction);
167
+ }
168
+ }
169
+ function getMatterModelCluster(clusterId) {
170
+ return MatterModel.standard.get(ClusterModel, clusterId);
171
+ }
172
+ function getMatterModelClusterCommand(clusterId, commandId) {
173
+ return getMatterModelCluster(clusterId)?.get(CommandModel, commandId);
174
+ }
175
+ class InteractionServer {
176
+ id = INTERACTION_PROTOCOL_ID;
177
+ requiresSecureSession = true;
178
+ #context;
179
+ #nextSubscriptionId = Crypto.getRandomUInt32();
180
+ #isClosing = false;
181
+ #clientHandler;
182
+ #subscriptionConfig;
183
+ #maxPathsPerInvoke;
184
+ #subscriptionEstablishmentStarted = Observable();
185
+ #changeListener;
186
+ #node;
187
+ #activity;
188
+ #newActivityBlocked = false;
189
+ #aclServer;
190
+ #aclUpdateIsDelayedInExchange = /* @__PURE__ */ new Set();
191
+ #serverInteraction;
192
+ constructor(node, sessions) {
193
+ this.#context = {
194
+ sessions,
195
+ exchangeManager: node.env.get(ExchangeManager),
196
+ structure: new InteractionEndpointStructure()
197
+ };
198
+ this.#subscriptionConfig = ServerSubscriptionConfig.of(node.state.network.subscriptionOptions);
199
+ this.#maxPathsPerInvoke = node.state.basicInformation.maxPathsPerInvoke ?? DEFAULT_MAX_PATHS_PER_INVOKE;
200
+ this.#context.structure.change.on(async () => {
201
+ this.#context.sessions.updateAllSubscriptions();
202
+ });
203
+ this.#activity = node.env.get(NodeActivity);
204
+ this.#node = node;
205
+ this.#serverInteraction = new ServerInteraction(node.protocol);
206
+ this.#updateStructure();
207
+ this.#changeListener = (type, endpoint) => {
208
+ switch (type) {
209
+ case EndpointLifecycle.Change.ServersChanged:
210
+ EndpointServer.forEndpoint(endpoint).updateServers();
211
+ this.#updateStructure();
212
+ break;
213
+ case EndpointLifecycle.Change.PartsReady:
214
+ case EndpointLifecycle.Change.ClientsChanged:
215
+ case EndpointLifecycle.Change.Destroyed:
216
+ this.#updateStructure();
217
+ break;
218
+ }
219
+ };
220
+ node.lifecycle.changed.on(this.#changeListener);
221
+ }
222
+ async [Symbol.asyncDispose]() {
223
+ this.#node.lifecycle.changed.off(this.#changeListener);
224
+ await this.close();
225
+ this.#context.structure.close();
226
+ await EndpointServer.forEndpoint(this.#node)[Symbol.asyncDispose]();
227
+ }
228
+ blockNewActivity() {
229
+ this.#newActivityBlocked = true;
230
+ }
231
+ get isClosing() {
232
+ return this.#isClosing;
233
+ }
234
+ get maxPathsPerInvoke() {
235
+ return this.#maxPathsPerInvoke;
236
+ }
237
+ get subscriptionEstablishmentStarted() {
238
+ return this.#subscriptionEstablishmentStarted;
239
+ }
240
+ async onNewExchange(exchange, message) {
241
+ var _stack = [];
242
+ try {
243
+ if (this.#newActivityBlocked || this.isClosing) {
244
+ return;
245
+ }
246
+ if (message.payloadHeader.messageType === MessageType.ReportData && this.clientHandler) {
247
+ return this.clientHandler.onNewExchange(exchange, message);
248
+ }
249
+ const activity = __using(_stack, this.#activity.begin(`session#${exchange.session.id.toString(16)}`));
250
+ exchange[activityKey] = activity;
251
+ return new InteractionServerMessenger(exchange).handleRequest(this).finally(() => delete exchange[activityKey]);
252
+ } catch (_) {
253
+ var _error = _, _hasError = true;
254
+ } finally {
255
+ __callDispose(_stack, _error, _hasError);
256
+ }
257
+ }
258
+ get aclServer() {
259
+ if (this.#aclServer !== void 0) {
260
+ return this.#aclServer;
261
+ }
262
+ const aclServer = this.#node.act((agent) => agent.get(AccessControlServer));
263
+ if (MaybePromise.is(aclServer)) {
264
+ throw new InternalError("AccessControlServer should already be initialized.");
265
+ }
266
+ return this.#aclServer = aclServer;
267
+ }
268
+ get clientHandler() {
269
+ return this.#clientHandler;
270
+ }
271
+ set clientHandler(clientHandler) {
272
+ this.#clientHandler = clientHandler;
273
+ }
274
+ async #collectEventDataForRead({ eventRequests, eventFilters, isFabricFiltered }, exchange, message) {
275
+ let eventReportsPayload;
276
+ if (eventRequests) {
277
+ eventReportsPayload = [];
278
+ for (const requestPath of eventRequests) {
279
+ validateReadEventPath(requestPath);
280
+ const events = this.#context.structure.getEvents([requestPath]);
281
+ if (events.length === 0) {
282
+ if (isConcreteEventPath(requestPath)) {
283
+ const { endpointId, clusterId, eventId } = requestPath;
284
+ try {
285
+ this.#context.structure.validateConcreteEventPath(endpointId, clusterId, eventId);
286
+ throw new InternalError(
287
+ "validateConcreteEventPath should throw StatusResponseError but did not."
288
+ );
289
+ } catch (e) {
290
+ StatusResponseError.accept(e);
291
+ logger.debug(
292
+ `Read event from ${exchange.channel.name}: ${this.#context.structure.resolveEventName(requestPath)}: unsupported path: Status=${e.code}`
293
+ );
294
+ eventReportsPayload?.push({
295
+ hasFabricSensitiveData: false,
296
+ eventStatus: { path: requestPath, status: { status: e.code } }
297
+ });
298
+ }
299
+ }
300
+ logger.debug(
301
+ `Read event from ${exchange.channel.name}: ${this.#context.structure.resolveEventName(
302
+ requestPath
303
+ )}: ignore non-existing event`
304
+ );
305
+ continue;
306
+ }
307
+ const reportsForPath = new Array();
308
+ for (const { path, event } of events) {
309
+ try {
310
+ const matchingEvents = await this.readEvent(
311
+ path,
312
+ eventFilters,
313
+ event,
314
+ exchange,
315
+ isFabricFiltered,
316
+ message
317
+ );
318
+ logger.debug(
319
+ `Read event from ${exchange.channel.name}: ${this.#context.structure.resolveEventName(
320
+ path
321
+ )}=${Diagnostic.json(matchingEvents)}`
322
+ );
323
+ const { schema } = event;
324
+ reportsForPath.push(
325
+ ...matchingEvents.map(({ number, priority, epochTimestamp, payload }) => ({
326
+ hasFabricSensitiveData: event.hasFabricSensitiveData,
327
+ eventData: {
328
+ path,
329
+ eventNumber: number,
330
+ priority,
331
+ epochTimestamp,
332
+ payload,
333
+ schema
334
+ }
335
+ }))
336
+ );
337
+ } catch (error) {
338
+ logger.error(
339
+ `Error while reading event from ${exchange.channel.name} to ${this.#context.structure.resolveEventName(path)}:`,
340
+ error
341
+ );
342
+ StatusResponseError.accept(error);
343
+ if (isConcreteEventPath(requestPath)) {
344
+ eventReportsPayload?.push({
345
+ hasFabricSensitiveData: false,
346
+ eventStatus: { path, status: { status: error.code } }
347
+ });
348
+ }
349
+ }
350
+ }
351
+ eventReportsPayload.push(
352
+ ...reportsForPath.sort((a, b) => {
353
+ const eventNumberA = a.eventData?.eventNumber ?? EventNumber(0);
354
+ const eventNumberB = b.eventData?.eventNumber ?? EventNumber(0);
355
+ if (eventNumberA > eventNumberB) {
356
+ return 1;
357
+ } else if (eventNumberA < eventNumberB) {
358
+ return -1;
359
+ } else {
360
+ return 0;
361
+ }
362
+ })
363
+ );
364
+ }
365
+ }
366
+ return eventReportsPayload;
367
+ }
368
+ /**
369
+ * Returns an iterator that yields the data reports and events data for the given read request.
370
+ */
371
+ async *#iterateReadAttributesPaths(readRequest, eventReportsPayload, exchange, message) {
372
+ const { isFabricFiltered } = readRequest;
373
+ const context = OnlineContext({
374
+ activity: exchange[activityKey],
375
+ fabricFiltered: isFabricFiltered,
376
+ message,
377
+ exchange,
378
+ tracer: this.#tracer,
379
+ actionType: ActionTracer.ActionType.Read,
380
+ node: this.#node
381
+ }).beginReadOnly();
382
+ for await (const chunk of this.#serverInteraction.read(readRequest, context)) {
383
+ for (const report of chunk) {
384
+ switch (report.kind) {
385
+ case "attr-value": {
386
+ const { path, value: payload, version: dataVersion, tlv: schema } = report;
387
+ if (schema === void 0) {
388
+ throw new InternalError(`Attribute ${path.clusterId}/${path.attributeId} not found`);
389
+ }
390
+ const data = {
391
+ attributeData: {
392
+ path,
393
+ payload,
394
+ schema,
395
+ dataVersion
396
+ },
397
+ hasFabricSensitiveData: true
398
+ // With this we disable the validation for missing data in encoding, we trust behavior logic
399
+ };
400
+ yield data;
401
+ break;
402
+ }
403
+ case "attr-status": {
404
+ const { path, status } = report;
405
+ const statusReport = {
406
+ attributeStatus: {
407
+ path,
408
+ status: { status }
409
+ },
410
+ hasFabricSensitiveData: false
411
+ };
412
+ yield statusReport;
413
+ break;
414
+ }
415
+ }
416
+ }
417
+ }
418
+ context[Symbol.dispose]();
419
+ if (eventReportsPayload !== void 0) {
420
+ for (const eventReport of eventReportsPayload) {
421
+ yield eventReport;
422
+ }
423
+ }
424
+ }
425
+ async handleReadRequest(exchange, readRequest, message) {
426
+ const { attributeRequests, eventRequests, isFabricFiltered, interactionModelRevision } = readRequest;
427
+ logger.debug(
428
+ `Received read request from ${exchange.channel.name}: attributes:${attributeRequests?.map((path) => this.#context.structure.resolveAttributeName(path)).join(", ") ?? "none"}, events:${eventRequests?.map((path) => this.#context.structure.resolveEventName(path)).join(", ") ?? "none"} isFabricFiltered=${isFabricFiltered}`
429
+ );
430
+ if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
431
+ logger.debug(
432
+ `Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`
433
+ );
434
+ }
435
+ if (attributeRequests === void 0 && eventRequests === void 0) {
436
+ return {
437
+ dataReport: {
438
+ interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
439
+ suppressResponse: true
440
+ }
441
+ };
442
+ }
443
+ if (message.packetHeader.sessionType !== SessionType.Unicast) {
444
+ throw new StatusResponseError(
445
+ "Subscriptions are only allowed on unicast sessions",
446
+ StatusCode.InvalidAction
447
+ );
448
+ }
449
+ return {
450
+ dataReport: {
451
+ interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
452
+ suppressResponse: true
453
+ },
454
+ payload: this.#iterateReadAttributesPaths(
455
+ readRequest,
456
+ await this.#collectEventDataForRead(readRequest, exchange, message),
457
+ exchange,
458
+ message
459
+ )
460
+ };
461
+ }
462
+ readAttribute(path, attribute, exchange, fabricFiltered, message, offline = false) {
463
+ const readAttribute = () => attribute.getWithVersion(exchange.session, fabricFiltered, offline ? void 0 : message);
464
+ const endpoint = this.#context.structure.getEndpoint(path.endpointId);
465
+ if (!endpoint) {
466
+ throw new InternalError("Endpoint not found for ACL check. This should never happen.");
467
+ }
468
+ const result = offline ? OfflineContext.act("offline-read", this.#activity, readAttribute) : OnlineContext({
469
+ activity: exchange[activityKey],
470
+ fabricFiltered,
471
+ message,
472
+ exchange,
473
+ tracer: this.#tracer,
474
+ actionType: ActionTracer.ActionType.Read,
475
+ node: this.#node
476
+ }).act(readAttribute);
477
+ if (MaybePromise.is(result)) {
478
+ throw new InternalError("Reads should not return a promise.");
479
+ }
480
+ return result;
481
+ }
482
+ /**
483
+ * Reads the attributes for the given endpoint.
484
+ * This can currently only be used for subscriptions because errors are ignored!
485
+ */
486
+ readEndpointAttributesForSubscription(attributes, exchange, fabricFiltered, message, offline = false) {
487
+ const readAttributes = () => {
488
+ const result2 = new Array();
489
+ for (const { path, attribute } of attributes) {
490
+ try {
491
+ const value = attribute.getWithVersion(
492
+ exchange.session,
493
+ fabricFiltered,
494
+ offline ? void 0 : message
495
+ );
496
+ result2.push({ path, attribute, value: value.value, version: value.version });
497
+ } catch (error) {
498
+ if (StatusResponseError.is(error, StatusCode.UnsupportedAccess)) {
499
+ logger.warn(
500
+ `Permission denied reading attribute ${this.#context.structure.resolveAttributeName(path)}`
501
+ );
502
+ } else {
503
+ logger.warn(
504
+ `Error reading attribute ${this.#context.structure.resolveAttributeName(path)}:`,
505
+ error
506
+ );
507
+ }
508
+ }
509
+ }
510
+ return result2;
511
+ };
512
+ const result = offline ? OfflineContext.act("offline-read", this.#activity, readAttributes) : OnlineContext({
513
+ activity: exchange[activityKey],
514
+ fabricFiltered,
515
+ message,
516
+ exchange,
517
+ tracer: this.#tracer,
518
+ actionType: ActionTracer.ActionType.Read,
519
+ node: this.#node
520
+ }).act(readAttributes);
521
+ if (MaybePromise.is(result)) {
522
+ throw new InternalError("Online read should not return a promise.");
523
+ }
524
+ return result;
525
+ }
526
+ async readEvent(path, eventFilters, event, exchange, fabricFiltered, message) {
527
+ const readEvent = (context) => {
528
+ if (context.authorityAt(event.readAcl, {
529
+ endpoint: path.endpointId,
530
+ cluster: path.clusterId
531
+ }) !== AccessControl.Authority.Granted) {
532
+ throw new AccessDeniedError(
533
+ `Access to ${path.endpointId}/${Diagnostic.hex(path.clusterId)} denied on ${exchange.session.name}.`
534
+ );
535
+ }
536
+ return event.get(exchange.session, fabricFiltered, message, eventFilters);
537
+ };
538
+ return OnlineContext({
539
+ activity: exchange[activityKey],
540
+ fabricFiltered,
541
+ message,
542
+ exchange,
543
+ tracer: this.#tracer,
544
+ actionType: ActionTracer.ActionType.Read,
545
+ node: this.#node
546
+ }).act(readEvent);
547
+ }
548
+ async handleWriteRequest(exchange, writeRequest, message) {
549
+ let result;
550
+ try {
551
+ result = await this.#handleWriteRequestLogic(exchange, writeRequest, message);
552
+ } catch (error) {
553
+ if (this.#aclUpdateIsDelayedInExchange.has(exchange)) {
554
+ this.#aclUpdateIsDelayedInExchange.delete(exchange);
555
+ if (this.#aclUpdateIsDelayedInExchange.size === 0) {
556
+ this.aclServer.resetDelayedAccessControlList();
557
+ } else {
558
+ logger.error("One of multiple concurrent ACL writes failed, unhandled case for now.");
559
+ }
560
+ }
561
+ throw error;
562
+ }
563
+ if (this.#aclUpdateIsDelayedInExchange.has(exchange)) {
564
+ this.#aclUpdateIsDelayedInExchange.delete(exchange);
565
+ if (this.#aclUpdateIsDelayedInExchange.size === 0) {
566
+ this.aclServer.aclUpdateDelayed = false;
567
+ } else {
568
+ logger.info("Multiple concurrent ACL writes, waiting for all to finish.");
569
+ }
570
+ }
571
+ return result;
572
+ }
573
+ async #handleWriteRequestLogic(exchange, { suppressResponse, timedRequest, writeRequests, interactionModelRevision, moreChunkedMessages }, message) {
574
+ const sessionType = message.packetHeader.sessionType;
575
+ logger.debug(
576
+ `Received write request from ${exchange.channel.name}: ${writeRequests.map((req) => this.#context.structure.resolveAttributeName(req.path)).join(", ")}, suppressResponse=${suppressResponse}, moreChunkedMessages=${moreChunkedMessages}`
577
+ );
578
+ if (moreChunkedMessages && suppressResponse) {
579
+ throw new StatusResponseError(
580
+ "MoreChunkedMessages and SuppressResponse cannot be used together in write messages",
581
+ StatusCode.InvalidAction
582
+ );
583
+ }
584
+ if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
585
+ logger.debug(
586
+ `Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`
587
+ );
588
+ }
589
+ const receivedWithinTimedInteraction = exchange.hasActiveTimedInteraction();
590
+ if (receivedWithinTimedInteraction && moreChunkedMessages) {
591
+ throw new StatusResponseError(
592
+ "Write Request action that is part of a Timed Write Interaction SHALL NOT be chunked.",
593
+ StatusCode.InvalidAction
594
+ );
595
+ }
596
+ if (exchange.hasExpiredTimedInteraction()) {
597
+ exchange.clearTimedInteraction();
598
+ throw new StatusResponseError(`Timed request window expired. Decline write request.`, StatusCode.Timeout);
599
+ }
600
+ if (timedRequest !== exchange.hasTimedInteraction()) {
601
+ throw new StatusResponseError(
602
+ `timedRequest flag of write interaction (${timedRequest}) mismatch with expected timed interaction (${receivedWithinTimedInteraction}).`,
603
+ StatusCode.TimedRequestMismatch
604
+ );
605
+ }
606
+ if (receivedWithinTimedInteraction) {
607
+ logger.debug(
608
+ `Write request from ${exchange.channel.name} successfully received while timed interaction is running.`
609
+ );
610
+ exchange.clearTimedInteraction();
611
+ if (sessionType !== SessionType.Unicast) {
612
+ throw new StatusResponseError(
613
+ "Write requests are only allowed on unicast sessions when a timed interaction is running.",
614
+ StatusCode.InvalidAction
615
+ );
616
+ }
617
+ }
618
+ if (sessionType === SessionType.Group && !suppressResponse) {
619
+ throw new StatusResponseError(
620
+ "Write requests are only allowed as group casts when suppressResponse=true.",
621
+ StatusCode.InvalidAction
622
+ );
623
+ }
624
+ const writeData = expandPathsInAttributeData(writeRequests, true);
625
+ const writeResults = new Array();
626
+ const attributeListWrites = /* @__PURE__ */ new Set();
627
+ const clusterDataVersionInfo = /* @__PURE__ */ new Map();
628
+ const inaccessiblePaths = /* @__PURE__ */ new Set();
629
+ for (const writeRequest of writeData) {
630
+ const { path: writePath, dataVersion } = writeRequest;
631
+ validateWriteAttributesPath(writePath);
632
+ const attributes = this.#context.structure.getAttributes([writePath], true);
633
+ if (attributes.length === 0) {
634
+ if (isConcreteAttributePath(writePath)) {
635
+ const { endpointId, clusterId, attributeId } = writePath;
636
+ try {
637
+ this.#context.structure.validateConcreteAttributePath(endpointId, clusterId, attributeId);
638
+ throw new StatusResponseError(
639
+ `Attribute ${attributeId} is not writable.`,
640
+ StatusCode.UnsupportedWrite
641
+ );
642
+ } catch (e) {
643
+ StatusResponseError.accept(e);
644
+ logger.debug(
645
+ `Write from ${exchange.channel.name}: ${this.#context.structure.resolveAttributeName(
646
+ writePath
647
+ )} not allowed: Status=${e.code}`
648
+ );
649
+ writeResults.push({ path: writePath, statusCode: e.code });
650
+ }
651
+ } else {
652
+ logger.debug(
653
+ `Write from ${exchange.channel.name}: ${this.#context.structure.resolveAttributeName(
654
+ writePath
655
+ )}: ignore non-existing (wildcard) attribute`
656
+ );
657
+ }
658
+ continue;
659
+ }
660
+ if (attributes.length === 1 && isConcreteAttributePath(writePath)) {
661
+ const { endpointId, clusterId } = writePath;
662
+ const { attribute } = attributes[0];
663
+ if (attribute.requiresTimedInteraction && !receivedWithinTimedInteraction) {
664
+ logger.debug(`This write requires a timed interaction which is not initialized.`);
665
+ writeResults.push({ path: writePath, statusCode: StatusCode.NeedsTimedInteraction });
666
+ continue;
667
+ }
668
+ if (attribute instanceof FabricScopedAttributeServer && (!exchange.session.isSecure || !exchange.session.fabric)) {
669
+ logger.debug(`This write requires a secure session with a fabric assigned which is missing.`);
670
+ writeResults.push({ path: writePath, statusCode: StatusCode.UnsupportedAccess });
671
+ continue;
672
+ }
673
+ if (dataVersion !== void 0) {
674
+ const datasource = this.#context.structure.getClusterServer(endpointId, clusterId)?.datasource;
675
+ const { nodeId } = writePath;
676
+ const clusterKey = clusterPathToId({ nodeId, endpointId, clusterId });
677
+ const currentDataVersion = clusterDataVersionInfo.get(clusterKey) ?? datasource?.version;
678
+ if (currentDataVersion !== void 0) {
679
+ if (dataVersion !== currentDataVersion) {
680
+ logger.debug(
681
+ `This write requires a specific data version (${dataVersion}) which do not match the current cluster data version (${currentDataVersion}).`
682
+ );
683
+ writeResults.push({ path: writePath, statusCode: StatusCode.DataVersionMismatch });
684
+ continue;
685
+ }
686
+ clusterDataVersionInfo.set(clusterKey, currentDataVersion);
687
+ }
688
+ }
689
+ }
690
+ for (const { path, attribute } of attributes) {
691
+ const { schema, defaultValue } = attribute;
692
+ const pathId = attributePathToId(path);
693
+ try {
694
+ if (!(attribute instanceof AttributeServer) && !(attribute instanceof FabricScopedAttributeServer)) {
695
+ throw new StatusResponseError(
696
+ "Fixed attributes cannot be written",
697
+ StatusCode.UnsupportedWrite
698
+ );
699
+ }
700
+ if (inaccessiblePaths.has(pathId)) {
701
+ logger.debug(`This write is not allowed due to previous access denied.`);
702
+ continue;
703
+ }
704
+ const { endpointId } = path;
705
+ const { listIndex } = writePath;
706
+ const value = listIndex === void 0 ? decodeAttributeValueWithSchema(schema, [writeRequest], defaultValue) : decodeListAttributeValueWithSchema(
707
+ schema,
708
+ [writeRequest],
709
+ this.readAttribute(path, attribute, exchange, true, message).value ?? defaultValue
710
+ );
711
+ logger.debug(
712
+ `Handle write request from ${exchange.channel.name} resolved to: ${this.#context.structure.resolveAttributeName(path)}=${Diagnostic.json(
713
+ value
714
+ )} (listIndex=${listIndex}, for-version=${dataVersion})`
715
+ );
716
+ if (attribute.requiresTimedInteraction && !receivedWithinTimedInteraction) {
717
+ logger.debug(`This write requires a timed interaction which is not initialized.`);
718
+ throw new StatusResponseError(
719
+ "This write requires a timed interaction which is not initialized.",
720
+ StatusCode.NeedsTimedInteraction
721
+ );
722
+ }
723
+ await this.writeAttribute(
724
+ path,
725
+ attribute,
726
+ value,
727
+ exchange,
728
+ message,
729
+ this.#context.structure.getEndpoint(endpointId),
730
+ receivedWithinTimedInteraction,
731
+ schema instanceof ArraySchema
732
+ );
733
+ if (schema instanceof ArraySchema && !attributeListWrites.has(attribute)) {
734
+ attributeListWrites.add(attribute);
735
+ }
736
+ } catch (error) {
737
+ if (StatusResponseError.is(error, StatusCode.UnsupportedAccess)) {
738
+ inaccessiblePaths.add(pathId);
739
+ }
740
+ if (attributes.length === 1 && isConcreteAttributePath(writePath)) {
741
+ logger.error(
742
+ `Error while handling write request from ${exchange.channel.name} to ${this.#context.structure.resolveAttributeName(path)}:`,
743
+ error instanceof StatusResponseError ? error.message : error
744
+ );
745
+ if (error instanceof StatusResponseError) {
746
+ writeResults.push({ path, statusCode: error.code, clusterStatusCode: error.clusterCode });
747
+ continue;
748
+ }
749
+ writeResults.push({ path, statusCode: StatusCode.ConstraintError });
750
+ continue;
751
+ } else {
752
+ logger.debug(
753
+ `While handling write request from ${exchange.channel.name} to ${this.#context.structure.resolveAttributeName(path)} ignored: ${error.message}`
754
+ );
755
+ }
756
+ }
757
+ writeResults.push({ path, statusCode: StatusCode.Success });
758
+ }
759
+ }
760
+ const errorResults = writeResults.filter(({ statusCode }) => statusCode !== StatusCode.Success);
761
+ logger.debug(
762
+ `Write request from ${exchange.channel.name} done ${errorResults.length ? `with following errors: ${errorResults.map(
763
+ ({ path, statusCode }) => `${this.#context.structure.resolveAttributeName(path)}=${Diagnostic.json(statusCode)}`
764
+ ).join(", ")}` : "without errors"}`
765
+ );
766
+ const response = {
767
+ interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
768
+ writeResponses: writeResults.map(({ path, statusCode, clusterStatusCode }) => ({
769
+ path,
770
+ status: { status: statusCode, clusterStatus: clusterStatusCode }
771
+ }))
772
+ };
773
+ for (const attribute of attributeListWrites.values()) {
774
+ try {
775
+ attribute.triggerDelayedChangeEvents();
776
+ } catch (error) {
777
+ logger.error(
778
+ `Ignored Error while writing attribute from ${exchange.channel.name} to ${attribute.name}:`,
779
+ error
780
+ );
781
+ }
782
+ }
783
+ return response;
784
+ }
785
+ async writeAttribute(path, attribute, value, exchange, message, _endpoint, timed, isListWrite = false) {
786
+ const writeAttribute = () => attribute.set(value, exchange.session, message, isListWrite);
787
+ if (path.endpointId === 0 && path.clusterId === AclClusterId && path.attributeId === AclAttributeId) {
788
+ this.aclServer.aclUpdateDelayed = true;
789
+ this.#aclUpdateIsDelayedInExchange.add(exchange);
790
+ } else {
791
+ if (this.#aclUpdateIsDelayedInExchange.has(exchange)) {
792
+ this.#aclUpdateIsDelayedInExchange.delete(exchange);
793
+ if (this.#aclUpdateIsDelayedInExchange.size === 0) {
794
+ this.aclServer.aclUpdateDelayed = false;
795
+ } else {
796
+ logger.info("Multiple concurrent ACL writes, waiting for all to finish.");
797
+ }
798
+ }
799
+ }
800
+ return OnlineContext({
801
+ activity: exchange[activityKey],
802
+ timed,
803
+ message,
804
+ exchange,
805
+ fabricFiltered: true,
806
+ tracer: this.#tracer,
807
+ actionType: ActionTracer.ActionType.Write,
808
+ node: this.#node
809
+ }).act(writeAttribute);
810
+ }
811
+ async handleSubscribeRequest(exchange, request, messenger, message) {
812
+ const {
813
+ minIntervalFloorSeconds,
814
+ maxIntervalCeilingSeconds,
815
+ attributeRequests,
816
+ dataVersionFilters,
817
+ eventRequests,
818
+ eventFilters,
819
+ keepSubscriptions,
820
+ isFabricFiltered,
821
+ interactionModelRevision
822
+ } = request;
823
+ logger.debug(
824
+ `Received subscribe request from ${exchange.channel.name} (keepSubscriptions=${keepSubscriptions}, isFabricFiltered=${isFabricFiltered})`
825
+ );
826
+ if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
827
+ logger.debug(
828
+ `Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`
829
+ );
830
+ }
831
+ if (message.packetHeader.sessionType !== SessionType.Unicast) {
832
+ throw new StatusResponseError(
833
+ "Subscriptions are only allowed on unicast sessions",
834
+ StatusCode.InvalidAction
835
+ );
836
+ }
837
+ assertSecureSession(exchange.session, "Subscriptions are only implemented on secure sessions");
838
+ const session = exchange.session;
839
+ const fabric = session.fabric;
840
+ if (fabric === void 0)
841
+ throw new StatusResponseError(
842
+ "Subscriptions are only implemented after a fabric has been assigned",
843
+ StatusCode.InvalidAction
844
+ );
845
+ if (!keepSubscriptions) {
846
+ const clearedCount = await this.#context.sessions.clearSubscriptionsForNode(
847
+ fabric.addressOf(session.peerNodeId),
848
+ true
849
+ );
850
+ if (clearedCount > 0) {
851
+ logger.debug(
852
+ `Cleared ${clearedCount} subscriptions for Subscriber node ${session.peerNodeId} because keepSubscriptions=false`
853
+ );
854
+ }
855
+ }
856
+ if ((!Array.isArray(attributeRequests) || attributeRequests.length === 0) && (!Array.isArray(eventRequests) || eventRequests.length === 0)) {
857
+ throw new StatusResponseError("No attributes or events requested", StatusCode.InvalidAction);
858
+ }
859
+ logger.debug(
860
+ `Subscribe to attributes:${attributeRequests?.map((path) => this.#context.structure.resolveAttributeName(path)).join(", ") ?? "none"}, events:${eventRequests?.map((path) => this.#context.structure.resolveEventName(path)).join(", ") ?? "none"}`
861
+ );
862
+ if (dataVersionFilters !== void 0 && dataVersionFilters.length > 0) {
863
+ logger.debug(
864
+ `DataVersionFilters: ${dataVersionFilters.map(
865
+ ({ path: { nodeId, endpointId, clusterId }, dataVersion }) => `${clusterPathToId({ nodeId, endpointId, clusterId })}=${dataVersion}`
866
+ ).join(", ")}`
867
+ );
868
+ }
869
+ if (eventFilters !== void 0 && eventFilters.length > 0)
870
+ logger.debug(
871
+ `Event filters: ${eventFilters.map((filter) => `${filter.nodeId}/${filter.eventMin}`).join(", ")}`
872
+ );
873
+ attributeRequests?.forEach((path) => validateReadAttributesPath(path));
874
+ eventRequests?.forEach((path) => validateReadEventPath(path));
875
+ if (minIntervalFloorSeconds < 0) {
876
+ throw new StatusResponseError(
877
+ "minIntervalFloorSeconds should be greater or equal to 0",
878
+ StatusCode.InvalidAction
879
+ );
880
+ }
881
+ if (maxIntervalCeilingSeconds < 0) {
882
+ throw new StatusResponseError(
883
+ "maxIntervalCeilingSeconds should be greater or equal to 1",
884
+ StatusCode.InvalidAction
885
+ );
886
+ }
887
+ if (maxIntervalCeilingSeconds < minIntervalFloorSeconds) {
888
+ throw new StatusResponseError(
889
+ "maxIntervalCeilingSeconds should be greater or equal to minIntervalFloorSeconds",
890
+ StatusCode.InvalidAction
891
+ );
892
+ }
893
+ if (this.#nextSubscriptionId === 4294967295) this.#nextSubscriptionId = 0;
894
+ const subscriptionId = this.#nextSubscriptionId++;
895
+ this.#subscriptionEstablishmentStarted.emit(session.peerAddress);
896
+ let subscription;
897
+ try {
898
+ subscription = await this.#establishSubscription(
899
+ subscriptionId,
900
+ request,
901
+ messenger,
902
+ session,
903
+ exchange,
904
+ message
905
+ );
906
+ } catch (error) {
907
+ logger.error(
908
+ `Subscription ${subscriptionId} for Session ${session.id}: Error while sending initial data reports`,
909
+ error instanceof MatterError ? error.message : error
910
+ );
911
+ if (error instanceof StatusResponseError && !(error instanceof ReceivedStatusResponseError)) {
912
+ logger.info(`Sending status response ${error.code} for interaction error: ${error.message}`);
913
+ await messenger.sendStatus(error.code, {
914
+ logContext: {
915
+ for: "I/SubscriptionSeed-Status"
916
+ }
917
+ });
918
+ }
919
+ await messenger.close();
920
+ return;
921
+ }
922
+ const maxInterval = subscription.maxInterval;
923
+ await messenger.send(
924
+ MessageType.SubscribeResponse,
925
+ TlvSubscribeResponse.encode({
926
+ subscriptionId,
927
+ maxInterval,
928
+ interactionModelRevision: Specification.INTERACTION_MODEL_REVISION
929
+ }),
930
+ {
931
+ logContext: {
932
+ subId: subscriptionId,
933
+ maxInterval
934
+ }
935
+ }
936
+ );
937
+ subscription.activate();
938
+ }
939
+ async #establishSubscription(id, {
940
+ minIntervalFloorSeconds,
941
+ maxIntervalCeilingSeconds,
942
+ attributeRequests,
943
+ dataVersionFilters,
944
+ eventRequests,
945
+ eventFilters,
946
+ isFabricFiltered
947
+ }, messenger, session, exchange, message) {
948
+ const context = {
949
+ session,
950
+ structure: this.#context.structure,
951
+ readAttribute: (path, attribute, offline) => this.readAttribute(path, attribute, exchange, isFabricFiltered, message, offline),
952
+ readEndpointAttributesForSubscription: (attributes) => this.readEndpointAttributesForSubscription(attributes, exchange, isFabricFiltered, message),
953
+ readEvent: (path, event, eventFilters2) => this.readEvent(path, eventFilters2, event, exchange, isFabricFiltered, message),
954
+ initiateExchange: (address, protocolId) => this.#context.exchangeManager.initiateExchange(address, protocolId)
955
+ };
956
+ const subscription = new ServerSubscription({
957
+ id,
958
+ context,
959
+ criteria: {
960
+ attributeRequests,
961
+ dataVersionFilters,
962
+ eventRequests,
963
+ eventFilters,
964
+ isFabricFiltered
965
+ },
966
+ minIntervalFloorSeconds,
967
+ maxIntervalCeilingSeconds,
968
+ subscriptionOptions: this.#subscriptionConfig
969
+ });
970
+ try {
971
+ await subscription.sendInitialReport(messenger);
972
+ } catch (error) {
973
+ await subscription.close();
974
+ throw error;
975
+ }
976
+ logger.info(
977
+ `Successfully created subscription ${id} for Session ${session.id} to ${session.peerAddress}. Updates: ${minIntervalFloorSeconds} - ${maxIntervalCeilingSeconds} => ${subscription.maxInterval} seconds (sendInterval = ${subscription.sendInterval} seconds)`
978
+ );
979
+ return subscription;
980
+ }
981
+ async establishFormerSubscription({
982
+ subscriptionId,
983
+ attributeRequests,
984
+ eventRequests,
985
+ isFabricFiltered,
986
+ minIntervalFloorSeconds,
987
+ maxIntervalCeilingSeconds,
988
+ maxInterval,
989
+ sendInterval
990
+ }, session) {
991
+ const exchange = this.#context.exchangeManager.initiateExchange(session.peerAddress, INTERACTION_PROTOCOL_ID);
992
+ const message = {};
993
+ logger.debug(
994
+ `Send DataReports to re-establish subscription ${subscriptionId} to `,
995
+ Diagnostic.dict({ isFabricFiltered, maxInterval, sendInterval })
996
+ );
997
+ const context = {
998
+ session,
999
+ structure: this.#context.structure,
1000
+ readAttribute: (path, attribute, offline) => this.readAttribute(path, attribute, exchange, isFabricFiltered, message, offline),
1001
+ readEndpointAttributesForSubscription: (attributes) => this.readEndpointAttributesForSubscription(attributes, exchange, isFabricFiltered, message),
1002
+ readEvent: (path, event, eventFilters) => this.readEvent(path, eventFilters, event, exchange, isFabricFiltered, message),
1003
+ initiateExchange: (address, protocolId) => this.#context.exchangeManager.initiateExchange(address, protocolId)
1004
+ };
1005
+ const subscription = new ServerSubscription({
1006
+ id: subscriptionId,
1007
+ context,
1008
+ minIntervalFloorSeconds,
1009
+ maxIntervalCeilingSeconds,
1010
+ criteria: {
1011
+ attributeRequests,
1012
+ eventRequests,
1013
+ isFabricFiltered
1014
+ },
1015
+ subscriptionOptions: this.#subscriptionConfig,
1016
+ useAsMaxInterval: maxInterval,
1017
+ useAsSendInterval: sendInterval
1018
+ });
1019
+ try {
1020
+ await subscription.sendInitialReport(new InteractionServerMessenger(exchange));
1021
+ subscription.activate();
1022
+ logger.info(
1023
+ `Successfully re-established subscription ${subscriptionId} for Session ${session.id} to ${session.peerAddress}. Updates: ${minIntervalFloorSeconds} - ${maxIntervalCeilingSeconds} => ${subscription.maxInterval} seconds (sendInterval = ${subscription.sendInterval} seconds)`
1024
+ );
1025
+ } catch (error) {
1026
+ await subscription.close();
1027
+ throw error;
1028
+ }
1029
+ return subscription;
1030
+ }
1031
+ async handleInvokeRequest(exchange, { invokeRequests, timedRequest, suppressResponse, interactionModelRevision }, messenger, message) {
1032
+ logger.debug(
1033
+ `Received invoke request from ${exchange.channel.name}${invokeRequests.length > 0 ? ` with ${invokeRequests.length} commands` : ""}: ${invokeRequests.map(
1034
+ ({ commandPath: { endpointId, clusterId, commandId } }) => this.#context.structure.resolveCommandName({ endpointId, clusterId, commandId })
1035
+ ).join(", ")}, suppressResponse=${suppressResponse}`
1036
+ );
1037
+ if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
1038
+ logger.debug(
1039
+ `Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`
1040
+ );
1041
+ }
1042
+ const receivedWithinTimedInteraction = exchange.hasActiveTimedInteraction();
1043
+ if (exchange.hasExpiredTimedInteraction()) {
1044
+ exchange.clearTimedInteraction();
1045
+ throw new StatusResponseError(`Timed request window expired. Decline invoke request.`, StatusCode.Timeout);
1046
+ }
1047
+ if (timedRequest !== exchange.hasTimedInteraction()) {
1048
+ throw new StatusResponseError(
1049
+ `timedRequest flag of invoke interaction (${timedRequest}) mismatch with expected timed interaction (${receivedWithinTimedInteraction}).`,
1050
+ StatusCode.TimedRequestMismatch
1051
+ );
1052
+ }
1053
+ if (receivedWithinTimedInteraction) {
1054
+ logger.debug(`Invoke request from ${exchange.channel.name} received while timed interaction is running.`);
1055
+ exchange.clearTimedInteraction();
1056
+ if (message.packetHeader.sessionType !== SessionType.Unicast) {
1057
+ throw new StatusResponseError(
1058
+ "Invoke requests are only allowed on unicast sessions when a timed interaction is running.",
1059
+ StatusCode.InvalidAction
1060
+ );
1061
+ }
1062
+ }
1063
+ if (invokeRequests.length > this.#maxPathsPerInvoke) {
1064
+ throw new StatusResponseError(
1065
+ `Only ${this.#maxPathsPerInvoke} invoke requests are supported in one message. This message contains ${invokeRequests.length}`,
1066
+ StatusCode.InvalidAction
1067
+ );
1068
+ }
1069
+ invokeRequests.forEach(({ commandPath }) => validateCommandPath(commandPath));
1070
+ if (invokeRequests.length > 1) {
1071
+ const pathsUsed = /* @__PURE__ */ new Set();
1072
+ const commandRefsUsed = /* @__PURE__ */ new Set();
1073
+ invokeRequests.forEach(({ commandPath, commandRef }) => {
1074
+ if (!isConcreteCommandPath(commandPath)) {
1075
+ throw new StatusResponseError("Illegal wildcard path in batch invoke", StatusCode.InvalidAction);
1076
+ }
1077
+ const commandPathId = commandPathToId(commandPath);
1078
+ if (pathsUsed.has(commandPathId)) {
1079
+ throw new StatusResponseError(
1080
+ `Duplicate command path (${commandPathId}) in batch invoke`,
1081
+ StatusCode.InvalidAction
1082
+ );
1083
+ }
1084
+ if (commandRef === void 0) {
1085
+ throw new StatusResponseError(
1086
+ `Command reference missing in batch invoke of ${commandPathId}`,
1087
+ StatusCode.InvalidAction
1088
+ );
1089
+ }
1090
+ if (commandRefsUsed.has(commandRef)) {
1091
+ throw new StatusResponseError(
1092
+ `Duplicate command reference ${commandRef} in invoke of ${commandPathId}`,
1093
+ StatusCode.InvalidAction
1094
+ );
1095
+ }
1096
+ pathsUsed.add(commandPathId);
1097
+ commandRefsUsed.add(commandRef);
1098
+ });
1099
+ }
1100
+ const isGroupSession = message.packetHeader.sessionType === SessionType.Group;
1101
+ const invokeResponseMessage = {
1102
+ suppressResponse: false,
1103
+ // Deprecated but must be present
1104
+ interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
1105
+ invokeResponses: [],
1106
+ moreChunkedMessages: invokeRequests.length > 1
1107
+ // Assume for now we have multiple responses when having multiple invokes
1108
+ };
1109
+ const emptyInvokeResponseBytes = TlvInvokeResponseForSend.encode(invokeResponseMessage);
1110
+ let messageSize = emptyInvokeResponseBytes.length;
1111
+ let invokeResultsProcessed = 0;
1112
+ const processResponseResult = async (invokeResponse) => {
1113
+ invokeResultsProcessed++;
1114
+ if (isGroupSession) {
1115
+ return;
1116
+ }
1117
+ const encodedInvokeResponse = TlvInvokeResponseData.encodeTlv(invokeResponse);
1118
+ const invokeResponseBytes = TlvAny.getEncodedByteLength(encodedInvokeResponse);
1119
+ if (messageSize + invokeResponseBytes > exchange.maxPayloadSize || invokeResultsProcessed === invokeRequests.length) {
1120
+ let lastMessageProcessed = false;
1121
+ if (messageSize + invokeResponseBytes <= exchange.maxPayloadSize) {
1122
+ invokeResponseMessage.invokeResponses.push(encodedInvokeResponse);
1123
+ lastMessageProcessed = true;
1124
+ }
1125
+ if (invokeResponseMessage.invokeResponses.length > 0) {
1126
+ if (invokeRequests.length > 1) {
1127
+ logger.debug(
1128
+ `Send ${lastMessageProcessed ? "final " : ""}invoke response for ${invokeResponseMessage.invokeResponses} commands`
1129
+ );
1130
+ }
1131
+ const moreChunkedMessages = lastMessageProcessed ? void 0 : true;
1132
+ await messenger.send(
1133
+ MessageType.InvokeResponse,
1134
+ TlvInvokeResponseForSend.encode({
1135
+ ...invokeResponseMessage,
1136
+ moreChunkedMessages
1137
+ }),
1138
+ {
1139
+ logContext: {
1140
+ invokeMsgFlags: Diagnostic.asFlags({
1141
+ suppressResponse,
1142
+ moreChunkedMessages
1143
+ })
1144
+ }
1145
+ }
1146
+ );
1147
+ invokeResponseMessage.invokeResponses = [];
1148
+ messageSize = emptyInvokeResponseBytes.length;
1149
+ }
1150
+ if (!lastMessageProcessed) {
1151
+ invokeResultsProcessed--;
1152
+ return processResponseResult(invokeResponse);
1153
+ }
1154
+ } else {
1155
+ invokeResponseMessage.invokeResponses.push(encodedInvokeResponse);
1156
+ messageSize += invokeResponseBytes;
1157
+ }
1158
+ };
1159
+ for (const { commandPath, commandFields, commandRef } of invokeRequests) {
1160
+ const commands = this.#context.structure.getCommands([commandPath]);
1161
+ if (commands.length === 0) {
1162
+ if (isConcreteCommandPath(commandPath)) {
1163
+ const { endpointId, clusterId, commandId } = commandPath;
1164
+ let result;
1165
+ try {
1166
+ this.#context.structure.validateConcreteCommandPath(endpointId, clusterId, commandId);
1167
+ throw new InternalError(
1168
+ "validateConcreteCommandPath should throw StatusResponseError but did not."
1169
+ );
1170
+ } catch (e) {
1171
+ StatusResponseError.accept(e);
1172
+ logger.debug(
1173
+ `Invoke from ${exchange.channel.name}: ${this.#context.structure.resolveCommandName(
1174
+ commandPath
1175
+ )} unsupported path: Status=${e.code}`
1176
+ );
1177
+ result = { status: { commandPath, status: { status: e.code }, commandRef } };
1178
+ }
1179
+ await processResponseResult(result);
1180
+ } else {
1181
+ logger.debug(
1182
+ `Invoke from ${exchange.channel.name}: ${this.#context.structure.resolveCommandName(
1183
+ commandPath
1184
+ )} ignore non-existing command`
1185
+ );
1186
+ }
1187
+ continue;
1188
+ }
1189
+ const isConcretePath = isConcreteCommandPath(commandPath);
1190
+ for (const { command, path } of commands) {
1191
+ const { endpointId, clusterId, commandId } = path;
1192
+ if (endpointId === void 0) {
1193
+ logger.error(
1194
+ `Invoke from ${exchange.channel.name}: ${this.#context.structure.resolveCommandName(
1195
+ path
1196
+ )} invalid path because empty endpoint!`
1197
+ );
1198
+ if (isConcretePath) {
1199
+ await processResponseResult({
1200
+ status: {
1201
+ commandPath: path,
1202
+ status: { status: StatusCode.UnsupportedEndpoint },
1203
+ commandRef
1204
+ }
1205
+ });
1206
+ }
1207
+ continue;
1208
+ }
1209
+ const endpoint = this.#context.structure.getEndpoint(endpointId);
1210
+ if (endpoint === void 0) {
1211
+ logger.error(
1212
+ `Invoke from ${exchange.channel.name}: ${this.#context.structure.resolveCommandName(
1213
+ path
1214
+ )} invalid path because endpoint not found!`
1215
+ );
1216
+ if (isConcretePath) {
1217
+ await processResponseResult({
1218
+ status: {
1219
+ commandPath: path,
1220
+ status: { status: StatusCode.UnsupportedEndpoint },
1221
+ commandRef
1222
+ }
1223
+ });
1224
+ }
1225
+ continue;
1226
+ }
1227
+ if (command.requiresTimedInteraction && !receivedWithinTimedInteraction) {
1228
+ logger.debug(`This invoke requires a timed interaction which is not initialized.`);
1229
+ if (isConcretePath) {
1230
+ await processResponseResult({
1231
+ status: {
1232
+ commandPath: path,
1233
+ status: { status: StatusCode.NeedsTimedInteraction },
1234
+ commandRef
1235
+ }
1236
+ });
1237
+ }
1238
+ continue;
1239
+ }
1240
+ if (getMatterModelClusterCommand(clusterId, commandId)?.fabricScoped && (!exchange.session.isSecure || !exchange.session.fabric)) {
1241
+ logger.debug(`This invoke requires a secure session with a fabric assigned which is missing.`);
1242
+ if (isConcretePath) {
1243
+ await processResponseResult({
1244
+ status: { commandPath: path, status: { status: StatusCode.UnsupportedAccess }, commandRef }
1245
+ });
1246
+ }
1247
+ continue;
1248
+ }
1249
+ let result;
1250
+ try {
1251
+ result = await this.invokeCommand(
1252
+ path,
1253
+ command,
1254
+ exchange,
1255
+ commandFields ?? TlvNoArguments.encodeTlv(commandFields),
1256
+ message,
1257
+ endpoint,
1258
+ receivedWithinTimedInteraction
1259
+ );
1260
+ } catch (e) {
1261
+ StatusResponseError.accept(e);
1262
+ let errorCode = e.code;
1263
+ const errorLogText = `Error ${Diagnostic.hex(errorCode)}${e.clusterCode !== void 0 ? `/${Diagnostic.hex(e.clusterCode)}` : ""} while invoking command: ${e.message}`;
1264
+ if (e instanceof ValidationError) {
1265
+ logger.info(
1266
+ `Validation-${errorLogText}${e.fieldName !== void 0 ? ` in field ${e.fieldName}` : ""}`
1267
+ );
1268
+ if (errorCode === StatusCode.InvalidAction) {
1269
+ errorCode = StatusCode.InvalidCommand;
1270
+ }
1271
+ } else {
1272
+ logger.info(errorLogText);
1273
+ }
1274
+ result = {
1275
+ code: errorCode,
1276
+ clusterCode: e.clusterCode,
1277
+ responseId: command.responseId,
1278
+ response: TlvNoResponse.encodeTlv()
1279
+ };
1280
+ }
1281
+ const { code, clusterCode, responseId, response } = result;
1282
+ if (response.length === 0) {
1283
+ await processResponseResult({
1284
+ status: { commandPath: path, status: { status: code, clusterStatus: clusterCode }, commandRef }
1285
+ });
1286
+ } else {
1287
+ await processResponseResult({
1288
+ command: {
1289
+ commandPath: { ...path, commandId: responseId },
1290
+ commandFields: response,
1291
+ commandRef
1292
+ }
1293
+ });
1294
+ }
1295
+ }
1296
+ }
1297
+ }
1298
+ async invokeCommand(path, command, exchange, commandFields, message, endpoint, timed = false) {
1299
+ const invokeCommand = (context) => {
1300
+ if (context.authorityAt(command.invokeAcl, {
1301
+ endpoint: endpoint.number,
1302
+ cluster: path.clusterId
1303
+ }) !== AccessControl.Authority.Granted) {
1304
+ throw new AccessDeniedError(
1305
+ `Access to ${endpoint.number}/${Diagnostic.hex(path.clusterId)} denied on ${exchange.session.name}.`
1306
+ );
1307
+ }
1308
+ return command.invoke(exchange.session, commandFields, message, endpoint);
1309
+ };
1310
+ return OnlineContext({
1311
+ activity: exchange[activityKey],
1312
+ command: true,
1313
+ timed,
1314
+ message,
1315
+ exchange,
1316
+ tracer: this.#tracer,
1317
+ actionType: ActionTracer.ActionType.Invoke,
1318
+ node: this.#node
1319
+ }).act(invokeCommand);
1320
+ }
1321
+ handleTimedRequest(exchange, { timeout, interactionModelRevision }) {
1322
+ logger.debug(`Received timed request (${timeout}ms) from ${exchange.channel.name}`);
1323
+ if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
1324
+ logger.debug(
1325
+ `Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`
1326
+ );
1327
+ }
1328
+ exchange.startTimedInteraction(timeout);
1329
+ }
1330
+ async close() {
1331
+ this.#isClosing = true;
1332
+ }
1333
+ get #tracer() {
1334
+ if (this.#node.env.has(ActionTracer)) {
1335
+ return this.#node.env.get(ActionTracer);
1336
+ }
1337
+ }
1338
+ #updateStructure() {
1339
+ if (this.#node.lifecycle.isPartsReady) {
1340
+ const server = EndpointServer.forEndpoint(this.#node);
1341
+ this.#context.structure.initializeFromEndpoint(server);
1342
+ }
1343
+ }
1344
+ }
1345
+ export {
1346
+ InteractionServer
1347
+ };
1348
+ //# sourceMappingURL=InteractionServer.js.map