@matter/protocol 0.13.1-alpha.0-20250515-a4c61c546 → 0.13.1-alpha.0-20250517-99a1e848a

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 (132) hide show
  1. package/dist/cjs/action/Interactable.d.ts +5 -1
  2. package/dist/cjs/action/Interactable.d.ts.map +1 -1
  3. package/dist/cjs/action/client/ClientInteraction.d.ts +3 -4
  4. package/dist/cjs/action/client/ClientInteraction.d.ts.map +1 -1
  5. package/dist/cjs/action/client/ClientInteraction.js +28 -16
  6. package/dist/cjs/action/client/ClientInteraction.js.map +1 -1
  7. package/dist/cjs/action/protocols.d.ts +7 -2
  8. package/dist/cjs/action/protocols.d.ts.map +1 -1
  9. package/dist/cjs/action/request/Write.d.ts +43 -2
  10. package/dist/cjs/action/request/Write.d.ts.map +1 -1
  11. package/dist/cjs/action/request/Write.js +74 -0
  12. package/dist/cjs/action/request/Write.js.map +2 -2
  13. package/dist/cjs/action/response/InvokeResult.d.ts +1 -2
  14. package/dist/cjs/action/response/InvokeResult.d.ts.map +1 -1
  15. package/dist/cjs/action/response/ReadResult.d.ts +5 -14
  16. package/dist/cjs/action/response/ReadResult.d.ts.map +1 -1
  17. package/dist/cjs/action/response/WriteResult.d.ts +19 -5
  18. package/dist/cjs/action/response/WriteResult.d.ts.map +1 -1
  19. package/dist/cjs/action/server/AccessControl.js +4 -4
  20. package/dist/cjs/action/server/AccessControl.js.map +1 -1
  21. package/dist/cjs/action/server/{AttributeResponse.d.ts → AttributeReadResponse.d.ts} +4 -4
  22. package/dist/cjs/action/server/AttributeReadResponse.d.ts.map +1 -0
  23. package/dist/cjs/action/server/{AttributeResponse.js → AttributeReadResponse.js} +12 -12
  24. package/dist/cjs/action/server/{AttributeResponse.js.map → AttributeReadResponse.js.map} +2 -2
  25. package/dist/cjs/action/server/AttributeSubscriptionResponse.d.ts +4 -4
  26. package/dist/cjs/action/server/AttributeSubscriptionResponse.d.ts.map +1 -1
  27. package/dist/cjs/action/server/AttributeSubscriptionResponse.js +2 -2
  28. package/dist/cjs/action/server/AttributeSubscriptionResponse.js.map +1 -1
  29. package/dist/cjs/action/server/AttributeWriteResponse.d.ts +28 -0
  30. package/dist/cjs/action/server/AttributeWriteResponse.d.ts.map +1 -0
  31. package/dist/cjs/action/server/AttributeWriteResponse.js +349 -0
  32. package/dist/cjs/action/server/AttributeWriteResponse.js.map +6 -0
  33. package/dist/cjs/action/server/DataResponse.d.ts +4 -3
  34. package/dist/cjs/action/server/DataResponse.d.ts.map +1 -1
  35. package/dist/cjs/action/server/DataResponse.js +1 -1
  36. package/dist/cjs/action/server/DataResponse.js.map +1 -1
  37. package/dist/cjs/action/server/{EventResponse.d.ts → EventReadResponse.d.ts} +4 -4
  38. package/dist/cjs/action/server/EventReadResponse.d.ts.map +1 -0
  39. package/dist/cjs/action/server/{EventResponse.js → EventReadResponse.js} +8 -8
  40. package/dist/cjs/action/server/EventReadResponse.js.map +6 -0
  41. package/dist/cjs/action/server/ServerInteraction.d.ts +3 -4
  42. package/dist/cjs/action/server/ServerInteraction.d.ts.map +1 -1
  43. package/dist/cjs/action/server/ServerInteraction.js +12 -10
  44. package/dist/cjs/action/server/ServerInteraction.js.map +1 -1
  45. package/dist/cjs/action/server/index.d.ts +3 -2
  46. package/dist/cjs/action/server/index.d.ts.map +1 -1
  47. package/dist/cjs/action/server/index.js +3 -2
  48. package/dist/cjs/action/server/index.js.map +1 -1
  49. package/dist/cjs/interaction/InteractionClient.d.ts.map +1 -1
  50. package/dist/cjs/interaction/InteractionClient.js +12 -1
  51. package/dist/cjs/interaction/InteractionClient.js.map +1 -1
  52. package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
  53. package/dist/cjs/interaction/InteractionMessenger.js +8 -3
  54. package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
  55. package/dist/esm/action/Interactable.d.ts +5 -1
  56. package/dist/esm/action/Interactable.d.ts.map +1 -1
  57. package/dist/esm/action/client/ClientInteraction.d.ts +3 -4
  58. package/dist/esm/action/client/ClientInteraction.d.ts.map +1 -1
  59. package/dist/esm/action/client/ClientInteraction.js +29 -17
  60. package/dist/esm/action/client/ClientInteraction.js.map +1 -1
  61. package/dist/esm/action/protocols.d.ts +7 -2
  62. package/dist/esm/action/protocols.d.ts.map +1 -1
  63. package/dist/esm/action/request/Write.d.ts +43 -2
  64. package/dist/esm/action/request/Write.d.ts.map +1 -1
  65. package/dist/esm/action/request/Write.js +70 -0
  66. package/dist/esm/action/request/Write.js.map +2 -2
  67. package/dist/esm/action/response/InvokeResult.d.ts +1 -2
  68. package/dist/esm/action/response/InvokeResult.d.ts.map +1 -1
  69. package/dist/esm/action/response/ReadResult.d.ts +5 -14
  70. package/dist/esm/action/response/ReadResult.d.ts.map +1 -1
  71. package/dist/esm/action/response/WriteResult.d.ts +19 -5
  72. package/dist/esm/action/response/WriteResult.d.ts.map +1 -1
  73. package/dist/esm/action/server/AccessControl.js +4 -4
  74. package/dist/esm/action/server/AccessControl.js.map +1 -1
  75. package/dist/esm/action/server/{AttributeResponse.d.ts → AttributeReadResponse.d.ts} +4 -4
  76. package/dist/esm/action/server/AttributeReadResponse.d.ts.map +1 -0
  77. package/dist/esm/action/server/{AttributeResponse.js → AttributeReadResponse.js} +9 -9
  78. package/dist/esm/action/server/{AttributeResponse.js.map → AttributeReadResponse.js.map} +2 -2
  79. package/dist/esm/action/server/AttributeSubscriptionResponse.d.ts +4 -4
  80. package/dist/esm/action/server/AttributeSubscriptionResponse.d.ts.map +1 -1
  81. package/dist/esm/action/server/AttributeSubscriptionResponse.js +2 -2
  82. package/dist/esm/action/server/AttributeSubscriptionResponse.js.map +1 -1
  83. package/dist/esm/action/server/AttributeWriteResponse.d.ts +28 -0
  84. package/dist/esm/action/server/AttributeWriteResponse.d.ts.map +1 -0
  85. package/dist/esm/action/server/AttributeWriteResponse.js +335 -0
  86. package/dist/esm/action/server/AttributeWriteResponse.js.map +6 -0
  87. package/dist/esm/action/server/DataResponse.d.ts +4 -3
  88. package/dist/esm/action/server/DataResponse.d.ts.map +1 -1
  89. package/dist/esm/action/server/DataResponse.js +1 -1
  90. package/dist/esm/action/server/DataResponse.js.map +1 -1
  91. package/dist/esm/action/server/{EventResponse.d.ts → EventReadResponse.d.ts} +4 -4
  92. package/dist/esm/action/server/EventReadResponse.d.ts.map +1 -0
  93. package/dist/esm/action/server/{EventResponse.js → EventReadResponse.js} +5 -5
  94. package/dist/esm/action/server/EventReadResponse.js.map +6 -0
  95. package/dist/esm/action/server/ServerInteraction.d.ts +3 -4
  96. package/dist/esm/action/server/ServerInteraction.d.ts.map +1 -1
  97. package/dist/esm/action/server/ServerInteraction.js +12 -10
  98. package/dist/esm/action/server/ServerInteraction.js.map +1 -1
  99. package/dist/esm/action/server/index.d.ts +3 -2
  100. package/dist/esm/action/server/index.d.ts.map +1 -1
  101. package/dist/esm/action/server/index.js +3 -2
  102. package/dist/esm/action/server/index.js.map +1 -1
  103. package/dist/esm/interaction/InteractionClient.d.ts.map +1 -1
  104. package/dist/esm/interaction/InteractionClient.js +12 -1
  105. package/dist/esm/interaction/InteractionClient.js.map +1 -1
  106. package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
  107. package/dist/esm/interaction/InteractionMessenger.js +8 -3
  108. package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
  109. package/package.json +6 -6
  110. package/src/action/Interactable.ts +6 -1
  111. package/src/action/client/ClientInteraction.ts +31 -23
  112. package/src/action/protocols.ts +8 -2
  113. package/src/action/request/Write.ts +127 -4
  114. package/src/action/response/InvokeResult.ts +1 -2
  115. package/src/action/response/ReadResult.ts +5 -22
  116. package/src/action/response/WriteResult.ts +21 -5
  117. package/src/action/server/AccessControl.ts +4 -4
  118. package/src/action/server/{AttributeResponse.ts → AttributeReadResponse.ts} +14 -13
  119. package/src/action/server/AttributeSubscriptionResponse.ts +5 -5
  120. package/src/action/server/AttributeWriteResponse.ts +437 -0
  121. package/src/action/server/DataResponse.ts +5 -4
  122. package/src/action/server/{EventResponse.ts → EventReadResponse.ts} +6 -5
  123. package/src/action/server/ServerInteraction.ts +16 -14
  124. package/src/action/server/index.ts +3 -2
  125. package/src/interaction/InteractionClient.ts +20 -1
  126. package/src/interaction/InteractionMessenger.ts +8 -3
  127. package/dist/cjs/action/server/AttributeResponse.d.ts.map +0 -1
  128. package/dist/cjs/action/server/EventResponse.d.ts.map +0 -1
  129. package/dist/cjs/action/server/EventResponse.js.map +0 -6
  130. package/dist/esm/action/server/AttributeResponse.d.ts.map +0 -1
  131. package/dist/esm/action/server/EventResponse.d.ts.map +0 -1
  132. package/dist/esm/action/server/EventResponse.js.map +0 -6
@@ -0,0 +1,437 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Project CHIP Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { InteractionSession } from "#action/Interactable.js";
8
+ import { AttributeTypeProtocol, ClusterProtocol, EndpointProtocol, NodeProtocol } from "#action/protocols.js";
9
+ import { Write } from "#action/request/Write.js";
10
+ import { WriteResult } from "#action/response/WriteResult.js";
11
+ import { AccessControl } from "#action/server/AccessControl.js";
12
+ import { DataResponse, FallbackLimits, WildcardPathFlagsCodec } from "#action/server/DataResponse.js";
13
+ import { Diagnostic, InternalError, Logger } from "#general";
14
+ import { AttributeModel, DataModelPath, ElementTag, FabricIndex as FabricIndexField } from "#model";
15
+ import {
16
+ ArraySchema,
17
+ AttributePath,
18
+ FabricIndex,
19
+ Status,
20
+ StatusCode,
21
+ StatusResponseError,
22
+ TlvSchema,
23
+ TlvStream,
24
+ } from "#types";
25
+
26
+ const logger = Logger.get("AttributeWriteResponse");
27
+
28
+ /**
29
+ * Implements read of attribute data for Matter "read" and "subscribe" interactions.
30
+ *
31
+ * TODO - profile; ensure nested functions are properly JITed and/or inlined
32
+ */
33
+ export class AttributeWriteResponse<
34
+ SessionT extends InteractionSession = InteractionSession,
35
+ > extends DataResponse<SessionT> {
36
+ #fabricIndex: FabricIndex;
37
+ // The following state updates as data producers execute. This serves both to convey state between functions and as
38
+ // a cache between producers that touch the same endpoint and/or cluster
39
+ #currentEndpoint?: EndpointProtocol;
40
+ #currentCluster?: ClusterProtocol;
41
+ #previousProcessedAttributePath?: WriteResult.ConcreteAttributePath;
42
+ #wildcardPathFlags = 0;
43
+
44
+ // Count how many attribute status (on error) and attribute values (on success) we have emitted
45
+ #statusCount = 0;
46
+ #successCount = 0;
47
+ #errorCount = 0;
48
+
49
+ constructor(node: NodeProtocol, session: SessionT) {
50
+ super(node, session);
51
+ this.#fabricIndex = session.fabric ?? FabricIndex.NO_FABRIC;
52
+ }
53
+
54
+ async process<T extends Write>({ writeRequests, suppressResponse }: T): WriteResult<T> {
55
+ const writeResponses = new Array<WriteResult.AttributeStatus>();
56
+ for (const { path, data, dataVersion } of writeRequests) {
57
+ if (path.endpointId === undefined || path.clusterId === undefined || path.attributeId === undefined) {
58
+ // dataVersion silently ignored for Wildcard?
59
+ const responses = await this.#processWildcard(path, data);
60
+ if (responses !== undefined) {
61
+ writeResponses.push(...responses);
62
+ }
63
+ } else {
64
+ writeResponses.push(
65
+ await this.#writeConcrete(path as WriteResult.ConcreteAttributePath, data, dataVersion),
66
+ );
67
+ }
68
+ }
69
+
70
+ if (!suppressResponse) {
71
+ return writeResponses as Awaited<WriteResult<T>>;
72
+ }
73
+ return undefined as Awaited<WriteResult<T>>;
74
+ }
75
+
76
+ /** Guarded accessor for this.#currentEndpoint. This should never be undefined */
77
+ get #guardedCurrentEndpoint() {
78
+ if (this.#currentEndpoint === undefined) {
79
+ throw new InternalError("currentEndpoint is not set. Should never happen");
80
+ }
81
+ return this.#currentEndpoint;
82
+ }
83
+
84
+ /** Guarded accessor for this.#currentCluster. This should never be undefined */
85
+ get #guardedCurrentCluster(): ClusterProtocol {
86
+ if (this.#currentCluster === undefined) {
87
+ throw new InternalError("currentCluster is not set. Should never happen");
88
+ }
89
+ return this.#currentCluster;
90
+ }
91
+
92
+ get counts() {
93
+ return {
94
+ status: this.#statusCount,
95
+ success: this.#successCount,
96
+ existent: this.#successCount + this.#errorCount,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Validate a wildcard path and update internal state.
102
+ */
103
+ async #processWildcard(path: AttributePath, value: TlvStream) {
104
+ const { nodeId, endpointId, wildcardPathFlags } = path;
105
+
106
+ if (nodeId !== undefined && nodeId !== this.nodeId) {
107
+ return;
108
+ }
109
+
110
+ // TODO clarify isf that's needed or not
111
+ this.#wildcardPathFlags = wildcardPathFlags ? WildcardPathFlagsCodec.encode(wildcardPathFlags) : 0;
112
+
113
+ // TODO: Add Group handling and validation
114
+ /*
115
+ if (isGroupSession && endpointId !== undefined) {
116
+ throw new StatusResponseError("Illegal write request with group ID and endpoint ID", StatusCode.InvalidAction);
117
+ }
118
+ */
119
+
120
+ if (endpointId === undefined) {
121
+ const responses = new Array<WriteResult.AttributeStatus>();
122
+ for (const endpoint of this.node) {
123
+ const response = await this.#writeEndpointForWildcard(endpoint, path, value);
124
+ if (response !== undefined) {
125
+ responses.push(response);
126
+ }
127
+ }
128
+ return responses;
129
+ }
130
+
131
+ const endpoint = this.node[endpointId];
132
+ if (endpoint) {
133
+ const response = await this.#writeEndpointForWildcard(endpoint, path, value);
134
+ if (response !== undefined) {
135
+ return [response];
136
+ }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Write to a concrete path and update internal state.
142
+ */
143
+ async #writeConcrete(path: WriteResult.ConcreteAttributePath, value: TlvStream, version?: number) {
144
+ const { nodeId, endpointId, clusterId, attributeId } = path;
145
+
146
+ if (nodeId !== undefined && this.nodeId !== nodeId) {
147
+ return this.#asStatus(path, Status.UnsupportedNode);
148
+ }
149
+
150
+ // Resolve path elements
151
+ const endpoint = this.node[endpointId];
152
+ const cluster = endpoint?.[clusterId];
153
+ const attribute = cluster?.type.attributes[attributeId];
154
+ let limits;
155
+ if (attribute === undefined) {
156
+ // We still need to authorize the user for access even though this path doesn't resolve. Spec is not
157
+ // explicit on what privilege level we should require as normally that information comes from the resolved
158
+ // attribute. So attempt to resolve via the active model
159
+ const modelAttr = this.node.matter
160
+ .member(path.clusterId, [ElementTag.Cluster])
161
+ ?.member(path.attributeId, [ElementTag.Attribute]);
162
+
163
+ if (modelAttr) {
164
+ // OK cluster doesn't exist at that location, but we do understand semantically, so use limits from the
165
+ // model
166
+ limits = AccessControl(modelAttr as AttributeModel).limits;
167
+ } else {
168
+ // We've got no idea. This effectively falls back to "view" privilege
169
+ limits = FallbackLimits;
170
+ }
171
+ } else {
172
+ limits = attribute.limits;
173
+ }
174
+
175
+ // Validate access. Order here prescribed by 1.4 core spec 8.4.3.2
176
+ // We need some fallback location if cluster is not defined
177
+ const location = {
178
+ ...(cluster?.location ?? {
179
+ path: DataModelPath.none,
180
+ endpoint: endpointId,
181
+ cluster: clusterId,
182
+ }),
183
+ owningFabric: this.session.fabric,
184
+ };
185
+
186
+ const permission = this.session.authorityAt(limits.writeLevel, location);
187
+ switch (permission) {
188
+ case AccessControl.Authority.Granted:
189
+ break;
190
+
191
+ case AccessControl.Authority.Unauthorized:
192
+ return this.#asStatus(path, Status.UnsupportedAccess);
193
+
194
+ case AccessControl.Authority.Restricted:
195
+ return this.#asStatus(path, Status.AccessRestricted);
196
+
197
+ default:
198
+ throw new InternalError(`Unsupported authorization state ${permission}`);
199
+ }
200
+
201
+ if (endpoint === undefined) {
202
+ return this.#asStatus(path, Status.UnsupportedEndpoint);
203
+ }
204
+ if (cluster === undefined) {
205
+ return this.#asStatus(path, Status.UnsupportedCluster);
206
+ }
207
+ if (attribute === undefined || !cluster.type.attributes[attribute.id]) {
208
+ return this.#asStatus(path, Status.UnsupportedAttribute);
209
+ }
210
+
211
+ if (!limits.writable) {
212
+ this.#errorCount++;
213
+ return this.#asStatus(path, Status.UnsupportedWrite);
214
+ }
215
+
216
+ // Old implementation aka Matter 1.2 and lower need the ACL check moved here.
217
+ // see https://github.com/project-chip/connectedhomeip/issues/33735
218
+ // We have patched our tests for now
219
+
220
+ if (limits.timed && !this.session.timed) {
221
+ this.#errorCount++;
222
+ return this.#asStatus(path, Status.NeedsTimedInteraction);
223
+ }
224
+ if (limits.fabricScoped && this.session.fabric === undefined) {
225
+ this.#errorCount++;
226
+ return this.#asStatus(path, Status.UnsupportedAccess);
227
+ }
228
+
229
+ if (version !== undefined && version !== cluster.version) {
230
+ this.#errorCount++;
231
+ return this.#asStatus(path, Status.DataVersionMismatch);
232
+ }
233
+
234
+ // Update internal state for target endpoint
235
+ if (this.#currentEndpoint !== endpoint) {
236
+ this.#currentEndpoint = endpoint;
237
+ this.#currentCluster = cluster;
238
+ } else if (this.#currentCluster !== cluster) {
239
+ this.#currentCluster = cluster;
240
+ }
241
+
242
+ return await this.writeValue(attribute, path, value);
243
+ }
244
+
245
+ /**
246
+ * Starts new chunk or adds to current chunk all values from {@link endpoint} selected by {@link path}.
247
+ *
248
+ * Emits previous chunk if it exists and was not for this endpoint. This means that our chunk size is one endpoint
249
+ * worth of data, except for the initial error chunk if there are path errors.
250
+ *
251
+ * {@link this.#wildcardPathFlags} to numeric bitmap must be set prior to invocation.
252
+ *
253
+ * TODO - skip endpoints for which subject is unauthorized as optimization
254
+ */
255
+ #writeEndpointForWildcard(endpoint: EndpointProtocol, path: AttributePath, value: TlvStream) {
256
+ const { clusterId, attributeId } = path;
257
+ if (clusterId === undefined || attributeId === undefined) {
258
+ throw new StatusResponseError(
259
+ "Wildcard path write must specify a clusterId and attributeId",
260
+ StatusCode.InvalidAction,
261
+ );
262
+ }
263
+
264
+ if (endpoint.wildcardPathFlags & this.#wildcardPathFlags) {
265
+ return; // TODO ???
266
+ }
267
+
268
+ if (this.#currentEndpoint !== endpoint) {
269
+ this.#currentEndpoint = endpoint;
270
+ this.#currentCluster = undefined;
271
+ }
272
+
273
+ const cluster = endpoint[clusterId];
274
+ if (cluster !== undefined) {
275
+ return this.#writeClusterForWildcard(cluster, path, value);
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Read values from a specific {@link cluster} for a wildcard path.
281
+ *
282
+ * Depends on state initialized by {@link #writeEndpointForWildcard}.
283
+ *
284
+ * TODO - skip clusters for which subject is unauthorized
285
+ */
286
+ #writeClusterForWildcard(cluster: ClusterProtocol, path: AttributePath, value: TlvStream) {
287
+ if (cluster.type.wildcardPathFlags & this.#wildcardPathFlags) {
288
+ return; // TODO ???
289
+ }
290
+
291
+ if (this.#currentCluster !== cluster) {
292
+ this.#currentCluster = cluster;
293
+ }
294
+ const { attributeId } = path;
295
+
296
+ if (attributeId === undefined) {
297
+ throw new StatusResponseError("Wildcard path write must specify an attributeId", StatusCode.InvalidAction);
298
+ } else {
299
+ const attribute = cluster.type.attributes[attributeId];
300
+ if (attribute !== undefined) {
301
+ return this.#writeAttributeForWildcard(attribute, path, value);
302
+ }
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Read values from a specific {@link attribute} for a wildcard path.
308
+ *
309
+ * Depends on state initialized by {@link #writeClusterForWildcard}.
310
+ */
311
+ #writeAttributeForWildcard(attribute: AttributeTypeProtocol, path: AttributePath, value: TlvStream) {
312
+ if (!this.#guardedCurrentCluster.type.attributes[attribute.id]) {
313
+ return;
314
+ }
315
+
316
+ if (attribute.wildcardPathFlags & this.#wildcardPathFlags) {
317
+ return; // TODO ????
318
+ }
319
+
320
+ if (
321
+ !attribute.limits.writable ||
322
+ this.session.authorityAt(attribute.limits.readLevel, this.#guardedCurrentCluster.location) !==
323
+ AccessControl.Authority.Granted ||
324
+ (attribute.limits.timed && !this.session.timed)
325
+ ) {
326
+ return;
327
+ }
328
+
329
+ return this.writeValue(
330
+ attribute,
331
+ {
332
+ ...path,
333
+ endpointId: this.#guardedCurrentEndpoint.id,
334
+ clusterId: this.#guardedCurrentCluster.type.id,
335
+ attributeId: attribute.id,
336
+ },
337
+ value,
338
+ );
339
+ }
340
+
341
+ /**
342
+ * Add a status value.
343
+ */
344
+ #asStatus(path: WriteResult.ConcreteAttributePath, status: Status, clusterStatus?: number) {
345
+ if (status !== Status.Success) {
346
+ logger.debug(
347
+ () =>
348
+ `Error writing attribute ${this.node.inspectPath(path)}: Status=${StatusCode[status]}(${status}), ClusterStatus=${clusterStatus}`,
349
+ );
350
+ }
351
+
352
+ const report: WriteResult.AttributeStatus = {
353
+ kind: "attr-status",
354
+ path,
355
+ status,
356
+ clusterStatus,
357
+ };
358
+
359
+ if (status !== Status.Success) {
360
+ this.#statusCount++;
361
+ }
362
+ return report;
363
+ }
364
+
365
+ protected async writeValue(
366
+ attribute: AttributeTypeProtocol,
367
+ path: WriteResult.ConcreteAttributePath,
368
+ value: TlvStream,
369
+ ) {
370
+ const { attributeId, listIndex } = path;
371
+
372
+ if (listIndex !== undefined && listIndex !== null) {
373
+ throw new StatusResponseError(
374
+ `Unsupported Write path provided: listIndex === ${listIndex}`,
375
+ Status.InvalidAction,
376
+ );
377
+ }
378
+
379
+ const previousPath = this.#previousProcessedAttributePath;
380
+ this.#previousProcessedAttributePath = path;
381
+
382
+ try {
383
+ const { tlv } = attribute;
384
+ if (listIndex === undefined) {
385
+ const decoded = this.#decodeWithSchema(tlv, value);
386
+ // REPLACE_ALL
387
+ //await this.session.transaction?.begin();
388
+ logger.debug(() => `Writing attribute ${this.node.inspectPath(path)}=${Diagnostic.json(decoded)}`);
389
+ const writeState = await this.#guardedCurrentCluster.openForWrite(this.session);
390
+ writeState[attributeId] = decoded;
391
+ await this.session.transaction?.commit();
392
+ } else if (listIndex === null) {
393
+ if (
394
+ previousPath?.endpointId !== path.endpointId ||
395
+ previousPath?.clusterId !== path.clusterId ||
396
+ previousPath?.attributeId !== path.attributeId
397
+ ) {
398
+ // Mimic chip sdk behavior
399
+ throw new StatusResponseError("ADD list action without a former REPLACE_ALL action", Status.Busy);
400
+ }
401
+ // ADD
402
+ if (!(tlv instanceof ArraySchema)) {
403
+ throw new StatusResponseError(
404
+ `Unsupported Write path provided: listIndex === ${listIndex} but attribute is not a list`,
405
+ Status.InvalidAction,
406
+ );
407
+ }
408
+ const writeState = await this.#guardedCurrentCluster.openForWrite(this.session);
409
+ const decoded = this.#decodeWithSchema(tlv.elementSchema, value);
410
+ logger.debug(
411
+ () => `Writing attribute chunk ${this.node.inspectPath(path)} adding ${Diagnostic.json(decoded)}`,
412
+ );
413
+ (writeState[attributeId] as any[]).push(decoded);
414
+ await this.session.transaction?.commit();
415
+ }
416
+ } catch (error) {
417
+ await this.session.transaction?.rollback();
418
+ if (StatusResponseError.is(error)) {
419
+ this.#errorCount++;
420
+ return this.#asStatus(path, error.code, error.clusterCode);
421
+ }
422
+ throw error;
423
+ }
424
+
425
+ this.#successCount++;
426
+ return this.#asStatus(path, Status.Success);
427
+ }
428
+
429
+ #decodeWithSchema(tlv: TlvSchema<any>, value: TlvStream) {
430
+ return tlv.injectField(
431
+ tlv.decodeTlv(value),
432
+ <number>FabricIndexField.id,
433
+ this.#fabricIndex,
434
+ () => true, // We always inject the current fabricIndex for writes
435
+ );
436
+ }
437
+ }
@@ -3,6 +3,7 @@
3
3
  * Copyright 2022-2025 Project CHIP Authors
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
+ import { InteractionSession } from "#action/index.js";
6
7
  import { NodeProtocol } from "#action/protocols.js";
7
8
  import { AccessControl } from "#action/server/AccessControl.js";
8
9
  import { AccessLevel } from "#model";
@@ -19,12 +20,12 @@ export const FallbackLimits: AccessControl.Limits = {
19
20
  writeLevel: AccessLevel.Administer,
20
21
  };
21
22
 
22
- export abstract class DataResponse<SessionT extends AccessControl.Session = AccessControl.Session> {
23
+ export abstract class DataResponse<SessionT extends InteractionSession = InteractionSession> {
23
24
  // Configuration
24
25
  #session: SessionT;
25
26
  #node: NodeProtocol;
26
27
 
27
- // The node ID may be expensive to retrieve and is invariant so we cache it here
28
+ // The node ID may be expensive to retrieve and is invariant, so we cache it here
28
29
  #cachedNodeId?: NodeId;
29
30
 
30
31
  constructor(node: NodeProtocol, session: SessionT) {
@@ -63,8 +64,8 @@ export abstract class DataResponse<SessionT extends AccessControl.Session = Acce
63
64
  status: number;
64
65
 
65
66
  /**
66
- * Number of value responses (aka success we have sent)
67
+ * Number of success or value responses (aka success we have sent)
67
68
  */
68
- value: number;
69
+ success: number;
69
70
  };
70
71
  }
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import { InteractionSession } from "#action/index.js";
7
8
  import { ClusterProtocol, EndpointProtocol, EventTypeProtocol, NodeProtocol } from "#action/protocols.js";
8
9
  import { Read } from "#action/request/Read.js";
9
10
  import { ReadResult } from "#action/response/ReadResult.js";
@@ -23,7 +24,7 @@ import {
23
24
  TlvSchema,
24
25
  } from "#types";
25
26
 
26
- const logger = Logger.get("EventResponse");
27
+ const logger = Logger.get("EventReadResponse");
27
28
 
28
29
  /**
29
30
  * Implements read of event data for Matter "read" and "subscribe" interactions.
@@ -32,8 +33,8 @@ const logger = Logger.get("EventResponse");
32
33
  *
33
34
  * TODO - profile; ensure nested functions are properly JITed and/or inlined
34
35
  */
35
- export class EventResponse<
36
- SessionT extends AccessControl.Session = AccessControl.Session,
36
+ export class EventReadResponse<
37
+ SessionT extends InteractionSession = InteractionSession,
37
38
  > extends DataResponse<SessionT> {
38
39
  // Normalized Event Filter to just our node-id
39
40
  #eventMinVersion?: EventNumber;
@@ -102,7 +103,7 @@ export class EventResponse<
102
103
  get counts() {
103
104
  return {
104
105
  status: this.#statusCount,
105
- value: this.#valueCount,
106
+ success: this.#valueCount,
106
107
  existent: this.#allowedEventPaths.size,
107
108
  };
108
109
  }
@@ -352,7 +353,7 @@ export class EventResponse<
352
353
  #asStatus(path: ReadResult.ConcreteEventPath, status: Status) {
353
354
  logger.debug(`Error reading event ${this.node.inspectPath(path)}: Status=${StatusCode[status]}(${status})`);
354
355
 
355
- const report: ReadResult.GlobalEventStatus = {
356
+ const report: ReadResult.EventStatus = {
356
357
  kind: "event-status",
357
358
  path,
358
359
  status,
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { Interactable } from "#action/Interactable.js";
7
+ import { Interactable, InteractionSession } from "#action/Interactable.js";
8
8
  import { NodeProtocol } from "#action/protocols.js";
9
9
  import { Invoke } from "#action/request/Invoke.js";
10
10
  import { Read } from "#action/request/Read.js";
@@ -14,10 +14,10 @@ import { InvokeResult } from "#action/response/InvokeResult.js";
14
14
  import { ReadResult } from "#action/response/ReadResult.js";
15
15
  import { SubscribeResult } from "#action/response/SubscribeResult.js";
16
16
  import { WriteResult } from "#action/response/WriteResult.js";
17
- import { AccessControl } from "#action/server/AccessControl.js";
18
- import { EventResponse } from "#action/server/EventResponse.js";
17
+ import { EventReadResponse } from "#action/server/EventReadResponse.js";
19
18
  import { Logger, NotImplementedError } from "#general";
20
- import { AttributeResponse } from "./AttributeResponse.js";
19
+ import { AttributeReadResponse } from "./AttributeReadResponse.js";
20
+ import { AttributeWriteResponse } from "./AttributeWriteResponse.js";
21
21
 
22
22
  const logger = Logger.get("ServerInteraction");
23
23
 
@@ -31,7 +31,7 @@ const logger = Logger.get("ServerInteraction");
31
31
  *
32
32
  * - InteractionEndpointStructure ({@link NodeProtocol} is largely duplicative)
33
33
  */
34
- export class ServerInteraction<SessionT extends AccessControl.Session = AccessControl.Session>
34
+ export class ServerInteraction<SessionT extends InteractionSession = InteractionSession>
35
35
  implements Interactable<SessionT>
36
36
  {
37
37
  #node: NodeProtocol;
@@ -45,18 +45,18 @@ export class ServerInteraction<SessionT extends AccessControl.Session = AccessCo
45
45
 
46
46
  let readInfo = "";
47
47
  if (Read.containsAttribute(request)) {
48
- const attributeReader = new AttributeResponse(this.#node, session);
48
+ const attributeReader = new AttributeReadResponse(this.#node, session);
49
49
  yield* attributeReader.process(request);
50
50
 
51
- const { existent, status, value } = attributeReader.counts;
52
- readInfo = `${existent} matching attributes (${status ? `${status} status, ` : ""}${value ? `${value} values` : ""})`;
51
+ const { existent, status, success } = attributeReader.counts;
52
+ readInfo = `${existent} matching attributes (${status ? `${status} status, ` : ""}${success ? `${success} values` : ""})`;
53
53
  }
54
54
 
55
55
  if (Read.containsEvent(request)) {
56
- const eventReader = new EventResponse(this.#node, session);
56
+ const eventReader = new EventReadResponse(this.#node, session);
57
57
  yield* eventReader.process(request);
58
- const { existent, status, value } = eventReader.counts;
59
- readInfo += `${readInfo.length > 0 ? ", " : ""}${existent} matching events (${status ? `${status} status, ` : ""}${value ? `${value} values` : ""})`;
58
+ const { existent, status, success } = eventReader.counts;
59
+ readInfo += `${readInfo.length > 0 ? ", " : ""}${existent} matching events (${status ? `${status} status, ` : ""}${success ? `${success} values` : ""})`;
60
60
  }
61
61
  logger.debug(`Read request resolved to ${readInfo}`);
62
62
  }
@@ -66,9 +66,11 @@ export class ServerInteraction<SessionT extends AccessControl.Session = AccessCo
66
66
  throw new NotImplementedError();
67
67
  }
68
68
 
69
- write<T extends Write>(_request: T, _session?: SessionT): WriteResult<T> {
70
- // TODO
71
- throw new NotImplementedError();
69
+ write<T extends Write>(request: T, session: SessionT): WriteResult<T> {
70
+ // TODO - validate request
71
+
72
+ const writer = new AttributeWriteResponse(this.#node, session);
73
+ return writer.process(request);
72
74
  }
73
75
 
74
76
  invoke<T extends Invoke>(_request: T, _session?: SessionT): InvokeResult<T> {
@@ -5,8 +5,9 @@
5
5
  */
6
6
 
7
7
  export * from "./AccessControl.js";
8
- export * from "./AttributeResponse.js";
8
+ export * from "./AttributeReadResponse.js";
9
9
  export * from "./AttributeSubscriptionResponse.js";
10
+ export * from "./AttributeWriteResponse.js";
10
11
  export * from "./DataResponse.js";
11
- export * from "./EventResponse.js";
12
+ export * from "./EventReadResponse.js";
12
13
  export * from "./ServerInteraction.js";
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import { AccessControl } from "#clusters/access-control";
7
8
  import {
8
9
  Diagnostic,
9
10
  Environment,
@@ -63,6 +64,15 @@ const REQUEST_ALL = [{}];
63
64
  const DEFAULT_TIMED_REQUEST_TIMEOUT_MS = 10_000; // 10 seconds
64
65
  const DEFAULT_MINIMUM_RESPONSE_TIMEOUT_WITH_FAILSAFE_MS = 30_000; // 30 seconds
65
66
 
67
+ const AclClusterId = AccessControl.Complete.id;
68
+ const AclAttributeId = AccessControl.Complete.attributes.acl.id;
69
+ const AclExtensionAttributeId = AccessControl.Complete.attributes.extension.id;
70
+
71
+ function isAclOrExtensionPath(path: { clusterId: ClusterId; attributeId: AttributeId }) {
72
+ const { clusterId, attributeId } = path;
73
+ return clusterId === AclClusterId && (attributeId === AclAttributeId || attributeId === AclExtensionAttributeId);
74
+ }
75
+
66
76
  export interface AttributeStatus {
67
77
  path: {
68
78
  nodeId?: NodeId;
@@ -554,9 +564,18 @@ export class InteractionClient {
554
564
  )
555
565
  .join(", ")}`,
556
566
  );
567
+ // TODO Add multi message write handling with streamed encoding
557
568
  const writeRequests = attributes.flatMap(
558
569
  ({ endpointId, clusterId, attribute: { id, schema }, value, dataVersion }) => {
559
- if (chunkLists && Array.isArray(value) && schema instanceof ArraySchema) {
570
+ if (
571
+ chunkLists &&
572
+ Array.isArray(value) &&
573
+ schema instanceof ArraySchema &&
574
+ // As implemented for Matter 1.4.2 in https://github.com/project-chip/connectedhomeip/pull/38263
575
+ // Acl writes will no longer be chunked by default, all others still
576
+ // Will be streamlined later ... see https://github.com/project-chip/connectedhomeip/issues/38270
577
+ !isAclOrExtensionPath({ clusterId, attributeId: id })
578
+ ) {
560
579
  return schema
561
580
  .encodeAsChunkedArray(value, { forWriteInteraction: true })
562
581
  .map(({ element: data, listIndex }) => ({
@@ -712,7 +712,7 @@ export class InteractionServerMessenger extends InteractionMessenger {
712
712
  return data;
713
713
  }
714
714
  case "attr-status": {
715
- const { path, status } = report;
715
+ const { path, status, clusterStatus } = report;
716
716
  const statusReport: AttributeReportPayload = {
717
717
  attributeStatus: {
718
718
  path,
@@ -720,6 +720,9 @@ export class InteractionServerMessenger extends InteractionMessenger {
720
720
  },
721
721
  hasFabricSensitiveData: false,
722
722
  };
723
+ if (clusterStatus !== undefined) {
724
+ statusReport.attributeStatus!.status.clusterStatus = clusterStatus;
725
+ }
723
726
  return statusReport;
724
727
  }
725
728
  case "event-value": {
@@ -745,7 +748,7 @@ export class InteractionServerMessenger extends InteractionMessenger {
745
748
  return data;
746
749
  }
747
750
  case "event-status": {
748
- const { path, status } = report;
751
+ const { path, status, clusterStatus } = report;
749
752
  const statusReport: EventReportPayload = {
750
753
  eventStatus: {
751
754
  path,
@@ -753,10 +756,12 @@ export class InteractionServerMessenger extends InteractionMessenger {
753
756
  },
754
757
  hasFabricSensitiveData: false,
755
758
  };
759
+ if (clusterStatus !== undefined) {
760
+ statusReport.eventStatus!.status.clusterStatus = clusterStatus;
761
+ }
756
762
  return statusReport;
757
763
  }
758
764
  }
759
- throw new InternalError(`Unknown report type: ${report.kind}`);
760
765
  }
761
766
  }
762
767