[Fabric-Sync] Port 'add-bridge' and 'remove-bridge' commands (#36241)

* [Fabric-Sync] Implemenat 'add-bridge' command

* Address review comments
diff --git a/examples/fabric-sync/BUILD.gn b/examples/fabric-sync/BUILD.gn
index de193bd..95b0ce3 100644
--- a/examples/fabric-sync/BUILD.gn
+++ b/examples/fabric-sync/BUILD.gn
@@ -22,21 +22,35 @@
 executable("fabric-sync") {
   cflags = [ "-Wconversion" ]
 
+  defines = []
+
+  if (chip_build_libshell) {
+    defines += [ "ENABLE_CHIP_SHELL" ]
+  }
+
   include_dirs = [
     ".",
     "${chip_root}/src/lib",
   ]
 
+  if (chip_build_libshell) {
+    include_dirs += [ "shell" ]
+  }
+
   sources = [ "main.cpp" ]
 
   deps = [
+    "${chip_root}/examples/fabric-sync/admin:fabric-admin-lib",
     "${chip_root}/examples/fabric-sync/bridge:fabric-bridge-lib",
-    "${chip_root}/examples/fabric-sync/bridge:fabric-bridge-zap",
     "${chip_root}/examples/platform/linux:app-main",
     "${chip_root}/src/lib",
     "${chip_root}/third_party/inipp",
   ]
 
+  if (chip_build_libshell) {
+    deps += [ "${chip_root}/examples/fabric-sync/shell" ]
+  }
+
   output_dir = root_out_dir
 }
 
diff --git a/examples/fabric-sync/admin/BUILD.gn b/examples/fabric-sync/admin/BUILD.gn
new file mode 100644
index 0000000..c2ab715
--- /dev/null
+++ b/examples/fabric-sync/admin/BUILD.gn
@@ -0,0 +1,30 @@
+# Copyright (c) 2024 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build_overrides/chip.gni")
+import("${chip_root}/src/app/chip_data_model.gni")
+
+source_set("fabric-admin-lib") {
+  sources = [
+    "DeviceManager.cpp",
+    "DeviceManager.h",
+    "PairingManager.cpp",
+    "PairingManager.h",
+  ]
+
+  deps = [
+    "${chip_root}/examples/fabric-sync/bridge:fabric-bridge-lib",
+    "${chip_root}/src/lib",
+  ]
+}
diff --git a/examples/fabric-sync/admin/DeviceManager.cpp b/examples/fabric-sync/admin/DeviceManager.cpp
new file mode 100644
index 0000000..79f41d5
--- /dev/null
+++ b/examples/fabric-sync/admin/DeviceManager.cpp
@@ -0,0 +1,111 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#include "DeviceManager.h"
+
+#include <crypto/RandUtils.h>
+#include <lib/support/StringBuilder.h>
+
+#include <cstdio>
+#include <string>
+
+using namespace chip;
+
+// Define the static member
+DeviceManager DeviceManager::sInstance;
+
+void DeviceManager::Init()
+{
+    // TODO: (#34113) Init mLastUsedNodeId from chip config file
+    mLastUsedNodeId = 1;
+    mInitialized    = true;
+
+    ChipLogProgress(NotSpecified, "DeviceManager initialized: last used nodeId " ChipLogFormatX64,
+                    ChipLogValueX64(mLastUsedNodeId));
+}
+
+NodeId DeviceManager::GetNextAvailableNodeId()
+{
+    mLastUsedNodeId++;
+    VerifyOrDieWithMsg(mLastUsedNodeId < std::numeric_limits<NodeId>::max(), NotSpecified, "No more available NodeIds.");
+
+    return mLastUsedNodeId;
+}
+
+void DeviceManager::UpdateLastUsedNodeId(NodeId nodeId)
+{
+    if (nodeId > mLastUsedNodeId)
+    {
+        mLastUsedNodeId = nodeId;
+        ChipLogProgress(NotSpecified, "Updating last used NodeId to " ChipLogFormatX64, ChipLogValueX64(mLastUsedNodeId));
+    }
+}
+
+void DeviceManager::SetRemoteBridgeNodeId(chip::NodeId nodeId)
+{
+    mRemoteBridgeNodeId = nodeId;
+}
+
+CHIP_ERROR DeviceManager::PairRemoteFabricBridge(NodeId nodeId, uint32_t setupPINCode, const char * deviceRemoteIp,
+                                                 uint16_t deviceRemotePort)
+{
+    CHIP_ERROR err = PairingManager::Instance().PairDevice(nodeId, setupPINCode, deviceRemoteIp, deviceRemotePort);
+
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(NotSpecified,
+                     "Failed to pair remote fabric bridge: Node ID " ChipLogFormatX64 " with error: %" CHIP_ERROR_FORMAT,
+                     ChipLogValueX64(nodeId), err.Format());
+        return err;
+    }
+
+    ChipLogProgress(NotSpecified, "Successfully paired remote fabric bridge: Node ID " ChipLogFormatX64, ChipLogValueX64(nodeId));
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR DeviceManager::UnpairRemoteFabricBridge()
+{
+    if (mRemoteBridgeNodeId == kUndefinedNodeId)
+    {
+        ChipLogError(NotSpecified, "Remote bridge node ID is undefined; cannot unpair device.");
+        return CHIP_ERROR_INCORRECT_STATE;
+    }
+
+    CHIP_ERROR err = PairingManager::Instance().UnpairDevice(mRemoteBridgeNodeId);
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(NotSpecified, "Failed to unpair remote bridge device " ChipLogFormatX64, ChipLogValueX64(mRemoteBridgeNodeId));
+        return err;
+    }
+
+    ChipLogProgress(NotSpecified, "Successfully unpaired remote fabric bridge: Node ID " ChipLogFormatX64,
+                    ChipLogValueX64(mRemoteBridgeNodeId));
+    return CHIP_NO_ERROR;
+}
+
+void DeviceManager::OnDeviceRemoved(NodeId deviceId, CHIP_ERROR err)
+{
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(NotSpecified, "Failed to remove synced device:(" ChipLogFormatX64 ") with error: %" CHIP_ERROR_FORMAT,
+                     ChipLogValueX64(deviceId), err.Format());
+        return;
+    }
+
+    ChipLogProgress(NotSpecified, "Synced device with NodeId:" ChipLogFormatX64 " has been removed.", ChipLogValueX64(deviceId));
+}
diff --git a/examples/fabric-sync/admin/DeviceManager.h b/examples/fabric-sync/admin/DeviceManager.h
new file mode 100644
index 0000000..5615df5
--- /dev/null
+++ b/examples/fabric-sync/admin/DeviceManager.h
@@ -0,0 +1,99 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#pragma once
+
+#include "PairingManager.h"
+
+#include <app-common/zap-generated/cluster-objects.h>
+#include <platform/CHIPDeviceLayer.h>
+
+class DeviceManager : public PairingDelegate
+{
+public:
+    DeviceManager() = default;
+
+    void Init();
+
+    chip::NodeId GetNextAvailableNodeId();
+
+    chip::NodeId GetRemoteBridgeNodeId() const { return mRemoteBridgeNodeId; }
+
+    void UpdateLastUsedNodeId(chip::NodeId nodeId);
+
+    void SetRemoteBridgeNodeId(chip::NodeId nodeId);
+
+    bool IsFabricSyncReady() const { return mRemoteBridgeNodeId != chip::kUndefinedNodeId; }
+
+    /**
+     * @brief Determines whether a given nodeId corresponds to the remote bridge device.
+     *
+     * @param nodeId            The ID of the node being checked.
+     *
+     * @return true if the nodeId matches the remote bridge device; otherwise, false.
+     */
+    bool IsCurrentBridgeDevice(chip::NodeId nodeId) const { return nodeId == mRemoteBridgeNodeId; }
+
+    /**
+     * @brief Pair a remote fabric bridge with a given node ID.
+     *
+     * This function initiates the pairing process for a remote fabric bridge using the specified parameters.
+
+     * @param nodeId            The user-defined ID for the node being commissioned. It doesn’t need to be the same ID,
+     *                          as for the first fabric.
+     * @param setupPINCode      The setup PIN code used to authenticate the pairing process.
+     * @param deviceRemoteIp    The IP address of the remote device that is being paired as part of the fabric bridge.
+     * @param deviceRemotePort  The secured device port of the remote device that is being paired as part of the fabric bridge.
+     *
+     * @return CHIP_ERROR       Returns CHIP_NO_ERROR on success or an appropriate error code on failure.
+     */
+    CHIP_ERROR PairRemoteFabricBridge(chip::NodeId nodeId, uint32_t setupPINCode, const char * deviceRemoteIp,
+                                      uint16_t deviceRemotePort);
+
+    CHIP_ERROR UnpairRemoteFabricBridge();
+
+private:
+    friend DeviceManager & DeviceMgr();
+
+    void OnDeviceRemoved(chip::NodeId deviceId, CHIP_ERROR err) override;
+
+    static DeviceManager sInstance;
+
+    chip::NodeId mLastUsedNodeId = 0;
+
+    // The Node ID of the remote bridge used for Fabric-Sync
+    // This represents the bridge on the other ecosystem.
+    chip::NodeId mRemoteBridgeNodeId = chip::kUndefinedNodeId;
+
+    bool mInitialized = false;
+};
+
+/**
+ * Returns the public interface of the DeviceManager singleton object.
+ *
+ * Applications should use this to access features of the DeviceManager
+ * object.
+ */
+inline DeviceManager & DeviceMgr()
+{
+    if (!DeviceManager::sInstance.mInitialized)
+    {
+        DeviceManager::sInstance.Init();
+    }
+    return DeviceManager::sInstance;
+}
diff --git a/examples/fabric-sync/admin/PairingManager.cpp b/examples/fabric-sync/admin/PairingManager.cpp
new file mode 100644
index 0000000..06c1f2b
--- /dev/null
+++ b/examples/fabric-sync/admin/PairingManager.cpp
@@ -0,0 +1,550 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#include "PairingManager.h"
+
+#include <netdb.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include <lib/support/logging/CHIPLogging.h>
+#include <setup_payload/ManualSetupPayloadParser.h>
+#include <setup_payload/QRCodeSetupPayloadParser.h>
+
+using namespace ::chip;
+using namespace ::chip::Controller;
+
+namespace {
+
+CHIP_ERROR GetPayload(const char * setUpCode, SetupPayload & payload)
+{
+    VerifyOrReturnValue(setUpCode, CHIP_ERROR_INVALID_ARGUMENT);
+    bool isQRCode = strncmp(setUpCode, kQRCodePrefix, strlen(kQRCodePrefix)) == 0;
+    if (isQRCode)
+    {
+        ReturnErrorOnFailure(QRCodeSetupPayloadParser(setUpCode).populatePayload(payload));
+        VerifyOrReturnError(payload.isValidQRCodePayload(), CHIP_ERROR_INVALID_ARGUMENT);
+    }
+    else
+    {
+        ReturnErrorOnFailure(ManualSetupPayloadParser(setUpCode).populatePayload(payload));
+        VerifyOrReturnError(payload.isValidManualCode(), CHIP_ERROR_INVALID_ARGUMENT);
+    }
+
+    return CHIP_NO_ERROR;
+}
+
+bool ParseAddressWithInterface(const char * addressString, Inet::IPAddress & address, Inet::InterfaceId & interfaceId)
+{
+    struct addrinfo hints;
+    struct addrinfo * result;
+    int ret;
+
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_family   = AF_UNSPEC;
+    hints.ai_socktype = SOCK_DGRAM;
+    ret               = getaddrinfo(addressString, nullptr, &hints, &result);
+    if (ret < 0)
+    {
+        ChipLogError(NotSpecified, "Invalid address: %s", addressString);
+        return false;
+    }
+
+    if (result->ai_family == AF_INET6)
+    {
+        struct sockaddr_in6 * addr = reinterpret_cast<struct sockaddr_in6 *>(result->ai_addr);
+        address                    = Inet::IPAddress::FromSockAddr(*addr);
+        interfaceId                = Inet::InterfaceId(addr->sin6_scope_id);
+    }
+#if INET_CONFIG_ENABLE_IPV4
+    else if (result->ai_family == AF_INET)
+    {
+        address     = Inet::IPAddress::FromSockAddr(*reinterpret_cast<struct sockaddr_in *>(result->ai_addr));
+        interfaceId = Inet::InterfaceId::Null();
+    }
+#endif // INET_CONFIG_ENABLE_IPV4
+    else
+    {
+        ChipLogError(NotSpecified, "Unsupported address: %s", addressString);
+        freeaddrinfo(result);
+        return false;
+    }
+
+    freeaddrinfo(result);
+    return true;
+}
+
+} // namespace
+
+PairingManager::PairingManager() :
+    mOnOpenCommissioningWindowCallback(OnOpenCommissioningWindowResponse, this),
+    mOnOpenCommissioningWindowVerifierCallback(OnOpenCommissioningWindowVerifierResponse, this),
+    mCurrentFabricRemoveCallback(OnCurrentFabricRemove, this)
+{}
+
+CHIP_ERROR PairingManager::Init(Controller::DeviceCommissioner * commissioner)
+{
+    VerifyOrReturnError(commissioner != nullptr, CHIP_ERROR_INCORRECT_STATE);
+
+    mCommissioner = commissioner;
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR PairingManager::OpenCommissioningWindow(NodeId nodeId, EndpointId endpointId, uint16_t commissioningTimeoutSec,
+                                                   uint32_t iterations, uint16_t discriminator, const ByteSpan & salt,
+                                                   const ByteSpan & verifier)
+{
+    if (mCommissioner == nullptr)
+    {
+        ChipLogError(NotSpecified, "Commissioner is null, cannot open commissioning window");
+        return CHIP_ERROR_INCORRECT_STATE;
+    }
+
+    // Check if a window is already open
+    if (mWindowOpener != nullptr)
+    {
+        ChipLogError(NotSpecified, "A commissioning window is already open");
+        return CHIP_ERROR_INCORRECT_STATE;
+    }
+
+    // Ensure salt and verifier sizes are valid
+    if (!salt.empty() && salt.size() > chip::Crypto::kSpake2p_Max_PBKDF_Salt_Length)
+    {
+        ChipLogError(NotSpecified, "Salt size exceeds buffer capacity");
+        return CHIP_ERROR_BUFFER_TOO_SMALL;
+    }
+
+    if (!verifier.empty() && verifier.size() > chip::Crypto::kSpake2p_VerifierSerialized_Length)
+    {
+        ChipLogError(NotSpecified, "Verifier size exceeds buffer capacity");
+        return CHIP_ERROR_BUFFER_TOO_SMALL;
+    }
+
+    if (!salt.empty())
+    {
+        memcpy(mSaltBuffer, salt.data(), salt.size());
+        mSalt = ByteSpan(mSaltBuffer, salt.size());
+    }
+    else
+    {
+        mSalt = ByteSpan();
+    }
+
+    if (!verifier.empty())
+    {
+        memcpy(mVerifierBuffer, verifier.data(), verifier.size());
+        mVerifier = ByteSpan(mVerifierBuffer, verifier.size());
+    }
+    else
+    {
+        mVerifier = ByteSpan();
+    }
+
+    return DeviceLayer::SystemLayer().ScheduleLambda([nodeId, endpointId, commissioningTimeoutSec, iterations, discriminator]() {
+        PairingManager & self = PairingManager::Instance();
+
+        if (self.mCommissioner == nullptr)
+        {
+            ChipLogError(NotSpecified, "Commissioner is null, cannot open commissioning window");
+            return;
+        }
+
+        self.mWindowOpener = Platform::MakeUnique<Controller::CommissioningWindowOpener>(self.mCommissioner);
+
+        if (!self.mVerifier.empty())
+        {
+            if (self.mSalt.empty())
+            {
+                ChipLogError(NotSpecified, "Salt is required when verifier is set");
+                self.mWindowOpener.reset();
+                return;
+            }
+
+            // Open the commissioning window with verifier parameters
+            CHIP_ERROR err =
+                self.mWindowOpener->OpenCommissioningWindow(Controller::CommissioningWindowVerifierParams()
+                                                                .SetNodeId(nodeId)
+                                                                .SetEndpointId(endpointId)
+                                                                .SetTimeout(commissioningTimeoutSec)
+                                                                .SetIteration(iterations)
+                                                                .SetDiscriminator(discriminator)
+                                                                .SetVerifier(self.mVerifier)
+                                                                .SetSalt(self.mSalt)
+                                                                .SetCallback(&self.mOnOpenCommissioningWindowVerifierCallback));
+            if (err != CHIP_NO_ERROR)
+            {
+                ChipLogError(NotSpecified, "Failed to open commissioning window with verifier: %s", ErrorStr(err));
+                self.mWindowOpener.reset();
+            }
+        }
+        else
+        {
+            SetupPayload ignored;
+            // Open the commissioning window with passcode parameters
+            CHIP_ERROR err = self.mWindowOpener->OpenCommissioningWindow(Controller::CommissioningWindowPasscodeParams()
+                                                                             .SetNodeId(nodeId)
+                                                                             .SetEndpointId(endpointId)
+                                                                             .SetTimeout(commissioningTimeoutSec)
+                                                                             .SetIteration(iterations)
+                                                                             .SetDiscriminator(discriminator)
+                                                                             .SetSetupPIN(NullOptional)
+                                                                             .SetSalt(NullOptional)
+                                                                             .SetCallback(&self.mOnOpenCommissioningWindowCallback),
+                                                                         ignored);
+            if (err != CHIP_NO_ERROR)
+            {
+                ChipLogError(NotSpecified, "Failed to open commissioning window with passcode: %s", ErrorStr(err));
+                self.mWindowOpener.reset();
+            }
+        }
+    });
+}
+
+void PairingManager::OnOpenCommissioningWindowResponse(void * context, NodeId remoteId, CHIP_ERROR err, SetupPayload payload)
+{
+    VerifyOrDie(context != nullptr);
+    PairingManager * self = static_cast<PairingManager *>(context);
+    if (self->mCommissioningWindowDelegate)
+    {
+        self->mCommissioningWindowDelegate->OnCommissioningWindowOpened(remoteId, err, payload);
+        self->SetOpenCommissioningWindowDelegate(nullptr);
+    }
+
+    OnOpenCommissioningWindowVerifierResponse(context, remoteId, err);
+}
+
+void PairingManager::OnOpenCommissioningWindowVerifierResponse(void * context, NodeId remoteId, CHIP_ERROR err)
+{
+    VerifyOrDie(context != nullptr);
+    PairingManager * self = static_cast<PairingManager *>(context);
+    LogErrorOnFailure(err);
+
+    // Reset the window opener once the window operation is complete
+    self->mWindowOpener.reset();
+}
+
+void PairingManager::OnStatusUpdate(DevicePairingDelegate::Status status)
+{
+    switch (status)
+    {
+    case DevicePairingDelegate::Status::SecurePairingSuccess:
+        ChipLogProgress(NotSpecified, "CASE establishment successful");
+        break;
+    case DevicePairingDelegate::Status::SecurePairingFailed:
+        ChipLogError(NotSpecified, "Secure Pairing Failed");
+        break;
+    }
+}
+
+void PairingManager::OnPairingComplete(CHIP_ERROR err)
+{
+    if (err == CHIP_NO_ERROR)
+    {
+        ChipLogProgress(NotSpecified, "PASE establishment successful");
+    }
+    else
+    {
+        ChipLogProgress(NotSpecified, "Pairing Failure: %s", ErrorStr(err));
+    }
+}
+
+void PairingManager::OnPairingDeleted(CHIP_ERROR err)
+{
+    if (err == CHIP_NO_ERROR)
+    {
+        ChipLogProgress(NotSpecified, "Pairing Deleted Success");
+    }
+    else
+    {
+        ChipLogProgress(NotSpecified, "Pairing Deleted Failure: %s", ErrorStr(err));
+    }
+}
+
+void PairingManager::OnCommissioningComplete(NodeId nodeId, CHIP_ERROR err)
+{
+    if (err == CHIP_NO_ERROR)
+    {
+        // print to console
+        fprintf(stderr, "New device with Node ID: " ChipLogFormatX64 " has been successfully added.\n", ChipLogValueX64(nodeId));
+    }
+    else
+    {
+        ChipLogProgress(NotSpecified, "Device commissioning Failure: %s", ErrorStr(err));
+    }
+
+    if (mCommissioningDelegate)
+    {
+        mCommissioningDelegate->OnCommissioningComplete(nodeId, err);
+        SetCommissioningDelegate(nullptr);
+    }
+}
+
+void PairingManager::OnReadCommissioningInfo(const Controller::ReadCommissioningInfo & info)
+{
+    ChipLogProgress(AppServer, "OnReadCommissioningInfo - vendorId=0x%04X productId=0x%04X", info.basic.vendorId,
+                    info.basic.productId);
+
+    // The string in CharSpan received from the device is not null-terminated, we use std::string here for coping and
+    // appending a null-terminator at the end of the string.
+    std::string userActiveModeTriggerInstruction;
+
+    // Note: the callback doesn't own the buffer, should make a copy if it will be used it later.
+    if (info.icd.userActiveModeTriggerInstruction.size() != 0)
+    {
+        userActiveModeTriggerInstruction =
+            std::string(info.icd.userActiveModeTriggerInstruction.data(), info.icd.userActiveModeTriggerInstruction.size());
+    }
+
+    if (info.icd.userActiveModeTriggerHint.HasAny())
+    {
+        ChipLogProgress(AppServer, "OnReadCommissioningInfo - LIT UserActiveModeTriggerHint=0x%08x",
+                        info.icd.userActiveModeTriggerHint.Raw());
+        ChipLogProgress(AppServer, "OnReadCommissioningInfo - LIT UserActiveModeTriggerInstruction=%s",
+                        userActiveModeTriggerInstruction.c_str());
+    }
+    ChipLogProgress(AppServer, "OnReadCommissioningInfo ICD - IdleModeDuration=%u activeModeDuration=%u activeModeThreshold=%u",
+                    info.icd.idleModeDuration, info.icd.activeModeDuration, info.icd.activeModeThreshold);
+}
+
+void PairingManager::OnDiscoveredDevice(const Dnssd::CommissionNodeData & nodeData)
+{
+    // Ignore nodes with closed commissioning window
+    VerifyOrReturn(nodeData.commissioningMode != 0);
+
+    auto & resolutionData = nodeData;
+
+    const uint16_t port = resolutionData.port;
+    char buf[Inet::IPAddress::kMaxStringLength];
+    resolutionData.ipAddress[0].ToString(buf);
+    ChipLogProgress(NotSpecified, "Discovered Device: %s:%u", buf, port);
+
+    // Stop Mdns discovery.
+    auto err = mCommissioner->StopCommissionableDiscovery();
+
+    // Some platforms does not implement a mechanism to stop mdns browse, so
+    // we just ignore CHIP_ERROR_NOT_IMPLEMENTED instead of bailing out.
+    if (CHIP_NO_ERROR != err && CHIP_ERROR_NOT_IMPLEMENTED != err)
+    {
+        return;
+    }
+
+    mCommissioner->RegisterDeviceDiscoveryDelegate(nullptr);
+
+    auto interfaceId = resolutionData.ipAddress[0].IsIPv6LinkLocal() ? resolutionData.interfaceId : Inet::InterfaceId::Null();
+    auto peerAddress = Transport::PeerAddress::UDP(resolutionData.ipAddress[0], port, interfaceId);
+    err              = Pair(mNodeId, peerAddress);
+    if (CHIP_NO_ERROR != err)
+    {
+        ChipLogProgress(NotSpecified, "Failed to pair device: " ChipLogFormatX64 " %s", ChipLogValueX64(mNodeId), ErrorStr(err));
+    }
+}
+
+Optional<uint16_t> PairingManager::FailSafeExpiryTimeoutSecs() const
+{
+    // No manual input, so do not need to extend.
+    return Optional<uint16_t>();
+}
+
+bool PairingManager::ShouldWaitAfterDeviceAttestation()
+{
+    // If there is a vendor ID and product ID, request OnDeviceAttestationCompleted().
+    // Currently this is added in the case that the example is performing reverse commissioning,
+    // but it would be an improvement to store that explicitly.
+    // TODO: Issue #35297 - [Fabric Sync] Improve where we get VID and PID when validating CCTRL CommissionNode command
+    SetupPayload payload;
+    CHIP_ERROR err = GetPayload(mOnboardingPayload, payload);
+    return err == CHIP_NO_ERROR && (payload.vendorID != 0 || payload.productID != 0);
+}
+
+void PairingManager::OnDeviceAttestationCompleted(Controller::DeviceCommissioner * deviceCommissioner, DeviceProxy * device,
+                                                  const Credentials::DeviceAttestationVerifier::AttestationDeviceInfo & info,
+                                                  Credentials::AttestationVerificationResult attestationResult)
+{
+    SetupPayload payload;
+    CHIP_ERROR parse_error = GetPayload(mOnboardingPayload, payload);
+    if (parse_error == CHIP_NO_ERROR && (payload.vendorID != 0 || payload.productID != 0))
+    {
+        if (payload.vendorID == 0 || payload.productID == 0)
+        {
+            ChipLogProgress(NotSpecified,
+                            "Failed validation: vendorID or productID must not be 0."
+                            "Requested VID: %u, Requested PID: %u.",
+                            payload.vendorID, payload.productID);
+            deviceCommissioner->ContinueCommissioningAfterDeviceAttestation(
+                device, Credentials::AttestationVerificationResult::kInvalidArgument);
+            return;
+        }
+
+        if (payload.vendorID != info.BasicInformationVendorId() || payload.productID != info.BasicInformationProductId())
+        {
+            ChipLogProgress(NotSpecified,
+                            "Failed validation of vendorID or productID."
+                            "Requested VID: %u, Requested PID: %u,"
+                            "Detected VID: %u, Detected PID %u.",
+                            payload.vendorID, payload.productID, info.BasicInformationVendorId(), info.BasicInformationProductId());
+            deviceCommissioner->ContinueCommissioningAfterDeviceAttestation(
+                device,
+                payload.vendorID == info.BasicInformationVendorId()
+                    ? Credentials::AttestationVerificationResult::kDacProductIdMismatch
+                    : Credentials::AttestationVerificationResult::kDacVendorIdMismatch);
+            return;
+        }
+
+        // NOTE: This will log errors even if the attestion was successful.
+        CHIP_ERROR err = deviceCommissioner->ContinueCommissioningAfterDeviceAttestation(device, attestationResult);
+        if (CHIP_NO_ERROR != err)
+        {
+            ChipLogError(NotSpecified, "Failed to continue commissioning after device attestation, error: %s", ErrorStr(err));
+        }
+        return;
+    }
+
+    // Don't bypass attestation, continue with error.
+    CHIP_ERROR err = deviceCommissioner->ContinueCommissioningAfterDeviceAttestation(device, attestationResult);
+    if (CHIP_NO_ERROR != err)
+    {
+        ChipLogError(NotSpecified, "Failed to continue commissioning after device attestation, error: %s", ErrorStr(err));
+    }
+}
+
+CommissioningParameters PairingManager::GetCommissioningParameters()
+{
+    auto params = CommissioningParameters();
+    params.SetSkipCommissioningComplete(false);
+    params.SetDeviceAttestationDelegate(this);
+
+    return params;
+}
+
+CHIP_ERROR PairingManager::Pair(NodeId remoteId, Transport::PeerAddress address)
+{
+    auto params = RendezvousParameters().SetSetupPINCode(mSetupPINCode).SetDiscriminator(mDiscriminator).SetPeerAddress(address);
+
+    CHIP_ERROR err           = CHIP_NO_ERROR;
+    auto commissioningParams = GetCommissioningParameters();
+    err                      = CurrentCommissioner().PairDevice(remoteId, params, commissioningParams);
+
+    return err;
+}
+
+void PairingManager::OnCurrentFabricRemove(void * context, NodeId nodeId, CHIP_ERROR err)
+{
+    PairingManager * self = reinterpret_cast<PairingManager *>(context);
+    VerifyOrReturn(self != nullptr, ChipLogError(NotSpecified, "OnCurrentFabricRemove: context is null"));
+
+    if (err == CHIP_NO_ERROR)
+    {
+        // print to console
+        fprintf(stderr, "Device with Node ID: " ChipLogFormatX64 "has been successfully removed.\n", ChipLogValueX64(nodeId));
+    }
+    else
+    {
+        ChipLogProgress(NotSpecified, "Device unpair Failure: " ChipLogFormatX64 " %s", ChipLogValueX64(nodeId), ErrorStr(err));
+    }
+}
+
+void PairingManager::InitPairingCommand()
+{
+    mCommissioner->RegisterPairingDelegate(this);
+}
+
+CHIP_ERROR PairingManager::PairDeviceWithCode(NodeId nodeId, const char * payload)
+{
+    if (payload == nullptr || strlen(payload) > kMaxManualCodeLength + 1)
+    {
+        ChipLogError(NotSpecified, "PairDeviceWithCode failed: Invalid pairing payload");
+        return CHIP_ERROR_INVALID_STRING_LENGTH;
+    }
+
+    Platform::CopyString(mOnboardingPayload, sizeof(mOnboardingPayload), payload);
+
+    return DeviceLayer::SystemLayer().ScheduleLambda([nodeId]() {
+        PairingManager & self = PairingManager::Instance();
+
+        self.InitPairingCommand();
+
+        CommissioningParameters commissioningParams = self.GetCommissioningParameters();
+        auto discoveryType                          = DiscoveryType::kDiscoveryNetworkOnly;
+
+        self.mNodeId = nodeId;
+
+        CHIP_ERROR err = self.mCommissioner->PairDevice(nodeId, self.mOnboardingPayload, commissioningParams, discoveryType);
+        if (err != CHIP_NO_ERROR)
+        {
+            ChipLogError(NotSpecified, "Failed to pair device with code, error: %s", ErrorStr(err));
+        }
+    });
+}
+
+CHIP_ERROR PairingManager::PairDevice(chip::NodeId nodeId, uint32_t setupPINCode, const char * deviceRemoteIp,
+                                      uint16_t deviceRemotePort)
+{
+    if (deviceRemoteIp == nullptr || strlen(deviceRemoteIp) > Inet::IPAddress::kMaxStringLength)
+    {
+        ChipLogError(NotSpecified, "PairDevice failed: Invalid device remote IP address");
+        return CHIP_ERROR_INVALID_STRING_LENGTH;
+    }
+
+    Platform::CopyString(mRemoteIpAddr, sizeof(mRemoteIpAddr), deviceRemoteIp);
+
+    return DeviceLayer::SystemLayer().ScheduleLambda([nodeId, setupPINCode, deviceRemotePort]() {
+        PairingManager & self = PairingManager::Instance();
+
+        self.InitPairingCommand();
+        self.mSetupPINCode = setupPINCode;
+
+        Inet::IPAddress address;
+        Inet::InterfaceId interfaceId;
+
+        if (!ParseAddressWithInterface(self.mRemoteIpAddr, address, interfaceId))
+        {
+            ChipLogError(NotSpecified, "Invalid IP address: %s", self.mRemoteIpAddr);
+            return;
+        }
+
+        CHIP_ERROR err = self.Pair(nodeId, Transport::PeerAddress::UDP(address, deviceRemotePort, interfaceId));
+        if (err != CHIP_NO_ERROR)
+        {
+            ChipLogError(NotSpecified, "Failed to pair device, error: %s", ErrorStr(err));
+        }
+    });
+}
+
+CHIP_ERROR PairingManager::UnpairDevice(NodeId nodeId)
+{
+    return DeviceLayer::SystemLayer().ScheduleLambda([nodeId]() {
+        PairingManager & self = PairingManager::Instance();
+
+        self.InitPairingCommand();
+
+        self.mCurrentFabricRemover = Platform::MakeUnique<Controller::CurrentFabricRemover>(self.mCommissioner);
+
+        if (!self.mCurrentFabricRemover)
+        {
+            ChipLogError(NotSpecified, "Failed to unpair device, mCurrentFabricRemover is null");
+            return;
+        }
+
+        CHIP_ERROR err = self.mCurrentFabricRemover->RemoveCurrentFabric(nodeId, &self.mCurrentFabricRemoveCallback);
+        if (err != CHIP_NO_ERROR)
+        {
+            ChipLogError(NotSpecified, "Failed to unpair device, error: %s", ErrorStr(err));
+        }
+    });
+}
diff --git a/examples/fabric-sync/admin/PairingManager.h b/examples/fabric-sync/admin/PairingManager.h
new file mode 100644
index 0000000..07cba74
--- /dev/null
+++ b/examples/fabric-sync/admin/PairingManager.h
@@ -0,0 +1,203 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#pragma once
+
+#include <controller/CHIPDeviceController.h>
+#include <controller/CommissioningDelegate.h>
+#include <controller/CommissioningWindowOpener.h>
+#include <controller/CurrentFabricRemover.h>
+#include <crypto/CHIPCryptoPAL.h>
+
+// Constants
+constexpr uint16_t kMaxManualCodeLength = 22;
+
+class CommissioningWindowDelegate
+{
+public:
+    virtual void OnCommissioningWindowOpened(chip::NodeId deviceId, CHIP_ERROR err, chip::SetupPayload payload) = 0;
+    virtual ~CommissioningWindowDelegate()                                                                      = default;
+};
+
+class CommissioningDelegate
+{
+public:
+    virtual void OnCommissioningComplete(chip::NodeId deviceId, CHIP_ERROR err) = 0;
+    virtual ~CommissioningDelegate()                                            = default;
+};
+
+class PairingDelegate
+{
+public:
+    virtual void OnDeviceRemoved(chip::NodeId deviceId, CHIP_ERROR err) = 0;
+    virtual ~PairingDelegate()                                          = default;
+};
+
+/**
+ * The PairingManager class is responsible for managing the commissioning and pairing process
+ * of Matter devices. PairingManager is designed to be used as a singleton, meaning that there
+ * should only be one instance of it running at any given time.
+ *
+ * Usage:
+ *
+ * 1. The class should be initialized when the system starts up, typically by invoking the static
+ *    instance method to get the singleton.
+ * 2. To open a commissioning window, the appropriate method should be called on the PairingManager instance.
+ * 3. The PairingManager will handle the lifecycle of the CommissioningWindowOpener and ensure that
+ *    resources are cleaned up appropriately when pairing is complete or the process is aborted.
+ *
+ * Example:
+ *
+ * @code
+ * PairingManager& manager = PairingManager::Instance();
+ * manager.OpenCommissioningWindow();
+ * @endcode
+ */
+class PairingManager : public chip::Controller::DevicePairingDelegate,
+                       public chip::Controller::DeviceDiscoveryDelegate,
+                       public chip::Credentials::DeviceAttestationDelegate
+{
+public:
+    static PairingManager & Instance()
+    {
+        static PairingManager instance;
+        return instance;
+    }
+
+    CHIP_ERROR Init(chip::Controller::DeviceCommissioner * commissioner);
+
+    void SetOpenCommissioningWindowDelegate(CommissioningWindowDelegate * delegate) { mCommissioningWindowDelegate = delegate; }
+    void SetCommissioningDelegate(CommissioningDelegate * delegate) { mCommissioningDelegate = delegate; }
+    void SetPairingDelegate(PairingDelegate * delegate) { mPairingDelegate = delegate; }
+    PairingDelegate * GetPairingDelegate() { return mPairingDelegate; }
+
+    chip::Controller::DeviceCommissioner & CurrentCommissioner() { return *mCommissioner; };
+
+    /**
+     * Opens a commissioning window on the specified node and endpoint.
+     * Only one commissioning window can be active at a time. If a commissioning
+     * window is already open, this function will return an error.
+     *
+     * @param nodeId The target node ID for commissioning.
+     * @param endpointId The target endpoint ID for commissioning.
+     * @param commissioningTimeoutSec Timeout for the commissioning window in seconds.
+     * @param iterations Iterations for PBKDF calculations.
+     * @param discriminator Discriminator for commissioning.
+     * @param salt Optional salt for verifier-based commissioning.
+     * @param verifier Optional verifier for enhanced commissioning security.
+     *
+     * @return CHIP_ERROR_INCORRECT_STATE if a commissioning window is already open.
+     */
+    CHIP_ERROR OpenCommissioningWindow(chip::NodeId nodeId, chip::EndpointId endpointId, uint16_t commissioningTimeoutSec,
+                                       uint32_t iterations, uint16_t discriminator, const chip::ByteSpan & salt,
+                                       const chip::ByteSpan & verifier);
+
+    /**
+     * Pairs a device using a setup code payload.
+     *
+     * @param nodeId The target node ID for pairing.
+     * @param payload The setup code payload, which typically contains device-specific pairing information.
+     *
+     * @return CHIP_NO_ERROR on successful initiation of the pairing process, or an appropriate CHIP_ERROR if pairing fails.
+     */
+    CHIP_ERROR PairDeviceWithCode(chip::NodeId nodeId, const char * payload);
+
+    /**
+     * Pairs a device using its setup PIN code and remote IP address.
+     *
+     * @param nodeId The target node ID for pairing.
+     * @param setupPINCode The setup PIN code for the device, used for establishing a secure connection.
+     * @param deviceRemoteIp The IP address of the remote device.
+     * @param deviceRemotePort The port number on which the device is listening for pairing requests.
+     *
+     * @return CHIP_NO_ERROR if the pairing process is initiated successfully, or an appropriate CHIP_ERROR if pairing fails.
+     */
+    CHIP_ERROR PairDevice(chip::NodeId nodeId, uint32_t setupPINCode, const char * deviceRemoteIp, uint16_t deviceRemotePort);
+
+    /**
+     * Unpairs a device with the specified node ID.
+     *
+     * @param nodeId The node ID of the device to be unpaired.
+     *
+     * @return CHIP_NO_ERROR if the device is successfully unpaired, or an appropriate CHIP_ERROR if the process fails.
+     */
+    CHIP_ERROR UnpairDevice(chip::NodeId nodeId);
+
+private:
+    // Constructors
+    PairingManager();
+    PairingManager(const PairingManager &)             = delete;
+    PairingManager & operator=(const PairingManager &) = delete;
+
+    // Private member functions (static and non-static)
+    chip::Controller::CommissioningParameters GetCommissioningParameters();
+    void InitPairingCommand();
+    CHIP_ERROR Pair(chip::NodeId remoteId, chip::Transport::PeerAddress address);
+
+    /////////// DevicePairingDelegate Interface /////////
+    void OnStatusUpdate(chip::Controller::DevicePairingDelegate::Status status) override;
+    void OnPairingComplete(CHIP_ERROR error) override;
+    void OnPairingDeleted(CHIP_ERROR error) override;
+    void OnReadCommissioningInfo(const chip::Controller::ReadCommissioningInfo & info) override;
+    void OnCommissioningComplete(chip::NodeId deviceId, CHIP_ERROR error) override;
+
+    /////////// DeviceDiscoveryDelegate Interface /////////
+    void OnDiscoveredDevice(const chip::Dnssd::CommissionNodeData & nodeData) override;
+
+    /////////// DeviceAttestationDelegate Interface /////////
+    chip::Optional<uint16_t> FailSafeExpiryTimeoutSecs() const override;
+    bool ShouldWaitAfterDeviceAttestation() override;
+    void OnDeviceAttestationCompleted(chip::Controller::DeviceCommissioner * deviceCommissioner, chip::DeviceProxy * device,
+                                      const chip::Credentials::DeviceAttestationVerifier::AttestationDeviceInfo & info,
+                                      chip::Credentials::AttestationVerificationResult attestationResult) override;
+
+    static void OnOpenCommissioningWindowResponse(void * context, chip::NodeId deviceId, CHIP_ERROR status,
+                                                  chip::SetupPayload payload);
+    static void OnOpenCommissioningWindowVerifierResponse(void * context, chip::NodeId deviceId, CHIP_ERROR status);
+    static void OnCurrentFabricRemove(void * context, chip::NodeId remoteNodeId, CHIP_ERROR status);
+
+    // Private data members
+    chip::Controller::DeviceCommissioner * mCommissioner = nullptr;
+
+    CommissioningWindowDelegate * mCommissioningWindowDelegate = nullptr;
+    CommissioningDelegate * mCommissioningDelegate             = nullptr;
+    PairingDelegate * mPairingDelegate                         = nullptr;
+
+    chip::NodeId mNodeId = chip::kUndefinedNodeId;
+    chip::ByteSpan mVerifier;
+    chip::ByteSpan mSalt;
+    uint16_t mDiscriminator = 0;
+    uint32_t mSetupPINCode  = 0;
+    uint8_t mVerifierBuffer[chip::Crypto::kSpake2p_VerifierSerialized_Length];
+    uint8_t mSaltBuffer[chip::Crypto::kSpake2p_Max_PBKDF_Salt_Length];
+    char mRemoteIpAddr[chip::Inet::IPAddress::kMaxStringLength];
+    char mOnboardingPayload[kMaxManualCodeLength + 1];
+
+    /**
+     * Holds the unique_ptr to the current CommissioningWindowOpener.
+     * Only one commissioning window opener can be active at a time.
+     * The pointer is reset when the commissioning window is closed or when an error occurs.
+     */
+    chip::Platform::UniquePtr<chip::Controller::CommissioningWindowOpener> mWindowOpener;
+    chip::Callback::Callback<chip::Controller::OnOpenCommissioningWindow> mOnOpenCommissioningWindowCallback;
+    chip::Callback::Callback<chip::Controller::OnOpenCommissioningWindowWithVerifier> mOnOpenCommissioningWindowVerifierCallback;
+
+    // For Unpair
+    chip::Platform::UniquePtr<chip::Controller::CurrentFabricRemover> mCurrentFabricRemover;
+    chip::Callback::Callback<chip::Controller::OnCurrentFabricRemove> mCurrentFabricRemoveCallback;
+};
diff --git a/examples/fabric-sync/main.cpp b/examples/fabric-sync/main.cpp
index 66541b3..45e0ce3 100644
--- a/examples/fabric-sync/main.cpp
+++ b/examples/fabric-sync/main.cpp
@@ -17,6 +17,11 @@
  */
 
 #include <AppMain.h>
+#include <admin/PairingManager.h>
+
+#if defined(ENABLE_CHIP_SHELL)
+#include "ShellCommands.h"
+#endif
 
 using namespace chip;
 
@@ -90,6 +95,19 @@
 
     VerifyOrDie(ChipLinuxAppInit(argc, argv) == 0);
 
+#if defined(ENABLE_CHIP_SHELL)
+    Shell::RegisterCommands();
+#endif
+
+    CHIP_ERROR err = PairingManager::Instance().Init(GetDeviceCommissioner());
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogProgress(NotSpecified, "Failed to init PairingManager: %s ", ErrorStr(err));
+
+        // End the program with non zero error code to indicate a error.
+        return 1;
+    }
+
     ChipLinuxAppMainLoop();
 
     return 0;
diff --git a/examples/fabric-sync/shell/AddBridgeCommand.cpp b/examples/fabric-sync/shell/AddBridgeCommand.cpp
new file mode 100644
index 0000000..d02ad9f
--- /dev/null
+++ b/examples/fabric-sync/shell/AddBridgeCommand.cpp
@@ -0,0 +1,80 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "AddBridgeCommand.h"
+
+#include <admin/DeviceManager.h>
+#include <lib/shell/streamer.h>
+
+using namespace ::chip;
+
+namespace commands {
+
+AddBridgeCommand::AddBridgeCommand(chip::NodeId nodeId, uint32_t setupPINCode, const char * remoteAddr, uint16_t remotePort) :
+    mBridgeNodeId(nodeId), mSetupPINCode(setupPINCode), mRemoteAddr(remoteAddr), mRemotePort(remotePort)
+{}
+
+void AddBridgeCommand::OnCommissioningComplete(NodeId deviceId, CHIP_ERROR err)
+{
+    if (mBridgeNodeId != deviceId)
+    {
+        if (err != CHIP_NO_ERROR)
+        {
+            ChipLogError(NotSpecified, "Failed to pair non-bridge device (0x:" ChipLogFormatX64 ") with error: %" CHIP_ERROR_FORMAT,
+                         ChipLogValueX64(deviceId), err.Format());
+        }
+        else
+        {
+            ChipLogProgress(NotSpecified, "Commissioning complete for non-bridge device: NodeId: " ChipLogFormatX64,
+                            ChipLogValueX64(deviceId));
+        }
+        return;
+    }
+
+    if (err == CHIP_NO_ERROR)
+    {
+        DeviceMgr().SetRemoteBridgeNodeId(mBridgeNodeId);
+        ChipLogProgress(NotSpecified, "Successfully paired bridge device: NodeId: " ChipLogFormatX64,
+                        ChipLogValueX64(mBridgeNodeId));
+
+        DeviceMgr().UpdateLastUsedNodeId(mBridgeNodeId);
+    }
+    else
+    {
+        ChipLogError(NotSpecified, "Failed to pair bridge device (0x:" ChipLogFormatX64 ") with error: %" CHIP_ERROR_FORMAT,
+                     ChipLogValueX64(deviceId), err.Format());
+    }
+
+    CommandRegistry::Instance().ResetActiveCommand();
+}
+
+CHIP_ERROR AddBridgeCommand::RunCommand()
+{
+    if (DeviceMgr().IsFabricSyncReady())
+    {
+        // print to console
+        fprintf(stderr, "Remote Fabric Bridge has already been configured.\n");
+        return CHIP_ERROR_BUSY;
+    }
+
+    PairingManager::Instance().SetCommissioningDelegate(this);
+
+    return DeviceMgr().PairRemoteFabricBridge(mBridgeNodeId, mSetupPINCode, mRemoteAddr, mRemotePort);
+}
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/AddBridgeCommand.h b/examples/fabric-sync/shell/AddBridgeCommand.h
new file mode 100644
index 0000000..d4b2c16
--- /dev/null
+++ b/examples/fabric-sync/shell/AddBridgeCommand.h
@@ -0,0 +1,40 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <CommandRegistry.h>
+#include <admin/PairingManager.h>
+
+namespace commands {
+
+class AddBridgeCommand : public Command, public CommissioningDelegate
+{
+public:
+    AddBridgeCommand(chip::NodeId nodeId, uint32_t setupPINCode, const char * remoteAddr, uint16_t remotePort);
+    void OnCommissioningComplete(chip::NodeId deviceId, CHIP_ERROR err) override;
+    CHIP_ERROR RunCommand() override;
+
+private:
+    chip::NodeId mBridgeNodeId;
+    uint32_t mSetupPINCode;
+    const char * mRemoteAddr;
+    uint16_t mRemotePort;
+};
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/BUILD.gn b/examples/fabric-sync/shell/BUILD.gn
new file mode 100644
index 0000000..6f64b91
--- /dev/null
+++ b/examples/fabric-sync/shell/BUILD.gn
@@ -0,0 +1,44 @@
+# Copyright (c) 2024 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build_overrides/chip.gni")
+import("${chip_root}/src/app/chip_data_model.gni")
+
+config("config") {
+  include_dirs = [
+    ".",
+    "${chip_root}/examples/common",
+    "${chip_root}/examples/fabric-sync",
+  ]
+}
+
+source_set("shell") {
+  public_configs = [ ":config" ]
+
+  sources = [
+    "AddBridgeCommand.cpp",
+    "AddBridgeCommand.h",
+    "CommandRegistry.h",
+    "RemoveBridgeCommand.cpp",
+    "RemoveBridgeCommand.h",
+    "ShellCommands.cpp",
+    "ShellCommands.h",
+  ]
+
+  deps = [
+    "${chip_root}/examples/fabric-sync/admin:fabric-admin-lib",
+    "${chip_root}/examples/fabric-sync/bridge:fabric-bridge-lib",
+    "${chip_root}/src/lib",
+  ]
+}
diff --git a/examples/fabric-sync/shell/CommandRegistry.h b/examples/fabric-sync/shell/CommandRegistry.h
new file mode 100644
index 0000000..ec53e17
--- /dev/null
+++ b/examples/fabric-sync/shell/CommandRegistry.h
@@ -0,0 +1,54 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <admin/PairingManager.h>
+
+namespace commands {
+
+class Command
+{
+public:
+    virtual ~Command()              = default;
+    virtual CHIP_ERROR RunCommand() = 0;
+};
+
+class CommandRegistry
+{
+public:
+    static CommandRegistry & Instance()
+    {
+        static CommandRegistry instance;
+        return instance;
+    }
+
+    void SetActiveCommand(std::unique_ptr<Command> command) { mActiveCommand = std::move(command); }
+
+    Command * GetActiveCommand() { return mActiveCommand.get(); }
+
+    void ResetActiveCommand() { mActiveCommand.reset(); }
+
+    bool IsCommandActive() const { return mActiveCommand != nullptr; }
+
+private:
+    CommandRegistry() = default;
+    std::unique_ptr<Command> mActiveCommand;
+};
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/RemoveBridgeCommand.cpp b/examples/fabric-sync/shell/RemoveBridgeCommand.cpp
new file mode 100644
index 0000000..9e8f1ed
--- /dev/null
+++ b/examples/fabric-sync/shell/RemoveBridgeCommand.cpp
@@ -0,0 +1,70 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "RemoveBridgeCommand.h"
+
+#include <admin/DeviceManager.h>
+#include <lib/shell/streamer.h>
+
+using namespace ::chip;
+
+namespace commands {
+
+void RemoveBridgeCommand::OnDeviceRemoved(NodeId deviceId, CHIP_ERROR err)
+{
+    if (mBridgeNodeId != deviceId)
+    {
+        ChipLogProgress(NotSpecified, "An non-bridge device: NodeId: " ChipLogFormatX64 " is removed.", ChipLogValueX64(deviceId));
+        return;
+    }
+
+    if (err == CHIP_NO_ERROR)
+    {
+        DeviceMgr().SetRemoteBridgeNodeId(kUndefinedNodeId);
+        ChipLogProgress(NotSpecified, "Successfully removed bridge device: NodeId: " ChipLogFormatX64,
+                        ChipLogValueX64(mBridgeNodeId));
+    }
+    else
+    {
+        ChipLogError(NotSpecified, "Failed to remove bridge device (0x:" ChipLogFormatX64 ") with error: %" CHIP_ERROR_FORMAT,
+                     ChipLogValueX64(deviceId), err.Format());
+    }
+
+    CommandRegistry::Instance().ResetActiveCommand();
+}
+
+CHIP_ERROR RemoveBridgeCommand::RunCommand()
+{
+    NodeId bridgeNodeId = DeviceMgr().GetRemoteBridgeNodeId();
+
+    if (bridgeNodeId == kUndefinedNodeId)
+    {
+        // print to console
+        fprintf(stderr, "Remote Fabric Bridge is not configured yet, nothing to remove.\n");
+        return CHIP_ERROR_BUSY;
+    }
+
+    mBridgeNodeId = bridgeNodeId;
+
+    PairingManager::Instance().SetPairingDelegate(this);
+    DeviceMgr().UnpairRemoteFabricBridge();
+
+    return CHIP_NO_ERROR;
+}
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/RemoveBridgeCommand.h b/examples/fabric-sync/shell/RemoveBridgeCommand.h
new file mode 100644
index 0000000..401a539
--- /dev/null
+++ b/examples/fabric-sync/shell/RemoveBridgeCommand.h
@@ -0,0 +1,36 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <CommandRegistry.h>
+#include <admin/PairingManager.h>
+
+namespace commands {
+
+class RemoveBridgeCommand : public Command, public PairingDelegate
+{
+public:
+    void OnDeviceRemoved(chip::NodeId deviceId, CHIP_ERROR err) override;
+    CHIP_ERROR RunCommand() override;
+
+private:
+    chip::NodeId mBridgeNodeId = chip::kUndefinedNodeId;
+};
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/ShellCommands.cpp b/examples/fabric-sync/shell/ShellCommands.cpp
new file mode 100644
index 0000000..1edbd43
--- /dev/null
+++ b/examples/fabric-sync/shell/ShellCommands.cpp
@@ -0,0 +1,147 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#include "ShellCommands.h"
+#include "AddBridgeCommand.h"
+#include "RemoveBridgeCommand.h"
+
+#include <admin/DeviceManager.h>
+#include <inttypes.h>
+#include <lib/core/CHIPCore.h>
+#include <lib/shell/Commands.h>
+#include <lib/shell/Engine.h>
+#include <lib/shell/commands/Help.h>
+#include <lib/support/CHIPArgParser.hpp>
+#include <lib/support/CHIPMem.h>
+#include <lib/support/CodeUtils.h>
+#include <platform/CHIPDeviceLayer.h>
+
+namespace chip {
+namespace Shell {
+
+static CHIP_ERROR PrintAllCommands()
+{
+    streamer_t * sout = streamer_get();
+    streamer_printf(sout, "  help                           Usage: app <subcommand>\r\n");
+    streamer_printf(sout,
+                    "  add-bridge       Pair remote fabric bridge to local fabric. Usage: app add-bridge node-id setup-pin-code "
+                    "device-remote-ip device-remote-port\r\n");
+    streamer_printf(sout, "  remove-bridge    Remove the remote fabric bridge from the local fabric. Usage: app remove-bridge\r\n");
+    streamer_printf(sout, "  sync-device      Sync a device from other ecosystem. Usage: app sync-device endpointid\r\n");
+    streamer_printf(sout, "\r\n");
+
+    return CHIP_NO_ERROR;
+}
+
+static CHIP_ERROR HandleAddBridgeCommand(int argc, char ** argv)
+{
+    if (argc != 5)
+    {
+        fprintf(stderr,
+                "Invalid arguments. Usage: app add-bridge <node-id> <setup-pin-code> <device-remote-ip> <device-remote-port>\n");
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    // Check if there is already an active command
+    if (commands::CommandRegistry::Instance().IsCommandActive())
+    {
+        fprintf(stderr, "Another command is currently active. Please wait until it completes.\n");
+        return CHIP_ERROR_BUSY;
+    }
+
+    // Parse arguments
+    chip::NodeId nodeId     = static_cast<chip::NodeId>(strtoull(argv[1], nullptr, 10));
+    uint32_t setupPINCode   = static_cast<uint32_t>(strtoul(argv[2], nullptr, 10));
+    const char * remoteAddr = argv[3];
+    uint16_t remotePort     = static_cast<uint16_t>(strtoul(argv[4], nullptr, 10));
+
+    auto command = std::make_unique<commands::AddBridgeCommand>(nodeId, setupPINCode, remoteAddr, remotePort);
+
+    CHIP_ERROR result = command->RunCommand();
+    if (result == CHIP_NO_ERROR)
+    {
+        commands::CommandRegistry::Instance().SetActiveCommand(std::move(command));
+    }
+
+    return result;
+}
+
+static CHIP_ERROR HandleRemoveBridgeCommand(int argc, char ** argv)
+{
+    if (argc != 1)
+    {
+        fprintf(stderr, "Invalid arguments. Usage: app remove-bridge\n");
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    // Check if there is already an active command
+    if (commands::CommandRegistry::Instance().IsCommandActive())
+    {
+        fprintf(stderr, "Another command is currently active. Please wait until it completes.\n");
+        return CHIP_ERROR_BUSY;
+    }
+
+    auto command = std::make_unique<commands::RemoveBridgeCommand>();
+
+    CHIP_ERROR result = command->RunCommand();
+    if (result == CHIP_NO_ERROR)
+    {
+        commands::CommandRegistry::Instance().SetActiveCommand(std::move(command));
+    }
+
+    return result;
+}
+
+static CHIP_ERROR AppPlatformHandler(int argc, char ** argv)
+{
+    CHIP_ERROR error = CHIP_NO_ERROR;
+
+    if (argc == 0 || strcmp(argv[0], "help") == 0)
+    {
+        return PrintAllCommands();
+    }
+    else if (argc == 0 || strcmp(argv[0], "?") == 0)
+    {
+        return PrintAllCommands();
+    }
+    else if (strcmp(argv[0], "add-bridge") == 0)
+    {
+        return HandleAddBridgeCommand(argc, argv);
+    }
+    else if (strcmp(argv[0], "remove-bridge") == 0)
+    {
+        return HandleRemoveBridgeCommand(argc, argv);
+    }
+    else
+    {
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+    return error;
+}
+
+void RegisterCommands()
+{
+
+    static const shell_command_t sDeviceComand = { &AppPlatformHandler, "app", "App commands. Usage: app [command_name]" };
+
+    // Register the root `device` command with the top-level shell.
+    Engine::Root().RegisterCommands(&sDeviceComand, 1);
+    return;
+}
+
+} // namespace Shell
+} // namespace chip
diff --git a/examples/fabric-sync/shell/ShellCommands.h b/examples/fabric-sync/shell/ShellCommands.h
new file mode 100644
index 0000000..ab957bc
--- /dev/null
+++ b/examples/fabric-sync/shell/ShellCommands.h
@@ -0,0 +1,27 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#include <platform/CHIPDeviceLayer.h>
+
+namespace chip {
+namespace Shell {
+
+void RegisterCommands();
+
+} // namespace Shell
+} // namespace chip