var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
var __typeError = (msg) => {
  throw TypeError(msg);
};
var __using = (stack, value, async) => {
  if (value != null) {
    if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
    var dispose, inner;
    if (async) dispose = value[__knownSymbol("asyncDispose")];
    if (dispose === void 0) {
      dispose = value[__knownSymbol("dispose")];
      if (async) inner = dispose;
    }
    if (typeof dispose !== "function") __typeError("Object not disposable");
    if (inner) dispose = function() {
      try {
        inner.call(this);
      } catch (e) {
        return Promise.reject(e);
      }
    };
    stack.push([async, dispose, value]);
  } else if (async) {
    stack.push([async]);
  }
  return value;
};
var __callDispose = (stack, error, hasError) => {
  var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
    return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
  };
  var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
  var next = (it) => {
    while (it = stack.pop()) {
      try {
        var result = it[1] && it[1].call(it[2]);
        if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
      } catch (e) {
        fail(e);
      }
    }
    if (hasError) throw error;
  };
  return next();
};
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { ClientInteraction } from "#action/client/ClientInteraction.js";
import { BleChannel, BleChannelClosedError } from "#ble/Ble.js";
import { CertificateAuthority } from "#certificate/CertificateAuthority.js";
import { DiscoveryDataDiagnostics } from "#common/Scanner.js";
import { CommissioningConnection } from "#peer/CommissioningConnection.js";
import { CommissioningError, PairRetransmissionLimitReachedError } from "#peer/CommissioningError.js";
import {
  ControllerCommissioningFlow,
  NodeIdConflictError
} from "#peer/ControllerCommissioningFlow.js";
import { SessionClosedError } from "#protocol/errors.js";
import { ExchangeManager } from "#protocol/ExchangeManager.js";
import { DedicatedChannelExchangeProvider } from "#protocol/ExchangeProvider.js";
import { ChannelStatusResponseError } from "#securechannel/SecureChannelMessenger.js";
import { PaseClient } from "#session/pase/PaseClient.js";
import { SessionManager } from "#session/SessionManager.js";
import {
  Abort,
  asError,
  causedBy,
  ChannelType,
  Environmental,
  ImplementationError,
  isIPv6,
  Logger,
  Millis,
  NoResponseTimeoutError,
  Seconds,
  ServerAddress,
  TransportSet
} from "@matter/general";
import { FabricIndex, NodeId, SECURE_CHANNEL_PROTOCOL_ID, SecureChannelStatusCode } from "@matter/types";
import { GeneralCommissioning } from "@matter/types/clusters/general-commissioning";
import { DclCertificateService } from "../dcl/DclCertificateService.js";
import { CommissioningTransitionError, PeerCommunicationError } from "./PeerCommunicationError.js";
import { PeerSet } from "./PeerSet.js";
const logger = Logger.get("ControllerCommissioner");
class ControllerCommissioner {
  #context;
  #paseClient;
  constructor(context) {
    this.#context = context;
    this.#paseClient = new PaseClient(context.sessions);
  }
  static [Environmental.create](env) {
    const instance = new ControllerCommissioner({
      peers: env.get(PeerSet),
      transports: env.get(TransportSet),
      sessions: env.get(SessionManager),
      exchanges: env.get(ExchangeManager),
      ca: env.get(CertificateAuthority),
      environment: env
    });
    env.set(ControllerCommissioner, instance);
    return instance;
  }
  /**
   * Commission a previously discovered node.
   */
  async commission(options) {
    const {
      passcode,
      addresses,
      discoveryData,
      fabric,
      nodeId,
      abort,
      claimNodeIdAfterPase,
      timeout = Seconds(30)
    } = options;
    this.#assertRequestedNodeIdAvailable(fabric, nodeId);
    this.#validateCommissioningOptions(options);
    const addressCandidates = this.#addressesToCandidates(addresses, discoveryData);
    const { session } = await this.#establishPaseFromCandidates({
      devices: addressCandidates,
      timeout,
      passcode,
      retryFailureAsPeerCommunication: "Could not connect to device",
      abort
    });
    let assignedNodeId = nodeId;
    if (claimNodeIdAfterPase !== void 0) {
      try {
        assignedNodeId = await claimNodeIdAfterPase();
      } catch (error) {
        await session.initiateForceClose({ cause: new CommissioningError("Claiming the node ID failed after PASE") }).catch(
          (closeError) => logger.info("Error closing PASE session after failed node ID claim:", closeError)
        );
        throw error;
      }
      if (assignedNodeId === void 0) {
        await session.initiateForceClose({
          cause: new CommissioningError("PASE established but other device connected faster")
        });
        throw new CommissioningError(
          "Commissioning cancelled: another device was already successfully connected"
        );
      }
    }
    return await this.#commissionConnectedNode(session, { ...options, nodeId: assignedNodeId }, discoveryData);
  }
  /**
   * Establishes a PASE session with a known device without running a commissioning flow.
   *
   * All provided addresses are tried in parallel.  The first to complete PASE wins; the rest are cancelled
   * via an abort signal.  A credential failure (wrong passcode) on any address immediately cancels all
   * other in-flight attempts for this device.
   */
  async establishPase(options) {
    const { addresses, discoveryData, passcode, timeout = Seconds(30), abort, continueAfterPase } = options;
    const candidates = this.#addressesToCandidates(addresses, discoveryData);
    const { session } = await this.#establishPaseFromCandidates({
      devices: candidates,
      timeout,
      passcode,
      retryFailureAsPeerCommunication: "Could not connect to device",
      abort
    });
    if (continueAfterPase !== void 0 && !continueAfterPase()) {
      await session.initiateForceClose({
        cause: new CommissioningError("PASE established but another candidate already won")
      });
      throw new CommissioningError("PASE cancelled: another candidate was already selected");
    }
    return { paseSession: session, discoveryData };
  }
  async #establishPaseFromCandidates(options) {
    try {
      return await CommissioningConnection({
        devices: options.devices,
        timeout: options.timeout,
        externalAbort: options.abort,
        establishSession: (address, device, signal) => this.#establishEphemeralNodeSession(address, options.passcode, device, signal)
      });
    } catch (error) {
      if (options.retryFailureAsPeerCommunication !== void 0 && causedBy(error, PairRetransmissionLimitReachedError)) {
        throw new PeerCommunicationError(options.retryFailureAsPeerCommunication);
      }
      throw error;
    }
  }
  /**
   * Method to start commission process with a PASE pairing.
   * If this not successful and throws an RetransmissionLimitReachedError the address is invalid or the passcode
   * is wrong.
   */
  async #establishEphemeralNodeSession(address, passcode, device, signal) {
    let paseChannel;
    if (device !== void 0) {
      logger.info(`Establish PASE to device`, DiscoveryDataDiagnostics(device));
    }
    if (ServerAddress.isIp(address)) {
      const { ip } = address;
      const isIpv6Address = isIPv6(ip);
      const paseInterface = this.#context.transports.interfaceFor(
        ChannelType.UDP,
        isIpv6Address ? "::" : "0.0.0.0"
      );
      if (paseInterface === void 0) {
        throw new PairRetransmissionLimitReachedError(
          `IPv${isIpv6Address ? "6" : "4"} interface not initialized. Cannot use ${ip} for commissioning.`
        );
      }
      paseChannel = await Abort.attempt(signal, paseInterface.openChannel(address));
    } else if (ServerAddress.isBle(address)) {
      const ble = this.#context.transports.interfaceFor(ChannelType.BLE);
      if (!ble) {
        throw new PairRetransmissionLimitReachedError(
          `BLE interface not initialized. Cannot use ${address.peripheralAddress} for commissioning.`
        );
      }
      paseChannel = await Abort.attempt(signal, ble.openChannel(address));
    } else {
      throw new ImplementationError(`Unsupported address type for Matter PASE commissioning`);
    }
    const unsecuredSession = this.#context.sessions.createUnsecuredSession({
      channel: paseChannel,
      // Use the session parameters from MDNS announcements when available and rest is assumed to be fallbacks
      sessionParameters: {
        idleInterval: Millis(device?.SII),
        activeInterval: Millis(device?.SAI),
        activeThreshold: Millis(device?.SAT)
      },
      isInitiator: true
    });
    const paseExchange = this.#context.exchanges.initiateExchangeForSession(
      unsecuredSession,
      SECURE_CHANNEL_PROTOCOL_ID
    );
    try {
      const caseSession = await this.#paseClient.pair(
        this.#context.sessions.sessionParameters,
        paseExchange,
        paseChannel,
        passcode,
        { abort: signal }
      );
      unsecuredSession.detachChannel();
      return caseSession;
    } catch (e) {
      if (causedBy(e, ChannelStatusResponseError)) {
        throw new NoResponseTimeoutError(
          `Establishing PASE channel failed with channel status response error ${asError(e).message}`
        );
      }
      throw e;
    } finally {
      await unsecuredSession.initiateForceClose({
        cause: new CommissioningTransitionError("PASE session has transitioned to CASE")
      });
    }
  }
  /** Validate if a Peer Address is already known and commissioned */
  #assertPeerAddress(address) {
    if (this.#context.peers.has(address)) {
      throw new NodeIdConflictError(`Node ID ${address.nodeId} is already commissioned and can not be reused`);
    }
  }
  #assertRequestedNodeIdAvailable(fabric, nodeId) {
    if (nodeId !== void 0) {
      this.#assertPeerAddress(fabric.addressOf(nodeId));
    }
  }
  #validateCommissioningOptions(options) {
    if (options.threadNetwork !== void 0) {
      const { operationalDataset } = options.threadNetwork;
      if (operationalDataset.length === 0) {
        throw new CommissioningError("Thread operational dataset must not be empty");
      }
      if (operationalDataset.length % 2 !== 0) {
        throw new CommissioningError("Thread operational dataset must have an even number of hex characters");
      }
      if (!/^[0-9a-fA-F]+$/.test(operationalDataset)) {
        throw new CommissioningError(
          "Thread operational dataset must only contain valid hexadecimal characters"
        );
      }
    }
    if (options.wifiNetwork !== void 0) {
      const { wifiSsid } = options.wifiNetwork;
      if (wifiSsid.length === 0) {
        throw new CommissioningError("Wi-Fi SSID must not be empty");
      }
    }
  }
  /**
   * Maps addresses to synthetic {@link CommissionableDevice} candidates for use with
   * {@link CommissioningConnection}.  Each address becomes its own candidate so a credential failure on one
   * does not cancel attempts on others.  UDP is partitioned ahead of BLE, preserving input order within each
   * group — the caller's {@link ServerAddressSet.compareDesirability} ranking is load-bearing.
   */
  #addressesToCandidates(addresses, discoveryData) {
    const sorted = [...addresses].sort((a, b) => ServerAddress.isIp(a) ? -1 : ServerAddress.isIp(b) ? 1 : 0);
    return sorted.map((address, index) => ({
      ...discoveryData ?? {},
      addresses: [address],
      deviceIdentifier: `known-address-${index}-${ServerAddress.urlFor(address)}`,
      D: 0,
      CM: 1
    }));
  }
  /** Finds an unused random Node-ID to use for commissioning if not already provided. */
  #determineAddress(fabric, nodeId) {
    while (true) {
      const address = fabric.addressOf(nodeId ?? NodeId.randomOperationalNodeId(fabric.crypto));
      try {
        this.#assertPeerAddress(address);
      } catch (error) {
        if (error instanceof CommissioningError && nodeId !== void 0) {
          throw error;
        }
        continue;
      }
      return address;
    }
  }
  /**
   * Method to commission a device with a PASE secure channel. It returns the NodeId of the commissioned device on
   * success.
   */
  async #commissionConnectedNode(ephemeralSession, options, discoveryData) {
    var _stack = [];
    try {
      const {
        fabric,
        finalizeCommissioning: performCaseCommissioning,
        commissioningFlowImpl = ControllerCommissioningFlow,
        caseConnectionTiming
      } = options;
      const commissioningOptions = {
        ...options,
        regulatoryLocation: options.regulatoryLocation ?? GeneralCommissioning.RegulatoryLocationType.Outdoor,
        regulatoryCountryCode: options.regulatoryCountryCode ?? "XX"
      };
      discoveryData = discoveryData ?? {};
      discoveryData.SII = ephemeralSession.parameters.idleInterval;
      discoveryData.SAI = ephemeralSession.parameters.activeInterval;
      discoveryData.SAT = ephemeralSession.parameters.activeThreshold;
      const address = this.#determineAddress(fabric, commissioningOptions.nodeId);
      logger.info(`Start commissioning of node ${address.toString()} into fabric ${fabric.fabricId}`);
      const exchangeProvider = new DedicatedChannelExchangeProvider(this.#context.exchanges, ephemeralSession);
      if (!ephemeralSession.isClosed) {
        const paseChannel = ephemeralSession.channel.transportChannel;
        if (paseChannel instanceof BleChannel) {
          paseChannel.closed.once(() => {
            ephemeralSession.initiateForceClose({
              cause: new BleChannelClosedError(`BLE transport closed on ${ephemeralSession.via}`)
            }).catch((error) => {
              if (error instanceof SessionClosedError) return;
              logger.warn("Error while force-closing PASE session on BLE close", error);
            });
          });
        }
      }
      const commissioner = __using(_stack, new commissioningFlowImpl(
        new ClientInteraction({
          environment: this.#context.environment,
          exchangeProvider,
          address
        }),
        this.#context.ca,
        fabric,
        {
          ...commissioningOptions,
          attestation: {
            challengeKey: ephemeralSession.attestationChallengeKey,
            dclCertificateService: this.#context.environment.has(DclCertificateService) ? this.#context.environment.get(DclCertificateService) : void 0,
            onFailure: options.onAttestationFailure
          }
        },
        async (address2, supportsConcurrentConnections) => {
          if (!supportsConcurrentConnections) {
            await ephemeralSession.initiateForceClose({
              cause: new CommissioningTransitionError(
                "Commissioning session closed because node has now joined fabric"
              )
            });
          }
          if (performCaseCommissioning !== void 0) {
            await performCaseCommissioning(address2, discoveryData);
            return;
          }
          const peer = this.#context.peers.for(address2);
          peer.descriptor.discoveryData = discoveryData;
          peer.descriptor.sessionParameters = ephemeralSession.parameters;
          await peer.connect({
            // 4m15s allows two ~2-minute server-side retry windows to complete before we abort.
            connectionTimeout: Seconds(255),
            timing: caseConnectionTiming,
            handleError: (error) => {
              const csre = ChannelStatusResponseError.of(error);
              if (csre?.protocolStatusCode === SecureChannelStatusCode.NoSharedTrustRoots) {
                logger.warn("Peer reports no shared trust roots during commissioning, retrying quickly");
                return Seconds(15);
              }
            }
          });
          return new ClientInteraction({
            environment: this.#context.environment,
            exchangeProvider: peer.exchangeProvider,
            address: address2
          });
        }
      ), true);
      let fabricIndexOnPeer;
      try {
        await commissioner.executeCommissioning();
        const captured = commissioner.fabricIndexOnPeer;
        fabricIndexOnPeer = captured === FabricIndex.NO_FABRIC ? void 0 : captured;
      } catch (error) {
        await this.#context.peers.get(address)?.delete();
        throw error;
      } finally {
        commissioner.close();
        await ephemeralSession.initiateForceClose({
          cause: new CommissioningTransitionError(
            "Commissioning session closed because node has now joined fabric"
          )
        });
      }
      return { address, fabricIndexOnPeer };
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      var _promise = __callDispose(_stack, _error, _hasError);
      _promise && await _promise;
    }
  }
}
export {
  ControllerCommissioner
};
//# sourceMappingURL=ControllerCommissioner.js.map
