/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { ActionContext } from "#behavior/context/ActionContext.js"; import { OnlineEvent } from "#behavior/Events.js"; import { NodeLifecycle } from "#node/NodeLifecycle.js"; import { Bytes, deepCopy, Diagnostic, InternalError, isDeepEqual, Logger, ObservableValue } from "@matter/general"; import { AccessControl, AclEndpointContext, AclEntry, AclList, Fabric, FabricManager, hasLocalActor, hasRemoteActor, IncomingSubjectDescriptor, MessageExchange, NodeSession, SecureSession, } from "@matter/protocol"; import { CaseAuthenticatedTag, ClusterId, DeviceTypeId, EndpointNumber, FabricIndex, GroupId, NodeId, Status, StatusResponseError, SubjectId, TlvTaggedList, TlvType, } from "@matter/types"; import { AccessControl as AccessControlTypes } from "@matter/types/clusters/access-control"; import { AccessControlBehavior } from "./AccessControlBehavior.js"; const logger = Logger.get("AccessControlServer"); const AccessControlBase = AccessControlBehavior.with("Extension"); /** * This is the default server implementation of AccessControlBehavior. * * When custom extensions are used, the `extensionEntryValidator` and `extensionEntryAccessCheck` methods can be * overridden to implement custom validation and access checks for the extension entries. */ export class AccessControlServer extends AccessControlBase { declare internal: AccessControlServer.Internal; declare readonly state: AccessControlServer.State; declare readonly events: AccessControlServer.Events; override initialize() { // Spec 1.5.1 tightened constraints to "4 to 65534" / "3 to 65534" — ensure valid defaults if (!this.state.subjectsPerAccessControlEntry) { this.state.subjectsPerAccessControlEntry = 4; } if (!this.state.targetsPerAccessControlEntry) { this.state.targetsPerAccessControlEntry = 4; } if (!this.state.accessControlEntriesPerFabric) { this.state.accessControlEntriesPerFabric = 4; } this.internal.auxiliaryEntryLimits = { subjects: this.state.subjectsPerAccessControlEntry, targets: this.state.targetsPerAccessControlEntry, }; this.reactTo(this.events.acl$Changing, this.#validateAccessControlListChanges); // Enhanced Validation this.reactTo(this.events.acl$Changed, this.#handleAccessControlListChange); // Event handling for changes if (this.state.extension !== undefined) { this.maybeReactTo(this.events.extension$Changing, this.#validateAccessControlExtensionChanges); // Enhanced Validation this.maybeReactTo(this.events.extension$Changed, this.#handleAccessControlExtensionChange); // Event handling for changes } const lifecycle = this.endpoint.lifecycle as NodeLifecycle; this.reactTo(lifecycle.online, this.#online); } #online() { const aclsForFabric = this.#mapFabricAcls(); // Initialize the ACL managers for each fabric const fabrics = this.env.get(FabricManager); for (const fabric of fabrics) { const fabricAcls = aclsForFabric.get(fabric.fabricIndex) ?? []; if (!fabricAcls.length) { // Handle Backward compatibility to Matter.js before 0.9.1 and add the missing ACL entry if no entry was set // so far by the controller const fallbackAcl: AccessControlTypes.AccessControlEntry = { privilege: AccessControlTypes.AccessControlEntryPrivilege.Administer, authMode: AccessControlTypes.AccessControlEntryAuthMode.Case, subjects: [fabric.rootNodeId], targets: null, // entire node fabricIndex: fabric.fabricIndex, }; this.state.acl.push(fallbackAcl); fabricAcls.push(fallbackAcl); } this.#applyFabricAcl(fabric, fabricAcls); fabric.accessControl.extensionEntryAccessCheck = this.extensionEntryAccessCheck.bind(this); } // TODO handle delete fabric more generically later to remove fabric scoped data this.reactTo(fabrics.events.added, this.#updateFabricAcls); this.reactTo(this.events.interactionBegin, this.#handleInteractionBegin); this.reactTo(this.events.interactionEnd, this.#handleInteractionEnd); this.reactTo(this.events.acl$Changed, this.#updateAccessControlList); this.internal.initialized = true; } addDefaultCaseAcl(fabric: Fabric, subjects: SubjectId[]) { const entry = { fabricIndex: fabric.fabricIndex, privilege: AccessControlTypes.AccessControlEntryPrivilege.Administer, authMode: AccessControlTypes.AccessControlEntryAuthMode.Case, subjects: subjects as NodeId[], targets: null, // entire node }; this.state.acl.push(entry); this.#updateFabricAcls(fabric); // The update of the ACL is not validated because it has no assigned sess/fabric (and even then would use // the wrong one), so we trigger the event ourself. this.events.accessControlEntryChanged?.emit( { changeType: AccessControlTypes.ChangeType.Added, adminNodeId: null, // When we add it, it is always from a PASE session adminPasscodeId: 0, // When we add it, it is always from a PASE session latestValue: entry, fabricIndex: fabric.fabricIndex, }, this.context, ); } #validateAccessControlListChanges( value: AccessControlTypes.AccessControlEntry[], _oldValue: AccessControlTypes.AccessControlEntry[], ) { const { context } = this; if (value.some(entry => entry.auxiliaryType !== undefined)) { // The spec forbids the field here without naming a status; CHIP answers FAILURE throw new StatusResponseError("ACL entries must not include AuxiliaryType", Status.Failure); } if (!hasRemoteActor(context)) { return; } const relevantFabricIndex = context.session.associatedFabric.fabricIndex; if (relevantFabricIndex === undefined) { return; } if (context !== undefined && context.exchange !== undefined) { const delayedChangeExchange = this.internal.aclUpdateDelayed.get(relevantFabricIndex); if (delayedChangeExchange !== undefined && delayedChangeExchange !== context.exchange) { // We are in a delayed ACL update with another exchange, so we do not process this one with Busy error // This is formally not in specification, but chip also does this that way logger.debug( "Decline parallel ACL changes from multiple exchanges", context.exchange.id, "vs.", delayedChangeExchange.id, ); throw new StatusResponseError("Parallel ACL change from multiple exchanges", Status.Busy); } } const fabricAcls = value.filter(entry => entry.fabricIndex === relevantFabricIndex); if (fabricAcls.length > this.state.accessControlEntriesPerFabric) { throw new StatusResponseError("AccessControlEntriesPerFabric exceeded", Status.ResourceExhausted); } for (const entry of fabricAcls) { // We need to convert empty arrays to null because expected by chip tests if (entry.subjects !== null && entry.subjects.length === 0) { entry.subjects = null; } if (entry.targets !== null && entry.targets.length === 0) { entry.targets = null; } const { privilege, subjects, targets, authMode } = entry; if (subjects !== null && subjects.length > this.state.subjectsPerAccessControlEntry) { throw new StatusResponseError("SubjectsPerAccessControlEntry exceeded", Status.ResourceExhausted); } if (targets !== null && targets.length > this.state.targetsPerAccessControlEntry) { throw new StatusResponseError("TargetsPerAccessControlEntry exceeded", Status.ResourceExhausted); } if (authMode === AccessControlTypes.AccessControlEntryAuthMode.Pase) { throw new StatusResponseError("AuthMode for ACL must not be PASE", Status.ConstraintError); } else if (authMode === AccessControlTypes.AccessControlEntryAuthMode.Case) { if (subjects !== null) { for (const subject of subjects) { if (NodeId.isCaseAuthenticatedTag(subject)) { const cat = NodeId.extractAsCaseAuthenticatedTag(subject); if (CaseAuthenticatedTag.getVersion(cat) === 0) { throw new StatusResponseError( "CaseAuthenticatedTag version 0 is not allowed", Status.ConstraintError, ); } } else if (!NodeId.isOperationalNodeId(subject)) { throw new StatusResponseError( "Subject must be a valid OperationalNodeId or CaseAuthenticatedTag", Status.ConstraintError, ); } } } } else if (authMode === AccessControlTypes.AccessControlEntryAuthMode.Group) { if (privilege === AccessControlTypes.AccessControlEntryPrivilege.Administer) { throw new StatusResponseError( "Group ACLs must not have Administer privilege", Status.ConstraintError, ); } if (subjects !== null) { for (const subject of subjects) { if (GroupId(Number(subject)) === GroupId.NO_GROUP_ID) { throw new StatusResponseError( "Subject must be a valid GroupId for Group ACLs", Status.ConstraintError, ); } } } // TODO For Group authentication, the Group ID identifies the required group, as defined in the Group Key Management Cluster. } if (targets !== null) { for (const target of targets) { const { cluster, endpoint, deviceType } = target; if (deviceType !== null && endpoint !== null) { throw new StatusResponseError( "DeviceType and Endpoint are mutually exclusive", Status.ConstraintError, ); } if (cluster === null && endpoint === null && deviceType === null) { throw new StatusResponseError("At least one field must be present", Status.ConstraintError); } if (cluster !== null && !ClusterId.isValid(cluster)) { throw new StatusResponseError("Cluster must be a valid ClusterId", Status.ConstraintError); } if (endpoint !== null && !EndpointNumber.isValid(endpoint)) { throw new StatusResponseError( "Endpoint must be a valid OperationalNodeId", Status.ConstraintError, ); } if (deviceType !== null && !DeviceTypeId.isValid(deviceType)) { throw new StatusResponseError("DeviceType must be a valid DeviceType", Status.ConstraintError); } } } } } /** * Determine and return the adminPassCodeId and adminNodeId from the node that initiated the ACL change. * it is either the root node of the fabric or 0 if the session is undefined or a PASE session. */ #adminDataFromSession(session: SecureSession | undefined) { if (session === undefined || (NodeSession.is(session) && session.isPase)) { return { adminPasscodeId: 0, adminNodeId: null }; } const adminNodeId = session?.associatedFabric.rootNodeId; if (adminNodeId === undefined) { throw new InternalError("Admin Node ID is undefined, should never happen"); } return { adminPasscodeId: null, adminNodeId }; } #handleAccessControlListChange( value: AccessControlTypes.AccessControlEntry[], oldValue: AccessControlTypes.AccessControlEntry[], ) { if (!this.internal.initialized) { return; // Too early to send events } const session = hasRemoteActor(this.context) ? this.context.session : undefined; // TODO: This might be not really correct for local ACL changes because there the session fabric could be // different which would lead to missing events const relevantFabricIndex = session?.associatedFabric.fabricIndex; if (relevantFabricIndex === undefined || this.events.accessControlEntryChanged === undefined) { return; } const { adminPasscodeId, adminNodeId } = this.#adminDataFromSession(session); const fabricAcls = value.filter(entry => entry.fabricIndex === relevantFabricIndex); const oldFabricAcls = oldValue.filter(entry => entry.fabricIndex === relevantFabricIndex); let i = 0; for (; i < fabricAcls.length; i++) { const changeType = oldFabricAcls[i] === undefined ? AccessControlTypes.ChangeType.Added : fabricAcls[i] === undefined ? AccessControlTypes.ChangeType.Removed : AccessControlTypes.ChangeType.Changed; this.events.accessControlEntryChanged.emit( { changeType, adminNodeId, adminPasscodeId, latestValue: (changeType === AccessControlTypes.ChangeType.Removed ? oldFabricAcls[i] : fabricAcls[i]) ?? null, fabricIndex: relevantFabricIndex, }, this.context, ); } if (oldFabricAcls.length > i) { for (let j = oldFabricAcls.length - 1; j >= i; j--) { this.events.accessControlEntryChanged.emit( { changeType: AccessControlTypes.ChangeType.Removed, adminNodeId, adminPasscodeId, latestValue: oldValue[j], fabricIndex: relevantFabricIndex, }, this.context, ); } } } #validateAccessControlExtensionChanges(value: AccessControlTypes.AccessControlExtension[]) { if (!hasRemoteActor(this.context)) { return; } const relevantFabricIndex = this.context.session.associatedFabric.fabricIndex; const fabricExtensions = value.filter(entry => entry.fabricIndex === relevantFabricIndex); if (fabricExtensions.length === 0) { return; } if (fabricExtensions.length > 1) { throw new StatusResponseError("Extension list must contain a single entry", Status.ConstraintError); } // we have exactly one entry this.extensionEntryValidator(fabricExtensions[0]); } #handleAccessControlExtensionChange( value: AccessControlTypes.AccessControlExtension[], oldValue: AccessControlTypes.AccessControlExtension[], ) { if (!this.internal.initialized) { return; // Too early to send events } const session = hasRemoteActor(this.context) ? this.context.session : undefined; // TODO: This might be not really correct for local ACL changes because there the session fabric could be // different which would lead to missing events of the relevant entries const relevantFabricIndex = session?.associatedFabric.fabricIndex; if (relevantFabricIndex === undefined || this.events.accessControlExtensionChanged === undefined) { return; } const { adminPasscodeId, adminNodeId } = this.#adminDataFromSession(session); const fabricExtensions = value.filter(entry => entry.fabricIndex === relevantFabricIndex); const oldFabricExtensions = oldValue.filter(entry => entry.fabricIndex === relevantFabricIndex); const changeType = fabricExtensions.length > oldFabricExtensions.length ? AccessControlTypes.ChangeType.Added : fabricExtensions.length < oldFabricExtensions.length ? AccessControlTypes.ChangeType.Removed : AccessControlTypes.ChangeType.Changed; this.events.accessControlExtensionChanged.emit( { changeType, adminNodeId, adminPasscodeId, latestValue: (changeType === AccessControlTypes.ChangeType.Removed ? oldFabricExtensions[0] : fabricExtensions[0]) ?? null, fabricIndex: relevantFabricIndex, }, this.context, ); } /** * This method allows to implement the validation of manufacturer specific ACL extensions when an extension entry is * added or changed. The default implementation checks whether the extension is a valid TLV and possible to decode. * * In case of an Error throws StatusResponseError. * * Override this method in your own behavior to implement custom validation. */ protected extensionEntryValidator(extension: AccessControlTypes.AccessControlExtension) { const { data } = extension; const extensionBytes = Bytes.of(data); if ( extensionBytes.length < 2 || extensionBytes[0] !== TlvType.List || extensionBytes[extensionBytes.length - 1] !== TlvType.EndOfContainer ) { // Easier to check that way that it is an Listen without any tags in general throw new StatusResponseError("Extension must be a valid TLV", Status.ConstraintError); } try { TlvTaggedList({} /* No fields, sufficient for validation */, true).decode(data); } catch (error) { logger.debug(`Extension TLV decoding failed:`, error); throw new StatusResponseError("Extension must be a valid TLV", Status.ConstraintError); } } /** * This method allows to implement the validation of manufacturer specific ACL extensions when access control is * checked to decide if access is allowed or not. * The default implementation always returns true. Override this method in your own behavior to implement custom * validation. */ protected extensionEntryAccessCheck( _aclList: AclList, _aclEntry: AclEntry, _subjectDesc: IncomingSubjectDescriptor, _endpoint: AclEndpointContext, _clusterId: ClusterId, ) { return true; } /** A fabric was added, so we need to initialize the ACL for this fabric */ #updateFabricAcls(fabric: Fabric) { const fabricIndex = fabric.fabricIndex; fabric.accessControl.extensionEntryAccessCheck = this.extensionEntryAccessCheck.bind(this); this.#applyFabricAcl( fabric, deepCopy(this.state.acl).filter(entry => entry.fabricIndex === fabricIndex), ); } /** * Install the effective ACL of a fabric: its real entries plus the current auxiliary entries. Without `realAcl` * the real entries applied last are kept, so an auxiliary change never applies a partially written ACL attribute. */ #applyFabricAcl(fabric: Fabric, realAcl?: AccessControlTypes.AccessControlEntry[]) { const { fabricIndex } = fabric; if (realAcl !== undefined) { this.internal.appliedAcl.set(fabricIndex, realAcl); } fabric.accessControl.auxiliaryFeatureEnabled = this.features.auxiliary; fabric.accessControl.aclList = [ ...(this.internal.appliedAcl.get(fabricIndex) ?? []), ...this.#auxiliaryAclFor(fabricIndex), ]; } /** * Register an observable that supplies auxiliary ACL entries. AccessControlServer subscribes to the observable * and rebuilds fabric ACLs whenever it emits new entries. Multiple providers are supported. * * The observable should emit `AccessControlEntry[]` (all entries for all fabrics from this provider). * AccessControlServer reads the current `observable.value` immediately on registration for initial sync. */ registerAuxAclProvider(observable: AccessControlServer.AuxAclObservable) { this.internal.auxiliaryAclProviders.add(observable); // Offline: the auxiliaryAcl state write runs in its own transaction rather than re-opening the provider's // already-committed one during postCommit this.reactTo(observable, this.#onProviderAuxAclChanged, { offline: true }); // Sync initial value without emitting events (node is still initializing) if (observable.value?.length) { this.#syncAuxAcl(false); } } /** When provider entries change after initialization, rebuild auxiliaryAcl and update changed fabrics. */ #onProviderAuxAclChanged(_entries: AccessControlTypes.AccessControlEntry[], context?: ActionContext) { this.#syncAuxAcl(true, context); } /** * Recompute auxiliaryAcl from all registered providers and install the effective ACL of every fabric that has * auxiliary entries. The attribute and the auxiliaryAccessUpdated events (when emitEvents is true) follow only * actual changes. */ #syncAuxAcl(emitEvents: boolean, providerContext?: ActionContext) { const newAuxAcl = this.#auxiliaryAclFor(); const oldAuxAcl = deepCopy(this.state.auxiliaryAcl ?? []); // Determine which fabrics have changed entries const allFabrics = new Set([...oldAuxAcl.map(e => e.fabricIndex), ...newAuxAcl.map(e => e.fabricIndex)]); const changedFabrics = [...allFabrics].filter(fi => { const oldEntries = oldAuxAcl.filter(e => e.fabricIndex === fi); const newEntries = newAuxAcl.filter(e => e.fabricIndex === fi); return !isDeepEqual(oldEntries, newEntries); }); // The attribute is persisted, so "unchanged" does not mean installed (e.g. after a restart) if (changedFabrics.length) { this.state.auxiliaryAcl = newAuxAcl; } const fabrics = this.env.get(FabricManager); // Extract admin node ID: prefer provider context (the command handler that triggered the // change), fall back to this behavior's own session context const ctx = providerContext ?? this.context; const session = hasRemoteActor(ctx) ? ctx.session : undefined; const { adminNodeId } = this.#adminDataFromSession(session); for (const fi of allFabrics) { if (!fabrics.has(fi)) { continue; } this.#applyFabricAcl(fabrics.for(fi)); if (emitEvents && changedFabrics.includes(fi)) { this.events.auxiliaryAccessUpdated?.emit({ adminNodeId, fabricIndex: fi }, this.context); } } } /** * When beginning an interaction for an online session, we register the potential ACL change for the associated * fabric index. If ACL data are really changed later, the exchange gets added then. */ #handleInteractionBegin(session?: AccessControl.Session) { if (hasRemoteActor(session)) { this.#prepareAclUpdateFor(session.fabric); } } /** * When an interaction is finished, we check if there was a delayed ACL update for the associated fabric and apply * it to the manager. For this we check if we have an exchange stored because otherwise the interaction was in fact * not changing the ACL. */ #handleInteractionEnd(session?: AccessControl.Session) { if (hasRemoteActor(session)) { if (this.internal.aclUpdateDelayed.get(session.fabric) !== undefined) { this.#applyDelayedAclUpdateFor(session.fabric); } } } /** The ACL list was changed, so we need to determine if and when to apply the update to the ACL manager */ #updateAccessControlList( acl: AccessControlTypes.AccessControlEntry[], _oldAcl: AccessControlTypes.AccessControlEntry[], context?: ActionContext, ) { if (hasLocalActor(context)) { // local or offline ACL change, so we update all fabrics because we do not know better this.#updateAllFabricsAcls(); } else { const fabric = context.session?.associatedFabric; if (fabric === undefined || fabric.fabricIndex === undefined || context.exchange === undefined) { throw new InternalError("We require a fabric bound online session to write ACL changes"); } this.#handleFabricAclUpdate(fabric, acl, context.exchange); } } /** * Handles the ACL update for a specific fabric. If an exchange is present, we delay the update until the * interaction is finished. */ #handleFabricAclUpdate(fabric: Fabric, acl: AccessControlTypes.AccessControlEntry[], exchange: MessageExchange) { const fabricIndex = fabric.fabricIndex; if (this.internal.aclUpdateDelayed.has(fabricIndex)) { // We have registered an interaction for this fabric, so we delay the update logger.debug( "ACL attribute updated, but interaction still in progress, delaying update of ACL manager for FabricIndex", fabricIndex, ); this.#delayAclUpdateFor(fabricIndex, exchange, acl); } else { // No interaction registered, so we apply directly because local/offline change logger.debug("ACL attribute updated, applying update to ACL manager", fabricIndex); this.#applyFabricAcl( fabric, deepCopy(acl).filter(entry => entry.fabricIndex === fabricIndex), ); } } #mapFabricAcls() { const acl = deepCopy(this.state.acl); const aclsForFabric = new Map(); // Collect ACLs for each fabric for (const entry of acl) { const { fabricIndex } = entry; const acls = aclsForFabric.get(fabricIndex) ?? []; acls.push(entry); aclsForFabric.set(fabricIndex, acls); } return aclsForFabric; } /** Update all fabrics with the current ACL list */ #updateAllFabricsAcls() { const aclsForFabric = this.#mapFabricAcls(); const fabrics = this.env.get(FabricManager); for (const fabric of fabrics) { this.#applyFabricAcl(fabric, aclsForFabric.get(fabric.fabricIndex) ?? []); } } /** * Register a potential change of ACL for a specific fabric index. if changes happened is checked when interaction * ends. */ #prepareAclUpdateFor(fabricIndex: FabricIndex) { if (!this.internal.aclUpdateDelayed.has(fabricIndex)) { logger.info("Register ACL update to be delayed for fabricIndex", fabricIndex); this.internal.aclUpdateDelayed.set(fabricIndex, undefined); } } /** * Register a concrete change of ACL for a specific fabric index. The exchange allows to also limit ACL changes to * that exchange until interaction is finished. */ #delayAclUpdateFor( fabricIndex: FabricIndex, exchange: MessageExchange, acl: AccessControlTypes.AccessControlEntry[], ) { if (!this.internal.aclUpdateDelayed.has(fabricIndex)) { logger.info("Register ACL update to be delayed for fabricIndex", fabricIndex); } this.internal.aclUpdateDelayed.set(fabricIndex, exchange); this.internal.delayedAclData.set( fabricIndex, deepCopy(acl).filter(entry => entry.fabricIndex === fabricIndex), ); } /** * Collect the auxiliary ACL entries of one fabric, or of all fabrics, from the registered providers. Entries are * split to respect SubjectsPerAccessControlEntry and TargetsPerAccessControlEntry (core§9.10.6.10). * * Reads the provider observables directly, so it is safe in reactors that run after an interaction context has * exited. */ #auxiliaryAclFor(fabricIndex?: FabricIndex) { const entries = new Array(); if (!this.features.auxiliary) { return entries; } const { subjects: maxSubjects, targets: maxTargets } = this.internal.auxiliaryEntryLimits; for (const obs of this.internal.auxiliaryAclProviders) { for (const entry of obs.value ?? []) { if (fabricIndex !== undefined && entry.fabricIndex !== fabricIndex) { continue; } if (entry.auxiliaryType === undefined) { logger.error("Ignoring auxiliary ACL entry without AuxiliaryType", Diagnostic.dict(entry)); continue; } for (const subjects of chunked(entry.subjects, maxSubjects)) { for (const targets of chunked(entry.targets, maxTargets)) { entries.push({ ...entry, subjects, targets }); } } } } return entries; } /** Applies the delayed ACL update for a specific fabric index, if existing */ #applyDelayedAclUpdateFor(fabricIndex: FabricIndex) { const updateDelayed = !!this.internal.aclUpdateDelayed.get(fabricIndex); const delayedData = this.internal.delayedAclData.get(fabricIndex); this.internal.delayedAclData.delete(fabricIndex); this.internal.aclUpdateDelayed.delete(fabricIndex); if (updateDelayed && delayedData !== undefined) { logger.info("Applying delayed ACL update for fabricIndex", fabricIndex); this.#applyFabricAcl(this.env.get(FabricManager).for(fabricIndex), delayedData); } } } export namespace AccessControlServer { export class State extends AccessControlBase.State { /** * Synthesized read-only ACL entries supplied by auxiliary providers (e.g. Groupcast), split to the per-entry * limits. Mirrors what access control enforces; only present when the Auxiliary feature is enabled. */ declare auxiliaryAcl?: AccessControlTypes.AccessControlEntry[]; } export class Events extends AccessControlBase.Events { /** Emitted when auxiliary ACL entries change. Only present when the Auxiliary feature is enabled. */ declare auxiliaryAccessUpdated?: OnlineEvent< [payload: AccessControlTypes.AuxiliaryAccessUpdatedEvent, context: ActionContext] >; } export class Internal { /** Is the cluster logic initialized? Used to block events before full initialization. */ initialized = false; /** * When an online and potentially chunked ACL writing happens, we will delay the update and store the exchange * used for the writing. With this we also verify that concurrent writes are blocked and will not mix the data. */ aclUpdateDelayed = new Map(); /** Latest delayed data of acl attribute */ delayedAclData = new Map(); /** Registered observable providers that supply auxiliary ACL entries. */ auxiliaryAclProviders = new Set(); /** Real ACL entries applied last per fabric; the effective ACL combines them with current auxiliary entries. */ appliedAcl = new Map(); /** Per-entry limits, captured at initialization so auxiliary entries can be split without a state access. */ auxiliaryEntryLimits = { subjects: 4, targets: 4 }; } export declare const ExtensionInterface: { extensionEntryValidator: (extension: AccessControlTypes.AccessControlExtension) => void; extensionEntryAccessCheck: ( aclList: AclList, aclEntry: AclEntry, subjectDesc: IncomingSubjectDescriptor, endpoint: AclEndpointContext, clusterId: ClusterId, ) => boolean; }; /** * An observable that supplies auxiliary ACL entries to {@link AccessControlServer}. * Register via {@link AccessControlServer.registerAuxAclProvider}. * * Emit `AccessControlEntry[]` (all entries across all fabrics) whenever the provider's entries change. * AccessControlServer subscribes and rebuilds its ACL cache automatically. */ export type AuxAclObservable = ObservableValue<[AccessControlTypes.AccessControlEntry[], ActionContext?]>; } /** Split a list into chunks of at most `size` elements; a `null` wildcard stays a single `null`. */ function chunked(list: T[] | null, size: number): (T[] | null)[] { if (list === null || list.length <= size) { return [list]; } const chunks = new Array(); for (let i = 0; i < list.length; i += size) { chunks.push(list.slice(i, i + size)); } return chunks; }