@wolpertingerlabs/drawlatch 1.0.0-alpha.2 → 1.0.0-alpha.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONNECTIONS.md +3 -0
- package/README.md +395 -465
- package/bin/drawlatch.js +1018 -49
- package/dist/auth/auth.d.ts +10 -0
- package/dist/auth/auth.js +146 -0
- package/dist/auth/env-writer.d.ts +6 -0
- package/dist/auth/env-writer.js +52 -0
- package/dist/auth/password.d.ts +8 -0
- package/dist/auth/password.js +28 -0
- package/dist/auth/sessions.d.ts +13 -0
- package/dist/auth/sessions.js +85 -0
- package/dist/cli/generate-keys.d.ts +4 -4
- package/dist/cli/generate-keys.js +30 -25
- package/dist/connections/{anthropic.json → ai/anthropic.json} +19 -1
- package/dist/connections/{devin.json → ai/devin.json} +7 -1
- package/dist/connections/{google-ai.json → ai/google-ai.json} +7 -1
- package/dist/connections/{openai.json → ai/openai.json} +7 -1
- package/dist/connections/{openrouter.json → ai/openrouter.json} +7 -1
- package/dist/connections/developer-tools/github.json +138 -0
- package/dist/connections/{hex.json → developer-tools/hex.json} +7 -1
- package/dist/connections/developer-tools/linear.json +75 -0
- package/dist/connections/{lichess.json → gaming/lichess.json} +7 -1
- package/dist/connections/messaging/agentmail.json +21 -0
- package/dist/connections/messaging/discord-bot.json +114 -0
- package/dist/connections/{discord-oauth.json → messaging/discord-oauth.json} +7 -1
- package/dist/connections/messaging/slack.json +75 -0
- package/dist/connections/messaging/telegram.json +66 -0
- package/dist/connections/{google.json → productivity/google.json} +7 -1
- package/dist/connections/productivity/notion.json +75 -0
- package/dist/connections/productivity/stripe.json +74 -0
- package/dist/connections/productivity/trello.json +113 -0
- package/dist/connections/{bluesky.json → social-media/bluesky.json} +41 -0
- package/dist/connections/social-media/mastodon.json +65 -0
- package/dist/connections/social-media/reddit.json +80 -0
- package/dist/connections/{twitch.json → social-media/twitch.json} +40 -0
- package/dist/connections/social-media/x.json +67 -0
- package/dist/mcp/server.js +544 -31
- package/dist/remote/admin-mutations.d.ts +37 -0
- package/dist/remote/admin-mutations.js +251 -0
- package/dist/remote/admin-types.d.ts +149 -0
- package/dist/remote/admin-types.js +11 -0
- package/dist/remote/admin.d.ts +37 -0
- package/dist/remote/admin.js +316 -0
- package/dist/remote/caller-bootstrap.d.ts +71 -0
- package/dist/remote/caller-bootstrap.js +141 -0
- package/dist/remote/ingestors/base-ingestor.d.ts +19 -3
- package/dist/remote/ingestors/base-ingestor.js +27 -6
- package/dist/remote/ingestors/discord/discord-gateway.d.ts +2 -2
- package/dist/remote/ingestors/discord/discord-gateway.js +29 -6
- package/dist/remote/ingestors/e2e/setup.d.ts +69 -0
- package/dist/remote/ingestors/e2e/setup.js +147 -0
- package/dist/remote/ingestors/manager.d.ts +110 -10
- package/dist/remote/ingestors/manager.js +449 -42
- package/dist/remote/ingestors/poll/poll-ingestor.d.ts +4 -2
- package/dist/remote/ingestors/poll/poll-ingestor.js +26 -5
- package/dist/remote/ingestors/registry.d.ts +2 -2
- package/dist/remote/ingestors/registry.js +2 -2
- package/dist/remote/ingestors/slack/socket-mode.d.ts +2 -2
- package/dist/remote/ingestors/slack/socket-mode.js +28 -6
- package/dist/remote/ingestors/types.d.ts +25 -0
- package/dist/remote/ingestors/webhook/base-webhook-ingestor.d.ts +46 -7
- package/dist/remote/ingestors/webhook/base-webhook-ingestor.js +115 -10
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.d.ts +30 -0
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.js +73 -2
- package/dist/remote/ingestors/webhook/lifecycle-types.d.ts +63 -0
- package/dist/remote/ingestors/webhook/lifecycle-types.js +12 -0
- package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.js +2 -2
- package/dist/remote/ingestors/webhook/trello-webhook-ingestor.d.ts +17 -5
- package/dist/remote/ingestors/webhook/trello-webhook-ingestor.js +32 -26
- package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.d.ts +46 -0
- package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.js +261 -0
- package/dist/remote/server.d.ts +25 -33
- package/dist/remote/server.js +681 -195
- package/dist/remote/tool-dispatch.d.ts +84 -0
- package/dist/remote/tool-dispatch.js +910 -0
- package/dist/remote/triggers/rule-engine.d.ts +40 -0
- package/dist/remote/triggers/rule-engine.js +228 -0
- package/dist/remote/triggers/types.d.ts +69 -0
- package/dist/remote/triggers/types.js +10 -0
- package/dist/remote/tunnel-state.d.ts +14 -0
- package/dist/remote/tunnel-state.js +20 -0
- package/dist/remote/tunnel.d.ts +53 -0
- package/dist/remote/tunnel.js +149 -0
- package/dist/shared/config.d.ts +97 -25
- package/dist/shared/config.js +48 -35
- package/dist/shared/connections.d.ts +21 -5
- package/dist/shared/connections.js +56 -14
- package/dist/shared/crypto/index.d.ts +1 -0
- package/dist/shared/crypto/index.js +1 -0
- package/dist/shared/crypto/key-manager.d.ts +67 -0
- package/dist/shared/crypto/key-manager.js +152 -0
- package/dist/shared/env-utils.d.ts +42 -0
- package/dist/shared/env-utils.js +150 -0
- package/dist/shared/listener-config.d.ts +157 -0
- package/dist/shared/listener-config.js +10 -0
- package/dist/shared/migrations.d.ts +40 -0
- package/dist/shared/migrations.js +122 -0
- package/dist/shared/protocol/handshake.js +14 -1
- package/dist/shared/protocol/index.d.ts +2 -0
- package/dist/shared/protocol/index.js +2 -0
- package/dist/shared/protocol/sync-client.d.ts +52 -0
- package/dist/shared/protocol/sync-client.js +99 -0
- package/dist/shared/protocol/sync.d.ts +71 -0
- package/dist/shared/protocol/sync.js +176 -0
- package/frontend/dist/assets/index-D4k5QKBP.css +1 -0
- package/frontend/dist/assets/index-IzwnSOmu.js +267 -0
- package/frontend/dist/index.html +15 -0
- package/package.json +66 -13
- package/dist/connections/discord-bot.json +0 -24
- package/dist/connections/github.json +0 -25
- package/dist/connections/linear.json +0 -29
- package/dist/connections/mastodon.json +0 -25
- package/dist/connections/notion.json +0 -33
- package/dist/connections/reddit.json +0 -28
- package/dist/connections/slack.json +0 -23
- package/dist/connections/stripe.json +0 -25
- package/dist/connections/telegram.json +0 -26
- package/dist/connections/trello.json +0 -25
- package/dist/connections/x.json +0 -27
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* This prevents unknown-key-share attacks.
|
|
31
31
|
*/
|
|
32
32
|
import crypto from 'node:crypto';
|
|
33
|
-
import { deriveSessionKeys, EncryptedChannel, } from '../crypto/index.js';
|
|
33
|
+
import { deriveSessionKeys, fingerprint, EncryptedChannel, } from '../crypto/index.js';
|
|
34
34
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
35
35
|
function signData(privateKey, ...parts) {
|
|
36
36
|
const combined = Buffer.concat(parts.map((p) => (typeof p === 'string' ? Buffer.from(p, 'utf-8') : p)));
|
|
@@ -149,6 +149,19 @@ export class HandshakeResponder {
|
|
|
149
149
|
return akPem === init.signingPubKey;
|
|
150
150
|
});
|
|
151
151
|
if (!authorized) {
|
|
152
|
+
// Log fingerprints for diagnosis
|
|
153
|
+
const initiatorFp = crypto
|
|
154
|
+
.createHash('sha256')
|
|
155
|
+
.update(initiatorSigningKey.export({ type: 'spki', format: 'der' }))
|
|
156
|
+
.digest();
|
|
157
|
+
const fpStr = Array.from(initiatorFp.subarray(0, 16))
|
|
158
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
159
|
+
.join(':');
|
|
160
|
+
console.error(`[handshake] Rejected: no authorized peer matches signing key fingerprint ${fpStr}`);
|
|
161
|
+
console.error('[handshake] Authorized peer fingerprints:');
|
|
162
|
+
for (const ak of this.authorizedKeys) {
|
|
163
|
+
console.error(`[handshake] ${fingerprint(ak)}`);
|
|
164
|
+
}
|
|
152
165
|
throw new Error('Handshake failed: initiator not authorized');
|
|
153
166
|
}
|
|
154
167
|
// Verify initiator's signature over (ephemeralPubKey || nonceI)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { type HandshakeInit, type HandshakeReply, type HandshakeFinish, type HandshakeMessage, HandshakeInitiator, HandshakeResponder, } from './handshake.js';
|
|
2
2
|
export { type ProxyRequest, type ProxyResponse, type PingMessage, type PongMessage, type AppMessage, } from './messages.js';
|
|
3
|
+
export { type SyncRequest, type SyncResponse, type SyncSession, type SyncErrorCode, generateSyncCode, generateSyncEncryptionKey, encryptSyncPayload, decryptSyncPayload, validateSyncRequest, isSyncSessionActive, DEFAULT_SYNC_TTL_MS, MAX_SYNC_ATTEMPTS, } from './sync.js';
|
|
4
|
+
export { type KeyExchangeInit, type SyncResult, type StartKeyExchangeOpts, SyncClientError, startKeyExchange, } from './sync-client.js';
|
|
3
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { HandshakeInitiator, HandshakeResponder, } from './handshake.js';
|
|
2
|
+
export { generateSyncCode, generateSyncEncryptionKey, encryptSyncPayload, decryptSyncPayload, validateSyncRequest, isSyncSessionActive, DEFAULT_SYNC_TTL_MS, MAX_SYNC_ATTEMPTS, } from './sync.js';
|
|
3
|
+
export { SyncClientError, startKeyExchange, } from './sync-client.js';
|
|
2
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync client — callboard-side API for the key exchange protocol.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const exchange = startKeyExchange({ remoteUrl, inviteCode, encryptionKey, callerAlias });
|
|
6
|
+
* // Display exchange.confirmCode to the user
|
|
7
|
+
* // User enters confirmCode into drawlatch
|
|
8
|
+
* const result = await exchange.complete();
|
|
9
|
+
* // Keys exchanged, remote public keys saved locally
|
|
10
|
+
*/
|
|
11
|
+
import type { SerializedPublicKeys } from '../crypto/keys.js';
|
|
12
|
+
import { type SyncErrorCode } from './sync.js';
|
|
13
|
+
export interface KeyExchangeInit {
|
|
14
|
+
/** Display this code to the user — they enter it into drawlatch. */
|
|
15
|
+
confirmCode: string;
|
|
16
|
+
/** The caller's public keys (already generated). */
|
|
17
|
+
publicKeys: SerializedPublicKeys;
|
|
18
|
+
/** Call after the user has entered the confirm code into drawlatch. */
|
|
19
|
+
complete(): Promise<SyncResult>;
|
|
20
|
+
}
|
|
21
|
+
export interface SyncResult {
|
|
22
|
+
remotePublicKeys: SerializedPublicKeys;
|
|
23
|
+
callerAlias: string;
|
|
24
|
+
fingerprint: string;
|
|
25
|
+
}
|
|
26
|
+
export declare class SyncClientError extends Error {
|
|
27
|
+
readonly code: SyncErrorCode;
|
|
28
|
+
readonly statusCode?: number | undefined;
|
|
29
|
+
constructor(message: string, code: SyncErrorCode, statusCode?: number | undefined);
|
|
30
|
+
}
|
|
31
|
+
export interface StartKeyExchangeOpts {
|
|
32
|
+
/** Drawlatch remote server URL (e.g., "http://host:9999"). */
|
|
33
|
+
remoteUrl: string;
|
|
34
|
+
/** Invite code from drawlatch sync output. */
|
|
35
|
+
inviteCode: string;
|
|
36
|
+
/** AES-256-GCM encryption key from drawlatch sync output (base64). */
|
|
37
|
+
encryptionKey: string;
|
|
38
|
+
/** Desired caller alias on the remote server. */
|
|
39
|
+
callerAlias: string;
|
|
40
|
+
/** Override config directory (default: ~/.drawlatch). */
|
|
41
|
+
configDir?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Start the key exchange from callboard's side.
|
|
45
|
+
*
|
|
46
|
+
* 1. Creates caller keys if they don't exist yet
|
|
47
|
+
* 2. Generates a confirm code
|
|
48
|
+
* 3. Returns the confirm code + a `complete()` function
|
|
49
|
+
* 4. `complete()` encrypts and sends POST /sync, decrypts response, saves remote keys
|
|
50
|
+
*/
|
|
51
|
+
export declare function startKeyExchange(opts: StartKeyExchangeOpts): KeyExchangeInit;
|
|
52
|
+
//# sourceMappingURL=sync-client.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync client — callboard-side API for the key exchange protocol.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const exchange = startKeyExchange({ remoteUrl, inviteCode, encryptionKey, callerAlias });
|
|
6
|
+
* // Display exchange.confirmCode to the user
|
|
7
|
+
* // User enters confirmCode into drawlatch
|
|
8
|
+
* const result = await exchange.complete();
|
|
9
|
+
* // Keys exchanged, remote public keys saved locally
|
|
10
|
+
*/
|
|
11
|
+
import { createCaller, exportCallerPublicKeys, saveServerPublicKeys, callerExists, } from '../crypto/key-manager.js';
|
|
12
|
+
import { generateSyncCode, encryptSyncPayload, decryptSyncPayload, } from './sync.js';
|
|
13
|
+
export class SyncClientError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
statusCode;
|
|
16
|
+
constructor(message, code, statusCode) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.statusCode = statusCode;
|
|
20
|
+
this.name = 'SyncClientError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Start the key exchange from callboard's side.
|
|
25
|
+
*
|
|
26
|
+
* 1. Creates caller keys if they don't exist yet
|
|
27
|
+
* 2. Generates a confirm code
|
|
28
|
+
* 3. Returns the confirm code + a `complete()` function
|
|
29
|
+
* 4. `complete()` encrypts and sends POST /sync, decrypts response, saves remote keys
|
|
30
|
+
*/
|
|
31
|
+
export function startKeyExchange(opts) {
|
|
32
|
+
const { remoteUrl, inviteCode, encryptionKey, callerAlias, configDir } = opts;
|
|
33
|
+
const keyOpts = configDir ? { configDir } : undefined;
|
|
34
|
+
// Create caller keys if they don't exist
|
|
35
|
+
let publicKeys;
|
|
36
|
+
if (callerExists(callerAlias, keyOpts)) {
|
|
37
|
+
publicKeys = exportCallerPublicKeys(callerAlias, keyOpts);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const result = createCaller(callerAlias, keyOpts);
|
|
41
|
+
publicKeys = result.publicKeys;
|
|
42
|
+
}
|
|
43
|
+
const confirmCode = generateSyncCode();
|
|
44
|
+
return {
|
|
45
|
+
confirmCode,
|
|
46
|
+
publicKeys,
|
|
47
|
+
async complete() {
|
|
48
|
+
const request = {
|
|
49
|
+
inviteCode,
|
|
50
|
+
confirmCode,
|
|
51
|
+
callerAlias,
|
|
52
|
+
publicKeys,
|
|
53
|
+
};
|
|
54
|
+
const encryptedBody = encryptSyncPayload(request, encryptionKey);
|
|
55
|
+
const url = remoteUrl.replace(/\/+$/, '') + '/sync';
|
|
56
|
+
const response = await fetch(url, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
59
|
+
body: encryptedBody,
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
let errorCode = 'NO_ACTIVE_SESSION';
|
|
63
|
+
try {
|
|
64
|
+
const errorBody = (await response.json());
|
|
65
|
+
if (errorBody.error)
|
|
66
|
+
errorCode = errorBody.error;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Could not parse error body
|
|
70
|
+
}
|
|
71
|
+
throw new SyncClientError(`Sync failed (HTTP ${response.status}): ${errorCode}`, errorCode, response.status);
|
|
72
|
+
}
|
|
73
|
+
const encryptedResponse = await response.text();
|
|
74
|
+
let decrypted;
|
|
75
|
+
try {
|
|
76
|
+
decrypted = decryptSyncPayload(encryptedResponse, encryptionKey);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new SyncClientError('Failed to decrypt sync response — encryption key mismatch?', 'DECRYPTION_FAILED');
|
|
80
|
+
}
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- validating unknown payload
|
|
82
|
+
const syncResponse = decrypted;
|
|
83
|
+
if (!syncResponse.remotePublicKeys?.signing ||
|
|
84
|
+
!syncResponse.remotePublicKeys.exchange ||
|
|
85
|
+
!syncResponse.callerAlias) {
|
|
86
|
+
throw new SyncClientError('Invalid sync response payload', 'INVALID_PAYLOAD');
|
|
87
|
+
}
|
|
88
|
+
// Save the remote server's public keys locally
|
|
89
|
+
const validResponse = syncResponse;
|
|
90
|
+
saveServerPublicKeys(validResponse.remotePublicKeys, keyOpts);
|
|
91
|
+
return {
|
|
92
|
+
remotePublicKeys: validResponse.remotePublicKeys,
|
|
93
|
+
callerAlias: validResponse.callerAlias,
|
|
94
|
+
fingerprint: validResponse.fingerprint,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=sync-client.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync protocol — types, code generation, and encryption helpers.
|
|
3
|
+
*
|
|
4
|
+
* Used by both the drawlatch remote server and the callboard sync client
|
|
5
|
+
* to coordinate a double-code key exchange with AES-256-GCM encryption.
|
|
6
|
+
*/
|
|
7
|
+
import type { SerializedPublicKeys } from '../crypto/keys.js';
|
|
8
|
+
/**
|
|
9
|
+
* Generate a human-readable sync code: WORD-NNNN (e.g., "WOLF-3847").
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateSyncCode(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Generate a one-time AES-256-GCM key for encrypting the sync exchange.
|
|
14
|
+
* Returns a base64-encoded 32-byte key.
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateSyncEncryptionKey(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Encrypt a JSON-serializable payload with AES-256-GCM.
|
|
19
|
+
*
|
|
20
|
+
* Wire format: base64(IV[12] || authTag[16] || ciphertext)
|
|
21
|
+
*/
|
|
22
|
+
export declare function encryptSyncPayload(payload: object, encryptionKey: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Decrypt an AES-256-GCM encrypted payload back to a parsed object.
|
|
25
|
+
*
|
|
26
|
+
* Throws on decryption failure (wrong key, tampered data, etc.).
|
|
27
|
+
*/
|
|
28
|
+
export declare function decryptSyncPayload(encrypted: string, encryptionKey: string): unknown;
|
|
29
|
+
/** Sent by callboard to drawlatch (encrypted with sync encryption key). */
|
|
30
|
+
export interface SyncRequest {
|
|
31
|
+
inviteCode: string;
|
|
32
|
+
confirmCode: string;
|
|
33
|
+
callerAlias: string;
|
|
34
|
+
publicKeys: SerializedPublicKeys;
|
|
35
|
+
}
|
|
36
|
+
/** Returned by drawlatch to callboard (encrypted with sync encryption key). */
|
|
37
|
+
export interface SyncResponse {
|
|
38
|
+
remotePublicKeys: SerializedPublicKeys;
|
|
39
|
+
callerAlias: string;
|
|
40
|
+
fingerprint: string;
|
|
41
|
+
}
|
|
42
|
+
/** Maximum number of failed sync code attempts before session invalidation. */
|
|
43
|
+
export declare const MAX_SYNC_ATTEMPTS = 5;
|
|
44
|
+
/** Server-side sync session state. */
|
|
45
|
+
export interface SyncSession {
|
|
46
|
+
inviteCode: string;
|
|
47
|
+
confirmCode: string | null;
|
|
48
|
+
encryptionKey: string;
|
|
49
|
+
createdAt: number;
|
|
50
|
+
ttlMs: number;
|
|
51
|
+
completed: boolean;
|
|
52
|
+
/** Number of failed code-match attempts (session invalidated at MAX_SYNC_ATTEMPTS). */
|
|
53
|
+
failedAttempts: number;
|
|
54
|
+
/** Populated after successful sync. */
|
|
55
|
+
result?: {
|
|
56
|
+
callerAlias: string;
|
|
57
|
+
fingerprint: string;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export type SyncErrorCode = 'NO_ACTIVE_SESSION' | 'CODE_MISMATCH' | 'SESSION_EXPIRED' | 'ALREADY_COMPLETED' | 'DECRYPTION_FAILED' | 'INVALID_PAYLOAD';
|
|
61
|
+
export declare const DEFAULT_SYNC_TTL_MS: number;
|
|
62
|
+
/**
|
|
63
|
+
* Check whether a sync session is still valid (not expired and not completed).
|
|
64
|
+
*/
|
|
65
|
+
export declare function isSyncSessionActive(session: SyncSession): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Validate a SyncRequest shape (after decryption).
|
|
68
|
+
* Returns null if valid, or an error message string.
|
|
69
|
+
*/
|
|
70
|
+
export declare function validateSyncRequest(data: unknown): string | null;
|
|
71
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync protocol — types, code generation, and encryption helpers.
|
|
3
|
+
*
|
|
4
|
+
* Used by both the drawlatch remote server and the callboard sync client
|
|
5
|
+
* to coordinate a double-code key exchange with AES-256-GCM encryption.
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
// ── Sync codes ───────────────────────────────────────────────────────────────
|
|
9
|
+
const SYNC_WORDS = [
|
|
10
|
+
'ALPHA',
|
|
11
|
+
'ARROW',
|
|
12
|
+
'ATLAS',
|
|
13
|
+
'BIRCH',
|
|
14
|
+
'BLADE',
|
|
15
|
+
'BLAZE',
|
|
16
|
+
'BLOOM',
|
|
17
|
+
'BRAVE',
|
|
18
|
+
'BRISK',
|
|
19
|
+
'CEDAR',
|
|
20
|
+
'CHALK',
|
|
21
|
+
'CLIFF',
|
|
22
|
+
'CORAL',
|
|
23
|
+
'CRANE',
|
|
24
|
+
'CREST',
|
|
25
|
+
'CROWN',
|
|
26
|
+
'DELTA',
|
|
27
|
+
'DRIFT',
|
|
28
|
+
'EAGLE',
|
|
29
|
+
'EMBER',
|
|
30
|
+
'FABLE',
|
|
31
|
+
'FLAME',
|
|
32
|
+
'FLINT',
|
|
33
|
+
'FORGE',
|
|
34
|
+
'FROST',
|
|
35
|
+
'GLEAM',
|
|
36
|
+
'GLOBE',
|
|
37
|
+
'GRAIN',
|
|
38
|
+
'GROVE',
|
|
39
|
+
'HAVEN',
|
|
40
|
+
'HAZEL',
|
|
41
|
+
'HERON',
|
|
42
|
+
'IVORY',
|
|
43
|
+
'JEWEL',
|
|
44
|
+
'LANCE',
|
|
45
|
+
'LARCH',
|
|
46
|
+
'LIGHT',
|
|
47
|
+
'LINEN',
|
|
48
|
+
'MAPLE',
|
|
49
|
+
'MARSH',
|
|
50
|
+
'NORTH',
|
|
51
|
+
'OCEAN',
|
|
52
|
+
'OLIVE',
|
|
53
|
+
'ONYX',
|
|
54
|
+
'ORBIT',
|
|
55
|
+
'PEARL',
|
|
56
|
+
'PETAL',
|
|
57
|
+
'PIXEL',
|
|
58
|
+
'PLUME',
|
|
59
|
+
'PRISM',
|
|
60
|
+
'QUAIL',
|
|
61
|
+
'QUEST',
|
|
62
|
+
'RAVEN',
|
|
63
|
+
'RIDGE',
|
|
64
|
+
'RIVER',
|
|
65
|
+
'ROWAN',
|
|
66
|
+
'SABLE',
|
|
67
|
+
'SHADE',
|
|
68
|
+
'SLATE',
|
|
69
|
+
'SOLAR',
|
|
70
|
+
'SPARK',
|
|
71
|
+
'SPIRE',
|
|
72
|
+
'STEEL',
|
|
73
|
+
'STONE',
|
|
74
|
+
'STORM',
|
|
75
|
+
'SWIFT',
|
|
76
|
+
'THORN',
|
|
77
|
+
'TIGER',
|
|
78
|
+
'TORCH',
|
|
79
|
+
'TRAIL',
|
|
80
|
+
'VAPOR',
|
|
81
|
+
'VIVID',
|
|
82
|
+
'WHALE',
|
|
83
|
+
'WREN',
|
|
84
|
+
'WOLF',
|
|
85
|
+
'ZENITH',
|
|
86
|
+
];
|
|
87
|
+
/**
|
|
88
|
+
* Generate a human-readable sync code: WORD-NNNN (e.g., "WOLF-3847").
|
|
89
|
+
*/
|
|
90
|
+
export function generateSyncCode() {
|
|
91
|
+
const word = SYNC_WORDS[crypto.randomInt(SYNC_WORDS.length)];
|
|
92
|
+
const num = crypto.randomInt(1000, 10000); // 4-digit number
|
|
93
|
+
return `${word}-${num}`;
|
|
94
|
+
}
|
|
95
|
+
// ── Encryption key ───────────────────────────────────────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* Generate a one-time AES-256-GCM key for encrypting the sync exchange.
|
|
98
|
+
* Returns a base64-encoded 32-byte key.
|
|
99
|
+
*/
|
|
100
|
+
export function generateSyncEncryptionKey() {
|
|
101
|
+
return crypto.randomBytes(32).toString('base64');
|
|
102
|
+
}
|
|
103
|
+
// ── Payload encryption ───────────────────────────────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* Encrypt a JSON-serializable payload with AES-256-GCM.
|
|
106
|
+
*
|
|
107
|
+
* Wire format: base64(IV[12] || authTag[16] || ciphertext)
|
|
108
|
+
*/
|
|
109
|
+
export function encryptSyncPayload(payload, encryptionKey) {
|
|
110
|
+
const key = Buffer.from(encryptionKey, 'base64');
|
|
111
|
+
if (key.length !== 32)
|
|
112
|
+
throw new Error('Encryption key must be 32 bytes (base64-encoded)');
|
|
113
|
+
const iv = crypto.randomBytes(12);
|
|
114
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
115
|
+
const plaintext = JSON.stringify(payload);
|
|
116
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
|
|
117
|
+
const authTag = cipher.getAuthTag();
|
|
118
|
+
return Buffer.concat([iv, authTag, encrypted]).toString('base64');
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Decrypt an AES-256-GCM encrypted payload back to a parsed object.
|
|
122
|
+
*
|
|
123
|
+
* Throws on decryption failure (wrong key, tampered data, etc.).
|
|
124
|
+
*/
|
|
125
|
+
export function decryptSyncPayload(encrypted, encryptionKey) {
|
|
126
|
+
const key = Buffer.from(encryptionKey, 'base64');
|
|
127
|
+
if (key.length !== 32)
|
|
128
|
+
throw new Error('Encryption key must be 32 bytes (base64-encoded)');
|
|
129
|
+
const data = Buffer.from(encrypted, 'base64');
|
|
130
|
+
if (data.length < 28)
|
|
131
|
+
throw new Error('Encrypted payload too short');
|
|
132
|
+
const iv = data.subarray(0, 12);
|
|
133
|
+
const authTag = data.subarray(12, 28);
|
|
134
|
+
const ciphertext = data.subarray(28);
|
|
135
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
136
|
+
decipher.setAuthTag(authTag);
|
|
137
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
138
|
+
return JSON.parse(decrypted.toString('utf-8'));
|
|
139
|
+
}
|
|
140
|
+
/** Maximum number of failed sync code attempts before session invalidation. */
|
|
141
|
+
export const MAX_SYNC_ATTEMPTS = 5;
|
|
142
|
+
export const DEFAULT_SYNC_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
143
|
+
/**
|
|
144
|
+
* Check whether a sync session is still valid (not expired and not completed).
|
|
145
|
+
*/
|
|
146
|
+
export function isSyncSessionActive(session) {
|
|
147
|
+
if (session.completed)
|
|
148
|
+
return false;
|
|
149
|
+
if (Date.now() - session.createdAt > session.ttlMs)
|
|
150
|
+
return false;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Validate a SyncRequest shape (after decryption).
|
|
155
|
+
* Returns null if valid, or an error message string.
|
|
156
|
+
*/
|
|
157
|
+
export function validateSyncRequest(data) {
|
|
158
|
+
if (typeof data !== 'object' || data === null)
|
|
159
|
+
return 'payload must be an object';
|
|
160
|
+
const obj = data;
|
|
161
|
+
if (typeof obj.inviteCode !== 'string')
|
|
162
|
+
return 'missing or invalid inviteCode';
|
|
163
|
+
if (typeof obj.confirmCode !== 'string')
|
|
164
|
+
return 'missing or invalid confirmCode';
|
|
165
|
+
if (typeof obj.callerAlias !== 'string')
|
|
166
|
+
return 'missing or invalid callerAlias';
|
|
167
|
+
if (typeof obj.publicKeys !== 'object' || obj.publicKeys === null)
|
|
168
|
+
return 'missing publicKeys';
|
|
169
|
+
const keys = obj.publicKeys;
|
|
170
|
+
if (typeof keys.signing !== 'string')
|
|
171
|
+
return 'missing publicKeys.signing';
|
|
172
|
+
if (typeof keys.exchange !== 'string')
|
|
173
|
+
return 'missing publicKeys.exchange';
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.dl-tunnel-card{gap:10px}.dl-tunnel-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.dl-tunnel-status{display:flex;align-items:center;gap:8px;min-width:0}.dl-tunnel-url{display:inline-flex;align-items:center;gap:4px;color:var(--accent, #5b9dff);font-size:13px;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-tunnel-url:hover{text-decoration:underline}.dl-tunnel-hint{line-height:1.45}.dl-tunnel-error{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:6px;border:1px solid rgba(248,81,73,.35);background:#f8514914;color:#f85149;font-size:12px}.dl-tunnel-switch{position:relative;display:inline-flex;align-items:center;flex-shrink:0;width:40px;height:22px;padding:0;border-radius:999px;border:1px solid #2a2a30;background:#0d0d0f;cursor:pointer;transition:background .15s,border-color .15s}.dl-tunnel-switch:hover:not(:disabled){border-color:#3a3a42}.dl-tunnel-switch.is-on{background:#5b9dff;border-color:#5b9dff}.dl-tunnel-switch.is-on:hover:not(:disabled){background:#74acff;border-color:#74acff}.dl-tunnel-switch:disabled{cursor:not-allowed;opacity:.6}.dl-tunnel-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#c5c5cd;transition:left .15s,background .15s}.dl-tunnel-switch.is-on .dl-tunnel-switch-knob{left:20px;background:#0d0d0f}.dl-tunnel-switch-spin{position:absolute;top:4px;right:4px;color:#c5c5cd;animation:dl-tunnel-spin .9s linear infinite}@keyframes dl-tunnel-spin{to{transform:rotate(360deg)}}.status-dot.warn{background:var(--warning, #d29922)}.status-dot.stopped{background:var(--text-muted, #8b8b94)}.banner.banner-warn{background:#d299221a;border:1px solid rgba(210,153,34,.35);color:var(--text, #e6e6ea)}.dl-tunnel-restart-banner{grid-column:span 2;margin-bottom:0;font-size:12.5px;line-height:1.5}.dl-tunnel-restart-banner code{font-family:var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);font-size:12px;padding:1px 5px;background:#ffffff0d;border-radius:4px}.dl-secrets-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:24px;background:#0d0d0fb8;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.dl-secrets-panel{width:100%;max-width:540px;max-height:84vh;display:flex;flex-direction:column;overflow:hidden;background:#16161a;border:1px solid #2a2a30;border-radius:12px;box-shadow:0 24px 64px #0000008c;color:#e6e6ea}.dl-secrets-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:20px 22px 16px;border-bottom:1px solid #2a2a30;flex-shrink:0}.dl-secrets-title{margin:0;font-size:17px;font-weight:600;line-height:1.3;color:#e6e6ea}.dl-secrets-alias{margin-top:4px;font-size:12px;color:#8b8b94}.dl-secrets-alias .dl-secrets-pill{color:#c5c5cd}.dl-secrets-pill{display:inline-block;padding:1px 7px;border-radius:999px;border:1px solid #2a2a30;background:#0d0d0f;font-size:11px;color:#c5c5cd}.dl-secrets-desc{margin:8px 0 0;font-size:12.5px;line-height:1.5;color:#8b8b94}.dl-secrets-docs{display:inline-flex;align-items:center;gap:4px;margin-top:8px;font-size:12px;color:#5b9dff;text-decoration:none}.dl-secrets-docs:hover{text-decoration:underline}.dl-secrets-close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;border-radius:7px;background:transparent;color:#8b8b94;cursor:pointer}.dl-secrets-close:hover{background:#2a2a30;color:#e6e6ea}.dl-secrets-body{flex:1;overflow-y:auto;padding:18px 22px}.dl-secrets-section+.dl-secrets-section{margin-top:22px}.dl-secrets-section-title{margin:0 0 12px;font-size:11px;font-weight:600;letter-spacing:.6px;text-transform:uppercase;color:#8b8b94}.dl-secrets-optional-toggle{display:flex;align-items:center;gap:6px;width:100%;padding:0;border:none;background:transparent;color:#8b8b94;font-size:11px;font-weight:600;letter-spacing:.6px;text-transform:uppercase;cursor:pointer}.dl-secrets-optional-toggle:hover{color:#c5c5cd}.dl-secrets-optional-toggle .dl-secrets-count{color:#5f5f68}.dl-secrets-fields{display:flex;flex-direction:column;gap:14px}.dl-secrets-optional-toggle+.dl-secrets-fields{margin-top:12px}.dl-secrets-field-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}.dl-secrets-label{font-size:12.5px;font-family:var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);font-weight:500;color:#e6e6ea;word-break:break-all}.dl-secrets-field-meta{display:inline-flex;align-items:center;gap:6px;flex-shrink:0}.dl-secrets-set-badge{display:inline-flex;align-items:center;gap:3px;font-size:11px;font-weight:500;color:#3fb950}.dl-secrets-clear-btn{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border:1px solid #2a2a30;border-radius:5px;background:transparent;color:#8b8b94;font-size:11px;cursor:pointer}.dl-secrets-clear-btn:hover{border-color:#f85149;color:#f85149}.dl-secrets-undo-btn{display:inline-flex;align-items:center;gap:4px;padding:2px 7px;border:1px solid rgba(248,81,73,.4);border-radius:5px;background:#f8514914;color:#f85149;font-size:11px;cursor:pointer}.dl-secrets-undo-btn:hover{background:#f8514929}.dl-secrets-input-wrap{position:relative}.dl-secrets-input{width:100%;box-sizing:border-box;padding:9px 38px 9px 12px;border:1px solid #2a2a30;border-radius:8px;background:#0d0d0f;color:#e6e6ea;font-size:12.5px;font-family:var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);outline:none}.dl-secrets-input::placeholder{color:#5f5f68}.dl-secrets-input:focus{border-color:#5b9dff}.dl-secrets-input.is-clearing{border-color:#f8514966;background:#1a1213;opacity:.65}.dl-secrets-eye{position:absolute;top:50%;right:8px;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border:none;border-radius:6px;background:transparent;color:#8b8b94;cursor:pointer}.dl-secrets-eye:hover{color:#e6e6ea}.dl-secrets-empty{padding:28px 12px;text-align:center;font-size:13px;color:#8b8b94}.dl-secrets-empty-title{display:block;margin-bottom:4px;color:#c5c5cd;font-weight:500}.dl-secrets-state{display:flex;align-items:center;gap:8px;padding:28px 12px;justify-content:center;font-size:13px;color:#8b8b94}.dl-secrets-state.is-error{color:#f85149}.dl-secrets-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 22px;border-top:1px solid #2a2a30;flex-shrink:0}.dl-secrets-status{flex:1;min-width:0;font-size:12px}.dl-secrets-status.is-error{color:#f85149}.dl-secrets-status.is-success{display:inline-flex;align-items:center;gap:4px;color:#3fb950}.dl-secrets-actions{display:flex;gap:10px;flex-shrink:0}.dl-secrets-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid transparent;font-size:13px;font-weight:500;cursor:pointer}.dl-secrets-btn-secondary{background:#0d0d0f;border-color:#2a2a30;color:#c5c5cd}.dl-secrets-btn-secondary:hover{border-color:#3a3a42;color:#e6e6ea}.dl-secrets-btn-primary{background:#5b9dff;color:#0d0d0f}.dl-secrets-btn-primary:hover:not(:disabled){background:#74acff}.dl-secrets-btn-primary.is-saved{background:#3fb950}.dl-secrets-btn:disabled{cursor:not-allowed;opacity:.5}.dl-secrets-spin{animation:dl-secrets-spin .9s linear infinite}@keyframes dl-secrets-spin{to{transform:rotate(360deg)}}.dl-confirm-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1100;display:flex;align-items:center;justify-content:center;padding:24px;background:#0d0d0fb8;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.dl-confirm-panel{width:100%;max-width:440px;display:flex;flex-direction:column;background:#16161a;border:1px solid #2a2a30;border-radius:12px;box-shadow:0 24px 64px #0000008c;color:#e6e6ea}.dl-confirm-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:18px 20px 12px;border-bottom:1px solid #2a2a30}.dl-confirm-header-text{display:flex;align-items:center;gap:10px;min-width:0}.dl-confirm-icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:30px;height:30px;border-radius:8px;background:#f851491f;color:#f85149}.dl-confirm-title{margin:0;font-size:15px;font-weight:600;line-height:1.35;color:#e6e6ea;word-break:break-word}.dl-confirm-close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:6px;background:transparent;color:#8b8b94;cursor:pointer}.dl-confirm-close:hover:not(:disabled){background:#2a2a30;color:#e6e6ea}.dl-confirm-close:disabled{cursor:not-allowed;opacity:.5}.dl-confirm-body{padding:16px 20px;font-size:13px;line-height:1.55;color:#c5c5cd}.dl-confirm-body code{font-family:var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);font-size:12px;padding:1px 6px;border:1px solid #2a2a30;background:#0d0d0f;border-radius:5px;color:#e6e6ea}.dl-confirm-body strong{color:#e6e6ea;font-weight:600}.dl-confirm-error{margin:0 20px 12px;padding:8px 12px;border-radius:8px;border:1px solid rgba(248,81,73,.35);background:#f8514914;color:#f85149;font-size:12.5px;line-height:1.45}.dl-confirm-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:14px 20px;border-top:1px solid #2a2a30}.dl-confirm-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid transparent;font-size:13px;font-weight:500;cursor:pointer}.dl-confirm-btn:disabled{cursor:not-allowed;opacity:.55}.dl-confirm-btn-secondary{background:#0d0d0f;border-color:#2a2a30;color:#c5c5cd}.dl-confirm-btn-secondary:hover:not(:disabled){border-color:#3a3a42;color:#e6e6ea}.dl-confirm-btn-danger{background:#f85149;border-color:#f85149;color:#0d0d0f}.dl-confirm-btn-danger:hover:not(:disabled){background:#ff6259;border-color:#ff6259}.dl-confirm-spin{animation:dl-confirm-spin .9s linear infinite}@keyframes dl-confirm-spin{to{transform:rotate(360deg)}}.dl-lcp-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:24px;background:#0d0d0fb8;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.dl-lcp-panel{width:100%;max-width:580px;max-height:86vh;display:flex;flex-direction:column;overflow:hidden;background:#16161a;border:1px solid #2a2a30;border-radius:12px;box-shadow:0 24px 64px #0000008c;color:#e6e6ea}.dl-lcp-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:20px 22px 16px;border-bottom:1px solid #2a2a30;flex-shrink:0}.dl-lcp-header-text{min-width:0;flex:1}.dl-lcp-title{margin:0;display:flex;align-items:center;gap:8px;font-size:17px;font-weight:600;line-height:1.3;color:#e6e6ea}.dl-lcp-title-icon{color:#5b9dff;flex-shrink:0}.dl-lcp-subtitle{margin:6px 0 0;font-size:12.5px;line-height:1.5;color:#8b8b94}.dl-lcp-close{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:none;border-radius:7px;background:transparent;color:#8b8b94;cursor:pointer}.dl-lcp-close:hover{background:#2a2a30;color:#e6e6ea}.dl-lcp-body{flex:1;overflow-y:auto;padding:18px 22px}.dl-lcp-banner{margin-bottom:16px}.dl-lcp-section+.dl-lcp-section{margin-top:22px}.dl-lcp-section-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}.dl-lcp-section-title{margin:0 0 12px;font-size:11px;font-weight:600;letter-spacing:.6px;text-transform:uppercase;color:#8b8b94}.dl-lcp-section-head .dl-lcp-section-title{margin:0}.dl-lcp-state{display:flex;align-items:center;justify-content:center;gap:8px;padding:28px 12px;font-size:13px;color:#8b8b94}.dl-lcp-state-sm{padding:14px 0}.dl-lcp-empty{padding:18px 12px;text-align:center;font-size:12.5px;color:#8b8b94;background:#0d0d0f;border:1px solid #2a2a30;border-radius:8px}.dl-lcp-empty-hint{display:block;margin-top:4px;font-size:11px;opacity:.8}.dl-lcp-card{background:#0d0d0f;border:1px solid #2a2a30;border-radius:8px;padding:14px}.dl-lcp-status-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.dl-lcp-status-state{display:flex;align-items:center;gap:8px}.dl-lcp-state-label{font-size:14px;font-weight:500;text-transform:capitalize}.dl-lcp-type-tag,.dl-lcp-disabled-badge,.dl-lcp-field-type,.dl-lcp-instance-key{font-size:10.5px;padding:1px 6px;border-radius:4px;background:#1f1f25;color:#8b8b94}.dl-lcp-instance-key{background:#5b9dff24;color:#5b9dff}.dl-lcp-dot{width:8px;height:8px}.dl-lcp-dot-connected{background:#3fb950;box-shadow:0 0 6px #3fb950}.dl-lcp-dot-error{background:#f85149}.dl-lcp-dot-starting{background:#d29922}.dl-lcp-dot-stopped{background:#8b8b94}.dl-lcp-stats{display:flex;flex-wrap:wrap;gap:16px;margin-bottom:12px;font-size:12px;color:#8b8b94}.dl-lcp-error-box{display:flex;align-items:flex-start;gap:6px;margin-bottom:12px;padding:6px 10px;border-radius:6px;font-size:12px;color:#f85149;background:#f8514914}.dl-lcp-controls{display:flex;gap:6px}.dl-lcp-ctrl{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:7px 0;border-radius:6px;border:1px solid #2a2a30;background:#16161a;color:#e6e6ea;font-size:12px;font-weight:500;cursor:pointer}.dl-lcp-ctrl:hover:not(:disabled){border-color:#3a3a42}.dl-lcp-ctrl:disabled{cursor:not-allowed;opacity:.5}.dl-lcp-ctrl-start{color:#3fb950;background:#3fb95014}.dl-lcp-ctrl-stop{color:#f85149;background:#f8514914}.dl-lcp-ctrl-sm{flex:0 0 auto;padding:4px 10px;font-size:11px}.dl-lcp-inline-result{display:flex;align-items:center;gap:6px;margin-top:8px;padding:6px 10px;border-radius:6px;font-size:12px}.dl-lcp-inline-result.is-ok{color:#3fb950;background:#3fb9501a}.dl-lcp-inline-result.is-err{color:#f85149;background:#f851491a}.dl-lcp-bulk{display:flex;gap:4px}.dl-lcp-bulk-btn{display:inline-flex;align-items:center;gap:3px;padding:3px 8px;border-radius:5px;border:1px solid #2a2a30;background:#16161a;color:#8b8b94;font-size:11px;font-weight:500;cursor:pointer}.dl-lcp-bulk-btn:disabled{cursor:not-allowed;opacity:.5}.dl-lcp-instance-list{display:flex;flex-direction:column;gap:8px}.dl-lcp-instance{background:#0d0d0f;border:1px solid #2a2a30;border-radius:8px;padding:10px 14px}.dl-lcp-instance.is-disabled{opacity:.65}.dl-lcp-instance-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.dl-lcp-instance-id-wrap{display:flex;align-items:center;gap:8px;min-width:0;flex:1}.dl-lcp-instance-id{font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-lcp-instance-actions{display:flex;gap:4px;flex-shrink:0}.dl-lcp-icon-btn{display:inline-flex;align-items:center;justify-content:center;padding:4px;border:none;border-radius:4px;background:transparent;color:#8b8b94;cursor:pointer}.dl-lcp-icon-btn:hover{color:#e6e6ea;background:#1f1f25}.dl-lcp-icon-btn.is-active{color:#5b9dff}.dl-lcp-icon-btn-danger:hover{color:#f85149}.dl-lcp-icon-btn:disabled{cursor:wait;opacity:.6}.dl-lcp-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}.dl-lcp-chip{padding:2px 6px;border-radius:4px;background:#1f1f25;color:#8b8b94;font-size:11px}.dl-lcp-instance-ctrls{display:flex;gap:4px}.dl-lcp-create{margin-top:10px}.dl-lcp-create-form{background:#0d0d0f;border:1px solid #2a2a30;border-radius:8px;padding:10px 14px}.dl-lcp-create-label{display:block;margin-bottom:6px;font-size:12px;font-weight:500;color:#8b8b94}.dl-lcp-create-row{display:flex;align-items:center;gap:6px}.dl-lcp-create-row .dl-lcp-input{flex:1;min-width:0}.dl-lcp-add-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:8px 12px;border-radius:8px;border:1px dashed #2a2a30;background:transparent;color:#5b9dff;font-size:12px;font-weight:500;cursor:pointer}.dl-lcp-add-btn:hover:not(:disabled){background:#5b9dff0f}.dl-lcp-add-btn:disabled{cursor:not-allowed;opacity:.5}.dl-lcp-editing-banner{display:flex;align-items:center;gap:6px;margin-bottom:12px;padding:6px 10px;border-radius:6px;font-size:12px;color:#5b9dff;background:#5b9dff1a}.dl-lcp-editing-cancel{margin-left:auto;padding:2px 6px;border:none;border-radius:4px;background:transparent;color:#8b8b94;font-size:11px;cursor:pointer}.dl-lcp-editing-cancel:hover{color:#e6e6ea}.dl-lcp-group+.dl-lcp-group{margin-top:12px}.dl-lcp-group-toggle{display:flex;align-items:center;gap:6px;margin-bottom:8px;padding:4px 0;border:none;background:transparent;color:#e6e6ea;font-size:13px;font-weight:500;cursor:pointer}.dl-lcp-fields{display:flex;flex-direction:column;gap:12px}.dl-lcp-field{background:#0d0d0f;border:1px solid #2a2a30;border-radius:8px;padding:12px 14px}.dl-lcp-field-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.dl-lcp-field-labels{display:flex;align-items:center;gap:6px;min-width:0}.dl-lcp-field-label{font-size:13px;font-weight:500;color:#e6e6ea;word-break:break-all}.dl-lcp-required{font-size:10px;font-weight:600;color:#f85149}.dl-lcp-field-desc{display:flex;align-items:flex-start;gap:4px;margin:0 0 8px;font-size:12px;line-height:1.5;color:#8b8b94}.dl-lcp-field-desc-icon{flex-shrink:0;margin-top:2px;opacity:.6}.dl-lcp-field-control{margin-top:4px}.dl-lcp-field-hints{display:flex;flex-wrap:wrap;gap:10px;margin-top:6px;font-size:11px;color:#8b8b94}.dl-lcp-input{width:100%;box-sizing:border-box;padding:7px 10px;border:1px solid #2a2a30;border-radius:6px;background:#16161a;color:#e6e6ea;font-size:12px;outline:none}.dl-lcp-input::placeholder{color:#5f5f68}.dl-lcp-input:focus{border-color:#5b9dff}.dl-lcp-input.is-err{border-color:#f85149}select.dl-lcp-input{cursor:pointer}.dl-lcp-toggle{position:relative;width:40px;height:22px;border:none;border-radius:11px;background:#2a2a30;cursor:pointer;transition:background .2s}.dl-lcp-toggle.is-on{background:#5b9dff}.dl-lcp-toggle-knob{position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#e6e6ea;transition:left .2s}.dl-lcp-toggle.is-on .dl-lcp-toggle-knob{left:21px}.dl-lcp-checklist{display:flex;flex-direction:column;gap:4px}.dl-lcp-check{display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer}.dl-lcp-check input{accent-color:#5b9dff}.dl-lcp-textlist{display:flex;flex-direction:column;gap:6px}.dl-lcp-textlist-row{display:flex;align-items:center;gap:6px}.dl-lcp-textlist-row .dl-lcp-input{flex:1}.dl-lcp-link{display:inline-flex;align-items:center;gap:4px;padding:0;border:none;background:transparent;color:#5b9dff;font-size:12px;cursor:pointer}.dl-lcp-link:hover{text-decoration:underline}.dl-lcp-hint{margin-top:6px;font-size:11px;color:#5f5f68}.dl-lcp-field-error{display:flex;align-items:center;gap:4px;margin-top:6px;font-size:11px;color:#f85149}.dl-lcp-meta{display:flex;flex-wrap:wrap;gap:12px;margin-top:16px;padding:10px 12px;border-radius:6px;background:#1f1f25;font-size:12px;color:#8b8b94}.dl-lcp-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 22px;border-top:1px solid #2a2a30;flex-shrink:0}.dl-lcp-footer-status{flex:1;min-width:0;display:flex;align-items:center;gap:12px;font-size:12px}.dl-lcp-dirty{display:inline-flex;align-items:center;gap:4px;color:#d29922}.dl-lcp-save-ok{display:inline-flex;align-items:center;gap:4px;color:#3fb950}.dl-lcp-save-err{display:inline-flex;align-items:center;gap:4px;color:#f85149}.dl-lcp-footer-actions{display:flex;gap:10px;flex-shrink:0}.dl-lcp-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid transparent;font-size:13px;font-weight:500;cursor:pointer}.dl-lcp-btn:disabled{cursor:not-allowed;opacity:.5}.dl-lcp-btn-secondary{background:#0d0d0f;border-color:#2a2a30;color:#c5c5cd}.dl-lcp-btn-secondary:hover:not(:disabled){border-color:#3a3a42;color:#e6e6ea}.dl-lcp-btn-primary{background:#5b9dff;color:#0d0d0f}.dl-lcp-btn-primary:hover:not(:disabled){background:#74acff}.dl-lcp-spin{animation:dl-lcp-spin .9s linear infinite}@keyframes dl-lcp-spin{to{transform:rotate(360deg)}}.dl-conn-controls{display:flex;gap:10px;margin-bottom:20px;flex-wrap:wrap}.dl-conn-muted-icon{color:#8b8b94;flex-shrink:0}.dl-conn-caller{position:relative;flex-shrink:0}.dl-conn-caller-btn{display:flex;align-items:center;gap:6px;padding:9px 12px;min-width:150px;border-radius:8px;border:1px solid #2a2a30;background:#16161a;color:#e6e6ea;font-size:14px;cursor:pointer}.dl-conn-caller-btn:hover{border-color:#3a3a42}.dl-conn-caller-name{flex:1;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dl-conn-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:90}.dl-conn-caller-menu{position:absolute;top:calc(100% + 4px);left:0;min-width:240px;z-index:100;background:#16161a;border:1px solid #2a2a30;border-radius:8px;box-shadow:0 8px 24px #0006;overflow:hidden;padding:4px}.dl-conn-caller-item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border-radius:6px;cursor:pointer}.dl-conn-caller-item:hover,.dl-conn-caller-item-active{background:#1b1b20}.dl-conn-caller-item-main{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.dl-conn-check{color:#5b9dff;flex-shrink:0}.dl-conn-caller-item-text{min-width:0}.dl-conn-caller-item-alias{font-size:13px;color:#e6e6ea;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-conn-caller-item-active .dl-conn-caller-item-alias{font-weight:600}.dl-conn-caller-item-count{font-size:11px;color:#8b8b94}.dl-conn-caller-divider{height:1px;background:#2a2a30;margin:4px 0}.dl-conn-caller-manage{display:flex;align-items:center;justify-content:space-between;gap:6px;width:100%;box-sizing:border-box;padding:8px 10px;border-radius:6px;background:transparent;color:#5b9dff;font-size:12.5px;text-decoration:none}.dl-conn-caller-manage:hover{background:#1b1b20}.dl-conn-search{position:relative;flex:1;min-width:180px}.dl-conn-search-icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#8b8b94;pointer-events:none}.dl-conn-search input{width:100%;box-sizing:border-box;padding:9px 12px 9px 36px;border-radius:8px;border:1px solid #2a2a30;background:#16161a;color:#e6e6ea;font-size:14px;outline:none}.dl-conn-search input:focus{border-color:#5b9dff}.dl-conn-stability{display:flex;flex-shrink:0;border:1px solid #2a2a30;border-radius:8px;overflow:hidden}.dl-conn-stability-btn{padding:8px 12px;border:none;border-right:1px solid #2a2a30;background:#16161a;color:#8b8b94;font-size:12px;font-weight:500;cursor:pointer;white-space:nowrap}.dl-conn-stability-btn:last-child{border-right:none}.dl-conn-stability-btn:hover{color:#e6e6ea}.dl-conn-stability-active,.dl-conn-stability-active:hover{background:#5b9dff;color:#0f0f12}.dl-conn-link-btn{border:none;background:none;padding:0;color:#5b9dff;font-size:inherit;cursor:pointer;text-decoration:underline}.dl-conn-group{margin-bottom:24px}.dl-conn-group-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#8b8b94;margin:0 0 10px}.dl-conn-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:12px}.dl-conn-card{background:#1b1b20;border:1px solid #2a2a30;border-radius:10px;padding:16px 18px;display:flex;flex-direction:column;gap:12px;min-width:0;transition:border-color .15s}.dl-conn-card:hover{border-color:#3a3a42}.dl-conn-card-off{opacity:.72}.dl-conn-card-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.dl-conn-card-id{display:flex;align-items:center;gap:10px;flex:1;min-width:0}.dl-conn-icon{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;background:#16161a;color:#8b8b94}.dl-conn-icon-on{background:#5b9dff1f;color:#5b9dff}.dl-conn-card-text{min-width:0;flex:1}.dl-conn-card-name{font-size:14px;font-weight:600;color:#e6e6ea;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-conn-card-desc{font-size:11px;color:#8b8b94;margin:1px 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-conn-toggle{position:relative;width:40px;height:22px;border-radius:11px;border:none;background:#2a2a30;cursor:pointer;flex-shrink:0;transition:background .2s}.dl-conn-toggle:disabled{cursor:wait}.dl-conn-toggle-on{background:#5b9dff}.dl-conn-toggle-knob{position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#e6e6ea;transition:left .2s;box-shadow:0 1px 2px #0006}.dl-conn-toggle-on .dl-conn-toggle-knob{left:21px}.dl-conn-badges{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.dl-conn-badge{display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:3px 7px;border-radius:6px;background:#16161a;color:#8b8b94}.dl-conn-badge-ingestor{background:#5b9dff1a;color:#5b9dff;border:1px solid rgba(91,157,255,.2)}.dl-conn-dot{width:6px;height:6px}.dl-conn-dot-connected{background:#3fb950;box-shadow:0 0 4px #3fb950}.dl-conn-dot-error{background:#f85149}.dl-conn-dot-warn{background:#d29922}.dl-conn-dot-stopped{background:#8b8b94}.dl-conn-dot-count{font-size:9px;color:#8b8b94}.dl-conn-secret-none{background:#16161a;color:#8b8b94}.dl-conn-secret-ready{background:#3fb9501f;color:#3fb950}.dl-conn-secret-partial{background:#d299221f;color:#d29922}.dl-conn-secret-missing{background:#16161a;color:#8b8b94}.dl-conn-docs{display:inline-flex;align-items:center;gap:3px;font-size:11px;color:#8b8b94;text-decoration:none;margin-left:auto}.dl-conn-docs:hover{color:#5b9dff}.dl-conn-actions{display:flex;gap:6px}.dl-conn-btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:8px 12px;border-radius:8px;border:1px solid #2a2a30;background:#16161a;color:#8b8b94;font-size:12px;font-weight:500;cursor:pointer;white-space:nowrap;transition:background .15s}.dl-conn-btn:hover:not(:disabled){background:#22222a;color:#e6e6ea}.dl-conn-btn:disabled{cursor:wait;opacity:.7}.dl-conn-btn-primary{flex:1;color:#e6e6ea}.dl-conn-listener-row{display:flex;align-items:center;gap:6px}.dl-conn-mini{display:inline-flex;align-items:center;gap:4px;padding:5px 10px;border-radius:6px;border:1px solid #2a2a30;background:#16161a;color:#8b8b94;font-size:11px;font-weight:500;cursor:pointer;transition:background .15s}.dl-conn-mini:hover:not(:disabled){background:#22222a}.dl-conn-mini:disabled{cursor:wait;opacity:.7}.dl-conn-mini-start{color:#3fb950}.dl-conn-mini-stop{color:#f85149}.dl-conn-mini-accent{color:#5b9dff;border-color:#5b9dff4d;background:#5b9dff0f}.dl-conn-mini-accent:hover:not(:disabled){background:#5b9dff24}.dl-conn-spacer{flex:1}.dl-conn-result{display:flex;align-items:flex-start;gap:6px;padding:8px 10px;border-radius:6px;font-size:12px}.dl-conn-result-ok{background:#3fb9501a;color:#3fb950}.dl-conn-result-err{background:#f851491a;color:#f85149}.dl-conn-result-icon{flex-shrink:0;margin-top:1px}.dl-conn-result-text{min-width:0;flex:1}.dl-conn-result-label{font-weight:600}.dl-conn-result-detail{font-size:11px;opacity:.85;margin-top:2px;word-break:break-word}.dl-conn-result-close{flex-shrink:0;margin-left:auto;border:none;background:transparent;color:inherit;font-size:16px;line-height:1;cursor:pointer;opacity:.6;padding:2px}.dl-conn-result-close:hover{opacity:1}.dl-conn-spin{animation:dl-conn-spin 1s linear infinite}@keyframes dl-conn-spin{to{transform:rotate(360deg)}}.dl-events-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.dl-events-caller-select{display:flex;align-items:center;gap:6px}.dl-events-caller-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#8b8b94}.dl-events-caller-select select{background:#16161a;border:1px solid #2a2a30;border-radius:6px;color:#e6e6ea;font-size:13px;padding:6px 10px;cursor:pointer}.dl-events-caller-select select:focus{outline:none;border-color:#5b9dff}.dl-events-caller-select select:disabled{opacity:.5;cursor:not-allowed}.dl-events-refresh{display:inline-flex;align-items:center;gap:6px;background:#16161a;border:1px solid #2a2a30;border-radius:6px;color:#e6e6ea;font-size:13px;font-weight:500;padding:6px 12px;cursor:pointer;transition:border-color .12s,background .12s}.dl-events-refresh:hover:not(:disabled){border-color:#5b9dff}.dl-events-refresh:disabled{opacity:.5;cursor:not-allowed}.dl-events-spin{animation:dl-events-spin 1s linear infinite}@keyframes dl-events-spin{to{transform:rotate(360deg)}}.dl-events-ingestors{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px}.dl-events-ingestor-card{flex:1 1 220px;min-width:200px;background:#16161a;border:1px solid #2a2a30;border-radius:10px;padding:12px 14px;font-size:12px}.dl-events-ingestor-head{display:flex;align-items:center;gap:8px;margin-bottom:8px}.dl-events-ingestor-conn{font-weight:700;font-size:13px;color:#e6e6ea}.dl-events-ingestor-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px;color:#8b8b94;font-size:11px}.dl-events-ingestor-error{margin-top:6px;color:#f85149;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-events-muted{color:#8b8b94}.dl-events-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.dl-events-dot-connected{background:#3fb950}.dl-events-dot-error{background:#f85149}.dl-events-dot-warn{background:#d29922}.dl-events-dot-stopped{background:#8b8b94}.dl-events-state{padding:1px 6px;border-radius:4px;background:#1f1f25;font-weight:600}.dl-events-state-connected{color:#3fb950}.dl-events-state-error{color:#f85149}.dl-events-state-warn{color:#d29922}.dl-events-state-stopped{color:#8b8b94}.dl-events-pills{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px}.dl-events-pill{padding:5px 12px;border-radius:6px;font-size:12px;font-weight:500;background:#16161a;border:1px solid #2a2a30;color:#e6e6ea;cursor:pointer;transition:border-color .12s,background .12s}.dl-events-pill:hover{border-color:#5b9dff}.dl-events-pill-active{background:#5b9dff;border-color:#5b9dff;color:#0d0d10}.dl-events-pill-active:hover{border-color:#5b9dff}.dl-events-feed{background:#16161a;border:1px solid #2a2a30;border-radius:10px;overflow:hidden}.dl-events-row-wrap:not(:last-child){border-bottom:1px solid #2a2a30}.dl-events-row{display:flex;align-items:center;gap:10px;width:100%;padding:11px 14px;background:transparent;border:none;color:#e6e6ea;font:inherit;text-align:left;cursor:pointer;transition:background .1s}.dl-events-row:hover{background:#1c1c22}.dl-events-chevron{color:#8b8b94;flex-shrink:0}.dl-events-source{flex-shrink:0;font-size:11px;font-weight:600;padding:2px 6px;border-radius:4px;background:#5b9dff1f;color:#5b9dff}.dl-events-instance{flex-shrink:0;font-size:10px;padding:1px 5px;border-radius:4px;background:#1f1f25;color:#8b8b94}.dl-events-type{flex-shrink:0;font-size:13px;font-weight:500;color:#e6e6ea}.dl-events-preview{flex:1;min-width:0;font-size:12px;color:#8b8b94;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-events-time{flex-shrink:0;font-size:11px;color:#8b8b94}.dl-events-detail{padding:0 16px 14px 38px;font-size:12px}.dl-events-detail-meta{display:flex;flex-wrap:wrap;gap:16px;margin-bottom:8px;color:#e6e6ea}.dl-events-json{background:#0f0f12;border:1px solid #2a2a30;border-radius:6px;padding:12px;font-size:11px;margin:0;overflow:auto;max-height:320px;white-space:pre-wrap;word-break:break-word;color:#c8c8d0}.dl-events-empty-icon{color:#8b8b94;opacity:.5;margin-bottom:4px}@media (max-width: 640px){.dl-events-preview{display:none}.dl-events-detail{padding-left:16px}}.dl-callers-header-right{display:flex;align-items:center;gap:12px}.dl-callers-new-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:7px;border:1px solid #2a2a30;background:#16161a;color:#c5c5cd;font-size:12.5px;font-weight:500;cursor:pointer}.dl-callers-new-btn:hover{border-color:#5b9dff;color:#5b9dff}.dl-callers-newcaller{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:0 0 14px;padding:12px 14px;border-radius:10px;border:1px solid #2a2a30;background:#16161a}.dl-callers-newcaller-input{flex:1 1 160px;min-width:140px;box-sizing:border-box;padding:7px 9px;border-radius:6px;border:1px solid #2a2a30;background:#0f0f12;color:#e6e6ea;font-size:12.5px;outline:none}.dl-callers-newcaller-input:focus{border-color:#5b9dff}.dl-callers-newcaller-add{padding:7px 12px;border:none;border-radius:6px;background:#5b9dff;color:#0f0f12;font-size:12px;font-weight:600;cursor:pointer}.dl-callers-newcaller-add:hover:not(:disabled){background:#74acff}.dl-callers-newcaller-add:disabled{cursor:not-allowed;opacity:.6}.dl-callers-newcaller-cancel{padding:7px 10px;border:1px solid #2a2a30;border-radius:6px;background:transparent;color:#8b8b94;font-size:12px;cursor:pointer}.dl-callers-newcaller-cancel:hover:not(:disabled){border-color:#3a3a42;color:#c5c5cd}.dl-callers-newcaller-error{flex-basis:100%;font-size:11.5px;color:#f85149}.dl-callers-action-cell{width:36px;text-align:right}.dl-callers-del-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:none;border-radius:6px;background:transparent;color:#8b8b94;cursor:pointer}.dl-callers-del-btn:hover{background:#f851491f;color:#f85149}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}:root{--bg: #0d1117;--surface: #161b22;--border: #30363d;--text: #e6edf3;--text-muted: #8b949e;--accent: #7c6aef;--accent-hover: #9484f0;--user-bg: #1a2332;--assistant-bg: #161b22;--code-bg: #0d1117;--danger: #f85149;--error: #f85149;--success: #3fb950;--warning: #d29922;--bg-secondary: #1c2128;--text-secondary: #8b949e;--border-light: #21262d;--font-mono: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;--radius: 12px;--safe-bottom: env(safe-area-inset-bottom, 0px);--text-on-accent: #ffffff;--text-on-danger: #ffffff;--accent-bg: rgba(124, 106, 239, .1);--accent-light: rgba(124, 106, 239, .1);--danger-bg: rgba(248, 81, 73, .1);--danger-border: rgba(220, 53, 69, .4);--warning-bg: rgba(210, 153, 34, .15);--success-bg: rgba(63, 185, 80, .1);--overlay-bg: rgba(0, 0, 0, .5);--shadow-sm: 0 1px 3px rgba(0, 0, 0, .2);--shadow-md: 0 4px 12px rgba(0, 0, 0, .15);--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, .1), 0 10px 10px -5px rgba(0, 0, 0, .04);--status-up: #3fb950;--status-down: #f85149;--status-unknown: #8b949e;--sidebar-width: 240px}html,body,#root{height:100%}body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Helvetica Neue,sans-serif;font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{color:var(--accent);text-decoration:none}a:hover{color:var(--accent-hover);text-decoration:underline}code,pre{font-family:var(--font-mono);font-size:13px}button{font:inherit;color:inherit;background:none;border:none;cursor:pointer}.app-shell{display:flex;height:100%;background:var(--bg)}.sidebar{width:var(--sidebar-width);flex-shrink:0;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;padding:16px 0}.sidebar-brand{padding:4px 20px 20px;display:flex;align-items:center;gap:10px;font-weight:600;font-size:16px;letter-spacing:.2px}.sidebar-brand-mark{width:24px;height:24px;border-radius:6px;background:linear-gradient(135deg,var(--accent) 0%,var(--accent-hover) 100%)}.sidebar-status{margin:0 16px 16px;padding:10px 12px;border-radius:8px;background:var(--bg-secondary);border:1px solid var(--border);display:flex;align-items:center;gap:10px;font-size:12px;color:var(--text-muted)}.status-dot{width:8px;height:8px;border-radius:50%;background:var(--status-unknown);flex-shrink:0}.status-dot.up{background:var(--status-up);box-shadow:0 0 8px #3fb95099}.status-dot.down{background:var(--status-down)}.sidebar-nav{display:flex;flex-direction:column;gap:2px;padding:0 8px}.sidebar-nav-link{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:8px;color:var(--text-muted);font-size:13px;font-weight:500;text-decoration:none;transition:background .15s ease,color .15s ease}.sidebar-nav-link:hover{background:var(--bg-secondary);color:var(--text);text-decoration:none}.sidebar-nav-link.active{background:var(--accent-bg);color:var(--accent)}.main-content{flex:1;overflow-y:auto;padding:32px 40px}.page-header{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:24px}.page-title{font-size:24px;font-weight:600;color:var(--text)}.page-subtitle{color:var(--text-muted);font-size:13px}.card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 20px;display:flex;flex-direction:column;gap:6px}.card-label{color:var(--text-muted);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.6px}.card-value{color:var(--text);font-size:22px;font-weight:600;word-break:break-word}.card-value-mono{font-family:var(--font-mono);font-size:14px}.card-sub{color:var(--text-muted);font-size:12px}.card.span-2{grid-column:span 2}.banner{border-radius:var(--radius);padding:16px 20px;margin-bottom:24px;display:flex;align-items:center;gap:12px;font-size:13px}.banner-offline{background:var(--danger-bg);border:1px solid var(--danger-border);color:var(--text)}.banner-loading{background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-muted)}.ingestor-breakdown{display:flex;gap:16px;flex-wrap:wrap;margin-top:4px}.ingestor-stat{display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text)}.ingestor-stat .status-dot{width:6px;height:6px}.ingestor-stat-connected .status-dot{background:var(--status-up)}.ingestor-stat-error .status-dot{background:var(--status-down)}.ingestor-stat-starting .status-dot{background:var(--warning)}.ingestor-stat-stopped .status-dot{background:var(--text-muted)}.placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:80px 20px;color:var(--text-muted);text-align:center;gap:12px}.placeholder-title{font-size:18px;color:var(--text);font-weight:500}.data-table-wrap{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}.data-table{width:100%;border-collapse:collapse;font-size:13px}.data-table thead th{text-align:left;padding:12px 16px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.6px;color:var(--text-muted);background:var(--bg-secondary);border-bottom:1px solid var(--border)}.data-table th.num,.data-table td.num{text-align:right;font-variant-numeric:tabular-nums}.data-table tbody td{padding:12px 16px;border-bottom:1px solid var(--border-light);vertical-align:middle}.data-table tbody tr:last-child td{border-bottom:none}.data-table-row-clickable{cursor:pointer;transition:background .12s ease}.data-table-row-clickable:hover{background:var(--bg-secondary)}.data-table-row-clickable:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.cell-primary{color:var(--text);font-weight:500}.cell-secondary{color:var(--text-muted);font-size:12px;margin-top:2px}.cell-muted{color:var(--text-muted)}.mono{font-family:var(--font-mono)}.tag{display:inline-block;padding:2px 8px;border-radius:999px;background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-muted);font-size:11px;font-weight:500;text-transform:lowercase}.stability-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.4px}.stability-stable{background:var(--success-bg);color:var(--success)}.stability-beta{background:var(--warning-bg);color:var(--warning)}.stability-dev{background:var(--accent-bg);color:var(--accent)}.back-link{display:inline-block;font-size:12px;color:var(--text-muted);margin-bottom:12px}.back-link:hover{color:var(--text);text-decoration:none}.title-row{display:flex;align-items:center;gap:12px}.subtitle-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;color:var(--text-muted);font-size:13px}.dot-sep{color:var(--border)}.detail-section{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px 24px;margin-bottom:16px}.detail-section .section-title{font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.6px;color:var(--text-muted);margin-bottom:14px}.detail-description{color:var(--text);line-height:1.6}.detail-note{color:var(--text-muted);font-size:12px;line-height:1.6;margin-bottom:16px}.detail-note code{background:var(--code-bg);padding:1px 5px;border-radius:4px;border:1px solid var(--border)}.endpoint-list{list-style:none;display:flex;flex-direction:column;gap:6px}.endpoint-item{background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:8px 12px;font-size:12px;color:var(--text);word-break:break-all}.secret-list-block{margin-top:12px}.secret-list-block .card-label{margin-bottom:8px;display:block}.secret-list{list-style:none;display:flex;flex-wrap:wrap;gap:6px}.secret-pill{background:var(--code-bg);border:1px solid var(--border);border-radius:4px;padding:4px 8px;font-size:12px;color:var(--text)}.kv-grid{display:grid;grid-template-columns:max-content 1fr;gap:8px 24px;font-size:13px}.kv-grid dt{color:var(--text-muted)}.kv-grid dd{color:var(--text)}.keys-dir-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.4px}.keys-dir-ok{background:var(--success-bg);color:var(--success)}.keys-dir-missing{background:var(--danger-bg);color:var(--danger)}.enabled-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.4px}.enabled-yes{background:var(--success-bg);color:var(--success)}.enabled-no{background:var(--bg-secondary);color:var(--text-muted);border:1px solid var(--border)}.tag-custom{background:var(--accent-bg);color:var(--accent);border-color:transparent;text-transform:uppercase;letter-spacing:.4px;font-weight:600}.badge-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.secret-ref-list{list-style:none;display:flex;flex-wrap:wrap;gap:6px}.secret-ref-pill{display:inline-flex;align-items:center;gap:6px;background:var(--code-bg);border:1px solid var(--border);border-radius:4px;padding:4px 8px;font-size:12px;color:var(--text)}.secret-ref-mark{font-weight:700;font-family:var(--font-mono)}.secret-ref-present .secret-ref-mark{color:var(--success)}.secret-ref-missing .secret-ref-mark{color:var(--danger)}.secret-ref-missing{border-color:var(--danger-border)}.ingestor-state{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.4px}.ingestor-state-connected{background:var(--success-bg);color:var(--success)}.ingestor-state-starting,.ingestor-state-reconnecting{background:var(--warning-bg);color:var(--warning)}.ingestor-state-stopped{background:var(--bg-secondary);color:var(--text-muted);border:1px solid var(--border)}.ingestor-state-error{background:var(--danger-bg);color:var(--danger)}.ingestor-table .ingestor-group-header>th{background:var(--bg-secondary);border-top:1px solid var(--border);border-bottom:1px solid var(--border-light);padding:8px 16px;text-transform:none;letter-spacing:0;font-weight:500;font-size:12px;color:var(--text);display:flex;align-items:center;gap:10px}.ingestor-table tbody tr:first-child .ingestor-group-header>th,.ingestor-table tbody .ingestor-group-header:first-child>th{border-top:none}.ingestor-group-label{color:var(--text-muted);font-size:10px;text-transform:uppercase;letter-spacing:.6px;font-weight:600}.ingestor-group-count{color:var(--text-muted);font-size:11px;margin-left:auto}.ingestor-error{color:var(--danger);font-family:var(--font-mono);font-size:12px;word-break:break-word}.secret-matrix-controls{display:flex;align-items:center;margin-bottom:16px}.secret-matrix-toggle{display:inline-flex;align-items:center;gap:8px;font-size:13px;color:var(--text-muted);cursor:pointer;-webkit-user-select:none;user-select:none}.secret-matrix-toggle input[type=checkbox]{cursor:pointer;accent-color:var(--accent)}.secret-matrix{display:flex;flex-direction:column;gap:20px}.secret-matrix-section{display:flex;flex-direction:column;gap:8px}.secret-matrix-section-header{display:flex;align-items:center;gap:10px;padding:0 4px}.secret-matrix-section-label{color:var(--text-muted);font-size:10px;text-transform:uppercase;letter-spacing:.6px;font-weight:600}.secret-matrix-section-meta{color:var(--text-muted);font-size:11px;margin-left:auto}.secret-matrix-table{table-layout:auto}.secret-matrix-table thead th.secret-matrix-col{text-align:center;text-transform:none;letter-spacing:normal;font-size:12px;color:var(--text);font-weight:500;padding:10px 12px;white-space:nowrap}.secret-matrix-table thead th.secret-matrix-col-optional .mono{color:var(--text-muted)}.secret-matrix-required-mark{color:var(--accent);font-weight:700;margin-left:4px}.secret-matrix-table tbody td.secret-matrix-cell{text-align:center;padding:10px 12px}.secret-matrix-mark{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;border-radius:4px;font-family:var(--font-mono);font-weight:700;font-size:14px;line-height:1}.secret-matrix-mark-required-present{background:var(--success-bg);color:var(--success)}.secret-matrix-mark-required-missing{background:var(--danger-bg);color:var(--danger);border:1px solid var(--danger-border)}.secret-matrix-mark-optional-present{color:var(--success)}.secret-matrix-mark-optional-missing{color:var(--text-muted)}.auth-splash{display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:13px}.auth-screen{display:flex;align-items:center;justify-content:center;min-height:100%;padding:32px 20px;background:var(--bg)}.auth-card{width:100%;max-width:360px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:28px 28px 24px;display:flex;flex-direction:column}.auth-brand{display:flex;align-items:center;gap:10px;font-size:14px;font-weight:600;color:var(--text-muted);margin-bottom:16px}.auth-title{font-size:20px;font-weight:600;color:var(--text);margin-bottom:20px}.auth-label{display:block;font-size:12px;font-weight:500;color:var(--text-muted);margin-bottom:6px}.auth-input{width:100%;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-size:14px;color:var(--text);font-family:inherit;margin-bottom:14px;transition:border-color .15s ease,box-shadow .15s ease}.auth-input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-bg)}.auth-input:disabled{opacity:.6;cursor:not-allowed}.auth-hint{color:var(--text-muted);font-size:12px;margin:-8px 0 14px}.auth-error{color:var(--danger);background:var(--danger-bg);border:1px solid var(--danger-border);border-radius:8px;padding:8px 12px;font-size:13px;line-height:1.4;margin-bottom:12px}.auth-success{color:var(--success);background:var(--success-bg);border:1px solid color-mix(in srgb,var(--success) 35%,transparent);border-radius:8px;padding:8px 12px;font-size:13px;line-height:1.4;margin-bottom:12px}.auth-banner{border-radius:8px;padding:10px 12px;font-size:13px;line-height:1.45;margin-bottom:14px}.auth-banner-error{color:var(--danger);background:var(--danger-bg);border:1px solid var(--danger-border)}.auth-submit{width:100%;min-height:40px;background:var(--accent);color:var(--text-on-accent);border-radius:8px;padding:10px 14px;font-size:14px;font-weight:500;cursor:pointer;transition:background .15s ease,opacity .15s ease}.auth-submit:hover:not(:disabled){background:var(--accent-hover)}.auth-submit:disabled{opacity:.55;cursor:not-allowed}.auth-form-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:24px;max-width:480px;display:flex;flex-direction:column}.auth-page-subtitle{margin-bottom:20px}.auth-actions{margin-top:4px}.sidebar-footer{margin-top:auto;padding:12px 8px 0;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:2px}.sidebar-logout{width:100%;text-align:left;font:inherit;cursor:pointer;background:none;border:none}.sidebar-logout:disabled{opacity:.55;cursor:not-allowed}.mobile-topbar{display:none;align-items:center;gap:12px;height:56px;padding:0 12px;background:var(--surface);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:20}.mobile-topbar-button{display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px;border-radius:8px;color:var(--text)}.mobile-topbar-button:hover{background:var(--bg-secondary)}.mobile-topbar-title{display:flex;align-items:center;gap:10px;font-weight:600;font-size:16px;letter-spacing:.2px}.mobile-drawer-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:var(--overlay-bg);z-index:30;display:flex}.mobile-drawer{width:min(280px,80vw);height:100%;max-width:100%;position:relative;border-right:1px solid var(--border)}.mobile-drawer-close{position:absolute;top:8px;right:8px;display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px;border-radius:8px;color:var(--text-muted)}.mobile-drawer-close:hover{background:var(--bg-secondary);color:var(--text)}@media (min-width: 769px) and (max-width: 1024px){.app-shell:not(.app-shell-mobile) .sidebar{width:56px;padding:16px 0}.app-shell:not(.app-shell-mobile) .sidebar-brand{padding:4px 0 20px;justify-content:center}.app-shell:not(.app-shell-mobile) .sidebar-brand-text{display:none}.app-shell:not(.app-shell-mobile) .sidebar-status{margin:0 8px 16px;padding:8px;justify-content:center}.app-shell:not(.app-shell-mobile) .sidebar-status-text{display:none}.app-shell:not(.app-shell-mobile) .sidebar-nav{padding:0 6px}.app-shell:not(.app-shell-mobile) .sidebar-nav-link{justify-content:center;padding:10px 8px}.app-shell:not(.app-shell-mobile) .sidebar-nav-label{display:none}.main-content{padding:24px 28px}}@media (max-width: 768px){.app-shell-mobile{flex-direction:column;height:100%;width:100%}.mobile-topbar{display:flex}.main-content-mobile{flex:1;padding:16px 12px;padding-bottom:calc(16px + var(--safe-bottom))}.page-header{flex-direction:column;align-items:flex-start;gap:8px}.page-title{font-size:20px}.card-grid{grid-template-columns:1fr}.card.span-2{grid-column:span 1}.detail-section{padding:16px}.kv-grid{grid-template-columns:1fr;gap:4px 0}.kv-grid dt{margin-top:8px}.data-table-wrap{overflow-x:auto}button,a.sidebar-nav-link,[role=button]{min-height:44px;min-width:44px}.sidebar-nav-link{padding:12px}}.mobile-card-list{display:flex;flex-direction:column;gap:10px}.mobile-card-groups{display:flex;flex-direction:column;gap:16px}.mobile-card-group{display:flex;flex-direction:column;gap:8px}.mobile-card-group-header{display:flex;align-items:center;gap:10px;padding:0 4px}.mobile-card-group-label{color:var(--text-muted);font-size:10px;text-transform:uppercase;letter-spacing:.6px;font-weight:600}.mobile-card-group-count{color:var(--text-muted);font-size:11px;margin-left:auto}.mobile-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;display:flex;flex-direction:column;gap:10px}.mobile-card-header{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}.mobile-card-title{color:var(--text);font-weight:500;font-size:14px;word-break:break-all;flex:1 1 auto;min-width:0}.mobile-card-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px;font-size:12px}.mobile-card-kv{display:grid;grid-template-columns:max-content 1fr;gap:4px 14px;font-size:13px}.mobile-card-kv-row{display:contents}.mobile-card-kv dt{color:var(--text-muted);font-size:12px}.mobile-card-kv dd{color:var(--text);text-align:right;word-break:break-word}.mobile-card-error{border-top:1px solid var(--border-light);padding-top:8px}.secret-matrix-scroll{position:relative}@media (max-width: 768px){.secret-matrix-wrap{overflow-x:auto;overflow-y:auto;max-height:70vh;-webkit-overflow-scrolling:touch}.secret-matrix-table thead th,.secret-matrix-table tbody td{background:var(--surface)}.secret-matrix-table thead th{background:var(--bg-secondary);position:sticky;top:0;z-index:1}.secret-matrix-table th.secret-matrix-caller-col,.secret-matrix-table td.secret-matrix-caller-col{position:sticky;left:0;z-index:2;box-shadow:1px 0 0 var(--border-light)}.secret-matrix-table thead th.secret-matrix-caller-col{z-index:3;background:var(--bg-secondary)}.secret-matrix-table tbody td.secret-matrix-cell{min-width:44px;height:44px;padding:6px 10px}.secret-matrix-scroll:after{content:"";position:absolute;top:0;right:0;bottom:0;width:24px;background:linear-gradient(to right,transparent,var(--bg) 80%);pointer-events:none;border-top-right-radius:var(--radius);border-bottom-right-radius:var(--radius)}}@media (hover: none){.data-table-row-clickable:hover{background:transparent}}
|