PAMレコードのリンク済み認証情報

PAMリソースレコードでアクセス可能な各フィールドタイプの説明

必要なSDKバージョン: 17.1.1以上

このドキュメントでは、PAMリソースレコードのリンク済み認証情報を操作する際に利用できるプロパティメソッドの一覧と、高度な活用事例を紹介しています。

すべてのプロパティメソッド

for (KeeperRecordLink link : record.getLinks()) {
    // 基本プロパティ
    String targetUid = link.getRecordUid();
    String linkPath = link.getPath(); // 例: "pamUser", "ai_settings", "jit_settings"
    String rawData = link.getData(); // Base64エンコードされたデータ

    // ユーザー権限メソッド
    boolean isAdmin = link.isAdminUser();
    boolean isLaunchCredential = link.isLaunchCredential();

    // 権限メソッド
    boolean allowsRotation = link.allowsRotation();
    boolean allowsConnections = link.allowsConnections();
    boolean allowsPortForwards = link.allowsPortForwards();
    boolean allowsSessionRecording = link.allowsSessionRecording();
    boolean allowsTypescriptRecording = link.allowsTypescriptRecording();
    boolean allowsRemoteBrowserIsolation = link.allowsRemoteBrowserIsolation();

    // 設定関連メソッド
    boolean rotatesOnTermination = link.rotatesOnTermination();
    Integer dataVersion = link.getLinkDataVersion();

    // データ解析メソッド
    boolean hasReadableData = link.hasReadableData();
    boolean hasEncryptedData = link.hasEncryptedData();
    boolean mightBeEncrypted = link.mightBeEncrypted();

    System.out.println("Link Analysis for " + targetUid + ":");
    System.out.println("  Path: " + linkPath);
    System.out.println("  Admin: " + isAdmin);
    System.out.println("  Launch Credential: " + isLaunchCredential);
    System.out.println("  Allows Rotation: " + allowsRotation);
    System.out.println("  Allows Connections: " + allowsConnections);
    System.out.println("  Has Encrypted Data: " + hasEncryptedData);
}

高度な活用事例

高度なデータアクセスパターン

暗号化データの処理

public void handleEncryptedLinkData(KeeperRecord record) {
    for (KeeperRecordLink link : record.getLinks()) {
        if (link.getData() != null) {
            System.out.println("\nAnalyzing link data for: " + link.getRecordUid());
            System.out.println("  Path: " + (link.getPath() != null ? link.getPath() : "null"));

            // 暗号化状態を確認
            boolean mightBeEncrypted = link.mightBeEncrypted();
            boolean hasEncryptedData = link.hasEncryptedData();
            boolean hasReadableData = link.hasReadableData();

            System.out.println("  Encryption Analysis:");
            System.out.println("    mightBeEncrypted(): " + mightBeEncrypted);
            System.out.println("    hasEncryptedData(): " + hasEncryptedData);
            System.out.println("    hasReadableData(): " + hasReadableData);

            try {
                if (hasEncryptedData || mightBeEncrypted) {
                    // 方法1: getDecryptedDataを使用して暗号化コンテンツを復号
                    String decryptedData = link.getDecryptedData(record.getRecordKey());
                    System.out.println("    Decrypted Data: " + decryptedData);

                    // 方法2: getLinkDataを使用して構造化データにアクセス
                    Map<String, Object> linkData = link.getLinkData(record.getRecordKey());
                    if (linkData != null) {
                        System.out.println("    Structured Data:");
                        for (Map.Entry<String, Object> entry : linkData.entrySet()) {
                            System.out.println("      " + entry.getKey() + ": " + entry.getValue());
                        }
                    }
                } else {
                    // プレーンなBase64データ
                    String plainData = link.getDecodedData();
                    System.out.println("    Plain Data: " + plainData);
                }

            } catch (Exception e) {
                System.out.println("    Error processing data: " + e.getMessage());
                System.out.println("    Raw Base64: " + link.getData());
            }
        }
    }
}

設定データ関連メソッドのデモ

public void demonstrateSettingsDataMethods(SecretsManagerOptions options) throws Exception {
    QueryOptions queryOptions = new QueryOptions(
        Collections.emptyList(),
        Collections.emptyList(),
        true  // リンクと設定データを取得
    );

    KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);

    System.out.println("Settings Data Methods Demo:");
    boolean foundSettings = false;

    for (KeeperRecord record : secrets.getRecords()) {
        if (record.getLinks() != null && !record.getLinks().isEmpty()) {
            System.out.println("\nRecord: " + record.getTitle() + " (" + record.getType() + ")");

            for (KeeperRecordLink link : record.getLinks()) {
                String linkPath = link.getPath();
                System.out.println("  Link to " + link.getRecordUid() + " (path: " + linkPath + ")");

                // AI設定メソッドをテスト
                if ("ai_settings".equals(linkPath)) {
                    foundSettings = true;
                    System.out.println("    Testing getAiSettingsData():");
                    try {
                        Map<String, Object> aiSettings = link.getAiSettingsData(record.getRecordKey());
                        if (aiSettings != null) {
                            System.out.println("      AI Settings found:");
                            for (Map.Entry<String, Object> entry : aiSettings.entrySet()) {
                                System.out.println("        " + entry.getKey() + ": " + entry.getValue());
                            }
                        } else {
                            System.out.println("      AI Settings data could not be parsed");
                        }
                    } catch (Exception e) {
                        System.out.println("      Error parsing AI settings: " + e.getMessage());
                    }
                }

                // JIT設定メソッドをテスト
                if ("jit_settings".equals(linkPath)) {
                    foundSettings = true;
                    System.out.println(" Testing getJitSettingsData():");
                    try {
                        Map<String, Object> jitSettings = link.getJitSettingsData(record.getRecordKey());
                        if (jitSettings != null) {
                            System.out.println("   JIT Settings found:");
                            for (Map.Entry<String, Object> entry : jitSettings.entrySet()) {
                                System.out.println("        " + entry.getKey() + ": " + entry.getValue());
                            }
                        } else {
                            System.out.println("       JIT Settings data could not be parsed");
                        }
                    } catch (Exception e) {
                        System.out.println("   Error parsing JIT settings: " + e.getMessage());
                    }
                }

                // 一般的な getSettingsForPath メソッドをテスト
                if (linkPath != null && linkPath.endsWith("_settings")) {
                    foundSettings = true;
                    System.out.println("  Testing getSettingsForPath(\"" + linkPath + "\"):");
                    try {
                        Map<String, Object> settings = link.getSettingsForPath(linkPath, record.getRecordKey());
                        if (settings != null) {
                            System.out.println("   Settings found for path '" + linkPath + "':");
                            for (Map.Entry<String, Object> entry : settings.entrySet()) {
                                System.out.println("        " + entry.getKey() + ": " + entry.getValue());
                            }
                        } else {
                            System.out.println("       Settings data could not be parsed");
                        }
                    } catch (Exception e) {
                        System.out.println("   Error parsing settings: " + e.getMessage());
                    }
                }
            }
        }
    }
    if (!foundSettings) {
        System.out.println(" No settings paths found in current records.");
        System.out.println(" Settings paths to look for: ai_settings, jit_settings, *_settings");
    }
}

複雑な関係性の分析 (DAG解析)

public class ComprehensiveDagAnalyzer {

    public static void analyzeLinkingPatterns(SecretsManagerOptions options) throws Exception {
        QueryOptions queryOptions = new QueryOptions(
            Collections.emptyList(),
            Collections.emptyList(),
            true // リンクを取得
        );

        KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);

        // 関係マップを構築
        Map<String, List<String>> relationshipMap = new HashMap<>();
        Map<String, String> recordTitles = new HashMap<>();
        Map<String, String> recordTypes = new HashMap<>();
        int totalLinks = 0;

        for (KeeperRecord record : secrets.getRecords()) {
            recordTitles.put(record.getRecordUid(), record.getTitle());
            recordTypes.put(record.getRecordUid(), record.getType());

            List<KeeperRecordLink> links = record.getLinks();
            if (links != null && !links.isEmpty()) {
                List<String> targets = new ArrayList<>();
                for (KeeperRecordLink link : links) {
                    targets.add(link.getRecordUid());
                    totalLinks++;
                }
                relationshipMap.put(record.getRecordUid(), targets);
            }
        }

        System.out.println("DAG Analysis Report:");
        System.out.println("===================");
        System.out.println("Total records: " + secrets.getRecords().size());
        System.out.println("Records with outgoing links: " + relationshipMap.size());
        System.out.println("Total links: " + totalLinks);

        // 参照対象 (被リンク) になっているレコードを抽出
        Set<String> targets = new HashSet<>();
        for (List<String> linkTargets : relationshipMap.values()) {
            targets.addAll(linkTargets);
        }
        System.out.println("Records that are link targets: " + targets.size());

        // 詳細な関係パターンを表示
        if (!relationshipMap.isEmpty()) {
            System.out.println("\nDetailed Relationship Patterns:");
            for (Map.Entry<String, List<String>> entry : relationshipMap.entrySet()) {
                String sourceTitle = recordTitles.get(entry.getKey());
                String sourceType = recordTypes.get(entry.getKey());
                System.out.println("  " + sourceTitle + " [" + sourceType + "] → " + entry.getValue().size() + " record(s)");

                for (String targetUid : entry.getValue()) {
                    String targetTitle = recordTitles.get(targetUid);
                    String targetType = recordTypes.get(targetUid);
                    System.out.println("    → " + (targetTitle != null ? targetTitle : "Unknown record: " + targetUid) +
                        " [" + (targetType != null ? targetType : "Unknown") + "]");
                }
            }
        } else {
            System.out.println("  No links found in current records.");
        }
    }
}

高度なPAMユーザー管理

public static List<UserStatus> getPamMachineUsers(String pamMachineUid, InMemoryStorage storage) {
    List<UserStatus> users = new ArrayList<>();

    try {
        QueryOptions queryOptions = new QueryOptions(
            Collections.emptyList(),
            Collections.emptyList(),
            true // リンクを取得
        );

        SecretsManagerOptions options = new SecretsManagerOptions(storage);
        KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);

        // PAMマシンレコードを検索
        KeeperRecord pamMachine = null;
        for (KeeperRecord record : secrets.getRecords()) {
            if (record.getRecordUid().equals(pamMachineUid) && "pamMachine".equals(record.getType())) {
                pamMachine = record;
                break;
            }
        }

        if (pamMachine != null && pamMachine.getLinks() != null) {
            System.out.println("PAM Machine: " + pamMachine.getTitle());
            System.out.println("このPAMマシンに関連付けられたユーザー数: " + pamMachine.getLinks().size());

            for (KeeperRecordLink link : pamMachine.getLinks()) {
                // リンクされたユーザーレコードを検索
                for (KeeperRecord record : secrets.getRecords()) {
                    if (record.getRecordUid().equals(link.getRecordUid()) && "pamUser".equals(record.getType())) {
                        // SDKユーティリティメソッドで管理者ステータスを確認
                        boolean isAdmin = link.isAdminUser();
                        boolean isLaunchCredential = link.isLaunchCredential();

                        users.add(new UserStatus(record.getTitle(), record.getRecordUid(), isAdmin, isLaunchCredential));
                        System.out.println("  " + record.getTitle() + " - [" + (isAdmin ? "IS ADMIN" : "IS NOT ADMIN") + "] [" +
                                         (isLaunchCredential ? "IS LAUNCH CREDENTIAL" : "IS NOT LAUNCH CREDENTIAL") + "]");
                        break;
                    }
                }
            }

            // PAM構成を検証(テストロジックに基づく)
            long adminUsersCount = users.stream().filter(UserStatus::isAdmin).count();
            long launchCredentialUsersCount = users.stream().filter(UserStatus::isLaunchCredential).count();

            System.out.println("Summary:");
            System.out.println("  Admin users: " + adminUsersCount);
            System.out.println("  Launch credential users: " + launchCredentialUsersCount);

            if (adminUsersCount > 1) {
                System.out.println("Warning: 複数の管理者ユーザーが検出されました(通常は1人であるべきです)");
            }
            if (launchCredentialUsersCount > 1) {
                System.out.println("Warning: 複数の起動用認証情報が検出されました(通常は1人であるべきです)");
            }
        }

    } catch (Exception e) {
        System.err.println("PAMマシンユーザーの取得中にエラーが発生しました: " + e.getMessage());
    }

    return users;
}

public static class UserStatus {
    private final String userName;
    private final String userUid;
    private final boolean isAdmin;
    private final boolean isLaunchCredential;

    public UserStatus(String userName, String userUid, boolean isAdmin, boolean isLaunchCredential) {
        this.userName = userName;
        this.userUid = userUid;
        this.isAdmin = isAdmin;
        this.isLaunchCredential = isLaunchCredential;
    }

    public String getUserName() { return userName; }
    public String getUserUid() { return userUid; }
    public boolean isAdmin() { return isAdmin; }
    public boolean isLaunchCredential() { return isLaunchCredential; }

    @Override
    public String toString() {
        return userName + " (" + userUid + ") - [" +
               (isAdmin ? "IS ADMIN" : "IS NOT ADMIN") + "] [" +
               (isLaunchCredential ? "IS LAUNCH CREDENTIAL" : "IS NOT LAUNCH CREDENTIAL") + "]";
    }
}

包括的なリンク済みレコードデータ分析

public static void analyzeLinkDataStructure(SecretsManagerOptions options) throws Exception {
    QueryOptions queryOptions = new QueryOptions(
        Collections.emptyList(),
        Collections.emptyList(),
        true
    );

    KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);

    System.out.println("Link Data Structure Analysis:");
    for (KeeperRecord record : secrets.getRecords()) {
        if (record.getLinks() != null && !record.getLinks().isEmpty()) {
            System.out.println("\nRecord: " + record.getTitle() + " (" + record.getType() + ")");
            System.out.println("UID: " + record.getRecordUid());

            for (int i = 0; i < record.getLinks().size(); i++) {
                KeeperRecordLink link = record.getLinks().get(i);
                System.out.println("  Link " + (i + 1) + ":");
                System.out.println("    Target UID: " + link.getRecordUid());
                System.out.println("    Path: " + (link.getPath() != null ? link.getPath() : "null"));

                if (link.getData() != null) {
                    try {
                        if (link.hasEncryptedData() || link.mightBeEncrypted()) {
                            String decodedData = link.getDecryptedData(record.getRecordKey());
                            System.out.println("    Decoded Data: " + decodedData);
                        } else {
                            String decodedData = link.getDecodedData();
                            System.out.println("    Decoded Data: " + decodedData);
                        }
                    } catch (Exception e) {
                        System.out.println("    Raw Base64: " + link.getData());
                        System.out.println("    Decode Error: " + e.getMessage());
                    }
                } else {
                    System.out.println("    Data: null");
                }
            }
        }
    }
}

ユーティリティメソッドの完全な例

public static void demonstrateLinkUtilityMethods(SecretsManagerOptions options) throws Exception {
    QueryOptions queryOptions = new QueryOptions(
        Collections.emptyList(),
        Collections.emptyList(),
        true
    );
    
    KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);
    
    System.out.println("Link Utility Methods Demo:");
    for (KeeperRecord record : secrets.getRecords()) {
        if (record.getLinks() != null && !record.getLinks().isEmpty()) {
            System.out.println("\nRecord: " + record.getTitle() + " (" + record.getType() + ")");
            
            for (int i = 0; i < record.getLinks().size(); i++) {
                KeeperRecordLink link = record.getLinks().get(i);
                System.out.println("  Link " + (i + 1) + " to " + link.getRecordUid() + ":");
                System.out.println("    Path: " + (link.getPath() != null ? link.getPath() : "null"));

                // ユーザー関連ユーティリティ
                if (link.isAdminUser()) {
                    System.out.println("     Admin user");
                }
                if (link.isLaunchCredential()) {
                    System.out.println("     Launch credential");
                }

                // 権限関連ユーティリティ
                if (link.allowsRotation()) {
                    System.out.println("     Allows rotation");
                }
                if (link.allowsConnections()) {
                    System.out.println("     Allows connections");
                }
                if (link.allowsPortForwards()) {
                    System.out.println("     Allows port forwards");
                }
                if (link.allowsSessionRecording()) {
                    System.out.println("     Allows session recording");
                }
                if (link.allowsTypescriptRecording()) {
                    System.out.println("     Allows typescript recording");
                }
                if (link.allowsRemoteBrowserIsolation()) {
                    System.out.println("     Allows remote browser isolation");
                }

                // 設定関連ユーティリティ
                if (link.rotatesOnTermination()) {
                    System.out.println("     Rotates on termination");
                }

                Integer version = link.getLinkDataVersion();
                if (version != null) {
                    System.out.println("     Data version: " + version);
                }

                if (link.hasReadableData()) {
                    System.out.println("     Has readable JSON data");
                } else if (link.getData() != null) {
                    System.out.println("     Has encrypted/binary data");
                } else {
                    System.out.println("     No data");
                }
            }
        }
    }
}

メソッド完全リファレンス

メソッド
戻り値
説明

getRecordUid()

String

対象レコードのUID

getPath()

String

リンクメタデータのタイプ

getData()

String

Base64エンコードされた生データ

isAdminUser()

boolean

ユーザーが管理者権限を持つか

isLaunchCredential()

boolean

起動用認証情報かどうか

allowsRotation()

boolean

パスワードローテーションが許可されているか

allowsConnections()

boolean

接続が許可されているか

allowsPortForwards()

boolean

ポートフォワーディングが許可されているか

allowsSessionRecording()

boolean

セッション録画が有効か

allowsTypescriptRecording()

boolean

Typescript録画が有効か

allowsRemoteBrowserIsolation()

boolean

リモートブラウザ分離が許可されているか

rotatesOnTermination()

boolean

セッション終了時にパスワードがローテーションされるか

getLinkDataVersion()

Integer

データ形式のバージョン番号

hasReadableData()

boolean

データが可読なJSON形式か

hasEncryptedData()

boolean

データが暗号化されているか

mightBeEncrypted()

boolean

データが暗号化されている可能性があるか (ヒューリスティック検出)

getDecryptedData(byte[])

String

レコードキーを使ってデータを復号

getDecodedData()

String

暗号化なしでBase64デコード

getLinkData(byte[])

Map<String, Object>

汎用的な暗号化データへのアクセス

getAiSettingsData(byte[])

Map<String, Object>

AI設定専用のアクセス

getJitSettingsData(byte[])

Map<String, Object>

JIT設定専用のアクセス

getSettingsForPath(String, byte[])

Map<String, Object>

パス指定による汎用的な設定アクセス

インフラ管理のためのDAG概念

有向非巡回グラフ (Directed Acyclic Graphs) の理解

GraphSyncは有向非巡回グラフ (DAG) 構造を実装しています。

  • 有向 (DIRECTED): リンクには方向があります (A → BはB → Aと同じではありません)

  • 非巡回 (ACYCLIC): 循環参照は存在しません (A → B → C → Aは許可されません)

  • グラフ (GRAPH): レコード (ノード) がリンク (エッジ) で接続されています

利点

  • ✅ 依存関係の追跡 (「このサーバーはこのデータベースを必要とする」)

  • ✅ 関連する認証情報の整理

  • ✅ インフラの関係性を把握

  • ✅ セキュリティ境界の維持


パフォーマンス最適化

効率的な処理戦略

public class OptimizedGraphOperations {

    // 戦略1: 選択的な取得
    public void processSpecificRecords(List<String> recordUids, SecretsManagerOptions options) throws Exception {
        // 特定のレコードのみリンク付きで取得し、帯域幅を削減
        QueryOptions filtered = new QueryOptions(
            recordUids,  // このレコードだけ
            Collections.emptyList(),
            true
        );

        KeeperSecrets secrets = SecretsManager.getSecrets2(options, filtered);
        // 必要なものだけを処理
    }

    // 戦略2: 複数の操作に備えたキャッシュ
    private Map<String, KeeperRecord> recordCache = new HashMap<>();
    private Map<String, List<KeeperRecord>> typeCache = new HashMap<>();

    public void initializeCache(SecretsManagerOptions options) throws Exception {
        QueryOptions queryOptions = new QueryOptions(null, null, true);
        KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);

        // 効率的な検索構造を構築
        for (KeeperRecord record : secrets.getRecords()) {
            recordCache.put(record.getRecordUid(), record);
            typeCache.computeIfAbsent(record.getType(), k -> new ArrayList<>()).add(record);
        }
    }

    public List<KeeperRecord> getPamMachines() {
        return typeCache.getOrDefault("pamMachine", new ArrayList<>());
    }

    public KeeperRecord getRecord(String uid) {
        return recordCache.get(uid);
    }
}

エラーハンドリングのベストプラクティス

public void robustLinkProcessing(SecretsManagerOptions options) {
    try {
        QueryOptions queryOptions = new QueryOptions(null, null, true);
        KeeperSecrets secrets = SecretsManager.getSecrets2(options, queryOptions);

        for (KeeperRecord record : secrets.getRecords()) {
            try {
                List<KeeperRecordLink> links = record.getLinks();

                // nullリンクへの対応(requestLinks=false の場合)
                if (links == null) {
                    System.err.println("Warning: Links not available for " + record.getTitle() +
                                     " - ensure requestLinks=true in QueryOptions");
                    continue;
                }

                // 各リンクを安全に処理
                for (KeeperRecordLink link : links) {
                    try {
                        // 暗号化データへ安全にアクセス
                        if (link.hasEncryptedData()) {
                            Map<String, Object> data = link.getLinkData(record.getRecordKey());
                            if (data != null) {
                                // 復号と処理に成功
                                System.out.println("Processed encrypted data for " + link.getRecordUid());
                            } else {
                                System.out.println("Could not decrypt data for link to " + link.getRecordUid());
                            }
                        }

                        // リンクのプロパティへ安全にアクセス
                        boolean isAdmin = link.isAdminUser();
                        boolean allowsRotation = link.allowsRotation();
                        // ... 他のプロパティ

                    } catch (Exception linkError) {
                        System.err.println("Error processing link to " + link.getRecordUid() + ": " + linkError.getMessage());
                    }
                }

            } catch (Exception recordError) {
                System.err.println("Error processing record " + record.getRecordUid() + ": " + recordError.getMessage());
            }
        }

    } catch (Exception e) {
        System.err.println("Failed to retrieve records with links: " + e.getMessage());
        e.printStackTrace();
    }
}

実装上の重要な注意点

重要な理解事項

  • リンクとファイルの違い: UpdateOptionslinksToRemove パラメータはレコードリンクではなくファイルを削除します

  • Nullと空の違い: requestLinks=false の場合、links フィールドはnullrequestLinks=true でもリンクが存在しない場合は空リストになります

  • パフォーマンスへの影響: リンクをリクエストするとレスポンスサイズと処理時間が大幅に増加します

  • 暗号化: リンクデータは暗号化されている可能性があり、復号にはレコードキーが必要です

セキュリティ上の考慮事項

  • キー管理: リンクデータを復号する際は、常にソースレコードのキーを使用すること

  • アクセス制御: リンクのプロパティは、どの操作が許可されているかを示します

  • 検証: 操作を行う前に必ずリンクプロパティを確認すること

テスト実装からのベストプラクティス

  • 必要なときのみリンクをリクエスト: パフォーマンスのために requestLinks=true は慎重に使用する

  • 可能な限りレコードをフィルタ: recordsFilter を利用してデータ取得を制限する

  • 結果をキャッシュ: 同じデータに対して複数の操作を行う場合はルックアップマップを構築する

  • エラーを適切に処理: リンクデータの復号やアクセスは失敗する可能性がある

  • 要件を検証: リンクプロパティが期待どおりの権限を持っているか確認する

  • 構成をテスト: PAMのセットアップで管理者や起動用認証情報の数が正しいことを検証する

Kotlin対応

// Kotlin実装(構文を強化)
fun analyzeAdvancedGraphRelationships(options: SecretsManagerOptions) {
    val queryOptions = QueryOptions(
        recordsFilter = emptyList(),
        foldersFilter = emptyList(),
        requestLinks = true
    )
    
    val secrets = getSecrets2(options, queryOptions)
    
    secrets.records.forEach { record ->
        record.links?.forEach { link ->
            println("${record.title} → ${link.recordUid}")
            
            // Kotlinの簡潔な構文を使った高度なプロパティチェック
            when {
                link.isAdminUser() -> println("   Admin privileges")
                link.isLaunchCredential() -> println("   Launch credential")
                link.allowsRotation() -> println("   Rotation allowed")
                link.allowsConnections() -> println("   Connections allowed")
                link.allowsSessionRecording() -> println("   Recording enabled")
            }
            
            // セーフコールを用いた設定データアクセス
            when (link.path) {
                "ai_settings" -> {
                    link.getAiSettingsData(record.recordKey)?.let { aiData ->
                        println("  AI: ${aiData["aiEnabled"]} - Model: ${aiData["aiModel"]}")
                    }
                }
                "jit_settings" -> {
                    link.getJitSettingsData(record.recordKey)?.let { jitData ->
                        println("  JIT: ${jitData["enabled"]} - TTL: ${jitData["ttl"]}s")
                    }
                }
            }
            
            // 汎用的な暗号化データアクセス
            link.getLinkData(record.recordKey)?.let { data ->
                data.forEach { (key, value) ->
                    println("  $key: $value")
                }
            }
        }
    }
}

最終更新