# 共有フォルダ - 同期なし (sync-downなし)

## 概要

アプリがKeeperボルトのデータをローカルのSDKインスタンスへ同期しない (`sync_down` / `VaultOnline` を使わない) とき、ログイン後は**認証済みのAPI呼び出し**のみで処理を進める場合に適したフローです。

共有フォルダのメタデータ参照、ユーザーの招待・更新・削除、**チームの追加や削除**、または `IAuthentication` が利用可能になった直後の **UID を指定したレコード本文の取得**が必要な場合に、このパスを使います。

**ボルト全体を同期する**構成では、`VaultOnline` における共有フォルダのモデルや最も広い統合面を利用できる **`IVaultSharedFolder`** / **`VaultOnline`** を優先してください。

### 対象範囲と制限

* **共有フォルダのスキップ同期**は、完全な `sync_down` / `VaultOnline` ツリーを実行しないアプリ向けです。認証直後から利用できるのは、共有フォルダおよびレコードに関する**限定的な**操作に限ります。
* **ユーザー:** これまでのリリースと同様、**個別ユーザー** (メール) の追加・更新・削除が可能です。
* **チーム:** ボルトを読み込まずに、共有フォルダへ**チームの追加・削除**や、**共有に利用できるチームの一覧**が可能です。
* **レコード:** 共有フォルダに紐づく**レコードUIDの一覧**と、ボルト同期なしでのレコードの**取得と復号**が可能です。

<details>

<summary>DotNet SDK 前提条件</summary>

ログイン後に有効な **`IAuthentication`** があること。具体例として、下記の **`VaultOnline`** の **`vault.Auth`**、`AuthenticateAndGetVault.GetAuthAsync` の戻り値など、**`IAuthentication`** を実装したオブジェクトが該当します。

**構造体とクラス**

| 型                                                         | 目的                                                                                                   |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `ISharedFolderSkipSyncDown`                               | 依存性注入とテスト用のインターフェース                                                                                  |
| `SharedFolderSkipSyncDown`                                | 静的エントリポイント (`GetSharedFolderAsync`, `PutUserToSharedFolderAsync`, `RemoveUserFromSharedFolderAsync`) |
| `SharedFolderSkipSyncDown.SharedFolderSkipSyncDownClient` | `ISharedFolderSkipSyncDown` の既定実装                                                                    |

アプリがKeeperへの認証完了後すぐに、共有フォルダの**メタデータの参照**、ユーザーの**招待・更新**、または共有フォルダからの**ユーザーの削除**が必要な場合にこのパスを使います。

ボルト全体の統合 (同期済みツリーやチームなど) では、`IVaultSharedFolder` / `VaultOnline` を使ってください。

</details>

### UIDによる共有フォルダの取得

UIDで共有フォルダを1件読み込みます。応答には共有フォルダ本体に加え、ユーザー情報・チーム情報およびレコードUIDの一覧が含まれます。

<details>

<summary>DotNet SDK</summary>

**関数:** `GetSharedFolderAsync`

{% code overflow="wrap" %}

```csharp
public static async Task<GetSharedFoldersResponse> GetSharedFolderAsync(
    IAuthentication auth,
    string sharedFolderUid)
```

{% endcode %}

セッションがフォルダにアクセスできない場合、または応答に共有フォルダエントリがない場合は **`null`** を返します。

**引数:**

| パラメーター          | 説明                                     |
| --------------- | -------------------------------------- |
| Auth            | `IAuthentication` インターフェースを持つ認証済みセッション |
| SharedFolderUid | データ取得対象の共有フォルダのUID                     |

</details>

<details>

<summary>PowerCommander</summary>

**コマンド:** `Get-KeeperSharedFolderDetailsSkipSync`

ボルト全体を同期せず、サーバーから共有フォルダのペイロード (**`get_shared_folders`**) を取得します。

| パラメーター                | 説明                                                                                    |
| --------------------- | ------------------------------------------------------------------------------------- |
| `-SharedFolderUid`    | **必須。** 共有フォルダのUID                                                                    |
| `-IncludePermissions` | レコードごとの**CanEdit** / **CanShare**、ユーザー別・チーム別フラグ、フォルダ鍵および既定権限フィールドを含めます。省略時はコンパクトな出力のみ |
| `-PassThru`           | SDK からの生の **`GetSharedFoldersResponse`** を返します                                        |

**使用例:**

```powershell
Get-KeeperSharedFolderDetailsSkipSync -SharedFolderUid '<shared_folder_uid>'
Get-KeeperSharedFolderDetailsSkipSync -SharedFolderUid '<uid>' -IncludePermissions
```

</details>

<details>

<summary>Python SDK</summary>

```python
from keepersdk.vault import skip_sync

sf = load_shared_folder_raw
```

</details>

### 共有フォルダからレコードを取得

フォルダに紐づく**重複のないレコードUID**を返します。フォルダが利用できない、またはレコードがない場合は空です。

<details>

<summary>DotNet SDK</summary>

**関数:** `GetRecordUidsFromSharedFolderAsync`

{% code overflow="wrap" %}

```csharp
public static async Task<IReadOnlyList<string>> GetRecordUidsFromSharedFolderAsync(
    IAuthentication auth,
    string sharedFolderUid)
```

{% endcode %}

**引数:**

| パラメーター             | 説明                  |
| ------------------ | ------------------- |
| `-SharedFolderUid` | レコード取得対象の共有フォルダのUID |

</details>

<details>

<summary>PowerCommander</summary>

**コマンド:** `Get-KeeperSharedFolderRecordUidsSkipSync`

ボルト全体を同期せず、サーバー経由でレコードUIDを取得します (内部で **`get_shared_folders`** を利用)。

**フラグ:**

| パラメーター             | 説明     |
| ------------------ | ------ |
| `-SharedFolderUid` | **必須** |

**使用例:**

```powershell
Get-KeeperSharedFolderRecordUidsSkipSync -SharedFolderUid '<shared_folder_uid>'
```

</details>

<details>

<summary>Python SDK</summary>

<pre class="language-python"><code class="lang-python">from keepersdk.vault import skip_sync

<strong>skip_sync.get_shared_folder_records_display(auth, shared_folder_uid)
</strong></code></pre>

</details>

### ユーザーへの共有フォルダ共有

共有フォルダにユーザーを追加または更新します。任意の **`IUserShareOptions`** (例: **`SharedFolderUserOptions`**) で権限と有効期限を制御します。

<details>

<summary>DotNet SDK</summary>

**関数:** `PutUserToSharedFolderAsync`

{% code overflow="wrap" %}

```csharp
public static async Task PutUserToSharedFolderAsync(
    IAuthentication auth,
    string sharedFolderUid,
    string userId,
    IUserShareOptions options = null)
```

{% endcode %}

オプションの設定例は以下のとおりです。

{% code overflow="wrap" %}

```csharp
var options = new SharedFolderUserOptions
                {
                    ManageRecords = true,
                    ManageUsers = true,
                    Expiration = DateTimeOffset.Now.AddMinutes(10)
                };
```

{% endcode %}

**引数:**

| パラメーター          | 説明                                       |
| --------------- | ---------------------------------------- |
| Auth            | `IAuthentication` インターフェースを持つ認証済みセッション   |
| SharedFolderUid | 対象の共有フォルダのUID                            |
| userId          | 共有先ユーザーのメール (API が受け付ける形式)               |
| options         | 上記のとおりの `SharedFolderUserOptions` オブジェクト |

</details>

<details>

<summary>PowerCommander</summary>

**コマンド:** `Grant-KeeperSharedFolderUserSkipSync`

| パラメーター                           | 説明                                                                                     |
| -------------------------------- | -------------------------------------------------------------------------------------- |
| `-SharedFolder`                  | **必須** (位置 0)                                                                          |
| `-User`                          | **必須** (位置 1)。API が受け付けるユーザーのメールまたはID                                                  |
| `-ManageRecords`, `-ManageUsers` | 省略可能な真偽値 (`$true` / `$false` / 省略)                                                     |
| `-ExpireIn`, `-ExpireAt`         | 省略可能な有効期限 (他の共有コマンドレット、例: `Grant-KeeperRecordAccess` と同じ意味)                            |
| `-ShowDetail`                    | 成功後に **`Get-KeeperSharedFolderRecordsSkipSync`** を **`-Mode SharedKey`** で実行し、概要を表示します |
| `-PassThru`                      | **`-ShowDetail`** 指定時、一覧ステップの **`RecordDetailsSkipSyncResult`** を返します                  |

**使用例:**

```powershell
Grant-KeeperSharedFolderUserSkipSync -SharedFolder '<folder_uid>' -User 'user@example.com' -ManageRecords $true -ManageUsers $true
```

</details>

<details>

<summary>Python SDK</summary>

```python
from keepersdk.vault import skip_sync

records = skip_sync.share_shared_folder_to_user(
        keeper_auth_context,
        shared_folder_uid=shared_folder_uid,
        username=username,
        manage_users=manage_users,
        manage_records=manage_records,
        expiration=expiration,
    )
```

</details>

### ユーザーから共有フォルダ共有を取り消す

共有フォルダからユーザーのアクセスを削除します。任意の **`IUserShareOptions`** (例: **`SharedFolderUserOptions`**) で権限と有効期限を制御できます。

<details>

<summary>DotNet SDK</summary>

**関数:** `RemoveUserFromSharedFolderAsync`

{% code overflow="wrap" %}

```csharp
public static async Task RemoveUserFromSharedFolderAsync(
    IAuthentication auth,
    string sharedFolderUid,
    string userId)
```

{% endcode %}

**引数:**

| パラメーター          | 説明                                     |
| --------------- | -------------------------------------- |
| Auth            | `IAuthentication` インターフェースを持つ認証済みセッション |
| SharedFolderUid | 対象の共有フォルダのUID                          |
| userId          | アクセスを取り消すユーザーのメール                      |

</details>

<details>

<summary>PowerCommander</summary>

**コマンド:** `Revoke-KeeperSharedFolderUserSkipSync`

共有フォルダからユーザーを外します (**`RemoveUserFromSharedFolderAsync`**)。**`ShouldProcess`** に対応します。

| パラメーター                     | 説明                  |
| -------------------------- | ------------------- |
| `-SharedFolder`, `-User`   | **必須** (位置 0 および 1) |
| `-ShowDetail`, `-PassThru` | 付与時と同じパターン          |

**使用例:**

```powershell
Revoke-KeeperSharedFolderUserSkipSync -SharedFolder '<folder_uid>' -User 'user@example.com'
```

</details>

<details>

<summary>Python SDK</summary>

```python
from keepersdk.vault import skip_sync

skip_sync.revoke_shared_folder_from_user(
        keeper_auth_context,
        shared_folder_uid=shared_folder_uid,
        username=username,
    )
```

</details>

### 共有可能チーム一覧の取得

現在のユーザーが共有に利用できるチームを返します。ボルト全体を同期せずに取得します。

<details>

<summary>DotNet SDK</summary>

**関数:** `GetAvailableTeamsForShareAsync`

{% code overflow="wrap" %}

```csharp
public static async Task<IEnumerable<TeamInfo>> GetAvailableTeamsForShareAsync(
    IAuthentication auth)
```

{% endcode %}

**引数:**

| パラメーター | 説明                                     |
| ------ | -------------------------------------- |
| Auth   | `IAuthentication` インターフェースを持つ認証済みセッション |

</details>

<details>

<summary>PowerCommander</summary>

**コマンド:** `Get-KeeperAvailableTeamsSkipSync`

共有に利用できるチームを一覧します (**`get_available_teams`**)。

**使用例:**

```powershell
Get-KeeperAvailableTeamsSkipSync
```

</details>

<details>

<summary>Python SDK</summary>

**近日公開**

</details>

### チーム名からUIDの解決

**チーム表示名**が一意に特定できる場合にのみ**チームUID**に解決します。一致がなければ**null**を返し、複数一致する場合は例外を送出します (明示的な**チーム名**またはUIDの利用を検討してください)。

<details>

<summary>DotNet SDK</summary>

**関数:** `GetTeamUidFromNameAsync`

{% code overflow="wrap" %}

```csharp
public static async Task<string> GetTeamUidFromNameAsync(
    IAuthentication auth,
    string teamName)
```

{% endcode %}

**引数:**

| パラメーター   | 説明                                     |
| -------- | -------------------------------------- |
| Auth     | `IAuthentication` インターフェースを持つ認証済みセッション |
| teamName | 解決対象のチーム表示名 (一意に決まる必要あり)               |

</details>

<details>

<summary>PowerCommander</summary>

**コマンド:** `Get-KeeperTeamUidSkipSync`

表示名が一意に特定できる場合に、チームUIDへ解決します。

| パラメーター      | 説明                       |
| ----------- | ------------------------ |
| `-TeamName` | 解決対象のチーム表示名 (一意に決まる必要あり) |

**使用例:**

```powershell
Get-KeeperTeamUidSkipSync -TeamName 'My Team'
```

</details>

<details>

<summary>Python SDK</summary>

**近日公開**

</details>

### 共有フォルダへのチーム追加・更新

共有フォルダにチームを追加または更新します。**チームUID**または、一意に特定できる**表示名**を渡します。UID が不明なときは **`GetTeamUidFromNameAsync`** で取得できます。権限や有効期限を渡せるときは **`IUserShareOptions`** で指定します。

<details>

<summary>DotNet SDK</summary>

**関数:** `PutTeamToSharedFolderAsync`

{% code overflow="wrap" %}

```csharp
public static async Task PutTeamToSharedFolderAsync(
    IAuthentication auth,
    string sharedFolderUid,
    string teamUid,
    IUserShareOptions options = null)
```

{% endcode %}

オプションの設定例は以下のとおりです。

{% code overflow="wrap" %}

```csharp
var options = new SharedFolderUserOptions
                {
                    ManageRecords = true,
                    ManageUsers = true,
                    Expiration = DateTimeOffset.Now.AddMinutes(10)
                };
```

{% endcode %}

**引数:**

| パラメーター          | 説明                                       |
| --------------- | ---------------------------------------- |
| Auth            | `IAuthentication` インターフェースを持つ認証済みセッション   |
| SharedFolderUid | 対象の共有フォルダのUID                            |
| teamUid         | 共有先のチームUID                               |
| options         | 上記のとおりの `SharedFolderUserOptions` オブジェクト |

</details>

<details>

<summary>PowerCommander</summary>

**コマンド:** `Grant-KeeperSharedFolderTeamSkipSync`

チームを追加または更新します (**`PutTeamToSharedFolderAsync`**)。チームは**UID**またはSDKが解決できる**名前**です。**`ShouldProcess`** に対応します。

| パラメーター                           | 説明                  |
| -------------------------------- | ------------------- |
| `-SharedFolder`, `-Team`         | **必須** (位置 0 および 1) |
| `-ManageRecords`, `-ManageUsers` | 省略可能                |
| `-ExpireIn`, `-ExpireAt`         | 省略可能                |
| `-ShowDetail`, `-PassThru`       | ユーザー付与時と同じ          |

**使用例:**

```powershell
Grant-KeeperSharedFolderTeamSkipSync -SharedFolder '<folder_uid>' -Team '<team_uid_or_name>' -ManageRecords $true
```

</details>

<details>

<summary>Python SDK</summary>

```python
from keepersdk.vault import skip_sync

records = skip_sync.share_shared_folder_to_team(
        keeper_auth_context,
        shared_folder_uid=shared_folder_uid,
        team_name_or_uid=team_name_or_uid,
        manage_users=manage_users,
        manage_records=manage_records,
        expiration=expiration,
    )
```

</details>

### 共有フォルダからのチームアクセス取り消し

共有フォルダ上のチームのアクセスを取り消します。**チームUID**または、一意に特定できる**表示名**を渡します。明示的な**UID**が必要な場合は **`GetTeamUidFromNameAsync`** を利用できます。**`IUserShareOptions`** は、対応する場合に任意で指定できます。

<details>

<summary>DotNet SDK</summary>

**関数:** `RemoveTeamFromSharedFolderAsync`

{% code overflow="wrap" %}

```csharp
public static async Task RemoveTeamFromSharedFolderAsync(
    IAuthentication auth,
    string sharedFolderUid,
    string teamUid)
```

{% endcode %}

**引数:**

| パラメーター          | 説明                                     |
| --------------- | -------------------------------------- |
| Auth            | `IAuthentication` インターフェースを持つ認証済みセッション |
| SharedFolderUid | 対象の共有フォルダのUID                          |
| teamUid         | アクセスを取り消すチームのUID                       |

</details>

<details>

<summary>PowerCommander</summary>

**コマンド:** `Revoke-KeeperSharedFolderTeamSkipSync`

共有フォルダからチームを外します (**`RemoveTeamFromSharedFolderAsync`**)。**`ShouldProcess`** に対応します。

| パラメーター                     | 説明               |
| -------------------------- | ---------------- |
| `-SharedFolder`, `-Team`   | **必須**           |
| `-ShowDetail`, `-PassThru` | ユーザー取り消し時と同じパターン |

**使用例:**

```powershell
Revoke-KeeperSharedFolderTeamSkipSync -SharedFolder '<folder_uid>' -Team '<team_uid_or_name>'
```

</details>

<details>

<summary>Python SDK</summary>

```python
 from keepersdk.vault import skip_sync

skip_sync.revoke_shared_folder_from_team(
        keeper_auth_context,
        shared_folder_uid=shared_folder_uid,
        team_name_or_uid=team_name_or_uid,
    )
```

</details>

### ボルト同期なしでのレコードペイロード

**レコードUID**をすでに持っている場合 (例: **`GetRecordUidsFromSharedFolderAsync`** から取得)、ボルト全体を同期せずに復号済みの **`KeeperRecord`** インスタンスが必要なときに使います。

<details>

<summary>DotNet SDK</summary>

**関数:** `GetRecordsAsync`

{% code overflow="wrap" %}

```csharp
public static async Task<RecordDetailsSkipSyncResult> GetRecordsAsync(
    IAuthentication auth,
    IEnumerable<string> recordUids,
    RecordDetailsInclude include = RecordDetailsInclude.DataPlusShare)
```

{% endcode %}

**引数:**

| パラメーター     | 説明                                                                                                                                                                                                                                               |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Auth       | `IAuthentication` インターフェースを持つ認証済みセッション                                                                                                                                                                                                           |
| recordUids | レコードUIDの一覧                                                                                                                                                                                                                                       |
| include    | <p>取り込むレコード詳細の範囲<br><strong><code>RecordDetailsInclude</code></strong> はAPIの列挙値に対応 (既定は <strong><code>DATA\_PLUS\_SHARE</code></strong>)。より狭いペイロードには <strong><code>DataOnly</code></strong> または <strong><code>ShareOnly</code></strong> を指定。</p> |

**戻り値:** **`RecordDetailsSkipSyncResult`**

| メンバー                         | 意味                                                      |
| ---------------------------- | ------------------------------------------------------- |
| **`Records`**                | 復号に成功したレコード                                             |
| **`NoPermissionRecordUids`** | APIの **`noPermissionRecordUid`** に対応するUID群              |
| **`FailedRecordUids`**       | 応答に含まれたが復号または読み込みに失敗したUID (例: 非対応の **`recordKeyType`**) |
| **`InvalidRecordUids`**      | 呼び出し側の文字列がバイナリのレコードUIDにデコードできず送信されなかったもの                |

</details>

<details>

<summary>PowerCommander</summary>

**コマンド:** `Get-KeeperRecordDetailsByUidSkipSync`

| パラメーター             | 説明                                                                           |
| ------------------ | ---------------------------------------------------------------------------- |
| `-RecordUid`       | 読み込む1件以上のレコードUID                                                             |
| `-SharedFolderUid` | 共有フォルダのUID。`SharedKey` モードでは必須。`Auto` モードでは、所有鍵復号に失敗したレコードのフォールバック先フォルダとして使用 |
| `-Mode`            | 復号戦略: `Auto`、`OwnedKey`、または `SharedKey`                                      |
| `-Include`         | 返すデータ: `DataPlusShare`、`DataOnly`、または `ShareOnly`                            |
| `-PassThru`        | 指定時は整形出力ではなく生の `RecordDetailsSkipSyncResult` オブジェクトを返す                       |

**使用例:**

```powershell
Get-KeeperRecordDetailsByUidSkipSync -RecordUid "uid1", "uid2", "uid3"
Get-KeeperRecordDetailsByUidSkipSync -RecordUid "ABCD1234efgh5678"
```

</details>

<details>

<summary>Python SDK</summary>

**近日公開**

</details>

### 例: 認証、フォルダメタデータ、ユーザー共有 (ボルト全体は同期しない)

<details>

<summary>DotNet SDK</summary>

{% code overflow="wrap" %}

```csharp
using System;
using System.Threading.Tasks;
using KeeperSecurity.Authentication;
using KeeperSecurity.Vault;
using Sample;

// 1) Auth only (no full vault sync)
var auth = await AuthenticateAndGetVault.GetAuthAsync(enablePersistentLogin: null);
if (auth == null)
{
    Console.WriteLine("Could not authenticate.");
    return;
}

var sharedFolderUid = "<your_shared_folder_uid>";
var userEmail = "user@example.com";

// 2) Load shared folder metadata (get_shared_folders)
var folder = await SharedFolderSkipSyncDown.GetSharedFolderAsync(auth, sharedFolderUid);
if (folder == null || folder.SharedFolders == null || folder.SharedFolders.Length == 0)
{
    Console.WriteLine("Could not load shared folder (check UID and access).");
    return;
}

var sf = folder.SharedFolders[0];
Console.WriteLine($"Folder UID: {sf.SharedFolderUid}, revision: {sf.Revision}");

var options = new SharedFolderUserOptions
{
    ManageRecords = true,
    ManageUsers = true,
    Expiration = DateTimeOffset.UtcNow.AddDays(7)
};

try
{
    await SharedFolderSkipSyncDown.PutUserToSharedFolderAsync(auth, sharedFolderUid, userEmail, options);
    Console.WriteLine("Shared folder updated.");
}
catch (Exception ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}

// await SharedFolderSkipSyncDown.RemoveUserFromSharedFolderAsync(auth, sharedFolderUid, userEmail);
```

{% endcode %}

</details>

<details>

<summary>Python SDK</summary>

```python
"""
Example: Share a shared folder to a user WITHOUT syncing the full vault down.

This uses the skip-sync helpers in keepersdk.vault.skip_sync.
"""

import getpass
import json
import logging
from typing import Dict, Optional

import fido2
import webbrowser

from keepersdk import errors, utils
from keepersdk.vault import skip_sync
from keepersdk.authentication import (
    configuration,
    endpoint,
    keeper_auth,
    login_auth,
)
from keepersdk.authentication.yubikey import (
    IKeeperUserInteraction,
    yubikey_authenticate,
)
from keepersdk.constants import KEEPER_PUBLIC_HOSTS

try:
    import pyperclip
except ImportError:
    pyperclip = None

logger = utils.get_logger()
logger.setLevel(logging.INFO)
if not logger.handlers:
    _handler = logging.StreamHandler()
    _handler.setLevel(logging.INFO)
    _handler.setFormatter(
        logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s")
    )
    logger.addHandler(_handler)


class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction):
    def output_text(self, text: str) -> None:
        print(text)

    def prompt_up(self) -> None:
        print(
            "\nTouch the flashing Security key to authenticate or "
            "press Ctrl-C to resume with the primary two factor authentication..."
        )

    def request_pin(self, permissions, rd_id):
        return getpass.getpass("Enter Security Key PIN: ")

    def request_uv(self, permissions, rd_id):
        print("User Verification required.")
        return True


# Two-factor duration codes (used by LoginFlow)
_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = {
    login_auth.TwoFactorDuration.EveryLogin: "login",
    login_auth.TwoFactorDuration.Every12Hours: "12_hours",
    login_auth.TwoFactorDuration.EveryDay: "24_hours",
    login_auth.TwoFactorDuration.Every30Days: "30_days",
    login_auth.TwoFactorDuration.Forever: "forever",
}


class LoginFlow:
    """
    Handles the full login process: server selection, username, password,
    device approval, 2FA, SSO data key, and SSO token.
    """

    def __init__(self) -> None:
        self._config = configuration.JsonConfigurationStorage()
        self._logged_in_with_persistent = True
        self._endpoint: Optional[endpoint.KeeperEndpoint] = None

    @property
    def endpoint(self) -> Optional[endpoint.KeeperEndpoint]:
        return self._endpoint

    @property
    def logged_in_with_persistent(self) -> bool:
        """True if login succeeded by resuming an existing persistent session (no step loop)."""
        return self._logged_in_with_persistent

    def run(self) -> Optional[keeper_auth.KeeperAuth]:
        """
        Run the login flow.

        Returns:
            Authenticated Keeper context, or None if login fails.
        """
        server = self._ensure_server()
        keeper_endpoint = endpoint.KeeperEndpoint(self._config, server)
        self._endpoint = keeper_endpoint
        login_auth_context = login_auth.LoginAuth(keeper_endpoint)

        username = self._config.get().last_login or input("Enter username: ")
        login_auth_context.resume_session = True
        login_auth_context.login(username)

        while not login_auth_context.login_step.is_final():
            step = login_auth_context.login_step
            if isinstance(step, login_auth.LoginStepDeviceApproval):
                self._handle_device_approval(step)
            elif isinstance(step, login_auth.LoginStepTwoFactor):
                self._handle_two_factor(step)
            elif isinstance(step, login_auth.LoginStepPassword):
                self._handle_password(step)
            elif isinstance(step, login_auth.LoginStepSsoToken):
                self._handle_sso_token(step)
            elif isinstance(step, login_auth.LoginStepSsoDataKey):
                self._handle_sso_data_key(step)
            elif isinstance(step, login_auth.LoginStepError):
                print(f"Login error: ({step.code}) {step.message}")
                return None
            else:
                raise NotImplementedError(
                    f"Unsupported login step type: {type(step).__name__}"
                )
            self._logged_in_with_persistent = False

        if self._logged_in_with_persistent:
            print("Successfully logged in with persistent login")

        if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected):
            return login_auth_context.login_step.take_keeper_auth()

        return None

    def _ensure_server(self) -> str:
        if not self._config.get().last_server:
            print("Available server options:")
            for region, host in KEEPER_PUBLIC_HOSTS.items():
                print(f"  {region}: {host}")
            server = (
                input("Enter server (default: keepersecurity.com): ").strip()
                or "keepersecurity.com"
            )
            self._config.get().last_server = server
        else:
            server = self._config.get().last_server
        return server

    def _handle_device_approval(
        self, step: login_auth.LoginStepDeviceApproval
    ) -> None:
        """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume)."""
        menu = [
            ("email_send", "to send email"),
            ("email_code=<code>", "to validate verification code sent via email"),
            ("keeper_push", "to send Keeper Push notification"),
            ("2fa_send", "to send 2FA code"),
            ("2fa_code=<code>", "to validate a code provided by 2FA application"),
            ("<Enter>", "to resume"),
        ]
        lines = ["Approve by selecting a method below"]
        lines.extend(f"  {cmd} {desc}" for cmd, desc in menu)
        print("\n".join(lines))

        selection = input("Type your selection or <Enter> to resume: ").strip()
        if selection is None:
            return
        if selection in ("email_send", "es"):
            step.send_push(channel=login_auth.DeviceApprovalChannel.Email)
            print("An email with instructions has been sent. Press <Enter> when approved.")
        elif selection.startswith("email_code="):
            code = selection[len("email_code=") :]
            step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code)
            print("Successfully verified email code.")
        elif selection in ("keeper_push", "kp"):
            step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush)
            print(
                "Successfully made a push notification to the approved device. "
                "Press <Enter> when approved."
            )
        elif selection in ("2fa_send", "2fs"):
            step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor)
            print("2FA code was sent.")
        elif selection.startswith("2fa_code="):
            code = selection[len("2fa_code=") :]
            step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code)
            print("Successfully verified 2FA code.")
        else:
            step.resume()

    def _handle_password(self, step: login_auth.LoginStepPassword) -> None:
        """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password)."""
        print(f"\nEnter password for {step.username}")
        while True:
            password = getpass.getpass("Password: ")
            if not password:
                raise KeyboardInterrupt()
            try:
                step.verify_password(password)
                break
            except errors.KeeperApiError as kae:
                print(
                    "Invalid email or password combination, please re-enter."
                    if kae.result_code == "auth_failed"
                    else kae.message
                )

    def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None:
        channels = [
            x
            for x in step.get_channels()
            if x.channel_type != login_auth.TwoFactorChannel.Other
        ]
        menu = []
        for i, channel in enumerate(channels):
            desc = self._two_factor_channel_desc(channel.channel_type)
            menu.append(
                (
                    str(i + 1),
                    f"{desc} {channel.channel_name} {channel.phone}",
                )
            )
        menu.append(("q", "Quit authentication attempt and return to Commander prompt."))

        lines = ["", "This account requires 2FA Authentication"]
        lines.extend(f"  {a}. {t}" for a, t in menu)
        print("\n".join(lines))

        while True:
            selection = input("Selection: ")
            if selection is None:
                return
            if selection in ("q", "Q"):
                raise KeyboardInterrupt()
            try:
                assert selection.isnumeric()
                idx = 1 if not selection else int(selection)
                assert 1 <= idx <= len(channels)
                channel = channels[idx - 1]
                desc = self._two_factor_channel_desc(channel.channel_type)
                print(f"Selected {idx}. {desc}")
            except AssertionError:
                print(
                    "Invalid entry, additional factors of authentication shown "
                    "may be configured if not currently enabled."
                )
                continue

            if channel.channel_type in (
                login_auth.TwoFactorChannel.TextMessage,
                login_auth.TwoFactorChannel.KeeperDNA,
                login_auth.TwoFactorChannel.DuoSecurity,
            ):
                action = next(
                    (
                        x
                        for x in step.get_channel_push_actions(channel.channel_uid)
                        if x
                        in (
                            login_auth.TwoFactorPushAction.TextMessage,
                            login_auth.TwoFactorPushAction.KeeperDna,
                        )
                    ),
                    None,
                )
                if action:
                    step.send_push(channel.channel_uid, action)

            if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey:
                try:
                    challenge = json.loads(channel.challenge)
                    signature = yubikey_authenticate(challenge, FidoCliInteraction())
                    if signature:
                        print("Verified Security Key.")
                        step.send_code(channel.channel_uid, signature)
                        return
                except Exception as e:
                    logger.error(e)
                continue

            # 2FA code path
            step.duration = min(step.duration, channel.max_expiration)
            available_dura = sorted(
                x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration
            )
            available_codes = [
                _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura
            ]

            while True:
                mfa_desc = self._two_factor_duration_desc(step.duration)
                prompt_exp = (
                    f"\n2FA Code Duration: {mfa_desc}.\n"
                    f"To change duration: 2fa_duration={'|'.join(available_codes)}"
                )
                print(prompt_exp)

                selection = input("\nEnter 2FA Code or Duration: ")
                if not selection:
                    return
                if selection in available_codes:
                    step.duration = self._two_factor_code_to_duration(selection)
                elif selection.startswith("2fa_duration="):
                    code = selection[len("2fa_duration=") :]
                    if code in available_codes:
                        step.duration = self._two_factor_code_to_duration(code)
                    else:
                        print(f"Invalid 2FA duration: {code}")
                else:
                    try:
                        step.send_code(channel.channel_uid, selection)
                        print("Successfully verified 2FA Code.")
                        return
                    except errors.KeeperApiError as kae:
                        print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}")

    def _handle_sso_data_key(
        self, step: login_auth.LoginStepSsoDataKey
    ) -> None:
        menu = [
            ("1", "Keeper Push. Send a push notification to your device."),
            ("2", "Admin Approval. Request your admin to approve this device."),
            ("r", "Resume SSO authentication after device is approved."),
            ("q", "Quit SSO authentication attempt and return to Commander prompt."),
        ]
        lines = ["Approve this device by selecting a method below:"]
        lines.extend(f"  {cmd:>3}. {text}" for cmd, text in menu)
        print("\n".join(lines))

        while True:
            answer = input("Selection: ")
            if answer is None:
                return
            if answer == "q":
                raise KeyboardInterrupt()
            if answer == "r":
                step.resume()
                break
            if answer in ("1", "2"):
                step.request_data_key(
                    login_auth.DataKeyShareChannel.KeeperPush
                    if answer == "1"
                    else login_auth.DataKeyShareChannel.AdminApproval
                )
            else:
                print(f'Action "{answer}" is not supported.')

    def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None:
        menu = [
            ("a", "SSO User with a Master Password."),
        ]
        if pyperclip:
            menu.append(("c", "Copy SSO Login URL to clipboard."))
        else:
            menu.append(("u", "Show SSO Login URL."))
        try:
            wb = webbrowser.get()
            menu.append(("o", "Navigate to SSO Login URL with the default web browser."))
        except Exception:
            wb = None
        if pyperclip:
            menu.append(("p", "Paste SSO Token from clipboard."))
        menu.append(("t", "Enter SSO Token manually."))
        menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt."))

        lines = [
            "",
            "SSO Login URL:",
            step.sso_login_url,
            "Navigate to SSO Login URL with your browser and complete authentication.",
            "Copy a returned SSO Token into clipboard."
            + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."),
            'NOTE: To copy SSO Token please click "Copy authentication token" '
            'button on "SSO Connect" page.',
            "",
        ]
        lines.extend(f"  {a:>3}. {t}" for a, t in menu)
        print("\n".join(lines))

        while True:
            token = input("Selection: ")
            if token == "q":
                raise KeyboardInterrupt()
            if token == "a":
                step.login_with_password()
                return
            if token == "c":
                token = None
                if pyperclip:
                    try:
                        pyperclip.copy(step.sso_login_url)
                        print("SSO Login URL is copied to clipboard.")
                    except Exception:
                        print("Failed to copy SSO Login URL to clipboard.")
                else:
                    print("Clipboard not available (install pyperclip).")
            elif token == "u":
                token = None
                if not pyperclip:
                    print("\nSSO Login URL:", step.sso_login_url, "\n")
                else:
                    print("Unsupported menu option (use 'c' to copy URL).")
            elif token == "o":
                token = None
                if wb:
                    try:
                        wb.open_new_tab(step.sso_login_url)
                    except Exception:
                        print("Failed to open web browser.")
            elif token == "p":
                if pyperclip:
                    try:
                        token = pyperclip.paste()
                    except Exception:
                        token = ""
                        print("Failed to paste from clipboard")
                else:
                    token = None
                    print("Clipboard not available (use 't' to enter token manually).")
            elif token == "t":
                token = getpass.getpass("Enter SSO Token: ").strip()
            else:
                if len(token) < 10:
                    print(f"Unsupported menu option: {token}")
                    continue

            if token:
                try:
                    step.set_sso_token(token)
                    break
                except errors.KeeperApiError as kae:
                    print(f"SSO Login error: ({kae.result_code}) {kae.message}")

    @staticmethod
    def _two_factor_channel_desc(
        channel_type: login_auth.TwoFactorChannel,
    ) -> str:
        return {
            login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)",
            login_auth.TwoFactorChannel.TextMessage: "Send SMS Code",
            login_auth.TwoFactorChannel.DuoSecurity: "DUO",
            login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID",
            login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)",
            login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)",
            login_auth.TwoFactorChannel.Backup: "Backup Code",
        }.get(channel_type, "Not Supported")

    @staticmethod
    def _two_factor_duration_desc(
        duration: login_auth.TwoFactorDuration,
    ) -> str:
        return {
            login_auth.TwoFactorDuration.EveryLogin: "Require Every Login",
            login_auth.TwoFactorDuration.Forever: "Save on this Device Forever",
            login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours",
            login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours",
            login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days",
        }.get(duration, "Require Every Login")

    @staticmethod
    def _two_factor_code_to_duration(
        text: str,
    ) -> login_auth.TwoFactorDuration:
        for dura, code in _TWO_FACTOR_DURATION_CODES.items():
            if code == text:
                return dura
        return login_auth.TwoFactorDuration.EveryLogin


def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None:
    """
    Enable persistent login and register data key for device.
    Sets persistent_login to on and logout_timer to 30 days.
    """
    keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1')
    keeper_auth.register_data_key_for_device(keeper_auth_context)
    mins_per_day = 60 * 24
    timeout_in_minutes = mins_per_day * 30  # 30 days
    keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes))
    print("Persistent login turned on successfully and device registered")


def login():
    """
    Handle the login process including server selection, authentication,
    and multi-factor authentication steps (device approval, password, 2FA
    with channel selection and Security Key, SSO data key, SSO token).

    Returns:
        tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails.
    """
    flow = LoginFlow()
    keeper_auth_context = flow.run()
    if keeper_auth_context and not flow.logged_in_with_persistent:
        enable_persistent_login(keeper_auth_context)
    keeper_endpoint = flow.endpoint if keeper_auth_context else None
    return keeper_auth_context, keeper_endpoint


def main() -> None:
    keeper_auth_context, _ = login()
    if not keeper_auth_context:
        print("Login failed. Unable to share folder.")
        return

    # Fill these values before running:
    shared_folder_uid = "<your_shared_folder_uid>"
    username = "<user_email@example.com>"

    # Optional share options
    manage_users = True   # allow this user to manage other users in the shared folder
    manage_records = True # allow this user to manage records in the shared folder
    expiration = None     # Unix timestamp seconds, or None for no expiration

    records = skip_sync.share_shared_folder_to_user(
        keeper_auth_context,
        shared_folder_uid=shared_folder_uid,
        username=username,
        manage_users=manage_users,
        manage_records=manage_records,
        expiration=expiration,
    )
    print(f'Shared folder "{shared_folder_uid}" with user "{username}" via skip-sync.')
    print("Records in folder (decrypted title):")
    for row in records:
        print(f"  {row.record_uid}\t{row.name!r}")

    keeper_auth_context.close()


if __name__ == "__main__":
    main()


```

</details>

### 例: チーム、レコードUID、レコード復号 (ボルト全体は同期しない)

<details>

<summary>DotNet SDK</summary>

{% code overflow="wrap" %}

```csharp
using System;
using System.Linq;
using System.Threading.Tasks;
using KeeperSecurity.Authentication;
using KeeperSecurity.Vault;
using Records;
using Sample;

var auth = await AuthenticateAndGetVault.GetAuthAsync(enablePersistentLogin: null);
if (auth == null)
{
    Console.WriteLine("Could not authenticate.");
    return;
}

var sharedFolderUid = "<your_shared_folder_uid>";
var teamUid = "<your_team_uid_base64url>";

var teams = await SharedFolderSkipSyncDown.GetAvailableTeamsForShareAsync(auth);
foreach (var t in teams)
    Console.WriteLine($"{t.Name}: {t.TeamUid}");

await SharedFolderSkipSyncDown.PutTeamToSharedFolderAsync(
    auth, sharedFolderUid, teamUid,
    new SharedFolderUserOptions { ManageRecords = true, ManageUsers = false });

var recordUids = await SharedFolderSkipSyncDown.GetRecordUidsFromSharedFolderAsync(auth, sharedFolderUid);
Console.WriteLine($"Records: {recordUids.Count}");

var details = await RecordSkipSyncDown.GetRecordsAsync(auth, recordUids);
foreach (var r in details.Records)
    Console.WriteLine($"{r.Uid}: {r.Title}");

if (details.NoPermissionRecordUids.Any())
    Console.WriteLine("No permission: " + string.Join(", ", details.NoPermissionRecordUids));
if (details.FailedRecordUids.Any())
    Console.WriteLine("Failed: " + string.Join(", ", details.FailedRecordUids));

// await SharedFolderSkipSyncDown.RemoveTeamFromSharedFolderAsync(auth, sharedFolderUid, teamUid);
```

{% endcode %}

</details>

<details>

<summary>Python SDK</summary>

**近日公開**

</details>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.keeper.io/keeperpam/jp/commander-sdk/keeper-commander-sdks/sdk-command-reference/sharing-commands/shared-folder-without-sync-down.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
