uxnan-bridge 0.0.1-alpha.20260621
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/README.md +150 -0
- package/dist/src/account-status.d.ts +13 -0
- package/dist/src/account-status.js +78 -0
- package/dist/src/account-status.js.map +1 -0
- package/dist/src/adapters/base-adapter.d.ts +18 -0
- package/dist/src/adapters/base-adapter.js +15 -0
- package/dist/src/adapters/base-adapter.js.map +1 -0
- package/dist/src/adapters/claude-adapter.d.ts +102 -0
- package/dist/src/adapters/claude-adapter.js +486 -0
- package/dist/src/adapters/claude-adapter.js.map +1 -0
- package/dist/src/adapters/claude-tools.d.ts +25 -0
- package/dist/src/adapters/claude-tools.js +146 -0
- package/dist/src/adapters/claude-tools.js.map +1 -0
- package/dist/src/adapters/codex-adapter.d.ts +116 -0
- package/dist/src/adapters/codex-adapter.js +912 -0
- package/dist/src/adapters/codex-adapter.js.map +1 -0
- package/dist/src/adapters/codex-app-server.d.ts +74 -0
- package/dist/src/adapters/codex-app-server.js +225 -0
- package/dist/src/adapters/codex-app-server.js.map +1 -0
- package/dist/src/adapters/codex-approval.d.ts +88 -0
- package/dist/src/adapters/codex-approval.js +160 -0
- package/dist/src/adapters/codex-approval.js.map +1 -0
- package/dist/src/adapters/codex-tools.d.ts +18 -0
- package/dist/src/adapters/codex-tools.js +106 -0
- package/dist/src/adapters/codex-tools.js.map +1 -0
- package/dist/src/adapters/content-blocks.d.ts +68 -0
- package/dist/src/adapters/content-blocks.js +205 -0
- package/dist/src/adapters/content-blocks.js.map +1 -0
- package/dist/src/adapters/echo-agent-adapter.d.ts +23 -0
- package/dist/src/adapters/echo-agent-adapter.js +72 -0
- package/dist/src/adapters/echo-agent-adapter.js.map +1 -0
- package/dist/src/adapters/gemini-adapter.d.ts +87 -0
- package/dist/src/adapters/gemini-adapter.js +594 -0
- package/dist/src/adapters/gemini-adapter.js.map +1 -0
- package/dist/src/adapters/gemini-tools.d.ts +4 -0
- package/dist/src/adapters/gemini-tools.js +48 -0
- package/dist/src/adapters/gemini-tools.js.map +1 -0
- package/dist/src/adapters/opencode-adapter.d.ts +74 -0
- package/dist/src/adapters/opencode-adapter.js +418 -0
- package/dist/src/adapters/opencode-adapter.js.map +1 -0
- package/dist/src/adapters/opencode-tools.d.ts +2 -0
- package/dist/src/adapters/opencode-tools.js +41 -0
- package/dist/src/adapters/opencode-tools.js.map +1 -0
- package/dist/src/adapters/pi-adapter.d.ts +92 -0
- package/dist/src/adapters/pi-adapter.js +467 -0
- package/dist/src/adapters/pi-adapter.js.map +1 -0
- package/dist/src/adapters/pi-tools.d.ts +10 -0
- package/dist/src/adapters/pi-tools.js +72 -0
- package/dist/src/adapters/pi-tools.js.map +1 -0
- package/dist/src/adapters/process-agent-adapter.d.ts +24 -0
- package/dist/src/adapters/process-agent-adapter.js +111 -0
- package/dist/src/adapters/process-agent-adapter.js.map +1 -0
- package/dist/src/adapters/resolve-claude.d.ts +13 -0
- package/dist/src/adapters/resolve-claude.js +57 -0
- package/dist/src/adapters/resolve-claude.js.map +1 -0
- package/dist/src/adapters/resolve-codex.d.ts +13 -0
- package/dist/src/adapters/resolve-codex.js +48 -0
- package/dist/src/adapters/resolve-codex.js.map +1 -0
- package/dist/src/adapters/resolve-gemini.d.ts +13 -0
- package/dist/src/adapters/resolve-gemini.js +47 -0
- package/dist/src/adapters/resolve-gemini.js.map +1 -0
- package/dist/src/adapters/resolve-opencode.d.ts +11 -0
- package/dist/src/adapters/resolve-opencode.js +49 -0
- package/dist/src/adapters/resolve-opencode.js.map +1 -0
- package/dist/src/adapters/resolve-pi.d.ts +13 -0
- package/dist/src/adapters/resolve-pi.js +46 -0
- package/dist/src/adapters/resolve-pi.js.map +1 -0
- package/dist/src/adapters/run-options.d.ts +22 -0
- package/dist/src/adapters/run-options.js +48 -0
- package/dist/src/adapters/run-options.js.map +1 -0
- package/dist/src/adapters/spawn.d.ts +20 -0
- package/dist/src/adapters/spawn.js +16 -0
- package/dist/src/adapters/spawn.js.map +1 -0
- package/dist/src/agents/agent-manager.d.ts +98 -0
- package/dist/src/agents/agent-manager.js +433 -0
- package/dist/src/agents/agent-manager.js.map +1 -0
- package/dist/src/agents/attachments.d.ts +28 -0
- package/dist/src/agents/attachments.js +121 -0
- package/dist/src/agents/attachments.js.map +1 -0
- package/dist/src/bridge-context.d.ts +45 -0
- package/dist/src/bridge-context.js +2 -0
- package/dist/src/bridge-context.js.map +1 -0
- package/dist/src/bridge-status.d.ts +12 -0
- package/dist/src/bridge-status.js +17 -0
- package/dist/src/bridge-status.js.map +1 -0
- package/dist/src/bridge.d.ts +37 -0
- package/dist/src/bridge.js +446 -0
- package/dist/src/bridge.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +194 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/conversation/session-history.d.ts +27 -0
- package/dist/src/conversation/session-history.js +1082 -0
- package/dist/src/conversation/session-history.js.map +1 -0
- package/dist/src/conversation/thread-store.d.ts +74 -0
- package/dist/src/conversation/thread-store.js +366 -0
- package/dist/src/conversation/thread-store.js.map +1 -0
- package/dist/src/daemon-config.d.ts +123 -0
- package/dist/src/daemon-config.js +64 -0
- package/dist/src/daemon-config.js.map +1 -0
- package/dist/src/daemon-state.d.ts +27 -0
- package/dist/src/daemon-state.js +76 -0
- package/dist/src/daemon-state.js.map +1 -0
- package/dist/src/git/git-runner.d.ts +24 -0
- package/dist/src/git/git-runner.js +63 -0
- package/dist/src/git/git-runner.js.map +1 -0
- package/dist/src/git/git-service.d.ts +76 -0
- package/dist/src/git/git-service.js +435 -0
- package/dist/src/git/git-service.js.map +1 -0
- package/dist/src/handler-router.d.ts +34 -0
- package/dist/src/handler-router.js +67 -0
- package/dist/src/handler-router.js.map +1 -0
- package/dist/src/handlers/account-handler.d.ts +4 -0
- package/dist/src/handlers/account-handler.js +27 -0
- package/dist/src/handlers/account-handler.js.map +1 -0
- package/dist/src/handlers/agent-handler.d.ts +2 -0
- package/dist/src/handlers/agent-handler.js +8 -0
- package/dist/src/handlers/agent-handler.js.map +1 -0
- package/dist/src/handlers/bridge-control-handler.d.ts +2 -0
- package/dist/src/handlers/bridge-control-handler.js +64 -0
- package/dist/src/handlers/bridge-control-handler.js.map +1 -0
- package/dist/src/handlers/desktop-handler.d.ts +12 -0
- package/dist/src/handlers/desktop-handler.js +5 -0
- package/dist/src/handlers/desktop-handler.js.map +1 -0
- package/dist/src/handlers/git-handler.d.ts +2 -0
- package/dist/src/handlers/git-handler.js +82 -0
- package/dist/src/handlers/git-handler.js.map +1 -0
- package/dist/src/handlers/index.d.ts +8 -0
- package/dist/src/handlers/index.js +22 -0
- package/dist/src/handlers/index.js.map +1 -0
- package/dist/src/handlers/not-implemented.d.ts +10 -0
- package/dist/src/handlers/not-implemented.js +21 -0
- package/dist/src/handlers/not-implemented.js.map +1 -0
- package/dist/src/handlers/notifications-handler.d.ts +2 -0
- package/dist/src/handlers/notifications-handler.js +62 -0
- package/dist/src/handlers/notifications-handler.js.map +1 -0
- package/dist/src/handlers/params.d.ts +11 -0
- package/dist/src/handlers/params.js +72 -0
- package/dist/src/handlers/params.js.map +1 -0
- package/dist/src/handlers/project-handler.d.ts +2 -0
- package/dist/src/handlers/project-handler.js +6 -0
- package/dist/src/handlers/project-handler.js.map +1 -0
- package/dist/src/handlers/thread-context-handler.d.ts +2 -0
- package/dist/src/handlers/thread-context-handler.js +211 -0
- package/dist/src/handlers/thread-context-handler.js.map +1 -0
- package/dist/src/handlers/workspace-handler.d.ts +2 -0
- package/dist/src/handlers/workspace-handler.js +101 -0
- package/dist/src/handlers/workspace-handler.js.map +1 -0
- package/dist/src/hooks/claude-approval-hook.d.ts +7 -0
- package/dist/src/hooks/claude-approval-hook.js +95 -0
- package/dist/src/hooks/claude-approval-hook.js.map +1 -0
- package/dist/src/hooks/gemini-approval-hook.d.ts +7 -0
- package/dist/src/hooks/gemini-approval-hook.js +113 -0
- package/dist/src/hooks/gemini-approval-hook.js.map +1 -0
- package/dist/src/index.d.ts +62 -0
- package/dist/src/index.js +65 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/keyring-secret-store.d.ts +36 -0
- package/dist/src/keyring-secret-store.js +70 -0
- package/dist/src/keyring-secret-store.js.map +1 -0
- package/dist/src/lock-file.d.ts +18 -0
- package/dist/src/lock-file.js +60 -0
- package/dist/src/lock-file.js.map +1 -0
- package/dist/src/logger.d.ts +28 -0
- package/dist/src/logger.js +99 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/pairing/pairing-code-service.d.ts +45 -0
- package/dist/src/pairing/pairing-code-service.js +183 -0
- package/dist/src/pairing/pairing-code-service.js.map +1 -0
- package/dist/src/projects/project-registry.d.ts +14 -0
- package/dist/src/projects/project-registry.js +60 -0
- package/dist/src/projects/project-registry.js.map +1 -0
- package/dist/src/push/push-sender.d.ts +21 -0
- package/dist/src/push/push-sender.js +96 -0
- package/dist/src/push/push-sender.js.map +1 -0
- package/dist/src/push/push-service.d.ts +122 -0
- package/dist/src/push/push-service.js +260 -0
- package/dist/src/push/push-service.js.map +1 -0
- package/dist/src/qr.d.ts +17 -0
- package/dist/src/qr.js +31 -0
- package/dist/src/qr.js.map +1 -0
- package/dist/src/secret-store.d.ts +23 -0
- package/dist/src/secret-store.js +27 -0
- package/dist/src/secret-store.js.map +1 -0
- package/dist/src/secure-device-state.d.ts +16 -0
- package/dist/src/secure-device-state.js +63 -0
- package/dist/src/secure-device-state.js.map +1 -0
- package/dist/src/service-installer.d.ts +57 -0
- package/dist/src/service-installer.js +254 -0
- package/dist/src/service-installer.js.map +1 -0
- package/dist/src/session-state.d.ts +14 -0
- package/dist/src/session-state.js +19 -0
- package/dist/src/session-state.js.map +1 -0
- package/dist/src/transport/crypto.d.ts +43 -0
- package/dist/src/transport/crypto.js +78 -0
- package/dist/src/transport/crypto.js.map +1 -0
- package/dist/src/transport/lan-server.d.ts +33 -0
- package/dist/src/transport/lan-server.js +105 -0
- package/dist/src/transport/lan-server.js.map +1 -0
- package/dist/src/transport/local-hosts.d.ts +17 -0
- package/dist/src/transport/local-hosts.js +30 -0
- package/dist/src/transport/local-hosts.js.map +1 -0
- package/dist/src/transport/mdns-advertiser.d.ts +83 -0
- package/dist/src/transport/mdns-advertiser.js +282 -0
- package/dist/src/transport/mdns-advertiser.js.map +1 -0
- package/dist/src/transport/message-io.d.ts +33 -0
- package/dist/src/transport/message-io.js +87 -0
- package/dist/src/transport/message-io.js.map +1 -0
- package/dist/src/transport/outbound-log.d.ts +24 -0
- package/dist/src/transport/outbound-log.js +78 -0
- package/dist/src/transport/outbound-log.js.map +1 -0
- package/dist/src/transport/relay-client.d.ts +19 -0
- package/dist/src/transport/relay-client.js +27 -0
- package/dist/src/transport/relay-client.js.map +1 -0
- package/dist/src/transport/secure-channel.d.ts +33 -0
- package/dist/src/transport/secure-channel.js +81 -0
- package/dist/src/transport/secure-channel.js.map +1 -0
- package/dist/src/transport/server-handshake.d.ts +49 -0
- package/dist/src/transport/server-handshake.js +137 -0
- package/dist/src/transport/server-handshake.js.map +1 -0
- package/dist/src/transport/session-handler.d.ts +19 -0
- package/dist/src/transport/session-handler.js +134 -0
- package/dist/src/transport/session-handler.js.map +1 -0
- package/dist/src/transport/session-registry.d.ts +58 -0
- package/dist/src/transport/session-registry.js +91 -0
- package/dist/src/transport/session-registry.js.map +1 -0
- package/dist/src/transport/trust-store.d.ts +23 -0
- package/dist/src/transport/trust-store.js +33 -0
- package/dist/src/transport/trust-store.js.map +1 -0
- package/dist/src/transport/ws-adapter.d.ts +7 -0
- package/dist/src/transport/ws-adapter.js +16 -0
- package/dist/src/transport/ws-adapter.js.map +1 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +13 -0
- package/dist/src/version.js.map +1 -0
- package/dist/src/workspace/browse-service.d.ts +10 -0
- package/dist/src/workspace/browse-service.js +97 -0
- package/dist/src/workspace/browse-service.js.map +1 -0
- package/dist/src/workspace/checkpoint-service.d.ts +21 -0
- package/dist/src/workspace/checkpoint-service.js +219 -0
- package/dist/src/workspace/checkpoint-service.js.map +1 -0
- package/dist/src/workspace/path-guard.d.ts +7 -0
- package/dist/src/workspace/path-guard.js +51 -0
- package/dist/src/workspace/path-guard.js.map +1 -0
- package/dist/src/workspace/workspace-service.d.ts +8 -0
- package/dist/src/workspace/workspace-service.js +111 -0
- package/dist/src/workspace/workspace-service.js.map +1 -0
- package/package.json +46 -0
- package/scripts/extract-gemini-hook.mjs +16 -0
- package/scripts/fake-approval-bridge.mjs +23 -0
- package/scripts/install-service-linux.sh +38 -0
- package/scripts/install-service-macos.sh +38 -0
- package/scripts/install-service-windows.ps1 +26 -0
- package/scripts/test-gemini-hook-e2e.mjs +168 -0
- package/scripts/write-gemini-settings.mjs +31 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uxnan-bridge — public API.
|
|
3
|
+
*
|
|
4
|
+
* `startBridge()` boots the daemon core (state, identity, JSON-RPC router).
|
|
5
|
+
* The live transport and agent runtimes are added in later increments.
|
|
6
|
+
*/
|
|
7
|
+
export { startBridge } from './bridge.js';
|
|
8
|
+
export { BRIDGE_VERSION } from './version.js';
|
|
9
|
+
export { HandlerRouter } from './handler-router.js';
|
|
10
|
+
export { DaemonState, DAEMON_FILES } from './daemon-state.js';
|
|
11
|
+
export { DEFAULT_DAEMON_CONFIG, resolveDaemonConfig, } from './daemon-config.js';
|
|
12
|
+
export { ProjectRegistry, projectIdFor } from './projects/project-registry.js';
|
|
13
|
+
export { PushService, } from './push/push-service.js';
|
|
14
|
+
export { createBridgePushSender, defaultServiceAccountPath, } from './push/push-sender.js';
|
|
15
|
+
export { SessionHistoryReader, } from './conversation/session-history.js';
|
|
16
|
+
export { PairingCodeService, } from './pairing/pairing-code-service.js';
|
|
17
|
+
export { MdnsAdvertiser, parseQuestions, buildMessage, encodeName, } from './transport/mdns-advertiser.js';
|
|
18
|
+
export { SecureDeviceState } from './secure-device-state.js';
|
|
19
|
+
export { InMemorySecretStore } from './secret-store.js';
|
|
20
|
+
export { KeyringSecretStore, createDefaultSecretStore, loadNativeKeyringBackend, } from './keyring-secret-store.js';
|
|
21
|
+
export { SessionState } from './session-state.js';
|
|
22
|
+
export { LockFile, isProcessAlive } from './lock-file.js';
|
|
23
|
+
export { buildServicePlan, buildWindowsStartupPlan, installService, uninstallService, currentServiceEnv, isServicePlatformSupported, } from './service-installer.js';
|
|
24
|
+
export { buildBridgeStatus } from './bridge-status.js';
|
|
25
|
+
export { getAuthStatus } from './account-status.js';
|
|
26
|
+
export { generatePairingPayload, renderPairingQr } from './qr.js';
|
|
27
|
+
export { createLogger, createFileLogger, redactSecrets, logFileFor, } from './logger.js';
|
|
28
|
+
export { BaseAgentAdapter } from './adapters/base-adapter.js';
|
|
29
|
+
export { CodexAdapter, codexUsageTokens, parseCodexConfigModels, parseCodexModelList, parseCodexModelWindows, parseCodexReasoning, } from './adapters/codex-adapter.js';
|
|
30
|
+
export { resolveCodexBinary } from './adapters/resolve-codex.js';
|
|
31
|
+
export { PiAdapter, parsePiLine, parsePiModelList, parsePiUsageTokens, parsePiContextWindow, } from './adapters/pi-adapter.js';
|
|
32
|
+
export { resolvePiBinary } from './adapters/resolve-pi.js';
|
|
33
|
+
export { GeminiAdapter, parseGeminiLine, } from './adapters/gemini-adapter.js';
|
|
34
|
+
export { resolveGeminiBinary } from './adapters/resolve-gemini.js';
|
|
35
|
+
export { OpenCodeAdapter, parseOpenCodeLine, parseModelList, parseOpenCodeModelWindows, openCodeUsageTokens, } from './adapters/opencode-adapter.js';
|
|
36
|
+
export { resolveOpenCodeBinary } from './adapters/resolve-opencode.js';
|
|
37
|
+
export { defaultSpawn } from './adapters/spawn.js';
|
|
38
|
+
export { ClaudeCodeAdapter, claudeContextWindow, claudeUsageTokens, parseClaudeLine, } from './adapters/claude-adapter.js';
|
|
39
|
+
export { resolveClaudeBinary } from './adapters/resolve-claude.js';
|
|
40
|
+
export { EchoAgentAdapter } from './adapters/echo-agent-adapter.js';
|
|
41
|
+
export { ProcessAgentAdapter, } from './adapters/process-agent-adapter.js';
|
|
42
|
+
// Conversation engine
|
|
43
|
+
export { ThreadStore, } from './conversation/thread-store.js';
|
|
44
|
+
export { AgentManager, } from './agents/agent-manager.js';
|
|
45
|
+
// Transport (live E2EE)
|
|
46
|
+
export { MessageQueue, queueFor, createInMemoryIoPair, } from './transport/message-io.js';
|
|
47
|
+
export { generateEphemeralKeyPair, deriveSessionKey, randomHex, verifyEd25519, aesGcmEncrypt, aesGcmDecrypt, } from './transport/crypto.js';
|
|
48
|
+
export { BridgeSecureChannel, ReplayError } from './transport/secure-channel.js';
|
|
49
|
+
export { performServerHandshake, HandshakeError, } from './transport/server-handshake.js';
|
|
50
|
+
export { handleSecureConnection, } from './transport/session-handler.js';
|
|
51
|
+
export { FileTrustStore } from './transport/trust-store.js';
|
|
52
|
+
export { connectRelayAsMac, } from './transport/relay-client.js';
|
|
53
|
+
export { startLanServer, } from './transport/lan-server.js';
|
|
54
|
+
export { wsToMessageIO, rawDataToBuffer } from './transport/ws-adapter.js';
|
|
55
|
+
export { localHostPorts } from './transport/local-hosts.js';
|
|
56
|
+
export { OutboundLog } from './transport/outbound-log.js';
|
|
57
|
+
export { SessionRegistry } from './transport/session-registry.js';
|
|
58
|
+
// Git + workspace services
|
|
59
|
+
export { GitService } from './git/git-service.js';
|
|
60
|
+
export { runGit, GitCommandError, sanitizePaths } from './git/git-runner.js';
|
|
61
|
+
export { WorkspaceService } from './workspace/workspace-service.js';
|
|
62
|
+
export { BrowseService, browseRootIdFor } from './workspace/browse-service.js';
|
|
63
|
+
export { CheckpointService } from './workspace/checkpoint-service.js';
|
|
64
|
+
export { resolveWithinRoot, isSensitiveName } from './workspace/path-guard.js';
|
|
65
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS-keychain-backed {@link SecretStore} (Windows Credential Manager, macOS
|
|
3
|
+
* Keychain, Linux Secret Service) via the optional `@napi-rs/keyring` native
|
|
4
|
+
* module. The bridge's Ed25519 identity is a secret and (per AGENTS.md) must
|
|
5
|
+
* never be persisted in plaintext — this is how it survives restarts.
|
|
6
|
+
*
|
|
7
|
+
* The native module is optional and loaded lazily; if it is unavailable (e.g. a
|
|
8
|
+
* headless Linux box with no Secret Service), the caller falls back to an
|
|
9
|
+
* in-memory store via {@link createDefaultSecretStore} — the bridge still runs,
|
|
10
|
+
* but the identity is ephemeral (real pairing then requires the keychain).
|
|
11
|
+
*/
|
|
12
|
+
import type { SecretStore } from './secret-store.js';
|
|
13
|
+
import type { Logger } from './logger.js';
|
|
14
|
+
/** Minimal synchronous keychain backend (one entry per service+account). */
|
|
15
|
+
export interface KeyringBackend {
|
|
16
|
+
getPassword(service: string, account: string): string | null;
|
|
17
|
+
setPassword(service: string, account: string, password: string): void;
|
|
18
|
+
deletePassword(service: string, account: string): boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare class KeyringSecretStore implements SecretStore {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(backend: KeyringBackend, service?: string);
|
|
23
|
+
get(key: string): Promise<string | null>;
|
|
24
|
+
set(key: string, value: string): Promise<void>;
|
|
25
|
+
delete(key: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Lazily load `@napi-rs/keyring` and adapt it to {@link KeyringBackend}.
|
|
29
|
+
* Returns `null` if the module (or its native binary) is unavailable.
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadNativeKeyringBackend(): Promise<KeyringBackend | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Build the default secret store: the OS keychain when available, otherwise a
|
|
34
|
+
* warned in-memory fallback.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createDefaultSecretStore(logger?: Logger): Promise<SecretStore>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { InMemorySecretStore } from './secret-store.js';
|
|
2
|
+
const SERVICE_NAME = 'uxnan-bridge';
|
|
3
|
+
export class KeyringSecretStore {
|
|
4
|
+
#backend;
|
|
5
|
+
#service;
|
|
6
|
+
constructor(backend, service = SERVICE_NAME) {
|
|
7
|
+
this.#backend = backend;
|
|
8
|
+
this.#service = service;
|
|
9
|
+
}
|
|
10
|
+
get(key) {
|
|
11
|
+
return Promise.resolve(this.#backend.getPassword(this.#service, key));
|
|
12
|
+
}
|
|
13
|
+
set(key, value) {
|
|
14
|
+
this.#backend.setPassword(this.#service, key, value);
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
}
|
|
17
|
+
delete(key) {
|
|
18
|
+
this.#backend.deletePassword(this.#service, key);
|
|
19
|
+
return Promise.resolve();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Lazily load `@napi-rs/keyring` and adapt it to {@link KeyringBackend}.
|
|
24
|
+
* Returns `null` if the module (or its native binary) is unavailable.
|
|
25
|
+
*/
|
|
26
|
+
export async function loadNativeKeyringBackend() {
|
|
27
|
+
try {
|
|
28
|
+
const mod = (await import('@napi-rs/keyring'));
|
|
29
|
+
const { Entry } = mod;
|
|
30
|
+
return {
|
|
31
|
+
getPassword: (service, account) => {
|
|
32
|
+
try {
|
|
33
|
+
return new Entry(service, account).getPassword();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null; // no entry / locked keychain
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
setPassword: (service, account, password) => {
|
|
40
|
+
new Entry(service, account).setPassword(password);
|
|
41
|
+
},
|
|
42
|
+
deletePassword: (service, account) => {
|
|
43
|
+
try {
|
|
44
|
+
return new Entry(service, account).deletePassword();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build the default secret store: the OS keychain when available, otherwise a
|
|
58
|
+
* warned in-memory fallback.
|
|
59
|
+
*/
|
|
60
|
+
export async function createDefaultSecretStore(logger) {
|
|
61
|
+
const backend = await loadNativeKeyringBackend();
|
|
62
|
+
if (backend) {
|
|
63
|
+
return new KeyringSecretStore(backend);
|
|
64
|
+
}
|
|
65
|
+
logger?.warn('OS keychain unavailable (@napi-rs/keyring not loaded); using an in-memory ' +
|
|
66
|
+
'identity store. The bridge identity will NOT survive restarts — install a ' +
|
|
67
|
+
'working keychain before real pairing. See bridge/FOR-DEV.md.');
|
|
68
|
+
return new InMemorySecretStore();
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=keyring-secret-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyring-secret-store.js","sourceRoot":"","sources":["../../src/keyring-secret-store.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGxD,MAAM,YAAY,GAAG,cAAc,CAAC;AASpC,MAAM,OAAO,kBAAkB;IACpB,QAAQ,CAAiB;IACzB,QAAQ,CAAS;IAE1B,YAAY,OAAuB,EAAE,UAAkB,YAAY;QACjE,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED,GAAG,CAAC,GAAW;QACb,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,KAAa;QAC5B,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QACrD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACjD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB;IAC5C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAS5C,CAAC;QACF,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC;QACtB,OAAO;YACL,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;gBAChC,IAAI,CAAC;oBACH,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;gBACnD,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,IAAI,CAAC,CAAC,6BAA6B;gBAC5C,CAAC;YACH,CAAC;YACD,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;gBAC1C,IAAI,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;YACpD,CAAC;YACD,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;gBACnC,IAAI,CAAC;oBACH,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,cAAc,EAAE,CAAC;gBACtD,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,MAAe;IAC5D,MAAM,OAAO,GAAG,MAAM,wBAAwB,EAAE,CAAC;IACjD,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,IAAI,kBAAkB,CAAC,OAAO,CAAC,CAAC;IACzC,CAAC;IACD,MAAM,EAAE,IAAI,CACV,4EAA4E;QAC1E,4EAA4E;QAC5E,8DAA8D,CACjE,CAAC;IACF,OAAO,IAAI,mBAAmB,EAAE,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface LockInfo {
|
|
2
|
+
pid: number;
|
|
3
|
+
startedAt: number;
|
|
4
|
+
}
|
|
5
|
+
/** Whether a process with the given pid is currently alive. */
|
|
6
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
7
|
+
export declare class LockFile {
|
|
8
|
+
#private;
|
|
9
|
+
constructor(path: string);
|
|
10
|
+
read(): Promise<LockInfo | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Try to acquire the lock. Returns `true` on success, or `false` if another
|
|
13
|
+
* live process already holds it. Stale locks are overwritten.
|
|
14
|
+
*/
|
|
15
|
+
acquire(pid?: number, now?: number): Promise<boolean>;
|
|
16
|
+
/** Release the lock if it is owned by `pid` (no-op otherwise). */
|
|
17
|
+
release(pid?: number): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-instance lock for the daemon (`~/.uxnan/bridge.lock`).
|
|
3
|
+
*
|
|
4
|
+
* Prevents a standalone bridge and an embedded one (or two standalone daemons)
|
|
5
|
+
* from running at once and fighting over the relay identity and LAN port
|
|
6
|
+
* (architecture/02e-bridge-integration.md §7.3). A lock owned by a process that
|
|
7
|
+
* is no longer alive is considered stale and may be taken over.
|
|
8
|
+
*/
|
|
9
|
+
import { readFile, unlink, writeFile } from 'node:fs/promises';
|
|
10
|
+
/** Whether a process with the given pid is currently alive. */
|
|
11
|
+
export function isProcessAlive(pid) {
|
|
12
|
+
try {
|
|
13
|
+
process.kill(pid, 0);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
// ESRCH → no such process; EPERM → exists but not signalable (alive).
|
|
18
|
+
return err.code === 'EPERM';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class LockFile {
|
|
22
|
+
#path;
|
|
23
|
+
constructor(path) {
|
|
24
|
+
this.#path = path;
|
|
25
|
+
}
|
|
26
|
+
async read() {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await readFile(this.#path, 'utf-8');
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null; // missing or corrupt → treat as no lock
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Try to acquire the lock. Returns `true` on success, or `false` if another
|
|
37
|
+
* live process already holds it. Stale locks are overwritten.
|
|
38
|
+
*/
|
|
39
|
+
async acquire(pid = process.pid, now = Date.now()) {
|
|
40
|
+
const existing = await this.read();
|
|
41
|
+
if (existing && existing.pid !== pid && isProcessAlive(existing.pid)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
await writeFile(this.#path, JSON.stringify({ pid, startedAt: now }), 'utf-8');
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
/** Release the lock if it is owned by `pid` (no-op otherwise). */
|
|
48
|
+
async release(pid = process.pid) {
|
|
49
|
+
const existing = await this.read();
|
|
50
|
+
if (existing && existing.pid !== pid)
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
await unlink(this.#path);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// already gone
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=lock-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock-file.js","sourceRoot":"","sources":["../../src/lock-file.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAO/D,+DAA+D;AAC/D,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,sEAAsE;QACtE,OAAQ,GAA6B,CAAC,IAAI,KAAK,OAAO,CAAC;IACzD,CAAC;AACH,CAAC;AAED,MAAM,OAAO,QAAQ;IACV,KAAK,CAAS;IAEvB,YAAY,IAAY;QACtB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YAChD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAa,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC,CAAC,wCAAwC;QACvD,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;QAC/D,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,QAAQ,IAAI,QAAQ,CAAC,GAAG,KAAK,GAAG,IAAI,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACrE,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;QAC9E,OAAO,IAAI,CAAC;IACd,CAAC;IAED,kEAAkE;IAClE,KAAK,CAAC,OAAO,CAAC,MAAc,OAAO,CAAC,GAAG;QACrC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,QAAQ,IAAI,QAAQ,CAAC,GAAG,KAAK,GAAG;YAAE,OAAO;QAC7C,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,eAAe;QACjB,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
2
|
+
export interface Logger {
|
|
3
|
+
debug(message: string, ...rest: unknown[]): void;
|
|
4
|
+
info(message: string, ...rest: unknown[]): void;
|
|
5
|
+
warn(message: string, ...rest: unknown[]): void;
|
|
6
|
+
error(message: string, ...rest: unknown[]): void;
|
|
7
|
+
}
|
|
8
|
+
/** Mask obvious secrets in a log line. Best-effort defense-in-depth. */
|
|
9
|
+
export declare function redactSecrets(text: string): string;
|
|
10
|
+
/** Console-only logger (stderr). */
|
|
11
|
+
export declare function createLogger(scope: string, minLevel?: LogLevel): Logger;
|
|
12
|
+
export interface FileLoggerOptions {
|
|
13
|
+
scope: string;
|
|
14
|
+
minLevel?: LogLevel;
|
|
15
|
+
/** Directory for daily log files (`bridge-YYYY-MM-DD.log`). */
|
|
16
|
+
logDir: string;
|
|
17
|
+
/** Also write to stderr (default true). */
|
|
18
|
+
toConsole?: boolean;
|
|
19
|
+
/** Injected clock for the timestamp + rotation filename (default `new Date()`). */
|
|
20
|
+
now?: () => Date;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Logger that writes to stderr and appends to a daily-rotated file. Logging never
|
|
24
|
+
* throws: file errors are swallowed so a logging failure can't crash the daemon.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createFileLogger(options: FileLoggerOptions): Logger;
|
|
27
|
+
/** `<dir>/bridge-YYYY-MM-DD.log` for the given date. */
|
|
28
|
+
export declare function logFileFor(dir: string, date: Date): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leveled logger for the bridge daemon, with optional file output (daily
|
|
3
|
+
* rotation under `~/.uxnan/logs/`) and a secret-redaction pass.
|
|
4
|
+
*
|
|
5
|
+
* Security (AGENTS.md): callers should not pass secrets to the logger, and
|
|
6
|
+
* {@link redactSecrets} is a defense-in-depth net that masks obvious secrets
|
|
7
|
+
* (JWTs, `key=…`/`token=…`/`secret=…` values, PEM key blocks) before they are
|
|
8
|
+
* written anywhere. All log output goes to stderr; stdout is reserved for IPC.
|
|
9
|
+
*
|
|
10
|
+
* Source: architecture/02a-system-architecture.md §5.8.3.
|
|
11
|
+
*/
|
|
12
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
const LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
15
|
+
/** Mask obvious secrets in a log line. Best-effort defense-in-depth. */
|
|
16
|
+
export function redactSecrets(text) {
|
|
17
|
+
return (text
|
|
18
|
+
// JWT-like a.b.c
|
|
19
|
+
.replace(/\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, '[REDACTED-JWT]')
|
|
20
|
+
// key=value / "token": "value" for secret-ish keys
|
|
21
|
+
.replace(/\b(token|secret|password|passwd|api[_-]?key|authorization|bearer|notificationSecret|privateKey|private_key)\b(["']?\s*[:=]\s*["']?)([^\s"',}]+)/gi, (_m, key, sep) => `${key}${sep}[REDACTED]`)
|
|
22
|
+
// PEM blocks
|
|
23
|
+
.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, '[REDACTED-KEY]'));
|
|
24
|
+
}
|
|
25
|
+
function stringify(value) {
|
|
26
|
+
if (typeof value === 'string')
|
|
27
|
+
return value;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.stringify(value);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return String(value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function formatLine(input) {
|
|
36
|
+
const prefix = input.timestamp ? `[${input.timestamp}] ` : '';
|
|
37
|
+
const extra = input.rest.length > 0 ? ` ${input.rest.map(stringify).join(' ')}` : '';
|
|
38
|
+
return redactSecrets(`${prefix}[${input.level.toUpperCase()}] (${input.scope}) ${input.message}${extra}`);
|
|
39
|
+
}
|
|
40
|
+
/** Console-only logger (stderr). */
|
|
41
|
+
export function createLogger(scope, minLevel = 'info') {
|
|
42
|
+
const threshold = LEVEL_ORDER[minLevel];
|
|
43
|
+
const emit = (level, message, rest) => {
|
|
44
|
+
if (LEVEL_ORDER[level] < threshold)
|
|
45
|
+
return;
|
|
46
|
+
console.error(formatLine({ level, scope, message, rest }));
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
debug: (m, ...r) => emit('debug', m, r),
|
|
50
|
+
info: (m, ...r) => emit('info', m, r),
|
|
51
|
+
warn: (m, ...r) => emit('warn', m, r),
|
|
52
|
+
error: (m, ...r) => emit('error', m, r),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Logger that writes to stderr and appends to a daily-rotated file. Logging never
|
|
57
|
+
* throws: file errors are swallowed so a logging failure can't crash the daemon.
|
|
58
|
+
*/
|
|
59
|
+
export function createFileLogger(options) {
|
|
60
|
+
const threshold = LEVEL_ORDER[options.minLevel ?? 'info'];
|
|
61
|
+
const now = options.now ?? (() => new Date());
|
|
62
|
+
const toConsole = options.toConsole !== false;
|
|
63
|
+
let ensured = false;
|
|
64
|
+
const emit = (level, message, rest) => {
|
|
65
|
+
if (LEVEL_ORDER[level] < threshold)
|
|
66
|
+
return;
|
|
67
|
+
const date = now();
|
|
68
|
+
const line = formatLine({
|
|
69
|
+
level,
|
|
70
|
+
scope: options.scope,
|
|
71
|
+
message,
|
|
72
|
+
rest,
|
|
73
|
+
timestamp: date.toISOString(),
|
|
74
|
+
});
|
|
75
|
+
if (toConsole)
|
|
76
|
+
console.error(line);
|
|
77
|
+
try {
|
|
78
|
+
if (!ensured) {
|
|
79
|
+
mkdirSync(options.logDir, { recursive: true });
|
|
80
|
+
ensured = true;
|
|
81
|
+
}
|
|
82
|
+
appendFileSync(logFileFor(options.logDir, date), `${line}\n`);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// logging must never throw
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
debug: (m, ...r) => emit('debug', m, r),
|
|
90
|
+
info: (m, ...r) => emit('info', m, r),
|
|
91
|
+
warn: (m, ...r) => emit('warn', m, r),
|
|
92
|
+
error: (m, ...r) => emit('error', m, r),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** `<dir>/bridge-YYYY-MM-DD.log` for the given date. */
|
|
96
|
+
export function logFileFor(dir, date) {
|
|
97
|
+
return join(dir, `bridge-${date.toISOString().slice(0, 10)}.log`);
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PairingPayload } from '@uxnan/shared';
|
|
2
|
+
export interface PairingCodeServiceOptions {
|
|
3
|
+
/** Builds the payload handed out on a valid code (the bridge's pairing data). */
|
|
4
|
+
buildPayload: () => PairingPayload;
|
|
5
|
+
/** Injected clock (epoch ms) for testability. */
|
|
6
|
+
now?: () => number;
|
|
7
|
+
/** Code lifetime before it rotates (default 10 min). */
|
|
8
|
+
ttlMs?: number;
|
|
9
|
+
/** Injected code generator (tests); defaults to an 8-char base32 code. */
|
|
10
|
+
generateCode?: () => string;
|
|
11
|
+
/** Per-IP resolve-attempt window + cap (anti-brute-force). */
|
|
12
|
+
rateWindowMs?: number;
|
|
13
|
+
rateMax?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Absolute path to persist the current code+expiry (e.g.
|
|
16
|
+
* `~/.uxnan/pairing-code.json`). When set, the code is shared ACROSS processes
|
|
17
|
+
* so the running daemon that serves `/pair/resolve` and a separate `qr`/`code`
|
|
18
|
+
* command (or an autostarted, console-less daemon) hand out the SAME code.
|
|
19
|
+
* Omit for an in-memory-only instance (tests).
|
|
20
|
+
*/
|
|
21
|
+
statePath?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class PairingCodeService {
|
|
24
|
+
#private;
|
|
25
|
+
constructor(options: PairingCodeServiceOptions);
|
|
26
|
+
/**
|
|
27
|
+
* The current pairing code, (re)issued if none exists or the previous one
|
|
28
|
+
* expired. Display this on the PC for the user to type on the phone. Returned
|
|
29
|
+
* grouped (`ABCD-EFGH`) for readability; {@link resolve} accepts either form.
|
|
30
|
+
*/
|
|
31
|
+
currentCode(): string;
|
|
32
|
+
/** Force a fresh code now (e.g. the user asked to regenerate). */
|
|
33
|
+
rotate(): string;
|
|
34
|
+
/**
|
|
35
|
+
* Validate a code presented by a phone and, if it matches the active
|
|
36
|
+
* (unexpired) code, return the pairing payload. Comparison is constant-time and
|
|
37
|
+
* input is normalized (case, grouping, Crockford look-alikes).
|
|
38
|
+
*/
|
|
39
|
+
resolve(code: string): PairingPayload | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Record a resolve attempt from `ip` and report whether it is now over the
|
|
42
|
+
* limit (the caller should answer 429 and NOT call {@link resolve}).
|
|
43
|
+
*/
|
|
44
|
+
rateLimited(ip: string): boolean;
|
|
45
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual-code pairing (bridge-side; architecture §5.10.1 reframed for the
|
|
3
|
+
* bridge-first model — the relay's `/trusted-session/resolve` was the off-LAN
|
|
4
|
+
* equivalent).
|
|
5
|
+
*
|
|
6
|
+
* The bridge shows a short, rotating **pairing code** on the PC (CLI / desktop).
|
|
7
|
+
* A phone that has located the bridge on the LAN (via mDNS discovery — a later
|
|
8
|
+
* slice — or by typing the host) calls `GET /pair/resolve?code=<code>` on the LAN
|
|
9
|
+
* server; this service validates the code and hands back the full
|
|
10
|
+
* {@link PairingPayload} (the same data the QR carries), which the phone then runs
|
|
11
|
+
* through the normal E2EE handshake.
|
|
12
|
+
*
|
|
13
|
+
* The code is the **consent gate**: only someone who can read the PC screen learns
|
|
14
|
+
* it, so a random LAN device cannot pull the payload and pair. This is the same
|
|
15
|
+
* trust posture as the QR (whoever sees the screen can pair) — the code adds no
|
|
16
|
+
* new secret beyond what the QR already exposes. Brute force is bounded by the
|
|
17
|
+
* code entropy (40 bits), a short TTL, and per-IP rate limiting.
|
|
18
|
+
*
|
|
19
|
+
* Security note: the payload is not a secret that grants access on its own — the
|
|
20
|
+
* phone must still complete the identity-keyed E2EE handshake. See bridge/FOR-DEV.md.
|
|
21
|
+
*/
|
|
22
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
23
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { dirname } from 'node:path';
|
|
25
|
+
/** Crockford base32 alphabet (no I, L, O, U — unambiguous when read aloud/typed). */
|
|
26
|
+
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
27
|
+
const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
28
|
+
const DEFAULT_RATE_WINDOW_MS = 60 * 1000; // 1 minute
|
|
29
|
+
const DEFAULT_RATE_MAX = 10; // attempts per window per IP
|
|
30
|
+
export class PairingCodeService {
|
|
31
|
+
#buildPayload;
|
|
32
|
+
#now;
|
|
33
|
+
#ttlMs;
|
|
34
|
+
#generateCode;
|
|
35
|
+
#rateWindowMs;
|
|
36
|
+
#rateMax;
|
|
37
|
+
#rate = new Map();
|
|
38
|
+
#statePath;
|
|
39
|
+
#code;
|
|
40
|
+
#expiresAt = 0;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.#buildPayload = options.buildPayload;
|
|
43
|
+
this.#now = options.now ?? (() => Date.now());
|
|
44
|
+
this.#ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
45
|
+
this.#generateCode = options.generateCode ?? defaultGenerateCode;
|
|
46
|
+
this.#rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
|
|
47
|
+
this.#rateMax = options.rateMax ?? DEFAULT_RATE_MAX;
|
|
48
|
+
this.#statePath = options.statePath;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The current pairing code, (re)issued if none exists or the previous one
|
|
52
|
+
* expired. Display this on the PC for the user to type on the phone. Returned
|
|
53
|
+
* grouped (`ABCD-EFGH`) for readability; {@link resolve} accepts either form.
|
|
54
|
+
*/
|
|
55
|
+
currentCode() {
|
|
56
|
+
const now = this.#now();
|
|
57
|
+
this.#syncFromDisk(now);
|
|
58
|
+
if (!this.#code || now >= this.#expiresAt) {
|
|
59
|
+
this.#code = this.#generateCode();
|
|
60
|
+
this.#expiresAt = now + this.#ttlMs;
|
|
61
|
+
this.#persist();
|
|
62
|
+
}
|
|
63
|
+
return group(this.#code);
|
|
64
|
+
}
|
|
65
|
+
/** Force a fresh code now (e.g. the user asked to regenerate). */
|
|
66
|
+
rotate() {
|
|
67
|
+
this.#code = this.#generateCode();
|
|
68
|
+
this.#expiresAt = this.#now() + this.#ttlMs;
|
|
69
|
+
this.#persist();
|
|
70
|
+
return group(this.#code);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Validate a code presented by a phone and, if it matches the active
|
|
74
|
+
* (unexpired) code, return the pairing payload. Comparison is constant-time and
|
|
75
|
+
* input is normalized (case, grouping, Crockford look-alikes).
|
|
76
|
+
*/
|
|
77
|
+
resolve(code) {
|
|
78
|
+
const now = this.#now();
|
|
79
|
+
// Re-read the shared code so a daemon serving `/pair/resolve` validates
|
|
80
|
+
// against the code another process (the `qr`/`code` command) may have issued.
|
|
81
|
+
this.#syncFromDisk(now);
|
|
82
|
+
if (!this.#code || now >= this.#expiresAt)
|
|
83
|
+
return undefined;
|
|
84
|
+
if (!constantTimeEqual(normalize(code), this.#code))
|
|
85
|
+
return undefined;
|
|
86
|
+
return this.#buildPayload();
|
|
87
|
+
}
|
|
88
|
+
/** Adopt the persisted (shared) code when present and still valid. */
|
|
89
|
+
#syncFromDisk(now) {
|
|
90
|
+
const persisted = this.#load();
|
|
91
|
+
if (persisted && now < persisted.expiresAt) {
|
|
92
|
+
this.#code = persisted.code;
|
|
93
|
+
this.#expiresAt = persisted.expiresAt;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Read the shared code from disk, or `undefined` (no path / missing / corrupt). */
|
|
97
|
+
#load() {
|
|
98
|
+
if (!this.#statePath)
|
|
99
|
+
return undefined;
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(readFileSync(this.#statePath, 'utf-8'));
|
|
102
|
+
if (parsed &&
|
|
103
|
+
typeof parsed === 'object' &&
|
|
104
|
+
typeof parsed.code === 'string' &&
|
|
105
|
+
typeof parsed.expiresAt === 'number') {
|
|
106
|
+
return {
|
|
107
|
+
code: parsed.code,
|
|
108
|
+
expiresAt: parsed.expiresAt,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* missing or corrupt → in-memory only */
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
/** Persist the current code so other processes hand out the same one. Best-effort. */
|
|
118
|
+
#persist() {
|
|
119
|
+
if (!this.#statePath || !this.#code)
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
mkdirSync(dirname(this.#statePath), { recursive: true });
|
|
123
|
+
writeFileSync(this.#statePath, JSON.stringify({ code: this.#code, expiresAt: this.#expiresAt }));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* best-effort: persistence failure falls back to in-memory */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Record a resolve attempt from `ip` and report whether it is now over the
|
|
131
|
+
* limit (the caller should answer 429 and NOT call {@link resolve}).
|
|
132
|
+
*/
|
|
133
|
+
rateLimited(ip) {
|
|
134
|
+
const now = this.#now();
|
|
135
|
+
const entry = this.#rate.get(ip);
|
|
136
|
+
if (!entry || now >= entry.resetAt) {
|
|
137
|
+
this.#rate.set(ip, { count: 1, resetAt: now + this.#rateWindowMs });
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
entry.count += 1;
|
|
141
|
+
return entry.count > this.#rateMax;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Generate an 8-char Crockford-base32 code from 40 bits of entropy. */
|
|
145
|
+
function defaultGenerateCode() {
|
|
146
|
+
const bytes = randomBytes(5); // 40 bits → 8 base32 chars
|
|
147
|
+
let bits = 0;
|
|
148
|
+
let value = 0;
|
|
149
|
+
let out = '';
|
|
150
|
+
for (const byte of bytes) {
|
|
151
|
+
value = (value << 8) | byte;
|
|
152
|
+
bits += 8;
|
|
153
|
+
while (bits >= 5) {
|
|
154
|
+
bits -= 5;
|
|
155
|
+
out += ALPHABET[(value >>> bits) & 31];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out.slice(0, 8);
|
|
159
|
+
}
|
|
160
|
+
/** Group an 8-char code as `ABCD-EFGH` for display. */
|
|
161
|
+
function group(code) {
|
|
162
|
+
return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Normalize a user-typed code: uppercase, strip non-alphanumerics (dashes/spaces),
|
|
166
|
+
* and fold Crockford look-alikes (O→0, I/L→1) so a misread is still accepted.
|
|
167
|
+
*/
|
|
168
|
+
function normalize(input) {
|
|
169
|
+
return input
|
|
170
|
+
.toUpperCase()
|
|
171
|
+
.replace(/[^0-9A-Z]/g, '')
|
|
172
|
+
.replace(/O/g, '0')
|
|
173
|
+
.replace(/[IL]/g, '1');
|
|
174
|
+
}
|
|
175
|
+
/** Constant-time string compare that never throws on length mismatch. */
|
|
176
|
+
function constantTimeEqual(a, b) {
|
|
177
|
+
const ba = Buffer.from(a, 'utf-8');
|
|
178
|
+
const bb = Buffer.from(b, 'utf-8');
|
|
179
|
+
if (ba.length !== bb.length)
|
|
180
|
+
return false;
|
|
181
|
+
return timingSafeEqual(ba, bb);
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=pairing-code-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing-code-service.js","sourceRoot":"","sources":["../../../src/pairing/pairing-code-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,qFAAqF;AACrF,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AAEpD,MAAM,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,aAAa;AACpD,MAAM,sBAAsB,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,WAAW;AACrD,MAAM,gBAAgB,GAAG,EAAE,CAAC,CAAC,6BAA6B;AAmC1D,MAAM,OAAO,kBAAkB;IACpB,aAAa,CAAuB;IACpC,IAAI,CAAe;IACnB,MAAM,CAAS;IACf,aAAa,CAAe;IAC5B,aAAa,CAAS;IACtB,QAAQ,CAAS;IACjB,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;IACrC,UAAU,CAAqB;IAExC,KAAK,CAAqB;IAC1B,UAAU,GAAG,CAAC,CAAC;IAEf,YAAY,OAAkC;QAC5C,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;QAC1C,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK,IAAI,cAAc,CAAC;QAC9C,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,YAAY,IAAI,mBAAmB,CAAC;QACjE,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,YAAY,IAAI,sBAAsB,CAAC;QACpE,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC;QACpD,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;IACtC,CAAC;IAED;;;;OAIG;IACH,WAAW;QACT,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,GAAG,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YAC1C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;YAClC,IAAI,CAAC,UAAU,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC;YACpC,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IAED,kEAAkE;IAClE,MAAM;QACJ,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QAClC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QAC5C,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,IAAY;QAClB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,wEAAwE;QACxE,8EAA8E;QAC9E,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,GAAG,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO,SAAS,CAAC;QAC5D,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QACtE,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC;IAC9B,CAAC;IAED,sEAAsE;IACtE,aAAa,CAAC,GAAW;QACvB,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,SAAS,IAAI,GAAG,GAAG,SAAS,CAAC,SAAS,EAAE,CAAC;YAC3C,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC;YAC5B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC,SAAS,CAAC;QACxC,CAAC;IACH,CAAC;IAED,oFAAoF;IACpF,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO,SAAS,CAAC;QACvC,IAAI,CAAC;YACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;YAC3E,IACE,MAAM;gBACN,OAAO,MAAM,KAAK,QAAQ;gBAC1B,OAAQ,MAAwB,CAAC,IAAI,KAAK,QAAQ;gBAClD,OAAQ,MAAwB,CAAC,SAAS,KAAK,QAAQ,EACvD,CAAC;gBACD,OAAO;oBACL,IAAI,EAAG,MAAwB,CAAC,IAAI;oBACpC,SAAS,EAAG,MAAwB,CAAC,SAAS;iBAC/C,CAAC;YACJ,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,yCAAyC;QAC3C,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,sFAAsF;IACtF,QAAQ;QACN,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO;QAC5C,IAAI,CAAC;YACH,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD,aAAa,CACX,IAAI,CAAC,UAAU,EACf,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CACjE,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,8DAA8D;QAChE,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,WAAW,CAAC,EAAU;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACjC,IAAI,CAAC,KAAK,IAAI,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;YACpE,OAAO,KAAK,CAAC;QACf,CAAC;QACD,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;QACjB,OAAO,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC;IACrC,CAAC;CACF;AAED,wEAAwE;AACxE,SAAS,mBAAmB;IAC1B,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,2BAA2B;IACzD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;QAC5B,IAAI,IAAI,CAAC,CAAC;QACV,OAAO,IAAI,IAAI,CAAC,EAAE,CAAC;YACjB,IAAI,IAAI,CAAC,CAAC;YACV,GAAG,IAAI,QAAQ,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACzB,CAAC;AAED,uDAAuD;AACvD,SAAS,KAAK,CAAC,IAAY;IACzB,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC3E,CAAC;AAED;;;GAGG;AACH,SAAS,SAAS,CAAC,KAAa;IAC9B,OAAO,KAAK;SACT,WAAW,EAAE;SACb,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC;SACzB,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC;SAClB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED,yEAAyE;AACzE,SAAS,iBAAiB,CAAC,CAAS,EAAE,CAAS;IAC7C,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACnC,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACnC,IAAI,EAAE,CAAC,MAAM,KAAK,EAAE,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1C,OAAO,eAAe,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;AACjC,CAAC"}
|