"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 SessionManager_exports = {};
__export(SessionManager_exports, {
  SessionManager: () => SessionManager,
  ShutdownError: () => ShutdownError
});
module.exports = __toCommonJS(SessionManager_exports);
var import_SupportedTransportsBitmap = require("#common/SupportedTransportsBitmap.js");
var import_FabricManager = require("#fabric/FabricManager.js");
var import_PeerAddress = require("#peer/PeerAddress.js");
var import_PeerCommunicationError = require("#peer/PeerCommunicationError.js");
var import_errors = require("#protocol/errors.js");
var import_GroupSession = require("#session/GroupSession.js");
var import_general = require("@matter/general");
var import_types = require("@matter/types");
var import_MessageCounter = require("../protocol/MessageCounter.js");
var import_NodeSession = require("./NodeSession.js");
var import_SecureSession = require("./SecureSession.js");
var import_SessionParameters = require("./SessionParameters.js");
var import_UnsecuredSession = require("./UnsecuredSession.js");
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
const logger = import_general.Logger.get("SessionManager");
const ID_SPACE_UPPER_BOUND = 65535;
class ShutdownError extends import_general.ClosedError {
  constructor(message = "Local node shutdown", options) {
    super(message, options);
  }
}
class SessionManager {
  #context;
  #unsecuredSessions = /* @__PURE__ */ new Map();
  #sessions = new import_general.BasicSet();
  #groupSessions = /* @__PURE__ */ new Map();
  #nextSessionId;
  #resumptionRecords = new import_PeerAddress.PeerAddressMap();
  #globalUnencryptedMessageCounter;
  #sessionParameters;
  #construction;
  #observers = new import_general.ObserverGroup();
  #subscriptionUpdateMutex = new import_general.Mutex(this);
  #idUpperBound = ID_SPACE_UPPER_BOUND;
  #subscriptionsChanged = (0, import_general.Observable)();
  #retry = (0, import_general.Observable)();
  constructor(context) {
    this.#context = context;
    const {
      fabrics: { crypto }
    } = context;
    this.#sessionParameters = (0, import_SessionParameters.SessionParameters)({ ...import_SessionParameters.SessionParameters.defaults, ...context.parameters });
    this.#nextSessionId = crypto.randomUint16;
    this.#globalUnencryptedMessageCounter = new import_MessageCounter.MessageCounter(crypto);
    this.#observers.on(context.fabrics.events.deleting, async (fabric) => {
      await this.deleteResumptionRecordsForFabric(fabric);
    });
    this.#sessions.added.on((session) => {
      const subscriptionsChanged = (subscription) => {
        if (session.isClosing) {
          return;
        }
        this.#subscriptionsChanged.emit(session, subscription);
      };
      session.subscriptions.added.on(subscriptionsChanged);
      session.subscriptions.deleted.on(subscriptionsChanged);
    });
    this.#construction = (0, import_general.Construction)(this, () => this.#initialize());
  }
  static [import_general.Environmental.create](env) {
    const instance = new SessionManager({
      storage: env.get(import_general.StorageManager).createContext("sessions"),
      fabrics: env.get(import_FabricManager.FabricManager)
    });
    env.set(SessionManager, instance);
    return instance;
  }
  get construction() {
    return this.#construction;
  }
  get context() {
    return this.#context;
  }
  get crypto() {
    return this.#context.fabrics.crypto;
  }
  /**
   * Active secure sessions.
   */
  get sessions() {
    return this.#sessions;
  }
  /**
   * Active unsecured sessions.
   */
  get unsecuredSessions() {
    return this.#unsecuredSessions;
  }
  /**
   * Our session parameters.  These are the parameters we provide during session negotiation.  The peer may specify
   * different parameters.
   */
  get sessionParameters() {
    const { supportedTransports, maxTcpMessageSize } = this.#sessionParameters;
    const tcpSupported = supportedTransports.tcpClient || supportedTransports.tcpServer;
    return {
      ...this.#sessionParameters,
      // The MAX_TCP_MESSAGE_SIZE field SHALL only be present if the SUPPORTED_TRANSPORTS field
      // indicates that TCP is supported.
      maxTcpMessageSize: tcpSupported ? maxTcpMessageSize : void 0
    };
  }
  /**
   * Change session parameters.
   *
   * Parameters values you omit in {@link parameters} will retain their current values.  This only affects new
   * sessions.
   */
  set sessionParameters(parameters) {
    this.#sessionParameters = {
      ...this.#sessionParameters,
      ...parameters
    };
  }
  /**
   * Emits when there is a change to the subscription set.
   */
  get subscriptionsChanged() {
    return this.#subscriptionsChanged;
  }
  /**
   * Emits when resubmission is necessary due to timeout or network error.
   */
  get retry() {
    return this.#retry;
  }
  /**
   * Convenience function for accessing a fabric by address.
   */
  fabricFor(address) {
    return this.#context.fabrics.for(address);
  }
  /**
   * @deprecated
   */
  get owner() {
    return this.#context.owner;
  }
  createUnsecuredSession(options) {
    this.#construction.assert();
    const { channel, initiatorNodeId, sessionParameters, isInitiator } = options;
    if (initiatorNodeId !== void 0) {
      if (this.#unsecuredSessions.has(initiatorNodeId)) {
        throw new import_general.MatterFlowError(`UnsecuredSession with NodeId ${initiatorNodeId} already exists`);
      }
    }
    let tries = 0;
    while (true) {
      const session = new import_UnsecuredSession.UnsecuredSession({
        crypto: this.#context.fabrics.crypto,
        manager: this,
        channel,
        messageCounter: this.#globalUnencryptedMessageCounter,
        initiatorNodeId,
        sessionParameters,
        isInitiator: isInitiator ?? false
      });
      const ephemeralNodeId = session.nodeId;
      if (this.#unsecuredSessions.has(ephemeralNodeId)) {
        if (++tries > 4) {
          throw new import_general.InternalError("Unable to allocate unique ephemeral node ID; entropy source is broken");
        }
        continue;
      }
      this.#unsecuredSessions.set(ephemeralNodeId, session);
      session.activate();
      return session;
    }
  }
  async createSecureSession(config) {
    return await import_NodeSession.NodeSession.create({
      crypto: this.crypto,
      ...config,
      manager: this
    });
  }
  /**
   * Deletes a resumption record for a given address.  Returns true if the record was deleted, false if it did not
   * exist.
   */
  async deleteResumptionRecord(address) {
    await this.#construction;
    const result = this.#resumptionRecords.delete(address);
    if (result) {
      await this.#storeResumptionRecords();
    }
    return result;
  }
  /**
   * Deletes all resumption records for a given fabric.  Returns true if any records were deleted, false if none
   * existed.
   */
  async deleteResumptionRecordsForFabric(fabric) {
    await this.#construction;
    let deletedCount = 0;
    for (const address of this.#resumptionRecords.keys()) {
      if (address.fabricIndex === fabric.fabricIndex) {
        if (this.#resumptionRecords.delete(address)) {
          deletedCount++;
        }
      }
    }
    if (deletedCount > 0) {
      await this.#storeResumptionRecords();
    }
    return deletedCount > 0;
  }
  findOldestInactiveSession() {
    this.#construction.assert();
    let oldestSession = void 0;
    for (const session of this.#sessions) {
      if (!oldestSession || session.activeTimestamp < oldestSession.activeTimestamp) {
        oldestSession = session;
      }
    }
    if (oldestSession === void 0) {
      throw new import_general.MatterFlowError("No session found to close and all session ids are taken.");
    }
    return oldestSession;
  }
  async getNextAvailableSessionId() {
    await this.#construction;
    for (let i = 0; i < this.#idUpperBound; i++) {
      const id = this.#nextSessionId;
      this.#nextSessionId = this.#nextSessionId + 1 & this.#idUpperBound;
      if (this.#nextSessionId === 0) this.#nextSessionId++;
      if (this.getSession(id) === void 0) {
        return id;
      }
    }
    const oldestSession = this.findOldestInactiveSession();
    await oldestSession.initiateClose(async () => {
      await oldestSession.closeSubscriptions(true);
    });
    this.#nextSessionId = oldestSession.id;
    return this.#nextSessionId++;
  }
  getSession(sessionId) {
    this.#construction.assert();
    return this.#sessions.get("id", sessionId);
  }
  getPaseSession() {
    this.#construction.assert();
    return [...this.#sessions].find((session) => import_NodeSession.NodeSession.is(session) && session.isPase && !session.isClosing);
  }
  forFabric(fabric) {
    this.#construction.assert();
    return [...this.#sessions].filter(
      (session) => import_NodeSession.NodeSession.is(session) && session.isSecure && session.fabric?.fabricIndex === fabric.fabricIndex
    );
  }
  sessionFor(peer) {
    const session = this.maybeSessionFor(peer);
    if (session) {
      return session;
    }
    throw new import_errors.SessionClosedError(`Not currently connected to ${(0, import_PeerAddress.PeerAddress)(peer)}`);
  }
  maybeSessionFor(address) {
    this.#construction.assert();
    let found;
    for (const session of this.#sessions) {
      if (!session.peerIs(address) || session.isClosing) {
        continue;
      }
      if (!found || found.activeTimestamp < session.activeTimestamp) {
        found = session;
      }
    }
    return found;
  }
  sessionsFor(address) {
    address = (0, import_PeerAddress.PeerAddress)(address);
    return this.#sessions.filter((session) => session.peerAddress === address && !session.isClosing);
  }
  sessionsForFabricIndex(fabricIndex) {
    return this.#sessions.filter((session) => session.fabric?.fabricIndex === fabricIndex);
  }
  /**
   * Removes all Peer sessions but keeps subscriptions intact because they could be refreshed on restart when the
   * device supports persistent subscriptions.
   */
  handlePeerShutdown(address, asOf) {
    return this.#handlePeerLoss({
      address,
      cause: new import_PeerCommunicationError.PeerShutdownError(),
      asOf: asOf ?? import_general.Time.nowMs,
      keepSubscriptions: true
    });
  }
  /**
   * Removes all Peer sessions and closes subscriptions.
   */
  async handlePeerLoss(address, cause, asOf) {
    return await this.#handlePeerLoss({ address, asOf: asOf ?? import_general.Time.nowMs, cause });
  }
  async #handlePeerLoss(context) {
    await this.#construction;
    const { address, asOf } = context;
    for (const session of this.#sessions) {
      if (!session.peerIs(address)) {
        continue;
      }
      if (asOf !== void 0 && session.createdAt >= asOf) {
        continue;
      }
      await session.handlePeerLoss(context);
    }
  }
  getUnsecuredSession(sourceNodeId) {
    this.#construction.assert();
    if (sourceNodeId === void 0) {
      return this.#unsecuredSessions.get(import_types.NodeId.UNSPECIFIED_NODE_ID);
    }
    return this.#unsecuredSessions.get(sourceNodeId);
  }
  /**
   * Obtain an outbound group session for a specific group.
   *
   * Returns the session for the current group epoch key.  The source is this node and the peer is the group.
   */
  async groupSessionForAddress(address, transports) {
    const groupId = import_types.GroupId.fromNodeId(address.nodeId);
    import_types.GroupId.assertGroupId(groupId);
    const fabric = this.fabricFor(address);
    const { key, keySetId, sessionId } = fabric.groups.currentKeyForGroup(groupId);
    if (sessionId === void 0 || key === void 0) {
      throw new import_general.UnexpectedDataError(
        `No group session data found for group ${groupId} in fabric ${fabric.fabricId}.`
      );
    }
    const session = this.#groupSessions.get(fabric.nodeId)?.get("id", sessionId);
    if (session) {
      return session;
    }
    return await import_GroupSession.GroupSession.create({
      transports,
      manager: this,
      id: sessionId,
      fabric,
      keySetId,
      operationalGroupKey: key,
      groupNodeId: address.nodeId
    });
  }
  /**
   * Obtain a Group session for an incoming packet.
   *
   * The session ID is determined by decrypting the packet with possible keys.
   *
   * Note that the resulting session is non-operational in the sense that attempting outbound communication will
   * result in an error.
   */
  groupSessionFromPacket(packet, aad) {
    const groupId = packet.header.destGroupId;
    if (groupId === void 0) {
      throw new import_general.UnexpectedDataError("Group ID is required for GroupSession fromPacket.");
    }
    import_types.GroupId.assertGroupId((0, import_types.GroupId)(groupId));
    const { message, key, sessionId, sourceNodeId, keySetId, fabric } = import_GroupSession.GroupSession.decode(
      this.#context.fabrics,
      packet,
      aad
    );
    let session = this.#groupSessions.get(sourceNodeId)?.get("id", sessionId);
    if (session === void 0) {
      session = new import_GroupSession.GroupSession({
        manager: this,
        id: sessionId,
        fabric,
        keySetId,
        operationalGroupKey: key,
        peerNodeId: sourceNodeId
      });
    }
    return { session, message, key };
  }
  registerGroupSession(session) {
    const sourceNodeId = session.peerNodeId;
    const peerSessions = this.#groupSessions.get(sourceNodeId) ?? new import_general.BasicSet();
    peerSessions.add(session);
    this.#groupSessions.set(sourceNodeId, peerSessions);
  }
  removeGroupSession(session) {
    const sourceNodeId = session.peerNodeId;
    const peerSessions = this.#groupSessions.get(sourceNodeId);
    if (peerSessions) {
      peerSessions.delete(session);
      if (peerSessions.size === 0) {
        this.#groupSessions.delete(sourceNodeId);
      }
    }
  }
  #asExposedResumptionRecord(record) {
    return { ...record, fabric: this.#fabricForId(record.fabricId, record.fabricIndex) };
  }
  findResumptionRecordById(resumptionId) {
    this.#construction.assert();
    const record = [...this.#resumptionRecords.values()].find(
      (record2) => import_general.Bytes.areEqual(record2.resumptionId, resumptionId)
    );
    if (record !== void 0) {
      return this.#asExposedResumptionRecord(record);
    }
  }
  findResumptionRecordByAddress(address) {
    this.#construction.assert();
    const record = this.#resumptionRecords.get(address);
    if (record !== void 0) {
      return this.#asExposedResumptionRecord(record);
    }
  }
  async saveResumptionRecord(resumptionRecord) {
    await this.#construction;
    const { fabric, ...rest } = resumptionRecord;
    const record = {
      ...rest,
      fabricId: fabric.fabricId,
      fabricIndex: fabric.fabricIndex
    };
    this.#resumptionRecords.set(fabric.addressOf(resumptionRecord.peerNodeId), record);
    await this.#storeResumptionRecords();
  }
  async #storeResumptionRecords() {
    await this.#construction;
    await this.#context.storage.set(
      "resumptionRecords",
      [...this.#resumptionRecords].map(
        ([
          address,
          {
            sharedSecret,
            resumptionId,
            peerNodeId,
            fabricId,
            fabricIndex,
            sessionParameters,
            caseAuthenticatedTags
          }
        ]) => ({
          nodeId: address.nodeId,
          sharedSecret,
          resumptionId,
          fabricId,
          fabricIndex,
          peerNodeId,
          sessionParameters: {
            ...sessionParameters,
            supportedTransports: sessionParameters.supportedTransports ? import_SupportedTransportsBitmap.SupportedTransportsSchema.encode(sessionParameters.supportedTransports) : void 0
          },
          caseAuthenticatedTags
        })
      )
    );
  }
  #maybeFabricForId(fabricId, fabricIndex) {
    return this.#context.fabrics.find(
      (fabric) => fabric.fabricId === fabricId && // Backward compatibility logic: fabricIndex was added later (0.15.5), so it might be undefined in older records
      (fabricIndex === void 0 || fabric.fabricIndex === fabricIndex)
    );
  }
  #fabricForId(fabricId, fabricIndex) {
    const fabric = this.#maybeFabricForId(fabricId, fabricIndex);
    if (fabric === void 0) {
      throw new import_general.InternalError(`Fabric not found for ID=${fabricId}, index=${fabricIndex}`);
    }
    return fabric;
  }
  async #initialize() {
    await this.#context.fabrics.construction;
    const storedResumptionRecords = await this.#context.storage.get(
      "resumptionRecords",
      []
    );
    storedResumptionRecords.forEach(
      ({
        nodeId,
        sharedSecret,
        resumptionId,
        fabricId,
        fabricIndex,
        peerNodeId,
        sessionParameters,
        caseAuthenticatedTags
      }) => {
        const fabric = this.#maybeFabricForId(fabricId, fabricIndex);
        if (!fabric) {
          logger.warn(
            `Ignoring resumption record for fabric 0x${(0, import_general.toHex)(fabricId)} and index ${fabricIndex} because we cannot find a matching fabric`
          );
          return;
        }
        logger.info(
          "restoring resumption record for node",
          fabric.addressOf(nodeId).toString(),
          "and peer node",
          fabric.addressOf(peerNodeId).toString(),
          "for fabric id",
          `0x${(0, import_general.toHex)(fabric.fabricId)}`,
          `(0x${(0, import_general.toHex)(fabric.rootVendorId)}, "${fabric?.label}")`
        );
        this.#resumptionRecords.set(fabric.addressOf(nodeId), {
          sharedSecret,
          resumptionId,
          fabricId,
          fabricIndex: fabric.fabricIndex,
          peerNodeId,
          // Make sure to initialize default values when restoring an older resumption record
          sessionParameters: (0, import_SessionParameters.SessionParameters)(sessionParameters),
          caseAuthenticatedTags
        });
      }
    );
  }
  getActiveSessionInformation() {
    this.#construction.assert();
    return [...this.#sessions].filter((session) => session.isSecure && !session.isPase).map((session) => ({
      name: `${session.via}`,
      nodeId: session.nodeId,
      peerNodeId: session.peerNodeId,
      fabric: session instanceof import_SecureSession.SecureSession ? session.fabric?.externalInformation : void 0,
      isPeerActive: session.isPeerActive,
      secure: session.isSecure,
      lastInteractionTimestamp: session instanceof import_SecureSession.SecureSession ? session.timestamp : void 0,
      lastActiveTimestamp: session instanceof import_SecureSession.SecureSession ? session.activeTimestamp : void 0,
      numberOfActiveSubscriptions: session instanceof import_SecureSession.SecureSession ? session.subscriptions.size : 0
    }));
  }
  async close() {
    if (this.#construction.status === import_general.Lifecycle.Status.Initializing) {
      await this.#construction;
    }
    this.#observers.close();
    await this.closeAllSessions();
  }
  async clear() {
    if (this.#construction.status === import_general.Lifecycle.Status.Initializing) {
      await this.#construction;
    }
    await this.closeAllSessions();
    await this.#context.storage.clearAll();
    this.#resumptionRecords.clear();
  }
  async closeAllSessions() {
    if (this.#construction.status === import_general.Lifecycle.Status.Initializing) {
      await this.#construction;
    }
    await this.#subscriptionUpdateMutex;
    const context = { cause: new ShutdownError("Session closed by node shutdown") };
    const closePromises = this.#sessions.map(async (session) => {
      await session.closeSubscriptions(true);
      await session.initiateForceClose(context);
      this.#sessions.delete(session);
    });
    for (const session of this.#unsecuredSessions.values()) {
      closePromises.push(session.initiateForceClose(context));
    }
    for (const sessions of this.#groupSessions.values()) {
      for (const session of sessions) {
        closePromises.push(session.initiateForceClose(context));
      }
    }
    await import_general.MatterAggregateError.allSettled(closePromises, "Error closing sessions").catch(
      (error) => logger.error(error)
    );
  }
  /**
   * Compress range of IDs.  This is intended for testing.
   */
  compressIdRange(upperBound) {
    this.#idUpperBound = upperBound;
    this.#nextSessionId = this.#context.fabrics.crypto.randomUint32 % upperBound;
    if (this.#nextSessionId === 0) this.#nextSessionId++;
  }
}
//# sourceMappingURL=SessionManager.js.map
