"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var AccessControlServer_exports = {};
__export(AccessControlServer_exports, {
  AccessControlServer: () => AccessControlServer
});
module.exports = __toCommonJS(AccessControlServer_exports);
var import_access_control = require("#clusters/access-control");
var import_general = require("#general");
var import_protocol = require("#protocol");
var import_types = require("#types");
var import_AccessControlBehavior = require("./AccessControlBehavior.js");
/**
 * @license
 * Copyright 2022-2025 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
const logger = import_general.Logger.get("AccessControlServer");
class AccessControlServer extends import_AccessControlBehavior.AccessControlBehavior.with("Extension") {
  initialize() {
    this.reactTo(this.events.acl$Changing, this.#validateAccessControlListChanges);
    this.reactTo(this.events.acl$Changed, this.#handleAccessControlListChange);
    if (this.state.extension !== void 0) {
      this.maybeReactTo(this.events.extension$Changing, this.#validateAccessControlExtensionChanges);
      this.maybeReactTo(this.events.extension$Changed, this.#handleAccessControlExtensionChange);
    }
    const lifecycle = this.endpoint.lifecycle;
    this.reactTo(lifecycle.online, this.#online);
  }
  #online() {
    const aclsForFabric = this.#mapFabricAcls();
    const fabrics = this.env.get(import_protocol.FabricManager);
    for (const fabric of fabrics) {
      const fabricAcls = aclsForFabric.get(fabric.fabricIndex) ?? [];
      if (!fabricAcls.length) {
        const fallbackAcl = {
          privilege: import_access_control.AccessControl.AccessControlEntryPrivilege.Administer,
          authMode: import_access_control.AccessControl.AccessControlEntryAuthMode.Case,
          subjects: [fabric.rootNodeId],
          targets: null,
          // entire node
          fabricIndex: fabric.fabricIndex
        };
        this.state.acl.push(fallbackAcl);
        fabricAcls.push(fallbackAcl);
      }
      fabric.accessControl.aclList = fabricAcls;
      fabric.accessControl.extensionEntryAccessCheck = this.extensionEntryAccessCheck.bind(this);
    }
    this.reactTo(fabrics.events.updated, this.#updateFabricAcls);
    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, subjects) {
    const entry = {
      fabricIndex: fabric.fabricIndex,
      privilege: import_access_control.AccessControl.AccessControlEntryPrivilege.Administer,
      authMode: import_access_control.AccessControl.AccessControlEntryAuthMode.Case,
      subjects,
      targets: null
      // entire node
    };
    this.state.acl.push(entry);
    this.#updateFabricAcls(fabric);
    this.events.accessControlEntryChanged?.emit(
      {
        changeType: import_access_control.AccessControl.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, _oldValue) {
    const { context } = this;
    if (!(0, import_protocol.hasRemoteActor)(context)) {
      return;
    }
    const relevantFabricIndex = context.session.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0) {
      return;
    }
    if (context !== void 0 && context.exchange !== void 0) {
      const delayedChangeExchange = this.internal.aclUpdateDelayed.get(relevantFabricIndex);
      if (delayedChangeExchange !== void 0 && delayedChangeExchange !== context.exchange) {
        logger.warn(
          "Decline parallel ACL changes from multiple exchanges",
          context.exchange.id,
          "vs.",
          delayedChangeExchange.id
        );
        throw new import_types.StatusResponseError("Parallel ACL change from multiple exchanges", import_types.StatusCode.Busy);
      }
    }
    const fabricAcls = value.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    if (fabricAcls.length > this.state.accessControlEntriesPerFabric) {
      throw new import_types.StatusResponseError("AccessControlEntriesPerFabric exceeded", import_types.StatusCode.ResourceExhausted);
    }
    for (const entry of fabricAcls) {
      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 import_types.StatusResponseError("SubjectsPerAccessControlEntry exceeded", import_types.StatusCode.ResourceExhausted);
      }
      if (targets !== null && targets.length > this.state.targetsPerAccessControlEntry) {
        throw new import_types.StatusResponseError("TargetsPerAccessControlEntry exceeded", import_types.StatusCode.ResourceExhausted);
      }
      if (authMode === import_access_control.AccessControl.AccessControlEntryAuthMode.Pase) {
        throw new import_types.StatusResponseError("AuthMode for ACL must not be PASE", import_types.StatusCode.ConstraintError);
      } else if (authMode === import_access_control.AccessControl.AccessControlEntryAuthMode.Case) {
        if (subjects !== null) {
          for (const subject of subjects) {
            if (import_types.NodeId.isCaseAuthenticatedTag(subject)) {
              const cat = import_types.NodeId.extractAsCaseAuthenticatedTag(subject);
              if (import_types.CaseAuthenticatedTag.getVersion(cat) === 0) {
                throw new import_types.StatusResponseError(
                  "CaseAuthenticatedTag version 0 is not allowed",
                  import_types.StatusCode.ConstraintError
                );
              }
            } else if (!import_types.NodeId.isOperationalNodeId(subject)) {
              throw new import_types.StatusResponseError(
                "Subject must be a valid OperationalNodeId or CaseAuthenticatedTag",
                import_types.StatusCode.ConstraintError
              );
            }
          }
        }
      } else if (authMode === import_access_control.AccessControl.AccessControlEntryAuthMode.Group) {
        if (privilege === import_access_control.AccessControl.AccessControlEntryPrivilege.Administer) {
          throw new import_types.StatusResponseError(
            "Group ACLs must not have Administer privilege",
            import_types.StatusCode.ConstraintError
          );
        }
        if (subjects !== null) {
          for (const subject of subjects) {
            if ((0, import_types.GroupId)(Number(subject)) === import_types.GroupId.NO_GROUP_ID) {
              throw new import_types.StatusResponseError(
                "Subject must be a valid GroupId for Group ACLs",
                import_types.StatusCode.ConstraintError
              );
            }
          }
        }
      }
      if (targets !== null) {
        for (const target of targets) {
          const { cluster, endpoint, deviceType } = target;
          if (deviceType !== null && endpoint !== null) {
            throw new import_types.StatusResponseError(
              "DeviceType and Endpoint are mutually exclusive",
              import_types.StatusCode.ConstraintError
            );
          }
          if (cluster === null && endpoint === null && deviceType === null) {
            throw new import_types.StatusResponseError("At least one field must be present", import_types.StatusCode.ConstraintError);
          }
          if (cluster !== null && !import_types.ClusterId.isValid(cluster)) {
            throw new import_types.StatusResponseError("Cluster must be a valid ClusterId", import_types.StatusCode.ConstraintError);
          }
          if (endpoint !== null && !import_types.EndpointNumber.isValid(endpoint)) {
            throw new import_types.StatusResponseError(
              "Endpoint must be a valid OperationalNodeId",
              import_types.StatusCode.ConstraintError
            );
          }
          if (deviceType !== null && !import_types.DeviceTypeId.isValid(deviceType)) {
            throw new import_types.StatusResponseError(
              "DeviceType must be a valid DeviceType",
              import_types.StatusCode.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) {
    if (session === void 0 || import_protocol.NodeSession.is(session) && session.isPase) {
      return { adminPasscodeId: 0, adminNodeId: null };
    }
    const adminNodeId = session?.associatedFabric.rootNodeId;
    if (adminNodeId === void 0) {
      throw new import_general.InternalError("Admin Node ID is undefined, should never happen");
    }
    return { adminPasscodeId: null, adminNodeId };
  }
  #handleAccessControlListChange(value, oldValue) {
    if (!this.internal.initialized) {
      return;
    }
    const session = (0, import_protocol.hasRemoteActor)(this.context) ? this.context.session : void 0;
    const relevantFabricIndex = session?.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0 || this.events.accessControlEntryChanged === void 0) {
      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] === void 0 ? import_access_control.AccessControl.ChangeType.Added : fabricAcls[i] === void 0 ? import_access_control.AccessControl.ChangeType.Removed : import_access_control.AccessControl.ChangeType.Changed;
      this.events.accessControlEntryChanged.emit(
        {
          changeType,
          adminNodeId,
          adminPasscodeId,
          latestValue: (changeType === import_access_control.AccessControl.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: import_access_control.AccessControl.ChangeType.Removed,
            adminNodeId,
            adminPasscodeId,
            latestValue: oldValue[j],
            fabricIndex: relevantFabricIndex
          },
          this.context
        );
      }
    }
  }
  #validateAccessControlExtensionChanges(value) {
    if (!(0, import_protocol.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 import_types.StatusResponseError("Extension list must contain a single entry", import_types.StatusCode.ConstraintError);
    }
    this.extensionEntryValidator(fabricExtensions[0]);
  }
  #handleAccessControlExtensionChange(value, oldValue) {
    if (!this.internal.initialized) {
      return;
    }
    const session = (0, import_protocol.hasRemoteActor)(this.context) ? this.context.session : void 0;
    const relevantFabricIndex = session?.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0 || this.events.accessControlExtensionChanged === void 0) {
      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 ? import_access_control.AccessControl.ChangeType.Added : fabricExtensions.length < oldFabricExtensions.length ? import_access_control.AccessControl.ChangeType.Removed : import_access_control.AccessControl.ChangeType.Changed;
    this.events.accessControlExtensionChanged.emit(
      {
        changeType,
        adminNodeId,
        adminPasscodeId,
        latestValue: (changeType === import_access_control.AccessControl.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.
   */
  extensionEntryValidator(extension) {
    const { data } = extension;
    const extensionBytes = import_general.Bytes.of(data);
    if (extensionBytes.length < 2 || extensionBytes[0] !== import_types.TlvType.List || extensionBytes[extensionBytes.length - 1] !== import_types.TlvType.EndOfContainer) {
      throw new import_types.StatusResponseError("Extension must be a valid TLV", import_types.StatusCode.ConstraintError);
    }
    try {
      (0, import_types.TlvTaggedList)({}, true).decode(data);
    } catch (error) {
      logger.debug(`Extension TLV decoding failed:`, error);
      throw new import_types.StatusResponseError("Extension must be a valid TLV", import_types.StatusCode.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.
   */
  extensionEntryAccessCheck(_aclList, _aclEntry, _subjectDesc, _endpoint, _clusterId) {
    return true;
  }
  /** A fabric was added or updated, so we need to initialize the ACL for this fabric */
  #updateFabricAcls(fabric) {
    const fabricIndex = fabric.fabricIndex;
    fabric.accessControl.aclList = (0, import_general.deepCopy)(this.state.acl).filter((entry) => entry.fabricIndex === fabricIndex);
  }
  /**
   * 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) {
    if ((0, import_protocol.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) {
    if ((0, import_protocol.hasRemoteActor)(session)) {
      if (this.internal.aclUpdateDelayed.get(session.fabric) !== void 0) {
        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, _oldAcl, context) {
    if ((0, import_protocol.hasLocalActor)(context)) {
      this.#updateAllFabricsAcls();
    } else {
      const fabric = context.session?.associatedFabric;
      if (fabric === void 0 || fabric.fabricIndex === void 0 || context.exchange === void 0) {
        throw new import_general.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, acl, exchange) {
    const fabricIndex = fabric.fabricIndex;
    if (this.internal.aclUpdateDelayed.has(fabricIndex)) {
      logger.debug(
        "ACL attribute updated, but interaction still in progress, delaying update of ACL manager for FabricIndex",
        fabricIndex
      );
      this.#delayAclUpdateFor(fabricIndex, exchange, acl);
    } else {
      logger.debug("ACL attribute updated, applying update to ACL manager", fabricIndex);
      fabric.accessControl.aclList = (0, import_general.deepCopy)(acl).filter((entry) => entry.fabricIndex === fabricIndex);
    }
  }
  #mapFabricAcls() {
    const acl = (0, import_general.deepCopy)(this.state.acl);
    const aclsForFabric = /* @__PURE__ */ new Map();
    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(import_protocol.FabricManager);
    for (const fabric of fabrics) {
      fabric.accessControl.aclList = 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) {
    if (!this.internal.aclUpdateDelayed.has(fabricIndex)) {
      logger.info("Register ACL update to be delayed for fabricIndex", fabricIndex);
      this.internal.aclUpdateDelayed.set(fabricIndex, void 0);
    }
  }
  /**
   * 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, exchange, acl) {
    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,
      (0, import_general.deepCopy)(acl).filter((entry) => entry.fabricIndex === fabricIndex)
    );
  }
  /** Applies the delayed ACL update for a specific fabric index, if existing */
  #applyDelayedAclUpdateFor(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 !== void 0) {
      this.env.get(import_protocol.FabricManager).for(fabricIndex).accessControl.aclList = delayedData;
    }
  }
}
((AccessControlServer2) => {
  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 = /* @__PURE__ */ new Map();
    /** Latest delayed data of acl attribute */
    delayedAclData = /* @__PURE__ */ new Map();
  }
  AccessControlServer2.Internal = Internal;
})(AccessControlServer || (AccessControlServer = {}));
//# sourceMappingURL=AccessControlServer.js.map
